feat: initial admin PAT has IAM_LOGIN_CLIENT (#10143)

# Which Problems Are Solved

We provide a seamless way to initialize Zitadel and the login together.

# How the Problems Are Solved

Additionally to the `IAM_OWNER` role, a set up admin user also gets the
`IAM_LOGIN_CLIENT` role if it is a machine user with a PAT.

# Additional Changes

- Simplifies the load balancing example, as the intermediate
configuration step is not needed anymore.

# Additional Context

- Depends on #10116 
- Contributes to https://github.com/zitadel/zitadel-charts/issues/332
- Contributes to https://github.com/zitadel/zitadel/issues/10016

---------

Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
This commit is contained in:
Elio Bischof
2025-07-02 11:14:36 +02:00
committed by GitHub
parent 2928c6ac2b
commit a02a534cd2
14 changed files with 337 additions and 104 deletions

View File

@@ -839,6 +839,13 @@ DefaultInstance:
Pat: Pat:
# date format: 2023-01-01T00:00:00Z # date format: 2023-01-01T00:00:00Z
ExpirationDate: # ZITADEL_DEFAULTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE ExpirationDate: # ZITADEL_DEFAULTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE
LoginClient:
Machine:
Username: # ZITADEL_DEFAULTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME
Name: # ZITADEL_DEFAULTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME
Pat:
# date format: 2023-01-01T00:00:00Z
ExpirationDate: # ZITADEL_DEFAULTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE
SecretGenerators: SecretGenerators:
ClientSecret: ClientSecret:
Length: 64 # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_CLIENTSECRET_LENGTH Length: 64 # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_CLIENTSECRET_LENGTH

View File

@@ -20,12 +20,13 @@ import (
) )
type FirstInstance struct { type FirstInstance struct {
InstanceName string InstanceName string
DefaultLanguage language.Tag DefaultLanguage language.Tag
Org command.InstanceOrgSetup Org command.InstanceOrgSetup
MachineKeyPath string MachineKeyPath string
PatPath string PatPath string
Features *command.InstanceFeatures LoginClientPatPath string
Features *command.InstanceFeatures
Skip bool Skip bool
@@ -121,16 +122,18 @@ func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error
} }
} }
_, token, key, _, err := cmd.SetUpInstance(ctx, &mig.instanceSetup) _, token, key, loginClientToken, _, err := cmd.SetUpInstance(ctx, &mig.instanceSetup)
if err != nil { if err != nil {
return err return err
} }
if mig.instanceSetup.Org.Machine != nil && if (mig.instanceSetup.Org.Machine != nil &&
((mig.instanceSetup.Org.Machine.Pat != nil && token == "") || ((mig.instanceSetup.Org.Machine.Pat != nil && token == "") ||
(mig.instanceSetup.Org.Machine.MachineKey != nil && key == nil)) { (mig.instanceSetup.Org.Machine.MachineKey != nil && key == nil))) ||
(mig.instanceSetup.Org.LoginClient != nil &&
(mig.instanceSetup.Org.LoginClient.Pat != nil && loginClientToken == "")) {
return err return err
} }
return mig.outputMachineAuthentication(key, token) return mig.outputMachineAuthentication(key, token, loginClientToken)
} }
func (mig *FirstInstance) verifyEncryptionKeys(ctx context.Context) (*crypto_db.Database, error) { func (mig *FirstInstance) verifyEncryptionKeys(ctx context.Context) (*crypto_db.Database, error) {
@@ -150,7 +153,7 @@ func (mig *FirstInstance) verifyEncryptionKeys(ctx context.Context) (*crypto_db.
return keyStorage, nil return keyStorage, nil
} }
func (mig *FirstInstance) outputMachineAuthentication(key *command.MachineKey, token string) error { func (mig *FirstInstance) outputMachineAuthentication(key *command.MachineKey, token, loginClientToken string) error {
if key != nil { if key != nil {
keyDetails, err := key.Detail() keyDetails, err := key.Detail()
if err != nil { if err != nil {
@@ -165,6 +168,11 @@ func (mig *FirstInstance) outputMachineAuthentication(key *command.MachineKey, t
return err return err
} }
} }
if loginClientToken != "" {
if err := outputStdoutOrPath(mig.LoginClientPatPath, loginClientToken); err != nil {
return err
}
}
return nil return nil
} }

View File

@@ -6,6 +6,7 @@ FirstInstance:
MachineKeyPath: # ZITADEL_FIRSTINSTANCE_MACHINEKEYPATH MachineKeyPath: # ZITADEL_FIRSTINSTANCE_MACHINEKEYPATH
# The personal access token from the section FirstInstance.Org.Machine.Pat is written to the PatPath. # The personal access token from the section FirstInstance.Org.Machine.Pat is written to the PatPath.
PatPath: # ZITADEL_FIRSTINSTANCE_PATPATH PatPath: # ZITADEL_FIRSTINSTANCE_PATPATH
LoginClientPatPath: # ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH
InstanceName: ZITADEL # ZITADEL_FIRSTINSTANCE_INSTANCENAME InstanceName: ZITADEL # ZITADEL_FIRSTINSTANCE_INSTANCENAME
DefaultLanguage: en # ZITADEL_FIRSTINSTANCE_DEFAULTLANGUAGE DefaultLanguage: en # ZITADEL_FIRSTINSTANCE_DEFAULTLANGUAGE
Org: Org:
@@ -46,6 +47,13 @@ FirstInstance:
Pat: Pat:
# date format: 2023-01-01T00:00:00Z # date format: 2023-01-01T00:00:00Z
ExpirationDate: # ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE ExpirationDate: # ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE
LoginClient:
Machine:
Username: # ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME
Name: # ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME
Pat:
# date format: 2023-01-01T00:00:00Z
ExpirationDate: # ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE
CorrectCreationDate: CorrectCreationDate:
FailAfter: 5m # ZITADEL_CORRECTCREATIONDATE_FAILAFTER FailAfter: 5m # ZITADEL_CORRECTCREATIONDATE_FAILAFTER

View File

@@ -1 +1 @@
.env-file .env-file

View File

@@ -41,17 +41,17 @@ services:
user: root user: root
entrypoint: '/bin/sh' entrypoint: '/bin/sh'
command: command:
- -c - -c
- > - >
/app/zitadel setup /app/zitadel setup
--config /example-zitadel-config.yaml --config /example-zitadel-config.yaml
--config /example-zitadel-secrets.yaml --config /example-zitadel-secrets.yaml
--steps /example-zitadel-init-steps.yaml --steps /example-zitadel-init-steps.yaml
--masterkey ${ZITADEL_MASTERKEY} && --masterkey ${ZITADEL_MASTERKEY} &&
mv /pat /.env-file/pat || exit 0 && mv /pat /.env-file/pat || exit 0 &&
echo ZITADEL_SERVICE_USER_TOKEN=$(cat /.env-file/pat) > /.env-file/.env && echo ZITADEL_SERVICE_USER_TOKEN=$(cat /.env-file/pat) > /.env-file/.env &&
chown -R 1001:${GID} /.env-file && chown -R 1001:${GID} /.env-file &&
chmod -R 770 /.env-file chmod -R 770 /.env-file
environment: environment:
- GID - GID
depends_on: depends_on:
@@ -154,4 +154,4 @@ networks:
backend: backend:
volumes: volumes:
data: data:

View File

@@ -26,4 +26,4 @@ SAML.DefaultLoginURLV2: "/ui/v2/login/login?authRequest=" # ZITADEL_SAML_DEFAULT
LogStore.Access.Stdout.Enabled: true LogStore.Access.Stdout.Enabled: true
# Skipping the MFA init step allows us to immediately authenticate at the console # Skipping the MFA init step allows us to immediately authenticate at the console
DefaultInstance.LoginPolicy.MfaInitSkipLifetime: "0s" DefaultInstance.LoginPolicy.MfaInitSkipLifetime: "0s"

View File

@@ -9,4 +9,4 @@ FirstInstance:
Machine: Machine:
Username: 'login-container' Username: 'login-container'
Name: 'Login Container' Name: 'Login Container'
Pat.ExpirationDate: '2029-01-01T00:00:00Z' Pat.ExpirationDate: '2029-01-01T00:00:00Z'

View File

@@ -71,4 +71,4 @@ Open your favorite internet browser at https://127.0.0.1.sslip.io/ui/console?log
Your browser warns you about the insecure self-signed TLS certificate. As 127.0.0.1.sslip.io resolves to your localhost, you can safely proceed. Your browser warns you about the insecure self-signed TLS certificate. As 127.0.0.1.sslip.io resolves to your localhost, you can safely proceed.
Use the password *Password1!* to log in. Use the password *Password1!* to log in.
Read more about [the login process](/guides/integrate/login/oidc/login-users). Read more about [the login process](/guides/integrate/login/oidc/login-users).

View File

@@ -64,4 +64,4 @@ mv /tmp/zitadel-admin-sa.json $HOME/zitadel-admin-sa.json
This key can be used to provision resources with for example [Terraform](/docs/guides/manage/terraform-provider). This key can be used to provision resources with for example [Terraform](/docs/guides/manage/terraform-provider).
<Next components={props.components} /> <Next components={props.components} />
<Disclaimer components={props.components} /> <Disclaimer components={props.components} />

View File

@@ -40,7 +40,7 @@ func (s *Server) GetInstance(ctx context.Context, req *system_pb.GetInstanceRequ
} }
func (s *Server) AddInstance(ctx context.Context, req *system_pb.AddInstanceRequest) (*system_pb.AddInstanceResponse, error) { func (s *Server) AddInstance(ctx context.Context, req *system_pb.AddInstanceRequest) (*system_pb.AddInstanceResponse, error) {
id, _, _, details, err := s.command.SetUpInstance(ctx, AddInstancePbToSetupInstance(req, s.defaultInstance, s.externalDomain)) id, _, _, _, details, err := s.command.SetUpInstance(ctx, AddInstancePbToSetupInstance(req, s.defaultInstance, s.externalDomain))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -61,7 +61,7 @@ func (s *Server) UpdateInstance(ctx context.Context, req *system_pb.UpdateInstan
} }
func (s *Server) CreateInstance(ctx context.Context, req *system_pb.CreateInstanceRequest) (*system_pb.CreateInstanceResponse, error) { func (s *Server) CreateInstance(ctx context.Context, req *system_pb.CreateInstanceRequest) (*system_pb.CreateInstanceResponse, error) {
id, pat, key, details, err := s.command.SetUpInstance(ctx, CreateInstancePbToSetupInstance(req, s.defaultInstance, s.externalDomain)) id, pat, key, _, details, err := s.command.SetUpInstance(ctx, CreateInstancePbToSetupInstance(req, s.defaultInstance, s.externalDomain))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -217,33 +217,33 @@ func (s *InstanceSetup) generateIDs(idGenerator id.Generator) (err error) {
return err return err
} }
func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (string, string, *MachineKey, *domain.ObjectDetails, error) { func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (string, string, *MachineKey, string, *domain.ObjectDetails, error) {
if err := setup.generateIDs(c.idGenerator); err != nil { if err := setup.generateIDs(c.idGenerator); err != nil {
return "", "", nil, nil, err return "", "", nil, "", nil, err
} }
ctx = contextWithInstanceSetupInfo(ctx, setup.zitadel.instanceID, setup.zitadel.projectID, setup.zitadel.consoleAppID, c.externalDomain, setup.DefaultLanguage) ctx = contextWithInstanceSetupInfo(ctx, setup.zitadel.instanceID, setup.zitadel.projectID, setup.zitadel.consoleAppID, c.externalDomain, setup.DefaultLanguage)
validations, pat, machineKey, err := setUpInstance(ctx, c, setup) validations, pat, machineKey, loginClientPat, err := setUpInstance(ctx, c, setup)
if err != nil { if err != nil {
return "", "", nil, nil, err return "", "", nil, "", nil, err
} }
//nolint:staticcheck //nolint:staticcheck
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...)
if err != nil { if err != nil {
return "", "", nil, nil, err return "", "", nil, "", nil, err
} }
_, err = c.eventstore.Push(ctx, cmds...) _, err = c.eventstore.Push(ctx, cmds...)
if err != nil { if err != nil {
return "", "", nil, nil, err return "", "", nil, "", nil, err
} }
// RolePermissions need to be pushed in separate transaction. // RolePermissions need to be pushed in separate transaction.
// https://github.com/zitadel/zitadel/issues/9293 // https://github.com/zitadel/zitadel/issues/9293
details, err := c.SynchronizeRolePermission(ctx, setup.zitadel.instanceID, setup.RolePermissionMappings) details, err := c.SynchronizeRolePermission(ctx, setup.zitadel.instanceID, setup.RolePermissionMappings)
if err != nil { if err != nil {
return "", "", nil, nil, err return "", "", nil, "", nil, err
} }
details.ResourceOwner = setup.zitadel.orgID details.ResourceOwner = setup.zitadel.orgID
@@ -251,8 +251,12 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
if pat != nil { if pat != nil {
token = pat.Token token = pat.Token
} }
var loginClientToken string
if loginClientPat != nil {
loginClientToken = loginClientPat.Token
}
return setup.zitadel.instanceID, token, machineKey, details, nil return setup.zitadel.instanceID, token, machineKey, loginClientToken, details, nil
} }
func contextWithInstanceSetupInfo(ctx context.Context, instanceID, projectID, consoleAppID, externalDomain string, defaultLanguage language.Tag) context.Context { func contextWithInstanceSetupInfo(ctx context.Context, instanceID, projectID, consoleAppID, externalDomain string, defaultLanguage language.Tag) context.Context {
@@ -274,38 +278,38 @@ func contextWithInstanceSetupInfo(ctx context.Context, instanceID, projectID, co
) )
} }
func setUpInstance(ctx context.Context, c *Commands, setup *InstanceSetup) (validations []preparation.Validation, pat *PersonalAccessToken, machineKey *MachineKey, err error) { func setUpInstance(ctx context.Context, c *Commands, setup *InstanceSetup) (validations []preparation.Validation, pat *PersonalAccessToken, machineKey *MachineKey, loginClientPat *PersonalAccessToken, err error) {
instanceAgg := instance.NewAggregate(setup.zitadel.instanceID) instanceAgg := instance.NewAggregate(setup.zitadel.instanceID)
validations = setupInstanceElements(instanceAgg, setup) validations = setupInstanceElements(instanceAgg, setup)
// default organization on setup'd instance // default organization on setup'd instance
pat, machineKey, err = setupDefaultOrg(ctx, c, &validations, instanceAgg, setup.Org.Name, setup.Org.Machine, setup.Org.Human, setup.zitadel) pat, machineKey, loginClientPat, err = setupDefaultOrg(ctx, c, &validations, instanceAgg, setup.Org.Name, setup.Org.Machine, setup.Org.Human, setup.Org.LoginClient, setup.zitadel)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, nil, err
} }
// domains // domains
if err := setupGeneratedDomain(ctx, c, &validations, instanceAgg, setup.InstanceName); err != nil { if err := setupGeneratedDomain(ctx, c, &validations, instanceAgg, setup.InstanceName); err != nil {
return nil, nil, nil, err return nil, nil, nil, nil, err
} }
setupCustomDomain(c, &validations, instanceAgg, setup.CustomDomain) setupCustomDomain(c, &validations, instanceAgg, setup.CustomDomain)
// optional setting if set // optional setting if set
setupMessageTexts(&validations, setup.MessageTexts, instanceAgg) setupMessageTexts(&validations, setup.MessageTexts, instanceAgg)
if err := setupQuotas(c, &validations, setup.Quotas, setup.zitadel.instanceID); err != nil { if err := setupQuotas(c, &validations, setup.Quotas, setup.zitadel.instanceID); err != nil {
return nil, nil, nil, err return nil, nil, nil, nil, err
} }
setupSMTPSettings(c, &validations, setup.SMTPConfiguration, instanceAgg) setupSMTPSettings(c, &validations, setup.SMTPConfiguration, instanceAgg)
if err := setupWebKeys(c, &validations, setup.zitadel.instanceID, setup); err != nil { if err := setupWebKeys(c, &validations, setup.zitadel.instanceID, setup); err != nil {
return nil, nil, nil, err return nil, nil, nil, nil, err
} }
setupOIDCSettings(c, &validations, setup.OIDCSettings, instanceAgg) setupOIDCSettings(c, &validations, setup.OIDCSettings, instanceAgg)
setupFeatures(&validations, setup.Features, setup.zitadel.instanceID) setupFeatures(&validations, setup.Features, setup.zitadel.instanceID)
setupLimits(c, &validations, limits.NewAggregate(setup.zitadel.limitsID, setup.zitadel.instanceID), setup.Limits) setupLimits(c, &validations, limits.NewAggregate(setup.zitadel.limitsID, setup.zitadel.instanceID), setup.Limits)
setupRestrictions(c, &validations, restrictions.NewAggregate(setup.zitadel.restrictionsID, setup.zitadel.instanceID, setup.zitadel.instanceID), setup.Restrictions) setupRestrictions(c, &validations, restrictions.NewAggregate(setup.zitadel.restrictionsID, setup.zitadel.instanceID, setup.zitadel.instanceID), setup.Restrictions)
setupInstanceCreatedMilestone(&validations, setup.zitadel.instanceID) setupInstanceCreatedMilestone(&validations, setup.zitadel.instanceID)
return validations, pat, machineKey, nil return validations, pat, machineKey, loginClientPat, nil
} }
func setupInstanceElements(instanceAgg *instance.Aggregate, setup *InstanceSetup) []preparation.Validation { func setupInstanceElements(instanceAgg *instance.Aggregate, setup *InstanceSetup) []preparation.Validation {
@@ -572,8 +576,9 @@ func setupDefaultOrg(ctx context.Context,
name string, name string,
machine *AddMachine, machine *AddMachine,
human *AddHuman, human *AddHuman,
loginClient *AddLoginClient,
ids ZitadelConfig, ids ZitadelConfig,
) (pat *PersonalAccessToken, machineKey *MachineKey, err error) { ) (pat *PersonalAccessToken, machineKey *MachineKey, loginClientPat *PersonalAccessToken, err error) {
orgAgg := org.NewAggregate(ids.orgID) orgAgg := org.NewAggregate(ids.orgID)
*validations = append( *validations = append(
@@ -582,12 +587,12 @@ func setupDefaultOrg(ctx context.Context,
commands.prepareSetDefaultOrg(instanceAgg, ids.orgID), commands.prepareSetDefaultOrg(instanceAgg, ids.orgID),
) )
projectOwner, pat, machineKey, err := setupAdmins(commands, validations, instanceAgg, orgAgg, machine, human) projectOwner, pat, machineKey, loginClientPat, err := setupAdmins(commands, validations, instanceAgg, orgAgg, machine, human, loginClient)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, nil, err
} }
setupMinimalInterfaces(commands, validations, instanceAgg, orgAgg, projectOwner, ids) setupMinimalInterfaces(commands, validations, instanceAgg, orgAgg, projectOwner, ids)
return pat, machineKey, nil return pat, machineKey, loginClientPat, nil
} }
func setupAdmins(commands *Commands, func setupAdmins(commands *Commands,
@@ -596,21 +601,22 @@ func setupAdmins(commands *Commands,
orgAgg *org.Aggregate, orgAgg *org.Aggregate,
machine *AddMachine, machine *AddMachine,
human *AddHuman, human *AddHuman,
) (owner string, pat *PersonalAccessToken, machineKey *MachineKey, err error) { loginClient *AddLoginClient,
) (owner string, pat *PersonalAccessToken, machineKey *MachineKey, loginClientPat *PersonalAccessToken, err error) {
if human == nil && machine == nil { if human == nil && machine == nil {
return "", nil, nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-z1yi2q2ot7", "Error.Instance.NoAdmin") return "", nil, nil, nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-z1yi2q2ot7", "Error.Instance.NoAdmin")
} }
if machine != nil && machine.Machine != nil && !machine.Machine.IsZero() { if machine != nil && machine.Machine != nil && !machine.Machine.IsZero() {
machineUserID, err := commands.idGenerator.Next() machineUserID, err := commands.idGenerator.Next()
if err != nil { if err != nil {
return "", nil, nil, err return "", nil, nil, nil, err
} }
owner = machineUserID owner = machineUserID
pat, machineKey, err = setupMachineAdmin(commands, validations, machine, orgAgg.ID, machineUserID) pat, machineKey, err = setupMachineAdmin(commands, validations, machine, orgAgg.ID, machineUserID)
if err != nil { if err != nil {
return "", nil, nil, err return "", nil, nil, nil, err
} }
setupAdminMembers(commands, validations, instanceAgg, orgAgg, machineUserID) setupAdminMembers(commands, validations, instanceAgg, orgAgg, machineUserID)
@@ -618,7 +624,7 @@ func setupAdmins(commands *Commands,
if human != nil { if human != nil {
humanUserID, err := commands.idGenerator.Next() humanUserID, err := commands.idGenerator.Next()
if err != nil { if err != nil {
return "", nil, nil, err return "", nil, nil, nil, err
} }
owner = humanUserID owner = humanUserID
human.ID = humanUserID human.ID = humanUserID
@@ -629,7 +635,18 @@ func setupAdmins(commands *Commands,
setupAdminMembers(commands, validations, instanceAgg, orgAgg, humanUserID) setupAdminMembers(commands, validations, instanceAgg, orgAgg, humanUserID)
} }
return owner, pat, machineKey, nil if loginClient != nil {
loginClientUserID, err := commands.idGenerator.Next()
if err != nil {
return "", nil, nil, nil, err
}
loginClientPat, err = setupLoginClient(commands, validations, instanceAgg, loginClient, orgAgg.ID, loginClientUserID)
if err != nil {
return "", nil, nil, nil, err
}
}
return owner, pat, machineKey, loginClientPat, nil
} }
func setupMachineAdmin(commands *Commands, validations *[]preparation.Validation, machine *AddMachine, orgID, userID string) (pat *PersonalAccessToken, machineKey *MachineKey, err error) { func setupMachineAdmin(commands *Commands, validations *[]preparation.Validation, machine *AddMachine, orgID, userID string) (pat *PersonalAccessToken, machineKey *MachineKey, err error) {
@@ -655,6 +672,22 @@ func setupMachineAdmin(commands *Commands, validations *[]preparation.Validation
return pat, machineKey, nil return pat, machineKey, nil
} }
func setupLoginClient(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, loginClient *AddLoginClient, orgID, userID string) (pat *PersonalAccessToken, err error) {
*validations = append(*validations,
AddMachineCommand(user.NewAggregate(userID, orgID), loginClient.Machine),
commands.AddInstanceMemberCommand(instanceAgg, userID, domain.RoleIAMLoginClient),
)
if loginClient.Pat != nil {
pat = NewPersonalAccessToken(orgID, userID, loginClient.Pat.ExpirationDate, loginClient.Pat.Scopes, domain.UserTypeMachine)
pat.TokenID, err = commands.idGenerator.Next()
if err != nil {
return nil, err
}
*validations = append(*validations, prepareAddPersonalAccessToken(pat, commands.keyAlgorithm))
}
return pat, nil
}
func setupAdminMembers(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, orgAgg *org.Aggregate, userID string) { func setupAdminMembers(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, orgAgg *org.Aggregate, userID string) {
*validations = append(*validations, *validations = append(*validations,
commands.AddOrgMemberCommand(orgAgg, userID, domain.RoleOrgOwner), commands.AddOrgMemberCommand(orgAgg, userID, domain.RoleOrgOwner),

View File

@@ -129,7 +129,7 @@ func oidcAppEvents(ctx context.Context, orgID, projectID, id, name, clientID str
} }
} }
func orgFilters(orgID string, machine, human bool) []expect { func orgFilters(orgID string, machine, human, loginClient bool) []expect {
filters := []expect{ filters := []expect{
expectFilter(), expectFilter(),
expectFilter( expectFilter(
@@ -144,13 +144,17 @@ func orgFilters(orgID string, machine, human bool) []expect {
filters = append(filters, humanFilters(orgID)...) filters = append(filters, humanFilters(orgID)...)
filters = append(filters, adminMemberFilters(orgID, "USER")...) filters = append(filters, adminMemberFilters(orgID, "USER")...)
} }
if loginClient {
filters = append(filters, loginClientFilters(orgID, true)...)
filters = append(filters, instanceMemberFilters(orgID, "USER-LOGIN-CLIENT")...)
}
return append(filters, return append(filters,
projectFilters()..., projectFilters()...,
) )
} }
func orgEvents(ctx context.Context, instanceID, orgID, name, projectID, defaultDomain string, externalSecure bool, machine, human bool) []eventstore.Command { func orgEvents(ctx context.Context, instanceID, orgID, name, projectID, defaultDomain string, externalSecure bool, machine, human, loginClient bool) []eventstore.Command {
instanceAgg := instance.NewAggregate(instanceID) instanceAgg := instance.NewAggregate(instanceID)
orgAgg := org.NewAggregate(orgID) orgAgg := org.NewAggregate(orgID)
domain := strings.ToLower(name + "." + defaultDomain) domain := strings.ToLower(name + "." + defaultDomain)
@@ -173,13 +177,17 @@ func orgEvents(ctx context.Context, instanceID, orgID, name, projectID, defaultD
events = append(events, humanEvents(ctx, instanceID, orgID, userID)...) events = append(events, humanEvents(ctx, instanceID, orgID, userID)...)
owner = userID owner = userID
} }
if loginClient {
userID := "USER-LOGIN-CLIENT"
events = append(events, loginClientEvents(ctx, instanceID, orgID, userID, "LOGIN-CLIENT-PAT")...)
}
events = append(events, projectAddedEvents(ctx, instanceID, orgID, projectID, owner, externalSecure)...) events = append(events, projectAddedEvents(ctx, instanceID, orgID, projectID, owner, externalSecure)...)
return events return events
} }
func orgIDs() []string { func orgIDs() []string {
return slices.Concat([]string{"USER-MACHINE", "PAT", "USER"}, projectClientIDs()) return slices.Concat([]string{"USER-MACHINE", "PAT", "USER", "USER-LOGIN-CLIENT", "LOGIN-CLIENT-PAT"}, projectClientIDs())
} }
func instancePoliciesFilters(instanceID string) []expect { func instancePoliciesFilters(instanceID string) []expect {
@@ -363,7 +371,7 @@ func instanceElementsConfig() *SecretGenerators {
func setupInstanceFilters(instanceID, orgID, projectID, appID, domain string) []expect { func setupInstanceFilters(instanceID, orgID, projectID, appID, domain string) []expect {
return slices.Concat( return slices.Concat(
setupInstanceElementsFilters(instanceID), setupInstanceElementsFilters(instanceID),
orgFilters(orgID, true, true), orgFilters(orgID, true, true, true),
generatedDomainFilters(instanceID, orgID, projectID, appID, domain), generatedDomainFilters(instanceID, orgID, projectID, appID, domain),
) )
} }
@@ -371,7 +379,7 @@ func setupInstanceFilters(instanceID, orgID, projectID, appID, domain string) []
func setupInstanceEvents(ctx context.Context, instanceID, orgID, projectID, appID, instanceName, orgName string, defaultLanguage language.Tag, domain string, externalSecure bool) []eventstore.Command { func setupInstanceEvents(ctx context.Context, instanceID, orgID, projectID, appID, instanceName, orgName string, defaultLanguage language.Tag, domain string, externalSecure bool) []eventstore.Command {
return slices.Concat( return slices.Concat(
setupInstanceElementsEvents(ctx, instanceID, instanceName, defaultLanguage), setupInstanceElementsEvents(ctx, instanceID, instanceName, defaultLanguage),
orgEvents(ctx, instanceID, orgID, orgName, projectID, domain, externalSecure, true, true), orgEvents(ctx, instanceID, orgID, orgName, projectID, domain, externalSecure, true, true, true),
generatedDomainEvents(ctx, instanceID, orgID, projectID, appID, domain), generatedDomainEvents(ctx, instanceID, orgID, projectID, appID, domain),
instanceCreatedMilestoneEvent(ctx, instanceID), instanceCreatedMilestoneEvent(ctx, instanceID),
) )
@@ -380,9 +388,10 @@ func setupInstanceEvents(ctx context.Context, instanceID, orgID, projectID, appI
func setupInstanceConfig() *InstanceSetup { func setupInstanceConfig() *InstanceSetup {
conf := setupInstanceElementsConfig() conf := setupInstanceElementsConfig()
conf.Org = InstanceOrgSetup{ conf.Org = InstanceOrgSetup{
Name: "ZITADEL", Name: "ZITADEL",
Machine: instanceSetupMachineConfig(), Machine: instanceSetupMachineConfig(),
Human: instanceSetupHumanConfig(), Human: instanceSetupHumanConfig(),
LoginClient: instanceSetupLoginClientConfig(),
} }
conf.CustomDomain = "" conf.CustomDomain = ""
return conf return conf
@@ -541,6 +550,43 @@ func instanceSetupMachineConfig() *AddMachine {
} }
} }
func loginClientFilters(orgID string, pat bool) []expect {
filters := []expect{
expectFilter(),
expectFilter(
org.NewDomainPolicyAddedEvent(
context.Background(),
&org.NewAggregate(orgID).Aggregate,
true,
true,
true,
),
),
}
if pat {
filters = append(filters,
expectFilter(),
expectFilter(),
)
}
return filters
}
func instanceSetupLoginClientConfig() *AddLoginClient {
return &AddLoginClient{
Machine: &Machine{
Username: "zitadel-login-client",
Name: "ZITADEL-login-client",
Description: "Login Client",
AccessTokenType: domain.OIDCTokenTypeBearer,
},
Pat: &AddPat{
ExpirationDate: time.Time{},
Scopes: nil,
},
}
}
func projectFilters() []expect { func projectFilters() []expect {
return []expect{ return []expect{
expectFilter(), expectFilter(),
@@ -551,11 +597,23 @@ func projectFilters() []expect {
} }
func adminMemberFilters(orgID, userID string) []expect { func adminMemberFilters(orgID, userID string) []expect {
filters := append(
orgMemberFilters(orgID, userID),
instanceMemberFilters(orgID, userID)...,
)
return filters
}
func orgMemberFilters(orgID, userID string) []expect {
return []expect{ return []expect{
expectFilter( expectFilter(
addHumanEvent(context.Background(), orgID, userID), addHumanEvent(context.Background(), orgID, userID),
), ),
expectFilter(), expectFilter(),
}
}
func instanceMemberFilters(orgID, userID string) []expect {
return []expect{
expectFilter( expectFilter(
addHumanEvent(context.Background(), orgID, userID), addHumanEvent(context.Background(), orgID, userID),
), ),
@@ -631,6 +689,40 @@ func addMachineEvent(ctx context.Context, orgID, userID string) *user.MachineAdd
) )
} }
// loginClientEvents all events from setup to create the login client user
func loginClientEvents(ctx context.Context, instanceID, orgID, userID, patID string) []eventstore.Command {
agg := user.NewAggregate(userID, orgID)
instanceAgg := instance.NewAggregate(instanceID)
events := []eventstore.Command{
addLoginClientEvent(ctx, orgID, userID),
instance.NewMemberAddedEvent(ctx, &instanceAgg.Aggregate, userID, domain.RoleIAMLoginClient),
}
if patID != "" {
events = append(events,
user.NewPersonalAccessTokenAddedEvent(
ctx,
&agg.Aggregate,
patID,
time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC),
nil,
),
)
}
return events
}
func addLoginClientEvent(ctx context.Context, orgID, userID string) *user.MachineAddedEvent {
agg := user.NewAggregate(userID, orgID)
return user.NewMachineAddedEvent(ctx,
&agg.Aggregate,
"zitadel-login-client",
"ZITADEL-login-client",
"Login Client",
false,
domain.OIDCTokenTypeBearer,
)
}
func testSetup(ctx context.Context, c *Commands, validations []preparation.Validation) error { func testSetup(ctx context.Context, c *Commands, validations []preparation.Validation) error {
//nolint:staticcheck //nolint:staticcheck
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...)
@@ -715,6 +807,13 @@ func TestCommandSide_setupMinimalInterfaces(t *testing.T) {
}) })
} }
} }
func validZitadelRoles() []authz.RoleMapping {
return []authz.RoleMapping{
{Role: domain.RoleOrgOwner, Permissions: []string{""}},
{Role: domain.RoleIAMOwner, Permissions: []string{""}},
{Role: domain.RoleIAMLoginClient, Permissions: []string{""}},
}
}
func TestCommandSide_setupAdmins(t *testing.T) { func TestCommandSide_setupAdmins(t *testing.T) {
type fields struct { type fields struct {
@@ -730,12 +829,14 @@ func TestCommandSide_setupAdmins(t *testing.T) {
orgAgg *org.Aggregate orgAgg *org.Aggregate
machine *AddMachine machine *AddMachine
human *AddHuman human *AddHuman
loginClient *AddLoginClient
} }
type res struct { type res struct {
owner string owner string
pat bool pat bool
machineKey bool machineKey bool
err func(error) bool loginClientPat bool
err func(error) bool
} }
tests := []struct { tests := []struct {
name string name string
@@ -763,10 +864,7 @@ func TestCommandSide_setupAdmins(t *testing.T) {
), ),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER"), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER"),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
roles: []authz.RoleMapping{ roles: validZitadelRoles(),
{Role: domain.RoleOrgOwner, Permissions: []string{""}},
{Role: domain.RoleIAMOwner, Permissions: []string{""}},
},
}, },
args: args{ args: args{
ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch),
@@ -800,11 +898,8 @@ func TestCommandSide_setupAdmins(t *testing.T) {
}, },
)..., )...,
), ),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT"), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT"),
roles: []authz.RoleMapping{ roles: validZitadelRoles(),
{Role: domain.RoleOrgOwner, Permissions: []string{""}},
{Role: domain.RoleIAMOwner, Permissions: []string{""}},
},
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
}, },
args: args{ args: args{
@@ -850,11 +945,8 @@ func TestCommandSide_setupAdmins(t *testing.T) {
), ),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT", "USER"), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT", "USER"),
roles: []authz.RoleMapping{ roles: validZitadelRoles(),
{Role: domain.RoleOrgOwner, Permissions: []string{""}}, keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
{Role: domain.RoleIAMOwner, Permissions: []string{""}},
},
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
}, },
args: args{ args: args{
ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch),
@@ -870,6 +962,63 @@ func TestCommandSide_setupAdmins(t *testing.T) {
err: nil, err: nil,
}, },
}, },
{
name: "human, machine and login client, ok",
fields: fields{
eventstore: expectEventstore(
slices.Concat(
machineFilters("ORG", true),
adminMemberFilters("ORG", "USER-MACHINE"),
humanFilters("ORG"),
adminMemberFilters("ORG", "USER"),
loginClientFilters("ORG", true),
instanceMemberFilters("ORG", "USER-LOGIN-CLIENT"),
[]expect{
expectPush(
slices.Concat(
machineEvents(context.Background(),
"INSTANCE",
"ORG",
"USER-MACHINE",
"PAT",
),
humanEvents(context.Background(),
"INSTANCE",
"ORG",
"USER",
),
loginClientEvents(context.Background(),
"INSTANCE",
"ORG",
"USER-LOGIN-CLIENT",
"LOGIN-CLIENT-PAT",
),
)...,
),
},
)...,
),
userPasswordHasher: mockPasswordHasher("x"),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT", "USER", "USER-LOGIN-CLIENT", "LOGIN-CLIENT-PAT"),
roles: validZitadelRoles(),
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch),
instanceAgg: instance.NewAggregate("INSTANCE"),
orgAgg: org.NewAggregate("ORG"),
machine: instanceSetupMachineConfig(),
human: instanceSetupHumanConfig(),
loginClient: instanceSetupLoginClientConfig(),
},
res: res{
owner: "USER",
pat: true,
machineKey: false,
loginClientPat: true,
err: nil,
},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@@ -881,7 +1030,7 @@ func TestCommandSide_setupAdmins(t *testing.T) {
keyAlgorithm: tt.fields.keyAlgorithm, keyAlgorithm: tt.fields.keyAlgorithm,
} }
validations := make([]preparation.Validation, 0) validations := make([]preparation.Validation, 0)
owner, pat, mk, err := setupAdmins(r, &validations, tt.args.instanceAgg, tt.args.orgAgg, tt.args.machine, tt.args.human) owner, pat, mk, loginClientPat, err := setupAdmins(r, &validations, tt.args.instanceAgg, tt.args.orgAgg, tt.args.machine, tt.args.human, tt.args.loginClient)
if tt.res.err == nil { if tt.res.err == nil {
assert.NoError(t, err) assert.NoError(t, err)
} }
@@ -905,6 +1054,9 @@ func TestCommandSide_setupAdmins(t *testing.T) {
if tt.res.machineKey { if tt.res.machineKey {
assert.NotNil(t, mk) assert.NotNil(t, mk)
} }
if tt.res.loginClientPat {
assert.NotNil(t, loginClientPat)
}
} }
}) })
} }
@@ -924,12 +1076,14 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) {
orgName string orgName string
machine *AddMachine machine *AddMachine
human *AddHuman human *AddHuman
loginClient *AddLoginClient
ids ZitadelConfig ids ZitadelConfig
} }
type res struct { type res struct {
pat bool pat bool
machineKey bool machineKey bool
err func(error) bool loginClientPat bool
err func(error) bool
} }
tests := []struct { tests := []struct {
name string name string
@@ -938,7 +1092,7 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) {
res res res res
}{ }{
{ {
name: "human and machine, ok", name: "human, machine and login client, ok",
fields: fields{ fields: fields{
eventstore: expectEventstore( eventstore: expectEventstore(
slices.Concat( slices.Concat(
@@ -946,6 +1100,7 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) {
"ORG", "ORG",
true, true,
true, true,
true,
), ),
[]expect{ []expect{
expectPush( expectPush(
@@ -959,6 +1114,7 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) {
false, false,
true, true,
true, true,
true,
), ),
)..., )...,
), ),
@@ -967,11 +1123,8 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) {
), ),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, orgIDs()...), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, orgIDs()...),
roles: []authz.RoleMapping{ roles: validZitadelRoles(),
{Role: domain.RoleOrgOwner, Permissions: []string{""}}, keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
{Role: domain.RoleIAMOwner, Permissions: []string{""}},
},
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
}, },
args: args{ args: args{
ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch),
@@ -1007,6 +1160,18 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) {
Password: "password", Password: "password",
PasswordChangeRequired: false, PasswordChangeRequired: false,
}, },
loginClient: &AddLoginClient{
Machine: &Machine{
Username: "zitadel-login-client",
Name: "ZITADEL-login-client",
Description: "Login Client",
AccessTokenType: domain.OIDCTokenTypeBearer,
},
Pat: &AddPat{
ExpirationDate: time.Time{},
Scopes: nil,
},
},
ids: ZitadelConfig{ ids: ZitadelConfig{
instanceID: "INSTANCE", instanceID: "INSTANCE",
orgID: "ORG", orgID: "ORG",
@@ -1018,9 +1183,10 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) {
}, },
}, },
res: res{ res: res{
pat: true, pat: true,
machineKey: false, machineKey: false,
err: nil, loginClientPat: true,
err: nil,
}, },
}, },
} }
@@ -1034,7 +1200,7 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) {
keyAlgorithm: tt.fields.keyAlgorithm, keyAlgorithm: tt.fields.keyAlgorithm,
} }
validations := make([]preparation.Validation, 0) validations := make([]preparation.Validation, 0)
pat, mk, err := setupDefaultOrg(tt.args.ctx, r, &validations, tt.args.instanceAgg, tt.args.orgName, tt.args.machine, tt.args.human, tt.args.ids) pat, mk, loginClientPat, err := setupDefaultOrg(tt.args.ctx, r, &validations, tt.args.instanceAgg, tt.args.orgName, tt.args.machine, tt.args.human, tt.args.loginClient, tt.args.ids)
if tt.res.err == nil { if tt.res.err == nil {
assert.NoError(t, err) assert.NoError(t, err)
} }
@@ -1057,6 +1223,9 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) {
if tt.res.machineKey { if tt.res.machineKey {
assert.NotNil(t, mk) assert.NotNil(t, mk)
} }
if tt.res.loginClientPat {
assert.NotNil(t, loginClientPat)
}
} }
}) })
} }
@@ -1140,9 +1309,10 @@ func TestCommandSide_setUpInstance(t *testing.T) {
setup *InstanceSetup setup *InstanceSetup
} }
type res struct { type res struct {
pat bool pat bool
machineKey bool machineKey bool
err func(error) bool loginClientPat bool
err func(error) bool
} }
tests := []struct { tests := []struct {
name string name string
@@ -1175,11 +1345,8 @@ func TestCommandSide_setUpInstance(t *testing.T) {
), ),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, orgIDs()...), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, orgIDs()...),
roles: []authz.RoleMapping{ roles: validZitadelRoles(),
{Role: domain.RoleOrgOwner, Permissions: []string{""}}, keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
{Role: domain.RoleIAMOwner, Permissions: []string{""}},
},
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
generateDomain: func(string, string) (string, error) { generateDomain: func(string, string) (string, error) {
return "DOMAIN", nil return "DOMAIN", nil
}, },
@@ -1204,7 +1371,7 @@ func TestCommandSide_setUpInstance(t *testing.T) {
GenerateDomain: tt.fields.generateDomain, GenerateDomain: tt.fields.generateDomain,
} }
validations, pat, mk, err := setUpInstance(tt.args.ctx, r, tt.args.setup) validations, pat, mk, loginClientPat, err := setUpInstance(tt.args.ctx, r, tt.args.setup)
if tt.res.err == nil { if tt.res.err == nil {
assert.NoError(t, err) assert.NoError(t, err)
} }
@@ -1227,6 +1394,9 @@ func TestCommandSide_setUpInstance(t *testing.T) {
if tt.res.machineKey { if tt.res.machineKey {
assert.NotNil(t, mk) assert.NotNil(t, mk)
} }
if tt.res.loginClientPat {
assert.NotNil(t, loginClientPat)
}
} }
}) })
} }

View File

@@ -24,9 +24,15 @@ type InstanceOrgSetup struct {
CustomDomain string CustomDomain string
Human *AddHuman Human *AddHuman
Machine *AddMachine Machine *AddMachine
LoginClient *AddLoginClient
Roles []string Roles []string
} }
type AddLoginClient struct {
Machine *Machine
Pat *AddPat
}
type OrgSetup struct { type OrgSetup struct {
Name string Name string
CustomDomain string CustomDomain string

View File

@@ -14,6 +14,7 @@ const (
RoleOrgOwner = "ORG_OWNER" RoleOrgOwner = "ORG_OWNER"
RoleOrgProjectCreator = "ORG_PROJECT_CREATOR" RoleOrgProjectCreator = "ORG_PROJECT_CREATOR"
RoleIAMOwner = "IAM_OWNER" RoleIAMOwner = "IAM_OWNER"
RoleIAMLoginClient = "IAM_LOGIN_CLIENT"
RoleProjectOwner = "PROJECT_OWNER" RoleProjectOwner = "PROJECT_OWNER"
RoleProjectOwnerGlobal = "PROJECT_OWNER_GLOBAL" RoleProjectOwnerGlobal = "PROJECT_OWNER_GLOBAL"
RoleProjectGrantOwner = "PROJECT_GRANT_OWNER" RoleProjectGrantOwner = "PROJECT_GRANT_OWNER"