inception-project / inception

INCEpTION provides a semantic annotation platform offering intelligent annotation assistance and knowledge management.
https://inception-project.github.io
Apache License 2.0
593 stars 151 forks source link

Mapping OIDC groups to INCEpTION's internal roles #4941

Open kzgrzendek opened 3 months ago

kzgrzendek commented 3 months ago

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 :

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 :

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

Thank you

reckart commented 3 months ago

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.

kzgrzendek commented 3 months ago

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.

reckart commented 3 months ago

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 commented 3 months ago

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 :

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 commented 3 months ago

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 commented 3 months ago

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 commented 3 months ago

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 commented 3 months ago

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 commented 3 months ago

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 :

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 commented 3 months ago

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 commented 3 months ago

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 :

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 commented 3 months ago

@kzgrzendek Would you like to propose a code change?

kzgrzendek commented 3 months ago

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 commented 2 months ago

@reckart, I've opened a PR here : https://github.com/inception-project/inception/pull/4982