mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 04:07:31 +00:00
fixup! fixup! fixup! Merge branch 'main' into fix_adding_org_same_id_twice
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
@@ -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),
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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"
|
||||
|
@@ -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
|
||||
|
@@ -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 {
|
||||
|
153
internal/serviceping/client.go
Normal file
153
internal/serviceping/client.go
Normal 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)
|
||||
}
|
18
internal/serviceping/config.go
Normal file
18
internal/serviceping/config.go
Normal 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
|
||||
}
|
5
internal/serviceping/mock/mock_gen.go
Normal file
5
internal/serviceping/mock/mock_gen.go
Normal 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
|
72
internal/serviceping/mock/queries.mock.go
Normal file
72
internal/serviceping/mock/queries.mock.go
Normal 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)
|
||||
}
|
62
internal/serviceping/mock/queue.mock.go
Normal file
62
internal/serviceping/mock/queue.mock.go
Normal 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...)
|
||||
}
|
83
internal/serviceping/mock/telemetry.mock.go
Normal file
83
internal/serviceping/mock/telemetry.mock.go
Normal 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...)
|
||||
}
|
17
internal/serviceping/report.go
Normal file
17
internal/serviceping/report.go
Normal 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"
|
||||
}
|
252
internal/serviceping/worker.go
Normal file
252
internal/serviceping/worker.go
Normal 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
|
||||
}
|
1052
internal/serviceping/worker_test.go
Normal file
1052
internal/serviceping/worker_test.go
Normal file
File diff suppressed because it is too large
Load Diff
44
internal/v2/system/event.go
Normal file
44
internal/v2/system/event.go
Normal 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,
|
||||
}
|
||||
}
|
@@ -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,
|
||||
|
153
internal/webauthn/converter_test.go
Normal file
153
internal/webauthn/converter_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
|
Reference in New Issue
Block a user