Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Endpoint authorization #10

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

jbrooks2-godaddy
Copy link

@jbrooks2-godaddy jbrooks2-godaddy commented Nov 8, 2022

Description

This RFC proposes a configuration schema for restricting access to authorized clients.

Rendered version

Click here to view the rendered markdown

References

Review Checklist

  • I have clicked on "allow edits by maintainers".
  • I have added documentation for new/changed functionality in this PR or in a PR to openfga.dev [Provide a link to any relevant PRs in the references section above] (N/A at this time, this will come with the implementation)
  • The correct base branch is being used, if not main
  • I have added tests to validate that the change in functionality is working as expected N/A

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@jbrooks2-godaddy
Copy link
Author

Oops, I missed the CLA requirement. Working on that internally now.

- It requires setup outside of just writing a configuration file.
- It couples the authorization system to other parts of OpenFGA.
- There is a chicken-and-egg problem; you cannot protect the system prior to starting it up.
- OpenFGA would be prescribing an authorization model that might not work for the operator
Copy link
Member

Choose a reason for hiding this comment

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

Just a note on this - the primary proposal mentioned herein is also very prescriptive (scopes/preshared keys per endpoint). A benefit of this alternative approach is that the model can be more fine grained and dynamic instead of statically prescribed. For example, with this approach the operators could potentially define their system access model using OpenFGA modeling.

@jon-whit
Copy link
Member

jon-whit commented Nov 8, 2022

@jbrooks2-godaddy thanks for the very detailed and thorough RFC submission 👏 ! This is awesome.

We've discussed per-endpoint authorization internally on the team various times and don't yet have it planned in our immediate roadmap (sometime next year was the goal), but we do understand the significance of it and how beneficial it could be.

I like the pragmatic approach to your proposal and I think this would be a reasonable approach to implementing it, however I do also believe that using OpenFGA within OpenFGA to enforce authorizations per-endpoint could be much more flexible and may cater to a larger audience of OpenFGA operators. For that reason I don't think we should discredit that as a viable option and it should be explored.

keys:
- cool-key-1
- cool-key-2
authz:
Copy link
Member

Choose a reason for hiding this comment

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

So that we're not mixing authn with authz I may recommend we introduce a separate block of config for the per-endpoint configuration. May I suggest an endpoints config?

authn:
  method: oidc
  oidc:
    issuer: ...
    audience: ...
      
endpoints:
  - openfga.v1.OpenFGAService/Write
     scopes:
       - openfga:write
  - openfga.v1.OpenFGAService/Read
     scopes:
       - openfga:read

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, it makes sense to split it out. I had it nested under the oidc/preshared block because the means of authorization differs between the two.

I suppose that the authorization block could be authn-type-agnostic, and we could enforce restrictions during config validation. For example, if you accidentally write a keys block when the authn method is oidc, the config schema could allow it but we can catch that in validation:

authz:
  global:
    scopes:
      - openfga:read
    keys:
      - cool-key-1 // specifying a key in addition to a scope would result in a config validation error
  endpoints:
    - openfga.v1.OpenFGAService/Write
         scopes:
           - openfga:write

Alternatively, if we want distinct schemas for each method type we could have blocks similar to authn:

authz:
  oidc:
    global:
      scopes:
        - openfga:read
    endpoints:
      - openfga.v1.OpenFGAService/Write
           scopes:
             - openfga:write
  preshared:
    <empty here, but would be filled out when authn is preshared>

Copy link
Member

@jon-whit jon-whit Nov 9, 2022

Choose a reason for hiding this comment

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

I like the idea of having distinct schemas for each authn method

authn:
  method: oidc
  oidc:
     ...

permissions:
  - endpoint: openfga.v1.OpenFGAService/Write
    oidc:
      scopes:
        - openfga:write
authn:
  method: preshared
  preshared:
    ...

permissions:
  - endpoint: openfga.v1.OpenFGAService/Read
    preshared:
      keys:
        - key1

It'd be nice if we could omit the usage of the term 'global' as well. It'd be nice to just specify a top-level requirement that is used unless the endpoint is specifically overridden. Will have to 🤔 about how that may best look?

@jbrooks2-godaddy
Copy link
Author

Thanks for the feedback @jon-whit. I'll spend some time exploring an OpenFGA-driven solution in more detail.

At a high level, though, do you think that a configuration-based approach like this could live alongside an OpenFGA-driven approach as two alternative options for adding authorization?

@jon-whit
Copy link
Member

jon-whit commented Nov 9, 2022

Thanks for the feedback @jon-whit. I'll spend some time exploring an OpenFGA-driven solution in more detail.

At a high level, though, do you think that a configuration-based approach like this could live alongside an OpenFGA-driven approach as two alternative options for adding authorization?

I definitely think we could support both, though only one would be configured at any point in time. That being said, maintaining the code for two configuration streams for per-endpoint permissions in OpenFGA seems like it would be a lot of effort and introduces a larger API surface footprint as well (more things to try and maintain backwards and forwards compatibility with). So if we can make a choice on something that fits more usage patterns up front it'll reduce the maintainence footprint on the core OpenFGA maintenance team.

* If the implementation is simple, it will not support all authorization use-cases (i.e. combinations of allow and deny).
* If the implementation allows for more complex authorization schemes, it will be more difficult to configure and more prone to implementation bugs.

# Alternatives
Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense to consider not to include subjects?

Given you'll need a OIDC server, you can take advantage of the ability of the server to assign scopes per client_id.

I'm not familiar with how this works with other OIDC servers but I know Auth0 supports it. Do you know if other OIDC servers do not have this feature?

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, that would be reasonable! I included the subjects as an option as the code currently extracts both the subject and the scopes. I think that most operators would likely authorize based on scopes, I think we could leave the subjects portion out unless someone asks for it.

@jbrooks2-godaddy
Copy link
Author

jbrooks2-godaddy commented Nov 10, 2022

@jon-whit in 2316dd5 I explored the possibilities of OpenFGA-driven authorization in more detail. It's a bit loose (schema is not the best, need to think through some implementation details) as I wanted to get some early feedback - if the direction makes sense I think I'll extract it out into a separate RFC dedicated to the approach. Let me know what you think!

@jon-whit
Copy link
Member

jon-whit commented Nov 10, 2022

@jbrooks2-godaddy I'm curious if you've considered per-store authorization as part of this discovery as well?

Your proposal introduces support for per-endpoint authorization, which will allow operators to restrict certain rights to the top-level endpoint. However, OpenFGA largely operates on the abstraction of an OpenFGA Store, which scopes/isolates models and tuples. OpenFGA Stores enable multi-tenancy effectively.

Another very valid use case for authorization is to restrict who can perform certain actions (Check, Write, Expand, Read, etc...) on an individual store. Do you have any use cases for that in your workflows? If you were to extend this proposal to support per-store authorization do you have some design ideas for how that would be done?


An approach I'd like to explore when I have a bit more time is bootstrapping OpenFGA with an internal store (call it _openfga or something) and bootstrap a single (static) authorization model that OpenFGA will use like so:

// subject represents a client calling OpenFGA APIs (could be on behalf of a user or app)
type subject

// _openfga here is a special type that is used to bootstrap top-level administrators/operators
type _openfga
  relations
      define admin: [subject]
      define create_store: [subject] as self or admin
      define share_access as admin

// the store type represents relationships/permissions you can grant subjects to manage a store
type store
  relations
      define parent: [_openfga]
      // manage the store (delete it, add authorization model to a store, etc..)
      define manage: [subject] as self or admin from parent
      define tuple_writer: [subject] as self or manage
      define tuple_reader: [subject] as self or manage or tuple_writer

You'd run OpenFGA with something like

./openfga run --admin-subjects bob@example.com, alice@example.com

which would bootstrap the model above and add the following tuples to the bootstrapped _openfga store:

store object relation user
_openfga _openfga:root admin subject:bob@example.com
_openfga _openfga:root admin subject:alice@example.com

This would allow alice@example.com and bob@example.com to bootstrap OpenFGA Stores and start assigning permissions to other subjects in the system.

Let's pretend that Alice then creates a new store (store1) and then gives peter@example.com the ability to manage the store. So now we'd have

store object relation user
_openfga _openfga:root admin subject:bob@example.com
_openfga _openfga:root admin subject:alice@example.com
_openfga store:store1 manage subject:peter@example.com

Now if Peter goes to manage authorization models, write tuples, or read tuples for store1 then he will be able to do so because he has the permission bootstrapped in the _openfga store that will be used to check if Peter has permission when he invokes, for example, /stores/store1/write (Check(_openfga, store:store1, tuple_writer, subject:peter@example.com) --> {allowed: true}).


Another variation to the approach I mention above could include allowing the developer to specify a model that OpenFGA will use to enforce per-store authorization. To do this you'd have to implement an authorization model interface, but the implementation of that interface is up to you. For example, let's say that the OpenFGA server requires you to define a model but you can define the model however you like so long as it contains the following definitions

type subject

type _openfga
    relations
        define admin: [subject]
        define create_store: [subject] <expression of your choice but has to contain 'as self'>

type store
    relations
      define parent: [_openfga]
      define manage: [subject] <expression of your choice but has to contain 'as self'>
      define tuple_writer: [subject] <expression of your choice but has to contain 'as self'>
      define tuple_reader: [subject] <expression of your choice but has to contain 'as self'>

Essentially you just have to make sure that the following relationships type definitions are met but the expressions are up to you:

  • subject - the subject type has to exist
  • _openfga#admin relation has to exist and it has to be directly related to subject
  • _openfga#create_store relation has to exist and be assignable to a subject but it's rewrite can be operator defined otherwise
  • store#parent has to be directly related to the root _openfga entity
  • store#manage, store#tuple_writer, and store#tuple_reader all have to be assignable to a subject type but it's rewrite can be operator defined otherwise.

This approach would allow operators to customize what the relation rewrite rules (expressions) are defined as for their unique use case(s) but guarantees a stable interface that the OpenFGA server can still use. OpenFGA will still issue internal checks of the form Check(store:<id>, tuple_write, subject:<id>), for example, when checking if the subject can call the Write API for the provided store.


Another option altogether here that I have been toying with the idea of is a pluggable module system. There are various behaviors of OpenFGA that we may want the community to be able to "plugin". For example, authentication and authorization are good candidates for this. One developer may have different authentication/authorization requirements for their deployment of OpenFGA. I think OpenFGA should have some baseline support for standardized authentication (like we do today with preshared keys and OIDC authn) and authorization mechanisms, but it would also be nice if developers could register their own plugins with the OpenFGA server at runtime and use the middleware(s) provided by those plugins to handle their custom use cases.

There's another option here where we could expose an authorizer interface which is just a grpc middleware function that you register with the OpenFGA server at startup. You'd implement your own authorization middleware in that plugin and it would get registered at startup and used for the server.

This would allow us to implement both your solution and my proposed solutions without necessarily introducing anything into the mainline of development in in the OpenFGA project (until we see a strong reason to do so). For example

@jbrooks2-godaddy
Copy link
Author

Lots of great ideas here @jon-whit! My proposal as written supports per-store authorization, but adding that precludes more fine-grained authorization within the store. That's a gap, some improvement is needed there. In order to support both the per-store authorization and more fine-grained dynamic authorization based on the request parameters as described I think that a multi-layered approach would be needed. Something like your bootstrapped approach where a subject is granted access to a store, and then further per-endpoint configs where a subject is restricted based on the type of request they are trying to make. Overall I'm less enthusiastic now about reaching that far into a request to authorize based on parameters like the object type... It feels a bit too complex.

Both of your ideas are interesting, but I think that the plugin system is the most appealing. So far we've discussed:

  • Authorization based on OIDC scopes or subjects, either globally or per-endpoint
  • Authorization based on API keys, either globally or per-endpoint
  • Per-store authorization based on a bootstrapped store+authZ model
  • Per-store authorization with a custom authZ model
  • Global and endpoint authorization with a custom authZ model and dynamic parameter substitution

I see some value in all of these approaches! An operator who is running OpenFGA as a pure platform provider will care a lot more about strong multi-tenancy than an operator running in-house trying to add some additional checks on internal clients. The plugin model would let all of these options co-exist while also making it easy for adopters to customize according to their needs.

In addition to authN/authZ I think that the storage layer is a natural candidate for a plugin system. It's easy enough to implement the storage interface but there's some code duplication needed to add it to the server at this time.

@RichiCoder1
Copy link

Howdy! Was anything further proposed or developed on this?

I def love the idea of being able to "bootstrap" OpenFGA with a store and being able to provide initial tuples based on that. The idea to have default "Authorization Model", but be able to provide a custom one is very appealing, as well as the ability to potentially provide OIDC Claims as "contextual tuples" with a well defined mapping. This is sort how OPA handles it (https://www.openpolicyagent.org/docs/latest/security/#authentication-and-authorization) though obviously it has both more and less flexibility as a policy language to be able to do that.

That said, a plugin enabling that behavior would also be a nice compromise.

As an addendum to the above, has the possibility of multiple authenticators been spoken of?

@jbrooks2-godaddy
Copy link
Author

Hey @RichiCoder1, I unfortunately did not pursue the approaches outlined here any further - we ended up implementing authorization in another layer due to time constraints. No plans to dig in further now (I suppose I should close this PR?), but happy to help review any other proposed approaches

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants