Skip to content

Conversation

MCarlomagno
Copy link
Member

Summary

This PR introduces a new proc macro for endpoints require_permissions that has a list of string literals as parameters indicating the permissions that the API key must contain for calling the endpoint.

Example

#[require_permissions(["relayers:get:all"])]
#[get("/relayers")]
async fn list_relayers(...) -> impl Responder {
    relayer::list_relayers(query.into_inner(), data).await
}

The permissions created are registered under constants/permissions.rs, API keys will be created using some subset of those permissions, then when creating a new permission, the developer must register it under permissions.rs and in the endpoint where applies.

Scopes

Permissions are scoped by resource (generally either relayer_id or plugin_id) like: resource:action:scope

Examples:
Permission to execute transactions in sepolia-example relayer: transactions:execute:sepolia-example
Permission to list all relayers: relayers:get:all
Permission to sign using sepolia-example relayer: signing:execute:sepolia-example
Permission to run my-plugin: plugins:execute:my-plugin

Testing Process

  1. Setup the server with more than one relayer: e.g. sepolia-example & solana-example
  2. Create a new API key with restricted permissions using
curl -X POST "http://localhost:8080/api/v1/api-keys" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ADMIN_API_KEY" \
    -d '{
      "name": "Get sepolia relayer",
      "permissions": ["relayers:get:sepolia-example"]
    }'
  1. In response, you should see the API key value, you can use this api key to load sepolia-example relayer:
curl -X GET http://localhost:8080/api/v1/relayers/sepolia-example \
      -H "Authorization: Bearer SEPOLIA_RELAYER_API_KEY_VALUE"
The call should succeed.
  1. Now using the same API key, try to load solana-example relayer:
curl -X GET http://localhost:8080/api/v1/relayers/solana-example \
      -H "Authorization: Bearer SEPOLIA_RELAYER_API_KEY_VALUE"
The call should succeed.

the call should fail with an unauthorized error.

Checklist

  • Add a reference to related issues in the PR description.
  • Add unit tests if applicable.

Copy link

coderabbitai bot commented Sep 26, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch enforce-api-keys

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

use relayer_macros::require_permissions;

/// List plugins
#[require_permissions(["plugins:get:all"])]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

example of regular permission (no parameters)

}

/// Calls a plugin method.
#[require_permissions(["plugins:execute:{plugin_id}"])]
Copy link
Member Author

@MCarlomagno MCarlomagno Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example of scoped permission, (plugin_id parameter), an API key that passes this authorization must contain a permission of the form:
plugins:execute:abc-abc-abc-abc AND the plugin_id passed as function parameter must be: abc-abc-abc-abc

Copy link
Collaborator

@zeljkoX zeljkoX left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your work!

In general i like the idea of macro at route level.

In the future for some advanced cases we would need standard methods to invoke to check permissions logic but for most cases macros would do the job.

I think we should just see how permissions should be defined.

I have added my two cents in the comments. In general i think permissions should be simple and we can define scope outside of permission string and use in the logic to check. This would eliminate need to define multiple permissions, * and for specific id and would keep permission system simpler.

fn test_macro_detects_template_parameters() {
// This is a compile-time test - if the macro compiles, it works
let tokens = quote::quote! {
#[require_permissions(["relayers:get:{relayer_id}"])]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can it work with multiple permissions

For example required permissions are to get specific relayer or to be able to get any relayer?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works with multiple ones, but in this specific case relayers:get:{relayer_id} should match both a specific or all relayers (using wildcards).

  • If API key contains relayers:get:abc-abc-abc-abc permission, then it passes.
  • If API key contains relayers:get:* permission, then also passes.

I think there might be endpoints where we have more than one as you said, e.g. if you want to enforce both signing:execute:{relayer_id} + transactions:execute:{relayer_id} in the same endpoint:

#[require_permissions(["signing:execute:{relayer_id}", "transactions:execute:{relayer_id}"])]

should work

}

/// Get API key permissions
#[require_permissions(["api_keys:get:{api_key_id}"])]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one api key can have api_keys:get:some_id

some key can have access to all. is that api_keys:get:all?

if we want OR condition are we defining both permissions like:
#[require_permissions(["api_keys:get:{api_key_id}", "api_keys:get:all"])]

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in case you want to have some user accessing all the api keys you will use a wildcard.

similar to what I mentioned here

The :all suffix in permissions corresponds to the permission to list, rather to access all (this is in cases where listing does not include all the information of each entry). But I see it creates some confusion, maybe replacing ..:get:all by ..:list:all in the action makes more sense

}

/// Calls a plugin method.
#[require_permissions(["plugins:execute:{plugin_id}"])]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In terms of plugins, only execute plugin permissions are needed?
We do not check resources used in plugin logic?

For example plugin sends transaction via some relayer. Does relayer permission needs to be there or just plugin perm is enough?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this was the most complicated part, the challenge there is that we don't really control the plugin interactions at "routes" level, Maybe doing some extra validation inside the relayer-api in /plugins works

fn test_macro_detects_template_parameters() {
// This is a compile-time test - if the macro compiles, it works
let tokens = quote::quote! {
#[require_permissions(["relayers:get:{relayer_id}"])]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we keep permissions simple like

notifications:list
notifications:read
notifications:create
notifications:update
notifications:delete

and then define scope for permissions which can be either global or list of ids.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way we are keeping permissions simple, for example relayers:read, and later in the logic we check scope of that permission which can be either global or list of items.

That way we do not need to tie permissions with specific entities.

for example api route can just be docorated with simple permission relayers:read. If api key has that permission our logic will check for permission scope which is defined at api key create phase.

Macro fingerprint would needed to be slightly modified, for example

#[require_permissions(["relayers:read"], "relayer_id")]

permission struct example:

{
"action": "relayers:read",
"resource": { "ids": ["*"] },
}

WDYT?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree! that makes it simpler :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants