{"id":21714927,"url":"https://github.com/jungsoft/rajska","last_synced_at":"2025-08-20T12:31:24.147Z","repository":{"id":38237779,"uuid":"194130540","full_name":"jungsoft/rajska","owner":"jungsoft","description":"Rajska is an elixir authorization library for Absinthe.","archived":false,"fork":false,"pushed_at":"2023-03-24T09:25:44.000Z","size":276,"stargazers_count":46,"open_issues_count":6,"forks_count":13,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-12-09T22:05:33.796Z","etag":null,"topics":["absinthe","authorization","elixir","middleware"],"latest_commit_sha":null,"homepage":"https://hexdocs.pm/rajska/","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jungsoft.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-06-27T16:38:29.000Z","updated_at":"2023-12-14T07:17:13.000Z","dependencies_parsed_at":"2023-02-19T00:01:27.235Z","dependency_job_id":null,"html_url":"https://github.com/jungsoft/rajska","commit_stats":null,"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jungsoft%2Frajska","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jungsoft%2Frajska/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jungsoft%2Frajska/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jungsoft%2Frajska/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jungsoft","download_url":"https://codeload.github.com/jungsoft/rajska/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230423563,"owners_count":18223435,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["absinthe","authorization","elixir","middleware"],"created_at":"2024-11-26T00:39:30.060Z","updated_at":"2024-12-19T11:12:02.605Z","avatar_url":"https://github.com/jungsoft.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Rajska\n\n[![Coverage Status](https://coveralls.io/repos/github/jungsoft/rajska/badge.svg?branch=master)](https://coveralls.io/github/jungsoft/rajska?branch=master)\n\nRajska is an elixir authorization library for [Absinthe](https://github.com/absinthe-graphql/absinthe).\n\nIt provides the following middlewares:\n\n- [Query Authorization](#query-authorization)\n- [Query Scope Authorization](#query-scope-authorization)\n- [Object Authorization](#object-authorization)\n- [Object Scope Authorization](#object-scope-authorization)\n- [Field Authorization](#field-authorization)\n- [Rate Limiter](#rate-limiter)\n\nDocumentation can be found at [https://hexdocs.pm/rajska/](https://hexdocs.pm/rajska).\n\n## Installation\n\nThe package can be installed by adding `rajska` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:rajska, \"~\u003e 1.3.2\"},\n  ]\nend\n```\n\n## Usage\n\nCreate your Authorization module, which will implement the [Rajska Authorization](https://hexdocs.pm/rajska/Rajska.Authorization.html) behaviour and contain the logic to validate user permissions and will be called by Rajska middlewares. Rajska provides some helper functions by default, such as [role_authorized?/2](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:role_authorized?/2) and [has_user_access?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/3), but you can override them with your application needs.\n\n```elixir\ndefmodule Authorization do\n  use Rajska,\n    valid_roles: [:user, :admin],\n    super_role: :admin,\n    default_rule: :default\nend\n```\n\nAdd your [Authorization](https://hexdocs.pm/rajska/Rajska.Authorization.html) module to your `Absinthe.Schema` [context/1](https://hexdocs.pm/absinthe/Absinthe.Schema.html#c:context/1) callback and the desired middlewares to the [middleware/3](https://hexdocs.pm/absinthe/Absinthe.Middleware.html#module-the-middleware-3-callback) callback:\n\n```elixir\ndef context(ctx), do: Map.put(ctx, :authorization, Authorization)\n\ndef middleware(middleware, field, %Absinthe.Type.Object{identifier: identifier})\nwhen identifier in [:query, :mutation] do\n  middleware\n  |\u003e Rajska.add_query_authorization(field, Authorization)\n  |\u003e Rajska.add_object_authorization()\nend\n\ndef middleware(middleware, field, object) do\n  Rajska.add_field_authorization(middleware, field, object)\nend\n```\n\nThe only exception is [Object Scope Authorization](#object-scope-authorization), which isn't a middleware, but an [Absinthe Phase](https://hexdocs.pm/absinthe/Absinthe.Phase.html). To use it, add it to your pipeline after the resolution:\n\n```elixir\n# router.ex\nalias Absinthe.Phase.Document.Execution.Resolution\nalias Absinthe.Pipeline\nalias Rajska.ObjectScopeAuthorization\n\nforward \"/graphql\", Absinthe.Plug,\n  schema: MyProjectWeb.Schema,\n  socket: MyProjectWeb.UserSocket,\n  pipeline: {__MODULE__, :pipeline} # Add this line\n\ndef pipeline(config, pipeline_opts) do\n  config\n  |\u003e Map.fetch!(:schema_mod)\n  |\u003e Pipeline.for_document(pipeline_opts)\n  |\u003e Pipeline.insert_after(Resolution, ObjectScopeAuthorization)\nend\n```\n\nSince Query Scope Authorization middleware must be used with Query Authorization, it is automatically called when adding the former.\n\nMiddlewares usage can be found below.\n\n## Middlewares\n\n### Query Authorization\n\nEnsures Absinthe's queries can only be accessed by determined users.\n\n#### Usage:\n\n[Create your Authorization module and add it and QueryAuthorization to your Absinthe.Schema](#usage). Then set the permitted role to access a query or mutation:\n\n```elixir\nmutation do\n  field :create_user, :user do\n    arg :params, non_null(:user_params)\n\n    middleware Rajska.QueryAuthorization, permit: :all\n    resolve \u0026AccountsResolver.create_user/2\n  end\n\n  field :update_user, :user do\n    arg :id, non_null(:integer)\n    arg :params, non_null(:user_params)\n\n    middleware Rajska.QueryAuthorization, [permit: [:user, :manager], scope: false]\n    resolve \u0026AccountsResolver.update_user/2\n  end\n\n  field :invite_user, :user do\n    arg :email, non_null(:string)\n\n    middleware Rajska.QueryAuthorization, permit: :admin\n    resolve \u0026AccountsResolver.invite_user/2\n  end\nend\n```\n\nQuery authorization will call [role_authorized?/2](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:role_authorized?/2) to check if the [user](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:get_current_user/1) [role](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:get_user_role/1) is authorized to perform the query.\n\n### Query Scope Authorization\n\nProvides scoping to Absinthe's queries, allowing for more complex authorization rules. It is used together with [Query Authorization](#query-authorization).\n\n```elixir\nmutation do\n  field :create_user, :user do\n    arg :params, non_null(:user_params)\n\n    # all does not require scoping, since it means anyone can execute this query, even without being logged in.\n    middleware Rajska.QueryAuthorization, permit: :all\n    resolve \u0026AccountsResolver.create_user/2\n  end\n\n  field :update_user, :user do\n    arg :id, non_null(:integer)\n    arg :params, non_null(:user_params)\n\n    middleware Rajska.QueryAuthorization, [permit: :user, scope: User] # same as [permit: :user, scope: User, args: :id]\n    resolve \u0026AccountsResolver.update_user/2\n  end\n\n  field :delete_user, :user do\n    arg :user_id, non_null(:integer)\n\n    # Providing a map for args is useful to map query argument to struct field.\n    middleware Rajska.QueryAuthorization, [permit: [:user, :manager], scope: User, args: %{id: :user_id}]\n    resolve \u0026AccountsResolver.delete_user/2\n  end\n\n  input_object :user_params do\n    field :id, non_null(:integer)\n  end\n\n  field :accept_user, :user do\n    arg :params, non_null(:user_params)\n\n    middleware Rajska.QueryAuthorization, [\n      permit: :user,\n      scope: User,\n      args: %{id: [:params, :id]},\n      rule: :accept_user\n    ]\n    resolve \u0026AccountsResolver.invite_user/2\n  end\nend\n```\n\nIn the above example, `:all` and `:admin` (`super_role`) permissions don't require the `:scope` keyword, but you can modify this behavior by overriding the [not_scoped_roles/0](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:not_scoped_roles/0) function.\n\nThere are also extra options for this middleware, supporting the definition of custom rules, access of nested parameters and allowing optional parameters. All possibilities are listed below:\n\n#### Options\n\nAll the following options are sent to [has_user_access?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/3):\n\n* `:scope`\n  - `false`: disables scoping\n  - `User`: a module that will be passed to `c:Rajska.Authorization.has_user_access?/3`. It must define a struct.\n* `:args`\n  - `%{user_id: [:params, :id]}`: where `user_id` is the scoped field and `id` is an argument nested inside the `params` argument.\n  - `:id`: this is the same as `%{id: :id}`, where `:id` is both the query argument and the scoped field that will be passed to [has_user_access?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/3)\n  - `[:code, :user_group_id]`: this is the same as `%{code: :code, user_group_id: :user_group_id}`, where `code` and `user_group_id` are both query arguments and scoped fields.\n* `:optional` (optional) - when set to true the arguments are optional, so if no argument is provided, the query will be authorized. Defaults to false.\n* `:rule` (optional) - allows the same struct to have different rules. See `Rajska.Authorization` for `rule` default settings.\n\n### Object Authorization\n\nAuthorizes all Absinthe's [objects](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#object/3) requested in a query by checking the permission defined in each object meta `authorize`.\n\n#### Usage:\n\n[Create your Authorization module and add it and ObjectAuthorization to your Absinthe.Schema](#usage). Then set the permitted role to access an object:\n\n```elixir\nobject :wallet_balance do\n  meta :authorize, :admin\n\n  field :total, :integer\nend\n\nobject :company do\n  meta :authorize, :user\n\n  field :name, :string\n  field :wallet_balance, :wallet_balance\nend\n\nobject :user do\n  meta :authorize, :all\n\n  field :email, :string\n  field :company, :company\nend\n```\n\nWith the permissions above, a query like the following would only be allowed by an admin user:\n\n```graphql\n{\n  userQuery {\n    name\n    email\n    company {\n      name\n      walletBalance { total }\n    }\n  }\n}\n```\n\nObject Authorization middleware runs after Query Authorization middleware (if added) and before the query is resolved by recursively checking the requested objects permissions in the [role_authorized?/2](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:role_authorized?/2) function (which is also used by Query Authorization). It can be overridden by your own implementation.\n\n### Object Scope Authorization\n\nAbsinthe Phase to perform object scoping.\n\nAuthorizes all Absinthe's [objects](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#object/3) requested in a query by checking the underlying struct.\n\n#### Usage:\n\n[Create your Authorization module and add it and ObjectScopeAuthorization to your Absinthe pipeline](#usage). Then set the scope of an object:\n\n```elixir\nobject :user do\n  # Turn on both Object and Field scoping, but if the FieldAuthorization middleware is not included, this is the same as using `scope_object?`\n  meta :scope?, true\n\n  field :id, :integer\n  field :email, :string\n  field :name, :string\n\n  field :company, :company\nend\n\nobject :company do\n  meta :scope_object?, true\n\n  field :id, :integer\n  field :user_id, :integer\n  field :name, :string\n  field :wallet, :wallet\nend\n\nobject :wallet do\n  meta :scope?, true\n  meta :rule, :object_authorization\n\n  field :total, :integer\nend\n```\n\nTo define custom rules for the scoping, use [has_user_access?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/3). For example:\n\n```elixir\ndefmodule Authorization do\n  use Rajska,\n    valid_roles: [:user, :admin],\n    super_role: :admin\n\n  @impl true\n  def has_user_access?(%{role: :admin}, %User{}, _rule), do: true\n  def has_user_access?(%{id: user_id}, %User{id: id}, _rule) when user_id === id, do: true\n  def has_user_access?(_current_user, %User{}, _rule), do: false\n\n  def has_user_access?(%{id: user_id}, %Wallet{user_id: id}, :object_authorization), do: user_id == id\nend\n```\n\nThis way different rules can be set to the same struct.\n\n### Field Authorization\n\nAuthorizes Absinthe's object [field](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#field/4) according to the result of the [has_user_access?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/3) function, which receives the user role, the `source` object that is resolving the field and the field rule.\n\n#### Usage:\n\n[Create your Authorization module and add it and FieldAuthorization to your Absinthe.Schema](#usage).\n\n```elixir\nobject :user do\n  # Turn on both Object and Field scoping, but if the ObjectScope Phase is not included, this is the same as using `scope_field?`\n  meta :scope?, true\n\n  field :name, :string\n  field :is_email_public, :boolean\n\n  field :phone, :string, meta: [private: true]\n  field :email, :string, meta: [private: \u0026 !\u00261.is_email_public]\n\n  # Can also use custom rules for each field\n  field :always_private, :string, meta: [private: true, rule: :private]\nend\n\nobject :field_scope_user do\n  meta :scope_field?, true\n\n  field :name, :string\n  field :phone, :string, meta: [private: true]\nend\n```\n\nAs seen in the example above, a function can also be passed as value to the meta `:private` key, in order to check if a field is private dynamically, depending of the value of another field.\n\n### Rate Limiter\n\nRate limiter absinthe middleware. Uses [Hammer](https://github.com/ExHammer/hammer).\n\n#### Usage\n\nFirst configure Hammer, following its documentation. For example:\n\n```elixir\nconfig :hammer,\n  backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4,\n                              cleanup_interval_ms: 60_000 * 10]}\n```\n\nAdd your middleware to the query that should be limited:\n\n```elixir\nfield :default_config, :string do\n  middleware Rajska.RateLimiter\n  resolve fn _, _ -\u003e {:ok, \"ok\"} end\nend\n```\n\nYou can also configure it and use multiple rules for limiting in one query:\n\n```elixir\nfield :login_user, :session do\n  arg :email, non_null(:string)\n  arg :password, non_null(:string)\n\n  middleware Rajska.RateLimiter, limit: 10 # Using the default identifier (user IP)\n  middleware Rajska.RateLimiter, keys: :email, limit: 5 # Using the value provided in the email arg\n  resolve \u0026AccountsResolver.login_user/2\nend\n```\n\nThe allowed configuration are:\n\n* `scale_ms`: The timespan for the maximum number of actions. Defaults to 60_000.\n* `limit`: The maximum number of actions in the specified timespan. Defaults to 10.\n* `id`: An atom or string to be used as the bucket identifier. Note that this will always be the same, so by using this the limit will be global instead of by user.\n* `keys`: An atom or a list of atoms to get a query argument as identifier. Use a list when the argument is nested.\n* `error_msg`: The error message to be displayed when rate limit exceeds. Defaults to `\"Too many requests\"`.\n\nNote that when neither `id` or `keys` is provided, the default is to use the user's IP. For that, the default behaviour is to use\n`c:Rajska.Authorization.get_ip/1` to fetch the IP from the absinthe context. That means you need to manually insert the user's IP in the\nabsinthe context before using it as an identifier. See the [absinthe docs](https://hexdocs.pm/absinthe/context-and-authentication.html#content)\nfor more information.\n\n## Related Projects\n\n[Crudry](https://github.com/jungsoft/crudry) is an elixir library for DRYing CRUD of Phoenix Contexts and Absinthe Resolvers.\n\n## License\n\nMIT License.\n\nSee [LICENSE](./LICENSE) for more information.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjungsoft%2Frajska","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjungsoft%2Frajska","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjungsoft%2Frajska/lists"}