feat: add resource owner scope / claim (#2274)

* feat: add resource owner scope / claime

* fix: private claimes

* fix: private claims

* fix: add claim description

* Update claims.md

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
This commit is contained in:
Fabi 2021-08-31 11:49:31 +02:00 committed by GitHub
parent c884a11f1b
commit 31a91a0039
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 119 additions and 45 deletions

View File

@ -6,7 +6,7 @@ ZITADEL asserts claims on different places according to the corresponding specif
Please check below the matrix for an overview where which scope is asserted. Please check below the matrix for an overview where which scope is asserted.
| Claims | Userinfo | Introspection | ID Token | Access Token | | Claims | Userinfo | Introspection | ID Token | Access Token |
|:------------------------------------------------|:---------------|----------------|---------------------------------------------|--------------------------------------| |:--------------------------------------------------|:---------------|----------------|---------------------------------------------|--------------------------------------|
| acr | No | No | Yes | No | | acr | No | No | Yes | No |
| address | When requested | When requested | When requested amd response_type `id_token` | No | | address | When requested | When requested | When requested amd response_type `id_token` | No |
| amr | No | No | Yes | No | | amr | No | No | Yes | No |
@ -30,6 +30,10 @@ Please check below the matrix for an overview where which scope is asserted.
| sub | Yes | Yes | Yes | When JWT | | sub | Yes | Yes | Yes | When JWT |
| urn:zitadel:iam:org:domain:primary:{domainname} | When requested | When requested | When requested | When JWT and requested | | urn:zitadel:iam:org:domain:primary:{domainname} | When requested | When requested | When requested | When JWT and requested |
| urn:zitadel:iam:org:project:roles:{rolename} | When requested | When requested | When requested or configured | When JWT and requested or configured | | urn:zitadel:iam:org:project:roles:{rolename} | When requested | When requested | When requested or configured | When JWT and requested or configured |
| urn:zitadel:iam:user:metadata | When requested | When requested | When requested | When JWT and requested |
| urn:zitadel:iam:user:resourceowner:id | When requested | When requested | When requested | When JWT and requested |
| urn:zitadel:iam:user:resourceowner:name | When requested | When requested | When requested | When JWT and requested |
| urn:zitadel:iam:user:resourceowner:primary_domain | When requested | When requested | When requested | When JWT and requested |
## Standard Claims ## Standard Claims
@ -65,7 +69,11 @@ Please check below the matrix for an overview where which scope is asserted.
ZITADEL reserves some claims to assert certain data. ZITADEL reserves some claims to assert certain data.
| Claims | Example | Description | | Claims | Example | Description |
|:------------------------------------------------|:-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |:--------------------------------------------------|:-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 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:{rolename} | `{"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:{rolename} | `{"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:roles:{rolename} | TBA | TBA | | urn:zitadel:iam:roles:{rolename} | TBA | TBA |
| 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: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: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. |

View File

@ -29,5 +29,6 @@ In addition to the standard compliant scopes we utilize the following scopes.
| urn:zitadel:iam:role:{rolename} | | | | urn:zitadel:iam:role:{rolename} | | |
| `urn:zitadel:iam:org:project:id:{projectid}:aud` | ZITADEL's Project id is `urn:zitadel:iam:org:project:id:69234237810729019:aud` | By adding this scope, the requested projectid will be added to the audience of the access and id token | | `urn:zitadel:iam:org:project:id:{projectid}:aud` | ZITADEL's Project id is `urn:zitadel:iam:org:project:id:69234237810729019:aud` | By adding this scope, the requested projectid will be added to the audience of the access and id 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: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: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. |
> If access to ZITADEL's API's is needed with a service user the scope `urn:zitadel:iam:org:project:id:69234237810729019:aud` needs to be used with the JWT Profile request > If access to ZITADEL's API's is needed with a service user the scope `urn:zitadel:iam:org:project:id:69234237810729019:aud` needs to be used with the JWT Profile request

View File

@ -27,6 +27,8 @@ const (
ClaimProjectRoles = "urn:zitadel:iam:org:project:roles" ClaimProjectRoles = "urn:zitadel:iam:org:project:roles"
ScopeUserMetaData = "urn:zitadel:iam:user:metadata" ScopeUserMetaData = "urn:zitadel:iam:user:metadata"
ClaimUserMetaData = ScopeUserMetaData ClaimUserMetaData = ScopeUserMetaData
ScopeResourceOwner = "urn:zitadel:iam:user:resourceowner"
ClaimResourceOwner = ScopeResourceOwner + ":"
oidcCtx = "oidc" oidcCtx = "oidc"
) )
@ -174,6 +176,23 @@ func (o *OPStorage) SetUserinfoFromScopes(ctx context.Context, userInfo oidc.Use
continue continue
} }
userInfo.SetAddress(oidc.NewUserInfoAddress(user.StreetAddress, user.Locality, user.Region, user.PostalCode, user.Country, "")) userInfo.SetAddress(oidc.NewUserInfoAddress(user.StreetAddress, user.Locality, user.Region, user.PostalCode, user.Country, ""))
case ScopeUserMetaData:
userMetaData, err := o.assertUserMetaData(ctx, userID)
if err != nil {
return err
}
if len(userMetaData) > 0 {
userInfo.AppendClaims(ClaimUserMetaData, userMetaData)
}
case ScopeResourceOwner:
resourceOwnerClaims, err := o.assertUserResourceOwner(ctx, userID)
if err != nil {
return err
}
for claim, value := range resourceOwnerClaims {
userInfo.AppendClaims(claim, value)
}
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))
@ -183,14 +202,6 @@ func (o *OPStorage) SetUserinfoFromScopes(ctx context.Context, userInfo oidc.Use
} }
} }
} }
userMetaData, err := o.assertUserMetaData(ctx, userID)
if err != nil {
return err
}
if len(userMetaData) > 0 {
userInfo.AppendClaims(ClaimUserMetaData, userMetaData)
}
if len(roles) == 0 || applicationID == "" { if len(roles) == 0 || applicationID == "" {
return nil return nil
} }
@ -230,6 +241,24 @@ func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection
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)
for _, scope := range scopes { for _, scope := range scopes {
switch scope {
case ScopeUserMetaData:
userMetaData, err := o.assertUserMetaData(ctx, userID)
if err != nil {
return nil, err
}
if len(userMetaData) > 0 {
claims = appendClaim(claims, ClaimUserMetaData, userMetaData)
}
case ScopeResourceOwner:
resourceOwnerClaims, err := o.assertUserResourceOwner(ctx, userID)
if err != nil {
return nil, err
}
for claim, value := range resourceOwnerClaims {
claims = appendClaim(claims, claim, value)
}
}
if strings.HasPrefix(scope, ScopeProjectRolePrefix) { if strings.HasPrefix(scope, ScopeProjectRolePrefix) {
roles = append(roles, strings.TrimPrefix(scope, ScopeProjectRolePrefix)) roles = append(roles, strings.TrimPrefix(scope, ScopeProjectRolePrefix))
} else if strings.HasPrefix(scope, model.OrgDomainPrimaryScope) { } else if strings.HasPrefix(scope, model.OrgDomainPrimaryScope) {
@ -246,13 +275,6 @@ func (o *OPStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clie
if len(projectRoles) > 0 { if len(projectRoles) > 0 {
claims = appendClaim(claims, ClaimProjectRoles, projectRoles) claims = appendClaim(claims, ClaimProjectRoles, projectRoles)
} }
userMetaData, err := o.assertUserMetaData(ctx, userID)
if err != nil {
return nil, err
}
if len(userMetaData) > 0 {
claims = appendClaim(claims, ClaimUserMetaData, userMetaData)
}
return claims, err return claims, err
} }
@ -287,6 +309,18 @@ func (o *OPStorage) assertUserMetaData(ctx context.Context, userID string) (map[
return userMetaData, nil return userMetaData, nil
} }
func (o *OPStorage) assertUserResourceOwner(ctx context.Context, userID string) (map[string]string, error) {
resourceOwner, err := o.repo.OrgByUserID(ctx, userID)
if err != nil {
return nil, err
}
return map[string]string{
ClaimResourceOwner + "id": resourceOwner.AggregateID,
ClaimResourceOwner + "name": resourceOwner.Name,
ClaimResourceOwner + "primary_domain": resourceOwner.PrimaryDomain,
}, nil
}
func checkGrantedRoles(roles map[string]map[string]string, grant *grant_model.UserGrantView, requestedRole string) { func checkGrantedRoles(roles map[string]map[string]string, grant *grant_model.UserGrantView, requestedRole string) {
for _, grantedRole := range grant.RoleKeys { for _, grantedRole := range grant.RoleKeys {
if requestedRole == grantedRole { if requestedRole == grantedRole {

View File

@ -109,6 +109,9 @@ func (c *Client) IsScopeAllowed(scope string) bool {
if strings.HasPrefix(scope, ScopeUserMetaData) { if strings.HasPrefix(scope, ScopeUserMetaData) {
return true return true
} }
if strings.HasPrefix(scope, ScopeResourceOwner) {
return true
}
for _, allowedScope := range c.allowedScopes { for _, allowedScope := range c.allowedScopes {
if scope == allowedScope { if scope == allowedScope {
return true return true

View File

@ -17,6 +17,7 @@ import (
iam_model "github.com/caos/zitadel/internal/iam/repository/view/model" iam_model "github.com/caos/zitadel/internal/iam/repository/view/model"
key_model "github.com/caos/zitadel/internal/key/model" key_model "github.com/caos/zitadel/internal/key/model"
key_view_model "github.com/caos/zitadel/internal/key/repository/view/model" key_view_model "github.com/caos/zitadel/internal/key/repository/view/model"
org_model "github.com/caos/zitadel/internal/org/repository/view/model"
"github.com/caos/zitadel/internal/telemetry/tracing" "github.com/caos/zitadel/internal/telemetry/tracing"
"github.com/caos/zitadel/internal/user/model" "github.com/caos/zitadel/internal/user/model"
usr_view "github.com/caos/zitadel/internal/user/repository/view" usr_view "github.com/caos/zitadel/internal/user/repository/view"
@ -307,6 +308,18 @@ func (repo *UserRepo) GetMyMetadataByKey(ctx context.Context, key string) (*doma
return iam_model.MetadataViewToDomain(data), nil return iam_model.MetadataViewToDomain(data), nil
} }
func (repo *UserRepo) OrgByUserID(ctx context.Context, userID string) (*domain.Org, error) {
user, err := repo.View.UserByID(userID)
if err != nil {
return nil, err
}
org, err := repo.View.OrgByID(user.ResourceOwner)
if err != nil {
return nil, err
}
return org_model.OrgToDomain(org), nil
}
func (repo *UserRepo) SearchUserMetadata(ctx context.Context, userID string) (*domain.MetadataSearchResponse, error) { func (repo *UserRepo) SearchUserMetadata(ctx context.Context, userID string) (*domain.MetadataSearchResponse, error) {
req := new(domain.MetadataSearchRequest) req := new(domain.MetadataSearchRequest)
return repo.searchUserMetadata(userID, "", req) return repo.searchUserMetadata(userID, "", req)

View File

@ -23,6 +23,7 @@ type UserRepository interface {
SearchUsers(ctx context.Context, request *model.UserSearchRequest) (*model.UserSearchResponse, error) SearchUsers(ctx context.Context, request *model.UserSearchRequest) (*model.UserSearchResponse, error)
SearchUserMetadata(ctx context.Context, userID string) (*domain.MetadataSearchResponse, error) SearchUserMetadata(ctx context.Context, userID string) (*domain.MetadataSearchResponse, error)
OrgByUserID(ctx context.Context, userID string) (*domain.Org, error)
} }
type myUserRepo interface { type myUserRepo interface {

View File

@ -2,6 +2,7 @@ package model
import ( import (
"encoding/json" "encoding/json"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/org/repository/eventsourcing/model" "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
"time" "time"
@ -55,6 +56,19 @@ func OrgToModel(org *OrgView) *org_model.OrgView {
} }
} }
func OrgToDomain(org *OrgView) *domain.Org {
return &domain.Org{
ObjectRoot: es_models.ObjectRoot{
AggregateID: org.ID,
ChangeDate: org.ChangeDate,
CreationDate: org.CreationDate,
Sequence: org.Sequence,
},
Name: org.Name,
PrimaryDomain: org.Domain,
}
}
func OrgsToModel(orgs []*OrgView) []*org_model.OrgView { func OrgsToModel(orgs []*OrgView) []*org_model.OrgView {
modelOrgs := make([]*org_model.OrgView, len(orgs)) modelOrgs := make([]*org_model.OrgView, len(orgs))