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:
# 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

View File

@@ -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
}

View File

@@ -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

View File

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

View File

@@ -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:

View File

@@ -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"

View File

@@ -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'

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.
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).
<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) {
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
}

View File

@@ -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),

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{
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)
}
}
})
}

View File

@@ -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

View File

@@ -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"