fixup! fixup! fixup! Merge branch 'main' into fix_adding_org_same_id_twice

This commit is contained in:
Iraq Jaber
2025-07-02 15:36:04 +02:00
449 changed files with 41328 additions and 119 deletions

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"

View File

@@ -101,3 +101,10 @@ SystemDefaults:
KeyConfig:
PrivateKeyLifetime: 7200h
PublicKeyLifetime: 14400h
OIDC:
DefaultLoginURLV2: "/login?authRequest=" # ZITADEL_OIDC_DEFAULTLOGINURLV2
DefaultLogoutURLV2: "/logout?post_logout_redirect=" # ZITADEL_OIDC_DEFAULTLOGOUTURLV2
SAML:
DefaultLoginURLV2: "/login?authRequest=" # ZITADEL_SAML_DEFAULTLOGINURLV2

View File

@@ -9,6 +9,7 @@ import (
"github.com/riverqueue/river/riverdriver/riverpgxv5"
"github.com/riverqueue/river/rivertype"
"github.com/riverqueue/rivercontrib/otelriver"
"github.com/robfig/cron/v3"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/database"
@@ -75,6 +76,26 @@ func (q *Queue) AddWorkers(w ...Worker) {
}
}
func (q *Queue) AddPeriodicJob(schedule cron.Schedule, jobArgs river.JobArgs, opts ...InsertOpt) (handle rivertype.PeriodicJobHandle) {
if q == nil {
logging.Info("skip adding periodic job because queue is not set")
return
}
options := new(river.InsertOpts)
for _, opt := range opts {
opt(options)
}
return q.client.PeriodicJobs().Add(
river.NewPeriodicJob(
schedule,
func() (river.JobArgs, *river.InsertOpts) {
return jobArgs, options
},
nil,
),
)
}
type InsertOpt func(*river.InsertOpts)
func WithMaxAttempts(maxAttempts uint8) InsertOpt {

View File

@@ -0,0 +1,153 @@
package serviceping
import (
"bytes"
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
"net/http"
"google.golang.org/grpc"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
analytics "github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta"
)
const (
pathBaseInformation = "/instances"
pathResourceCounts = "/resource_counts"
)
type Client struct {
httpClient *http.Client
endpoint string
}
func (c Client) ReportBaseInformation(ctx context.Context, in *analytics.ReportBaseInformationRequest, opts ...grpc.CallOption) (*analytics.ReportBaseInformationResponse, error) {
reportResponse := new(analytics.ReportBaseInformationResponse)
err := c.callTelemetryService(ctx, pathBaseInformation, in, reportResponse)
if err != nil {
return nil, err
}
return reportResponse, nil
}
func (c Client) ReportResourceCounts(ctx context.Context, in *analytics.ReportResourceCountsRequest, opts ...grpc.CallOption) (*analytics.ReportResourceCountsResponse, error) {
reportResponse := new(analytics.ReportResourceCountsResponse)
err := c.callTelemetryService(ctx, pathResourceCounts, in, reportResponse)
if err != nil {
return nil, err
}
return reportResponse, nil
}
func (c Client) callTelemetryService(ctx context.Context, path string, in proto.Message, out proto.Message) error {
requestBody, err := protojson.Marshal(in)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint+path, bytes.NewReader(requestBody))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return &TelemetryError{
StatusCode: resp.StatusCode,
Body: body,
}
}
return protojson.UnmarshalOptions{
AllowPartial: true,
DiscardUnknown: true,
}.Unmarshal(body, out)
}
func NewClient(config *Config) Client {
return Client{
httpClient: http.DefaultClient,
endpoint: config.Endpoint,
}
}
func GenerateSystemID() (string, error) {
randBytes := make([]byte, 64)
if _, err := rand.Read(randBytes); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(randBytes), nil
}
func instanceInformationToPb(instances *query.Instances) []*analytics.InstanceInformation {
instanceInformation := make([]*analytics.InstanceInformation, len(instances.Instances))
for i, instance := range instances.Instances {
domains := instanceDomainToPb(instance)
instanceInformation[i] = &analytics.InstanceInformation{
Id: instance.ID,
Domains: domains,
CreatedAt: timestamppb.New(instance.CreationDate),
}
}
return instanceInformation
}
func instanceDomainToPb(instance *query.Instance) []string {
domains := make([]string, len(instance.Domains))
for i, domain := range instance.Domains {
domains[i] = domain.Domain
}
return domains
}
func resourceCountsToPb(counts []query.ResourceCount) []*analytics.ResourceCount {
resourceCounts := make([]*analytics.ResourceCount, len(counts))
for i, count := range counts {
resourceCounts[i] = &analytics.ResourceCount{
InstanceId: count.InstanceID,
ParentType: countParentTypeToPb(count.ParentType),
ParentId: count.ParentID,
ResourceName: count.Resource,
TableName: count.TableName,
UpdatedAt: timestamppb.New(count.UpdatedAt),
Amount: uint32(count.Amount),
}
}
return resourceCounts
}
func countParentTypeToPb(parentType domain.CountParentType) analytics.CountParentType {
switch parentType {
case domain.CountParentTypeInstance:
return analytics.CountParentType_COUNT_PARENT_TYPE_INSTANCE
case domain.CountParentTypeOrganization:
return analytics.CountParentType_COUNT_PARENT_TYPE_ORGANIZATION
default:
return analytics.CountParentType_COUNT_PARENT_TYPE_UNSPECIFIED
}
}
type TelemetryError struct {
StatusCode int
Body []byte
}
func (e *TelemetryError) Error() string {
return fmt.Sprintf("telemetry error %d: %s", e.StatusCode, e.Body)
}

View File

@@ -0,0 +1,18 @@
package serviceping
type Config struct {
Enabled bool
Endpoint string
Interval string
MaxAttempts uint8
Telemetry TelemetryConfig
}
type TelemetryConfig struct {
ResourceCount ResourceCount
}
type ResourceCount struct {
Enabled bool
BulkSize int
}

View File

@@ -0,0 +1,5 @@
package mock
//go:generate mockgen -package mock -destination queue.mock.go github.com/zitadel/zitadel/internal/serviceping Queue
//go:generate mockgen -package mock -destination queries.mock.go github.com/zitadel/zitadel/internal/serviceping Queries
//go:generate mockgen -package mock -destination telemetry.mock.go github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta TelemetryServiceClient

View File

@@ -0,0 +1,72 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/zitadel/zitadel/internal/serviceping (interfaces: Queries)
//
// Generated by this command:
//
// mockgen -package mock -destination queries.mock.go github.com/zitadel/zitadel/internal/serviceping Queries
//
// Package mock is a generated GoMock package.
package mock
import (
context "context"
reflect "reflect"
query "github.com/zitadel/zitadel/internal/query"
gomock "go.uber.org/mock/gomock"
)
// MockQueries is a mock of Queries interface.
type MockQueries struct {
ctrl *gomock.Controller
recorder *MockQueriesMockRecorder
isgomock struct{}
}
// MockQueriesMockRecorder is the mock recorder for MockQueries.
type MockQueriesMockRecorder struct {
mock *MockQueries
}
// NewMockQueries creates a new mock instance.
func NewMockQueries(ctrl *gomock.Controller) *MockQueries {
mock := &MockQueries{ctrl: ctrl}
mock.recorder = &MockQueriesMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockQueries) EXPECT() *MockQueriesMockRecorder {
return m.recorder
}
// ListResourceCounts mocks base method.
func (m *MockQueries) ListResourceCounts(ctx context.Context, lastID, size int) ([]query.ResourceCount, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListResourceCounts", ctx, lastID, size)
ret0, _ := ret[0].([]query.ResourceCount)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListResourceCounts indicates an expected call of ListResourceCounts.
func (mr *MockQueriesMockRecorder) ListResourceCounts(ctx, lastID, size any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListResourceCounts", reflect.TypeOf((*MockQueries)(nil).ListResourceCounts), ctx, lastID, size)
}
// SearchInstances mocks base method.
func (m *MockQueries) SearchInstances(ctx context.Context, queries *query.InstanceSearchQueries) (*query.Instances, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SearchInstances", ctx, queries)
ret0, _ := ret[0].(*query.Instances)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SearchInstances indicates an expected call of SearchInstances.
func (mr *MockQueriesMockRecorder) SearchInstances(ctx, queries any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchInstances", reflect.TypeOf((*MockQueries)(nil).SearchInstances), ctx, queries)
}

View File

@@ -0,0 +1,62 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/zitadel/zitadel/internal/serviceping (interfaces: Queue)
//
// Generated by this command:
//
// mockgen -package mock -destination queue.mock.go github.com/zitadel/zitadel/internal/serviceping Queue
//
// Package mock is a generated GoMock package.
package mock
import (
context "context"
reflect "reflect"
river "github.com/riverqueue/river"
queue "github.com/zitadel/zitadel/internal/queue"
gomock "go.uber.org/mock/gomock"
)
// MockQueue is a mock of Queue interface.
type MockQueue struct {
ctrl *gomock.Controller
recorder *MockQueueMockRecorder
isgomock struct{}
}
// MockQueueMockRecorder is the mock recorder for MockQueue.
type MockQueueMockRecorder struct {
mock *MockQueue
}
// NewMockQueue creates a new mock instance.
func NewMockQueue(ctrl *gomock.Controller) *MockQueue {
mock := &MockQueue{ctrl: ctrl}
mock.recorder = &MockQueueMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockQueue) EXPECT() *MockQueueMockRecorder {
return m.recorder
}
// Insert mocks base method.
func (m *MockQueue) Insert(ctx context.Context, args river.JobArgs, opts ...queue.InsertOpt) error {
m.ctrl.T.Helper()
varargs := []any{ctx, args}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Insert", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Insert indicates an expected call of Insert.
func (mr *MockQueueMockRecorder) Insert(ctx, args any, opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, args}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockQueue)(nil).Insert), varargs...)
}

View File

@@ -0,0 +1,83 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta (interfaces: TelemetryServiceClient)
//
// Generated by this command:
//
// mockgen -package mock -destination telemetry.mock.go github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta TelemetryServiceClient
//
// Package mock is a generated GoMock package.
package mock
import (
context "context"
reflect "reflect"
analytics "github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta"
gomock "go.uber.org/mock/gomock"
grpc "google.golang.org/grpc"
)
// MockTelemetryServiceClient is a mock of TelemetryServiceClient interface.
type MockTelemetryServiceClient struct {
ctrl *gomock.Controller
recorder *MockTelemetryServiceClientMockRecorder
isgomock struct{}
}
// MockTelemetryServiceClientMockRecorder is the mock recorder for MockTelemetryServiceClient.
type MockTelemetryServiceClientMockRecorder struct {
mock *MockTelemetryServiceClient
}
// NewMockTelemetryServiceClient creates a new mock instance.
func NewMockTelemetryServiceClient(ctrl *gomock.Controller) *MockTelemetryServiceClient {
mock := &MockTelemetryServiceClient{ctrl: ctrl}
mock.recorder = &MockTelemetryServiceClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTelemetryServiceClient) EXPECT() *MockTelemetryServiceClientMockRecorder {
return m.recorder
}
// ReportBaseInformation mocks base method.
func (m *MockTelemetryServiceClient) ReportBaseInformation(ctx context.Context, in *analytics.ReportBaseInformationRequest, opts ...grpc.CallOption) (*analytics.ReportBaseInformationResponse, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, in}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ReportBaseInformation", varargs...)
ret0, _ := ret[0].(*analytics.ReportBaseInformationResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReportBaseInformation indicates an expected call of ReportBaseInformation.
func (mr *MockTelemetryServiceClientMockRecorder) ReportBaseInformation(ctx, in any, opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, in}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportBaseInformation", reflect.TypeOf((*MockTelemetryServiceClient)(nil).ReportBaseInformation), varargs...)
}
// ReportResourceCounts mocks base method.
func (m *MockTelemetryServiceClient) ReportResourceCounts(ctx context.Context, in *analytics.ReportResourceCountsRequest, opts ...grpc.CallOption) (*analytics.ReportResourceCountsResponse, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, in}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ReportResourceCounts", varargs...)
ret0, _ := ret[0].(*analytics.ReportResourceCountsResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReportResourceCounts indicates an expected call of ReportResourceCounts.
func (mr *MockTelemetryServiceClientMockRecorder) ReportResourceCounts(ctx, in any, opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, in}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportResourceCounts", reflect.TypeOf((*MockTelemetryServiceClient)(nil).ReportResourceCounts), varargs...)
}

View File

@@ -0,0 +1,17 @@
package serviceping
type ReportType uint
const (
ReportTypeBaseInformation ReportType = iota
ReportTypeResourceCounts
)
type ServicePingReport struct {
ReportID string
ReportType ReportType
}
func (r *ServicePingReport) Kind() string {
return "service_ping_report"
}

View File

@@ -0,0 +1,252 @@
package serviceping
import (
"context"
"errors"
"net/http"
"github.com/muhlemmer/gu"
"github.com/riverqueue/river"
"github.com/robfig/cron/v3"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/cmd/build"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/queue"
"github.com/zitadel/zitadel/internal/v2/system"
analytics "github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta"
)
const (
QueueName = "service_ping_report"
)
var (
ErrInvalidReportType = errors.New("invalid report type")
_ river.Worker[*ServicePingReport] = (*Worker)(nil)
)
type Worker struct {
river.WorkerDefaults[*ServicePingReport]
reportClient analytics.TelemetryServiceClient
db Queries
queue Queue
config *Config
systemID string
version string
}
type Queries interface {
SearchInstances(ctx context.Context, queries *query.InstanceSearchQueries) (*query.Instances, error)
ListResourceCounts(ctx context.Context, lastID int, size int) ([]query.ResourceCount, error)
}
type Queue interface {
Insert(ctx context.Context, args river.JobArgs, opts ...queue.InsertOpt) error
}
// Register implements the [queue.Worker] interface.
func (w *Worker) Register(workers *river.Workers, queues map[string]river.QueueConfig) {
river.AddWorker[*ServicePingReport](workers, w)
queues[QueueName] = river.QueueConfig{
MaxWorkers: 1, // for now, we only use a single worker to prevent too much side effects on other queues
}
}
// Work implements the [river.Worker] interface.
func (w *Worker) Work(ctx context.Context, job *river.Job[*ServicePingReport]) (err error) {
defer func() {
err = w.handleClientError(err)
}()
switch job.Args.ReportType {
case ReportTypeBaseInformation:
reportID, err := w.reportBaseInformation(ctx)
if err != nil {
return err
}
return w.createReportJobs(ctx, reportID)
case ReportTypeResourceCounts:
return w.reportResourceCounts(ctx, job.Args.ReportID)
default:
logging.WithFields("reportType", job.Args.ReportType, "reportID", job.Args.ReportID).
Error("unknown job type")
return river.JobCancel(ErrInvalidReportType)
}
}
func (w *Worker) reportBaseInformation(ctx context.Context) (string, error) {
instances, err := w.db.SearchInstances(ctx, &query.InstanceSearchQueries{})
if err != nil {
return "", err
}
instanceInformation := instanceInformationToPb(instances)
resp, err := w.reportClient.ReportBaseInformation(ctx, &analytics.ReportBaseInformationRequest{
SystemId: w.systemID,
Version: w.version,
Instances: instanceInformation,
})
if err != nil {
return "", err
}
return resp.GetReportId(), nil
}
func (w *Worker) reportResourceCounts(ctx context.Context, reportID string) error {
lastID := 0
// iterate over the resource counts until there are no more counts to report
// or the context gets cancelled
for {
select {
case <-ctx.Done():
return nil
default:
counts, err := w.db.ListResourceCounts(ctx, lastID, w.config.Telemetry.ResourceCount.BulkSize)
if err != nil {
return err
}
// if there are no counts, we can stop the loop
if len(counts) == 0 {
return nil
}
request := &analytics.ReportResourceCountsRequest{
SystemId: w.systemID,
ResourceCounts: resourceCountsToPb(counts),
}
if reportID != "" {
request.ReportId = gu.Ptr(reportID)
}
resp, err := w.reportClient.ReportResourceCounts(ctx, request)
if err != nil {
return err
}
// in case the resource counts returned by the database are less than the bulk size,
// we can assume that we have reached the end of the resource counts and can stop the loop
if len(counts) < w.config.Telemetry.ResourceCount.BulkSize {
return nil
}
// update the lastID for the next iteration
lastID = counts[len(counts)-1].ID
// In case we get a report ID back from the server (it could be the first call of the report),
// we update it to use it for the next batch.
if resp.GetReportId() != "" && resp.GetReportId() != reportID {
reportID = resp.GetReportId()
}
}
}
}
func (w *Worker) handleClientError(err error) error {
telemetryError := new(TelemetryError)
if !errors.As(err, &telemetryError) {
// If the error is not a TelemetryError, we can assume that it is a transient error
// and can be retried by the queue.
return err
}
switch telemetryError.StatusCode {
case http.StatusBadRequest,
http.StatusNotFound,
http.StatusNotImplemented,
http.StatusConflict,
http.StatusPreconditionFailed:
// In case of these errors, we can assume that a retry does not make sense,
// so we can cancel the job.
return river.JobCancel(err)
default:
// As of now we assume that all other errors are transient and can be retried.
// So we just return the error, which will be handled by the queue as a failed attempt.
return err
}
}
func (w *Worker) createReportJobs(ctx context.Context, reportID string) error {
errs := make([]error, 0)
if w.config.Telemetry.ResourceCount.Enabled {
err := w.addReportJob(ctx, reportID, ReportTypeResourceCounts)
if err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
func (w *Worker) addReportJob(ctx context.Context, reportID string, reportType ReportType) error {
job := &ServicePingReport{
ReportID: reportID,
ReportType: reportType,
}
return w.queue.Insert(ctx, job,
queue.WithQueueName(QueueName),
queue.WithMaxAttempts(w.config.MaxAttempts),
)
}
type systemIDReducer struct {
id string
}
func (s *systemIDReducer) Reduce() error {
return nil
}
func (s *systemIDReducer) AppendEvents(events ...eventstore.Event) {
for _, event := range events {
if idEvent, ok := event.(*system.IDGeneratedEvent); ok {
s.id = idEvent.ID
}
}
}
func (s *systemIDReducer) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AddQuery().
AggregateTypes(system.AggregateType).
EventTypes(system.IDGeneratedType).
Builder()
}
func Register(
ctx context.Context,
q *queue.Queue,
queries *query.Queries,
eventstoreClient *eventstore.Eventstore,
config *Config,
) error {
if !config.Enabled {
return nil
}
systemID := new(systemIDReducer)
err := eventstoreClient.FilterToQueryReducer(ctx, systemID)
if err != nil {
return err
}
q.AddWorkers(&Worker{
reportClient: NewClient(config),
db: queries,
queue: q,
config: config,
systemID: systemID.id,
version: build.Version(),
})
return nil
}
func Start(config *Config, q *queue.Queue) error {
if !config.Enabled {
return nil
}
schedule, err := cron.ParseStandard(config.Interval)
if err != nil {
return err
}
q.AddPeriodicJob(
schedule,
&ServicePingReport{},
queue.WithQueueName(QueueName),
queue.WithMaxAttempts(config.MaxAttempts),
)
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,44 @@
package system
import (
"context"
"github.com/zitadel/zitadel/internal/eventstore"
)
func init() {
eventstore.RegisterFilterEventMapper(AggregateType, IDGeneratedType, eventstore.GenericEventMapper[IDGeneratedEvent])
}
const IDGeneratedType = AggregateType + ".id.generated"
type IDGeneratedEvent struct {
eventstore.BaseEvent `json:"-"`
ID string `json:"id"`
}
func (e *IDGeneratedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
e.BaseEvent = *b
}
func (e *IDGeneratedEvent) Payload() interface{} {
return e
}
func (e *IDGeneratedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewIDGeneratedEvent(
ctx context.Context,
id string,
) *IDGeneratedEvent {
return &IDGeneratedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
eventstore.NewAggregate(ctx, AggregateOwner, AggregateType, "v1"),
IDGeneratedType),
ID: id,
}
}

View File

@@ -1,16 +1,26 @@
package webauthn
import (
"context"
"strings"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/domain"
)
func WebAuthNsToCredentials(webAuthNs []*domain.WebAuthNToken, rpID string) []webauthn.Credential {
func WebAuthNsToCredentials(ctx context.Context, webAuthNs []*domain.WebAuthNToken, rpID string) []webauthn.Credential {
creds := make([]webauthn.Credential, 0)
for _, webAuthN := range webAuthNs {
if webAuthN.State == domain.MFAStateReady && webAuthN.RPID == rpID {
// only add credentials that are ready and
// either match the rpID or
// if they were added through Console / old login UI, there is no stored rpID set;
// then we check if the requested rpID matches the instance domain
if webAuthN.State == domain.MFAStateReady &&
(webAuthN.RPID == rpID ||
(webAuthN.RPID == "" && rpID == strings.Split(http.DomainContext(ctx).InstanceHost, ":")[0])) {
creds = append(creds, webauthn.Credential{
ID: webAuthN.KeyID,
PublicKey: webAuthN.PublicKey,

View File

@@ -0,0 +1,153 @@
package webauthn
import (
"context"
"testing"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/domain"
)
func TestWebAuthNsToCredentials(t *testing.T) {
type args struct {
ctx context.Context
webAuthNs []*domain.WebAuthNToken
rpID string
}
tests := []struct {
name string
args args
want []webauthn.Credential
}{
{
name: "unready credential",
args: args{
ctx: context.Background(),
webAuthNs: []*domain.WebAuthNToken{
{
KeyID: []byte("key1"),
PublicKey: []byte("publicKey1"),
AttestationType: "attestation1",
AAGUID: []byte("aaguid1"),
SignCount: 1,
State: domain.MFAStateNotReady,
},
},
rpID: "example.com",
},
want: []webauthn.Credential{},
},
{
name: "not matching rpID",
args: args{
ctx: context.Background(),
webAuthNs: []*domain.WebAuthNToken{
{
KeyID: []byte("key1"),
PublicKey: []byte("publicKey1"),
AttestationType: "attestation1",
AAGUID: []byte("aaguid1"),
SignCount: 1,
State: domain.MFAStateReady,
RPID: "other.com",
},
},
rpID: "example.com",
},
want: []webauthn.Credential{},
},
{
name: "matching rpID",
args: args{
ctx: context.Background(),
webAuthNs: []*domain.WebAuthNToken{
{
KeyID: []byte("key1"),
PublicKey: []byte("publicKey1"),
AttestationType: "attestation1",
AAGUID: []byte("aaguid1"),
SignCount: 1,
State: domain.MFAStateReady,
RPID: "example.com",
},
},
rpID: "example.com",
},
want: []webauthn.Credential{
{
ID: []byte("key1"),
PublicKey: []byte("publicKey1"),
AttestationType: "attestation1",
Authenticator: webauthn.Authenticator{
AAGUID: []byte("aaguid1"),
SignCount: 1,
},
},
},
},
{
name: "no rpID, different host",
args: args{
ctx: http.WithDomainContext(context.Background(), &http.DomainCtx{
InstanceHost: "other.com:443",
PublicHost: "other.com:443",
Protocol: "https",
}),
webAuthNs: []*domain.WebAuthNToken{
{
KeyID: []byte("key1"),
PublicKey: []byte("publicKey1"),
AttestationType: "attestation1",
AAGUID: []byte("aaguid1"),
SignCount: 1,
State: domain.MFAStateReady,
RPID: "",
},
},
rpID: "example.com",
},
want: []webauthn.Credential{},
},
{
name: "no rpID, same host",
args: args{
ctx: http.WithDomainContext(context.Background(), &http.DomainCtx{
InstanceHost: "example.com:443",
PublicHost: "example.com:443",
Protocol: "https",
}),
webAuthNs: []*domain.WebAuthNToken{
{
KeyID: []byte("key1"),
PublicKey: []byte("publicKey1"),
AttestationType: "attestation1",
AAGUID: []byte("aaguid1"),
SignCount: 1,
State: domain.MFAStateReady,
RPID: "",
},
},
rpID: "example.com",
},
want: []webauthn.Credential{
{
ID: []byte("key1"),
PublicKey: []byte("publicKey1"),
AttestationType: "attestation1",
Authenticator: webauthn.Authenticator{
AAGUID: []byte("aaguid1"),
SignCount: 1,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, WebAuthNsToCredentials(tt.args.ctx, tt.args.webAuthNs, tt.args.rpID), "WebAuthNsToCredentials(%v, %v, %v)", tt.args.ctx, tt.args.webAuthNs, tt.args.rpID)
})
}
}

View File

@@ -57,7 +57,7 @@ func (w *Config) BeginRegistration(ctx context.Context, user *domain.Human, acco
if err != nil {
return nil, err
}
creds := WebAuthNsToCredentials(webAuthNs, rpID)
creds := WebAuthNsToCredentials(ctx, webAuthNs, rpID)
existing := make([]protocol.CredentialDescriptor, len(creds))
for i, cred := range creds {
existing[i] = protocol.CredentialDescriptor{
@@ -136,7 +136,7 @@ func (w *Config) BeginLogin(ctx context.Context, user *domain.Human, userVerific
}
assertion, sessionData, err := webAuthNServer.BeginLogin(&webUser{
Human: user,
credentials: WebAuthNsToCredentials(webAuthNs, rpID),
credentials: WebAuthNsToCredentials(ctx, webAuthNs, rpID),
}, webauthn.WithUserVerification(UserVerificationFromDomain(userVerification)))
if err != nil {
logging.WithFields("error", tryExtractProtocolErrMsg(err)).Debug("webauthn login could not be started")
@@ -163,7 +163,7 @@ func (w *Config) FinishLogin(ctx context.Context, user *domain.Human, webAuthN *
}
webUser := &webUser{
Human: user,
credentials: WebAuthNsToCredentials(webAuthNs, webAuthN.RPID),
credentials: WebAuthNsToCredentials(ctx, webAuthNs, webAuthN.RPID),
}
webAuthNServer, err := w.serverFromContext(ctx, webAuthN.RPID, assertionData.Response.CollectedClientData.Origin)
if err != nil {