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"