Authorization architecture on distributed OTP apps

hello everyone,

I am trying to build an ecosystem on top of OTP, where I can create really well defined OTP apps and connected together so I can reuse it cross multiple businesses.

Even when I will use GraphQL with Absinthe this is an issue no matter what

Right now, I am using GraphQL (with Absinthe) for the IO throw HTTP. My expectations is to move the GraphQL schemas to the OTP apps, so every single app knows about it own GraphQL structure.

But I have a problem: Authorization.

Use Canada package keep in my this code

defimpl Canada.Can, for: StrawHat.Schema.UserSchema do
  # ... other validations
  def can?(_, _, _), do: false
end

defimpl Canada.Can, for: StrawHat.Schema.UnauthenticatedUserSchema do
  # other validations
  def can?(_, action, _) when action in [
    :register_user, :create_session], do: true
  def can?(_, _, _), do: false
end

You could see those modules as my current role, which it’s actually the type of user but it’s the same for my current application.

Because authorization is something that is attach to the user and/or role I can’t move right now those schemas outside of my monolith GraphQL endpoint (you could see it as specific business)

Example of Schema

object :user_mutations do
    field :create_admin, :user do
      arg :input, non_null(:user_input)
      
      # this is where the authorization happens
      # basically forwards the current user to the Canada implementation
      middleware AuthorizationMiddleware, [action: :create_admin]

      resolve &UserResolver.create_admin_user/2
      middleware ErrorFormatterMiddleware
    end
  end

Middleware implementation

defmodule StrawHat.GraphQL.Middleware.AuthorizationMiddleware do
  @behaviour Absinthe.Middleware
  alias Absinthe.Resolution
  alias StrawHat.Schema.UnauthenticatedUserSchema

  def call(%{context: context, state: :unresolved} = resolution, [action: action]) do
    current_user = Map.get(context, :current_user, %UnauthenticatedUserSchema{})
    
    # pretty simple, I am just using Canada with the current authenticated user if there is any
    if Canada.Can.can?(current_user, action, context) do
      resolution
    else
      Resolution.put_result(resolution, {:error, {:unauthorized, "You are not allow to perform this operation"}})
    end
  end
  def call(resolution, _), do: resolution
end

Because in that specific GraphQL endpoint live on specific business and that business have it own way to define how many type of users (which right now are roles as well) now I can’t create a truly independent OTP app because it’s have to know about the users roles.

Btw, I could move the authorization piece into my OTP apps which will be ideally because nobody have to implement anything related to authorization but, I can’t or actually I don’t know how to do it without attach every single OTP to specific business structure.

So the first phase would be to actually put those things inside a database so I can make it dynamic but that’s where I don’t know what is the best architecture for this, I am reading about Amazon Policies and it seems to be legic, I could actually force everything to pass a policy to my OTP module and with that policy I could check if you are authorize to perform the operation. Also, I am confused how to handle authorization per fields base, for example lunch_misil property which only specific rule could allow todo this. And then: read , write, execute a module and so on use cases that probably older programmers know about it

But I know for sure I will miss something I need your help to tackle this issue that could actually help everyone in the community. I would love if you could help me out because my knowledge is limited in this case on best practices and I want to think something independent of my business right now, because I know it will be a problem on my second business :smile:

Any link, video or knowledge that you can share will be appreciated.

Probably the idea is to create an OTP app that handles this but the architecture of that OTP and the communication with the other ones is where I need help

Is there a way to articulate this question that abstracts it from GraphQL specifically? Ultimately it seems like all you need is for the Canada.Can.can?(current_user, action, context) function to be sufficiently nuanced, but it’s a bit unclear what about the can? function doesn’t do what you want it to do.

The problem with Canada is that required specific Module that defines the protocol. Those modules are related to specific business which could change depending of whatever the business code base want to use or not the same module.

Also, I have to be modify codebase when I want to switch any rule, like don't allow X action

Is there a way to articulate this question that abstracts it from GraphQL specifically?

It’s more about handling authorization on distributed OTP apps actually

Right now the dilemma is to do something like Amazon does, where basically you (from what I am assuming) every OTP will check versus some Broker OTP for checking the Policy used

or

We go something like Microsoft does with groups where I don’t need to check versus some system to know if you are allow to perform the operation, I just need to check for specific role that every OTP system have define.

For example.

Let say it’s like a pass card when we go to the museum. If I get the card allowed to pass everywhere every single security person do not need to check versus some system to know if I am allow to pass or not, the security person just checks if my badge is allow to pass.

Or we could change it to the Amazon style where that security person will check versus another system to allow me to pass.

The amazon piece is really complicate IMO, because resources and actions in resources change too much and the central OTP that manage the policies and those stuff have to be sync with all the changes.

In your case of having a reusable “repository” otp app you need to separate where a user is assigned permissions/roles and where your check if he has those.

Business App:
Assign permissions/roles based on the specific business rules, e.g.:
%BusinessUser{permissions: [:create-blog-post, :publish-blog-post]}

-> send permissions/roles to repository ->

Reusable Repository:
%RepoUser{permissions: [:create-blog-post, :publish-blog-post]}
You’d implement the canada interface for %RepoUser{} and create that user from the input sent by the Business App.

Which permissions/roles you’re using here is up to your repository otp app to define (and document), but they probably need to be way more granular than the ones in the business app. E.g. the business app might have a single role reviewer, which does allow the user to have all the blog post related permissions ([:create-blog-post, :publish-blog-post, :delete-blog-post]) when using your repository otp app.

This has the added benefit, that nobody needs to know your using canada in the repository app and the business app can use canada, but as easily any other system. It does just need to map their own permissions/roles to the ones used by the repository app.

Would be better to do something like Kerberos where I can define base on roles what the user could do

but,

When I load the user I will find the his roles or group (that have some roles attached like Amazon), so I can move the logic of User + Role into some database (meaning that I dont have to code every single time which user have which permission)

but,

With that logic, I don’t have the power of do something like Amazon where it can denied some resources rather than look at it as group.

I am trying to find a solution around OTP apps rather than just Canada way. Like, how to handle the whole authorization piece inside my OTP architecture.