feat: handle Zitadel provider IdP login via login v1 (#12469)

# Which Problems Are Solved

This PR adds support for using a ZITADEL IdP provider in the login v1 UI
flow and conditionally grants a support-user instance membership based
on a ZITADEL-specific project-roles token claim.

# How the Problems Are Solved

- Introduced ZITADEL IdP handling in the login v1 handler, including
extracting the `urn:zitadel:iam:org:project:roles` claim and granting
`IAM_OWNER_VIEWER` instance membership when configured orgs match.
- Added a new `AddInstanceMemberFromLogin` to assign an instance
membership during the login v1 ZITADEL-IdP flow without permission
checks (as the authorization is established based on the project roles
in the token claim)
- Added unit tests

# Additional Changes

N/A

# Additional Context
- Closes https://github.com/zitadel/zitadel/issues/12446
This commit is contained in:
gayathri
2026-07-28 15:20:46 +02:00
committed by GitHub
parent 318acd4bc3
commit ffd9b39bdb
14 changed files with 908 additions and 492 deletions
@@ -20,6 +20,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
http_utils "github.com/zitadel/zitadel/internal/api/http"
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/domain/federatedlogout"
@@ -36,6 +37,7 @@ import (
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
"github.com/zitadel/zitadel/internal/idp/providers/saml"
"github.com/zitadel/zitadel/internal/idp/providers/saml/requesttracker"
"github.com/zitadel/zitadel/internal/idp/providers/zitadel"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -46,6 +48,9 @@ const (
queryRelayState = "RelayState"
queryMethod = "method"
tmplExternalNotFoundOption = "externalnotfoundoption"
// zitadelProjectRolesClaim is the token claim a ZITADEL provider uses to convey
// a user's project roles ({role: {orgID: orgDomain}}).
zitadelProjectRolesClaim = "urn:zitadel:iam:org:project:roles"
)
var (
@@ -190,6 +195,8 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai
provider, err = l.ldapProvider(r.Context(), identityProvider)
case domain.IDPTypeSAML:
provider, err = l.samlProvider(r.Context(), identityProvider)
case domain.IDPTypeZitadel:
provider, err = l.zitadelProvider(r.Context(), identityProvider)
case domain.IDPTypeUnspecified:
fallthrough
default:
@@ -368,6 +375,13 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
return
}
case domain.IDPTypeZitadel:
provider, err := l.zitadelProvider(r.Context(), identityProvider)
if err != nil {
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
return
}
session = openid.NewSession(provider.Provider, data.Code, authReq.SelectedIDPConfigArgs)
case domain.IDPTypeJWT,
domain.IDPTypeLDAP,
domain.IDPTypeUnspecified:
@@ -459,6 +473,10 @@ func (l *Login) handleExternalUserAuthenticated(
callback func(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest),
) {
externalUser := mapIDPUserToExternalUser(user, provider.ID)
// a ZITADEL provider may convey support-user project roles, which are carried through to user creation
if provider.Type == domain.IDPTypeZitadel {
externalUser.ProjectRoles = projectRolesFromIDPUser(user)
}
// ensure the linked IDP is added to the login policy
if err := l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, provider.ID, authReq.AgentID, authReq.SelectedIDPConfigArgs); err != nil {
l.renderError(w, r, authReq, err)
@@ -516,6 +534,17 @@ func (l *Login) handleExternalUserAuthenticated(
return
}
}
// (re)assign the support-user instance membership for an existing user
// so that the role assignment remains idempotent
// in case user creation succeeded but role assignment failed in a previous request.
err = l.ensureSupportUserInstanceMembership(r, provider, authReq, externalUser)
if err != nil && !userLinked {
l.renderError(w, r, authReq, err)
return
}
// if a user was just linked we don't render further errors (see above),
// but a failed support-membership grant must still be observable, so logging it.
logging.WithFields("authReq", authReq.ID, "user", authReq.UserID).OnError(err).Error("unable to grant support user instance membership")
callback(w, r, authReq)
}
@@ -653,12 +682,12 @@ func (l *Login) createOrLinkUser(w http.ResponseWriter, r *http.Request, authReq
return
}
}
l.autoCreateExternalUser(w, r, authReq)
l.autoCreateExternalUser(w, r, provider, authReq)
return false
}
// autoCreateExternalUser takes the externalUser and creates it automatically (without user interaction)
func (l *Login) autoCreateExternalUser(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
func (l *Login) autoCreateExternalUser(w http.ResponseWriter, r *http.Request, provider *query.IDPTemplate, authReq *domain.AuthRequest) {
if len(authReq.LinkingUsers) == 0 {
l.renderError(w, r, authReq, zerrors.ThrowPreconditionFailed(nil, "LOGIN-asfg3", "Errors.ExternalIDP.NoExternalUserData"))
return
@@ -667,7 +696,7 @@ func (l *Login) autoCreateExternalUser(w http.ResponseWriter, r *http.Request, a
// TODO (LS): how do we get multiple and why do we use the last of them (taken as is)?
linkingUser := authReq.LinkingUsers[len(authReq.LinkingUsers)-1]
l.registerExternalUser(w, r, authReq, linkingUser)
l.registerExternalUser(w, r, provider, authReq, linkingUser)
}
// renderExternalNotFoundOption renders a page, where the user is able to edit the IDP data,
@@ -790,14 +819,18 @@ func (l *Login) handleExternalNotFoundOptionCheck(w http.ResponseWriter, r *http
return
}
linkingUser := mapExternalNotFoundOptionFormDataToLoginUser(data)
l.registerExternalUser(w, r, authReq, linkingUser)
// the form only carries the editable profile fields, so preserve the ZITADEL project
// roles captured at authentication (stored on the linking user) to keep the
// support-user instance membership grant working on the manual creation path.
preserveProjectRoles(linkingUser, authReq.LinkingUsers)
l.registerExternalUser(w, r, idpTemplate, authReq, linkingUser)
}
// registerExternalUser creates an externalUser with the provided data
// incl. execution of pre and post creation actions
//
// it is called from either the [autoCreateExternalUser] or [handleExternalNotFoundOptionCheck]
func (l *Login) registerExternalUser(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, externalUser *domain.ExternalUser) {
func (l *Login) registerExternalUser(w http.ResponseWriter, r *http.Request, provider *query.IDPTemplate, authReq *domain.AuthRequest, externalUser *domain.ExternalUser) {
resourceOwner := determineResourceOwner(r.Context(), authReq)
orgIamPolicy, err := l.getOrgDomainPolicy(r, resourceOwner)
@@ -833,9 +866,81 @@ func (l *Login) registerExternalUser(w http.ResponseWriter, r *http.Request, aut
l.renderError(w, r, authReq, err)
return
}
// a ZITADEL provider may grant a support user an instance membership based on the roles present in its token claim.
if err = l.ensureSupportUserInstanceMembership(r, provider, authReq, externalUser); err != nil {
l.renderError(w, r, authReq, err)
return
}
l.renderNextStep(w, r, authReq)
}
// ensureSupportUserInstanceMembership makes sure an external user holds the instance member
// roles conveyed by their `urn:zitadel:iam:org:project:roles` claim when they authenticated
// via a ZITADEL provider.
func (l *Login) ensureSupportUserInstanceMembership(r *http.Request, provider *query.IDPTemplate, authReq *domain.AuthRequest, externalUser *domain.ExternalUser) error {
roles := supportUserInstanceRoles(provider, externalUser)
if len(roles) == 0 {
return nil
}
member := &command.AddInstanceMember{
InstanceID: authz.GetInstance(r.Context()).InstanceID(),
UserID: authReq.UserID,
Roles: roles,
}
_, err := l.command.EnsureInstanceMemberRolesFromLogin(setContext(r.Context(), ""), member)
if err != nil && !zerrors.IsErrorAlreadyExists(err) {
return err
}
return nil
}
// supportUserInstanceRoles returns the instance member roles an external user must hold,
// derived from the IAM roles of their ZITADEL project roles claim. Only roles claimed for an
// organization configured in the IDP's InstanceRolesInfo are returned so that the set of
// organizations able to confer instance roles stays under the control of the instance admins.
func supportUserInstanceRoles(provider *query.IDPTemplate, externalUser *domain.ExternalUser) []string {
// instance-wide role grants may only originate from an instance-scoped IDP.
// An organization-scoped provider must never confer instance membership, even if
// its InstanceRolesInfo is somehow populated (e.g. stale data): honoring it would
// let an org owner escalate themselves to instance-level roles.
if provider.OwnerType != domain.IdentityProviderTypeSystem {
return nil
}
// only a ZITADEL provider conveys support-user project roles
if provider.Type != domain.IDPTypeZitadel || provider.ZitadelIDPTemplate == nil {
return nil
}
var roles []string
for role, claimOrgs := range externalUser.ProjectRoles {
// only an IAM role can be conferred by an instance membership
if !strings.HasPrefix(role, domain.IAMRolePrefix) {
continue
}
// a claim org must be the same as a configured organization (ID and domain)
if !claimMatchesConfiguredOrg(claimOrgs, provider.ZitadelIDPTemplate) {
continue
}
roles = append(roles, role)
}
// the claim is a map, so sort to keep the resulting role set deterministic
slices.Sort(roles)
return roles
}
// claimMatchesConfiguredOrg reports whether any organization from the roles claim
// (orgID → orgDomain) exactly matches an organization (by ID and domain)
// configured in the ZITADEL IDP's InstanceRolesInfo.
func claimMatchesConfiguredOrg(claimOrgs map[string]string, zitadelTemplate *query.ZitadelIDPTemplate) bool {
for orgID, orgDomain := range claimOrgs {
for _, configured := range zitadelTemplate.InstanceRolesInfo {
if configured.OrganizationID == orgID && configured.OrganizationDomain == orgDomain {
return true
}
}
}
return false
}
// updateExternalUser will update the existing user (email, phone, profile) with data provided by the IDP
func (l *Login) updateExternalUser(ctx context.Context, authReq *domain.AuthRequest, externalUser *domain.ExternalUser) error {
user, err := l.query.GetUserByID(ctx, true, authReq.UserID)
@@ -1290,6 +1395,22 @@ func (l *Login) appleProvider(ctx context.Context, identityProvider *query.IDPTe
)
}
func (l *Login) zitadelProvider(ctx context.Context, identityProvider *query.IDPTemplate) (*zitadel.Provider, error) {
secret, err := crypto.DecryptString(identityProvider.ZitadelIDPTemplate.ClientSecret, l.idpConfigAlg)
if err != nil {
return nil, err
}
return zitadel.New(
identityProvider.ZitadelIDPTemplate.Issuer,
identityProvider.ZitadelIDPTemplate.ClientID,
secret,
l.baseURL(ctx)+EndpointExternalLoginCallback,
identityProvider.ZitadelIDPTemplate.Scopes,
l.httpClient,
openid.WithSelectAccount(),
)
}
func (l *Login) appendUserGrants(ctx context.Context, userGrants []*domain.UserGrant, resourceOwner string) error {
if len(userGrants) == 0 {
return nil
@@ -1629,3 +1750,51 @@ func WrapIdPError(err error) *IdPError {
}
return &IdPError{err: zerrors.CreateZitadelError(zerrors.KindPreconditionFailed, err, id, "Errors.User.ExternalIDP.LoginFailedSwitchLocal", 1)}
}
// projectRolesFromIDPUser extracts the ZITADEL project-roles claim from an IDP
// user's OIDC userinfo and returns it as a map {role: {orgID: orgDomain}}.
// It returns nil when the user is not an OIDC user or the claim is absent or malformed.
func projectRolesFromIDPUser(user idp.User) map[string]map[string]string {
oidcUser, ok := user.(*openid.User)
if !ok || oidcUser.UserInfo == nil {
return nil
}
rawRoles, ok := oidcUser.Claims[zitadelProjectRolesClaim].(map[string]any)
if !ok {
return nil
}
roles := make(map[string]map[string]string, len(rawRoles))
for role, rawOrgs := range rawRoles {
orgs, ok := rawOrgs.(map[string]any)
if !ok {
continue
}
orgMap := make(map[string]string, len(orgs))
for orgID, rawDomain := range orgs {
// skip entries whose domain is not a string.
domainName, ok := rawDomain.(string)
if !ok {
continue
}
orgMap[orgID] = domainName
}
if len(orgMap) == 0 {
continue
}
roles[role] = orgMap
}
if len(roles) == 0 {
return nil
}
return roles
}
// preserveProjectRoles carries the ZITADEL project roles captured at authentication
// onto the form-mapped external user. The "external user not found" form only submits profile fields.
// The roles claim is restored from the most recent linking user stored on the auth request.
func preserveProjectRoles(user *domain.ExternalUser, linkingUsers []*domain.ExternalUser) {
if user == nil || len(linkingUsers) == 0 {
return
}
user.ProjectRoles = linkingUsers[len(linkingUsers)-1].ProjectRoles
}
@@ -3,8 +3,14 @@ package login
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
"github.com/zitadel/zitadel/internal/query"
provideridp "github.com/zitadel/zitadel/internal/repository/idp"
)
func Test_hasEmailChanged(t *testing.T) {
@@ -284,3 +290,312 @@ func Test_hasPhoneChanged(t *testing.T) {
})
}
}
func Test_projectRolesFromIDPUser(t *testing.T) {
tests := []struct {
name string
user idp.User
want map[string]map[string]string
}{
{
"not an oidc user",
struct{ idp.User }{},
nil,
},
{
"no claims",
openid.NewUser(&oidc.UserInfo{Claims: nil}),
nil,
},
{
"claim absent",
openid.NewUser(&oidc.UserInfo{Claims: map[string]any{"some": "other"}}),
nil,
},
{
"claim wrong type",
openid.NewUser(&oidc.UserInfo{Claims: map[string]any{zitadelProjectRolesClaim: "not-a-map"}}),
nil,
},
{
"role with non-map orgs is skipped",
openid.NewUser(&oidc.UserInfo{Claims: map[string]any{
zitadelProjectRolesClaim: map[string]any{
"IAM_OWNER_VIEWER": "not-a-map",
}},
}),
nil,
},
{
"single role and org",
openid.NewUser(&oidc.UserInfo{Claims: map[string]any{
zitadelProjectRolesClaim: map[string]any{
"IAM_OWNER_VIEWER": map[string]any{
"orgID1": "org1.example.com",
},
}},
}),
map[string]map[string]string{
"IAM_OWNER_VIEWER": {"orgID1": "org1.example.com"},
},
},
{
"multiple roles and orgs",
openid.NewUser(&oidc.UserInfo{Claims: map[string]any{
zitadelProjectRolesClaim: map[string]any{
"IAM_OWNER_VIEWER": map[string]any{
"orgID1": "org1.example.com",
"orgID2": "org2.example.com",
},
"OTHER_ROLE": map[string]any{
"orgID3": "org3.example.com",
},
}},
}),
map[string]map[string]string{
"IAM_OWNER_VIEWER": {"orgID1": "org1.example.com", "orgID2": "org2.example.com"},
"OTHER_ROLE": {"orgID3": "org3.example.com"},
},
},
{
"non-string org domain is skipped",
openid.NewUser(&oidc.UserInfo{Claims: map[string]any{
zitadelProjectRolesClaim: map[string]any{
"IAM_OWNER_VIEWER": map[string]any{
"orgID1": 55,
},
}},
}),
nil,
},
{
"role keeps only orgs with string domains",
openid.NewUser(&oidc.UserInfo{Claims: map[string]any{
zitadelProjectRolesClaim: map[string]any{
"IAM_OWNER_VIEWER": map[string]any{
"orgID1": "org1.example.com",
"orgID2": 55,
},
}},
}),
map[string]map[string]string{
"IAM_OWNER_VIEWER": {"orgID1": "org1.example.com"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, projectRolesFromIDPUser(tt.user))
})
}
}
func Test_claimMatchesConfiguredOrg(t *testing.T) {
tests := []struct {
name string
claimOrgs map[string]string
template *query.ZitadelIDPTemplate
want bool
}{
{
"id and domain match",
map[string]string{"orgID1": "org1.example.com"},
&query.ZitadelIDPTemplate{InstanceRolesInfo: []provideridp.RolesInfo{{OrganizationID: "orgID1", OrganizationDomain: "org1.example.com"}}},
true,
},
{
"id matches, domain differs",
map[string]string{"orgID1": "evil.example.com"},
&query.ZitadelIDPTemplate{InstanceRolesInfo: []provideridp.RolesInfo{{OrganizationID: "orgID1", OrganizationDomain: "org1.example.com"}}},
false,
},
{
"domain matches, id differs",
map[string]string{"otherID": "org1.example.com"},
&query.ZitadelIDPTemplate{InstanceRolesInfo: []provideridp.RolesInfo{{OrganizationID: "orgID1", OrganizationDomain: "org1.example.com"}}},
false,
},
{
"neither id nor domain match",
map[string]string{"orgIDx": "orgx.example.com"},
&query.ZitadelIDPTemplate{InstanceRolesInfo: []provideridp.RolesInfo{{OrganizationID: "orgID1", OrganizationDomain: "org1.example.com"}}},
false,
},
{
"no configured orgs",
map[string]string{"orgID1": "org1.example.com"},
&query.ZitadelIDPTemplate{},
false,
},
{
"no claim orgs",
map[string]string{},
&query.ZitadelIDPTemplate{InstanceRolesInfo: []provideridp.RolesInfo{{OrganizationID: "orgID1", OrganizationDomain: "org1.example.com"}}},
false,
},
{
"one of multiple claim orgs matches",
map[string]string{"orgIDx": "orgx.example.com", "orgID1": "org1.example.com"},
&query.ZitadelIDPTemplate{InstanceRolesInfo: []provideridp.RolesInfo{{OrganizationID: "orgID1", OrganizationDomain: "org1.example.com"}}},
true,
},
{
"one of multiple configured orgs matches",
map[string]string{"orgID2": "org2.example.com"},
&query.ZitadelIDPTemplate{InstanceRolesInfo: []provideridp.RolesInfo{
{OrganizationID: "orgID1", OrganizationDomain: "org1.example.com"},
{OrganizationID: "orgID2", OrganizationDomain: "org2.example.com"},
}},
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, claimMatchesConfiguredOrg(tt.claimOrgs, tt.template))
})
}
}
func Test_preserveProjectRoles(t *testing.T) {
roles := map[string]map[string]string{
"IAM_OWNER_VIEWER": {"orgID1": "org1.example.com"},
}
tests := []struct {
name string
user *domain.ExternalUser
linkingUsers []*domain.ExternalUser
want map[string]map[string]string
}{
{
// the form-mapped user has no roles claim; it must be restored from the linking user
name: "roles copied from the single linking user",
user: &domain.ExternalUser{},
linkingUsers: []*domain.ExternalUser{{ProjectRoles: roles}},
want: roles,
},
{
// linking users accumulate on account-linking flows; the most recent one is mapped
name: "roles copied from the most recent linking user",
user: &domain.ExternalUser{},
linkingUsers: []*domain.ExternalUser{
{ProjectRoles: map[string]map[string]string{"OTHER_ROLE": {"orgIDx": "orgx.example.com"}}},
{ProjectRoles: roles},
},
want: roles,
},
{
name: "no linking users, nil",
user: &domain.ExternalUser{},
linkingUsers: nil,
want: nil,
},
{
name: "last linking user without roles, nil",
user: &domain.ExternalUser{},
linkingUsers: []*domain.ExternalUser{{ProjectRoles: roles}, {}},
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
preserveProjectRoles(tt.user, tt.linkingUsers)
assert.Equal(t, tt.want, tt.user.ProjectRoles)
})
}
}
func Test_supportUserInstanceRoles(t *testing.T) {
configured := &query.ZitadelIDPTemplate{
InstanceRolesInfo: []provideridp.RolesInfo{{OrganizationID: "orgID1", OrganizationDomain: "org1.example.com"}},
}
supportClaim := map[string]map[string]string{
domain.RoleIAMOwnerViewer: {"orgID1": "org1.example.com"},
}
tests := []struct {
name string
provider *query.IDPTemplate
externalUser *domain.ExternalUser
want []string
}{
{
name: "instance zitadel provider, support role claim, matching org",
provider: &query.IDPTemplate{OwnerType: domain.IdentityProviderTypeSystem, Type: domain.IDPTypeZitadel, ZitadelIDPTemplate: configured},
externalUser: &domain.ExternalUser{ProjectRoles: supportClaim},
want: []string{domain.RoleIAMOwnerViewer},
},
{
name: "multiple IAM roles claimed, all returned sorted",
provider: &query.IDPTemplate{Type: domain.IDPTypeZitadel, ZitadelIDPTemplate: configured},
externalUser: &domain.ExternalUser{ProjectRoles: map[string]map[string]string{
domain.RoleIAMOwnerViewer: {"orgID1": "org1.example.com"},
domain.RoleIAMOwner: {"orgID1": "org1.example.com"},
}},
want: []string{domain.RoleIAMOwner, domain.RoleIAMOwnerViewer},
},
{
name: "only the roles of a configured org are returned",
provider: &query.IDPTemplate{Type: domain.IDPTypeZitadel, ZitadelIDPTemplate: configured},
externalUser: &domain.ExternalUser{ProjectRoles: map[string]map[string]string{
domain.RoleIAMOwnerViewer: {"orgID1": "org1.example.com"},
domain.RoleIAMOwner: {"orgID2": "org2.example.com"},
}},
want: []string{domain.RoleIAMOwnerViewer},
},
{
name: "non-IAM roles are not returned",
provider: &query.IDPTemplate{Type: domain.IDPTypeZitadel, ZitadelIDPTemplate: configured},
externalUser: &domain.ExternalUser{ProjectRoles: map[string]map[string]string{
domain.RoleIAMOwnerViewer: {"orgID1": "org1.example.com"},
domain.RoleOrgOwner: {"orgID1": "org1.example.com"},
}},
want: []string{domain.RoleIAMOwnerViewer},
},
{
name: "org-scoped zitadel provider, matching org, skipped",
provider: &query.IDPTemplate{OwnerType: domain.IdentityProviderTypeOrg, Type: domain.IDPTypeZitadel, ZitadelIDPTemplate: configured},
externalUser: &domain.ExternalUser{ProjectRoles: supportClaim},
want: nil,
},
{
name: "non-zitadel provider, skipped",
provider: &query.IDPTemplate{Type: domain.IDPTypeOIDC, ZitadelIDPTemplate: configured},
externalUser: &domain.ExternalUser{ProjectRoles: supportClaim},
want: nil,
},
{
name: "zitadel type but no zitadel template, skipped",
provider: &query.IDPTemplate{Type: domain.IDPTypeZitadel},
externalUser: &domain.ExternalUser{ProjectRoles: supportClaim},
want: nil,
},
{
name: "no roles claim at all, skipped",
provider: &query.IDPTemplate{Type: domain.IDPTypeZitadel, ZitadelIDPTemplate: configured},
externalUser: &domain.ExternalUser{},
want: nil,
},
{
name: "roles claim without an IAM role, skipped",
provider: &query.IDPTemplate{Type: domain.IDPTypeZitadel, ZitadelIDPTemplate: configured},
externalUser: &domain.ExternalUser{ProjectRoles: map[string]map[string]string{"OTHER_ROLE": {"orgID1": "org1.example.com"}}},
want: nil,
},
{
name: "support role present but empty org set, skipped",
provider: &query.IDPTemplate{Type: domain.IDPTypeZitadel, ZitadelIDPTemplate: configured},
externalUser: &domain.ExternalUser{ProjectRoles: map[string]map[string]string{domain.RoleIAMOwnerViewer: {}}},
want: nil,
},
{
name: "support role claim for a non-configured org, skipped",
provider: &query.IDPTemplate{Type: domain.IDPTypeZitadel, ZitadelIDPTemplate: configured},
externalUser: &domain.ExternalUser{ProjectRoles: map[string]map[string]string{domain.RoleIAMOwnerViewer: {"orgID1": "evil.example.com"}}},
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, supportUserInstanceRoles(tt.provider, tt.externalUser))
})
}
}