mirror of
https://github.com/zitadel/zitadel.git
synced 2025-05-20 18:08:23 +00:00
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:
parent
c884a11f1b
commit
31a91a0039
@ -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. |
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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))
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user