diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 9697e354c5..f1fc6a2414 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -839,6 +839,13 @@ DefaultInstance: Pat: # date format: 2023-01-01T00:00:00Z 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: ClientSecret: Length: 64 # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_CLIENTSECRET_LENGTH diff --git a/cmd/setup/03.go b/cmd/setup/03.go index 588ac71610..e8c51c79c6 100644 --- a/cmd/setup/03.go +++ b/cmd/setup/03.go @@ -20,12 +20,13 @@ import ( ) type FirstInstance struct { - InstanceName string - DefaultLanguage language.Tag - Org command.InstanceOrgSetup - MachineKeyPath string - PatPath string - Features *command.InstanceFeatures + InstanceName string + DefaultLanguage language.Tag + Org command.InstanceOrgSetup + MachineKeyPath string + PatPath string + LoginClientPatPath string + Features *command.InstanceFeatures 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 { 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.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 mig.outputMachineAuthentication(key, token) + return mig.outputMachineAuthentication(key, token, loginClientToken) } 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 } -func (mig *FirstInstance) outputMachineAuthentication(key *command.MachineKey, token string) error { +func (mig *FirstInstance) outputMachineAuthentication(key *command.MachineKey, token, loginClientToken string) error { if key != nil { keyDetails, err := key.Detail() if err != nil { @@ -165,6 +168,11 @@ func (mig *FirstInstance) outputMachineAuthentication(key *command.MachineKey, t return err } } + if loginClientToken != "" { + if err := outputStdoutOrPath(mig.LoginClientPatPath, loginClientToken); err != nil { + return err + } + } return nil } diff --git a/cmd/setup/steps.yaml b/cmd/setup/steps.yaml index d2a7cc68dd..709becf2c3 100644 --- a/cmd/setup/steps.yaml +++ b/cmd/setup/steps.yaml @@ -6,6 +6,7 @@ FirstInstance: MachineKeyPath: # ZITADEL_FIRSTINSTANCE_MACHINEKEYPATH # The personal access token from the section FirstInstance.Org.Machine.Pat is written to the PatPath. PatPath: # ZITADEL_FIRSTINSTANCE_PATPATH + LoginClientPatPath: # ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH InstanceName: ZITADEL # ZITADEL_FIRSTINSTANCE_INSTANCENAME DefaultLanguage: en # ZITADEL_FIRSTINSTANCE_DEFAULTLANGUAGE Org: @@ -46,6 +47,13 @@ FirstInstance: Pat: # date format: 2023-01-01T00:00:00Z 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: FailAfter: 5m # ZITADEL_CORRECTCREATIONDATE_FAILAFTER diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/.gitignore b/docs/docs/self-hosting/deploy/loadbalancing-example/.gitignore index bd98bacd66..8a28618b17 100644 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/.gitignore +++ b/docs/docs/self-hosting/deploy/loadbalancing-example/.gitignore @@ -1 +1 @@ -.env-file +.env-file \ No newline at end of file diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml index 013fc2aa22..96a87fa8d7 100644 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml +++ b/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml @@ -41,17 +41,17 @@ services: user: root entrypoint: '/bin/sh' command: - - -c - - > - /app/zitadel setup - --config /example-zitadel-config.yaml - --config /example-zitadel-secrets.yaml - --steps /example-zitadel-init-steps.yaml - --masterkey ${ZITADEL_MASTERKEY} && - mv /pat /.env-file/pat || exit 0 && - echo ZITADEL_SERVICE_USER_TOKEN=$(cat /.env-file/pat) > /.env-file/.env && - chown -R 1001:${GID} /.env-file && - chmod -R 770 /.env-file + - -c + - > + /app/zitadel setup + --config /example-zitadel-config.yaml + --config /example-zitadel-secrets.yaml + --steps /example-zitadel-init-steps.yaml + --masterkey ${ZITADEL_MASTERKEY} && + mv /pat /.env-file/pat || exit 0 && + echo ZITADEL_SERVICE_USER_TOKEN=$(cat /.env-file/pat) > /.env-file/.env && + chown -R 1001:${GID} /.env-file && + chmod -R 770 /.env-file environment: - GID depends_on: @@ -154,4 +154,4 @@ networks: backend: volumes: - data: + data: \ No newline at end of file diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml index fadd39373d..af5bb5145c 100644 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml +++ b/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml @@ -26,4 +26,4 @@ SAML.DefaultLoginURLV2: "/ui/v2/login/login?authRequest=" # ZITADEL_SAML_DEFAULT LogStore.Access.Stdout.Enabled: true # Skipping the MFA init step allows us to immediately authenticate at the console -DefaultInstance.LoginPolicy.MfaInitSkipLifetime: "0s" +DefaultInstance.LoginPolicy.MfaInitSkipLifetime: "0s" \ No newline at end of file diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml index be63164ced..9bdf41269d 100644 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml +++ b/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml @@ -9,4 +9,4 @@ FirstInstance: Machine: Username: 'login-container' Name: 'Login Container' - Pat.ExpirationDate: '2029-01-01T00:00:00Z' + Pat.ExpirationDate: '2029-01-01T00:00:00Z' \ No newline at end of file diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx b/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx index d4c27ccd95..3fb4784ea0 100644 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx +++ b/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx @@ -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. 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). \ No newline at end of file diff --git a/docs/docs/self-hosting/deploy/macos.mdx b/docs/docs/self-hosting/deploy/macos.mdx index beb3182208..aea5fb07e9 100644 --- a/docs/docs/self-hosting/deploy/macos.mdx +++ b/docs/docs/self-hosting/deploy/macos.mdx @@ -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). - + \ No newline at end of file diff --git a/internal/api/grpc/system/instance.go b/internal/api/grpc/system/instance.go index a5dd7b81bc..ccfcfecbf3 100644 --- a/internal/api/grpc/system/instance.go +++ b/internal/api/grpc/system/instance.go @@ -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) { - 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 { 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) { - 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 { return nil, err } diff --git a/internal/command/instance.go b/internal/command/instance.go index cfafb1d298..9e8f3d47c7 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -217,33 +217,33 @@ func (s *InstanceSetup) generateIDs(idGenerator id.Generator) (err error) { 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 { - return "", "", nil, nil, err + return "", "", nil, "", nil, err } 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 { - return "", "", nil, nil, err + return "", "", nil, "", nil, err } //nolint:staticcheck cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...) if err != nil { - return "", "", nil, nil, err + return "", "", nil, "", nil, err } _, err = c.eventstore.Push(ctx, cmds...) if err != nil { - return "", "", nil, nil, err + return "", "", nil, "", nil, err } // RolePermissions need to be pushed in separate transaction. // https://github.com/zitadel/zitadel/issues/9293 details, err := c.SynchronizeRolePermission(ctx, setup.zitadel.instanceID, setup.RolePermissionMappings) if err != nil { - return "", "", nil, nil, err + return "", "", nil, "", nil, err } details.ResourceOwner = setup.zitadel.orgID @@ -251,8 +251,12 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str if pat != nil { 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 { @@ -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) validations = setupInstanceElements(instanceAgg, setup) // 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 { - return nil, nil, nil, err + return nil, nil, nil, nil, err } // domains 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) // optional setting if set setupMessageTexts(&validations, setup.MessageTexts, instanceAgg) 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) 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) setupFeatures(&validations, setup.Features, setup.zitadel.instanceID) 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) 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 { @@ -572,8 +576,9 @@ func setupDefaultOrg(ctx context.Context, name string, machine *AddMachine, human *AddHuman, + loginClient *AddLoginClient, ids ZitadelConfig, -) (pat *PersonalAccessToken, machineKey *MachineKey, err error) { +) (pat *PersonalAccessToken, machineKey *MachineKey, loginClientPat *PersonalAccessToken, err error) { orgAgg := org.NewAggregate(ids.orgID) *validations = append( @@ -582,12 +587,12 @@ func setupDefaultOrg(ctx context.Context, 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 { - return nil, nil, err + return nil, nil, nil, err } setupMinimalInterfaces(commands, validations, instanceAgg, orgAgg, projectOwner, ids) - return pat, machineKey, nil + return pat, machineKey, loginClientPat, nil } func setupAdmins(commands *Commands, @@ -596,21 +601,22 @@ func setupAdmins(commands *Commands, orgAgg *org.Aggregate, machine *AddMachine, 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 { - 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() { machineUserID, err := commands.idGenerator.Next() if err != nil { - return "", nil, nil, err + return "", nil, nil, nil, err } owner = machineUserID pat, machineKey, err = setupMachineAdmin(commands, validations, machine, orgAgg.ID, machineUserID) if err != nil { - return "", nil, nil, err + return "", nil, nil, nil, err } setupAdminMembers(commands, validations, instanceAgg, orgAgg, machineUserID) @@ -618,7 +624,7 @@ func setupAdmins(commands *Commands, if human != nil { humanUserID, err := commands.idGenerator.Next() if err != nil { - return "", nil, nil, err + return "", nil, nil, nil, err } owner = humanUserID human.ID = humanUserID @@ -629,7 +635,18 @@ func setupAdmins(commands *Commands, 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) { @@ -655,6 +672,22 @@ func setupMachineAdmin(commands *Commands, validations *[]preparation.Validation 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) { *validations = append(*validations, commands.AddOrgMemberCommand(orgAgg, userID, domain.RoleOrgOwner), diff --git a/internal/command/instance_test.go b/internal/command/instance_test.go index 2b82818a7e..b40bba19af 100644 --- a/internal/command/instance_test.go +++ b/internal/command/instance_test.go @@ -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{ expectFilter(), expectFilter( @@ -144,13 +144,17 @@ func orgFilters(orgID string, machine, human bool) []expect { filters = append(filters, humanFilters(orgID)...) 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, 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) orgAgg := org.NewAggregate(orgID) 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)...) 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)...) return events } 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 { @@ -363,7 +371,7 @@ func instanceElementsConfig() *SecretGenerators { func setupInstanceFilters(instanceID, orgID, projectID, appID, domain string) []expect { return slices.Concat( setupInstanceElementsFilters(instanceID), - orgFilters(orgID, true, true), + orgFilters(orgID, true, true, true), 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 { return slices.Concat( 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), instanceCreatedMilestoneEvent(ctx, instanceID), ) @@ -380,9 +388,10 @@ func setupInstanceEvents(ctx context.Context, instanceID, orgID, projectID, appI func setupInstanceConfig() *InstanceSetup { conf := setupInstanceElementsConfig() conf.Org = InstanceOrgSetup{ - Name: "ZITADEL", - Machine: instanceSetupMachineConfig(), - Human: instanceSetupHumanConfig(), + Name: "ZITADEL", + Machine: instanceSetupMachineConfig(), + Human: instanceSetupHumanConfig(), + LoginClient: instanceSetupLoginClientConfig(), } conf.CustomDomain = "" 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 { return []expect{ expectFilter(), @@ -551,11 +597,23 @@ func projectFilters() []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{ expectFilter( addHumanEvent(context.Background(), orgID, userID), ), expectFilter(), + } +} + +func instanceMemberFilters(orgID, userID string) []expect { + return []expect{ expectFilter( 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 { //nolint:staticcheck 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) { type fields struct { @@ -730,12 +829,14 @@ func TestCommandSide_setupAdmins(t *testing.T) { orgAgg *org.Aggregate machine *AddMachine human *AddHuman + loginClient *AddLoginClient } type res struct { - owner string - pat bool - machineKey bool - err func(error) bool + owner string + pat bool + machineKey bool + loginClientPat bool + err func(error) bool } tests := []struct { name string @@ -763,10 +864,7 @@ func TestCommandSide_setupAdmins(t *testing.T) { ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER"), userPasswordHasher: mockPasswordHasher("x"), - roles: []authz.RoleMapping{ - {Role: domain.RoleOrgOwner, Permissions: []string{""}}, - {Role: domain.RoleIAMOwner, Permissions: []string{""}}, - }, + roles: validZitadelRoles(), }, args: args{ 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"), - roles: []authz.RoleMapping{ - {Role: domain.RoleOrgOwner, Permissions: []string{""}}, - {Role: domain.RoleIAMOwner, Permissions: []string{""}}, - }, + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT"), + roles: validZitadelRoles(), keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ @@ -850,11 +945,8 @@ func TestCommandSide_setupAdmins(t *testing.T) { ), userPasswordHasher: mockPasswordHasher("x"), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT", "USER"), - roles: []authz.RoleMapping{ - {Role: domain.RoleOrgOwner, Permissions: []string{""}}, - {Role: domain.RoleIAMOwner, Permissions: []string{""}}, - }, - keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + roles: validZitadelRoles(), + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), @@ -870,6 +962,63 @@ func TestCommandSide_setupAdmins(t *testing.T) { 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 { t.Run(tt.name, func(t *testing.T) { @@ -881,7 +1030,7 @@ func TestCommandSide_setupAdmins(t *testing.T) { keyAlgorithm: tt.fields.keyAlgorithm, } 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 { assert.NoError(t, err) } @@ -905,6 +1054,9 @@ func TestCommandSide_setupAdmins(t *testing.T) { if tt.res.machineKey { assert.NotNil(t, mk) } + if tt.res.loginClientPat { + assert.NotNil(t, loginClientPat) + } } }) } @@ -924,12 +1076,14 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { orgName string machine *AddMachine human *AddHuman + loginClient *AddLoginClient ids ZitadelConfig } type res struct { - pat bool - machineKey bool - err func(error) bool + pat bool + machineKey bool + loginClientPat bool + err func(error) bool } tests := []struct { name string @@ -938,7 +1092,7 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { res res }{ { - name: "human and machine, ok", + name: "human, machine and login client, ok", fields: fields{ eventstore: expectEventstore( slices.Concat( @@ -946,6 +1100,7 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { "ORG", true, true, + true, ), []expect{ expectPush( @@ -959,6 +1114,7 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { false, true, true, + true, ), )..., ), @@ -967,11 +1123,8 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { ), userPasswordHasher: mockPasswordHasher("x"), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, orgIDs()...), - roles: []authz.RoleMapping{ - {Role: domain.RoleOrgOwner, Permissions: []string{""}}, - {Role: domain.RoleIAMOwner, Permissions: []string{""}}, - }, - keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + roles: validZitadelRoles(), + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), @@ -1007,6 +1160,18 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { Password: "password", 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{ instanceID: "INSTANCE", orgID: "ORG", @@ -1018,9 +1183,10 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { }, }, res: res{ - pat: true, - machineKey: false, - err: nil, + pat: true, + machineKey: false, + loginClientPat: true, + err: nil, }, }, } @@ -1034,7 +1200,7 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { keyAlgorithm: tt.fields.keyAlgorithm, } 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 { assert.NoError(t, err) } @@ -1057,6 +1223,9 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { if tt.res.machineKey { assert.NotNil(t, mk) } + if tt.res.loginClientPat { + assert.NotNil(t, loginClientPat) + } } }) } @@ -1140,9 +1309,10 @@ func TestCommandSide_setUpInstance(t *testing.T) { setup *InstanceSetup } type res struct { - pat bool - machineKey bool - err func(error) bool + pat bool + machineKey bool + loginClientPat bool + err func(error) bool } tests := []struct { name string @@ -1175,11 +1345,8 @@ func TestCommandSide_setUpInstance(t *testing.T) { ), userPasswordHasher: mockPasswordHasher("x"), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, orgIDs()...), - roles: []authz.RoleMapping{ - {Role: domain.RoleOrgOwner, Permissions: []string{""}}, - {Role: domain.RoleIAMOwner, Permissions: []string{""}}, - }, - keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + roles: validZitadelRoles(), + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), generateDomain: func(string, string) (string, error) { return "DOMAIN", nil }, @@ -1204,7 +1371,7 @@ func TestCommandSide_setUpInstance(t *testing.T) { 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 { assert.NoError(t, err) } @@ -1227,6 +1394,9 @@ func TestCommandSide_setUpInstance(t *testing.T) { if tt.res.machineKey { assert.NotNil(t, mk) } + if tt.res.loginClientPat { + assert.NotNil(t, loginClientPat) + } } }) } diff --git a/internal/command/org.go b/internal/command/org.go index faab882d68..876c256a0a 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -24,9 +24,15 @@ type InstanceOrgSetup struct { CustomDomain string Human *AddHuman Machine *AddMachine + LoginClient *AddLoginClient Roles []string } +type AddLoginClient struct { + Machine *Machine + Pat *AddPat +} + type OrgSetup struct { Name string CustomDomain string diff --git a/internal/domain/roles.go b/internal/domain/roles.go index c40eef6120..2cebd26d30 100644 --- a/internal/domain/roles.go +++ b/internal/domain/roles.go @@ -14,6 +14,7 @@ const ( RoleOrgOwner = "ORG_OWNER" RoleOrgProjectCreator = "ORG_PROJECT_CREATOR" RoleIAMOwner = "IAM_OWNER" + RoleIAMLoginClient = "IAM_LOGIN_CLIENT" RoleProjectOwner = "PROJECT_OWNER" RoleProjectOwnerGlobal = "PROJECT_OWNER_GLOBAL" RoleProjectGrantOwner = "PROJECT_GRANT_OWNER"