diff --git a/cmd/start/start.go b/cmd/start/start.go index 9c1e2a4d28..adbac7f822 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -482,7 +482,7 @@ func startAPIs( if err := apis.RegisterService(ctx, session_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, settings_v2beta.CreateServer(commands, queries)); err != nil { + if err := apis.RegisterService(ctx, settings_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { return nil, err } if err := apis.RegisterService(ctx, org_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { diff --git a/internal/api/grpc/settings/v2/server.go b/internal/api/grpc/settings/v2/server.go index bfaec17fc2..3f7c2a02ec 100644 --- a/internal/api/grpc/settings/v2/server.go +++ b/internal/api/grpc/settings/v2/server.go @@ -19,8 +19,9 @@ import ( var _ settingsconnect.SettingsServiceHandler = (*Server)(nil) type Server struct { - command *command.Commands - query *query.Queries + command *command.Commands + query *query.Queries + assetsAPIDomain func(context.Context) string } diff --git a/internal/api/grpc/settings/v2beta/integration_test/query_test.go b/internal/api/grpc/settings/v2beta/integration_test/query_test.go new file mode 100644 index 0000000000..7886bac539 --- /dev/null +++ b/internal/api/grpc/settings/v2beta/integration_test/query_test.go @@ -0,0 +1,281 @@ +//go:build integration + +package settings_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" +) + +func TestServer_ListOrganizationSettings(t *testing.T) { + instance := integration.NewInstance(CTX) + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + type args struct { + ctx context.Context + dep func(*settings.ListOrganizationSettingsRequest, *settings.ListOrganizationSettingsResponse) + req *settings.ListOrganizationSettingsRequest + } + tests := []struct { + name string + args args + want *settings.ListOrganizationSettingsResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *settings.ListOrganizationSettingsRequest, response *settings.ListOrganizationSettingsResponse) { + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.SetOrganizationSettings(iamOwnerCtx, t, orgResp.GetOrganizationId(), true) + + request.Filters[0].Filter = &settings.OrganizationSettingsSearchFilter_InOrganizationIdsFilter{ + InOrganizationIdsFilter: &filter.InIDsFilter{ + Ids: []string{orgResp.GetOrganizationId()}, + }, + } + }, + req: &settings.ListOrganizationSettingsRequest{ + Filters: []*settings.OrganizationSettingsSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + dep: func(request *settings.ListOrganizationSettingsRequest, response *settings.ListOrganizationSettingsResponse) { + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.SetOrganizationSettings(iamOwnerCtx, t, orgResp.GetOrganizationId(), true) + + request.Filters[0].Filter = &settings.OrganizationSettingsSearchFilter_InOrganizationIdsFilter{ + InOrganizationIdsFilter: &filter.InIDsFilter{ + Ids: []string{orgResp.GetOrganizationId()}, + }, + } + }, + req: &settings.ListOrganizationSettingsRequest{ + Filters: []*settings.OrganizationSettingsSearchFilter{{}}, + }, + }, + want: &settings.ListOrganizationSettingsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + OrganizationSettings: []*settings.OrganizationSettings{}, + }, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *settings.ListOrganizationSettingsRequest, response *settings.ListOrganizationSettingsResponse) { + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.SetOrganizationSettings(iamOwnerCtx, t, orgResp.GetOrganizationId(), true) + + request.Filters[0].Filter = &settings.OrganizationSettingsSearchFilter_InOrganizationIdsFilter{ + InOrganizationIdsFilter: &filter.InIDsFilter{ + Ids: []string{orgResp.GetOrganizationId()}, + }, + } + }, + req: &settings.ListOrganizationSettingsRequest{ + Filters: []*settings.OrganizationSettingsSearchFilter{{}}, + }, + }, + want: &settings.ListOrganizationSettingsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + OrganizationSettings: []*settings.OrganizationSettings{}, + }, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + + req: &settings.ListOrganizationSettingsRequest{ + Filters: []*settings.OrganizationSettingsSearchFilter{{ + Filter: &settings.OrganizationSettingsSearchFilter_InOrganizationIdsFilter{ + InOrganizationIdsFilter: &filter.InIDsFilter{ + Ids: []string{"notexisting"}, + }, + }, + }}, + }, + }, + want: &settings.ListOrganizationSettingsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + OrganizationSettings: []*settings.OrganizationSettings{}, + }, + }, + { + name: "list single id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *settings.ListOrganizationSettingsRequest, response *settings.ListOrganizationSettingsResponse) { + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + settingsResp := instance.SetOrganizationSettings(iamOwnerCtx, t, orgResp.GetOrganizationId(), true) + + request.Filters[0].Filter = &settings.OrganizationSettingsSearchFilter_InOrganizationIdsFilter{ + InOrganizationIdsFilter: &filter.InIDsFilter{ + Ids: []string{orgResp.GetOrganizationId()}, + }, + } + response.OrganizationSettings[0] = &settings.OrganizationSettings{ + OrganizationId: orgResp.GetOrganizationId(), + CreationDate: settingsResp.GetSetDate(), + ChangeDate: settingsResp.GetSetDate(), + OrganizationScopedUsernames: true, + } + }, + req: &settings.ListOrganizationSettingsRequest{ + Filters: []*settings.OrganizationSettingsSearchFilter{{}}, + }, + }, + want: &settings.ListOrganizationSettingsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + OrganizationSettings: []*settings.OrganizationSettings{{}}, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *settings.ListOrganizationSettingsRequest, response *settings.ListOrganizationSettingsResponse) { + orgResp1 := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + settingsResp1 := instance.SetOrganizationSettings(iamOwnerCtx, t, orgResp1.GetOrganizationId(), true) + orgResp2 := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + settingsResp2 := instance.SetOrganizationSettings(iamOwnerCtx, t, orgResp2.GetOrganizationId(), true) + orgResp3 := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + settingsResp3 := instance.SetOrganizationSettings(iamOwnerCtx, t, orgResp3.GetOrganizationId(), true) + + request.Filters[0].Filter = &settings.OrganizationSettingsSearchFilter_InOrganizationIdsFilter{ + InOrganizationIdsFilter: &filter.InIDsFilter{ + Ids: []string{orgResp1.GetOrganizationId(), orgResp2.GetOrganizationId(), orgResp3.GetOrganizationId()}, + }, + } + response.OrganizationSettings[2] = &settings.OrganizationSettings{ + OrganizationId: orgResp1.GetOrganizationId(), + CreationDate: settingsResp1.GetSetDate(), + ChangeDate: settingsResp1.GetSetDate(), + OrganizationScopedUsernames: true, + } + response.OrganizationSettings[1] = &settings.OrganizationSettings{ + OrganizationId: orgResp2.GetOrganizationId(), + CreationDate: settingsResp2.GetSetDate(), + ChangeDate: settingsResp2.GetSetDate(), + OrganizationScopedUsernames: true, + } + response.OrganizationSettings[0] = &settings.OrganizationSettings{ + OrganizationId: orgResp3.GetOrganizationId(), + CreationDate: settingsResp3.GetSetDate(), + ChangeDate: settingsResp3.GetSetDate(), + OrganizationScopedUsernames: true, + } + }, + req: &settings.ListOrganizationSettingsRequest{ + Filters: []*settings.OrganizationSettingsSearchFilter{{}}, + }, + }, + want: &settings.ListOrganizationSettingsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + OrganizationSettings: []*settings.OrganizationSettings{{}, {}, {}}, + }, + }, + { + name: "list multiple id, only org scoped usernames", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *settings.ListOrganizationSettingsRequest, response *settings.ListOrganizationSettingsResponse) { + orgResp1 := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.SetOrganizationSettings(iamOwnerCtx, t, orgResp1.GetOrganizationId(), false) + orgResp2 := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + settingsResp2 := instance.SetOrganizationSettings(iamOwnerCtx, t, orgResp2.GetOrganizationId(), true) + orgResp3 := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.SetOrganizationSettings(iamOwnerCtx, t, orgResp3.GetOrganizationId(), false) + + request.Filters[0].Filter = &settings.OrganizationSettingsSearchFilter_InOrganizationIdsFilter{ + InOrganizationIdsFilter: &filter.InIDsFilter{ + Ids: []string{orgResp1.GetOrganizationId(), orgResp2.GetOrganizationId(), orgResp3.GetOrganizationId()}, + }, + } + request.Filters[1].Filter = &settings.OrganizationSettingsSearchFilter_OrganizationScopedUsernamesFilter{ + OrganizationScopedUsernamesFilter: &settings.OrganizationScopedUsernamesFilter{ + OrganizationScopedUsernames: true, + }, + } + response.OrganizationSettings[0] = &settings.OrganizationSettings{ + OrganizationId: orgResp2.GetOrganizationId(), + CreationDate: settingsResp2.GetSetDate(), + ChangeDate: settingsResp2.GetSetDate(), + OrganizationScopedUsernames: true, + } + }, + req: &settings.ListOrganizationSettingsRequest{ + Filters: []*settings.OrganizationSettingsSearchFilter{{}, {}}, + }, + }, + want: &settings.ListOrganizationSettingsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + OrganizationSettings: []*settings.OrganizationSettings{{}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instance.Client.SettingsV2beta.ListOrganizationSettings(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.OrganizationSettings, len(tt.want.OrganizationSettings)) { + for i := range tt.want.OrganizationSettings { + assert.EqualExportedValues(ttt, tt.want.OrganizationSettings[i], got.OrganizationSettings[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationResponse, actual *filter.PaginationResponse) { + assert.Equal(t, expected.AppliedLimit, actual.AppliedLimit) + assert.Equal(t, expected.TotalResult, actual.TotalResult) +} diff --git a/internal/api/grpc/settings/v2beta/integration_test/settings_test.go b/internal/api/grpc/settings/v2beta/integration_test/settings_test.go index d5c1914ba9..4e028fb6d5 100644 --- a/internal/api/grpc/settings/v2beta/integration_test/settings_test.go +++ b/internal/api/grpc/settings/v2beta/integration_test/settings_test.go @@ -7,6 +7,8 @@ import ( "testing" "time" + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" @@ -178,3 +180,279 @@ func TestServer_SetSecuritySettings(t *testing.T) { }) } } + +func TestServer_SetOrganizationSettings(t *testing.T) { + instance := integration.NewInstance(CTX) + iamOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + type args struct { + ctx context.Context + req *settings.SetOrganizationSettingsRequest + } + type want struct { + set bool + setDate bool + } + tests := []struct { + name string + prepare func(req *settings.SetOrganizationSettingsRequest) + args args + want want + wantErr bool + }{ + { + name: "permission error", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &settings.SetOrganizationSettingsRequest{ + OrganizationId: Instance.DefaultOrg.GetId(), + OrganizationScopedUsernames: gu.Ptr(true), + }, + }, + wantErr: true, + }, + { + name: "org not provided", + args: args{ + ctx: iamOwnerCTX, + req: &settings.SetOrganizationSettingsRequest{ + OrganizationId: "", + OrganizationScopedUsernames: gu.Ptr(true), + }, + }, + wantErr: true, + }, + { + name: "org not existing", + args: args{ + ctx: iamOwnerCTX, + req: &settings.SetOrganizationSettingsRequest{ + OrganizationId: "notexisting", + OrganizationScopedUsernames: gu.Ptr(true), + }, + }, + wantErr: true, + }, + { + name: "success no changes", + prepare: func(req *settings.SetOrganizationSettingsRequest) { + orgResp := instance.CreateOrganization(iamOwnerCTX, gofakeit.Company(), gofakeit.Email()) + req.OrganizationId = orgResp.GetOrganizationId() + }, + args: args{ + ctx: iamOwnerCTX, + req: &settings.SetOrganizationSettingsRequest{}, + }, + want: want{ + set: false, + setDate: true, + }, + }, + { + name: "success user uniqueness", + prepare: func(req *settings.SetOrganizationSettingsRequest) { + orgResp := instance.CreateOrganization(iamOwnerCTX, gofakeit.Company(), gofakeit.Email()) + req.OrganizationId = orgResp.GetOrganizationId() + }, + args: args{ + ctx: iamOwnerCTX, + req: &settings.SetOrganizationSettingsRequest{ + OrganizationScopedUsernames: gu.Ptr(true), + }, + }, + want: want{ + set: true, + setDate: true, + }, + }, + { + name: "success no change", + prepare: func(req *settings.SetOrganizationSettingsRequest) { + orgResp := instance.CreateOrganization(iamOwnerCTX, gofakeit.Company(), gofakeit.Email()) + req.OrganizationId = orgResp.GetOrganizationId() + }, + args: args{ + ctx: iamOwnerCTX, + req: &settings.SetOrganizationSettingsRequest{ + OrganizationScopedUsernames: gu.Ptr(false), + }, + }, + want: want{ + set: false, + setDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + if tt.prepare != nil { + tt.prepare(tt.args.req) + } + + got, err := instance.Client.SettingsV2beta.SetOrganizationSettings(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + setDate := time.Time{} + if tt.want.set { + setDate = time.Now().UTC() + } + assert.NoError(t, err) + assertOrganizationSettingsResponse(t, creationDate, setDate, tt.want.setDate, got) + }) + } +} + +func assertOrganizationSettingsResponse(t *testing.T, creationDate, setDate time.Time, expectedSetDate bool, actualResp *settings.SetOrganizationSettingsResponse) { + if expectedSetDate { + if !setDate.IsZero() { + assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, setDate) + } else { + assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.SetDate) + } +} + +func TestServer_DeleteOrganizationSettings(t *testing.T) { + instance := integration.NewInstance(CTX) + iamOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + type args struct { + ctx context.Context + req *settings.DeleteOrganizationSettingsRequest + } + type want struct { + deletion bool + deletionDate bool + } + tests := []struct { + name string + prepare func(t *testing.T, req *settings.DeleteOrganizationSettingsRequest) + args args + want want + wantErr bool + }{ + { + name: "permission error", + prepare: func(t *testing.T, req *settings.DeleteOrganizationSettingsRequest) { + orgResp := instance.CreateOrganization(iamOwnerCTX, gofakeit.Company(), gofakeit.Email()) + req.OrganizationId = orgResp.GetOrganizationId() + instance.SetOrganizationSettings(iamOwnerCTX, t, orgResp.GetOrganizationId(), true) + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &settings.DeleteOrganizationSettingsRequest{ + OrganizationId: Instance.DefaultOrg.GetId(), + }, + }, + wantErr: true, + }, + { + name: "org not provided", + args: args{ + ctx: iamOwnerCTX, + req: &settings.DeleteOrganizationSettingsRequest{ + OrganizationId: "", + }, + }, + wantErr: true, + }, + { + name: "org not existing", + args: args{ + ctx: iamOwnerCTX, + req: &settings.DeleteOrganizationSettingsRequest{ + OrganizationId: "notexisting", + }, + }, + want: want{ + deletion: false, + deletionDate: false, + }, + }, + { + name: "success user uniqueness", + prepare: func(t *testing.T, req *settings.DeleteOrganizationSettingsRequest) { + orgResp := instance.CreateOrganization(iamOwnerCTX, gofakeit.Company(), gofakeit.Email()) + req.OrganizationId = orgResp.GetOrganizationId() + instance.SetOrganizationSettings(iamOwnerCTX, t, orgResp.GetOrganizationId(), true) + }, + args: args{ + ctx: iamOwnerCTX, + req: &settings.DeleteOrganizationSettingsRequest{}, + }, + want: want{ + deletion: true, + deletionDate: true, + }, + }, + { + name: "success no existing", + prepare: func(t *testing.T, req *settings.DeleteOrganizationSettingsRequest) { + orgResp := instance.CreateOrganization(iamOwnerCTX, gofakeit.Company(), gofakeit.Email()) + req.OrganizationId = orgResp.GetOrganizationId() + }, + args: args{ + ctx: iamOwnerCTX, + req: &settings.DeleteOrganizationSettingsRequest{}, + }, + want: want{ + deletion: false, + deletionDate: true, + }, + }, + { + name: "success already deleted", + prepare: func(t *testing.T, req *settings.DeleteOrganizationSettingsRequest) { + orgResp := instance.CreateOrganization(iamOwnerCTX, gofakeit.Company(), gofakeit.Email()) + req.OrganizationId = orgResp.GetOrganizationId() + instance.SetOrganizationSettings(iamOwnerCTX, t, orgResp.GetOrganizationId(), true) + instance.DeleteOrganizationSettings(iamOwnerCTX, t, orgResp.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCTX, + req: &settings.DeleteOrganizationSettingsRequest{}, + }, + want: want{ + deletion: false, + deletionDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + if tt.prepare != nil { + tt.prepare(t, tt.args.req) + } + + got, err := instance.Client.SettingsV2beta.DeleteOrganizationSettings(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + deletionDate := time.Time{} + if tt.want.deletion { + deletionDate = time.Now().UTC() + } + assert.NoError(t, err) + assertDeleteOrganizationSettingsResponse(t, creationDate, deletionDate, tt.want.deletionDate, got) + }) + } +} + +func assertDeleteOrganizationSettingsResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *settings.DeleteOrganizationSettingsResponse) { + if expectedDeletionDate { + if !deletionDate.IsZero() { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, deletionDate) + } else { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.DeletionDate) + } +} diff --git a/internal/api/grpc/settings/v2beta/query.go b/internal/api/grpc/settings/v2beta/query.go new file mode 100644 index 0000000000..ac07031ed9 --- /dev/null +++ b/internal/api/grpc/settings/v2beta/query.go @@ -0,0 +1,114 @@ +package settings + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" +) + +func (s *Server) ListOrganizationSettings(ctx context.Context, req *connect.Request[settings.ListOrganizationSettingsRequest]) (*connect.Response[settings.ListOrganizationSettingsResponse], error) { + queries, err := s.listOrganizationSettingsRequestToModel(req.Msg) + if err != nil { + return nil, err + } + resp, err := s.query.SearchOrganizationSettings(ctx, queries, s.checkPermission) + if err != nil { + return nil, err + } + return connect.NewResponse(&settings.ListOrganizationSettingsResponse{ + OrganizationSettings: organizationSettingsListToPb(resp.OrganizationSettingsList), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), + }), nil +} + +func (s *Server) listOrganizationSettingsRequestToModel(req *settings.ListOrganizationSettingsRequest) (*query.OrganizationSettingsSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := organizationSettingsFiltersToQuery(req.Filters) + if err != nil { + return nil, err + } + return &query.OrganizationSettingsSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: organizationSettingsFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func organizationSettingsFieldNameToSortingColumn(field *settings.OrganizationSettingsFieldName) query.Column { + if field == nil { + return query.OrganizationSettingsColumnCreationDate + } + switch *field { + case settings.OrganizationSettingsFieldName_ORGANIZATION_SETTINGS_FIELD_NAME_CREATION_DATE: + return query.OrganizationSettingsColumnCreationDate + case settings.OrganizationSettingsFieldName_ORGANIZATION_SETTINGS_FIELD_NAME_ORGANIZATION_ID: + return query.OrganizationSettingsColumnID + case settings.OrganizationSettingsFieldName_ORGANIZATION_SETTINGS_FIELD_NAME_CHANGE_DATE: + return query.OrganizationSettingsColumnChangeDate + case settings.OrganizationSettingsFieldName_ORGANIZATION_SETTINGS_FIELD_NAME_UNSPECIFIED: + return query.OrganizationSettingsColumnCreationDate + default: + return query.OrganizationSettingsColumnCreationDate + } +} + +func organizationSettingsFiltersToQuery(queries []*settings.OrganizationSettingsSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, qry := range queries { + q[i], err = organizationSettingsToModel(qry) + if err != nil { + return nil, err + } + } + return q, nil +} + +func organizationSettingsToModel(filter *settings.OrganizationSettingsSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *settings.OrganizationSettingsSearchFilter_InOrganizationIdsFilter: + return organizationInIDsFilterToQuery(q.InOrganizationIdsFilter) + case *settings.OrganizationSettingsSearchFilter_OrganizationScopedUsernamesFilter: + return organizationScopedUsernamesFilterToQuery(q.OrganizationScopedUsernamesFilter) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "SETTINGS-uvTDqZHlvS", "List.Query.Invalid") + } +} + +func organizationInIDsFilterToQuery(q *filter_pb.InIDsFilter) (query.SearchQuery, error) { + return query.NewOrganizationSettingsOrganizationIDSearchQuery(q.Ids) +} + +func organizationScopedUsernamesFilterToQuery(q *settings.OrganizationScopedUsernamesFilter) (query.SearchQuery, error) { + return query.NewOrganizationSettingsOrganizationScopedUsernamesSearchQuery(q.OrganizationScopedUsernames) +} + +func organizationSettingsListToPb(settingsList []*query.OrganizationSettings) []*settings.OrganizationSettings { + o := make([]*settings.OrganizationSettings, len(settingsList)) + for i, organizationSettings := range settingsList { + o[i] = organizationSettingsToPb(organizationSettings) + } + return o +} + +func organizationSettingsToPb(organizationSettings *query.OrganizationSettings) *settings.OrganizationSettings { + return &settings.OrganizationSettings{ + OrganizationId: organizationSettings.ID, + CreationDate: timestamppb.New(organizationSettings.CreationDate), + ChangeDate: timestamppb.New(organizationSettings.ChangeDate), + OrganizationScopedUsernames: organizationSettings.OrganizationScopedUsernames, + } +} diff --git a/internal/api/grpc/settings/v2beta/server.go b/internal/api/grpc/settings/v2beta/server.go index a8200a7216..3eea310006 100644 --- a/internal/api/grpc/settings/v2beta/server.go +++ b/internal/api/grpc/settings/v2beta/server.go @@ -11,6 +11,8 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta/settingsconnect" @@ -19,20 +21,27 @@ import ( var _ settingsconnect.SettingsServiceHandler = (*Server)(nil) type Server struct { - command *command.Commands - query *query.Queries + systemDefaults systemdefaults.SystemDefaults + command *command.Commands + query *query.Queries + + checkPermission domain.PermissionCheck assetsAPIDomain func(context.Context) string } type Config struct{} func CreateServer( + systemDefaults systemdefaults.SystemDefaults, command *command.Commands, query *query.Queries, + checkPermission domain.PermissionCheck, ) *Server { return &Server{ + systemDefaults: systemDefaults, command: command, query: query, + checkPermission: checkPermission, assetsAPIDomain: assets.AssetAPI(), } } diff --git a/internal/api/grpc/settings/v2beta/settings.go b/internal/api/grpc/settings/v2beta/settings.go index 53d2c37c32..7388c0449a 100644 --- a/internal/api/grpc/settings/v2beta/settings.go +++ b/internal/api/grpc/settings/v2beta/settings.go @@ -167,3 +167,31 @@ func (s *Server) SetSecuritySettings(ctx context.Context, req *connect.Request[s Details: object.DomainToDetailsPb(details), }), nil } + +func (s *Server) SetOrganizationSettings(ctx context.Context, req *connect.Request[settings.SetOrganizationSettingsRequest]) (*connect.Response[settings.SetOrganizationSettingsResponse], error) { + details, err := s.command.SetOrganizationSettings(ctx, organizationSettingsToCommand(req.Msg)) + if err != nil { + return nil, err + } + var setDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + setDate = timestamppb.New(details.EventDate) + } + return connect.NewResponse(&settings.SetOrganizationSettingsResponse{ + SetDate: setDate, + }), nil +} + +func (s *Server) DeleteOrganizationSettings(ctx context.Context, req *connect.Request[settings.DeleteOrganizationSettingsRequest]) (*connect.Response[settings.DeleteOrganizationSettingsResponse], error) { + details, err := s.command.DeleteOrganizationSettings(ctx, req.Msg.GetOrganizationId()) + if err != nil { + return nil, err + } + var deletionDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + deletionDate = timestamppb.New(details.EventDate) + } + return connect.NewResponse(&settings.DeleteOrganizationSettingsResponse{ + DeletionDate: deletionDate, + }), nil +} diff --git a/internal/api/grpc/settings/v2beta/settings_converter.go b/internal/api/grpc/settings/v2beta/settings_converter.go index 2b20e738e1..ad4ad2ebab 100644 --- a/internal/api/grpc/settings/v2beta/settings_converter.go +++ b/internal/api/grpc/settings/v2beta/settings_converter.go @@ -243,3 +243,10 @@ func securitySettingsToCommand(req *settings.SetSecuritySettingsRequest) *comman EnableImpersonation: req.GetEnableImpersonation(), } } + +func organizationSettingsToCommand(req *settings.SetOrganizationSettingsRequest) *command.SetOrganizationSettings { + return &command.SetOrganizationSettings{ + OrganizationID: req.OrganizationId, + OrganizationScopedUsernames: req.OrganizationScopedUsernames, + } +} diff --git a/internal/command/instance_policy_domain.go b/internal/command/instance_policy_domain.go index 969bc219fe..449475f8d6 100644 --- a/internal/command/instance_policy_domain.go +++ b/internal/command/instance_policy_domain.go @@ -130,11 +130,20 @@ func prepareChangeDefaultDomainPolicy( // loop over all found organisations to get their usernames // and to compute the username changed events for _, orgID := range orgsWriteModel.OrgIDs { + organizationScopedUsernames, err := checkOrganizationScopedUsernames(ctx, filter, a.ID, nil) + if err != nil { + return nil, err + } + usersWriteModel, err := domainPolicyUsernames(ctx, filter, orgID) if err != nil { return nil, err } - cmds = append(cmds, usersWriteModel.NewUsernameChangedEvents(ctx, userLoginMustBeDomain)...) + cmds = append(cmds, usersWriteModel.NewUsernameChangedEvents(ctx, + userLoginMustBeDomain, + organizationScopedUsernames, + writeModel.UserLoginMustBeDomain, + )...) } return cmds, nil }, nil diff --git a/internal/command/instance_policy_domain_test.go b/internal/command/instance_policy_domain_test.go index 745d4a7efe..17c975b800 100644 --- a/internal/command/instance_policy_domain_test.go +++ b/internal/command/instance_policy_domain_test.go @@ -19,7 +19,7 @@ import ( func TestCommandSide_AddDefaultDomainPolicy(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -40,8 +40,7 @@ func TestCommandSide_AddDefaultDomainPolicy(t *testing.T) { { name: "domain policy already existing, already exists error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewDomainPolicyAddedEvent(context.Background(), @@ -67,8 +66,7 @@ func TestCommandSide_AddDefaultDomainPolicy(t *testing.T) { { name: "add policy,ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), expectPush( instance.NewDomainPolicyAddedEvent(context.Background(), @@ -96,7 +94,7 @@ func TestCommandSide_AddDefaultDomainPolicy(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } got, err := r.AddDefaultDomainPolicy(tt.args.ctx, tt.args.userLoginMustBeDomain, tt.args.validateOrgDomains, tt.args.smtpSenderAddressMatchesInstanceDomain) if tt.res.err == nil { @@ -114,7 +112,7 @@ func TestCommandSide_AddDefaultDomainPolicy(t *testing.T) { func TestCommandSide_ChangeDefaultDomainPolicy(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -135,8 +133,7 @@ func TestCommandSide_ChangeDefaultDomainPolicy(t *testing.T) { { name: "domain policy not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -153,8 +150,7 @@ func TestCommandSide_ChangeDefaultDomainPolicy(t *testing.T) { { name: "no changes, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewDomainPolicyAddedEvent(context.Background(), @@ -180,8 +176,7 @@ func TestCommandSide_ChangeDefaultDomainPolicy(t *testing.T) { { name: "change, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewDomainPolicyAddedEvent(context.Background(), @@ -236,6 +231,7 @@ func TestCommandSide_ChangeDefaultDomainPolicy(t *testing.T) { ), // domainPolicyUsernames for each org // org1 + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewDomainPrimarySetEvent( @@ -266,6 +262,7 @@ func TestCommandSide_ChangeDefaultDomainPolicy(t *testing.T) { ), ), // org3 + expectFilterOrganizationSettings("org3", false, false), expectFilter( eventFromEventPusher( org.NewDomainPrimarySetEvent( @@ -302,14 +299,16 @@ func TestCommandSide_ChangeDefaultDomainPolicy(t *testing.T) { "user1", "user1@org1.com", false, - user.UsernameChangedEventWithPolicyChange(), + false, + user.UsernameChangedEventWithPolicyChange(true), ), user.NewUsernameChangedEvent(context.Background(), &user.NewAggregate("user1", "org3").Aggregate, "user1", "user1@org3.com", false, - user.UsernameChangedEventWithPolicyChange(), + false, + user.UsernameChangedEventWithPolicyChange(true), ), ), ), @@ -326,11 +325,315 @@ func TestCommandSide_ChangeDefaultDomainPolicy(t *testing.T) { }, }, }, + { + name: "change, organization scoped usernames, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewDomainPolicyAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + true, + true, + true, + ), + ), + ), + // domainPolicyOrgs + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org1", + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org2").Aggregate, + "org2", + ), + ), + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &org.NewAggregate("org2").Aggregate, + false, + false, + false, + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org3").Aggregate, + "org3", + ), + ), + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &org.NewAggregate("org3").Aggregate, + false, + false, + false, + ), + ), + eventFromEventPusher( + org.NewDomainPolicyRemovedEvent(context.Background(), + &org.NewAggregate("org3").Aggregate, + ), + ), + ), + // domainPolicyUsernames for each org + // org1 + expectFilterOrganizationSettings("org1", true, true), + expectFilter( + eventFromEventPusher( + org.NewDomainPrimarySetEvent( + context.Background(), + &org.NewAggregate("org1").Aggregate, + "org1.com", + ), + ), + eventFromEventPusher( + org.NewDomainPrimarySetEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org1.com", + ), + ), + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1", + "firstname", + "lastname", + "nickname", + "displayname", + language.English, + domain.GenderUnspecified, + "user1@org1.com", + false, + ), + ), + ), + // org3 + expectFilterOrganizationSettings("org3", true, true), + expectFilter( + eventFromEventPusher( + org.NewDomainPrimarySetEvent( + context.Background(), + &org.NewAggregate("org3").Aggregate, + "org3.com", + ), + ), + eventFromEventPusher( + org.NewDomainPrimarySetEvent(context.Background(), + &org.NewAggregate("org3").Aggregate, + "org3.com", + ), + ), + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org3").Aggregate, + "user1", + "firstname", + "lastname", + "nickname", + "displayname", + language.English, + domain.GenderUnspecified, + "user1@org3.com", + false, + ), + ), + ), + expectPush( + newDefaultDomainPolicyChangedEvent(context.Background(), false, false, false), + user.NewUsernameChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1", + "user1@org1.com", + false, + true, + user.UsernameChangedEventWithPolicyChange(true), + ), + user.NewUsernameChangedEvent(context.Background(), + &user.NewAggregate("user1", "org3").Aggregate, + "user1", + "user1@org3.com", + false, + true, + user.UsernameChangedEventWithPolicyChange(true), + ), + ), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + userLoginMustBeDomain: false, + validateOrgDomains: false, + smtpSenderAddressMatchesInstanceDomain: false, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "change, organization scoped usernames, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewDomainPolicyAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + false, + true, + true, + ), + ), + ), + // domainPolicyOrgs + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org1", + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org2").Aggregate, + "org2", + ), + ), + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &org.NewAggregate("org2").Aggregate, + true, + false, + false, + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org3").Aggregate, + "org3", + ), + ), + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &org.NewAggregate("org3").Aggregate, + true, + false, + false, + ), + ), + eventFromEventPusher( + org.NewDomainPolicyRemovedEvent(context.Background(), + &org.NewAggregate("org3").Aggregate, + ), + ), + ), + // domainPolicyUsernames for each org + // org1 + expectFilterOrganizationSettings("org1", true, true), + expectFilter( + eventFromEventPusher( + org.NewDomainPrimarySetEvent( + context.Background(), + &org.NewAggregate("org1").Aggregate, + "org1.com", + ), + ), + eventFromEventPusher( + org.NewDomainPrimarySetEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org1.com", + ), + ), + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1@org1.com", + "firstname", + "lastname", + "nickname", + "displayname", + language.English, + domain.GenderUnspecified, + "user1@org1.com", + false, + ), + ), + ), + // org3 + expectFilterOrganizationSettings("org3", true, true), + expectFilter( + eventFromEventPusher( + org.NewDomainPrimarySetEvent( + context.Background(), + &org.NewAggregate("org3").Aggregate, + "org3.com", + ), + ), + eventFromEventPusher( + org.NewDomainPrimarySetEvent(context.Background(), + &org.NewAggregate("org3").Aggregate, + "org3.com", + ), + ), + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org3").Aggregate, + "user1@org3.com", + "firstname", + "lastname", + "nickname", + "displayname", + language.English, + domain.GenderUnspecified, + "user1@org3.com", + true, + ), + ), + ), + expectPush( + newDefaultDomainPolicyChangedEvent(context.Background(), true, false, false), + user.NewUsernameChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1@org1.com", + "user1@org1.com", + true, + true, + user.UsernameChangedEventWithPolicyChange(false), + ), + user.NewUsernameChangedEvent(context.Background(), + &user.NewAggregate("user1", "org3").Aggregate, + "user1@org3.com", + "user1@org3.com", + true, + true, + user.UsernameChangedEventWithPolicyChange(true), + ), + ), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + userLoginMustBeDomain: true, + validateOrgDomains: false, + smtpSenderAddressMatchesInstanceDomain: false, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } got, err := r.ChangeDefaultDomainPolicy(tt.args.ctx, tt.args.userLoginMustBeDomain, tt.args.validateOrgDomains, tt.args.smtpSenderAddressMatchesInstanceDomain) if tt.res.err == nil { diff --git a/internal/command/instance_test.go b/internal/command/instance_test.go index b40bba19af..0eca521a7f 100644 --- a/internal/command/instance_test.go +++ b/internal/command/instance_test.go @@ -478,6 +478,7 @@ func humanFilters(orgID string) []expect { true, ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( org.NewPasswordComplexityPolicyAddedEvent( context.Background(), @@ -519,6 +520,7 @@ func machineFilters(orgID string, pat bool) []expect { true, ), ), + expectFilterOrganizationSettings("org1", false, false), } if pat { filters = append(filters, @@ -562,6 +564,7 @@ func loginClientFilters(orgID string, pat bool) []expect { true, ), ), + expectFilterOrganizationSettings("org1", false, false), } if pat { filters = append(filters, diff --git a/internal/command/org.go b/internal/command/org.go index 215fe0b5cc..6505c8eef5 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -522,10 +522,16 @@ func (c *Commands) prepareRemoveOrg(a *org.Aggregate) preparation.Validation { return nil, zerrors.ThrowNotFound(nil, "COMMA-aps2n", "Errors.Org.NotFound") } - domainPolicy, err := c.domainPolicyWriteModel(ctx, a.ID) + domainPolicy, err := domainPolicyWriteModel(ctx, filter, a.ID) if err != nil { return nil, err } + + organizationScopedUsername, err := checkOrganizationScopedUsernames(ctx, filter, a.ID, nil) + if err != nil { + return nil, err + } + usernames, err := OrgUsers(ctx, filter, a.ID) if err != nil { return nil, err @@ -542,7 +548,7 @@ func (c *Commands) prepareRemoveOrg(a *org.Aggregate) preparation.Validation { if err != nil { return nil, err } - return []eventstore.Command{org.NewOrgRemovedEvent(ctx, &a.Aggregate, writeModel.Name, usernames, domainPolicy.UserLoginMustBeDomain, domains, links, entityIds)}, nil + return []eventstore.Command{org.NewOrgRemovedEvent(ctx, &a.Aggregate, writeModel.Name, usernames, domainPolicy.UserLoginMustBeDomain || organizationScopedUsername, domains, links, entityIds)}, nil }, nil } } diff --git a/internal/command/org_domain_test.go b/internal/command/org_domain_test.go index df79710955..6aaee20c2a 100644 --- a/internal/command/org_domain_test.go +++ b/internal/command/org_domain_test.go @@ -1050,6 +1050,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { org.NewDomainPolicyAddedEvent(context.Background(), &org.NewAggregate("org2").Aggregate, false, false, false))), + expectFilterOrganizationSettings("org2", false, false), expectPush( org.NewDomainVerifiedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, @@ -1084,6 +1085,93 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { }, }, }, + { + name: "domain verification, claimed users, orgScopedUsername, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "name", + ), + ), + eventFromEventPusher( + org.NewDomainAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "domain.ch", + ), + ), + eventFromEventPusher( + org.NewDomainVerificationAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "domain.ch", + domain.OrgDomainValidationTypeDNS, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org2").Aggregate, + "username@domain.ch", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &org.NewAggregate("org2").Aggregate, + false, false, false))), + expectFilterOrganizationSettings("org2", true, true), + expectPush( + org.NewDomainVerifiedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "domain.ch", + ), + user.NewDomainClaimedEvent(http.WithRequestedHost(context.Background(), "zitadel.ch"), + &user.NewAggregate("user1", "org2").Aggregate, + "tempid@temporary.zitadel.ch", + "username@domain.ch", + true, + ), + ), + ), + alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + domainValidationFunc: validDomainVerification, + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "tempid"), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + ValidationType: domain.OrgDomainValidationTypeDNS, + }, + claimedUserIDs: []string{"user1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/command/org_policy_domain.go b/internal/command/org_policy_domain.go index c9a4fd547c..3729031629 100644 --- a/internal/command/org_policy_domain.go +++ b/internal/command/org_policy_domain.go @@ -124,13 +124,23 @@ func prepareAddOrgDomainPolicy( if instancePolicy.UserLoginMustBeDomain == userLoginMustBeDomain { return cmds, nil } + + organizationScopedUsernames, err := checkOrganizationScopedUsernames(ctx, filter, a.ID, nil) + if err != nil { + return nil, err + } + // the UserLoginMustBeDomain setting will be different from the instance // therefore get all usernames and the current primary domain usersWriteModel, err := domainPolicyUsernames(ctx, filter, a.ID) if err != nil { return nil, err } - return append(cmds, usersWriteModel.NewUsernameChangedEvents(ctx, userLoginMustBeDomain)...), nil + return append(cmds, usersWriteModel.NewUsernameChangedEvents(ctx, + userLoginMustBeDomain, + organizationScopedUsernames, + instancePolicy.UserLoginMustBeDomain, + )...), nil }, nil } } @@ -163,13 +173,22 @@ func prepareChangeOrgDomainPolicy( if !usernameChange { return cmds, err } + + organizationScopedUsernames, err := checkOrganizationScopedUsernames(ctx, filter, a.ID, nil) + if err != nil { + return nil, err + } // get all usernames and the primary domain usersWriteModel, err := domainPolicyUsernames(ctx, filter, a.ID) if err != nil { return nil, err } // to compute the username changed events - return append(cmds, usersWriteModel.NewUsernameChangedEvents(ctx, userLoginMustBeDomain)...), nil + return append(cmds, usersWriteModel.NewUsernameChangedEvents(ctx, + userLoginMustBeDomain, + organizationScopedUsernames, + writeModel.UserLoginMustBeDomain, + )...), nil }, nil } } @@ -190,13 +209,20 @@ func prepareRemoveOrgDomainPolicy( if err != nil { return nil, err } + policyChange := org.NewDomainPolicyRemovedEvent(ctx, &a.Aggregate) cmds := []eventstore.Command{ - org.NewDomainPolicyRemovedEvent(ctx, &a.Aggregate), + policyChange, } + + organizationScopedUsernames, err := checkOrganizationScopedUsernames(ctx, filter, a.ID, nil) + if err != nil { + return nil, err + } + // regardless if the UserLoginMustBeDomain setting is true or false, // if it will be the same value as currently on the instance, // then there no further changes are needed - if instancePolicy.UserLoginMustBeDomain == writeModel.UserLoginMustBeDomain { + if writeModel.UserLoginMustBeDomain == instancePolicy.UserLoginMustBeDomain { return cmds, nil } // get all usernames and the primary domain @@ -205,7 +231,11 @@ func prepareRemoveOrgDomainPolicy( return nil, err } // to compute the username changed events - return append(cmds, usersWriteModel.NewUsernameChangedEvents(ctx, instancePolicy.UserLoginMustBeDomain)...), nil + return append(cmds, usersWriteModel.NewUsernameChangedEvents(ctx, + instancePolicy.UserLoginMustBeDomain, + organizationScopedUsernames, + writeModel.UserLoginMustBeDomain, + )...), nil }, nil } } diff --git a/internal/command/org_policy_domain_test.go b/internal/command/org_policy_domain_test.go index 24020d8026..a48f7eb794 100644 --- a/internal/command/org_policy_domain_test.go +++ b/internal/command/org_policy_domain_test.go @@ -18,7 +18,7 @@ import ( func TestCommandSide_AddDomainPolicy(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -40,9 +40,7 @@ func TestCommandSide_AddDomainPolicy(t *testing.T) { { name: "org id missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -57,8 +55,7 @@ func TestCommandSide_AddDomainPolicy(t *testing.T) { { name: "policy already existing, already exists error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -85,8 +82,7 @@ func TestCommandSide_AddDomainPolicy(t *testing.T) { { name: "add policy, no userLoginMustBeDomain change, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), expectFilter( eventFromEventPusher( @@ -124,8 +120,7 @@ func TestCommandSide_AddDomainPolicy(t *testing.T) { { name: "add policy, userLoginMustBeDomain changed, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), expectFilter( eventFromEventPusher( @@ -137,6 +132,7 @@ func TestCommandSide_AddDomainPolicy(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewDomainVerifiedEvent( @@ -170,7 +166,7 @@ func TestCommandSide_AddDomainPolicy(t *testing.T) { eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, - "user1@org.com", + "user1", "firstname", "lastname", "nickname", @@ -205,17 +201,19 @@ func TestCommandSide_AddDomainPolicy(t *testing.T) { ), user.NewUsernameChangedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, - "user1@org.com", + "user1", "user1", true, - user.UsernameChangedEventWithPolicyChange(), + false, + user.UsernameChangedEventWithPolicyChange(false), ), user.NewUsernameChangedEvent(context.Background(), &user.NewAggregate("user2", "org1").Aggregate, "user@test.com", "user@test.com", true, - user.UsernameChangedEventWithPolicyChange(), + false, + user.UsernameChangedEventWithPolicyChange(false), ), ), ), @@ -233,11 +231,239 @@ func TestCommandSide_AddDomainPolicy(t *testing.T) { }, }, }, + { + name: "add policy, userLoginMustBeDomain changed, org scoped usernames, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectFilter( + eventFromEventPusher( + instance.NewDomainPolicyAddedEvent(context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + false, + false, + false, + ), + ), + ), + expectFilterOrganizationSettings("org1", true, true), + expectFilter( + eventFromEventPusher( + org.NewDomainVerifiedEvent( + context.Background(), + &org.NewAggregate("org1").Aggregate, + "org.com", + ), + ), + eventFromEventPusher( + org.NewDomainVerifiedEvent( + context.Background(), + &org.NewAggregate("org1").Aggregate, + "test.com", + ), + ), + eventFromEventPusher( + org.NewDomainRemovedEvent( + context.Background(), + &org.NewAggregate("org1").Aggregate, + "test.com", + true, + ), + ), + eventFromEventPusher( + org.NewDomainPrimarySetEvent( + context.Background(), + &org.NewAggregate("org1").Aggregate, + "org.com", + ), + ), + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1", + "firstname", + "lastname", + "nickname", + "displayname", + language.English, + domain.GenderUnspecified, + "user1@org.com", + false, + ), + ), + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user2", "org1").Aggregate, + "user@test.com", + "firstname", + "lastname", + "nickname", + "displayname", + language.English, + domain.GenderUnspecified, + "user@test.com", + false, + ), + ), + ), + expectPush( + org.NewDomainPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + true, + true, + true, + ), + user.NewUsernameChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1", + "user1", + true, + true, + user.UsernameChangedEventWithPolicyChange(false), + ), + user.NewUsernameChangedEvent(context.Background(), + &user.NewAggregate("user2", "org1").Aggregate, + "user@test.com", + "user@test.com", + true, + true, + user.UsernameChangedEventWithPolicyChange(false), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userLoginMustBeDomain: true, + validateOrgDomains: true, + smtpSenderAddressMatchesInstanceDomain: true, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "add policy, userLoginMustBeDomain removed, org scoped usernames, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectFilter( + eventFromEventPusher( + instance.NewDomainPolicyAddedEvent(context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + true, + false, + false, + ), + ), + ), + expectFilterOrganizationSettings("org1", true, true), + expectFilter( + eventFromEventPusher( + org.NewDomainVerifiedEvent( + context.Background(), + &org.NewAggregate("org1").Aggregate, + "org.com", + ), + ), + eventFromEventPusher( + org.NewDomainVerifiedEvent( + context.Background(), + &org.NewAggregate("org1").Aggregate, + "test.com", + ), + ), + eventFromEventPusher( + org.NewDomainRemovedEvent( + context.Background(), + &org.NewAggregate("org1").Aggregate, + "test.com", + true, + ), + ), + eventFromEventPusher( + org.NewDomainPrimarySetEvent( + context.Background(), + &org.NewAggregate("org1").Aggregate, + "org.com", + ), + ), + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1", + "firstname", + "lastname", + "nickname", + "displayname", + language.English, + domain.GenderUnspecified, + "user1@org.com", + true, + ), + ), + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user2", "org1").Aggregate, + "user@test.com", + "firstname", + "lastname", + "nickname", + "displayname", + language.English, + domain.GenderUnspecified, + "user@test.com", + true, + ), + ), + ), + expectPush( + org.NewDomainPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + false, + true, + true, + ), + user.NewUsernameChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1", + "user1@org.com", + false, + true, + user.UsernameChangedEventWithPolicyChange(true), + ), + user.NewUsernameChangedEvent(context.Background(), + &user.NewAggregate("user2", "org1").Aggregate, + "user@test.com", + "user@test.com@org.com", + false, + true, + user.UsernameChangedEventWithPolicyChange(true), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userLoginMustBeDomain: false, + validateOrgDomains: true, + smtpSenderAddressMatchesInstanceDomain: true, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } got, err := r.AddOrgDomainPolicy(tt.args.ctx, tt.args.orgID, tt.args.userLoginMustBeDomain, tt.args.validateOrgDomains, tt.args.smtpSenderAddressMatchesInstanceDomain) if tt.res.err == nil { @@ -255,7 +481,7 @@ func TestCommandSide_AddDomainPolicy(t *testing.T) { func TestCommandSide_ChangeDomainPolicy(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -277,9 +503,7 @@ func TestCommandSide_ChangeDomainPolicy(t *testing.T) { { name: "org id missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -294,8 +518,7 @@ func TestCommandSide_ChangeDomainPolicy(t *testing.T) { { name: "policy not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -313,8 +536,7 @@ func TestCommandSide_ChangeDomainPolicy(t *testing.T) { { name: "no changes, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -341,8 +563,7 @@ func TestCommandSide_ChangeDomainPolicy(t *testing.T) { { name: "change, no userLoginMustBeDomain change, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -377,8 +598,7 @@ func TestCommandSide_ChangeDomainPolicy(t *testing.T) { { name: "change, userLoginMustBeDomain changed, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -389,6 +609,7 @@ func TestCommandSide_ChangeDomainPolicy(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewDomainPrimarySetEvent( @@ -429,7 +650,156 @@ func TestCommandSide_ChangeDomainPolicy(t *testing.T) { "user1", "user1@org.com", false, - user.UsernameChangedEventWithPolicyChange(), + false, + user.UsernameChangedEventWithPolicyChange(true), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userLoginMustBeDomain: false, + validateOrgDomains: false, + smtpSenderAddressMatchesInstanceDomain: false, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "change, userLoginMustBeDomain changed, org scoped usernames, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + false, + true, + true, + ), + ), + ), + expectFilterOrganizationSettings("org1", true, true), + expectFilter( + eventFromEventPusher( + org.NewDomainPrimarySetEvent( + context.Background(), + &org.NewAggregate("org1").Aggregate, + "org.com", + ), + ), + eventFromEventPusher( + org.NewDomainPrimarySetEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org.com", + ), + ), + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1", + "firstname", + "lastname", + "nickname", + "displayname", + language.English, + domain.GenderUnspecified, + "user1@org.com", + false, + ), + ), + ), + expectPush( + newDomainPolicyChangedEvent(context.Background(), "org1", + policy.ChangeUserLoginMustBeDomain(true), + policy.ChangeValidateOrgDomains(false), + policy.ChangeSMTPSenderAddressMatchesInstanceDomain(false), + ), + user.NewUsernameChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1", + "user1", + true, + true, + user.UsernameChangedEventWithPolicyChange(false), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userLoginMustBeDomain: true, + validateOrgDomains: false, + smtpSenderAddressMatchesInstanceDomain: false, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "change, userLoginMustBeDomain removed, org scoped usernames, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilterOrganizationSettings("org1", true, true), + expectFilter( + eventFromEventPusher( + org.NewDomainPrimarySetEvent( + context.Background(), + &org.NewAggregate("org1").Aggregate, + "org.com", + ), + ), + eventFromEventPusher( + org.NewDomainPrimarySetEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org.com", + ), + ), + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1", + "firstname", + "lastname", + "nickname", + "displayname", + language.English, + domain.GenderUnspecified, + "user1@org.com", + true, + ), + ), + ), + expectPush( + newDomainPolicyChangedEvent(context.Background(), "org1", + policy.ChangeUserLoginMustBeDomain(false), + policy.ChangeValidateOrgDomains(false), + policy.ChangeSMTPSenderAddressMatchesInstanceDomain(false), + ), + user.NewUsernameChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1", + "user1@org.com", + false, + true, + user.UsernameChangedEventWithPolicyChange(true), ), ), ), @@ -451,7 +821,7 @@ func TestCommandSide_ChangeDomainPolicy(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } got, err := r.ChangeOrgDomainPolicy(tt.args.ctx, tt.args.orgID, tt.args.userLoginMustBeDomain, tt.args.validateOrgDomains, tt.args.smtpSenderAddressMatchesInstanceDomain) if tt.res.err == nil { @@ -469,7 +839,7 @@ func TestCommandSide_ChangeDomainPolicy(t *testing.T) { func TestCommandSide_RemoveDomainPolicy(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -488,9 +858,7 @@ func TestCommandSide_RemoveDomainPolicy(t *testing.T) { { name: "org id missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -502,8 +870,7 @@ func TestCommandSide_RemoveDomainPolicy(t *testing.T) { { name: "policy not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -518,8 +885,7 @@ func TestCommandSide_RemoveDomainPolicy(t *testing.T) { { name: "remove, no userLoginMustBeDomain change, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -540,6 +906,7 @@ func TestCommandSide_RemoveDomainPolicy(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( org.NewDomainPolicyRemovedEvent(context.Background(), &org.NewAggregate("org1").Aggregate), @@ -559,8 +926,7 @@ func TestCommandSide_RemoveDomainPolicy(t *testing.T) { { name: "remove, userLoginMustBeDomain changed, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -581,6 +947,7 @@ func TestCommandSide_RemoveDomainPolicy(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewDomainPrimarySetEvent( @@ -619,7 +986,166 @@ func TestCommandSide_RemoveDomainPolicy(t *testing.T) { "user1", "user1@org.com", false, - user.UsernameChangedEventWithPolicyChange(), + false, + user.UsernameChangedEventWithPolicyChange(true), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "remove, userLoginMustBeDomain removed, org scoped usernames, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewDomainPolicyAddedEvent(context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + false, + true, + true, + ), + ), + ), + expectFilterOrganizationSettings("org1", true, true), + expectFilter( + eventFromEventPusher( + org.NewDomainPrimarySetEvent( + context.Background(), + &org.NewAggregate("org1").Aggregate, + "org.com", + ), + ), + eventFromEventPusher( + org.NewDomainPrimarySetEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org.com", + ), + ), + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1", + "firstname", + "lastname", + "nickname", + "displayname", + language.English, + domain.GenderUnspecified, + "user1@org.com", + false, + ), + ), + ), + expectPush( + org.NewDomainPolicyRemovedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + ), + user.NewUsernameChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1", + "user1@org.com", + false, + true, + user.UsernameChangedEventWithPolicyChange(true), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "remove, userLoginMustBeDomain changed, org scoped usernames, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + false, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewDomainPolicyAddedEvent(context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilterOrganizationSettings("org1", true, true), + expectFilter( + eventFromEventPusher( + org.NewDomainPrimarySetEvent( + context.Background(), + &org.NewAggregate("org1").Aggregate, + "org.com", + ), + ), + eventFromEventPusher( + org.NewDomainPrimarySetEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org.com", + ), + ), + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1", + "firstname", + "lastname", + "nickname", + "displayname", + language.English, + domain.GenderUnspecified, + "user1@org.com", + true, + ), + ), + ), + expectPush( + org.NewDomainPolicyRemovedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + ), + user.NewUsernameChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1", + "user1", + true, + true, + user.UsernameChangedEventWithPolicyChange(false), ), ), ), @@ -638,7 +1164,7 @@ func TestCommandSide_RemoveDomainPolicy(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } got, err := r.RemoveOrgDomainPolicy(tt.args.ctx, tt.args.orgID) if tt.res.err == nil { diff --git a/internal/command/org_test.go b/internal/command/org_test.go index c07d5f7678..a2c2713874 100644 --- a/internal/command/org_test.go +++ b/internal/command/org_test.go @@ -1101,6 +1101,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter(), expectFilter(), expectFilter(), @@ -1143,6 +1144,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter(), expectFilter(), expectFilter(), @@ -1183,6 +1185,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -1397,6 +1400,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter(), // org member check expectFilter( eventFromEventPusher( @@ -1706,6 +1710,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter(), expectFilter(), expectFilter(), // org member check diff --git a/internal/command/organization_settings.go b/internal/command/organization_settings.go new file mode 100644 index 0000000000..2459fd60d8 --- /dev/null +++ b/internal/command/organization_settings.go @@ -0,0 +1,140 @@ +package command + +import ( + "context" + + "github.com/zitadel/zitadel/internal/command/preparation" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type SetOrganizationSettings struct { + OrganizationID string + + OrganizationScopedUsernames *bool +} + +func (e *SetOrganizationSettings) IsValid() error { + if e.OrganizationID == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-zI4z7cLLRJ", "Errors.Org.Settings.Invalid") + } + return nil +} + +func (c *Commands) SetOrganizationSettings(ctx context.Context, set *SetOrganizationSettings) (_ *domain.ObjectDetails, err error) { + if err := set.IsValid(); err != nil { + return nil, err + } + wm, err := c.getOrganizationSettingsWriteModelByID(ctx, set.OrganizationID) + if err != nil { + return nil, err + } + if !wm.OrganizationState.Exists() { + return nil, zerrors.ThrowNotFound(nil, "COMMAND-oDzwP5kmdP", "Errors.NotFound") + } + + domainPolicy, err := c.domainPolicyWriteModel(ctx, wm.AggregateID) + if err != nil { + return nil, err + } + + events, err := wm.NewSet(ctx, + set.OrganizationScopedUsernames, + domainPolicy.UserLoginMustBeDomain, + c.getOrganizationScopedUsernames, + ) + if err != nil { + return nil, err + } + + return c.pushAppendAndReduceDetails(ctx, wm, events...) +} + +func (c *Commands) DeleteOrganizationSettings(ctx context.Context, id string) (*domain.ObjectDetails, error) { + if id == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-eU5hkMy3Pf", "Errors.IDMissing") + } + wm, err := c.getOrganizationSettingsWriteModelByID(ctx, id) + if err != nil { + return nil, err + } + if !wm.State.Exists() { + return writeModelToObjectDetails(wm.GetWriteModel()), nil + } + + domainPolicy, err := c.domainPolicyWriteModel(ctx, wm.AggregateID) + if err != nil { + return nil, err + } + + events, err := wm.NewRemoved(ctx, + domainPolicy.UserLoginMustBeDomain, + c.getOrganizationScopedUsernames, + ) + if err != nil { + return nil, err + } + + return c.pushAppendAndReduceDetails(ctx, wm, events...) +} + +func checkOrganizationScopedUsernames(ctx context.Context, filter preparation.FilterToQueryReducer, id string, checkPermission domain.PermissionCheck) (_ bool, err error) { + wm := NewOrganizationSettingsWriteModel(id, checkPermission) + events, err := filter(ctx, wm.Query()) + if err != nil { + return false, err + } + if len(events) == 0 { + return false, nil + } + wm.AppendEvents(events...) + err = wm.Reduce() + if err != nil { + return false, err + } + + return wm.State.Exists() && wm.OrganizationScopedUsernames, nil +} + +func (c *Commands) getOrganizationSettingsWriteModelByID(ctx context.Context, id string) (*OrganizationSettingsWriteModel, error) { + wm := NewOrganizationSettingsWriteModel(id, c.checkPermission) + err := c.eventstore.FilterToQueryReducer(ctx, wm) + if err != nil { + return nil, err + } + return wm, nil +} + +func (c *Commands) checkOrganizationScopedUsernames(ctx context.Context, orgID string) (_ bool, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + wm, err := c.getOrganizationSettingsWriteModelByID(ctx, orgID) + if err != nil { + return false, err + } + + return wm.State.Exists() && wm.OrganizationScopedUsernames, nil +} + +func (c *Commands) getOrganizationScopedUsernamesWriteModelByID(ctx context.Context, id string) (*OrganizationScopedUsernamesWriteModel, error) { + wm := NewOrganizationScopedUsernamesWriteModel(id) + err := c.eventstore.FilterToQueryReducer(ctx, wm) + if err != nil { + return nil, err + } + return wm, nil +} + +func (c *Commands) getOrganizationScopedUsernames(ctx context.Context, id string) ([]string, error) { + wm, err := c.getOrganizationScopedUsernamesWriteModelByID(ctx, id) + if err != nil { + return nil, err + } + usernames := make([]string, len(wm.Users)) + for i, user := range wm.Users { + usernames[i] = user.username + } + return usernames, nil +} diff --git a/internal/command/organization_settings_model.go b/internal/command/organization_settings_model.go new file mode 100644 index 0000000000..890cf671e5 --- /dev/null +++ b/internal/command/organization_settings_model.go @@ -0,0 +1,245 @@ +package command + +import ( + "context" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/org" + settings "github.com/zitadel/zitadel/internal/repository/organization_settings" + "github.com/zitadel/zitadel/internal/repository/user" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type OrganizationSettingsWriteModel struct { + eventstore.WriteModel + + OrganizationScopedUsernames bool + + OrganizationState domain.OrgState + + State domain.OrganizationSettingsState + checkPermission domain.PermissionCheck +} + +func (wm *OrganizationSettingsWriteModel) GetWriteModel() *eventstore.WriteModel { + return &wm.WriteModel +} + +func (wm *OrganizationSettingsWriteModel) checkPermissionWrite( + ctx context.Context, + resourceOwner string, + aggregateID string, +) error { + if wm.checkPermission == nil { + return zerrors.ThrowPermissionDenied(nil, "COMMAND-8Dttuyj0B4", "Permission check not defined") + } + return wm.checkPermission(ctx, domain.PermissionIAMPolicyWrite, resourceOwner, aggregateID) +} + +func (wm *OrganizationSettingsWriteModel) checkPermissionDelete( + ctx context.Context, + resourceOwner string, + aggregateID string, +) error { + if wm.checkPermission == nil { + return zerrors.ThrowPermissionDenied(nil, "COMMAND-6R54f4vWqv", "Permission check not defined") + } + return wm.checkPermission(ctx, domain.PermissionIAMPolicyDelete, resourceOwner, aggregateID) +} + +func NewOrganizationSettingsWriteModel(id string, checkPermission domain.PermissionCheck) *OrganizationSettingsWriteModel { + return &OrganizationSettingsWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: id, + ResourceOwner: id, + }, + checkPermission: checkPermission, + } +} + +func (wm *OrganizationSettingsWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *settings.OrganizationSettingsSetEvent: + wm.OrganizationScopedUsernames = e.OrganizationScopedUsernames + wm.State = domain.OrganizationSettingsStateActive + case *settings.OrganizationSettingsRemovedEvent: + wm.OrganizationScopedUsernames = false + wm.State = domain.OrganizationSettingsStateRemoved + case *org.OrgAddedEvent: + wm.OrganizationState = domain.OrgStateActive + wm.OrganizationScopedUsernames = false + case *org.OrgRemovedEvent: + wm.OrganizationState = domain.OrgStateRemoved + wm.OrganizationScopedUsernames = false + } + } + return wm.WriteModel.Reduce() +} + +func (wm *OrganizationSettingsWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(settings.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes(settings.OrganizationSettingsSetEventType, + settings.OrganizationSettingsRemovedEventType). + Or(). + AggregateTypes(org.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes(org.OrgAddedEventType, + org.OrgRemovedEventType). + Builder() +} + +func (wm *OrganizationSettingsWriteModel) NewSet( + ctx context.Context, + organizationScopedUsernames *bool, + userLoginMustBeDomain bool, + usernamesF func(ctx context.Context, orgID string) ([]string, error), +) (_ []eventstore.Command, err error) { + if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil { + return nil, err + } + // no changes + if organizationScopedUsernames == nil || *organizationScopedUsernames == wm.OrganizationScopedUsernames { + return nil, nil + } + + var usernames []string + if (wm.OrganizationScopedUsernames || userLoginMustBeDomain) != (*organizationScopedUsernames || userLoginMustBeDomain) { + usernames, err = usernamesF(ctx, wm.AggregateID) + if err != nil { + return nil, err + } + } + events := []eventstore.Command{ + settings.NewOrganizationSettingsAddedEvent(ctx, + SettingsAggregateFromWriteModel(&wm.WriteModel), + usernames, + *organizationScopedUsernames || userLoginMustBeDomain, + wm.OrganizationScopedUsernames || userLoginMustBeDomain, + ), + } + return events, nil +} + +func (wm *OrganizationSettingsWriteModel) NewRemoved( + ctx context.Context, + userLoginMustBeDomain bool, + usernamesF func(ctx context.Context, orgID string) ([]string, error), +) (_ []eventstore.Command, err error) { + if err := wm.checkPermissionDelete(ctx, wm.ResourceOwner, wm.AggregateID); err != nil { + return nil, err + } + + var usernames []string + if userLoginMustBeDomain != wm.OrganizationScopedUsernames { + usernames, err = usernamesF(ctx, wm.AggregateID) + if err != nil { + return nil, err + } + } + events := []eventstore.Command{ + settings.NewOrganizationSettingsRemovedEvent(ctx, + SettingsAggregateFromWriteModel(&wm.WriteModel), + usernames, + userLoginMustBeDomain, + wm.OrganizationScopedUsernames || userLoginMustBeDomain, + ), + } + return events, nil +} + +func SettingsAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate { + return &eventstore.Aggregate{ + ID: wm.AggregateID, + Type: settings.AggregateType, + ResourceOwner: wm.ResourceOwner, + InstanceID: wm.InstanceID, + Version: settings.AggregateVersion, + } +} + +type OrganizationScopedUsernamesWriteModel struct { + eventstore.WriteModel + + Users []*organizationScopedUser +} + +type organizationScopedUser struct { + id string + username string +} + +func NewOrganizationScopedUsernamesWriteModel(orgID string) *OrganizationScopedUsernamesWriteModel { + return &OrganizationScopedUsernamesWriteModel{ + WriteModel: eventstore.WriteModel{ + ResourceOwner: orgID, + }, + Users: make([]*organizationScopedUser, 0), + } +} + +func (wm *OrganizationScopedUsernamesWriteModel) AppendEvents(events ...eventstore.Event) { + wm.WriteModel.AppendEvents(events...) +} + +func (wm *OrganizationScopedUsernamesWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *user.HumanAddedEvent: + wm.Users = append(wm.Users, &organizationScopedUser{id: e.Aggregate().ID, username: e.UserName}) + case *user.HumanRegisteredEvent: + wm.Users = append(wm.Users, &organizationScopedUser{id: e.Aggregate().ID, username: e.UserName}) + case *user.MachineAddedEvent: + wm.Users = append(wm.Users, &organizationScopedUser{id: e.Aggregate().ID, username: e.UserName}) + case *user.UsernameChangedEvent: + for _, user := range wm.Users { + if user.id == e.Aggregate().ID { + user.username = e.UserName + break + } + } + case *user.DomainClaimedEvent: + for _, user := range wm.Users { + if user.id == e.Aggregate().ID { + user.username = e.UserName + break + } + } + case *user.UserRemovedEvent: + wm.removeUser(e.Aggregate().ID) + } + } + return wm.WriteModel.Reduce() +} + +func (wm *OrganizationScopedUsernamesWriteModel) removeUser(userID string) { + for i, user := range wm.Users { + if user.id == userID { + wm.Users[i] = wm.Users[len(wm.Users)-1] + wm.Users[len(wm.Users)-1] = nil + wm.Users = wm.Users[:len(wm.Users)-1] + return + } + } +} + +func (wm *OrganizationScopedUsernamesWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(user.AggregateType). + EventTypes( + user.HumanAddedType, + user.HumanRegisteredType, + user.MachineAddedEventType, + user.UserUserNameChangedType, + user.UserDomainClaimedType, + user.UserRemovedType, + ). + Builder() +} diff --git a/internal/command/organization_settings_test.go b/internal/command/organization_settings_test.go new file mode 100644 index 0000000000..f6317fd35f --- /dev/null +++ b/internal/command/organization_settings_test.go @@ -0,0 +1,542 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/org" + settings "github.com/zitadel/zitadel/internal/repository/organization_settings" + "github.com/zitadel/zitadel/internal/repository/user" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestCommandSide_SetSettingsOrganization(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + ctx context.Context + settings *SetOrganizationSettings + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + settings: &SetOrganizationSettings{ + OrganizationID: "", + OrganizationScopedUsernames: boolPtr(true), + }, + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "org not found, not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + settings: &SetOrganizationSettings{ + OrganizationID: "org1", + OrganizationScopedUsernames: boolPtr(true), + }, + }, + res: res{ + err: zerrors.IsNotFound, + }, + }, + { + name: "settings already existing, no changes", + fields: fields{ + eventstore: expectEventstore( + expectFilterPreOrganizationSettings("org1", true, true, true), + expectFilterOrgDomainPolicy(false), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + settings: &SetOrganizationSettings{ + OrganizationID: "org1", + OrganizationScopedUsernames: boolPtr(true), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "org1", + }, + }, + }, + { + name: "settings set, new", + fields: fields{ + eventstore: expectEventstore( + expectFilterPreOrganizationSettings("org1", true, false, false), + expectFilterOrgDomainPolicy(false), + expectFilterOrganizationScopedUsernames(false, "username1", "username2", "username3"), + expectPush( + settings.NewOrganizationSettingsAddedEvent(context.Background(), + &settings.NewAggregate("org1", "org1").Aggregate, + []string{"username1", "username2", "username3"}, + true, + false, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + settings: &SetOrganizationSettings{ + OrganizationID: "org1", + OrganizationScopedUsernames: boolPtr(true), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "org1", + }, + }, + }, + { + name: "settings set, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilterPreOrganizationSettings("org1", true, false, true), + expectFilterOrgDomainPolicy(false), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + settings: &SetOrganizationSettings{ + OrganizationID: "org1", + OrganizationScopedUsernames: boolPtr(true), + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, + { + name: "settings set, changed", + fields: fields{ + eventstore: expectEventstore( + expectFilterPreOrganizationSettings("org1", true, true, false), + expectFilterOrgDomainPolicy(false), + expectFilterOrganizationScopedUsernames(false, "username1", "username2", "username3"), + expectPush( + settings.NewOrganizationSettingsAddedEvent(context.Background(), + &settings.NewAggregate("org1", "org1").Aggregate, + []string{"username1", "username2", "username3"}, + true, + false, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + settings: &SetOrganizationSettings{ + OrganizationID: "org1", + OrganizationScopedUsernames: boolPtr(true), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "org1", + }, + }, + }, + { + name: "settings not set, not existing", + fields: fields{ + eventstore: expectEventstore( + expectFilterPreOrganizationSettings("org1", true, false, false), + expectFilterOrgDomainPolicy(false), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + settings: &SetOrganizationSettings{ + OrganizationID: "org1", + OrganizationScopedUsernames: boolPtr(false), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "org1", + }, + }, + }, + { + name: "settings set, changed, usernameMustBeDomain set", + fields: fields{ + eventstore: expectEventstore( + expectFilterPreOrganizationSettings("org1", true, true, false), + expectFilterOrgDomainPolicy(true), + expectPush( + settings.NewOrganizationSettingsAddedEvent(context.Background(), + &settings.NewAggregate("org1", "org1").Aggregate, + []string{"username1", "username2", "username3"}, + true, + true, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + settings: &SetOrganizationSettings{ + OrganizationID: "org1", + OrganizationScopedUsernames: boolPtr(true), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + got, err := r.SetOrganizationSettings(tt.args.ctx, tt.args.settings) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_DeleteSettingsOrganization(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + ctx context.Context + orgID string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "settings delete, no change", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "org1", + }, + }, + }, + { + name: "settings delete, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilterOrganizationSettings("org1", true, true), + expectFilterOrgDomainPolicy(false), + expectFilterOrganizationScopedUsernames(false, "username1", "username2", "username3"), + expectPush( + settings.NewOrganizationSettingsRemovedEvent(context.Background(), + &settings.NewAggregate("org1", "org1").Aggregate, + []string{"username1", "username2", "username3"}, + false, + true, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "org1", + }, + }, + }, + { + name: "settings delete, unset, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilterOrganizationSettings("org1", true, false), + expectFilterOrgDomainPolicy(false), + expectPush( + settings.NewOrganizationSettingsRemovedEvent(context.Background(), + &settings.NewAggregate("org1", "org1").Aggregate, + []string{"username1", "username2", "username3"}, + false, + false, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "org1", + }, + }, + }, + { + name: "settings delete, unset, usernameMustBeDomain set", + fields: fields{ + eventstore: expectEventstore( + expectFilterOrganizationSettings("org1", true, false), + expectFilterOrgDomainPolicy(true), + expectFilterOrganizationScopedUsernames(true, "username1", "username2", "username3"), + expectPush( + settings.NewOrganizationSettingsRemovedEvent(context.Background(), + &settings.NewAggregate("org1", "org1").Aggregate, + []string{"username1", "username2", "username3"}, + true, + true, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "org1", + }, + }, + }, + { + name: "settings delete, set, usernameMustBeDomain set", + fields: fields{ + eventstore: expectEventstore( + expectFilterOrganizationSettings("org1", true, true), + expectFilterOrgDomainPolicy(true), + expectPush( + settings.NewOrganizationSettingsRemovedEvent(context.Background(), + &settings.NewAggregate("org1", "org1").Aggregate, + []string{"username1", "username2", "username3"}, + true, + true, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "org1", + }, + }, + }, + { + name: "settings delete, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilterOrganizationSettings("org1", true, true), + expectFilterOrgDomainPolicy(true), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + got, err := r.DeleteOrganizationSettings(tt.args.ctx, tt.args.orgID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func expectFilterPreOrganizationSettings(orgID string, orgExisting, settingExisting, orgScopedUsernames bool) expect { + var events []eventstore.Event + events = append(events, + expectFilterPreOrganizationSettingsEvents(context.Background(), orgID, orgExisting)..., + ) + events = append(events, + expectFilterOrganizationSettingsEvents(context.Background(), orgID, settingExisting, orgScopedUsernames)..., + ) + return expectFilter( + events..., + ) +} + +func expectFilterPreOrganizationSettingsEvents(ctx context.Context, orgID string, orgExisting bool) []eventstore.Event { + var events []eventstore.Event + if orgExisting { + events = append(events, + eventFromEventPusher( + org.NewOrgAddedEvent(ctx, + &org.NewAggregate(orgID).Aggregate, + "org", + ), + ), + ) + } + return events +} + +func expectFilterOrganizationSettings(orgID string, settingExisting, orgScopedUsernames bool) expect { + return expectFilter( + expectFilterOrganizationSettingsEvents(context.Background(), orgID, settingExisting, orgScopedUsernames)..., + ) +} + +func expectFilterOrganizationSettingsEvents(ctx context.Context, orgID string, settingExisting, orgScopedUsernames bool) []eventstore.Event { + var events []eventstore.Event + if settingExisting { + events = append(events, + eventFromEventPusher( + settings.NewOrganizationSettingsAddedEvent(ctx, + &settings.NewAggregate(orgID, orgID).Aggregate, + []string{}, + orgScopedUsernames, + !orgScopedUsernames, + ), + ), + ) + } + return events +} + +func expectFilterOrgDomainPolicy(userLoginMustBeDomain bool) expect { + return expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + userLoginMustBeDomain, false, false, + ), + ), + ) +} + +func expectFilterOrganizationScopedUsernames(userMustBeDomain bool, usernames ...string) expect { + events := make([]eventstore.Event, len(usernames)) + for i, username := range usernames { + events[i] = eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate(username, "org1").Aggregate, + username, + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + userMustBeDomain, + ), + ) + } + return expectFilter( + events..., + ) +} diff --git a/internal/command/policy_org_model.go b/internal/command/policy_org_model.go index cc0c8bcd6f..9bf56aa981 100644 --- a/internal/command/policy_org_model.go +++ b/internal/command/policy_org_model.go @@ -148,7 +148,7 @@ func (wm *DomainPolicyUsernamesWriteModel) Query() *eventstore.SearchQueryBuilde Builder() } -func (wm *DomainPolicyUsernamesWriteModel) NewUsernameChangedEvents(ctx context.Context, userLoginMustBeDomain bool) []eventstore.Command { +func (wm *DomainPolicyUsernamesWriteModel) NewUsernameChangedEvents(ctx context.Context, userLoginMustBeDomain, organizationScopedUsernames, oldUserLoginMustBeDomain bool) []eventstore.Command { events := make([]eventstore.Command, 0, len(wm.Users)) for _, changeUser := range wm.Users { events = append(events, user.NewUsernameChangedEvent(ctx, @@ -156,12 +156,21 @@ func (wm *DomainPolicyUsernamesWriteModel) NewUsernameChangedEvents(ctx context. changeUser.username, wm.newUsername(changeUser.username, userLoginMustBeDomain), userLoginMustBeDomain, - user.UsernameChangedEventWithPolicyChange()), - ) + organizationScopedUsernames, + user.UsernameChangedEventWithPolicyChange(oldUserLoginMustBeDomain), + )) } return events } +func (wm *DomainPolicyUsernamesWriteModel) Usernames() []string { + usernames := make([]string, 0, len(wm.Users)) + for i, user := range wm.Users { + usernames[i] = user.username + } + return usernames +} + func (wm *DomainPolicyUsernamesWriteModel) newUsername(username string, userLoginMustBeDomain bool) string { if !userLoginMustBeDomain { // if the UserLoginMustBeDomain will be false, then it's currently true diff --git a/internal/command/user.go b/internal/command/user.go index d834169f8a..b803c5496e 100644 --- a/internal/command/user.go +++ b/internal/command/user.go @@ -43,10 +43,15 @@ func (c *Commands) ChangeUsername(ctx context.Context, orgID, userID, userName s if err = c.userValidateDomain(ctx, orgID, userName, domainPolicy.UserLoginMustBeDomain); err != nil { return nil, err } + orgScopedUsernames, err := c.checkOrganizationScopedUsernames(ctx, orgID) + if err != nil { + return nil, err + } + userAgg := UserAggregateFromWriteModel(&existingUser.WriteModel) pushedEvents, err := c.eventstore.Push(ctx, - user.NewUsernameChangedEvent(ctx, userAgg, existingUser.UserName, userName, domainPolicy.UserLoginMustBeDomain)) + user.NewUsernameChangedEvent(ctx, userAgg, existingUser.UserName, userName, domainPolicy.UserLoginMustBeDomain, orgScopedUsernames)) if err != nil { return nil, err } @@ -189,9 +194,13 @@ func (c *Commands) RemoveUser(ctx context.Context, userID, resourceOwner string, if err != nil { return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-3M9fs", "Errors.Org.DomainPolicy.NotExisting") } + orgScopedUsername, err := c.checkOrganizationScopedUsernames(ctx, existingUser.ResourceOwner) + if err != nil { + return nil, err + } var events []eventstore.Command userAgg := UserAggregateFromWriteModel(&existingUser.WriteModel) - events = append(events, user.NewUserRemovedEvent(ctx, userAgg, existingUser.UserName, existingUser.IDPLinks, domainPolicy.UserLoginMustBeDomain)) + events = append(events, user.NewUserRemovedEvent(ctx, userAgg, existingUser.UserName, existingUser.IDPLinks, domainPolicy.UserLoginMustBeDomain || orgScopedUsername)) for _, grantID := range cascadingGrantIDs { removeEvent, _, err := c.removeUserGrant(ctx, grantID, "", true, false, nil) @@ -269,6 +278,11 @@ func (c *Commands) userDomainClaimed(ctx context.Context, userID string) (events return nil, nil, err } + organizationScopedUsername, err := c.checkOrganizationScopedUsernames(ctx, existingUser.ResourceOwner) + if err != nil { + return nil, nil, err + } + id, err := c.idGenerator.Next() if err != nil { return nil, nil, err @@ -279,7 +293,8 @@ func (c *Commands) userDomainClaimed(ctx context.Context, userID string) (events userAgg, fmt.Sprintf("%s@temporary.%s", id, http_util.DomainContext(ctx).RequestedDomain()), existingUser.UserName, - domainPolicy.UserLoginMustBeDomain), + domainPolicy.UserLoginMustBeDomain || organizationScopedUsername, + ), }, changedUserGrant, nil } @@ -295,6 +310,12 @@ func (c *Commands) prepareUserDomainClaimed(ctx context.Context, filter preparat if err != nil { return nil, err } + + organizationScopedUsername, err := checkOrganizationScopedUsernames(ctx, filter, userWriteModel.ResourceOwner, nil) + if err != nil { + return nil, err + } + userAgg := UserAggregateFromWriteModel(&userWriteModel.WriteModel) id, err := c.idGenerator.Next() @@ -307,7 +328,8 @@ func (c *Commands) prepareUserDomainClaimed(ctx context.Context, filter preparat userAgg, fmt.Sprintf("%s@temporary.%s", id, http_util.DomainContext(ctx).RequestedDomain()), userWriteModel.UserName, - domainPolicy.UserLoginMustBeDomain), nil + domainPolicy.UserLoginMustBeDomain || organizationScopedUsername, + ), nil } func (c *Commands) UserDomainClaimedSent(ctx context.Context, orgID, userID string) (err error) { diff --git a/internal/command/user_human.go b/internal/command/user_human.go index 07628b9e19..e4a6148159 100644 --- a/internal/command/user_human.go +++ b/internal/command/user_human.go @@ -199,6 +199,11 @@ func (c *Commands) AddHumanCommand(human *AddHuman, orgID string, hasher *crypto return nil, err } + organizationScopedUsername, err := checkOrganizationScopedUsernames(ctx, filter, a.ResourceOwner, nil) + if err != nil { + return nil, err + } + var createCmd humanCreationCommand if human.Register { createCmd = user.NewHumanRegisteredEvent( @@ -212,7 +217,7 @@ func (c *Commands) AddHumanCommand(human *AddHuman, orgID string, hasher *crypto human.PreferredLanguage, human.Gender, human.Email.Address, - domainPolicy.UserLoginMustBeDomain, + domainPolicy.UserLoginMustBeDomain || organizationScopedUsername, "", // no user agent id available ) } else { @@ -227,7 +232,7 @@ func (c *Commands) AddHumanCommand(human *AddHuman, orgID string, hasher *crypto human.PreferredLanguage, human.Gender, human.Email.Address, - domainPolicy.UserLoginMustBeDomain, + domainPolicy.UserLoginMustBeDomain || organizationScopedUsername, ) } @@ -439,6 +444,12 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain. if err != nil { return nil, nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-2N9fs", "Errors.Org.DomainPolicy.NotFound") } + + organizationScopedUsername, err := c.checkOrganizationScopedUsernames(ctx, orgID) + if err != nil { + return nil, nil, err + } + pwPolicy, err := c.getOrgPasswordComplexityPolicy(ctx, orgID) if err != nil { return nil, nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-4N8gs", "Errors.Org.PasswordComplexityPolicy.NotFound") @@ -455,7 +466,7 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain. } } - events, userAgg, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, links, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator) + events, userAgg, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, links, domainPolicy, organizationScopedUsername, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator) if err != nil { return nil, nil, err } @@ -501,7 +512,7 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain. return writeModelToHuman(addedHuman), passwordlessCode, nil } -func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, userAgg *eventstore.Aggregate, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) { +func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, domainPolicy *domain.DomainPolicy, orgScopedUsername bool, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, userAgg *eventstore.Aggregate, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -511,7 +522,7 @@ func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain. if err = human.Normalize(); err != nil { return nil, nil, nil, nil, "", err } - events, userAgg, humanWriteModel, err = c.createHuman(ctx, orgID, human, links, passwordless, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) + events, userAgg, humanWriteModel, err = c.createHuman(ctx, orgID, human, links, passwordless, domainPolicy, orgScopedUsername, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) if err != nil { return nil, nil, nil, nil, "", err } @@ -526,7 +537,7 @@ func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain. return events, userAgg, humanWriteModel, passwordlessCodeWriteModel, code, nil } -func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, links []*domain.UserIDPLink, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) (events []eventstore.Command, userAgg *eventstore.Aggregate, addedHuman *HumanWriteModel, err error) { +func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, links []*domain.UserIDPLink, passwordless bool, domainPolicy *domain.DomainPolicy, orgScopedUsername bool, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) (events []eventstore.Command, userAgg *eventstore.Aggregate, addedHuman *HumanWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -559,7 +570,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. // TODO: adlerhurst maybe we could simplify the code below userAgg = UserAggregateFromWriteModelCtx(ctx, &addedHuman.WriteModel) - events = append(events, createAddHumanEvent(ctx, userAgg, human, domainPolicy.UserLoginMustBeDomain)) + events = append(events, createAddHumanEvent(ctx, userAgg, human, domainPolicy.UserLoginMustBeDomain, orgScopedUsername)) for _, link := range links { event, err := c.addUserIDPLink(ctx, userAgg, link, false) @@ -619,7 +630,7 @@ func (c *Commands) HumanSkipMFAInit(ctx context.Context, userID, resourceowner s } // TODO: adlerhurst maybe we can simplify createAddHumanEvent and createRegisterHumanEvent -func createAddHumanEvent(ctx context.Context, aggregate *eventstore.Aggregate, human *domain.Human, userLoginMustBeDomain bool) *user.HumanAddedEvent { +func createAddHumanEvent(ctx context.Context, aggregate *eventstore.Aggregate, human *domain.Human, userLoginMustBeDomain, orgScopedUsername bool) *user.HumanAddedEvent { addEvent := user.NewHumanAddedEvent( ctx, aggregate, @@ -631,7 +642,7 @@ func createAddHumanEvent(ctx context.Context, aggregate *eventstore.Aggregate, h human.PreferredLanguage, human.Gender, human.EmailAddress, - userLoginMustBeDomain, + userLoginMustBeDomain || orgScopedUsername, ) if human.Phone != nil { addEvent.AddPhoneData(human.PhoneNumber) diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go index 1ef3e2aab6..a5641d69c0 100644 --- a/internal/command/user_human_test.go +++ b/internal/command/user_human_test.go @@ -190,6 +190,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter(), expectFilter(), ), @@ -231,6 +232,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewHumanAddedEvent(context.Background(), &userAgg.Aggregate, @@ -299,6 +301,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewHumanAddedEvent(context.Background(), &userAgg.Aggregate, @@ -368,6 +371,77 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), + expectPush( + user.NewHumanAddedEvent(context.Background(), + &userAgg.Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + AllowedLanguage, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &userAgg.Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("userinit"), + }, + time.Hour*1, + "", + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + newCode: mockEncryptedCode("userinit", time.Hour), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &AddHuman{ + Username: "username", + FirstName: "firstname", + LastName: "lastname", + Email: Email{ + Address: "email@test.ch", + }, + PreferredLanguage: AllowedLanguage, + }, + secretGenerator: GetMockSecretGenerator(t), + allowInitMail: true, + }, + res: res{ + want: &domain.ObjectDetails{ + Sequence: 0, + EventDate: time.Time{}, + ResourceOwner: "org1", + }, + wantID: "user1", + }, + }, + { + name: "add human (with initial code), orgScopedUsername, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &userAgg.Aggregate, + false, + true, + true, + ), + ), + ), + expectFilterOrganizationSettings("org1", true, true), expectPush( user.NewHumanAddedEvent(context.Background(), &userAgg.Aggregate, @@ -437,6 +511,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -507,6 +582,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -580,6 +656,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -654,6 +731,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -714,6 +792,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -777,6 +856,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -840,6 +920,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -963,6 +1044,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -1042,6 +1124,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -1151,6 +1234,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( newAddHumanEvent("", false, true, "+41711234567", AllowedLanguage), user.NewHumanInitialCodeAddedEvent( @@ -1216,6 +1300,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -1326,6 +1411,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( newAddHumanEvent("", false, true, "", AllowedLanguage), user.NewHumanInitialCodeAddedEvent( @@ -1518,6 +1604,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter(), expectFilter(), ), @@ -1557,6 +1644,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -1602,6 +1690,94 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + newAddHumanEvent("$plain$x$password", true, true, "", AllowedLanguage), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + "", + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Password: &domain.Password{ + SecretString: "password", + ChangeRequired: true, + }, + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } + }, + res: res{ + wantHuman: &domain.Human{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + DisplayName: "firstname lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + State: domain.UserStateInitial, + }, + }, + }, + { + name: "add human (with password and initial code), orgScopedUsername, ok", + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + false, + true, + true, + ), + ), + ), + expectFilterOrganizationSettings("org1", true, true), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -1688,6 +1864,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -1768,6 +1945,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -1867,6 +2045,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -1970,6 +2149,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -2105,6 +2285,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -2200,6 +2381,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -2285,6 +2467,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -2371,6 +2554,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -2516,6 +2700,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -2659,6 +2844,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -2831,6 +3017,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -3003,6 +3190,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -3182,6 +3370,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -3829,6 +4018,10 @@ func TestAddHumanCommand(t *testing.T) { ), }, nil }). + Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return expectFilterOrganizationSettingsEvents(ctx, "org1", false, false), nil + }). Append( func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { return []eventstore.Event{ @@ -3882,6 +4075,87 @@ func TestAddHumanCommand(t *testing.T) { ), }, nil }). + Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return expectFilterOrganizationSettingsEvents(ctx, "org1", false, false), nil + }). + Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewPasswordComplexityPolicyAddedEvent( + ctx, + &org.NewAggregate("id").Aggregate, + 2, + false, + false, + false, + false, + ), + }, nil + }). + Filter(), + }, + want: Want{ + Commands: []eventstore.Command{ + func() *user.HumanAddedEvent { + event := user.NewHumanAddedEvent( + context.Background(), + &agg.Aggregate, + "username", + "gigi", + "giraffe", + "", + "gigi giraffe", + AllowedLanguage, + 0, + "support@zitadel.com", + true, + ) + event.AddPasswordData("$plain$x$password", false) + return event + }(), + user.NewHumanEmailVerifiedEvent(context.Background(), &agg.Aggregate), + }, + }, + }, + { + name: "correct, orgScopedUsername", + fields: fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id"), + }, + args: args{ + human: &AddHuman{ + Email: Email{Address: "support@zitadel.com", Verified: true}, + FirstName: "gigi", + LastName: "giraffe", + Password: "password", + Username: "username", + PreferredLanguage: AllowedLanguage, + }, + orgID: "ro", + hasher: mockPasswordHasher("x"), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + filter: NewMultiFilter().Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{}, nil + }). + Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewDomainPolicyAddedEvent( + ctx, + &org.NewAggregate("id").Aggregate, + true, + true, + true, + ), + }, nil + }). + Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + // never set, as only used in creation of instance and org + return expectFilterOrganizationSettingsEvents(ctx, "org1", false, false), nil + }). Append( func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { return []eventstore.Event{ @@ -3953,6 +4227,10 @@ func TestAddHumanCommand(t *testing.T) { ), }, nil }). + Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return expectFilterOrganizationSettingsEvents(ctx, "org1", false, false), nil + }). Append( func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { return []eventstore.Event{ @@ -4025,6 +4303,10 @@ func TestAddHumanCommand(t *testing.T) { ), }, nil }). + Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return expectFilterOrganizationSettingsEvents(ctx, "org1", false, false), nil + }). Append( func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { return []eventstore.Event{ @@ -4097,6 +4379,10 @@ func TestAddHumanCommand(t *testing.T) { ), }, nil }). + Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return expectFilterOrganizationSettingsEvents(ctx, "org1", false, false), nil + }). Append( func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { return []eventstore.Event{ diff --git a/internal/command/user_machine.go b/internal/command/user_machine.go index 75ed43ee69..c281617000 100644 --- a/internal/command/user_machine.go +++ b/internal/command/user_machine.go @@ -61,8 +61,12 @@ func AddMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validati if err != nil { return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-3M9fs", "Errors.Org.DomainPolicy.NotFound") } + orgScopedUsername, err := checkOrganizationScopedUsernames(ctx, filter, a.ResourceOwner, nil) + if err != nil { + return nil, err + } return []eventstore.Command{ - user.NewMachineAddedEvent(ctx, &a.Aggregate, machine.Username, machine.Name, machine.Description, domainPolicy.UserLoginMustBeDomain, machine.AccessTokenType), + user.NewMachineAddedEvent(ctx, &a.Aggregate, machine.Username, machine.Name, machine.Description, domainPolicy.UserLoginMustBeDomain || orgScopedUsername, machine.AccessTokenType), }, nil }, nil } diff --git a/internal/command/user_machine_test.go b/internal/command/user_machine_test.go index 6d94154a42..32664b5c1b 100644 --- a/internal/command/user_machine_test.go +++ b/internal/command/user_machine_test.go @@ -121,6 +121,54 @@ func TestCommandSide_AddMachine(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "name", + "description", + true, + domain.OIDCTokenTypeBearer, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Description: "description", + Name: "name", + Username: "username", + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "add machine, orgScopedUsername, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + false, + true, + true, + ), + ), + ), + expectFilterOrganizationSettings("org1", true, true), expectPush( user.NewMachineAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, @@ -167,6 +215,7 @@ func TestCommandSide_AddMachine(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewMachineAddedEvent(context.Background(), &user.NewAggregate("optionalID1", "org1").Aggregate, @@ -214,6 +263,7 @@ func TestCommandSide_AddMachine(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewMachineAddedEvent(context.Background(), &user.NewAggregate("aggregateID", "org1").Aggregate, @@ -264,6 +314,7 @@ func TestCommandSide_AddMachine(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewMachineAddedEvent(context.Background(), &user.NewAggregate("aggregateID", "org1").Aggregate, @@ -312,6 +363,7 @@ func TestCommandSide_AddMachine(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewMachineAddedEvent(context.Background(), &user.NewAggregate("aggregateID", "org1").Aggregate, @@ -361,6 +413,7 @@ func TestCommandSide_AddMachine(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewMachineAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, @@ -436,6 +489,7 @@ func TestCommandSide_AddMachine(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewMachineAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, @@ -489,6 +543,7 @@ func TestCommandSide_AddMachine(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewMachineAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, diff --git a/internal/command/user_test.go b/internal/command/user_test.go index 6a1597fc8b..c971f0939d 100644 --- a/internal/command/user_test.go +++ b/internal/command/user_test.go @@ -295,12 +295,14 @@ func TestCommandSide_UsernameChange(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewUsernameChangedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, "username", "test@test.ch", true, + false, ), ), ), @@ -317,6 +319,61 @@ func TestCommandSide_UsernameChange(t *testing.T) { }, }, }, + { + name: "email as username, orgScopedUsername, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter(), + expectFilter( + eventFromEventPusher( + instance.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + false, + true, + true, + ), + ), + ), + expectFilterOrganizationSettings("org1", true, true), + expectPush( + user.NewUsernameChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "test", + false, + true, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + username: "test", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, { name: "email as username, verified domain, ok", fields: fields{ @@ -348,6 +405,7 @@ func TestCommandSide_UsernameChange(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewDomainVerifiedEvent(context.Background(), @@ -362,6 +420,7 @@ func TestCommandSide_UsernameChange(t *testing.T) { "username", "test@test.ch", false, + false, ), ), ), @@ -409,12 +468,14 @@ func TestCommandSide_UsernameChange(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewUsernameChangedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, "username", "username1", true, + false, ), ), ), @@ -462,11 +523,13 @@ func TestCommandSide_UsernameChange(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewUsernameChangedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, "username", "username1", + false, true, ), ), @@ -506,7 +569,7 @@ func TestCommandSide_UsernameChange(t *testing.T) { func TestCommandSide_DeactivateUser(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type ( args struct { @@ -528,9 +591,7 @@ func TestCommandSide_DeactivateUser(t *testing.T) { { name: "userid missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -544,8 +605,7 @@ func TestCommandSide_DeactivateUser(t *testing.T) { { name: "user not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -561,8 +621,7 @@ func TestCommandSide_DeactivateUser(t *testing.T) { { name: "user already inactive, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -598,8 +657,7 @@ func TestCommandSide_DeactivateUser(t *testing.T) { { name: "deactivate user, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -638,7 +696,7 @@ func TestCommandSide_DeactivateUser(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } got, err := r.DeactivateUser(tt.args.ctx, tt.args.userID, tt.args.orgID) if tt.res.err == nil { @@ -656,7 +714,7 @@ func TestCommandSide_DeactivateUser(t *testing.T) { func TestCommandSide_ReactivateUser(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type ( args struct { @@ -678,9 +736,7 @@ func TestCommandSide_ReactivateUser(t *testing.T) { { name: "userid missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -694,8 +750,7 @@ func TestCommandSide_ReactivateUser(t *testing.T) { { name: "user not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -711,8 +766,7 @@ func TestCommandSide_ReactivateUser(t *testing.T) { { name: "user already active, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -743,8 +797,7 @@ func TestCommandSide_ReactivateUser(t *testing.T) { { name: "reactivate user, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -787,7 +840,7 @@ func TestCommandSide_ReactivateUser(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } got, err := r.ReactivateUser(tt.args.ctx, tt.args.userID, tt.args.orgID) if tt.res.err == nil { @@ -805,7 +858,7 @@ func TestCommandSide_ReactivateUser(t *testing.T) { func TestCommandSide_LockUser(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type ( args struct { @@ -827,9 +880,7 @@ func TestCommandSide_LockUser(t *testing.T) { { name: "userid missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -843,8 +894,7 @@ func TestCommandSide_LockUser(t *testing.T) { { name: "user not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -860,8 +910,7 @@ func TestCommandSide_LockUser(t *testing.T) { { name: "user already locked, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -897,8 +946,7 @@ func TestCommandSide_LockUser(t *testing.T) { { name: "lock user, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -937,7 +985,7 @@ func TestCommandSide_LockUser(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } got, err := r.LockUser(tt.args.ctx, tt.args.userID, tt.args.orgID) if tt.res.err == nil { @@ -955,7 +1003,7 @@ func TestCommandSide_LockUser(t *testing.T) { func TestCommandSide_UnlockUser(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type ( args struct { @@ -977,9 +1025,7 @@ func TestCommandSide_UnlockUser(t *testing.T) { { name: "userid missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -993,8 +1039,7 @@ func TestCommandSide_UnlockUser(t *testing.T) { { name: "user not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -1010,8 +1055,7 @@ func TestCommandSide_UnlockUser(t *testing.T) { { name: "user already active, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -1042,8 +1086,7 @@ func TestCommandSide_UnlockUser(t *testing.T) { { name: "unlock user, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -1086,7 +1129,7 @@ func TestCommandSide_UnlockUser(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } got, err := r.UnlockUser(tt.args.ctx, tt.args.userID, tt.args.orgID) if tt.res.err == nil { @@ -1104,7 +1147,7 @@ func TestCommandSide_UnlockUser(t *testing.T) { func TestCommandSide_RemoveUser(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type ( args struct { @@ -1129,9 +1172,7 @@ func TestCommandSide_RemoveUser(t *testing.T) { { name: "userid missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -1145,8 +1186,7 @@ func TestCommandSide_RemoveUser(t *testing.T) { { name: "user not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -1162,8 +1202,7 @@ func TestCommandSide_RemoveUser(t *testing.T) { { name: "org iam policy not found, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -1196,8 +1235,7 @@ func TestCommandSide_RemoveUser(t *testing.T) { { name: "remove user, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -1225,6 +1263,60 @@ func TestCommandSide_RemoveUser(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), + expectPush( + user.NewUserRemovedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + nil, + true, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "remove user, orgScopedUsername, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter(), + expectFilter( + eventFromEventPusher( + instance.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + false, + true, + true, + ), + ), + ), + expectFilterOrganizationSettings("org1", true, true), expectPush( user.NewUserRemovedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, @@ -1249,8 +1341,7 @@ func TestCommandSide_RemoveUser(t *testing.T) { { name: "remove user with erxternal idp, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -1286,6 +1377,7 @@ func TestCommandSide_RemoveUser(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewUserRemovedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, @@ -1315,8 +1407,7 @@ func TestCommandSide_RemoveUser(t *testing.T) { { name: "remove user with user memberships, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -1344,6 +1435,7 @@ func TestCommandSide_RemoveUser(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewUserRemovedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, @@ -1418,7 +1510,7 @@ func TestCommandSide_RemoveUser(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } got, err := r.RemoveUser(tt.args.ctx, tt.args.userID, tt.args.orgID, tt.args.cascadeUserMemberships, tt.args.cascadeUserGrants...) if tt.res.err == nil { @@ -1434,7 +1526,7 @@ func TestCommandSide_RemoveUser(t *testing.T) { func TestCommands_RevokeAccessToken(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -1455,7 +1547,7 @@ func TestCommands_RevokeAccessToken(t *testing.T) { { "id missing error", fields{ - eventstoreExpect(t), + expectEventstore(), }, args{ context.Background(), @@ -1471,7 +1563,7 @@ func TestCommands_RevokeAccessToken(t *testing.T) { { "not active error", fields{ - eventstoreExpect(t, + expectEventstore( expectFilter( eventFromEventPusher( user.NewUserTokenAddedEvent(context.Background(), @@ -1507,7 +1599,7 @@ func TestCommands_RevokeAccessToken(t *testing.T) { { "active ok", fields{ - eventstoreExpect(t, + expectEventstore( expectFilter( eventFromEventPusher( user.NewUserTokenAddedEvent(context.Background(), @@ -1552,7 +1644,7 @@ func TestCommands_RevokeAccessToken(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } got, err := c.RevokeAccessToken(tt.args.ctx, tt.args.userID, tt.args.orgID, tt.args.tokenID) if tt.res.err == nil { @@ -1570,7 +1662,7 @@ func TestCommands_RevokeAccessToken(t *testing.T) { func TestCommandSide_UserDomainClaimedSent(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -1589,9 +1681,7 @@ func TestCommandSide_UserDomainClaimedSent(t *testing.T) { { name: "userid missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -1604,8 +1694,7 @@ func TestCommandSide_UserDomainClaimedSent(t *testing.T) { { name: "user not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -1621,8 +1710,7 @@ func TestCommandSide_UserDomainClaimedSent(t *testing.T) { { name: "code sent, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -1657,7 +1745,7 @@ func TestCommandSide_UserDomainClaimedSent(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } err := r.UserDomainClaimedSent(tt.args.ctx, tt.args.resourceOwner, tt.args.userID) if tt.res.err == nil { diff --git a/internal/command/user_v2.go b/internal/command/user_v2.go index d6c5e7de53..028fda7f4b 100644 --- a/internal/command/user_v2.go +++ b/internal/command/user_v2.go @@ -2,6 +2,7 @@ package command import ( "context" + "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/domain" @@ -145,8 +146,13 @@ func (c *Commands) RemoveUserV2(ctx context.Context, userID, resourceOwner strin if err != nil { return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-l40ykb3xh2", "Errors.Org.DomainPolicy.NotExisting") } + organizationScopedUsername, err := c.checkOrganizationScopedUsernames(ctx, existingUser.ResourceOwner) + if err != nil { + return nil, err + } + var events []eventstore.Command - events = append(events, user.NewUserRemovedEvent(ctx, &existingUser.Aggregate().Aggregate, existingUser.UserName, existingUser.IDPLinks, domainPolicy.UserLoginMustBeDomain)) + events = append(events, user.NewUserRemovedEvent(ctx, &existingUser.Aggregate().Aggregate, existingUser.UserName, existingUser.IDPLinks, domainPolicy.UserLoginMustBeDomain || organizationScopedUsername)) for _, grantID := range cascadingGrantIDs { removeEvent, _, err := c.removeUserGrant(ctx, grantID, "", true, true, nil) diff --git a/internal/command/user_v2_human.go b/internal/command/user_v2_human.go index 0945ae7578..3bb54c6d07 100644 --- a/internal/command/user_v2_human.go +++ b/internal/command/user_v2_human.go @@ -165,6 +165,11 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human return err } + organizationScopedUsername, err := c.checkOrganizationScopedUsernames(ctx, resourceOwner) + if err != nil { + return err + } + var createCmd humanCreationCommand if human.Register { createCmd = user.NewHumanRegisteredEvent( @@ -178,7 +183,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human human.PreferredLanguage, human.Gender, human.Email.Address, - domainPolicy.UserLoginMustBeDomain, + domainPolicy.UserLoginMustBeDomain || organizationScopedUsername, human.UserAgentID, ) } else { @@ -193,7 +198,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human human.PreferredLanguage, human.Gender, human.Email.Address, - domainPolicy.UserLoginMustBeDomain, + domainPolicy.UserLoginMustBeDomain || organizationScopedUsername, ) } diff --git a/internal/command/user_v2_human_test.go b/internal/command/user_v2_human_test.go index e44e182b92..afa570cd58 100644 --- a/internal/command/user_v2_human_test.go +++ b/internal/command/user_v2_human_test.go @@ -60,6 +60,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { } userAgg := user.NewAggregate("user1", "org1") + orgAgg := org.NewAggregate("org1") cryptoAlg := crypto.CreateMockEncryptionAlg(gomock.NewController(t)) totpSecret := "TOTPSecret" @@ -191,7 +192,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, true, true, true, @@ -199,6 +200,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { ), ), expectFilter(), + expectFilterOrganizationSettings("org1", false, false), expectFilter(), ), checkPermission: newMockPermissionCheckAllowed(), @@ -233,13 +235,14 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, true, true, true, ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewHumanRegisteredEvent(context.Background(), &userAgg.Aggregate, @@ -335,13 +338,14 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, true, true, true, ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewHumanAddedEvent(context.Background(), &userAgg.Aggregate, @@ -412,6 +416,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -483,6 +488,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( newAddHumanEvent("", false, true, "", language.English), user.NewHumanEmailCodeAddedEventV2(context.Background(), @@ -542,6 +548,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -616,6 +623,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -684,13 +692,14 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, true, true, true, ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -748,13 +757,14 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, true, true, true, ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -812,13 +822,14 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, false, true, true, ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -876,7 +887,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, false, true, true, @@ -929,7 +940,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, false, true, true, @@ -944,6 +955,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -1017,13 +1029,14 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, true, true, true, ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -1127,13 +1140,14 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, true, true, true, ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -1232,13 +1246,14 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, true, true, true, ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( newAddHumanEvent("", false, true, "+41711234567", language.English), user.NewHumanInitialCodeAddedEvent( @@ -1305,6 +1320,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), @@ -1409,13 +1425,14 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, true, true, true, ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( newAddHumanEvent("", false, true, "", language.English), user.NewHumanInitialCodeAddedEvent( @@ -1479,13 +1496,14 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, true, true, true, ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewGoogleIDPAddedEvent(context.Background(), @@ -1566,13 +1584,14 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, true, true, true, ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectFilter( eventFromEventPusher( org.NewGoogleIDPAddedEvent(context.Background(), @@ -1646,13 +1665,98 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, true, true, true, ), ), ), + expectFilterOrganizationSettings("org1", false, false), + expectPush( + user.NewHumanRegisteredEvent(context.Background(), + &userAgg.Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + language.English, + domain.GenderUnspecified, + "email@test.ch", + true, + "userAgentID", + ), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &userAgg.Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("userinit"), + }, + time.Hour*1, + "authRequestID", + ), + user.NewHumanOTPAddedEvent(context.Background(), + &userAgg.Aggregate, + totpSecretEnc, + ), + user.NewHumanOTPVerifiedEvent(context.Background(), + &userAgg.Aggregate, + "", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + newCode: mockEncryptedCode("userinit", time.Hour), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &AddHuman{ + Username: "username", + FirstName: "firstname", + LastName: "lastname", + Email: Email{ + Address: "email@test.ch", + }, + PreferredLanguage: language.English, + Register: true, + UserAgentID: "userAgentID", + AuthRequestID: "authRequestID", + TOTPSecret: totpSecret, + }, + secretGenerator: GetMockSecretGenerator(t), + allowInitMail: true, + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + res: res{ + want: &domain.ObjectDetails{ + Sequence: 0, + EventDate: time.Time{}, + ResourceOwner: "org1", + }, + wantID: "user1", + }, + }, + { + name: "register human with TOTPSecret, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &orgAgg.Aggregate, + true, + true, + true, + ), + ), + ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewHumanRegisteredEvent(context.Background(), &userAgg.Aggregate, @@ -1729,7 +1833,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, false, true, true, @@ -1775,14 +1879,14 @@ func TestCommandSide_AddUserHuman(t *testing.T) { }, }, { - name: "register human (validate domain), ok", + name: "register human (validate domain), orgScopedUsername, ok", fields: fields{ eventstore: expectEventstore( expectFilter(), expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, false, true, true, @@ -1797,6 +1901,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", true, true), expectPush( user.NewHumanRegisteredEvent(context.Background(), &userAgg.Aggregate, @@ -1808,7 +1913,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { language.English, domain.GenderUnspecified, "email@example.com", - false, + true, "userAgentID", ), user.NewHumanInitialCodeAddedEvent(context.Background(), @@ -1864,7 +1969,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, false, true, true, @@ -1886,6 +1991,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewHumanRegisteredEvent(context.Background(), &userAgg.Aggregate, @@ -1953,7 +2059,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, false, true, true, @@ -1979,6 +2085,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewHumanRegisteredEvent(context.Background(), &userAgg.Aggregate, @@ -2107,6 +2214,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { } userAgg := user.NewAggregate("user1", "org1") + orgAgg := user.NewAggregate("org1", "org1") tests := []struct { name string @@ -2199,19 +2307,68 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, true, true, true, ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewUsernameChangedEvent(context.Background(), &userAgg.Aggregate, "username", "changed", true, + false, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &ChangeHuman{ + Username: gu.Ptr("changed"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + Sequence: 0, + EventDate: time.Time{}, + ResourceOwner: "org1", + }, + }, + }, + { + name: "change human username, orgScopedUsername, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + newAddHumanEvent("$plain$x$password", true, false, "", language.English), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &orgAgg.Aggregate, + false, + true, + true, + ), + ), + ), + expectFilterOrganizationSettings("org1", true, true), + expectPush( + user.NewUsernameChangedEvent(context.Background(), + &userAgg.Aggregate, + "username", + "changed", + false, + true, ), ), ), diff --git a/internal/command/user_v2_machine_test.go b/internal/command/user_v2_machine_test.go index 14df4bfae7..c5883286b8 100644 --- a/internal/command/user_v2_machine_test.go +++ b/internal/command/user_v2_machine_test.go @@ -32,6 +32,8 @@ func TestCommandSide_ChangeUserMachine(t *testing.T) { } userAgg := user.NewAggregate("user1", "org1") + orgAgg := org.NewAggregate("org1") + userAddedEvent := user.NewMachineAddedEvent(context.Background(), &userAgg.Aggregate, "username", @@ -101,19 +103,65 @@ func TestCommandSide_ChangeUserMachine(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &userAgg.Aggregate, + &orgAgg.Aggregate, true, true, true, ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewUsernameChangedEvent(context.Background(), &userAgg.Aggregate, "username", "changed", true, + false, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Username: gu.Ptr("changed"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + Sequence: 0, + EventDate: time.Time{}, + ResourceOwner: "org1", + }, + }, + }, { + name: "change machine username, orgScopedUsername, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &orgAgg.Aggregate, + false, + true, + true, + ), + ), + ), + expectFilterOrganizationSettings("org1", true, true), + expectPush( + user.NewUsernameChangedEvent(context.Background(), + &userAgg.Aggregate, + "username", + "changed", + false, + true, ), ), ), diff --git a/internal/command/user_v2_model_test.go b/internal/command/user_v2_model_test.go index cb12c8fcda..ecaa2db400 100644 --- a/internal/command/user_v2_model_test.go +++ b/internal/command/user_v2_model_test.go @@ -343,6 +343,7 @@ func TestCommandSide_userExistsWriteModel(t *testing.T) { "username", "changed", true, + false, ), ), ), diff --git a/internal/command/user_v2_test.go b/internal/command/user_v2_test.go index 685ad95253..f878569a06 100644 --- a/internal/command/user_v2_test.go +++ b/internal/command/user_v2_test.go @@ -18,6 +18,7 @@ import ( ) func TestCommandSide_LockUserV2(t *testing.T) { + userAgg := &user.NewAggregate("user1", "org1").Aggregate type fields struct { eventstore func(*testing.T) *eventstore.Eventstore checkPermission domain.PermissionCheck @@ -79,7 +80,7 @@ func TestCommandSide_LockUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "firstname", "lastname", @@ -93,7 +94,7 @@ func TestCommandSide_LockUserV2(t *testing.T) { ), eventFromEventPusher( user.NewUserLockedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, ), ), ), @@ -117,7 +118,7 @@ func TestCommandSide_LockUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewMachineAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "name", "description", @@ -127,7 +128,7 @@ func TestCommandSide_LockUserV2(t *testing.T) { ), eventFromEventPusher( user.NewUserLockedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, ), ), ), @@ -151,7 +152,7 @@ func TestCommandSide_LockUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "firstname", "lastname", @@ -166,7 +167,7 @@ func TestCommandSide_LockUserV2(t *testing.T) { ), expectPush( user.NewUserLockedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, ), ), ), @@ -189,7 +190,7 @@ func TestCommandSide_LockUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "firstname", "lastname", @@ -222,7 +223,7 @@ func TestCommandSide_LockUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewMachineAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "name", "description", @@ -233,7 +234,7 @@ func TestCommandSide_LockUserV2(t *testing.T) { ), expectPush( user.NewUserLockedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, ), ), ), @@ -271,6 +272,7 @@ func TestCommandSide_LockUserV2(t *testing.T) { } func TestCommandSide_UnlockUserV2(t *testing.T) { + userAgg := &user.NewAggregate("user1", "org1").Aggregate type fields struct { eventstore func(*testing.T) *eventstore.Eventstore checkPermission domain.PermissionCheck @@ -332,7 +334,7 @@ func TestCommandSide_UnlockUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "firstname", "lastname", @@ -364,7 +366,7 @@ func TestCommandSide_UnlockUserV2(t *testing.T) { eventstore: expectEventstore( expectFilter( user.NewMachineAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "name", "description", @@ -392,7 +394,7 @@ func TestCommandSide_UnlockUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "firstname", "lastname", @@ -406,12 +408,12 @@ func TestCommandSide_UnlockUserV2(t *testing.T) { ), eventFromEventPusher( user.NewUserLockedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate), + userAgg), ), ), expectPush( user.NewUserUnlockedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, ), ), ), @@ -434,7 +436,7 @@ func TestCommandSide_UnlockUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "firstname", "lastname", @@ -448,7 +450,7 @@ func TestCommandSide_UnlockUserV2(t *testing.T) { ), eventFromEventPusher( user.NewUserLockedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate), + userAgg), ), ), ), @@ -471,7 +473,7 @@ func TestCommandSide_UnlockUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewMachineAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "name", "description", @@ -481,12 +483,12 @@ func TestCommandSide_UnlockUserV2(t *testing.T) { ), eventFromEventPusher( user.NewUserLockedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate), + userAgg), ), ), expectPush( user.NewUserUnlockedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, ), ), ), @@ -524,6 +526,7 @@ func TestCommandSide_UnlockUserV2(t *testing.T) { } func TestCommandSide_DeactivateUserV2(t *testing.T) { + userAgg := &user.NewAggregate("user1", "org1").Aggregate type fields struct { eventstore func(*testing.T) *eventstore.Eventstore checkPermission domain.PermissionCheck @@ -585,7 +588,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "firstname", "lastname", @@ -599,7 +602,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) { ), eventFromEventPusher( user.NewHumanInitialCodeAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, nil, time.Hour*1, "", ), @@ -625,7 +628,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "firstname", "lastname", @@ -639,7 +642,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) { ), eventFromEventPusher( user.NewUserDeactivatedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, ), ), ), @@ -663,7 +666,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "firstname", "lastname", @@ -677,13 +680,13 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) { ), eventFromEventPusher( user.NewHumanInitializedCheckSucceededEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, ), ), ), expectPush( user.NewUserDeactivatedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, ), ), ), @@ -706,7 +709,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "firstname", "lastname", @@ -720,7 +723,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) { ), eventFromEventPusher( user.NewHumanInitializedCheckSucceededEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, ), ), ), @@ -744,7 +747,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewMachineAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "name", "description", @@ -754,7 +757,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) { ), eventFromEventPusher( user.NewUserDeactivatedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, ), ), ), @@ -778,7 +781,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewMachineAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "name", "description", @@ -789,7 +792,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) { ), expectPush( user.NewUserDeactivatedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, ), ), ), @@ -827,6 +830,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) { } func TestCommandSide_ReactivateUserV2(t *testing.T) { + userAgg := &user.NewAggregate("user1", "org1").Aggregate type fields struct { eventstore func(*testing.T) *eventstore.Eventstore checkPermission domain.PermissionCheck @@ -888,7 +892,7 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "firstname", "lastname", @@ -921,7 +925,7 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewMachineAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "name", "description", @@ -950,7 +954,7 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "firstname", "lastname", @@ -964,12 +968,12 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) { ), eventFromEventPusher( user.NewUserDeactivatedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate), + userAgg), ), ), expectPush( user.NewUserReactivatedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, ), ), ), @@ -992,7 +996,7 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "firstname", "lastname", @@ -1006,7 +1010,7 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) { ), eventFromEventPusher( user.NewUserDeactivatedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate), + userAgg), ), ), ), @@ -1029,7 +1033,7 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewMachineAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "name", "description", @@ -1039,12 +1043,12 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) { ), eventFromEventPusher( user.NewUserDeactivatedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate), + userAgg), ), ), expectPush( user.NewUserReactivatedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, ), ), ), @@ -1084,6 +1088,8 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) { func TestCommandSide_RemoveUserV2(t *testing.T) { ctxUserID := "ctxUserID" ctx := authz.SetCtxData(context.Background(), authz.CtxData{UserID: ctxUserID}) + userAgg := &user.NewAggregate("user1", "org1").Aggregate + orgAgg := &org.NewAggregate("org1").Aggregate type fields struct { eventstore func(*testing.T) *eventstore.Eventstore checkPermission domain.PermissionCheck @@ -1144,7 +1150,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(ctx, - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "firstname", "lastname", @@ -1158,7 +1164,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), eventFromEventPusher( user.NewUserRemovedEvent(ctx, - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", nil, true, @@ -1184,7 +1190,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(ctx, - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "firstname", "lastname", @@ -1199,17 +1205,18 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), expectFilter( eventFromEventPusher( - org.NewDomainPolicyAddedEvent(ctx, - &user.NewAggregate("user1", "org1").Aggregate, + org.NewDomainPolicyAddedEvent(context.Background(), + orgAgg, true, true, true, ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewUserRemovedEvent(ctx, - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", nil, true, @@ -1234,7 +1241,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(ctx, - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "firstname", "lastname", @@ -1248,7 +1255,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), eventFromEventPusher( user.NewHumanInitializedCheckSucceededEvent(ctx, - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, ), ), ), @@ -1269,7 +1276,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewMachineAddedEvent(ctx, - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "name", "description", @@ -1279,7 +1286,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), eventFromEventPusher( user.NewUserRemovedEvent(ctx, - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", nil, true, @@ -1304,7 +1311,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { expectFilter( eventFromEventPusher( user.NewMachineAddedEvent(ctx, - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", "name", "description", @@ -1315,17 +1322,18 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), expectFilter( eventFromEventPusher( - org.NewDomainPolicyAddedEvent(ctx, - &user.NewAggregate("user1", "org1").Aggregate, + org.NewDomainPolicyAddedEvent(context.Background(), + orgAgg, true, true, true, ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewUserRemovedEvent(ctx, - &user.NewAggregate("user1", "org1").Aggregate, + userAgg, "username", nil, true, @@ -1366,13 +1374,14 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(ctx, - &user.NewAggregate(ctxUserID, "org1").Aggregate, + orgAgg, true, true, true, ), ), ), + expectFilterOrganizationSettings("org1", false, false), expectPush( user.NewUserRemovedEvent(ctx, &user.NewAggregate(ctxUserID, "org1").Aggregate, diff --git a/internal/command/user_v2_username.go b/internal/command/user_v2_username.go index a9132aceb9..cfec4f3b0f 100644 --- a/internal/command/user_v2_username.go +++ b/internal/command/user_v2_username.go @@ -18,10 +18,21 @@ func (c *Commands) changeUsername(ctx context.Context, cmds []eventstore.Command if err != nil { return cmds, zerrors.ThrowPreconditionFailed(err, "COMMAND-79pv6e1q62", "Errors.Org.DomainPolicy.NotExisting") } + + organizationScopedUsername, err := c.checkOrganizationScopedUsernames(ctx, orgID) + if err != nil { + return cmds, err + } + if err = c.userValidateDomain(ctx, orgID, userName, domainPolicy.UserLoginMustBeDomain); err != nil { return cmds, err } return append(cmds, - user.NewUsernameChangedEvent(ctx, &wm.Aggregate().Aggregate, wm.UserName, userName, domainPolicy.UserLoginMustBeDomain), + user.NewUsernameChangedEvent(ctx, &wm.Aggregate().Aggregate, + wm.UserName, + userName, + domainPolicy.UserLoginMustBeDomain, + organizationScopedUsername, + ), ), nil } diff --git a/internal/domain/organization_settings.go b/internal/domain/organization_settings.go new file mode 100644 index 0000000000..3d9ed819fe --- /dev/null +++ b/internal/domain/organization_settings.go @@ -0,0 +1,19 @@ +package domain + +type OrganizationSettingsState int32 + +const ( + OrganizationSettingsStateUnspecified OrganizationSettingsState = iota + OrganizationSettingsStateActive + OrganizationSettingsStateRemoved + + organizationSettingsStateCount +) + +func (c OrganizationSettingsState) Valid() bool { + return c >= 0 && c < organizationSettingsStateCount +} + +func (s OrganizationSettingsState) Exists() bool { + return s.Valid() && s != OrganizationSettingsStateUnspecified && s != OrganizationSettingsStateRemoved +} diff --git a/internal/domain/permission.go b/internal/domain/permission.go index 1405991dae..39738b1e84 100644 --- a/internal/domain/permission.go +++ b/internal/domain/permission.go @@ -65,6 +65,9 @@ const ( PermissionUserGrantWrite = "user.grant.write" PermissionUserGrantRead = "user.grant.read" PermissionUserGrantDelete = "user.grant.delete" + PermissionIAMPolicyWrite = "iam.policy.write" + PermissionIAMPolicyDelete = "iam.policy.delete" + PermissionPolicyRead = "policy.read" ) // ProjectPermissionCheck is used as a check for preconditions dependent on application, project, user resourceowner and usergrants. diff --git a/internal/integration/client.go b/internal/integration/client.go index a89e4fa621..d4e57d06d0 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -405,6 +405,27 @@ func (i *Instance) CreateOrganizationWithUserID(ctx context.Context, name, userI return resp } +func (i *Instance) SetOrganizationSettings(ctx context.Context, t *testing.T, orgID string, organizationScopedUsernames bool) *settings_v2beta.SetOrganizationSettingsResponse { + resp, err := i.Client.SettingsV2beta.SetOrganizationSettings(ctx, + &settings_v2beta.SetOrganizationSettingsRequest{ + OrganizationId: orgID, + OrganizationScopedUsernames: gu.Ptr(organizationScopedUsernames), + }, + ) + require.NoError(t, err) + return resp +} + +func (i *Instance) DeleteOrganizationSettings(ctx context.Context, t *testing.T, orgID string) *settings_v2beta.DeleteOrganizationSettingsResponse { + resp, err := i.Client.SettingsV2beta.DeleteOrganizationSettings(ctx, + &settings_v2beta.DeleteOrganizationSettingsRequest{ + OrganizationId: orgID, + }, + ) + require.NoError(t, err) + return resp +} + func (i *Instance) CreateHumanUserVerified(ctx context.Context, org, email, phone string) *user_v2.AddHumanUserResponse { resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ diff --git a/internal/query/administrators.go b/internal/query/administrators.go index 20bb955bbd..0128c0f25e 100644 --- a/internal/query/administrators.go +++ b/internal/query/administrators.go @@ -185,7 +185,7 @@ func (q *Queries) searchAdministrators(ctx context.Context, queries *MembershipS eq := sq.Eq{membershipInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInvalidArgument(err, "QUERY-TODO", "Errors.Query.InvalidRequest") + return nil, zerrors.ThrowInvalidArgument(err, "QUERY-xhEnpLFNpJ", "Errors.Query.InvalidRequest") } latestState, err := q.latestState(ctx, orgMemberTable, instanceMemberTable, projectMemberTable, projectGrantMemberTable) if err != nil { @@ -335,7 +335,7 @@ func prepareAdministratorsQuery(ctx context.Context, queries *MembershipSearchQu } if err := rows.Close(); err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-TODO", "Errors.Query.CloseRows") + return nil, zerrors.ThrowInternal(err, "QUERY-ajYcn0eK7f", "Errors.Query.CloseRows") } return &Administrators{ diff --git a/internal/query/organization_settings.go b/internal/query/organization_settings.go new file mode 100644 index 0000000000..2670fb4f8b --- /dev/null +++ b/internal/query/organization_settings.go @@ -0,0 +1,196 @@ +package query + +import ( + "context" + "database/sql" + "slices" + "time" + + sq "github.com/Masterminds/squirrel" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query/projection" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +var ( + organizationSettingsTable = table{ + name: projection.OrganizationSettingsTable, + instanceIDCol: projection.OrganizationSettingsInstanceIDCol, + } + OrganizationSettingsColumnID = Column{ + name: projection.OrganizationSettingsIDCol, + table: organizationSettingsTable, + } + OrganizationSettingsColumnCreationDate = Column{ + name: projection.OrganizationSettingsCreationDateCol, + table: organizationSettingsTable, + } + OrganizationSettingsColumnChangeDate = Column{ + name: projection.OrganizationSettingsChangeDateCol, + table: organizationSettingsTable, + } + OrganizationSettingsColumnResourceOwner = Column{ + name: projection.OrganizationSettingsResourceOwnerCol, + table: organizationSettingsTable, + } + OrganizationSettingsColumnInstanceID = Column{ + name: projection.OrganizationSettingsInstanceIDCol, + table: organizationSettingsTable, + } + OrganizationSettingsColumnSequence = Column{ + name: projection.OrganizationSettingsSequenceCol, + table: organizationSettingsTable, + } + OrganizationSettingsColumnOrganizationScopedUsernames = Column{ + name: projection.OrganizationSettingsOrganizationScopedUsernamesCol, + table: organizationSettingsTable, + } +) + +type OrganizationSettingsList struct { + SearchResponse + OrganizationSettingsList []*OrganizationSettings +} + +func organizationSettingsListCheckPermission(ctx context.Context, organizationSettingsList *OrganizationSettingsList, permissionCheck domain.PermissionCheck) { + organizationSettingsList.OrganizationSettingsList = slices.DeleteFunc(organizationSettingsList.OrganizationSettingsList, + func(organizationSettings *OrganizationSettings) bool { + return organizationSettingsCheckPermission(ctx, organizationSettings.ResourceOwner, organizationSettings.ID, permissionCheck) != nil + }, + ) +} + +func organizationSettingsCheckPermission(ctx context.Context, resourceOwner string, id string, permissionCheck domain.PermissionCheck) error { + return permissionCheck(ctx, domain.PermissionPolicyRead, resourceOwner, id) +} + +type OrganizationSettings struct { + ID string + CreationDate time.Time + ChangeDate time.Time + ResourceOwner string + Sequence uint64 + + OrganizationScopedUsernames bool +} + +type OrganizationSettingsSearchQueries struct { + SearchRequest + Queries []SearchQuery +} + +func (q *OrganizationSettingsSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + query = q.SearchRequest.toQuery(query) + for _, q := range q.Queries { + query = q.toQuery(query) + } + return query +} + +func organizationSettingsPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *OrganizationSettingsSearchQueries) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + OrganizationSettingsColumnID, + domain.PermissionPolicyRead, + SingleOrgPermissionOption(queries.Queries), + ) + return query.JoinClause(join, args...) +} + +func (q *Queries) SearchOrganizationSettings(ctx context.Context, queries *OrganizationSettingsSearchQueries, permissionCheck domain.PermissionCheck) (*OrganizationSettingsList, error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + settings, err := q.searchOrganizationSettings(ctx, queries, permissionCheckV2) + if err != nil { + return nil, err + } + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + organizationSettingsListCheckPermission(ctx, settings, permissionCheck) + } + return settings, nil +} + +func (q *Queries) searchOrganizationSettings(ctx context.Context, queries *OrganizationSettingsSearchQueries, permissionCheckV2 bool) (settingsList *OrganizationSettingsList, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + query, scan := prepareOrganizationSettingsListQuery() + query = organizationSettingsPermissionCheckV2(ctx, query, permissionCheckV2, queries) + eq := sq.Eq{OrganizationSettingsColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} + stmt, args, err := queries.toQuery(query).Where(eq).ToSql() + if err != nil { + return nil, zerrors.ThrowInvalidArgument(err, "QUERY-qNPeOXlMwj", "Errors.Query.InvalidRequest") + } + + err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { + settingsList, err = scan(rows) + return err + }, stmt, args...) + if err != nil { + return nil, err + } + return settingsList, nil +} + +func NewOrganizationSettingsOrganizationIDSearchQuery(ids []string) (SearchQuery, error) { + list := make([]interface{}, len(ids)) + for i, value := range ids { + list[i] = value + } + return NewListQuery(OrganizationSettingsColumnID, list, ListIn) +} + +func NewOrganizationSettingsOrganizationScopedUsernamesSearchQuery(organizationScopedUsernames bool) (SearchQuery, error) { + return NewBoolQuery(OrganizationSettingsColumnOrganizationScopedUsernames, organizationScopedUsernames) +} + +func prepareOrganizationSettingsListQuery() (sq.SelectBuilder, func(*sql.Rows) (*OrganizationSettingsList, error)) { + return sq.Select( + OrganizationSettingsColumnID.identifier(), + OrganizationSettingsColumnCreationDate.identifier(), + OrganizationSettingsColumnChangeDate.identifier(), + OrganizationSettingsColumnResourceOwner.identifier(), + OrganizationSettingsColumnSequence.identifier(), + OrganizationSettingsColumnOrganizationScopedUsernames.identifier(), + countColumn.identifier(), + ).From(organizationSettingsTable.identifier()). + PlaceholderFormat(sq.Dollar), + func(rows *sql.Rows) (*OrganizationSettingsList, error) { + settingsList := make([]*OrganizationSettings, 0) + var ( + count uint64 + ) + for rows.Next() { + settings := new(OrganizationSettings) + err := rows.Scan( + &settings.ID, + &settings.CreationDate, + &settings.ChangeDate, + &settings.ResourceOwner, + &settings.Sequence, + &settings.OrganizationScopedUsernames, + &count, + ) + if err != nil { + return nil, err + } + settingsList = append(settingsList, settings) + } + + if err := rows.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-mmC1K0t5Fq", "Errors.Query.CloseRows") + } + + return &OrganizationSettingsList{ + OrganizationSettingsList: settingsList, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil + } +} diff --git a/internal/query/organization_settings_test.go b/internal/query/organization_settings_test.go new file mode 100644 index 0000000000..7ee370c548 --- /dev/null +++ b/internal/query/organization_settings_test.go @@ -0,0 +1,180 @@ +package query + +import ( + "database/sql" + "database/sql/driver" + "errors" + "fmt" + "regexp" + "testing" +) + +var ( + prepareOrganizationSettingsListStmt = `SELECT projections.organization_settings.id,` + + ` projections.organization_settings.creation_date,` + + ` projections.organization_settings.change_date,` + + ` projections.organization_settings.resource_owner,` + + ` projections.organization_settings.sequence,` + + ` projections.organization_settings.organization_scoped_usernames,` + + ` COUNT(*) OVER ()` + + ` FROM projections.organization_settings` + prepareOrganizationSettingsListCols = []string{ + "id", + "creation_date", + "change_date", + "resource_owner", + "sequence", + "organization_scoped_usernames", + "count", + } +) + +func Test_OrganizationSettingsListPrepares(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + prepare interface{} + want want + object interface{} + }{ + { + name: "prepareOrganizationSettingsListQuery no result", + prepare: prepareOrganizationSettingsListQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareOrganizationSettingsListStmt), + nil, + nil, + ), + }, + object: &OrganizationSettingsList{OrganizationSettingsList: []*OrganizationSettings{}}, + }, + { + name: "prepareOrganizationSettingsListQuery one result", + prepare: prepareOrganizationSettingsListQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareOrganizationSettingsListStmt), + prepareOrganizationSettingsListCols, + [][]driver.Value{ + { + "id", + testNow, + testNow, + "ro", + uint64(20211108), + true, + }, + }, + ), + }, + object: &OrganizationSettingsList{ + SearchResponse: SearchResponse{ + Count: 1, + }, + OrganizationSettingsList: []*OrganizationSettings{ + { + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211108, + OrganizationScopedUsernames: true, + }, + }, + }, + }, + { + name: "prepareOrganizationSettingsListQuery multiple result", + prepare: prepareOrganizationSettingsListQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareOrganizationSettingsListStmt), + prepareOrganizationSettingsListCols, + [][]driver.Value{ + { + "id-1", + testNow, + testNow, + "ro", + uint64(20211108), + true, + }, + { + "id-2", + testNow, + testNow, + "ro", + uint64(20211108), + false, + }, + { + "id-3", + testNow, + testNow, + "ro", + uint64(20211108), + true, + }, + }, + ), + }, + object: &OrganizationSettingsList{ + SearchResponse: SearchResponse{ + Count: 3, + }, + OrganizationSettingsList: []*OrganizationSettings{ + { + ID: "id-1", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211108, + OrganizationScopedUsernames: true, + }, + { + ID: "id-2", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211108, + OrganizationScopedUsernames: false, + }, + { + ID: "id-3", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211108, + OrganizationScopedUsernames: true, + }, + }, + }, + }, + { + name: "prepareOrganizationSettingsListQuery sql err", + prepare: prepareOrganizationSettingsListQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(prepareOrganizationSettingsListStmt), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: (*OrganizationSettingsList)(nil), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) + }) + } +} diff --git a/internal/query/projection/organization_settings.go b/internal/query/projection/organization_settings.go new file mode 100644 index 0000000000..c97bb2691a --- /dev/null +++ b/internal/query/projection/organization_settings.go @@ -0,0 +1,141 @@ +package projection + +import ( + "context" + + "github.com/zitadel/zitadel/internal/eventstore" + old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" + settings "github.com/zitadel/zitadel/internal/repository/organization_settings" +) + +const ( + OrganizationSettingsTable = "projections.organization_settings" + OrganizationSettingsIDCol = "id" + OrganizationSettingsCreationDateCol = "creation_date" + OrganizationSettingsChangeDateCol = "change_date" + OrganizationSettingsResourceOwnerCol = "resource_owner" + OrganizationSettingsInstanceIDCol = "instance_id" + OrganizationSettingsSequenceCol = "sequence" + OrganizationSettingsOrganizationScopedUsernamesCol = "organization_scoped_usernames" +) + +type organizationSettingsProjection struct{} + +func newOrganizationSettingsProjection(ctx context.Context, config handler.Config) *handler.Handler { + return handler.NewHandler(ctx, &config, new(organizationSettingsProjection)) +} + +func (*organizationSettingsProjection) Name() string { + return OrganizationSettingsTable +} + +func (*organizationSettingsProjection) Init() *old_handler.Check { + return handler.NewTableCheck( + handler.NewTable([]*handler.InitColumn{ + handler.NewColumn(OrganizationSettingsIDCol, handler.ColumnTypeText), + handler.NewColumn(OrganizationSettingsCreationDateCol, handler.ColumnTypeTimestamp), + handler.NewColumn(OrganizationSettingsChangeDateCol, handler.ColumnTypeTimestamp), + handler.NewColumn(OrganizationSettingsResourceOwnerCol, handler.ColumnTypeText), + handler.NewColumn(OrganizationSettingsInstanceIDCol, handler.ColumnTypeText), + handler.NewColumn(OrganizationSettingsSequenceCol, handler.ColumnTypeInt64), + handler.NewColumn(OrganizationSettingsOrganizationScopedUsernamesCol, handler.ColumnTypeBool), + }, + handler.NewPrimaryKey(OrganizationSettingsInstanceIDCol, OrganizationSettingsResourceOwnerCol, OrganizationSettingsIDCol), + handler.WithIndex(handler.NewIndex("resource_owner", []string{OrganizationSettingsResourceOwnerCol})), + ), + ) +} + +func (p *organizationSettingsProjection) Reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: settings.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: settings.OrganizationSettingsSetEventType, + Reduce: p.reduceOrganizationSettingsSet, + }, + { + Event: settings.OrganizationSettingsRemovedEventType, + Reduce: p.reduceOrganizationSettingsRemoved, + }, + }, + }, + { + Aggregate: org.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: org.OrgRemovedEventType, + Reduce: p.reduceOrgRemoved, + }, + }, + }, + { + Aggregate: instance.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: instance.InstanceRemovedEventType, + Reduce: reduceInstanceRemovedHelper(OrganizationSettingsInstanceIDCol), + }, + }, + }, + } +} + +func (p *organizationSettingsProjection) reduceOrganizationSettingsSet(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*settings.OrganizationSettingsSetEvent](event) + if err != nil { + return nil, err + } + + return handler.NewUpsertStatement(e, + []handler.Column{ + handler.NewCol(OrganizationSettingsInstanceIDCol, e.Aggregate().InstanceID), + handler.NewCol(OrganizationSettingsResourceOwnerCol, e.Aggregate().ResourceOwner), + handler.NewCol(OrganizationSettingsIDCol, e.Aggregate().ID), + }, + []handler.Column{ + handler.NewCol(OrganizationSettingsInstanceIDCol, e.Aggregate().InstanceID), + handler.NewCol(OrganizationSettingsResourceOwnerCol, e.Aggregate().ResourceOwner), + handler.NewCol(OrganizationSettingsIDCol, e.Aggregate().ID), + handler.NewCol(OrganizationSettingsCreationDateCol, handler.OnlySetValueOnInsert(OrganizationSettingsTable, e.CreationDate())), + handler.NewCol(OrganizationSettingsChangeDateCol, e.CreationDate()), + handler.NewCol(OrganizationSettingsSequenceCol, e.Sequence()), + handler.NewCol(OrganizationSettingsOrganizationScopedUsernamesCol, e.OrganizationScopedUsernames), + }, + ), nil +} + +func (p *organizationSettingsProjection) reduceOrganizationSettingsRemoved(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*settings.OrganizationSettingsRemovedEvent](event) + if err != nil { + return nil, err + } + + return handler.NewDeleteStatement(e, + []handler.Condition{ + handler.NewCond(OrganizationSettingsInstanceIDCol, e.Aggregate().InstanceID), + handler.NewCond(OrganizationSettingsResourceOwnerCol, e.Aggregate().ResourceOwner), + handler.NewCond(OrganizationSettingsIDCol, e.Aggregate().ID), + }, + ), nil +} + +func (p *organizationSettingsProjection) reduceOrgRemoved(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*org.OrgRemovedEvent](event) + if err != nil { + return nil, err + } + + return handler.NewDeleteStatement( + e, + []handler.Condition{ + handler.NewCond(OrganizationSettingsInstanceIDCol, e.Aggregate().InstanceID), + handler.NewCond(OrganizationSettingsResourceOwnerCol, e.Aggregate().ResourceOwner), + handler.NewCond(OrganizationSettingsIDCol, e.Aggregate().ID), + }, + ), nil +} diff --git a/internal/query/projection/organization_settings_test.go b/internal/query/projection/organization_settings_test.go new file mode 100644 index 0000000000..e69e42c71e --- /dev/null +++ b/internal/query/projection/organization_settings_test.go @@ -0,0 +1,154 @@ +package projection + +import ( + "testing" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" + settings "github.com/zitadel/zitadel/internal/repository/organization_settings" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestOrganizationSettingsProjection_reduces(t *testing.T) { + type args struct { + event func(t *testing.T) eventstore.Event + } + tests := []struct { + name string + args args + reduce func(event eventstore.Event) (*handler.Statement, error) + want wantReduce + }{ + { + name: "reduce organization settings set", + args: args{ + event: getEvent( + testEvent( + settings.OrganizationSettingsSetEventType, + settings.AggregateType, + []byte(`{"organizationScopedUsernames": true}`), + ), eventstore.GenericEventMapper[settings.OrganizationSettingsSetEvent], + ), + }, + reduce: (&organizationSettingsProjection{}).reduceOrganizationSettingsSet, + want: wantReduce{ + aggregateType: eventstore.AggregateType("organization_settings"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO projections.organization_settings (instance_id, resource_owner, id, creation_date, change_date, sequence, organization_scoped_usernames) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (instance_id, resource_owner, id) DO UPDATE SET (creation_date, change_date, sequence, organization_scoped_usernames) = (projections.organization_settings.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.organization_scoped_usernames)", + expectedArgs: []interface{}{ + "instance-id", + "ro-id", + "agg-id", + anyArg{}, + anyArg{}, + uint64(15), + true, + }, + }, + }, + }, + }, + }, + { + name: "reduce organization settings removed", + args: args{ + event: getEvent( + testEvent( + settings.OrganizationSettingsRemovedEventType, + settings.AggregateType, + []byte(`{}`), + ), eventstore.GenericEventMapper[settings.OrganizationSettingsRemovedEvent], + ), + }, + reduce: (&organizationSettingsProjection{}).reduceOrganizationSettingsRemoved, + want: wantReduce{ + aggregateType: eventstore.AggregateType("organization_settings"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.organization_settings WHERE (instance_id = $1) AND (resource_owner = $2) AND (id = $3)", + expectedArgs: []interface{}{ + "instance-id", + "ro-id", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceOrgRemoved", + args: args{ + event: getEvent( + testEvent( + org.OrgRemovedEventType, + org.AggregateType, + nil, + ), org.OrgRemovedEventMapper), + }, + reduce: (&organizationSettingsProjection{}).reduceOrgRemoved, + want: wantReduce{ + aggregateType: eventstore.AggregateType("org"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.organization_settings WHERE (instance_id = $1) AND (resource_owner = $2) AND (id = $3)", + expectedArgs: []interface{}{ + "instance-id", + "ro-id", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceInstanceRemoved", + args: args{ + event: getEvent( + testEvent( + instance.InstanceRemovedEventType, + instance.AggregateType, + nil, + ), instance.InstanceRemovedEventMapper), + }, + reduce: reduceInstanceRemovedHelper(OrganizationSettingsInstanceIDCol), + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.organization_settings WHERE (instance_id = $1)", + expectedArgs: []interface{}{ + "agg-id", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := baseEvent(t) + got, err := tt.reduce(event) + if ok := zerrors.IsErrorInvalidArgument(err); !ok { + t.Errorf("no wrong event mapping: %v, got: %v", err, got) + } + + event = tt.args.event(t) + got, err = tt.reduce(event) + assertReduce(t, got, err, OrganizationSettingsTable, tt.want) + }) + } +} diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index 5ad62380ea..e6f9c64b01 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -87,6 +87,7 @@ var ( WebKeyProjection *handler.Handler DebugEventsProjection *handler.Handler HostedLoginTranslationProjection *handler.Handler + OrganizationSettingsProjection *handler.Handler ProjectGrantFields *handler.FieldHandler OrgDomainVerifiedFields *handler.FieldHandler @@ -181,6 +182,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore, WebKeyProjection = newWebKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["web_keys"])) DebugEventsProjection = newDebugEventsProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["debug_events"])) HostedLoginTranslationProjection = newHostedLoginTranslationProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["hosted_login_translation"])) + OrganizationSettingsProjection = newOrganizationSettingsProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["organization_settings"])) ProjectGrantFields = newFillProjectGrantFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsProjectGrant])) OrgDomainVerifiedFields = newFillOrgDomainVerifiedFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsOrgDomainVerified])) @@ -360,5 +362,6 @@ func newProjectionsList() { WebKeyProjection, DebugEventsProjection, HostedLoginTranslationProjection, + OrganizationSettingsProjection, } } diff --git a/internal/repository/org/org.go b/internal/repository/org/org.go index cf8a3ce114..562cbd85f8 100644 --- a/internal/repository/org/org.go +++ b/internal/repository/org/org.go @@ -275,13 +275,13 @@ func OrgReactivatedEventMapper(event eventstore.Event) (eventstore.Event, error) } type OrgRemovedEvent struct { - eventstore.BaseEvent `json:"-"` - name string - usernames []string - loginMustBeDomain bool - domains []string - externalIDPs []*domain.UserIDPLink - samlEntityIDs []string + eventstore.BaseEvent `json:"-"` + name string + usernames []string + organizationScopedUsernames bool + domains []string + externalIDPs []*domain.UserIDPLink + samlEntityIDs []string } func (e *OrgRemovedEvent) Payload() interface{} { @@ -293,7 +293,7 @@ func (e *OrgRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { NewRemoveOrgNameUniqueConstraint(e.name), } for _, name := range e.usernames { - constraints = append(constraints, user.NewRemoveUsernameUniqueConstraint(name, e.Aggregate().ID, e.loginMustBeDomain)) + constraints = append(constraints, user.NewRemoveUsernameUniqueConstraint(name, e.Aggregate().ID, e.organizationScopedUsernames)) } for _, domain := range e.domains { constraints = append(constraints, NewRemoveOrgDomainUniqueConstraint(domain)) @@ -314,19 +314,19 @@ func (e *OrgRemovedEvent) Fields() []*eventstore.FieldOperation { } } -func NewOrgRemovedEvent(ctx context.Context, aggregate *eventstore.Aggregate, name string, usernames []string, loginMustBeDomain bool, domains []string, externalIDPs []*domain.UserIDPLink, samlEntityIDs []string) *OrgRemovedEvent { +func NewOrgRemovedEvent(ctx context.Context, aggregate *eventstore.Aggregate, name string, usernames []string, organizationScopedUsernames bool, domains []string, externalIDPs []*domain.UserIDPLink, samlEntityIDs []string) *OrgRemovedEvent { return &OrgRemovedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( ctx, aggregate, OrgRemovedEventType, ), - name: name, - usernames: usernames, - domains: domains, - externalIDPs: externalIDPs, - samlEntityIDs: samlEntityIDs, - loginMustBeDomain: loginMustBeDomain, + name: name, + usernames: usernames, + domains: domains, + externalIDPs: externalIDPs, + samlEntityIDs: samlEntityIDs, + organizationScopedUsernames: organizationScopedUsernames, } } diff --git a/internal/repository/organization_settings/aggregate.go b/internal/repository/organization_settings/aggregate.go new file mode 100644 index 0000000000..11ea000785 --- /dev/null +++ b/internal/repository/organization_settings/aggregate.go @@ -0,0 +1,23 @@ +package organization_settings + +import "github.com/zitadel/zitadel/internal/eventstore" + +const ( + AggregateType = "organization_settings" + AggregateVersion = "v1" +) + +type Aggregate struct { + eventstore.Aggregate +} + +func NewAggregate(id, resourceOwner string) *Aggregate { + return &Aggregate{ + Aggregate: eventstore.Aggregate{ + Type: AggregateType, + Version: AggregateVersion, + ID: id, + ResourceOwner: resourceOwner, + }, + } +} diff --git a/internal/repository/organization_settings/eventstore.go b/internal/repository/organization_settings/eventstore.go new file mode 100644 index 0000000000..e3cca585d6 --- /dev/null +++ b/internal/repository/organization_settings/eventstore.go @@ -0,0 +1,8 @@ +package organization_settings + +import "github.com/zitadel/zitadel/internal/eventstore" + +func init() { + eventstore.RegisterFilterEventMapper(AggregateType, OrganizationSettingsSetEventType, eventstore.GenericEventMapper[OrganizationSettingsSetEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, OrganizationSettingsRemovedEventType, eventstore.GenericEventMapper[OrganizationSettingsRemovedEvent]) +} diff --git a/internal/repository/organization_settings/organization.go b/internal/repository/organization_settings/organization.go new file mode 100644 index 0000000000..c157316ba9 --- /dev/null +++ b/internal/repository/organization_settings/organization.go @@ -0,0 +1,96 @@ +package organization_settings + +import ( + "context" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/user" +) + +const ( + organizationSettingsPrefix = "settings.organization." + OrganizationSettingsSetEventType = organizationSettingsPrefix + "set" + OrganizationSettingsRemovedEventType = organizationSettingsPrefix + "removed" +) + +type OrganizationSettingsSetEvent struct { + *eventstore.BaseEvent `json:"-"` + + OrganizationScopedUsernames bool `json:"organizationScopedUsernames,omitempty"` + oldOrganizationScopedUsernames bool + usernameChanges []string +} + +func (e *OrganizationSettingsSetEvent) SetBaseEvent(b *eventstore.BaseEvent) { + e.BaseEvent = b +} + +func (e *OrganizationSettingsSetEvent) Payload() any { + return e +} + +func (e *OrganizationSettingsSetEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + if len(e.usernameChanges) == 0 || e.oldOrganizationScopedUsernames == e.OrganizationScopedUsernames { + return []*eventstore.UniqueConstraint{} + } + changes := make([]*eventstore.UniqueConstraint, len(e.usernameChanges)*2) + for i, username := range e.usernameChanges { + changes[i*2] = user.NewRemoveUsernameUniqueConstraint(username, e.Aggregate().ResourceOwner, e.oldOrganizationScopedUsernames) + changes[i*2+1] = user.NewAddUsernameUniqueConstraint(username, e.Aggregate().ResourceOwner, e.OrganizationScopedUsernames) + } + return changes +} + +func NewOrganizationSettingsAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + usernameChanges []string, + organizationScopedUsernames bool, + oldOrganizationScopedUsernames bool, +) *OrganizationSettingsSetEvent { + return &OrganizationSettingsSetEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, aggregate, OrganizationSettingsSetEventType, + ), + OrganizationScopedUsernames: organizationScopedUsernames, + oldOrganizationScopedUsernames: oldOrganizationScopedUsernames, + usernameChanges: usernameChanges, + } +} + +type OrganizationSettingsRemovedEvent struct { + *eventstore.BaseEvent `json:"-"` + + organizationScopedUsernames bool + oldOrganizationScopedUsernames bool + usernameChanges []string +} + +func (e *OrganizationSettingsRemovedEvent) SetBaseEvent(b *eventstore.BaseEvent) { + e.BaseEvent = b +} + +func (e *OrganizationSettingsRemovedEvent) Payload() any { + return e +} + +func (e *OrganizationSettingsRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return user.NewUsernameUniqueConstraints(e.usernameChanges, e.Aggregate().ResourceOwner, e.organizationScopedUsernames, e.oldOrganizationScopedUsernames) +} + +func NewOrganizationSettingsRemovedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + usernameChanges []string, + organizationScopedUsernames bool, + oldOrganizationScopedUsernames bool, +) *OrganizationSettingsRemovedEvent { + return &OrganizationSettingsRemovedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, aggregate, OrganizationSettingsRemovedEventType, + ), + organizationScopedUsernames: organizationScopedUsernames, + oldOrganizationScopedUsernames: oldOrganizationScopedUsernames, + usernameChanges: usernameChanges, + } +} diff --git a/internal/repository/policy/policy_domain.go b/internal/repository/policy/policy_domain.go index bd1d9c1b7e..ad459625b2 100644 --- a/internal/repository/policy/policy_domain.go +++ b/internal/repository/policy/policy_domain.go @@ -2,6 +2,7 @@ package policy import ( "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -122,6 +123,10 @@ func DomainPolicyChangedEventMapper(event eventstore.Event) (eventstore.Event, e type DomainPolicyRemovedEvent struct { eventstore.BaseEvent `json:"-"` + + usernameChanges []string + userLoginMustBeDomain bool + oldUserLoginMustBeDomain bool } func (e *DomainPolicyRemovedEvent) Payload() interface{} { @@ -129,7 +134,7 @@ func (e *DomainPolicyRemovedEvent) Payload() interface{} { } func (e *DomainPolicyRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { - return nil + return user.NewUsernameUniqueConstraints(e.usernameChanges, e.Aggregate().ResourceOwner, e.userLoginMustBeDomain, e.oldUserLoginMustBeDomain) } func NewDomainPolicyRemovedEvent(base *eventstore.BaseEvent) *DomainPolicyRemovedEvent { @@ -143,3 +148,9 @@ func DomainPolicyRemovedEventMapper(event eventstore.Event) (eventstore.Event, e BaseEvent: *eventstore.BaseEventFromRepo(event), }, nil } + +func (e *DomainPolicyRemovedEvent) AddUniqueConstraintChanges(usernameChanges []string, userLoginMustBeDomain, oldUserLoginMustBeDomain bool) { + e.usernameChanges = usernameChanges + e.userLoginMustBeDomain = userLoginMustBeDomain + e.oldUserLoginMustBeDomain = oldUserLoginMustBeDomain +} diff --git a/internal/repository/user/human.go b/internal/repository/user/human.go index d503ecc899..823a86aaed 100644 --- a/internal/repository/user/human.go +++ b/internal/repository/user/human.go @@ -31,8 +31,8 @@ const ( type HumanAddedEvent struct { eventstore.BaseEvent `json:"-"` - UserName string `json:"userName"` - userLoginMustBeDomain bool + UserName string `json:"userName"` + orgScopedUsername bool FirstName string `json:"firstName,omitempty"` LastName string `json:"lastName,omitempty"` @@ -63,7 +63,7 @@ func (e *HumanAddedEvent) Payload() interface{} { } func (e *HumanAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { - return []*eventstore.UniqueConstraint{NewAddUsernameUniqueConstraint(e.UserName, e.Aggregate().ResourceOwner, e.userLoginMustBeDomain)} + return []*eventstore.UniqueConstraint{NewAddUsernameUniqueConstraint(e.UserName, e.Aggregate().ResourceOwner, e.orgScopedUsername)} } func (e *HumanAddedEvent) AddAddressData( @@ -106,7 +106,7 @@ func NewHumanAddedEvent( preferredLanguage language.Tag, gender domain.Gender, emailAddress domain.EmailAddress, - userLoginMustBeDomain bool, + orgScopedUsername bool, ) *HumanAddedEvent { return &HumanAddedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -114,15 +114,15 @@ func NewHumanAddedEvent( aggregate, HumanAddedType, ), - UserName: userName, - FirstName: firstName, - LastName: lastName, - NickName: nickName, - DisplayName: displayName, - PreferredLanguage: preferredLanguage, - Gender: gender, - EmailAddress: emailAddress, - userLoginMustBeDomain: userLoginMustBeDomain, + UserName: userName, + FirstName: firstName, + LastName: lastName, + NickName: nickName, + DisplayName: displayName, + PreferredLanguage: preferredLanguage, + Gender: gender, + EmailAddress: emailAddress, + orgScopedUsername: orgScopedUsername, } } @@ -139,22 +139,24 @@ func HumanAddedEventMapper(event eventstore.Event) (eventstore.Event, error) { } type HumanRegisteredEvent struct { - eventstore.BaseEvent `json:"-"` - UserName string `json:"userName"` - userLoginMustBeDomain bool - FirstName string `json:"firstName,omitempty"` - LastName string `json:"lastName,omitempty"` - NickName string `json:"nickName,omitempty"` - DisplayName string `json:"displayName,omitempty"` - PreferredLanguage language.Tag `json:"preferredLanguage,omitempty"` - Gender domain.Gender `json:"gender,omitempty"` - EmailAddress domain.EmailAddress `json:"email,omitempty"` - PhoneNumber domain.PhoneNumber `json:"phone,omitempty"` - Country string `json:"country,omitempty"` - Locality string `json:"locality,omitempty"` - PostalCode string `json:"postalCode,omitempty"` - Region string `json:"region,omitempty"` - StreetAddress string `json:"streetAddress,omitempty"` + eventstore.BaseEvent `json:"-"` + + UserName string `json:"userName"` + orgScopedUsername bool + + FirstName string `json:"firstName,omitempty"` + LastName string `json:"lastName,omitempty"` + NickName string `json:"nickName,omitempty"` + DisplayName string `json:"displayName,omitempty"` + PreferredLanguage language.Tag `json:"preferredLanguage,omitempty"` + Gender domain.Gender `json:"gender,omitempty"` + EmailAddress domain.EmailAddress `json:"email,omitempty"` + PhoneNumber domain.PhoneNumber `json:"phone,omitempty"` + Country string `json:"country,omitempty"` + Locality string `json:"locality,omitempty"` + PostalCode string `json:"postalCode,omitempty"` + Region string `json:"region,omitempty"` + StreetAddress string `json:"streetAddress,omitempty"` // New events only use EncodedHash. However, the secret field // is preserved to handle events older than the switch to Passwap. @@ -170,7 +172,7 @@ func (e *HumanRegisteredEvent) Payload() interface{} { } func (e *HumanRegisteredEvent) UniqueConstraints() []*eventstore.UniqueConstraint { - return []*eventstore.UniqueConstraint{NewAddUsernameUniqueConstraint(e.UserName, e.Aggregate().ResourceOwner, e.userLoginMustBeDomain)} + return []*eventstore.UniqueConstraint{NewAddUsernameUniqueConstraint(e.UserName, e.Aggregate().ResourceOwner, e.orgScopedUsername)} } func (e *HumanRegisteredEvent) AddAddressData( @@ -213,7 +215,7 @@ func NewHumanRegisteredEvent( preferredLanguage language.Tag, gender domain.Gender, emailAddress domain.EmailAddress, - userLoginMustBeDomain bool, + orgScopedUsername bool, userAgentID string, ) *HumanRegisteredEvent { return &HumanRegisteredEvent{ @@ -222,16 +224,16 @@ func NewHumanRegisteredEvent( aggregate, HumanRegisteredType, ), - UserName: userName, - FirstName: firstName, - LastName: lastName, - NickName: nickName, - DisplayName: displayName, - PreferredLanguage: preferredLanguage, - Gender: gender, - EmailAddress: emailAddress, - userLoginMustBeDomain: userLoginMustBeDomain, - UserAgentID: userAgentID, + UserName: userName, + FirstName: firstName, + LastName: lastName, + NickName: nickName, + DisplayName: displayName, + PreferredLanguage: preferredLanguage, + Gender: gender, + EmailAddress: emailAddress, + orgScopedUsername: orgScopedUsername, + UserAgentID: userAgentID, } } diff --git a/internal/repository/user/machine.go b/internal/repository/user/machine.go index a466f92fe3..b54149639a 100644 --- a/internal/repository/user/machine.go +++ b/internal/repository/user/machine.go @@ -17,8 +17,8 @@ const ( type MachineAddedEvent struct { eventstore.BaseEvent `json:"-"` - UserName string `json:"userName"` - userLoginMustBeDomain bool + UserName string `json:"userName"` + orgScopedUsername bool Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` @@ -30,7 +30,7 @@ func (e *MachineAddedEvent) Payload() interface{} { } func (e *MachineAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { - return []*eventstore.UniqueConstraint{NewAddUsernameUniqueConstraint(e.UserName, e.Aggregate().ResourceOwner, e.userLoginMustBeDomain)} + return []*eventstore.UniqueConstraint{NewAddUsernameUniqueConstraint(e.UserName, e.Aggregate().ResourceOwner, e.orgScopedUsername)} } func NewMachineAddedEvent( @@ -39,7 +39,7 @@ func NewMachineAddedEvent( userName, name, description string, - userLoginMustBeDomain bool, + orgScopedUsername bool, accessTokenType domain.OIDCTokenType, ) *MachineAddedEvent { return &MachineAddedEvent{ @@ -48,11 +48,11 @@ func NewMachineAddedEvent( aggregate, MachineAddedEventType, ), - UserName: userName, - Name: name, - Description: description, - userLoginMustBeDomain: userLoginMustBeDomain, - AccessTokenType: accessTokenType, + UserName: userName, + Name: name, + Description: description, + orgScopedUsername: orgScopedUsername, + AccessTokenType: accessTokenType, } } diff --git a/internal/repository/user/user.go b/internal/repository/user/user.go index e8faddb645..470edc6f16 100644 --- a/internal/repository/user/user.go +++ b/internal/repository/user/user.go @@ -27,9 +27,9 @@ const ( UserUserNameChangedType = userEventTypePrefix + "username.changed" ) -func NewAddUsernameUniqueConstraint(userName, resourceOwner string, userLoginMustBeDomain bool) *eventstore.UniqueConstraint { +func NewAddUsernameUniqueConstraint(userName, resourceOwner string, orgScopedUsername bool) *eventstore.UniqueConstraint { uniqueUserName := userName - if userLoginMustBeDomain { + if orgScopedUsername { uniqueUserName = userName + resourceOwner } return eventstore.NewAddEventUniqueConstraint( @@ -38,9 +38,9 @@ func NewAddUsernameUniqueConstraint(userName, resourceOwner string, userLoginMus "Errors.User.AlreadyExists") } -func NewRemoveUsernameUniqueConstraint(userName, resourceOwner string, userLoginMustBeDomain bool) *eventstore.UniqueConstraint { +func NewRemoveUsernameUniqueConstraint(userName, resourceOwner string, orgScopedUsername bool) *eventstore.UniqueConstraint { uniqueUserName := userName - if userLoginMustBeDomain { + if orgScopedUsername { uniqueUserName = userName + resourceOwner } return eventstore.NewRemoveUniqueConstraint( @@ -48,6 +48,18 @@ func NewRemoveUsernameUniqueConstraint(userName, resourceOwner string, userLogin uniqueUserName) } +func NewUsernameUniqueConstraints(usernameChanges []string, resourceOwner string, orgScopedUsername, oldOrgScopedUsername bool) []*eventstore.UniqueConstraint { + if len(usernameChanges) == 0 || oldOrgScopedUsername == orgScopedUsername { + return []*eventstore.UniqueConstraint{} + } + changes := make([]*eventstore.UniqueConstraint, len(usernameChanges)*2) + for i, username := range usernameChanges { + changes[i*2] = NewRemoveUsernameUniqueConstraint(username, resourceOwner, oldOrgScopedUsername) + changes[i*2+1] = NewAddUsernameUniqueConstraint(username, resourceOwner, orgScopedUsername) + } + return changes +} + type UserLockedEvent struct { eventstore.BaseEvent `json:"-"` } @@ -165,7 +177,7 @@ type UserRemovedEvent struct { userName string externalIDPs []*domain.UserIDPLink - loginMustBeDomain bool + orgScopedUsername bool } func (e *UserRemovedEvent) Payload() interface{} { @@ -175,7 +187,7 @@ func (e *UserRemovedEvent) Payload() interface{} { func (e *UserRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { events := make([]*eventstore.UniqueConstraint, 0) if e.userName != "" { - events = append(events, NewRemoveUsernameUniqueConstraint(e.userName, e.Aggregate().ResourceOwner, e.loginMustBeDomain)) + events = append(events, NewRemoveUsernameUniqueConstraint(e.userName, e.Aggregate().ResourceOwner, e.orgScopedUsername)) } for _, idp := range e.externalIDPs { events = append(events, NewRemoveUserIDPLinkUniqueConstraint(idp.IDPConfigID, idp.ExternalUserID)) @@ -188,7 +200,7 @@ func NewUserRemovedEvent( aggregate *eventstore.Aggregate, userName string, externalIDPs []*domain.UserIDPLink, - userLoginMustBeDomain bool, + orgScopedUsername bool, ) *UserRemovedEvent { return &UserRemovedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -198,7 +210,7 @@ func NewUserRemovedEvent( ), userName: userName, externalIDPs: externalIDPs, - loginMustBeDomain: userLoginMustBeDomain, + orgScopedUsername: orgScopedUsername, } } @@ -393,10 +405,10 @@ func UserTokenRemovedEventMapper(event eventstore.Event) (eventstore.Event, erro type DomainClaimedEvent struct { eventstore.BaseEvent `json:"-"` - UserName string `json:"userName"` - TriggeredAtOrigin string `json:"triggerOrigin,omitempty"` - oldUserName string - userLoginMustBeDomain bool + UserName string `json:"userName"` + TriggeredAtOrigin string `json:"triggerOrigin,omitempty"` + oldUserName string + orgScopedUsername bool } func (e *DomainClaimedEvent) Payload() interface{} { @@ -405,8 +417,8 @@ func (e *DomainClaimedEvent) Payload() interface{} { func (e *DomainClaimedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { return []*eventstore.UniqueConstraint{ - NewRemoveUsernameUniqueConstraint(e.oldUserName, e.Aggregate().ResourceOwner, e.userLoginMustBeDomain), - NewAddUsernameUniqueConstraint(e.UserName, e.Aggregate().ResourceOwner, e.userLoginMustBeDomain), + NewRemoveUsernameUniqueConstraint(e.oldUserName, e.Aggregate().ResourceOwner, e.orgScopedUsername), + NewAddUsernameUniqueConstraint(e.UserName, e.Aggregate().ResourceOwner, e.orgScopedUsername), } } @@ -419,7 +431,7 @@ func NewDomainClaimedEvent( aggregate *eventstore.Aggregate, userName, oldUserName string, - userLoginMustBeDomain bool, + orgScopedUsername bool, ) *DomainClaimedEvent { return &DomainClaimedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -427,10 +439,10 @@ func NewDomainClaimedEvent( aggregate, UserDomainClaimedType, ), - UserName: userName, - oldUserName: oldUserName, - userLoginMustBeDomain: userLoginMustBeDomain, - TriggeredAtOrigin: http.DomainContext(ctx).Origin(), + UserName: userName, + oldUserName: oldUserName, + orgScopedUsername: orgScopedUsername, + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } @@ -480,10 +492,11 @@ func DomainClaimedSentEventMapper(event eventstore.Event) (eventstore.Event, err type UsernameChangedEvent struct { eventstore.BaseEvent `json:"-"` - UserName string `json:"userName"` - oldUserName string - userLoginMustBeDomain bool - oldUserLoginMustBeDomain bool + UserName string `json:"userName"` + oldUserName string + userLoginMustBeDomain bool + oldUserLoginMustBeDomain bool + organizationScopedUsernames bool } func (e *UsernameChangedEvent) Payload() interface{} { @@ -491,9 +504,20 @@ func (e *UsernameChangedEvent) Payload() interface{} { } func (e *UsernameChangedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + newSetting := e.userLoginMustBeDomain || e.organizationScopedUsernames + oldSetting := e.oldUserLoginMustBeDomain || e.organizationScopedUsernames + + // changes only necessary if username changed or setting for usernames changed + // if user login must be domain is set, there is a possibility that the username changes + // organization scoped usernames are included here so that the unique constraint only gets changed if necessary + if e.oldUserName == e.UserName && + newSetting == oldSetting { + return []*eventstore.UniqueConstraint{} + } + return []*eventstore.UniqueConstraint{ - NewRemoveUsernameUniqueConstraint(e.oldUserName, e.Aggregate().ResourceOwner, e.oldUserLoginMustBeDomain), - NewAddUsernameUniqueConstraint(e.UserName, e.Aggregate().ResourceOwner, e.userLoginMustBeDomain), + NewRemoveUsernameUniqueConstraint(e.oldUserName, e.Aggregate().ResourceOwner, oldSetting), + NewAddUsernameUniqueConstraint(e.UserName, e.Aggregate().ResourceOwner, newSetting), } } @@ -503,6 +527,7 @@ func NewUsernameChangedEvent( oldUserName, newUserName string, userLoginMustBeDomain bool, + organizationScopedUsernames bool, opts ...UsernameChangedEventOption, ) *UsernameChangedEvent { event := &UsernameChangedEvent{ @@ -511,10 +536,11 @@ func NewUsernameChangedEvent( aggregate, UserUserNameChangedType, ), - UserName: newUserName, - oldUserName: oldUserName, - userLoginMustBeDomain: userLoginMustBeDomain, - oldUserLoginMustBeDomain: userLoginMustBeDomain, + UserName: newUserName, + oldUserName: oldUserName, + userLoginMustBeDomain: userLoginMustBeDomain, + oldUserLoginMustBeDomain: userLoginMustBeDomain, + organizationScopedUsernames: organizationScopedUsernames, } for _, opt := range opts { opt(event) @@ -526,9 +552,9 @@ type UsernameChangedEventOption func(*UsernameChangedEvent) // UsernameChangedEventWithPolicyChange signals that the change occurs because of / during a domain policy change // (will ensure the unique constraint change is handled correctly) -func UsernameChangedEventWithPolicyChange() UsernameChangedEventOption { +func UsernameChangedEventWithPolicyChange(oldUserLoginMustBeDomain bool) UsernameChangedEventOption { return func(e *UsernameChangedEvent) { - e.oldUserLoginMustBeDomain = !e.userLoginMustBeDomain + e.oldUserLoginMustBeDomain = oldUserLoginMustBeDomain } } diff --git a/proto/zitadel/filter/v2/filter.proto b/proto/zitadel/filter/v2/filter.proto index 3817324d31..ec85519b44 100644 --- a/proto/zitadel/filter/v2/filter.proto +++ b/proto/zitadel/filter/v2/filter.proto @@ -94,3 +94,23 @@ message TimestampFilter { (validate.rules).enum.defined_only = true ]; } + +message InIDsFilter { + // Defines the ids to query for. + repeated string ids = 1 [ + (validate.rules).repeated = { + unique: true + items: { + string: { + min_len: 1 + max_len: 200 + } + } + }, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "[\"69629023906488334\",\"69622366012355662\"]"; + } + ]; +} diff --git a/proto/zitadel/project/v2beta/project_service.proto b/proto/zitadel/project/v2beta/project_service.proto index 66f221b911..3fd1513d2d 100644 --- a/proto/zitadel/project/v2beta/project_service.proto +++ b/proto/zitadel/project/v2beta/project_service.proto @@ -638,7 +638,7 @@ service ProjectService { // Returns a list of project grants. A project grant is when the organization grants its project to another organization. // // Required permission: - // - `project.grant.write` + // - `project.grant.read` rpc ListProjectGrants(ListProjectGrantsRequest) returns (ListProjectGrantsResponse) { option (google.api.http) = { post: "/v2beta/projects/grants/search" diff --git a/proto/zitadel/settings/v2/settings_service.proto b/proto/zitadel/settings/v2/settings_service.proto index 0a1f13e7e7..ea3e2c3653 100644 --- a/proto/zitadel/settings/v2/settings_service.proto +++ b/proto/zitadel/settings/v2/settings_service.proto @@ -17,6 +17,8 @@ import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; import "google/protobuf/struct.proto"; import "zitadel/settings/v2/settings.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/filter/v2/filter.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2;settings"; @@ -415,7 +417,7 @@ service SettingsService { } }; }; - + option (google.api.http) = { put: "/v2/settings/hosted_login_translation"; body: "*" @@ -579,8 +581,8 @@ message GetHostedLoginTranslationResponse { google.protobuf.Struct translations = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "{\"common\":{\"back\":\"Indietro\"},\"accounts\":{\"title\":\"Account\",\"description\":\"Seleziona l'account che desideri utilizzare.\",\"addAnother\":\"Aggiungi un altro account\",\"noResults\":\"Nessun account trovato\"}}"; - description: "Translations contains the translations in the request language."; + example: "{\"common\":{\"back\":\"Indietro\"},\"accounts\":{\"title\":\"Account\",\"description\":\"Seleziona l'account che desideri utilizzare.\",\"addAnother\":\"Aggiungi un altro account\",\"noResults\":\"Nessun account trovato\"}}"; + description: "Translations contains the translations in the request language."; } ]; } @@ -590,7 +592,7 @@ message SetHostedLoginTranslationRequest { bool instance = 1 [(validate.rules).bool = {const: true}]; string organization_id = 2; } - + string locale = 3 [ (validate.rules).string = {min_len: 2}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -601,8 +603,8 @@ message SetHostedLoginTranslationRequest { google.protobuf.Struct translations = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "{\"common\":{\"back\":\"Indietro\"},\"accounts\":{\"title\":\"Account\",\"description\":\"Seleziona l'account che desideri utilizzare.\",\"addAnother\":\"Aggiungi un altro account\",\"noResults\":\"Nessun account trovato\"}}"; - description: "Translations should contain the translations in the specified locale."; + example: "{\"common\":{\"back\":\"Indietro\"},\"accounts\":{\"title\":\"Account\",\"description\":\"Seleziona l'account che desideri utilizzare.\",\"addAnother\":\"Aggiungi un altro account\",\"noResults\":\"Nessun account trovato\"}}"; + description: "Translations should contain the translations in the specified locale."; } ]; } @@ -617,4 +619,4 @@ message SetHostedLoginTranslationResponse { example: "\"42a1ba123e6ea6f0c93e286ed97c7018\""; } ]; -} \ No newline at end of file +} diff --git a/proto/zitadel/settings/v2beta/organization_settings.proto b/proto/zitadel/settings/v2beta/organization_settings.proto new file mode 100644 index 0000000000..743dbf9881 --- /dev/null +++ b/proto/zitadel/settings/v2beta/organization_settings.proto @@ -0,0 +1,55 @@ +syntax = "proto3"; + +package zitadel.settings.v2beta; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta;settings"; + +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/filter/v2beta/filter.proto"; + +message OrganizationSettings { + // The unique identifier of the organization the settings belong to. + string organization_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the organization settings creation. + google.protobuf.Timestamp creation_date = 2[ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change to the organization settings. + google.protobuf.Timestamp change_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // Defines if the usernames have to be unique in the organization context. + bool organization_scoped_usernames = 4; +} + +enum OrganizationSettingsFieldName { + ORGANIZATION_SETTINGS_FIELD_NAME_UNSPECIFIED = 0; + ORGANIZATION_SETTINGS_FIELD_NAME_ORGANIZATION_ID = 1; + ORGANIZATION_SETTINGS_FIELD_NAME_CREATION_DATE = 2; + ORGANIZATION_SETTINGS_FIELD_NAME_CHANGE_DATE = 3; +} + +message OrganizationSettingsSearchFilter { + oneof filter { + option (validate.required) = true; + + zitadel.filter.v2beta.InIDsFilter in_organization_ids_filter = 1; + OrganizationScopedUsernamesFilter organization_scoped_usernames_filter = 2; + } +} + +// Query for organization settings with specific scopes usernames. +message OrganizationScopedUsernamesFilter { + bool organization_scoped_usernames = 1; +} \ No newline at end of file diff --git a/proto/zitadel/settings/v2beta/settings_service.proto b/proto/zitadel/settings/v2beta/settings_service.proto index 9404e002a7..a1679b7cb7 100644 --- a/proto/zitadel/settings/v2beta/settings_service.proto +++ b/proto/zitadel/settings/v2beta/settings_service.proto @@ -15,6 +15,9 @@ import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/filter/v2beta/filter.proto"; +import "zitadel/settings/v2beta/organization_settings.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta;settings"; @@ -360,6 +363,98 @@ service SettingsService { description: "Set the security settings of the ZITADEL instance." }; } + + // Set Organization Settings + // + // Sets the settings specific to an organization. + // Organization scopes usernames defines that the usernames have to be unique in the organization scope, can only be changed if the usernames of the users are unique in the scope. + // + // Required permissions: + // - `iam.policy.write` + rpc SetOrganizationSettings(SetOrganizationSettingsRequest) returns (SetOrganizationSettingsResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The translations was successfully set."; + } + }; + }; + + option (google.api.http) = { + post: "/v2/settings/organization"; + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Delete Organization Settings + // + // Delete the settings specific to an organization. + // + // Required permissions: + // - `iam.policy.delete` + rpc DeleteOrganizationSettings(DeleteOrganizationSettingsRequest) returns (DeleteOrganizationSettingsResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The translations was successfully set."; + } + }; + }; + + option (google.api.http) = { + delete: "/v2/settings/organization"; + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // List Organization Settings + // + // Returns a list of organization settings. + // + // Required permission: + // - `iam.policy.read` + // - `org.policy.read` + rpc ListOrganizationSettings(ListOrganizationSettingsRequest) returns (ListOrganizationSettingsResponse) { + option (google.api.http) = { + post: "/v2/settings/organization/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all project grants matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } } message GetLoginSettingsRequest { @@ -474,4 +569,55 @@ message SetSecuritySettingsRequest{ message SetSecuritySettingsResponse{ zitadel.object.v2beta.Details details = 1; -} \ No newline at end of file +} + + +message SetOrganizationSettingsRequest { + // Organization ID in which this settings are set. + string organization_id = 1; + // Force the usernames in the organization to be unique, only possible to set if the existing users already have unique usernames in the organization context. + optional bool organization_scoped_usernames = 2; +} + +message SetOrganizationSettingsResponse { + // The timestamp of the set of the organization settings. + google.protobuf.Timestamp set_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeleteOrganizationSettingsRequest { + // Organization ID in which this settings are set. + string organization_id = 1; +} + +message DeleteOrganizationSettingsResponse { + // The timestamp of the deletion of the organization settings. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListOrganizationSettingsRequest{ + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 1; + // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. + optional OrganizationSettingsFieldName sorting_column = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"ORGANIZATION_SETTINGS_FIELD_NAME_CREATION_DATE\"" + } + ]; + repeated OrganizationSettingsSearchFilter filters = 4; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"sortingColumn\":\"ORGANIZATION_SETTINGS_FIELD_NAME_CREATION_DATE\",\"filters\":[{\"inOrganizationIdsFilter\":{\"ids\":[\"28746028909593987\"]}}]}"; + }; +} + +message ListOrganizationSettingsResponse { + zitadel.filter.v2beta.PaginationResponse pagination = 1; + repeated OrganizationSettings organization_settings = 2; +}