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

Mapping OIDC groups to INCEpTION's internal roles #4941

Open
kzgrzendek opened this issue Jul 13, 2024 · 14 comments
Open

Mapping OIDC groups to INCEpTION's internal roles #4941

kzgrzendek opened this issue Jul 13, 2024 · 14 comments
Assignees
Labels
⭐️ Enhancement New feature or request
Milestone

Comments

@kzgrzendek
Copy link

kzgrzendek commented Jul 13, 2024

Hello,

I would like to suggest a feature regarding the mapping of OIDC/OAUTH2 groups (when this authentication mode is enabled) to the internal roles of INCEpTION (ADMIN, USER, etc.)

Context

I'm working for the Greater Paris University Hospitals, and we would like to propose INCEpTION as our go-to text annotation tool for our users, as part of our 'Health Data Warehouse' offer (link content in French only, unfortunately).

As you can guess, we are tied by very strict legal and industrial obligation, one of them being the obligation to federate the authentication and authorization of our userbase when we provide services.

This implicates that the applications we're deploying, if they need 'internal roles', should be able to map those with pre-existing groups defined in our SSO solution (Keycloak, in our case).

The go-to standard for doing that being to use OIDC/OAUTH2 to fetch those groups in the user's token or with the userInfo endpoint, that's the reason why I'm proposing this feature, and I guess we're not the only one with those obligations.

Description of the feature

If we simply look at a solution like JupyterHub, we can see that its OIDC module is proposing to set :

  • The name of the claim containing the groups (c.GenericOAuthenticator.claim_groups_key)
  • The groups allowed as users (c.GenericOAuthenticator.allowed_group)
  • The groups allowed as admins (c.GenericOAuthenticator.admin_groups)

Implicitly, all users not members of those groups are denied by the application.

Edge cases to bear in mind

I can think of a few edge cases, but you're welcome to add yours as well :

  • If a user is in several groups, granting him different access level to the application, he should have the highest possible access level
    • eg, if a user can be mapped to USER and ADMIN groups, he should be mapped as ADMIN (unless INCEpTION supports having users in several groups!)
  • If a user can't be mapped to any group, he should have his access denied (returning à HTTP 403 code)
  • If a user is promoted to a group, it should be reflected in the application
    • eg, if a user was in an group mapped to USER, and is now in a group mapped to ADMIN, he should appear as an ADMIN in INCEpTION, and no more as a USER

I'm hoping you'll consider this feature, and am of course willing to discuss about it :)

Thank you

@kzgrzendek kzgrzendek changed the title Mapping OIDC groups to INCEpTION-s internal groups Mapping OIDC groups to INCEpTION's internal groups Jul 13, 2024
@reckart
Copy link
Member

reckart commented Jul 15, 2024

Actually, INCEpTION has no concept of groups.

We have global roles like ROLE_ADMIN or ROLE_PROJECT_CREATOR and project-local roles such as MANAGER. It would seem to me that mapping Keycloak's "client roles" might be mapped to the global roles.

I don't think it would be sensibel to try deferring the project roles to an external mechanism.

@reckart reckart added the ⭐️ Enhancement New feature or request label Jul 15, 2024
@kzgrzendek
Copy link
Author

Thanks for your quick reply and your clarification @reckart!

I've updated the Issue title to avoid any confusions.

It doesn't seem very different from what you're doing with your external preauth mechanism though? I mean, in that configuration, you can already assign roles to a user according to a header, in that case it would be doing the same thing, but based on oauth2's authorization mechanism, from a claim in the token, rather than a header parameter.

@kzgrzendek kzgrzendek changed the title Mapping OIDC groups to INCEpTION's internal groups Mapping OIDC groups to INCEpTION's internal roles Jul 15, 2024
@reckart
Copy link
Member

reckart commented Jul 15, 2024

External pre-authentication does only that - authentication.

INCEpTION does currently not support external authorization mechanisms.

It is possible to override the roles of an externally authenticated user using auth.user.<username>.roles in the settings.properties file. This is meant to allow e.g. assign the admin role to an externally authenticated user.

I checked Keycloak. It supports the concept of roles and groups. Roles are mapped to permissions while groups are aggregations of users. INCEpTION does not have the concept of groups to aggregate users. It does have the concept of roles (global and per-project).

Keycloak allows to define per-client roles. So if you set up INCEpTION as a client in Keycloak, then you could e.g. define the per-client roles ROLE_ADMIN, ROLE_USER, ROLE_PROJECT_CREATOR there as per-client roles and those could be sent to INCEpTION as part of the ID token. Looking only at Keycloak, that would seem to be a reasonable approach that would not mix up the concepts of roles and groups.

@kzgrzendek
Copy link
Author

kzgrzendek commented Jul 15, 2024

Thank you for the feedback

I agree to say that authentication and authorization are of course two different mechanism, but am not sure about your example - if I take this example from the pre-auth part of your documentation :

auth.mode                     = preauth
auth.preauth.header.principal = remote_user
auth.preauth.newuser.roles    = ROLE_PROJECT_CREATOR
auth.user.Franz.roles         = ROLE_ADMIN

That's literally RBAC access, when you map a role to a user. In this specific case, we're even granting admin permission to the user Franz, so to me you're effectively deciding the scope of what privileges an authenticated user can have - which looks like authorization to me.

I fail to see why it's an issue to map a role to a group of user when it's already a possibility for one specific user - is it because INCEpTION doesn't manage groups of users, and you dont like the idea of inheriting groups from an SSO (plugged to an AD), because those are not hardcoded in the DB?

To reframe the issue :

  • I have a Keycloak containing my company's users and groups
  • I want to deploy one instance of INCEpTION per research project, I have hundreds of them
    • Each project have a dedicated group of user in my Keycloak
    • My company's admins also have a dedicated user group in Keycloak, and must be admins of all my deployments of INCEpTION

How do I setup my INCEpTION deployments to bind each project's user groups to their corresponding instance of INCEpTION, while insuring that admins can have the correct group in every deployments?

@reckart
Copy link
Member

reckart commented Jul 15, 2024

Typically, there is a single deployment shared between all users. Please mind that every deployment requires its own dedicated database and file system - you cannot share databases and file systems between instances.

@reckart
Copy link
Member

reckart commented Jul 15, 2024

That's literally RBAC access, when you map a role to a user. In this specific case, we're even granting admin permission to the user Franz, so to me you're effectively deciding the scope of what privileges an authenticated user can have - which looks like authorization to me.

That is correct, it is authorization. But the authorization does not come from an external system. It is part of the configuration files of INCEpTION in this case.

I fail to see why it's an issue to map a role to a group of user when it's already a possibility for one specific user - is it because INCEpTION doesn't manage groups of users, and you dont like the idea of inheriting groups from an SSO (plugged to an AD), because those are not hardcoded in the DB?

As I said, INCEpTION does not have a concept of user groups, so it is not possible to assign a role to a group of users.

What you are asking is that I implement a mechanism that dynamically assigns a role to a user based on some claim in the authentication token. You would then want to put a claim into that token that indicates the group membership and the mechanism on the INCEpTION side would need to see that token and then dynamically add the user role. That means, I would need to implement a quite flexible mechanism like if claim-property contains XXX then assign role_x_ to current-user.

It would be much simpler for me if there was a mapping mechanism outside of INCEpTION that already prepares the role assignment as INCEpTION needs it. So if you could put into your claim something like resource-access.inception-client-oauth.roles = { ROLE_ADMIN, ROLE_USER} then I could just pick up the roles them there and apply them to the user. INCEpTION would not need to know about your groups at all.

@kzgrzendek
Copy link
Author

kzgrzendek commented Jul 15, 2024

Typically, there is a single deployment shared between all users. Please mind that every deployment requires its own dedicated database and file system - you cannot share databases and file systems between instances.

Ok, I thought it would be the case. We're using our Kubernetes to deploy separate instances with our gitops cd workflow, so the deployments are separated.

Unfortunately, laws regarding segregation of access to the data makes it hard in our case to deploy a "one-deployment-fits-all" case.

@reckart
Copy link
Member

reckart commented Jul 15, 2024

As long as you fully segragate your deployments, it is fine. I am just mentioning it because some users have tried to run multiple INCEpTION instances against the same database and file system - basically trying to share the data layer across multiple INCEpTION instances - and that is not supported. An INCEpTION instance expects that it has exclusive access to its database and filesystem and will cache certain things in memory assuming that no parallel instance or external process will make any modifications.

@kzgrzendek
Copy link
Author

kzgrzendek commented Jul 15, 2024

That's literally RBAC access, when you map a role to a user. In this specific case, we're even granting admin permission to the user Franz, so to me you're effectively deciding the scope of what privileges an authenticated user can have - which looks like authorization to me.

That is correct, it is authorization. But the authorization does not come from an external system. It is part of the configuration files of INCEpTION in this case.

I fail to see why it's an issue to map a role to a group of user when it's already a possibility for one specific user - is it because INCEpTION doesn't manage groups of users, and you dont like the idea of inheriting groups from an SSO (plugged to an AD), because those are not hardcoded in the DB?

As I said, INCEpTION does not have a concept of user groups, so it is not possible to assign a role to a group of users.

What you are asking is that I implement a mechanism that dynamically assigns a role to a user based on some claim in the authentication token. You would then want to put a claim into that token that indicates the group membership and the mechanism on the INCEpTION side would need to see that token and then dynamically add the user role. That means, I would need to implement a quite flexible mechanism like if claim-property contains XXX then assign role_x_ to current-user.

It would be much simpler for me if there was a mapping mechanism outside of INCEpTION that already prepares the role assignment as INCEpTION needs it. So if you could put into your claim something like resource-access.inception-client-oauth.roles = { ROLE_ADMIN, ROLE_USER} then I could just pick up the roles them there and apply them to the user. INCEpTION would not need to know about your groups at all.

I can see two limitations with that approach :

  • That custom claim would have the same value for all the users of that client
  • Even if I would be able with a plugin or a custom solution from Keycloak to change the value of that claim according to the user's group, that would result into having a tight coupling between INCEpTION and Keycloak, whereas every OAuth2 provider must be able to implement the groups claim as indicated in the RFC

I can totally see the issue though, that's no small development.

One question :

If I can set in the setting file the mapping in that fashion :

auth.oauth2.user-group-attribute="groups"
auth.oauth2.user.group="/AD_USER_GROUP"
auth.oauth2.admin.group="/AD_ADMIN_GROUP"
auth.oauth2.project-creator.group="/AD_ADMIN_GROUP"

Wouldn't it be possible on Spring Security side to get the value of the group claim from the token in the request and to compare it to the mapping defined in the property file, to decide if the user can do an action?

I guess it's like what you're doing in pre-authentication mode, unless you would get the group from that mapping done in the config file rather extracting it from a header - in both cases noting is store in DB so you don't have to create a whole new user groups model

@reckart
Copy link
Member

reckart commented Jul 15, 2024

I checked the Spring Security code. The implementation does map SCOPE_ information to authorizations in Spring Security. But that does not help either of use because INCEpTION uses ROLE_ authorizations and you want to have groups.

I stepped with the debugger through the code to find out if it might be easy to simple configure INCEpTION / Spring Security to pick up authorizations from a roles claim instead of a scope claim, but unfortunately, it appears that the relevant parts of Spring Security are hard-coded to use the scope. I didn't note the classes down, so I'd have to fire up the debugger again to find the exact places.

Of course, this behavior could be changed. INCEpTION is already using quite a bit of custom code to interface with Spring Security, OAuth and SAML.

That custom claim would have the same value for all the users of that client

As far as I understand, that claim would not have the same value for all users of the client.

You can define per-client roles:

Screenshot 2024-07-15 at 13 07 35

Then you can assign these roles to users:

Screenshot 2024-07-15 at 13 08 30

Then you can map these as a claim (the "client roles" mapped is available from the "Add bultin" menu):

Screenshot 2024-07-15 at 13 10 00

And finally ensure that the claim is part of the ID Token

Screenshot 2024-07-15 at 13 11 31

At least, that made the ROLE_ADMIN in this case show on in the claim on the INCEpTION side where I could more-or-less reasonably extract it. Again, custom code would need to be written on the INCEpTION side to make that actually happen. I didn't find a way to simply do it using existing (Spring Security) configuration options.

@kzgrzendek
Copy link
Author

kzgrzendek commented Jul 16, 2024

Thank you for the effort you've put to see this through.

I took the time to browse a bit your codebase (last time I worked with Spirng Security was years ago!), and if I understand things correctly :

  • I thought you relied completely on Spring Security internal mechanisms to manipulate the access token, but it seems you rather have a bit of custom code as you said.
    • You're already mapping token claims here, so that maybe could be a good place to map the groups claim
  • In the OAuth2AdapterImpl class, you're calling direclty to the PreAuthUtils utils class to give the default "new user role" (which I suspected you would, given the similarities with the pre-auth workflow I pointed a few comments above)
    • Maybe it would be possibe to call to another utils method that would do the mapping between groups and roles from the settings file?

As for what you're pointing with Keycloak, that would demand to create roles and permissions inside Keycloak. It's a valid suggestion, not applicable in our case as we're using Keycloak to gather groups from our Active Directory, so we can't create roles and authorizations in two different places.
And, I still believe it would be writing custom code matching internal Keycloak mechanisms which are not widely OAuth2 standards (many providers or authorization servers doesn't offer the possibility of creating custom internal roles but are still valid OAuth2 implementations)

@reckart
Copy link
Member

reckart commented Jul 16, 2024

@kzgrzendek Would you like to propose a code change?

@kzgrzendek
Copy link
Author

kzgrzendek commented Jul 16, 2024

I could have a look at it once I will be done with what I'm working on atm - it might need a bit of back and forth since I'm not familiar with the codebase

@kzgrzendek
Copy link
Author

@reckart, I've opened a PR here : #4982

@reckart reckart added this to the 35.0 milestone Sep 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
⭐️ Enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants