mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 06:57:33 +00:00
feat: role claims for service user tokens (#5577)
tokens of service users can now contain role claims by requesting them through scopes
This commit is contained in:
@@ -73,13 +73,14 @@ You can add custom claims using the [complement token flow](/docs/apis/actions/c
|
|||||||
|
|
||||||
ZITADEL reserves some claims to assert certain data. Please check out the [reserved scopes](scopes#reserved-scopes).
|
ZITADEL reserves some claims to assert certain data. Please check out the [reserved scopes](scopes#reserved-scopes).
|
||||||
|
|
||||||
| Claims | Example | Description |
|
| Claims | Example | Description |
|
||||||
|:--------------------------------------------------|:-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|:--------------------------------------------------|:---------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| urn:zitadel:iam:action:{actionname}:log | `{"urn:zitadel:iam:action:appendCustomClaims:log": ["test log", "another test log"]}` | This claim is set during Actions as a log, e.g. if two custom claims with the same keys are set. |
|
| urn:zitadel:iam:action:{actionname}:log | `{"urn:zitadel:iam:action:appendCustomClaims:log": ["test log", "another test log"]}` | This claim is set during Actions as a log, e.g. if two custom claims with the same keys are set. |
|
||||||
| urn:zitadel:iam:org:domain:primary:{domainname} | `{"urn:zitadel:iam:org:domain:primary": "acme.ch"}` | This claim represents the primary domain of the organization the user belongs to. |
|
| urn:zitadel:iam:org:domain:primary:{domainname} | `{"urn:zitadel:iam:org:domain:primary": "acme.ch"}` | This claim represents the primary domain of the organization the user belongs to. |
|
||||||
| urn:zitadel:iam:org:project:roles | `{"urn:zitadel:iam:org:project:roles": [ {"user": {"id1": "acme.zitade.ch", "id2": "caos.ch"} } ] }` | When roles are asserted, ZITADEL does this by providing the `id` and `primaryDomain` below the role. This gives you the option to check in which organization a user has the role. |
|
| urn:zitadel:iam:org:project:roles | `{"urn:zitadel:iam:org:project:roles": [ {"user": {"id1": "acme.zitade.ch", "id2": "caos.ch"} } ] }` | When roles are asserted, ZITADEL does this by providing the `id` and `primaryDomain` below the role. This gives you the option to check in which organization a user has the role on the current project (where your client belongs to). |
|
||||||
| urn:zitadel:iam:roles:{rolename} | TBA | TBA |
|
| urn:zitadel:iam:org:project:{projectid}:roles | `{"urn:zitadel:iam:org:project:id3:roles": [ {"user": {"id1": "acme.zitade.ch", "id2": "caos.ch"} } ] }` | When roles are asserted, ZITADEL does this by providing the `id` and `primaryDomain` below the role. This gives you the option to check in which organization a user has the role on a specific project. |
|
||||||
| urn:zitadel:iam:user:metadata | `{"urn:zitadel:iam:user:metadata": [ {"key": "VmFsdWU=" } ] }` | The metadata claim will include all metadata of a user. The values are base64 encoded. |
|
| urn:zitadel:iam:roles:{rolename} | TBA | TBA |
|
||||||
| urn:zitadel:iam:user:resourceowner:id | `{"urn:zitadel:iam:user:resourceowner:id": "orgid"}` | This claim represents the id of the resource owner organisation of the user. |
|
| urn:zitadel:iam:user:metadata | `{"urn:zitadel:iam:user:metadata": [ {"key": "VmFsdWU=" } ] }` | The metadata claim will include all metadata of a user. The values are base64 encoded. |
|
||||||
| urn:zitadel:iam:user:resourceowner:name | `{"urn:zitadel:iam:user:resourceowner:name": "ACME"}` | This claim represents the name of the resource owner organisation of the user. |
|
| urn:zitadel:iam:user:resourceowner:id | `{"urn:zitadel:iam:user:resourceowner:id": "orgid"}` | This claim represents the id of the resource owner organisation of the user. |
|
||||||
| urn:zitadel:iam:user:resourceowner:primary_domain | `{"urn:zitadel:iam:user:resourceowner:primary_domain": "acme.ch"}` | This claim represents the primary domain of the resource owner organisation of the user. |
|
| urn:zitadel:iam:user:resourceowner:name | `{"urn:zitadel:iam:user:resourceowner:name": "ACME"}` | This claim represents the name of the resource owner organisation of the user. |
|
||||||
|
| urn:zitadel:iam:user:resourceowner:primary_domain | `{"urn:zitadel:iam:user:resourceowner:primary_domain": "acme.ch"}` | This claim represents the primary domain of the resource owner organisation of the user. |
|
||||||
|
@@ -22,14 +22,15 @@ ZITADEL supports the usage of scopes as way of requesting information from the I
|
|||||||
|
|
||||||
In addition to the standard compliant scopes we utilize the following scopes.
|
In addition to the standard compliant scopes we utilize the following scopes.
|
||||||
|
|
||||||
| Scopes | Example | Description |
|
| Scopes | Example | Description |
|
||||||
| :------------------------------------------------ | :----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|:--------------------------------------------------|:-------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `urn:zitadel:iam:org:project:role:{rolekey}` | `urn:zitadel:iam:org:project:role:user` | By using this scope a client can request the claim urn:zitadel:iam:roles to be asserted when possible. As an alternative approach you can enable all roles to be asserted from the [project](/guides/manage/console/roles#authorizations) a client belongs to. |
|
| `urn:zitadel:iam:org:project:role:{rolekey}` | `urn:zitadel:iam:org:project:role:user` | By using this scope a client can request the claim `urn:zitadel:iam:org:project:roles` to be asserted when possible. As an alternative approach you can enable all roles to be asserted from the [project](/guides/manage/console/roles#authorizations) a client belongs to. |
|
||||||
| `urn:zitadel:iam:org:id:{id}` | `urn:zitadel:iam:org:id:178204173316174381` | When requesting this scope **ZITADEL** will enforce that the user is a member of the selected organization. If the organization does not exist a failure is displayed. It will assert the `urn:zitadel:iam:user:resourceowner` claims. |
|
| `urn:zitadel:iam:org:projects:roles` | `urn:zitadel:iam:org:projects:roles` | By using this scope a client can request the claim `urn:zitadel:iam:org:project:{projectid}:roles` to be asserted for each requested project. All projects of the token audience, requested by the `urn:zitadel:iam:org:project:id:{projectid}:aud` scopes will be used. |
|
||||||
| `urn:zitadel:iam:org:domain:primary:{domainname}` | `urn:zitadel:iam:org:domain:primary:acme.ch` | When requesting this scope **ZITADEL** will enforce that the user is a member of the selected organization and the username is suffixed by the provided domain. If the organization does not exist a failure is displayed |
|
| `urn:zitadel:iam:org:id:{id}` | `urn:zitadel:iam:org:id:178204173316174381` | When requesting this scope **ZITADEL** will enforce that the user is a member of the selected organization. If the organization does not exist a failure is displayed. It will assert the `urn:zitadel:iam:user:resourceowner` claims. |
|
||||||
| `urn:zitadel:iam:role:{rolename}` | | |
|
| `urn:zitadel:iam:org:domain:primary:{domainname}` | `urn:zitadel:iam:org:domain:primary:acme.ch` | When requesting this scope **ZITADEL** will enforce that the user is a member of the selected organization and the username is suffixed by the provided domain. If the organization does not exist a failure is displayed |
|
||||||
| `urn:zitadel:iam:org:project:id:{projectid}:aud` | `urn:zitadel:iam:org:project:id:69234237810729019:aud` | By adding this scope, the requested projectid will be added to the audience of the access token |
|
| `urn:zitadel:iam:role:{rolename}` | | |
|
||||||
| `urn:zitadel:iam:org:project:id:zitadel:aud` | `urn:zitadel:iam:org:project:id:zitadel:aud` | By adding this scope, the ZITADEL project ID will be added to the audience of the access token |
|
| `urn:zitadel:iam:org:project:id:{projectid}:aud` | `urn:zitadel:iam:org:project:id:69234237810729019:aud` | By adding this scope, the requested projectid will be added to the audience of the access token |
|
||||||
| `urn:zitadel:iam:user:metadata` | `urn:zitadel:iam:user:metadata` | By adding this scope, the metadata of the user will be included in the token. The values are base64 encoded. |
|
| `urn:zitadel:iam:org:project:id:zitadel:aud` | `urn:zitadel:iam:org:project:id:zitadel:aud` | By adding this scope, the ZITADEL project ID will be added to the audience of the access token |
|
||||||
| `urn:zitadel:iam:user:resourceowner` | `urn:zitadel:iam:user:resourceowner` | By adding this scope, the resourceowner (id, name, primary_domain) of the user will be included in the token. |
|
| `urn:zitadel:iam:user:metadata` | `urn:zitadel:iam:user:metadata` | By adding this scope, the metadata of the user will be included in the token. The values are base64 encoded. |
|
||||||
| `urn:zitadel:iam:org:idp:id:{idp_id}` | `urn:zitadel:iam:org:idp:id:76625965177954913` | By adding this scope the user will directly be redirected to the identity provider to authenticate. Make sure you also send the primary domain scope if a custom login policy is configured. Otherwise the system will not be able to identify the identity provider. |
|
| `urn:zitadel:iam:user:resourceowner` | `urn:zitadel:iam:user:resourceowner` | By adding this scope, the resourceowner (id, name, primary_domain) of the user will be included in the token. |
|
||||||
|
| `urn:zitadel:iam:org:idp:id:{idp_id}` | `urn:zitadel:iam:org:idp:id:76625965177954913` | By adding this scope the user will directly be redirected to the identity provider to authenticate. Make sure you also send the primary domain scope if a custom login policy is configured. Otherwise the system will not be able to identify the identity provider. |
|
||||||
|
@@ -245,12 +245,8 @@ func (o *OPStorage) assertProjectRoleScopes(ctx context.Context, clientID string
|
|||||||
return scopes, nil
|
return scopes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OPStorage) assertClientScopesForPAT(ctx context.Context, token *model.TokenView, clientID string) error {
|
func (o *OPStorage) assertClientScopesForPAT(ctx context.Context, token *model.TokenView, clientID, projectID string) error {
|
||||||
token.Audience = append(token.Audience, clientID)
|
token.Audience = append(token.Audience, clientID)
|
||||||
projectID, err := o.query.ProjectIDFromClientID(ctx, clientID, false)
|
|
||||||
if err != nil {
|
|
||||||
return errors.ThrowPreconditionFailed(nil, "OIDC-AEG4d", "Errors.Internal")
|
|
||||||
}
|
|
||||||
projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(projectID)
|
projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal")
|
return errors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal")
|
||||||
|
@@ -25,13 +25,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ScopeProjectRolePrefix = "urn:zitadel:iam:org:project:role:"
|
ScopeProjectRolePrefix = "urn:zitadel:iam:org:project:role:"
|
||||||
ClaimProjectRoles = "urn:zitadel:iam:org:project:roles"
|
ScopeProjectsRoles = "urn:zitadel:iam:org:projects:roles"
|
||||||
ScopeUserMetaData = "urn:zitadel:iam:user:metadata"
|
ClaimProjectRoles = "urn:zitadel:iam:org:project:roles"
|
||||||
ClaimUserMetaData = ScopeUserMetaData
|
ClaimProjectRolesFormat = "urn:zitadel:iam:org:project:%s:roles"
|
||||||
ScopeResourceOwner = "urn:zitadel:iam:user:resourceowner"
|
ScopeUserMetaData = "urn:zitadel:iam:user:metadata"
|
||||||
ClaimResourceOwner = ScopeResourceOwner + ":"
|
ClaimUserMetaData = ScopeUserMetaData
|
||||||
ClaimActionLogFormat = "urn:zitadel:iam:action:%s:log"
|
ScopeResourceOwner = "urn:zitadel:iam:user:resourceowner"
|
||||||
|
ClaimResourceOwner = ScopeResourceOwner + ":"
|
||||||
|
ClaimActionLogFormat = "urn:zitadel:iam:action:%s:log"
|
||||||
|
|
||||||
oidcCtx = "oidc"
|
oidcCtx = "oidc"
|
||||||
)
|
)
|
||||||
@@ -130,7 +132,7 @@ func (o *OPStorage) SetUserinfoFromToken(ctx context.Context, userInfo *oidc.Use
|
|||||||
return errors.ThrowPermissionDenied(nil, "OIDC-da1f3", "origin is not allowed")
|
return errors.ThrowPermissionDenied(nil, "OIDC-da1f3", "origin is not allowed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return o.setUserinfo(ctx, userInfo, token.UserID, token.ApplicationID, token.Scopes)
|
return o.setUserinfo(ctx, userInfo, token.UserID, token.ApplicationID, token.Scopes, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OPStorage) SetUserinfoFromScopes(ctx context.Context, userInfo *oidc.UserInfo, userID, applicationID string, scopes []string) (err error) {
|
func (o *OPStorage) SetUserinfoFromScopes(ctx context.Context, userInfo *oidc.UserInfo, userID, applicationID string, scopes []string) (err error) {
|
||||||
@@ -148,7 +150,7 @@ func (o *OPStorage) SetUserinfoFromScopes(ctx context.Context, userInfo *oidc.Us
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return o.setUserinfo(ctx, userInfo, userID, applicationID, scopes)
|
return o.setUserinfo(ctx, userInfo, userID, applicationID, scopes, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) error {
|
func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) error {
|
||||||
@@ -161,7 +163,7 @@ func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection
|
|||||||
return errors.ThrowPermissionDenied(nil, "OIDC-Adfg5", "client not found")
|
return errors.ThrowPermissionDenied(nil, "OIDC-Adfg5", "client not found")
|
||||||
}
|
}
|
||||||
if token.IsPAT {
|
if token.IsPAT {
|
||||||
err = o.assertClientScopesForPAT(ctx, token, clientID)
|
err = o.assertClientScopesForPAT(ctx, token, clientID, projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.ThrowPreconditionFailed(err, "OIDC-AGefw", "Errors.Internal")
|
return errors.ThrowPreconditionFailed(err, "OIDC-AGefw", "Errors.Internal")
|
||||||
}
|
}
|
||||||
@@ -169,7 +171,7 @@ func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection
|
|||||||
for _, aud := range token.Audience {
|
for _, aud := range token.Audience {
|
||||||
if aud == clientID || aud == projectID {
|
if aud == clientID || aud == projectID {
|
||||||
userInfo := new(oidc.UserInfo)
|
userInfo := new(oidc.UserInfo)
|
||||||
err := o.setUserinfo(ctx, userInfo, subject, clientID, token.Scopes)
|
err := o.setUserinfo(ctx, userInfo, subject, clientID, token.Scopes, []string{projectID}) // always
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -254,13 +256,14 @@ func (o *OPStorage) checkOrgScopes(ctx context.Context, user *query.User, scopes
|
|||||||
return scopes, nil
|
return scopes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OPStorage) setUserinfo(ctx context.Context, userInfo *oidc.UserInfo, userID, applicationID string, scopes []string) (err error) {
|
func (o *OPStorage) setUserinfo(ctx context.Context, userInfo *oidc.UserInfo, userID, applicationID string, scopes []string, roleAudience []string) (err error) {
|
||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
user, err := o.query.GetUserByID(ctx, true, userID, false)
|
user, err := o.query.GetUserByID(ctx, true, userID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
var allRoles bool
|
||||||
roles := make([]string, 0)
|
roles := make([]string, 0)
|
||||||
for _, scope := range scopes {
|
for _, scope := range scopes {
|
||||||
switch scope {
|
switch scope {
|
||||||
@@ -282,6 +285,8 @@ func (o *OPStorage) setUserinfo(ctx context.Context, userInfo *oidc.UserInfo, us
|
|||||||
if err := o.setUserInfoResourceOwner(ctx, userInfo, userID); err != nil {
|
if err := o.setUserInfoResourceOwner(ctx, userInfo, userID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case ScopeProjectsRoles:
|
||||||
|
allRoles = true
|
||||||
default:
|
default:
|
||||||
if strings.HasPrefix(scope, ScopeProjectRolePrefix) {
|
if strings.HasPrefix(scope, ScopeProjectRolePrefix) {
|
||||||
roles = append(roles, strings.TrimPrefix(scope, ScopeProjectRolePrefix))
|
roles = append(roles, strings.TrimPrefix(scope, ScopeProjectRolePrefix))
|
||||||
@@ -298,14 +303,16 @@ func (o *OPStorage) setUserinfo(ctx context.Context, userInfo *oidc.UserInfo, us
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
userGrants, projectRoles, err := o.assertRoles(ctx, userID, applicationID, roles)
|
// if all roles are requested take the audience for those from the scopes
|
||||||
|
if allRoles && len(roleAudience) == 0 {
|
||||||
|
roleAudience = domain.AddAudScopeToAudience(ctx, roleAudience, scopes)
|
||||||
|
}
|
||||||
|
|
||||||
|
userGrants, projectRoles, err := o.assertRoles(ctx, userID, applicationID, roles, roleAudience)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
o.setUserInfoRoleClaims(userInfo, projectRoles)
|
||||||
if len(projectRoles) > 0 {
|
|
||||||
userInfo.AppendClaims(ClaimProjectRoles, projectRoles)
|
|
||||||
}
|
|
||||||
|
|
||||||
return o.userinfoFlows(ctx, user.ResourceOwner, userGrants, userInfo)
|
return o.userinfoFlows(ctx, user.ResourceOwner, userGrants, userInfo)
|
||||||
}
|
}
|
||||||
@@ -367,6 +374,17 @@ func (o *OPStorage) setUserInfoResourceOwner(ctx context.Context, userInfo *oidc
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *OPStorage) setUserInfoRoleClaims(userInfo *oidc.UserInfo, roles *projectsRoles) {
|
||||||
|
if roles != nil && len(roles.projects) > 0 {
|
||||||
|
if roles, ok := roles.projects[roles.requestProjectID]; ok {
|
||||||
|
userInfo.AppendClaims(ClaimProjectRoles, roles)
|
||||||
|
}
|
||||||
|
for projectID, roles := range roles.projects {
|
||||||
|
userInfo.AppendClaims(fmt.Sprintf(ClaimProjectRolesFormat, projectID), roles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (o *OPStorage) userinfoFlows(ctx context.Context, resourceOwner string, userGrants *query.UserGrants, userInfo *oidc.UserInfo) error {
|
func (o *OPStorage) userinfoFlows(ctx context.Context, resourceOwner string, userGrants *query.UserGrants, userInfo *oidc.UserInfo) error {
|
||||||
queriedActions, err := o.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeCustomiseToken, domain.TriggerTypePreUserinfoCreation, resourceOwner, false)
|
queriedActions, err := o.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeCustomiseToken, domain.TriggerTypePreUserinfoCreation, resourceOwner, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -494,6 +512,7 @@ func (o *OPStorage) userinfoFlows(ctx context.Context, resourceOwner string, use
|
|||||||
|
|
||||||
func (o *OPStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) {
|
func (o *OPStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) {
|
||||||
roles := make([]string, 0)
|
roles := make([]string, 0)
|
||||||
|
var allRoles bool
|
||||||
for _, scope := range scopes {
|
for _, scope := range scopes {
|
||||||
switch scope {
|
switch scope {
|
||||||
case ScopeUserMetaData:
|
case ScopeUserMetaData:
|
||||||
@@ -512,6 +531,8 @@ func (o *OPStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clie
|
|||||||
for claim, value := range resourceOwnerClaims {
|
for claim, value := range resourceOwnerClaims {
|
||||||
claims = appendClaim(claims, claim, value)
|
claims = appendClaim(claims, claim, value)
|
||||||
}
|
}
|
||||||
|
case ScopeProjectsRoles:
|
||||||
|
allRoles = true
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(scope, ScopeProjectRolePrefix) {
|
if strings.HasPrefix(scope, ScopeProjectRolePrefix) {
|
||||||
roles = append(roles, strings.TrimPrefix(scope, ScopeProjectRolePrefix))
|
roles = append(roles, strings.TrimPrefix(scope, ScopeProjectRolePrefix))
|
||||||
@@ -531,13 +552,25 @@ func (o *OPStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clie
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
userGrants, projectRoles, err := o.assertRoles(ctx, userID, clientID, roles)
|
// If requested, use the audience as context for the roles,
|
||||||
|
// otherwise the project itself will be used
|
||||||
|
var roleAudience []string
|
||||||
|
if allRoles {
|
||||||
|
roleAudience = domain.AddAudScopeToAudience(ctx, roleAudience, scopes)
|
||||||
|
}
|
||||||
|
|
||||||
|
userGrants, projectRoles, err := o.assertRoles(ctx, userID, clientID, roles, roleAudience)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(projectRoles) > 0 {
|
if projectRoles != nil && len(projectRoles.projects) > 0 {
|
||||||
claims = appendClaim(claims, ClaimProjectRoles, projectRoles)
|
if roles, ok := projectRoles.projects[projectRoles.requestProjectID]; ok {
|
||||||
|
claims = appendClaim(claims, ClaimProjectRoles, roles)
|
||||||
|
}
|
||||||
|
for projectID, roles := range projectRoles.projects {
|
||||||
|
claims = appendClaim(claims, fmt.Sprintf(ClaimProjectRolesFormat, projectID), roles)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return o.privateClaimsFlows(ctx, userID, userGrants, claims)
|
return o.privateClaimsFlows(ctx, userID, userGrants, claims)
|
||||||
@@ -662,35 +695,53 @@ func (o *OPStorage) privateClaimsFlows(ctx context.Context, userID string, userG
|
|||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OPStorage) assertRoles(ctx context.Context, userID, applicationID string, requestedRoles []string) (*query.UserGrants, map[string]map[string]string, error) {
|
func (o *OPStorage) assertRoles(ctx context.Context, userID, applicationID string, requestedRoles, roleAudience []string) (*query.UserGrants, *projectsRoles, error) {
|
||||||
if applicationID == "" || len(requestedRoles) == 0 {
|
if (applicationID == "" || len(requestedRoles) == 0) && len(roleAudience) == 0 {
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
projectID, err := o.query.ProjectIDFromClientID(ctx, applicationID, false)
|
projectID, err := o.query.ProjectIDFromClientID(ctx, applicationID, false)
|
||||||
|
// applicationID might contain a username (e.g. client credentials) -> ignore the not found
|
||||||
|
if err != nil && !errors.IsNotFound(err) {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
// ensure the projectID of the requesting is part of the roleAudience
|
||||||
|
if projectID != "" {
|
||||||
|
roleAudience = append(roleAudience, projectID)
|
||||||
|
}
|
||||||
|
queries := make([]query.SearchQuery, 0, 2)
|
||||||
|
projectQuery, err := query.NewUserGrantProjectIDsSearchQuery(roleAudience)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
projectQuery, err := query.NewUserGrantProjectIDSearchQuery(projectID)
|
queries = append(queries, projectQuery)
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
userIDQuery, err := query.NewUserGrantUserIDSearchQuery(userID)
|
userIDQuery, err := query.NewUserGrantUserIDSearchQuery(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
queries = append(queries, userIDQuery)
|
||||||
grants, err := o.query.UserGrants(ctx, &query.UserGrantsQueries{
|
grants, err := o.query.UserGrants(ctx, &query.UserGrantsQueries{
|
||||||
Queries: []query.SearchQuery{projectQuery, userIDQuery},
|
Queries: queries,
|
||||||
}, true, false)
|
}, true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
projectRoles := make(map[string]map[string]string)
|
roles := new(projectsRoles)
|
||||||
for _, requestedRole := range requestedRoles {
|
// if specific roles where requested, check if they are granted and append them in the roles list
|
||||||
for _, grant := range grants.UserGrants {
|
if len(requestedRoles) > 0 {
|
||||||
checkGrantedRoles(projectRoles, grant, requestedRole)
|
for _, requestedRole := range requestedRoles {
|
||||||
|
for _, grant := range grants.UserGrants {
|
||||||
|
checkGrantedRoles(roles, grant, requestedRole, grant.ProjectID == projectID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grants, roles, nil
|
||||||
|
}
|
||||||
|
// now specific roles were requested, so convert any grants into roles
|
||||||
|
for _, grant := range grants.UserGrants {
|
||||||
|
for _, role := range grant.Roles {
|
||||||
|
roles.Add(grant.ProjectID, role, grant.ResourceOwner, grant.OrgPrimaryDomain, grant.ProjectID == projectID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return grants, projectRoles, nil
|
return grants, roles, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OPStorage) assertUserMetaData(ctx context.Context, userID string) (map[string]string, error) {
|
func (o *OPStorage) assertUserMetaData(ctx context.Context, userID string) (map[string]string, error) {
|
||||||
@@ -722,21 +773,52 @@ func (o *OPStorage) assertUserResourceOwner(ctx context.Context, userID string)
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkGrantedRoles(roles map[string]map[string]string, grant *query.UserGrant, requestedRole string) {
|
func checkGrantedRoles(roles *projectsRoles, grant *query.UserGrant, requestedRole string, isRequested bool) {
|
||||||
for _, grantedRole := range grant.Roles {
|
for _, grantedRole := range grant.Roles {
|
||||||
if requestedRole == grantedRole {
|
if requestedRole == grantedRole {
|
||||||
appendRole(roles, grantedRole, grant.ResourceOwner, grant.OrgPrimaryDomain)
|
roles.Add(grant.ProjectID, grantedRole, grant.ResourceOwner, grant.OrgPrimaryDomain, isRequested)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendRole(roles map[string]map[string]string, role, orgID, orgPrimaryDomain string) {
|
// projectsRoles contains all projects with all their roles for a user
|
||||||
if roles[role] == nil {
|
type projectsRoles struct {
|
||||||
roles[role] = make(map[string]string, 0)
|
// key is projectID
|
||||||
}
|
projects map[string]projectRoles
|
||||||
roles[role][orgID] = orgPrimaryDomain
|
|
||||||
|
requestProjectID string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *projectsRoles) Add(projectID, roleKey, orgID, domain string, isRequested bool) {
|
||||||
|
if p.projects == nil {
|
||||||
|
p.projects = make(map[string]projectRoles, 1)
|
||||||
|
}
|
||||||
|
if p.projects[projectID] == nil {
|
||||||
|
p.projects[projectID] = make(projectRoles)
|
||||||
|
}
|
||||||
|
if isRequested {
|
||||||
|
p.requestProjectID = projectID
|
||||||
|
}
|
||||||
|
p.projects[projectID].Add(roleKey, orgID, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// projectRoles contains the roles of a project of multiple organisations
|
||||||
|
//
|
||||||
|
// key is the role key
|
||||||
|
type projectRoles map[string][]projectRole
|
||||||
|
|
||||||
|
func (p projectRoles) Add(roleKey, orgID, domain string) {
|
||||||
|
if len(p[roleKey]) == 0 {
|
||||||
|
p[roleKey] = make([]projectRole, 0, 1)
|
||||||
|
}
|
||||||
|
p[roleKey] = append(p[roleKey], projectRole{orgID: domain})
|
||||||
|
}
|
||||||
|
|
||||||
|
// projectRole contains all the organisations where a user is granted a certain role
|
||||||
|
//
|
||||||
|
// key is the org id, value the org domain
|
||||||
|
type projectRole map[string]string
|
||||||
|
|
||||||
func getGender(gender domain.Gender) oidc.Gender {
|
func getGender(gender domain.Gender) oidc.Gender {
|
||||||
switch gender {
|
switch gender {
|
||||||
case domain.GenderFemale:
|
case domain.GenderFemale:
|
||||||
|
@@ -112,10 +112,13 @@ func (c *Client) IsScopeAllowed(scope string) bool {
|
|||||||
if strings.HasPrefix(scope, domain.SelectIDPScope) {
|
if strings.HasPrefix(scope, domain.SelectIDPScope) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(scope, ScopeUserMetaData) {
|
if scope == ScopeUserMetaData {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(scope, ScopeResourceOwner) {
|
if scope == ScopeResourceOwner {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if scope == ScopeProjectsRoles {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
for _, allowedScope := range c.allowedScopes {
|
for _, allowedScope := range c.allowedScopes {
|
||||||
|
@@ -75,6 +75,14 @@ func NewUserGrantProjectIDSearchQuery(id string) (SearchQuery, error) {
|
|||||||
return NewTextQuery(UserGrantProjectID, id, TextEquals)
|
return NewTextQuery(UserGrantProjectID, id, TextEquals)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewUserGrantProjectIDsSearchQuery(ids []string) (SearchQuery, error) {
|
||||||
|
list := make([]interface{}, len(ids))
|
||||||
|
for i, value := range ids {
|
||||||
|
list[i] = value
|
||||||
|
}
|
||||||
|
return NewListQuery(UserGrantProjectID, list, ListIn)
|
||||||
|
}
|
||||||
|
|
||||||
func NewUserGrantProjectOwnerSearchQuery(id string) (SearchQuery, error) {
|
func NewUserGrantProjectOwnerSearchQuery(id string) (SearchQuery, error) {
|
||||||
return NewTextQuery(ProjectColumnResourceOwner, id, TextEquals)
|
return NewTextQuery(ProjectColumnResourceOwner, id, TextEquals)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user