diff --git a/API_DESIGN.md b/API_DESIGN.md index 11b7766a49..cdf43a71df 100644 --- a/API_DESIGN.md +++ b/API_DESIGN.md @@ -135,6 +135,8 @@ message CreateUserRequest { ``` Only allow providing a context where it is required. The context MUST not be provided if not required. +If the context is required but deferrable, the context can be defaulted. +For example, creating an Authorization without an organization id will default the organization id to the projects resource owner. For example, when retrieving or updating a user, the `organization_id` is not required, since the user can be determined by the user's id. However, it is possible to provide the `organization_id` as a filter to retrieve a list of users of a specific organization. diff --git a/cmd/start/start.go b/cmd/start/start.go index 50bb9fbdb3..9c1e2a4d28 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -38,10 +38,12 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/admin" app "github.com/zitadel/zitadel/internal/api/grpc/app/v2beta" "github.com/zitadel/zitadel/internal/api/grpc/auth" + authorization_v2beta "github.com/zitadel/zitadel/internal/api/grpc/authorization/v2beta" feature_v2 "github.com/zitadel/zitadel/internal/api/grpc/feature/v2" feature_v2beta "github.com/zitadel/zitadel/internal/api/grpc/feature/v2beta" idp_v2 "github.com/zitadel/zitadel/internal/api/grpc/idp/v2" instance "github.com/zitadel/zitadel/internal/api/grpc/instance/v2beta" + internal_permission_v2beta "github.com/zitadel/zitadel/internal/api/grpc/internal_permission/v2beta" "github.com/zitadel/zitadel/internal/api/grpc/management" oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2" oidc_v2beta "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2beta" @@ -474,7 +476,7 @@ func startAPIs( if err := apis.RegisterService(ctx, user_v2beta.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, user_v2.CreateServer(config.SystemDefaults, commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil { + if err := apis.RegisterService(ctx, user_v2.CreateServer(commands, queries, config.SystemDefaults, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil { return nil, err } if err := apis.RegisterService(ctx, session_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil { @@ -510,6 +512,9 @@ func startAPIs( if err := apis.RegisterService(ctx, project_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { return nil, err } + if err := apis.RegisterService(ctx, internal_permission_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { + return nil, err + } if err := apis.RegisterService(ctx, userschema_v3_alpha.CreateServer(config.SystemDefaults, commands, queries)); err != nil { return nil, err } @@ -525,6 +530,9 @@ func startAPIs( if err := apis.RegisterService(ctx, debug_events.CreateServer(commands, queries)); err != nil { return nil, err } + if err := apis.RegisterService(ctx, authorization_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { + return nil, err + } if err := apis.RegisterService(ctx, app.CreateServer(commands, queries, permissionCheck)); err != nil { return nil, err } diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index ffca8b21de..a22b1b80fc 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -406,6 +406,24 @@ module.exports = { categoryLinkSource: "auto", }, }, + authorization_v2: { + specPath: + ".artifacts/openapi3/zitadel/authorization/v2beta/authorization_service.openapi.yaml", + outputDir: "docs/apis/resources/authorization_service_v2", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "auto", + }, + }, + internal_permission_v2: { + specPath: + ".artifacts/openapi3/zitadel/internal_permission/v2beta/internal_permission_service.openapi.yaml", + outputDir: "docs/apis/resources/internal_permission_service_v2", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "auto", + }, + }, }, }, ], diff --git a/docs/sidebars.js b/docs/sidebars.js index a0de30271d..94978b8150 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -16,6 +16,8 @@ const sidebar_api_actions_v2 = require("./docs/apis/resources/action_service_v2/ const sidebar_api_project_service_v2 = require("./docs/apis/resources/project_service_v2/sidebar.ts").default const sidebar_api_webkey_service_v2 = require("./docs/apis/resources/webkey_service_v2/sidebar.ts").default const sidebar_api_instance_service_v2 = require("./docs/apis/resources/instance_service_v2/sidebar.ts").default +const sidebar_api_authorization_service_v2 = require("./docs/apis/resources/authorization_service_v2/sidebar.ts").default +const sidebar_api_permission_service_v2 = require("./docs/apis/resources/internal_permission_service_v2/sidebar.ts").default const sidebar_api_app_v2 = require("./docs/apis/resources/application_service_v2/sidebar.ts").default module.exports = { @@ -914,6 +916,37 @@ module.exports = { }, items: sidebar_api_app_v2, }, + { + type: "category", + label: "Authorizations (Beta)", + link: { + type: "generated-index", + title: "Authorization Service API (Beta)", + slug: "/apis/resources/authorization_service_v2", + description: + "AuthorizationService provides methods to manage authorizations for users within your projects and applications.\n" + + "\n" + + "For managing permissions and roles for ZITADEL internal resources, like organizations, projects,\n" + + "users, etc., please use the InternalPermissionService."+ + "\n"+ + "This API is in beta state. It can AND will continue breaking until a stable version is released.\n" + }, + items: sidebar_api_authorization_service_v2, + }, + { + type: "category", + label: "Permissions (Beta)", + link: { + type: "generated-index", + title: "Permission Service API (Beta)", + slug: "/apis/resources/permission_service_v2", + description: + "This API is intended to manage internal permissions in ZITADEL.\n" + + "\n"+ + "This API is in beta state. It can AND will continue breaking until a stable version is released.\n" + }, + items: sidebar_api_permission_service_v2, + }, ], }, { diff --git a/internal/api/grpc/action/v2beta/integration_test/query_test.go b/internal/api/grpc/action/v2beta/integration_test/query_test.go index 5c59bee5d1..2d74486f3e 100644 --- a/internal/api/grpc/action/v2beta/integration_test/query_test.go +++ b/internal/api/grpc/action/v2beta/integration_test/query_test.go @@ -782,12 +782,3 @@ func TestServer_ListExecutions(t *testing.T) { }) } } - -func containExecution(t *assert.CollectT, executionList []*action.Execution, execution *action.Execution) bool { - for _, exec := range executionList { - if assert.EqualExportedValues(t, execution, exec) { - return true - } - } - return false -} diff --git a/internal/api/grpc/admin/export.go b/internal/api/grpc/admin/export.go index 8024cd9d6e..7ce2dbd7b5 100644 --- a/internal/api/grpc/admin/export.go +++ b/internal/api/grpc/admin/export.go @@ -659,7 +659,7 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w if err != nil { return nil, nil, nil, nil, err } - metadataList, err := s.query.SearchUserMetadata(ctx, false, user.ID, &query.UserMetadataSearchQueries{Queries: []query.SearchQuery{metadataOrgSearch}}, false) + metadataList, err := s.query.SearchUserMetadata(ctx, false, user.ID, &query.UserMetadataSearchQueries{Queries: []query.SearchQuery{metadataOrgSearch}}, nil) metaspan.EndWithError(err) if err != nil { return nil, nil, nil, nil, err @@ -984,7 +984,7 @@ func (s *Server) getNecessaryUserGrantsForOrg(ctx context.Context, org string, p return nil, err } - queriedUserGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantSearchOrg}}, true) + queriedUserGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantSearchOrg}}, true, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/admin/iam_member.go b/internal/api/grpc/admin/iam_member.go index 8f9b11ce2a..301acec6d6 100644 --- a/internal/api/grpc/admin/iam_member.go +++ b/internal/api/grpc/admin/iam_member.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/member" "github.com/zitadel/zitadel/internal/api/grpc/object" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" @@ -33,35 +34,35 @@ func (s *Server) ListIAMMembers(ctx context.Context, req *admin_pb.ListIAMMember } func (s *Server) AddIAMMember(ctx context.Context, req *admin_pb.AddIAMMemberRequest) (*admin_pb.AddIAMMemberResponse, error) { - member, err := s.command.AddInstanceMember(ctx, req.UserId, req.Roles...) + member, err := s.command.AddInstanceMember(ctx, AddIAMMemberToCommand(req, authz.GetInstance(ctx).InstanceID())) if err != nil { return nil, err } return &admin_pb.AddIAMMemberResponse{ Details: object.AddToDetailsPb( member.Sequence, - member.ChangeDate, + member.EventDate, member.ResourceOwner, ), }, nil } func (s *Server) UpdateIAMMember(ctx context.Context, req *admin_pb.UpdateIAMMemberRequest) (*admin_pb.UpdateIAMMemberResponse, error) { - member, err := s.command.ChangeInstanceMember(ctx, UpdateIAMMemberToDomain(req)) + member, err := s.command.ChangeInstanceMember(ctx, UpdateIAMMemberToCommand(req, authz.GetInstance(ctx).InstanceID())) if err != nil { return nil, err } return &admin_pb.UpdateIAMMemberResponse{ Details: object.ChangeToDetailsPb( member.Sequence, - member.ChangeDate, + member.EventDate, member.ResourceOwner, ), }, nil } func (s *Server) RemoveIAMMember(ctx context.Context, req *admin_pb.RemoveIAMMemberRequest) (*admin_pb.RemoveIAMMemberResponse, error) { - objectDetails, err := s.command.RemoveInstanceMember(ctx, req.UserId) + objectDetails, err := s.command.RemoveInstanceMember(ctx, authz.GetInstance(ctx).InstanceID(), req.UserId) if err != nil { return nil, err } diff --git a/internal/api/grpc/admin/iam_member_converter.go b/internal/api/grpc/admin/iam_member_converter.go index 2fe75214fd..695711d9cc 100644 --- a/internal/api/grpc/admin/iam_member_converter.go +++ b/internal/api/grpc/admin/iam_member_converter.go @@ -3,23 +3,25 @@ package admin import ( member_grpc "github.com/zitadel/zitadel/internal/api/grpc/member" "github.com/zitadel/zitadel/internal/api/grpc/object" - "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/query" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" member_pb "github.com/zitadel/zitadel/pkg/grpc/member" ) -func AddIAMMemberToDomain(req *admin_pb.AddIAMMemberRequest) *domain.Member { - return &domain.Member{ - UserID: req.UserId, - Roles: req.Roles, +func AddIAMMemberToCommand(req *admin_pb.AddIAMMemberRequest, instanceID string) *command.AddInstanceMember { + return &command.AddInstanceMember{ + InstanceID: instanceID, + UserID: req.UserId, + Roles: req.Roles, } } -func UpdateIAMMemberToDomain(req *admin_pb.UpdateIAMMemberRequest) *domain.Member { - return &domain.Member{ - UserID: req.UserId, - Roles: req.Roles, +func UpdateIAMMemberToCommand(req *admin_pb.UpdateIAMMemberRequest, instanceID string) *command.ChangeInstanceMember { + return &command.ChangeInstanceMember{ + InstanceID: instanceID, + UserID: req.UserId, + Roles: req.Roles, } } diff --git a/internal/api/grpc/admin/iam_member_converter_test.go b/internal/api/grpc/admin/iam_member_converter_test.go index 74dd329ee1..70e282d621 100644 --- a/internal/api/grpc/admin/iam_member_converter_test.go +++ b/internal/api/grpc/admin/iam_member_converter_test.go @@ -27,7 +27,7 @@ func TestAddIAMMemberToDomain(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := AddIAMMemberToDomain(tt.args.req) + got := AddIAMMemberToCommand(tt.args.req, "INSTANCE") test.AssertFieldsMapped(t, got, "ObjectRoot") }) } @@ -53,7 +53,7 @@ func TestUpdateIAMMemberToDomain(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := UpdateIAMMemberToDomain(tt.args.req) + got := UpdateIAMMemberToCommand(tt.args.req, "INSTANCE") test.AssertFieldsMapped(t, got, "ObjectRoot") }) } diff --git a/internal/api/grpc/admin/import.go b/internal/api/grpc/admin/import.go index 84b0215f03..ac085c135b 100644 --- a/internal/api/grpc/admin/import.go +++ b/internal/api/grpc/admin/import.go @@ -1046,7 +1046,7 @@ func importOrg2(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataEr if org.UserGrants != nil { for _, grant := range org.GetUserGrants() { logging.Debugf("import usergrant: %s", grant.GetProjectId()+"_"+grant.GetUserId()) - _, err := s.command.AddUserGrant(ctx, management.AddUserGrantRequestToDomain(grant), org.GetOrgId()) + _, err := s.command.AddUserGrant(ctx, management.AddUserGrantRequestToDomain(grant, org.GetOrgId()), nil) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "user_grant", Id: org.GetOrgId() + "_" + grant.GetProjectId() + "_" + grant.GetUserId(), Message: err.Error()}) if isCtxTimeout(ctx) { @@ -1088,7 +1088,7 @@ func importOrgMembers(ctx context.Context, s *Server, errors *[]*admin_pb.Import } for _, member := range org.GetOrgMembers() { logging.Debugf("import orgmember: %s", member.GetUserId()) - _, err := s.command.AddOrgMember(ctx, org.GetOrgId(), member.GetUserId(), member.GetRoles()...) + _, err := s.command.AddOrgMember(ctx, management.AddOrgMemberRequestToCommand(member, org.GetOrgId())) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "org_member", Id: org.GetOrgId() + "_" + member.GetUserId(), Message: err.Error()}) if isCtxTimeout(ctx) { @@ -1112,7 +1112,7 @@ func importProjectGrantMembers(ctx context.Context, s *Server, errors *[]*admin_ } for _, member := range org.GetProjectGrantMembers() { logging.Debugf("import projectgrantmember: %s", member.GetProjectId()+"_"+member.GetGrantId()+"_"+member.GetUserId()) - _, err := s.command.AddProjectGrantMember(ctx, management.AddProjectGrantMemberRequestToDomain(member)) + _, err := s.command.AddProjectGrantMember(ctx, management.AddProjectGrantMemberRequestToCommand(member, org.GetOrgId())) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "project_grant_member", Id: org.GetOrgId() + "_" + member.GetProjectId() + "_" + member.GetGrantId() + "_" + member.GetUserId(), Message: err.Error()}) if isCtxTimeout(ctx) { @@ -1136,7 +1136,7 @@ func importProjectMembers(ctx context.Context, s *Server, errors *[]*admin_pb.Im } for _, member := range org.GetProjectMembers() { logging.Debugf("import orgmember: %s", member.GetProjectId()+"_"+member.GetUserId()) - _, err := s.command.AddProjectMember(ctx, management.AddProjectMemberRequestToDomain(member), org.GetOrgId()) + _, err := s.command.AddProjectMember(ctx, management.AddProjectMemberRequestToCommand(member, org.GetOrgId())) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "project_member", Id: org.GetOrgId() + "_" + member.GetProjectId() + "_" + member.GetUserId(), Message: err.Error()}) if isCtxTimeout(ctx) { diff --git a/internal/api/grpc/admin/integration_test/import_test.go b/internal/api/grpc/admin/integration_test/import_test.go index 4a546bbcf1..3f0d364aec 100644 --- a/internal/api/grpc/admin/integration_test/import_test.go +++ b/internal/api/grpc/admin/integration_test/import_test.go @@ -457,7 +457,7 @@ func TestServer_ImportData(t *testing.T) { { Type: "project_grant_member", Id: orgIDs[5] + "_" + projectIDs[4] + "_" + grantIDs[5] + "_" + userIDs[2], - Message: "ID=V3-DKcYh Message=Errors.Project.Member.AlreadyExists Parent=(ERROR: duplicate key value violates unique constraint \"unique_constraints_pkey\" (SQLSTATE 23505))", + Message: "ID=PROJECT-37fug Message=Errors.AlreadyExists", }, }, Success: &admin.ImportDataSuccess{ diff --git a/internal/api/grpc/auth/user.go b/internal/api/grpc/auth/user.go index 13f955fd81..4015b0d370 100644 --- a/internal/api/grpc/auth/user.go +++ b/internal/api/grpc/auth/user.go @@ -32,7 +32,7 @@ func (s *Server) RemoveMyUser(ctx context.Context, _ *auth_pb.RemoveMyUserReques return nil, err } queries := &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantUserID}} - grants, err := s.query.UserGrants(ctx, queries, true) + grants, err := s.query.UserGrants(ctx, queries, true, nil) if err != nil { return nil, err } @@ -97,7 +97,7 @@ func (s *Server) ListMyMetadata(ctx context.Context, req *auth_pb.ListMyMetadata if err != nil { return nil, err } - res, err := s.query.SearchUserMetadata(ctx, true, authz.GetCtxData(ctx).UserID, queries, false) + res, err := s.query.SearchUserMetadata(ctx, true, authz.GetCtxData(ctx).UserID, queries, nil) if err != nil { return nil, err } @@ -151,7 +151,7 @@ func (s *Server) ListMyUserGrants(ctx context.Context, req *auth_pb.ListMyUserGr if err != nil { return nil, err } - res, err := s.query.UserGrants(ctx, queries, false) + res, err := s.query.UserGrants(ctx, queries, false, nil) if err != nil { return nil, err } @@ -180,7 +180,7 @@ func (s *Server) ListMyProjectOrgs(ctx context.Context, req *auth_pb.ListMyProje return nil, err } - grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantProjectID, userGrantUserID}}, false) + grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantProjectID, userGrantUserID}}, false, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/authorization/v2beta/authorization.go b/internal/api/grpc/authorization/v2beta/authorization.go new file mode 100644 index 0000000000..f5410c959a --- /dev/null +++ b/internal/api/grpc/authorization/v2beta/authorization.go @@ -0,0 +1,76 @@ +package authorization + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta" +) + +func (s *Server) CreateAuthorization(ctx context.Context, req *connect.Request[authorization.CreateAuthorizationRequest]) (*connect.Response[authorization.CreateAuthorizationResponse], error) { + grant := &domain.UserGrant{ + UserID: req.Msg.UserId, + ProjectID: req.Msg.ProjectId, + RoleKeys: req.Msg.RoleKeys, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: req.Msg.GetOrganizationId(), + }, + } + grant, err := s.command.AddUserGrant(ctx, grant, s.command.NewPermissionCheckUserGrantWrite(ctx)) + if err != nil { + return nil, err + } + return connect.NewResponse(&authorization.CreateAuthorizationResponse{ + Id: grant.AggregateID, + CreationDate: timestamppb.New(grant.ChangeDate), + }), nil +} + +func (s *Server) UpdateAuthorization(ctx context.Context, request *connect.Request[authorization.UpdateAuthorizationRequest]) (*connect.Response[authorization.UpdateAuthorizationResponse], error) { + userGrant, err := s.command.ChangeUserGrant(ctx, &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: request.Msg.Id, + }, + RoleKeys: request.Msg.RoleKeys, + }, true, true, s.command.NewPermissionCheckUserGrantWrite(ctx)) + if err != nil { + return nil, err + } + return connect.NewResponse(&authorization.UpdateAuthorizationResponse{ + ChangeDate: timestamppb.New(userGrant.ChangeDate), + }), nil +} + +func (s *Server) DeleteAuthorization(ctx context.Context, request *connect.Request[authorization.DeleteAuthorizationRequest]) (*connect.Response[authorization.DeleteAuthorizationResponse], error) { + details, err := s.command.RemoveUserGrant(ctx, request.Msg.Id, "", true, s.command.NewPermissionCheckUserGrantDelete(ctx)) + if err != nil { + return nil, err + } + return connect.NewResponse(&authorization.DeleteAuthorizationResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }), nil +} + +func (s *Server) ActivateAuthorization(ctx context.Context, request *connect.Request[authorization.ActivateAuthorizationRequest]) (*connect.Response[authorization.ActivateAuthorizationResponse], error) { + details, err := s.command.ReactivateUserGrant(ctx, request.Msg.Id, "", s.command.NewPermissionCheckUserGrantWrite(ctx)) + if err != nil { + return nil, err + } + return connect.NewResponse(&authorization.ActivateAuthorizationResponse{ + ChangeDate: timestamppb.New(details.EventDate), + }), nil +} + +func (s *Server) DeactivateAuthorization(ctx context.Context, request *connect.Request[authorization.DeactivateAuthorizationRequest]) (*connect.Response[authorization.DeactivateAuthorizationResponse], error) { + details, err := s.command.DeactivateUserGrant(ctx, request.Msg.Id, "", s.command.NewPermissionCheckUserGrantWrite(ctx)) + if err != nil { + return nil, err + } + return connect.NewResponse(&authorization.DeactivateAuthorizationResponse{ + ChangeDate: timestamppb.New(details.EventDate), + }), nil +} diff --git a/internal/api/grpc/authorization/v2beta/integration_test/authorization_test.go b/internal/api/grpc/authorization/v2beta/integration_test/authorization_test.go new file mode 100644 index 0000000000..d24844f2a2 --- /dev/null +++ b/internal/api/grpc/authorization/v2beta/integration_test/authorization_test.go @@ -0,0 +1,1023 @@ +//go:build integration + +package authorization_test + +import ( + "context" + "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" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/integration" + authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta" + project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func TestServer_CreateAuthorization(t *testing.T) { + type args struct { + prepare func(*testing.T, *authorization.CreateAuthorizationRequest) context.Context + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "add authorization, project owned, PROJECT_OWNER, ok", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.OrganizationId = &selfOrgId + request.ProjectId = Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + request.RoleKeys = []string{gofakeit.AppName()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], gofakeit.AppName(), "") + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, request.ProjectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + }, + { + name: "add authorization, project owned, PROJECT_OWNER, no org id, ok", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.ProjectId = Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + request.RoleKeys = []string{gofakeit.AppName()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], gofakeit.AppName(), "") + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, request.ProjectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + }, + { + name: "add authorization, project owned, ORG_OWNER, ok", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.OrganizationId = &selfOrgId + request.ProjectId = Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + request.RoleKeys = []string{gofakeit.AppName()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], gofakeit.AppName(), "") + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateOrgMembership(t, IAMCTX, selfOrgId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + }, + { + name: "add authorization, project owned, no permission, error", + args: args{ + + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.OrganizationId = &selfOrgId + request.ProjectId = Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + request.RoleKeys = []string{gofakeit.AppName()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], gofakeit.AppName(), "") + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + { + name: "add authorization, role does not exist, error", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.OrganizationId = &selfOrgId + request.ProjectId = Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + request.RoleKeys = []string{gofakeit.AppName()} + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, request.ProjectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + { + name: "add authorization, project does not exist, error", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.OrganizationId = &selfOrgId + request.ProjectId = gofakeit.AppName() + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + { + name: "add authorization, org does not exist, error", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.OrganizationId = gu.Ptr(gofakeit.AppName()) + request.ProjectId = Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, request.ProjectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + + }, + }, + wantErr: true, + }, + { + name: "add authorization, project owner, project granted, no permission", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.OrganizationId = &selfOrgId + foreignOrg := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()) + request.ProjectId = Instance.CreateProject(IAMCTX, t, foreignOrg.OrganizationId, gofakeit.AppName(), false, false).Id + request.RoleKeys = []string{gofakeit.AppName()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], gofakeit.AppName(), "") + Instance.CreateProjectGrant(IAMCTX, t, request.ProjectId, selfOrgId, request.RoleKeys...) + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, request.ProjectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + { + name: "add authorization, role key not granted, error", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.OrganizationId = &selfOrgId + foreignOrg := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()) + request.ProjectId = Instance.CreateProject(IAMCTX, t, foreignOrg.OrganizationId, gofakeit.AppName(), false, false).Id + request.RoleKeys = []string{gofakeit.AppName()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], gofakeit.AppName(), "") + Instance.CreateProjectGrant(IAMCTX, t, request.ProjectId, selfOrgId) + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, request.ProjectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + { + name: "add authorization, grant does not exist, error", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.OrganizationId = &selfOrgId + foreignOrg := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()) + projectID := Instance.CreateProject(IAMCTX, t, foreignOrg.OrganizationId, gofakeit.AppName(), false, false).Id + request.ProjectId = projectID + request.RoleKeys = []string{gofakeit.AppName()} + Instance.AddProjectRole(IAMCTX, t, projectID, request.RoleKeys[0], gofakeit.AppName(), "") + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectID, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + + }, + }, + wantErr: true, + }, + { + name: "add authorization, PROJECT_OWNER on wrong org, error", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.OrganizationId = &selfOrgId + foreignOrg := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()) + request.ProjectId = Instance.CreateProject(IAMCTX, t, foreignOrg.OrganizationId, gofakeit.AppName(), false, false).Id + request.RoleKeys = []string{gofakeit.AppName()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], gofakeit.AppName(), "") + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, authz.SetCtxData(IAMCTX, authz.CtxData{OrgID: foreignOrg.OrganizationId}), request.ProjectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + req := &authorization.CreateAuthorizationRequest{} + ctx := tt.args.prepare(t, req) + got, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(ctx, req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.Id, "id is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_UpdateAuthorization(t *testing.T) { + type args struct { + prepare func(*testing.T, *authorization.UpdateAuthorizationRequest) context.Context + } + tests := []struct { + name string + args args + wantErr bool + wantChangedDateDuringPrepare bool + }{ + { + name: "update authorization, owned project, ok", + args: args{ + func(t *testing.T, request *authorization.UpdateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + request.RoleKeys = []string{projectRole1} + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + }, + { + name: "update authorization, owned project, role not found, error", + args: args{ + func(t *testing.T, request *authorization.UpdateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + request.RoleKeys = []string{projectRole1, projectRole2, gofakeit.AppName()} + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + { + name: "update authorization, owned project, unchanged, ok, changed date is creation date", + args: args{ + func(t *testing.T, request *authorization.UpdateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + request.RoleKeys = []string{projectRole1, projectRole2} + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantChangedDateDuringPrepare: true, + }, + { + name: "update authorization, granted project, ok", + args: args{ + func(t *testing.T, request *authorization.UpdateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + projectRole3 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole3, projectRole3, "") + Instance.CreateProjectGrant(IAMCTX, t, projectId, selfOrgId, projectRole1, projectRole2) + + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + OrganizationId: &selfOrgId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + request.RoleKeys = []string{projectRole1} + token := createUserWithProjectGrantMembership(IAMCTX, t, Instance, projectId, selfOrgId) + return integration.WithAuthorizationToken(EmptyCTX, token) + + }, + }, + }, + { + name: "update authorization, granted project, role not granted, error", + args: args{ + func(t *testing.T, request *authorization.UpdateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + projectRole3 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole3, projectRole3, "") + Instance.CreateProjectGrant(IAMCTX, t, projectId, selfOrgId, projectRole1, projectRole2) + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + OrganizationId: &selfOrgId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + request.RoleKeys = []string{projectId, projectRole3, projectRole3} + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectGrantMembership(t, IAMCTX, projectId, selfOrgId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + { + name: "update authorization, granted project, grant removed, error", + args: args{ + func(t *testing.T, request *authorization.UpdateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + projectRole3 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole3, projectRole3, "") + Instance.CreateProjectGrant(IAMCTX, t, projectId, selfOrgId, projectRole1, projectRole2) + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + OrganizationId: &selfOrgId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + request.RoleKeys = []string{projectRole1} + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectGrantMembership(t, IAMCTX, projectId, selfOrgId, callingUser.Id) + _, err = Instance.Client.Projectv2Beta.DeleteProjectGrant(IAMCTX, &project.DeleteProjectGrantRequest{ + ProjectId: projectId, + GrantedOrganizationId: selfOrgId, + }) + require.NoError(t, err) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + req := &authorization.UpdateAuthorizationRequest{} + ctx := tt.args.prepare(t, req) + afterPrepare := time.Now() + got, err := Instance.Client.AuthorizationV2Beta.UpdateAuthorization(ctx, req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.ChangeDate, "change date is empty") + changeDate := got.ChangeDate.AsTime() + assert.Greater(t, changeDate, now, "change date is before the test started") + if tt.wantChangedDateDuringPrepare { + assert.Less(t, changeDate, afterPrepare, "change date is after prepare finished") + } else { + assert.Less(t, changeDate, time.Now(), "change date is in the future") + } + }) + } +} + +func TestServer_DeleteAuthorization(t *testing.T) { + type args struct { + prepare func(*testing.T, *authorization.DeleteAuthorizationRequest) context.Context + } + tests := []struct { + name string + args args + wantErr bool + wantDeletionDateDuringPrepare bool + }{ + { + name: "delete authorization, project owned by calling users org, ok", + args: args{ + func(t *testing.T, request *authorization.DeleteAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + }, { + name: "delete authorization, owned project, user membership on project owning org, ok", + args: args{ + func(t *testing.T, request *authorization.DeleteAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, authz.SetCtxData(IAMCTX, authz.CtxData{OrgID: foreignOrgId}), projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + }, + { + name: "delete authorization, granted project, user membership on project owning org, error", + args: args{ + func(t *testing.T, request *authorization.DeleteAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + projectRole3 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole3, projectRole3, "") + Instance.CreateProjectGrant(IAMCTX, t, projectId, selfOrgId, projectRole1, projectRole2) + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + OrganizationId: &selfOrgId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, authz.SetCtxData(IAMCTX, authz.CtxData{OrgID: foreignOrgId}), projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + { + name: "delete authorization, granted project, user membership on project granted org, ok", + args: args{ + func(t *testing.T, request *authorization.DeleteAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + projectRole3 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole3, projectRole3, "") + Instance.CreateProjectGrant(IAMCTX, t, projectId, selfOrgId, projectRole1, projectRole2) + + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + OrganizationId: &selfOrgId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + return integration.WithAuthorizationToken(EmptyCTX, createUserWithProjectGrantMembership(IAMCTX, t, Instance, projectId, selfOrgId)) + + }, + }, + }, + { + name: "delete authorization, already deleted, ok, deletion date is creation date", + args: args{ + func(t *testing.T, request *authorization.DeleteAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + _, err = Instance.Client.AuthorizationV2Beta.DeleteAuthorization(IAMCTX, &authorization.DeleteAuthorizationRequest{ + Id: preparedAuthorization.Id, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantDeletionDateDuringPrepare: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + req := &authorization.DeleteAuthorizationRequest{} + ctx := tt.args.prepare(t, req) + afterPrepare := time.Now() + got, err := Instance.Client.AuthorizationV2Beta.DeleteAuthorization(ctx, req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.DeletionDate, "deletion date is empty") + changeDate := got.DeletionDate.AsTime() + assert.Greater(t, changeDate, now, "deletion date is before the test started") + if tt.wantDeletionDateDuringPrepare { + assert.Less(t, changeDate, afterPrepare, "deletion date is after prepare finished") + } else { + assert.Less(t, changeDate, time.Now(), "deletion date is in the future") + } + }) + } +} + +func TestServer_DeactivateAuthorization(t *testing.T) { + type args struct { + prepare func(*testing.T, *authorization.DeactivateAuthorizationRequest) context.Context + } + tests := []struct { + name string + args args + wantErr bool + wantDeletionDateDuringPrepare bool + }{ + { + name: "deactivate authorization, project owned by calling users org, ok", + args: args{ + func(t *testing.T, request *authorization.DeactivateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + }, { + name: "deactivate authorization, owned project, user membership on project owning org, ok", + args: args{ + func(t *testing.T, request *authorization.DeactivateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, authz.SetCtxData(IAMCTX, authz.CtxData{OrgID: foreignOrgId}), projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + }, + { + name: "deactivate authorization, granted project, user membership on project owning org, error", + args: args{ + func(t *testing.T, request *authorization.DeactivateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + projectRole3 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole3, projectRole3, "") + Instance.CreateProjectGrant(IAMCTX, t, projectId, selfOrgId, projectRole1, projectRole2) + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + OrganizationId: &selfOrgId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, authz.SetCtxData(IAMCTX, authz.CtxData{OrgID: foreignOrgId}), projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + { + name: "deactivate authorization, granted project, user membership on project granted org, ok", + args: args{ + func(t *testing.T, request *authorization.DeactivateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + projectRole3 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole3, projectRole3, "") + Instance.CreateProjectGrant(IAMCTX, t, projectId, selfOrgId, projectRole1, projectRole2) + + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + OrganizationId: &selfOrgId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + return integration.WithAuthorizationToken(EmptyCTX, createUserWithProjectGrantMembership(IAMCTX, t, Instance, projectId, selfOrgId)) + }, + }, + }, + { + name: "deactivate authorization, already inactive, ok, change date is creation date", + args: args{ + func(t *testing.T, request *authorization.DeactivateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + _, err = Instance.Client.AuthorizationV2Beta.DeactivateAuthorization(IAMCTX, &authorization.DeactivateAuthorizationRequest{ + Id: preparedAuthorization.Id, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantDeletionDateDuringPrepare: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + req := &authorization.DeactivateAuthorizationRequest{} + ctx := tt.args.prepare(t, req) + afterPrepare := time.Now() + got, err := Instance.Client.AuthorizationV2Beta.DeactivateAuthorization(ctx, req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.ChangeDate, "change date is empty") + changeDate := got.ChangeDate.AsTime() + assert.Greater(t, changeDate, now, "change date is before the test started") + if tt.wantDeletionDateDuringPrepare { + assert.Less(t, changeDate, afterPrepare, "change date is after prepare finished") + } else { + assert.Less(t, changeDate, time.Now(), "change date is in the future") + } + }) + } +} + +func TestServer_ActivateAuthorization(t *testing.T) { + type args struct { + prepare func(*testing.T, *authorization.ActivateAuthorizationRequest) context.Context + } + tests := []struct { + name string + args args + wantErr bool + wantDeletionDateDuringPrepare bool + }{ + { + name: "activate authorization, project owned by calling users org, ok", + args: args{ + func(t *testing.T, request *authorization.ActivateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectId, callingUser.Id) + _, err = Instance.Client.AuthorizationV2Beta.DeactivateAuthorization(IAMCTX, &authorization.DeactivateAuthorizationRequest{ + Id: preparedAuthorization.Id, + }) + require.NoError(t, err) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + }, { + name: "activate authorization, owned project, user membership on project owning org, ok", + args: args{ + func(t *testing.T, request *authorization.ActivateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, authz.SetCtxData(IAMCTX, authz.CtxData{OrgID: foreignOrgId}), projectId, callingUser.Id) + _, err = Instance.Client.AuthorizationV2Beta.DeactivateAuthorization(IAMCTX, &authorization.DeactivateAuthorizationRequest{ + Id: preparedAuthorization.Id, + }) + require.NoError(t, err) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + }, + { + name: "activate authorization, granted project, user membership on project owning org, error", + args: args{ + func(t *testing.T, request *authorization.ActivateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + projectRole3 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole3, projectRole3, "") + Instance.CreateProjectGrant(IAMCTX, t, projectId, selfOrgId, projectRole1, projectRole2) + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + OrganizationId: &selfOrgId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, authz.SetCtxData(IAMCTX, authz.CtxData{OrgID: foreignOrgId}), projectId, callingUser.Id) + _, err = Instance.Client.AuthorizationV2Beta.DeactivateAuthorization(IAMCTX, &authorization.DeactivateAuthorizationRequest{ + Id: preparedAuthorization.Id, + }) + require.NoError(t, err) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + { + name: "activate authorization, granted project, user membership on project granted org, ok", + args: args{ + func(t *testing.T, request *authorization.ActivateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + projectRole3 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole3, projectRole3, "") + Instance.CreateProjectGrant(IAMCTX, t, projectId, selfOrgId, projectRole1, projectRole2) + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + OrganizationId: &selfOrgId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + _, err = Instance.Client.AuthorizationV2Beta.DeactivateAuthorization(IAMCTX, &authorization.DeactivateAuthorizationRequest{ + Id: preparedAuthorization.Id, + }) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, createUserWithProjectGrantMembership(IAMCTX, t, Instance, projectId, selfOrgId)) + }, + }, + }, + { + name: "activate authorization, already active, ok, change date is creation date", + args: args{ + func(t *testing.T, request *authorization.ActivateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantDeletionDateDuringPrepare: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + req := &authorization.ActivateAuthorizationRequest{} + ctx := tt.args.prepare(t, req) + afterPrepare := time.Now() + got, err := Instance.Client.AuthorizationV2Beta.ActivateAuthorization(ctx, req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.ChangeDate, "change date is empty") + changeDate := got.ChangeDate.AsTime() + assert.Greater(t, changeDate, now, "change date is before the test started") + if tt.wantDeletionDateDuringPrepare { + assert.Less(t, changeDate, afterPrepare, "change date is after prepare finished") + } else { + assert.Less(t, changeDate, time.Now(), "change date is in the future") + } + }) + } +} + +func createUserWithProjectGrantMembership(ctx context.Context, t *testing.T, instance *integration.Instance, projectID, grantID string) string { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + callingUser := instance.CreateUserTypeMachine(ctx, selfOrgId) + instance.CreateProjectGrantMembership(t, ctx, projectID, grantID, callingUser.Id) + token, err := instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 10*time.Minute) + require.EventuallyWithT(t, func(tt *assert.CollectT) { + got, err := instance.Client.AuthorizationV2Beta.ListAuthorizations(ctx, &authorization.ListAuthorizationsRequest{ + Filters: nil, + }) + assert.NoError(tt, err) + if !assert.NotEmpty(tt, got.Pagination.TotalResult) { + return + } + }, retryDuration, tick) + return token.GetToken() +} diff --git a/internal/api/grpc/authorization/v2beta/integration_test/query_test.go b/internal/api/grpc/authorization/v2beta/integration_test/query_test.go new file mode 100644 index 0000000000..fd0a807c1d --- /dev/null +++ b/internal/api/grpc/authorization/v2beta/integration_test/query_test.go @@ -0,0 +1,971 @@ +//go:build integration + +package authorization_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" + project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func TestServer_ListAuthorizations(t *testing.T) { + iamOwnerCtx := Instance.WithAuthorizationToken(EmptyCTX, integration.UserTypeIAMOwner) + projectOwnerResp := Instance.CreateMachineUser(iamOwnerCtx) + projectOwnerPatResp := Instance.CreatePersonalAccessToken(iamOwnerCtx, projectOwnerResp.GetUserId()) + projectResp := createProject(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), false, false) + Instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), projectOwnerResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(EmptyCTX, projectOwnerPatResp.Token) + + projectGrantOwnerResp := Instance.CreateMachineUser(iamOwnerCtx) + projectGrantOwnerPatResp := Instance.CreatePersonalAccessToken(iamOwnerCtx, projectGrantOwnerResp.GetUserId()) + grantedProjectResp := createGrantedProject(iamOwnerCtx, Instance, t, projectResp) + Instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), grantedProjectResp.GetGrantedOrganizationId(), projectGrantOwnerResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(EmptyCTX, projectGrantOwnerPatResp.Token) + + type args struct { + ctx context.Context + dep func(*authorization.ListAuthorizationsRequest, *authorization.ListAuthorizationsResponse) + req *authorization.ListAuthorizationsRequest + } + tests := []struct { + name string + args args + want *authorization.ListAuthorizationsResponse + wantErr bool + }{ + { + name: "list by user id, unauthenticated", + args: args{ + ctx: EmptyCTX, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: Instance.WithAuthorizationToken(EmptyCTX, integration.UserTypeNoPermission), + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{}, + }, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + {Filter: &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: "notexisting", + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id, project", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + response.Authorizations[0] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + resp := createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false) + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_AuthorizationIds{ + AuthorizationIds: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, + }, + } + response.Authorizations[0] = resp + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single project id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + resp := createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false) + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_ProjectId{ + ProjectId: &filter.IDFilter{ + Id: resp.GetProjectId(), + }, + } + response.Authorizations[0] = resp + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single project name", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + resp := createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false) + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_ProjectName{ + ProjectName: &authorization.ProjectNameQuery{ + Name: resp.GetProjectName(), + Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS, + }, + } + response.Authorizations[0] = resp + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single id, project grant", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + response.Authorizations[0] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), true) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single grant id, project grant", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + resp := createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), true) + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_ProjectGrantId{ + ProjectGrantId: &filter.IDFilter{ + Id: resp.GetProjectGrantId(), + }, + } + response.Authorizations[0] = resp + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single id, project and project grant, multiple", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + response.Authorizations[5] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false) + response.Authorizations[4] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false) + response.Authorizations[3] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false) + response.Authorizations[2] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), true) + response.Authorizations[1] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), true) + response.Authorizations[0] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), true) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 6, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, {}, {}, {}, {}, {}, + }, + }, + }, + { + name: "list single id, project and project grant, org owner", + args: args{ + ctx: Instance.WithAuthorizationToken(EmptyCTX, integration.UserTypeOrgOwner), + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + + response.Authorizations[1] = createAuthorizationForProject(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId()) + response.Authorizations[0] = createAuthorizationWithProjectGrant(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId()) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, {}, + }, + }, + }, + { + name: "list single id, project and project grant, project owner", + args: args{ + ctx: projectOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + + response.Authorizations[0] = createAuthorizationForProject(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId()) + createAuthorizationWithProjectGrant(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId()) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single id, project and project grant, project grant owner", + args: args{ + ctx: projectGrantOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + + createAuthorizationForProject(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId()) + response.Authorizations[0] = createAuthorizationForProjectGrant(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId(), grantedProjectResp.GetGrantedOrganizationId()) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + } + 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.AuthorizationV2Beta.ListAuthorizations(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.Authorizations, len(tt.want.Authorizations)) { + for i := range tt.want.Authorizations { + assert.EqualExportedValues(ttt, tt.want.Authorizations[i], got.Authorizations[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) +} + +func createAuthorization(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, userID string, grant bool) *authorization.Authorization { + projectName := gofakeit.AppName() + projectResp := instance.CreateProject(ctx, t, orgID, projectName, false, false) + + if grant { + return createAuthorizationWithProjectGrant(ctx, instance, t, orgID, userID, projectName, projectResp.GetId()) + } + return createAuthorizationForProject(ctx, instance, t, orgID, userID, projectName, projectResp.GetId()) +} + +func createAuthorizationForProject(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, userID, projectName, projectID string) *authorization.Authorization { + userResp, err := instance.Client.UserV2.GetUserByID(ctx, &user.GetUserByIDRequest{UserId: userID}) + require.NoError(t, err) + + userGrantResp := instance.CreateProjectUserGrant(t, ctx, projectID, userID) + return &authorization.Authorization{ + Id: userGrantResp.GetUserGrantId(), + ProjectId: projectID, + ProjectName: projectName, + ProjectOrganizationId: orgID, + OrganizationId: orgID, + CreationDate: userGrantResp.Details.GetCreationDate(), + ChangeDate: userGrantResp.Details.GetCreationDate(), + State: 1, + User: &authorization.User{ + Id: userID, + PreferredLoginName: userResp.User.GetPreferredLoginName(), + DisplayName: userResp.User.GetHuman().GetProfile().GetDisplayName(), + AvatarUrl: userResp.User.GetHuman().GetProfile().GetAvatarUrl(), + OrganizationId: userResp.GetUser().GetDetails().GetResourceOwner(), + }, + } +} + +func createAuthorizationWithProjectGrant(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, userID, projectName, projectID string) *authorization.Authorization { + grantedOrgName := gofakeit.Company() + integration.RandString(10) + grantedOrg := instance.CreateOrganization(ctx, grantedOrgName, gofakeit.Email()) + instance.CreateProjectGrant(ctx, t, projectID, grantedOrg.GetOrganizationId()) + + return createAuthorizationForProjectGrant(ctx, instance, t, orgID, userID, projectName, projectID, grantedOrg.GetOrganizationId()) +} + +func createAuthorizationForProjectGrant(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, userID, projectName, projectID, grantedOrgID string) *authorization.Authorization { + userResp, err := instance.Client.UserV2.GetUserByID(ctx, &user.GetUserByIDRequest{UserId: userID}) + require.NoError(t, err) + + userGrantResp := instance.CreateProjectGrantUserGrant(ctx, orgID, projectID, grantedOrgID, userID) + return &authorization.Authorization{ + Id: userGrantResp.GetUserGrantId(), + ProjectId: projectID, + ProjectName: projectName, + ProjectOrganizationId: orgID, + ProjectGrantId: gu.Ptr(grantedOrgID), + GrantedOrganizationId: gu.Ptr(grantedOrgID), + OrganizationId: orgID, + CreationDate: userGrantResp.Details.GetCreationDate(), + ChangeDate: userGrantResp.Details.GetCreationDate(), + State: 1, + User: &authorization.User{ + Id: userID, + PreferredLoginName: userResp.User.GetPreferredLoginName(), + DisplayName: userResp.User.GetHuman().GetProfile().GetDisplayName(), + AvatarUrl: userResp.User.GetHuman().GetProfile().GetAvatarUrl(), + OrganizationId: userResp.GetUser().GetDetails().GetResourceOwner(), + }, + } +} + +func createProject(ctx context.Context, instance *integration.Instance, t *testing.T, orgID string, projectRoleCheck, hasProjectCheck bool) *project.Project { + name := gofakeit.AppName() + resp := instance.CreateProject(ctx, t, orgID, name, projectRoleCheck, hasProjectCheck) + return &project.Project{ + Id: resp.GetId(), + Name: name, + OrganizationId: orgID, + CreationDate: resp.GetCreationDate(), + ChangeDate: resp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: hasProjectCheck, + AuthorizationRequired: projectRoleCheck, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + } +} + +func createGrantedProject(ctx context.Context, instance *integration.Instance, t *testing.T, projectToGrant *project.Project) *project.Project { + grantedOrgName := gofakeit.AppName() + grantedOrg := instance.CreateOrganization(ctx, grantedOrgName, gofakeit.Email()) + projectGrantResp := instance.CreateProjectGrant(ctx, t, projectToGrant.GetId(), grantedOrg.GetOrganizationId()) + + return &project.Project{ + Id: projectToGrant.GetId(), + Name: projectToGrant.GetName(), + OrganizationId: projectToGrant.GetOrganizationId(), + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: projectToGrant.GetProjectAccessRequired(), + AuthorizationRequired: projectToGrant.GetAuthorizationRequired(), + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + GrantedOrganizationId: gu.Ptr(grantedOrg.GetOrganizationId()), + GrantedOrganizationName: gu.Ptr(grantedOrgName), + GrantedState: 1, + } +} + +func TestServer_ListAuthorizations_PermissionsV2(t *testing.T) { + ensureFeaturePermissionV2Enabled(t, InstancePermissionV2) + iamOwnerCtx := InstancePermissionV2.WithAuthorizationToken(EmptyCTX, integration.UserTypeIAMOwner) + + projectOwnerResp := InstancePermissionV2.CreateMachineUser(iamOwnerCtx) + projectOwnerPatResp := InstancePermissionV2.CreatePersonalAccessToken(iamOwnerCtx, projectOwnerResp.GetUserId()) + projectResp := createProject(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), false, false) + InstancePermissionV2.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), projectOwnerResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(EmptyCTX, projectOwnerPatResp.Token) + + //projectGrantOwnerResp := InstancePermissionV2.CreateMachineUser(iamOwnerCtx) + //projectGrantOwnerPatResp := InstancePermissionV2.CreatePersonalAccessToken(iamOwnerCtx, projectGrantOwnerResp.GetUserId()) + grantedProjectResp := createGrantedProject(iamOwnerCtx, InstancePermissionV2, t, projectResp) + //InstancePermissionV2.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), grantedProjectResp.GetGrantedOrganizationId(), projectGrantOwnerResp.GetUserId()) + //projectGrantOwnerCtx := integration.WithAuthorizationToken(EmptyCTX, projectGrantOwnerPatResp.Token) + + type args struct { + ctx context.Context + dep func(*authorization.ListAuthorizationsRequest, *authorization.ListAuthorizationsResponse) + req *authorization.ListAuthorizationsRequest + } + tests := []struct { + name string + args args + want *authorization.ListAuthorizationsResponse + wantErr bool + }{ + { + name: "list by user id, unauthenticated", + args: args{ + ctx: EmptyCTX, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: InstancePermissionV2.WithAuthorizationToken(EmptyCTX, integration.UserTypeNoPermission), + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{}, + }, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + {Filter: &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: "notexisting", + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id, project", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + response.Authorizations[0] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + resp := createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false) + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_AuthorizationIds{ + AuthorizationIds: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, + }, + } + response.Authorizations[0] = resp + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single project id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + resp := createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false) + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_ProjectId{ + ProjectId: &filter.IDFilter{ + Id: resp.GetProjectId(), + }, + } + response.Authorizations[0] = resp + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single project name", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + resp := createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false) + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_ProjectName{ + ProjectName: &authorization.ProjectNameQuery{ + Name: resp.GetProjectName(), + Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS, + }, + } + response.Authorizations[0] = resp + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single id, project grant", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + response.Authorizations[0] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), true) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single grant id, project grant", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + resp := createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), true) + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_ProjectGrantId{ + ProjectGrantId: &filter.IDFilter{ + Id: resp.GetProjectGrantId(), + }, + } + response.Authorizations[0] = resp + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single id, project and project grant, multiple", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + response.Authorizations[5] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false) + response.Authorizations[4] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false) + response.Authorizations[3] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false) + response.Authorizations[2] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), true) + response.Authorizations[1] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), true) + response.Authorizations[0] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), true) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 6, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, {}, {}, {}, {}, {}, + }, + }, + }, + { + name: "list single id, project and project grant, org owner", + args: args{ + ctx: InstancePermissionV2.WithAuthorizationToken(EmptyCTX, integration.UserTypeOrgOwner), + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + + response.Authorizations[1] = createAuthorizationForProject(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId()) + response.Authorizations[0] = createAuthorizationWithProjectGrant(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId()) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, {}, + }, + }, + }, + { + name: "list single id, project and project grant, project owner", + args: args{ + ctx: projectOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + + response.Authorizations[1] = createAuthorizationForProject(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId()) + response.Authorizations[0] = createAuthorizationWithProjectGrant(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId()) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, {}, + }, + }, + }, + /* + TODO: correct when permission check is added for project grants https://github.com/zitadel/zitadel/issues/9972 + { + name: "list single id, project and project grant, project grant owner", + args: args{ + ctx: projectGrantOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + + createAuthorizationForProject(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId()) + response.Authorizations[0] = createAuthorizationForProjectGrant(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId()) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + */ + } + 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 := InstancePermissionV2.Client.AuthorizationV2Beta.ListAuthorizations(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.Authorizations, len(tt.want.Authorizations)) { + for i := range tt.want.Authorizations { + assert.EqualExportedValues(ttt, tt.want.Authorizations[i], got.Authorizations[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} diff --git a/internal/api/grpc/authorization/v2beta/integration_test/server_test.go b/internal/api/grpc/authorization/v2beta/integration_test/server_test.go new file mode 100644 index 0000000000..fc59713708 --- /dev/null +++ b/internal/api/grpc/authorization/v2beta/integration_test/server_test.go @@ -0,0 +1,65 @@ +//go:build integration + +package authorization_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" +) + +var ( + EmptyCTX context.Context + IAMCTX context.Context + Instance *integration.Instance + InstancePermissionV2 *integration.Instance +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + EmptyCTX = ctx + Instance = integration.NewInstance(ctx) + IAMCTX = Instance.WithAuthorizationToken(ctx, integration.UserTypeIAMOwner) + InstancePermissionV2 = integration.NewInstance(ctx) + return m.Run() + }()) +} + +func ensureFeaturePermissionV2Enabled(t *testing.T, instance *integration.Instance) { + ctx := instance.WithAuthorizationToken(EmptyCTX, integration.UserTypeIAMOwner) + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + require.NoError(t, err) + if f.PermissionCheckV2.GetEnabled() { + return + } + _, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{ + PermissionCheckV2: gu.Ptr(true), + }) + require.NoError(t, err) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) + require.EventuallyWithT(t, + func(ttt *assert.CollectT) { + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + assert.NoError(ttt, err) + if f.PermissionCheckV2.GetEnabled() { + return + } + }, + retryDuration, + tick, + "timed out waiting for ensuring instance feature") +} diff --git a/internal/api/grpc/authorization/v2beta/query.go b/internal/api/grpc/authorization/v2beta/query.go new file mode 100644 index 0000000000..75c3d67178 --- /dev/null +++ b/internal/api/grpc/authorization/v2beta/query.go @@ -0,0 +1,208 @@ +package authorization + +import ( + "context" + "errors" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta" + filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" +) + +func (s *Server) ListAuthorizations(ctx context.Context, req *connect.Request[authorization.ListAuthorizationsRequest]) (*connect.Response[authorization.ListAuthorizationsResponse], error) { + queries, err := s.listAuthorizationsRequestToModel(req.Msg) + if err != nil { + return nil, err + } + resp, err := s.query.UserGrants(ctx, queries, false, s.checkPermission) + if err != nil { + return nil, err + } + return connect.NewResponse(&authorization.ListAuthorizationsResponse{ + Authorizations: userGrantsToPb(resp.UserGrants), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), + }), nil +} + +func (s *Server) listAuthorizationsRequestToModel(req *authorization.ListAuthorizationsRequest) (*query.UserGrantsQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := AuthorizationQueriesToQuery(req.Filters) + if err != nil { + return nil, err + } + return &query.UserGrantsQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: authorizationFieldNameToSortingColumn(req.GetSortingColumn()), + }, + Queries: queries, + }, nil +} + +func authorizationFieldNameToSortingColumn(field authorization.AuthorizationFieldName) query.Column { + switch field { + case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_UNSPECIFIED: + return query.UserGrantCreationDate + case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_CREATED_DATE: + return query.UserGrantCreationDate + case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_CHANGED_DATE: + return query.UserGrantChangeDate + case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_ID: + return query.UserGrantID + case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_USER_ID: + return query.UserGrantUserID + case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_PROJECT_ID: + return query.UserGrantProjectID + case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_ORGANIZATION_ID: + return query.UserGrantResourceOwner + case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_USER_ORGANIZATION_ID: + return query.UserResourceOwnerCol + default: + return query.UserGrantCreationDate + } +} + +func AuthorizationQueriesToQuery(queries []*authorization.AuthorizationsSearchFilter) (q []query.SearchQuery, err error) { + q = make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = AuthorizationSearchFilterToQuery(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func AuthorizationSearchFilterToQuery(query *authorization.AuthorizationsSearchFilter) (query.SearchQuery, error) { + switch q := query.Filter.(type) { + case *authorization.AuthorizationsSearchFilter_AuthorizationIds: + return AuthorizationIDQueryToModel(q.AuthorizationIds) + case *authorization.AuthorizationsSearchFilter_OrganizationId: + return AuthorizationOrganizationIDQueryToModel(q.OrganizationId) + case *authorization.AuthorizationsSearchFilter_State: + return AuthorizationStateQueryToModel(q.State) + case *authorization.AuthorizationsSearchFilter_UserId: + return AuthorizationUserUserIDQueryToModel(q.UserId) + case *authorization.AuthorizationsSearchFilter_UserOrganizationId: + return AuthorizationUserOrganizationIDQueryToModel(q.UserOrganizationId) + case *authorization.AuthorizationsSearchFilter_UserPreferredLoginName: + return AuthorizationUserNameQueryToModel(q.UserPreferredLoginName) + case *authorization.AuthorizationsSearchFilter_UserDisplayName: + return AuthorizationDisplayNameQueryToModel(q.UserDisplayName) + case *authorization.AuthorizationsSearchFilter_ProjectId: + return AuthorizationProjectIDQueryToModel(q.ProjectId) + case *authorization.AuthorizationsSearchFilter_ProjectName: + return AuthorizationProjectNameQueryToModel(q.ProjectName) + case *authorization.AuthorizationsSearchFilter_RoleKey: + return AuthorizationRoleKeyQueryToModel(q.RoleKey) + case *authorization.AuthorizationsSearchFilter_ProjectGrantId: + return AuthorizationProjectGrantIDQueryToModel(q.ProjectGrantId) + default: + return nil, errors.New("invalid query") + } +} + +func AuthorizationIDQueryToModel(q *filter_pb.InIDsFilter) (query.SearchQuery, error) { + return query.NewUserGrantInIDsSearchQuery(q.Ids) +} + +func AuthorizationDisplayNameQueryToModel(q *authorization.UserDisplayNameQuery) (query.SearchQuery, error) { + return query.NewUserGrantDisplayNameQuery(q.DisplayName, filter.TextMethodPbToQuery(q.Method)) +} + +func AuthorizationOrganizationIDQueryToModel(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewUserGrantResourceOwnerSearchQuery(q.Id) +} + +func AuthorizationProjectIDQueryToModel(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewUserGrantProjectIDSearchQuery(q.Id) +} + +func AuthorizationProjectNameQueryToModel(q *authorization.ProjectNameQuery) (query.SearchQuery, error) { + return query.NewUserGrantProjectNameQuery(q.Name, filter.TextMethodPbToQuery(q.Method)) +} + +func AuthorizationProjectGrantIDQueryToModel(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewUserGrantGrantIDSearchQuery(q.Id) +} + +func AuthorizationRoleKeyQueryToModel(q *authorization.RoleKeyQuery) (query.SearchQuery, error) { + return query.NewUserGrantRoleQuery(q.Key) +} + +func AuthorizationUserNameQueryToModel(q *authorization.UserPreferredLoginNameQuery) (query.SearchQuery, error) { + return query.NewUserGrantUsernameQuery(q.LoginName, filter.TextMethodPbToQuery(q.Method)) +} + +func AuthorizationUserUserIDQueryToModel(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewUserGrantUserIDSearchQuery(q.Id) +} + +func AuthorizationUserOrganizationIDQueryToModel(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewUserGrantUserResourceOwnerSearchQuery(q.Id) +} + +func AuthorizationStateQueryToModel(q *authorization.StateQuery) (query.SearchQuery, error) { + return query.NewUserGrantStateQuery(domain.UserGrantState(q.State)) +} + +func userGrantsToPb(userGrants []*query.UserGrant) []*authorization.Authorization { + o := make([]*authorization.Authorization, len(userGrants)) + for i, grant := range userGrants { + o[i] = userGrantToPb(grant) + } + return o +} + +func userGrantToPb(userGrant *query.UserGrant) *authorization.Authorization { + var grantID, grantedOrgID *string + if userGrant.GrantID != "" { + grantID = &userGrant.GrantID + } + if userGrant.GrantedOrgID != "" { + grantedOrgID = &userGrant.GrantedOrgID + } + return &authorization.Authorization{ + Id: userGrant.ID, + ProjectId: userGrant.ProjectID, + ProjectName: userGrant.ProjectName, + ProjectOrganizationId: userGrant.ProjectResourceOwner, + ProjectGrantId: grantID, + GrantedOrganizationId: grantedOrgID, + OrganizationId: userGrant.ResourceOwner, + CreationDate: timestamppb.New(userGrant.CreationDate), + ChangeDate: timestamppb.New(userGrant.ChangeDate), + State: userGrantStateToPb(userGrant.State), + User: &authorization.User{ + Id: userGrant.UserID, + PreferredLoginName: userGrant.PreferredLoginName, + DisplayName: userGrant.DisplayName, + AvatarUrl: userGrant.AvatarURL, + OrganizationId: userGrant.UserResourceOwner, + }, + Roles: userGrant.Roles, + } +} + +func userGrantStateToPb(state domain.UserGrantState) authorization.State { + switch state { + case domain.UserGrantStateActive: + return authorization.State_STATE_ACTIVE + case domain.UserGrantStateInactive: + return authorization.State_STATE_INACTIVE + case domain.UserGrantStateUnspecified, domain.UserGrantStateRemoved: + return authorization.State_STATE_UNSPECIFIED + default: + return authorization.State_STATE_UNSPECIFIED + } +} diff --git a/internal/api/grpc/authorization/v2beta/server.go b/internal/api/grpc/authorization/v2beta/server.go new file mode 100644 index 0000000000..4d66309d2a --- /dev/null +++ b/internal/api/grpc/authorization/v2beta/server.go @@ -0,0 +1,67 @@ +package authorization + +import ( + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" + + "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" + authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta/authorizationconnect" +) + +var _ authorizationconnect.AuthorizationServiceHandler = (*Server)(nil) + +type Server struct { + systemDefaults systemdefaults.SystemDefaults + command *command.Commands + query *query.Queries + + checkPermission domain.PermissionCheck +} + +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, + } +} + +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return authorizationconnect.NewAuthorizationServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return authorization.File_zitadel_authorization_v2beta_authorization_service_proto +} + +func (s *Server) AppName() string { + return authorization.AuthorizationService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return authorization.AuthorizationService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return authorization.AuthorizationService_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return authorization.RegisterAuthorizationServiceHandler +} diff --git a/internal/api/grpc/filter/v2/converter.go b/internal/api/grpc/filter/v2/converter.go index f797ad4bba..98e8bdd4f8 100644 --- a/internal/api/grpc/filter/v2/converter.go +++ b/internal/api/grpc/filter/v2/converter.go @@ -9,6 +9,29 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/filter/v2" ) +func TextMethodPbToQuery(method filter.TextFilterMethod) query.TextComparison { + switch method { + case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS: + return query.TextEquals + case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE: + return query.TextEqualsIgnoreCase + case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH: + return query.TextStartsWith + case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE: + return query.TextStartsWithIgnoreCase + case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS: + return query.TextContains + case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE: + return query.TextContainsIgnoreCase + case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH: + return query.TextEndsWith + case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE: + return query.TextEndsWithIgnoreCase + default: + return -1 + } +} + func TimestampMethodPbToQuery(method filter.TimestampFilterMethod) query.TimestampComparison { switch method { case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_EQUALS: @@ -48,26 +71,3 @@ func QueryToPaginationPb(request query.SearchRequest, response query.SearchRespo TotalResult: response.Count, } } - -func TextMethodPbToQuery(method filter.TextFilterMethod) query.TextComparison { - switch method { - case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS: - return query.TextEquals - case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE: - return query.TextEqualsIgnoreCase - case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH: - return query.TextStartsWith - case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE: - return query.TextStartsWithIgnoreCase - case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS: - return query.TextContains - case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE: - return query.TextContainsIgnoreCase - case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH: - return query.TextEndsWith - case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE: - return query.TextEndsWithIgnoreCase - default: - return -1 - } -} diff --git a/internal/api/grpc/filter/v2beta/converter.go b/internal/api/grpc/filter/v2beta/converter.go index e34f9dd9d7..e15895e9d9 100644 --- a/internal/api/grpc/filter/v2beta/converter.go +++ b/internal/api/grpc/filter/v2beta/converter.go @@ -32,6 +32,23 @@ func TextMethodPbToQuery(method filter.TextFilterMethod) query.TextComparison { } } +func TimestampMethodPbToQuery(method filter.TimestampFilterMethod) query.TimestampComparison { + switch method { + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_EQUALS: + return query.TimestampEquals + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_LESS: + return query.TimestampLess + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_GREATER: + return query.TimestampGreater + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_LESS_OR_EQUALS: + return query.TimestampLessOrEquals + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_GREATER_OR_EQUALS: + return query.TimestampGreaterOrEquals + default: + return -1 + } +} + func PaginationPbToQuery(defaults systemdefaults.SystemDefaults, query *filter.PaginationRequest) (offset, limit uint64, asc bool, err error) { limit = defaults.DefaultQueryLimit if query == nil { diff --git a/internal/api/grpc/internal_permission/v2beta/administrator.go b/internal/api/grpc/internal_permission/v2beta/administrator.go new file mode 100644 index 0000000000..86ee7d9454 --- /dev/null +++ b/internal/api/grpc/internal_permission/v2beta/administrator.go @@ -0,0 +1,220 @@ +package internal_permission + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/zerrors" + internal_permission "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta" +) + +func (s *Server) CreateAdministrator(ctx context.Context, req *connect.Request[internal_permission.CreateAdministratorRequest]) (*connect.Response[internal_permission.CreateAdministratorResponse], error) { + var creationDate *timestamppb.Timestamp + + switch resource := req.Msg.GetResource().GetResource().(type) { + case *internal_permission.ResourceType_Instance: + if resource.Instance { + member, err := s.command.AddInstanceMember(ctx, createAdministratorInstanceToCommand(authz.GetInstance(ctx).InstanceID(), req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + creationDate = timestamppb.New(member.EventDate) + } + } + case *internal_permission.ResourceType_OrganizationId: + member, err := s.command.AddOrgMember(ctx, createAdministratorOrganizationToCommand(resource, req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + creationDate = timestamppb.New(member.EventDate) + } + case *internal_permission.ResourceType_ProjectId: + member, err := s.command.AddProjectMember(ctx, createAdministratorProjectToCommand(resource, req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + creationDate = timestamppb.New(member.EventDate) + } + case *internal_permission.ResourceType_ProjectGrant_: + member, err := s.command.AddProjectGrantMember(ctx, createAdministratorProjectGrantToCommand(resource, req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + creationDate = timestamppb.New(member.EventDate) + } + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ADMIN-IbPp47HDP5", "Errors.Invalid.Argument") + } + + return connect.NewResponse(&internal_permission.CreateAdministratorResponse{ + CreationDate: creationDate, + }), nil +} + +func createAdministratorInstanceToCommand(instanceID, userID string, roles []string) *command.AddInstanceMember { + return &command.AddInstanceMember{ + InstanceID: instanceID, + UserID: userID, + Roles: roles, + } +} + +func createAdministratorOrganizationToCommand(req *internal_permission.ResourceType_OrganizationId, userID string, roles []string) *command.AddOrgMember { + return &command.AddOrgMember{ + OrgID: req.OrganizationId, + UserID: userID, + Roles: roles, + } +} + +func createAdministratorProjectToCommand(req *internal_permission.ResourceType_ProjectId, userID string, roles []string) *command.AddProjectMember { + return &command.AddProjectMember{ + ProjectID: req.ProjectId, + UserID: userID, + Roles: roles, + } +} + +func createAdministratorProjectGrantToCommand(req *internal_permission.ResourceType_ProjectGrant_, userID string, roles []string) *command.AddProjectGrantMember { + return &command.AddProjectGrantMember{ + GrantID: req.ProjectGrant.ProjectGrantId, + ProjectID: req.ProjectGrant.ProjectId, + UserID: userID, + Roles: roles, + } +} + +func (s *Server) UpdateAdministrator(ctx context.Context, req *connect.Request[internal_permission.UpdateAdministratorRequest]) (*connect.Response[internal_permission.UpdateAdministratorResponse], error) { + var changeDate *timestamppb.Timestamp + + switch resource := req.Msg.GetResource().GetResource().(type) { + case *internal_permission.ResourceType_Instance: + if resource.Instance { + member, err := s.command.ChangeInstanceMember(ctx, updateAdministratorInstanceToCommand(authz.GetInstance(ctx).InstanceID(), req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + changeDate = timestamppb.New(member.EventDate) + } + } + case *internal_permission.ResourceType_OrganizationId: + member, err := s.command.ChangeOrgMember(ctx, updateAdministratorOrganizationToCommand(resource, req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + changeDate = timestamppb.New(member.EventDate) + } + case *internal_permission.ResourceType_ProjectId: + member, err := s.command.ChangeProjectMember(ctx, updateAdministratorProjectToCommand(resource, req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + changeDate = timestamppb.New(member.EventDate) + } + case *internal_permission.ResourceType_ProjectGrant_: + member, err := s.command.ChangeProjectGrantMember(ctx, updateAdministratorProjectGrantToCommand(resource, req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + changeDate = timestamppb.New(member.EventDate) + } + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ADMIN-i0V2IbdloZ", "Errors.Invalid.Argument") + } + + return connect.NewResponse(&internal_permission.UpdateAdministratorResponse{ + ChangeDate: changeDate, + }), nil +} + +func updateAdministratorInstanceToCommand(instanceID, userID string, roles []string) *command.ChangeInstanceMember { + return &command.ChangeInstanceMember{ + InstanceID: instanceID, + UserID: userID, + Roles: roles, + } +} + +func updateAdministratorOrganizationToCommand(req *internal_permission.ResourceType_OrganizationId, userID string, roles []string) *command.ChangeOrgMember { + return &command.ChangeOrgMember{ + OrgID: req.OrganizationId, + UserID: userID, + Roles: roles, + } +} + +func updateAdministratorProjectToCommand(req *internal_permission.ResourceType_ProjectId, userID string, roles []string) *command.ChangeProjectMember { + return &command.ChangeProjectMember{ + ProjectID: req.ProjectId, + UserID: userID, + Roles: roles, + } +} + +func updateAdministratorProjectGrantToCommand(req *internal_permission.ResourceType_ProjectGrant_, userID string, roles []string) *command.ChangeProjectGrantMember { + return &command.ChangeProjectGrantMember{ + GrantID: req.ProjectGrant.ProjectGrantId, + ProjectID: req.ProjectGrant.ProjectId, + UserID: userID, + Roles: roles, + } +} + +func (s *Server) DeleteAdministrator(ctx context.Context, req *connect.Request[internal_permission.DeleteAdministratorRequest]) (*connect.Response[internal_permission.DeleteAdministratorResponse], error) { + var deletionDate *timestamppb.Timestamp + + switch resource := req.Msg.GetResource().GetResource().(type) { + case *internal_permission.ResourceType_Instance: + if resource.Instance { + member, err := s.command.RemoveInstanceMember(ctx, authz.GetInstance(ctx).InstanceID(), req.Msg.UserId) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + deletionDate = timestamppb.New(member.EventDate) + } + } + case *internal_permission.ResourceType_OrganizationId: + member, err := s.command.RemoveOrgMember(ctx, resource.OrganizationId, req.Msg.UserId) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + deletionDate = timestamppb.New(member.EventDate) + } + case *internal_permission.ResourceType_ProjectId: + member, err := s.command.RemoveProjectMember(ctx, resource.ProjectId, req.Msg.UserId, "") + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + deletionDate = timestamppb.New(member.EventDate) + } + case *internal_permission.ResourceType_ProjectGrant_: + member, err := s.command.RemoveProjectGrantMember(ctx, resource.ProjectGrant.ProjectId, req.Msg.UserId, resource.ProjectGrant.ProjectGrantId) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + deletionDate = timestamppb.New(member.EventDate) + } + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ADMIN-3UOjLtuohh", "Errors.Invalid.Argument") + } + + return connect.NewResponse(&internal_permission.DeleteAdministratorResponse{ + DeletionDate: deletionDate, + }), nil +} diff --git a/internal/api/grpc/internal_permission/v2beta/integration/administrator_test.go b/internal/api/grpc/internal_permission/v2beta/integration/administrator_test.go new file mode 100644 index 0000000000..4d8e1c057c --- /dev/null +++ b/internal/api/grpc/internal_permission/v2beta/integration/administrator_test.go @@ -0,0 +1,1848 @@ +//go:build integration + +package project_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/integration" + internal_permission "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta" +) + +func TestServer_CreateAdministrator(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *internal_permission.CreateAdministratorRequest) + req *internal_permission.CreateAdministratorRequest + want + wantErr bool + }{ + { + name: "empty user ID", + ctx: iamOwnerCtx, + req: &internal_permission.CreateAdministratorRequest{ + UserId: "", + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "empty roles", + ctx: iamOwnerCtx, + req: &internal_permission.CreateAdministratorRequest{ + UserId: "notexisting", + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{}, + }, + wantErr: true, + }, + { + name: "empty resource", + ctx: iamOwnerCtx, + req: &internal_permission.CreateAdministratorRequest{ + UserId: "notexisting", + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "already existing, error", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "instance, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "instance, not existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + { + name: "org, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: "notexisting", + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + wantErr: true, + }, + { + name: "org, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, no existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + { + name: "project, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: "notexisting", + }, + }, + Roles: []string{"PROJECT_OWNER"}, + }, + wantErr: true, + }, + { + name: "project, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + }, + req: &internal_permission.CreateAdministratorRequest{ + Roles: []string{"PROJECT_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project, not existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + }, + req: &internal_permission.CreateAdministratorRequest{ + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + { + name: "project grant, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: "notexisting", + }, + }, + } + }, + req: &internal_permission.CreateAdministratorRequest{ + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + wantErr: true, + }, + { + name: "project grant, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + } + }, + req: &internal_permission.CreateAdministratorRequest{ + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project grant, not existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + } + }, + req: &internal_permission.CreateAdministratorRequest{ + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + creationDate := time.Now().UTC() + got, err := instance.Client.InternalPermissionv2Beta.CreateAdministrator(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateAdministratorResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func TestServer_CreateAdministrator_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + userProjectResp := instance.CreateMachineUser(iamOwnerCtx) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userProjectResp.GetUserId()) + patProjectResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectResp.Token) + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + userProjectGrantResp := instance.CreateMachineUser(iamOwnerCtx) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userProjectGrantResp.GetUserId()) + patProjectGrantResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectGrantResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectGrantResp.Token) + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *internal_permission.CreateAdministratorRequest) + req *internal_permission.CreateAdministratorRequest + want + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "instance, missing permission, org owner", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "instance, instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, org owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + wantErr: true, + }, + { + name: "project, project owner, ok", + ctx: projectOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + }, + Roles: []string{"PROJECT_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project, missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + }, + Roles: []string{"PROJECT_OWNER"}, + }, + wantErr: true, + }, + { + name: "project grant, project grant owner, ok", + ctx: projectGrantOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project grant, missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + creationDate := time.Now().UTC() + got, err := instance.Client.InternalPermissionv2Beta.CreateAdministrator(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateAdministratorResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func assertCreateAdministratorResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate bool, actualResp *internal_permission.CreateAdministratorResponse) { + if expectedCreationDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.CreationDate) + } +} + +func TestServer_UpdateAdministrator(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *internal_permission.UpdateAdministratorRequest) + req *internal_permission.UpdateAdministratorRequest + want + wantErr bool + }{ + { + name: "empty user ID", + ctx: iamOwnerCtx, + req: &internal_permission.UpdateAdministratorRequest{ + UserId: "", + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "empty roles", + ctx: iamOwnerCtx, + req: &internal_permission.UpdateAdministratorRequest{ + UserId: "notexisting", + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{}, + }, + wantErr: true, + }, + { + name: "empty resource", + ctx: iamOwnerCtx, + req: &internal_permission.UpdateAdministratorRequest{ + UserId: "notexisting", + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "instance, no change", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "instance, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER_VIEWER"}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance, not existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + { + name: "org, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: "notexisting", + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + wantErr: true, + }, + { + name: "org, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER_VIEWER"}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "org, no change", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "org, no existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + { + name: "project, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: "notexisting", + }, + }, + Roles: []string{"PROJECT_OWNER"}, + }, + wantErr: true, + }, + { + name: "project, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"PROJECT_OWNER_VIEWER"}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "project, no change", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"PROJECT_OWNER"}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "project, not existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + { + name: "project grant, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: "notexisting", + }, + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"PROJECT_GRANT_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "project grant, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"PROJECT_GRANT_OWNER_VIEWER"}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "project grant, no change", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "project grant, not existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + if tt.prepare != nil { + tt.prepare(tt.req) + } + got, err := instance.Client.InternalPermissionv2Beta.UpdateAdministrator(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateAdministratorResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_UpdateAdministrator_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + userProjectResp := instance.CreateMachineUser(iamOwnerCtx) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userProjectResp.GetUserId()) + patProjectResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectResp.Token) + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + userProjectGrantResp := instance.CreateMachineUser(iamOwnerCtx) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userProjectGrantResp.GetUserId()) + patProjectGrantResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectGrantResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectGrantResp.Token) + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *internal_permission.UpdateAdministratorRequest) + req *internal_permission.UpdateAdministratorRequest + want + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "instance, missing permission, org owner", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "instance, instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER_VIEWER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER_VIEWER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, org owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER_VIEWER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "project, project owner, ok", + ctx: projectOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + }, + Roles: []string{"PROJECT_OWNER_VIEWER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project, missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + }, + Roles: []string{"PROJECT_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "project grant, project grant owner, ok", + ctx: projectGrantOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER_VIEWER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project grant, missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "project grant, project owner, error", + ctx: projectOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER_VIEWER"}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + creationDate := time.Now().UTC() + got, err := instance.Client.InternalPermissionv2Beta.UpdateAdministrator(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertUpdateAdministratorResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func assertUpdateAdministratorResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *internal_permission.UpdateAdministratorResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} + +func TestServer_DeleteAdministrator(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + tests := []struct { + name string + ctx context.Context + prepare func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) + req *internal_permission.DeleteAdministratorRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "empty user ID", + ctx: iamOwnerCtx, + req: &internal_permission.DeleteAdministratorRequest{ + UserId: "", + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantErr: true, + }, + { + name: "instance, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantDeletionDate: false, + }, + { + name: "instance, already removed", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + instance.DeleteInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantDeletionDate: true, + }, + { + name: "instance, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + return creationDate, time.Time{} + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantDeletionDate: true, + }, + { + name: "org, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: "notexisting", + }, + }, + }, + wantDeletionDate: false, + }, + { + name: "org, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + return creationDate, time.Time{} + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + }, + wantDeletionDate: true, + }, + { + name: "org, already removed", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + instance.DeleteOrgMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + }, + wantDeletionDate: true, + }, + { + name: "project, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: "notexisting", + }, + }, + }, + wantDeletionDate: false, + }, + { + name: "project, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + return creationDate, time.Time{} + }, + req: &internal_permission.DeleteAdministratorRequest{}, + wantDeletionDate: true, + }, + { + name: "project, already removed", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + instance.DeleteProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{}, + wantDeletionDate: true, + }, + { + name: "project grant, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: "notexisting", + }, + }, + } + return creationDate, time.Time{} + }, + req: &internal_permission.DeleteAdministratorRequest{}, + wantDeletionDate: false, + }, + { + name: "project grant, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + } + return creationDate, time.Time{} + }, + req: &internal_permission.DeleteAdministratorRequest{}, + wantDeletionDate: true, + }, + { + name: "project grant, already removed", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + instance.DeleteProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + } + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.InternalPermissionv2Beta.DeleteAdministrator(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteAdministratorResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func TestServer_DeleteAdministrator_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + userProjectResp := instance.CreateMachineUser(iamOwnerCtx) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userProjectResp.GetUserId()) + patProjectResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectResp.Token) + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + userProjectGrantResp := instance.CreateMachineUser(iamOwnerCtx) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userProjectGrantResp.GetUserId()) + patProjectGrantResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectGrantResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectGrantResp.Token) + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *internal_permission.DeleteAdministratorRequest) + req *internal_permission.DeleteAdministratorRequest + want + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantErr: true, + }, + { + name: "missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantErr: true, + }, + { + name: "instance, missing permission, org owner", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantErr: true, + }, + { + name: "instance, instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, org owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + }, + wantErr: true, + }, + { + name: "project, project owner, ok", + ctx: projectOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + }, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project, missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + }, + }, + wantErr: true, + }, + { + name: "project grant, project grant owner, ok", + ctx: projectGrantOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + }, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project grant, missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + }, + }, + wantErr: true, + }, + { + name: "project grant, project owner, error", + ctx: projectOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + creationDate := time.Now().UTC() + got, err := instance.Client.InternalPermissionv2Beta.DeleteAdministrator(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteAdministratorResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func assertDeleteAdministratorResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *internal_permission.DeleteAdministratorResponse) { + 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/internal_permission/v2beta/integration/query_test.go b/internal/api/grpc/internal_permission/v2beta/integration/query_test.go new file mode 100644 index 0000000000..63b07c5194 --- /dev/null +++ b/internal/api/grpc/internal_permission/v2beta/integration/query_test.go @@ -0,0 +1,1173 @@ +//go:build integration + +package project_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" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" + internal_permission "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta" +) + +func TestServer_ListAdministrators(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + projectName := gofakeit.AppName() + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), projectName, false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + + userProjectResp := instance.CreateMachineUser(iamOwnerCtx) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userProjectResp.GetUserId()) + patProjectResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectResp.Token) + + userProjectGrantResp := instance.CreateMachineUser(iamOwnerCtx) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userProjectGrantResp.GetUserId()) + patProjectGrantResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectGrantResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectGrantResp.Token) + + type args struct { + ctx context.Context + dep func(*internal_permission.ListAdministratorsRequest, *internal_permission.ListAdministratorsResponse) + req *internal_permission.ListAdministratorsRequest + } + tests := []struct { + name string + args args + want *internal_permission.ListAdministratorsResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createInstanceAdministrator(iamOwnerCtx, instance, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createInstanceAdministrator(iamOwnerCtx, instance, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{}, + }, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createInstanceAdministrator(iamOwnerCtx, instance, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{}, + }, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{ + { + Filter: &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{Ids: []string{"notexisting"}}, + }, + }, + }, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id, instance", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createInstanceAdministrator(iamOwnerCtx, instance, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list single id, instance", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createInstanceAdministrator(iamOwnerCtx, instance, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, instance", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createInstanceAdministrator(iamOwnerCtx, instance, t) + admin2 := createInstanceAdministrator(iamOwnerCtx, instance, t) + admin3 := createInstanceAdministrator(iamOwnerCtx, instance, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin3 + response.Administrators[1] = admin2 + response.Administrators[2] = admin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list single id, org", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createOrganizationAdministrator(iamOwnerCtx, instance, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, org", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createOrganizationAdministrator(iamOwnerCtx, instance, t) + admin2 := createOrganizationAdministrator(iamOwnerCtx, instance, t) + admin3 := createOrganizationAdministrator(iamOwnerCtx, instance, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin3 + response.Administrators[1] = admin2 + response.Administrators[2] = admin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list single id, project", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createProjectAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, project", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createProjectAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin2 := createProjectAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin3 := createProjectAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin3 + response.Administrators[1] = admin2 + response.Administrators[2] = admin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list single id, project grant", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createProjectGrantAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, project grant", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createProjectGrantAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + admin2 := createProjectGrantAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + admin3 := createProjectGrantAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin3 + response.Administrators[1] = admin2 + response.Administrators[2] = admin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, instance owner", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createInstanceAdministrator(iamOwnerCtx, instance, t) + admin2 := createOrganizationAdministrator(iamOwnerCtx, instance, t) + admin3 := createProjectAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin4 := createProjectGrantAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId(), admin4.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin4 + response.Administrators[1] = admin3 + response.Administrators[2] = admin2 + response.Administrators[3] = admin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, org owner", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createInstanceAdministrator(iamOwnerCtx, instance, t) + admin2 := createOrganizationAdministrator(iamOwnerCtx, instance, t) + admin3 := createProjectAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin4 := createProjectGrantAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId(), admin4.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin4 + response.Administrators[1] = admin3 + response.Administrators[2] = admin2 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, project owner", + args: args{ + ctx: projectOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createInstanceAdministrator(iamOwnerCtx, instance, t) + admin2 := createOrganizationAdministrator(iamOwnerCtx, instance, t) + admin3 := createProjectAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin4 := createProjectGrantAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId(), admin4.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin4 + response.Administrators[1] = admin3 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, + }, + }, + }, + { + name: "list multiple id, project grant owner", + args: args{ + ctx: projectGrantOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createInstanceAdministrator(iamOwnerCtx, instance, t) + admin2 := createOrganizationAdministrator(iamOwnerCtx, instance, t) + admin3 := createProjectAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin4 := createProjectGrantAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId(), admin4.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin4 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + } + 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.InternalPermissionv2Beta.ListAdministrators(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.Administrators, len(tt.want.Administrators)) { + for i := range tt.want.Administrators { + assert.EqualExportedValues(ttt, tt.want.Administrators[i], got.Administrators[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) +} + +func createInstanceAdministrator(ctx context.Context, instance *integration.Instance, t *testing.T) *internal_permission.Administrator { + email := gofakeit.Email() + userResp := instance.CreateUserTypeHuman(ctx, email) + memberResp := instance.CreateInstanceMembership(t, ctx, userResp.GetId()) + return &internal_permission.Administrator{ + CreationDate: memberResp.GetCreationDate(), + ChangeDate: memberResp.GetCreationDate(), + User: &internal_permission.User{ + Id: userResp.GetId(), + PreferredLoginName: email, + DisplayName: "Mickey Mouse", + OrganizationId: instance.DefaultOrg.GetId(), + }, + Resource: &internal_permission.Administrator_Instance{ + Instance: true, + }, + Roles: []string{"IAM_OWNER"}, + } +} + +func createOrganizationAdministrator(ctx context.Context, instance *integration.Instance, t *testing.T) *internal_permission.Administrator { + email := gofakeit.Email() + userResp := instance.CreateUserTypeHuman(ctx, email) + memberResp := instance.CreateOrgMembership(t, ctx, instance.DefaultOrg.Id, userResp.GetId()) + return &internal_permission.Administrator{ + CreationDate: memberResp.GetCreationDate(), + ChangeDate: memberResp.GetCreationDate(), + User: &internal_permission.User{ + Id: userResp.GetId(), + PreferredLoginName: email, + DisplayName: "Mickey Mouse", + OrganizationId: instance.DefaultOrg.GetId(), + }, + Resource: &internal_permission.Administrator_Organization{ + Organization: &internal_permission.Organization{ + Id: instance.DefaultOrg.GetId(), + Name: instance.DefaultOrg.GetName(), + }, + }, + Roles: []string{"ORG_OWNER"}, + } +} + +func createProjectAdministrator(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, projectID, projectName string) *internal_permission.Administrator { + email := gofakeit.Email() + userResp := instance.CreateUserTypeHuman(ctx, email) + memberResp := instance.CreateProjectMembership(t, ctx, projectID, userResp.GetId()) + return &internal_permission.Administrator{ + CreationDate: memberResp.GetCreationDate(), + ChangeDate: memberResp.GetCreationDate(), + User: &internal_permission.User{ + Id: userResp.GetId(), + PreferredLoginName: email, + DisplayName: "Mickey Mouse", + OrganizationId: instance.DefaultOrg.GetId(), + }, + Resource: &internal_permission.Administrator_Project{ + Project: &internal_permission.Project{ + Id: projectID, + Name: projectName, + OrganizationId: orgID, + }, + }, + Roles: []string{"PROJECT_OWNER"}, + } +} + +func createProjectGrantAdministrator(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, projectID, projectName, grantedOrgID string) *internal_permission.Administrator { + email := gofakeit.Email() + userResp := instance.CreateUserTypeHuman(ctx, email) + memberResp := instance.CreateProjectGrantMembership(t, ctx, projectID, grantedOrgID, userResp.GetId()) + return &internal_permission.Administrator{ + CreationDate: memberResp.GetCreationDate(), + ChangeDate: memberResp.GetCreationDate(), + User: &internal_permission.User{ + Id: userResp.GetId(), + PreferredLoginName: email, + DisplayName: "Mickey Mouse", + OrganizationId: instance.DefaultOrg.GetId(), + }, + Resource: &internal_permission.Administrator_ProjectGrant{ + ProjectGrant: &internal_permission.ProjectGrant{ + Id: grantedOrgID, + ProjectId: projectID, + ProjectName: projectName, + OrganizationId: orgID, + GrantedOrganizationId: grantedOrgID, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER"}, + } +} + +func TestServer_ListAdministrators_PermissionV2(t *testing.T) { + ensureFeaturePermissionV2Enabled(t, instancePermissionV2) + iamOwnerCtx := instancePermissionV2.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + projectName := gofakeit.AppName() + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), projectName, false, false) + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + + userProjectResp := instancePermissionV2.CreateMachineUser(iamOwnerCtx) + instancePermissionV2.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userProjectResp.GetUserId()) + patProjectResp := instancePermissionV2.CreatePersonalAccessToken(iamOwnerCtx, userProjectResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectResp.Token) + + userProjectGrantResp := instancePermissionV2.CreateMachineUser(iamOwnerCtx) + instancePermissionV2.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userProjectGrantResp.GetUserId()) + patProjectGrantResp := instancePermissionV2.CreatePersonalAccessToken(iamOwnerCtx, userProjectGrantResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectGrantResp.Token) + + type args struct { + ctx context.Context + dep func(*internal_permission.ListAdministratorsRequest, *internal_permission.ListAdministratorsResponse) + req *internal_permission.ListAdministratorsRequest + } + tests := []struct { + name string + args args + want *internal_permission.ListAdministratorsResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeNoPermission), + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{}, + }, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{}, + }, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{ + { + Filter: &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{Ids: []string{"notexisting"}}, + }, + }, + }, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id, instance", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list single id, instance", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, instance", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin2 := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin3 := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin3 + response.Administrators[1] = admin2 + response.Administrators[2] = admin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list single id, org", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createOrganizationAdministrator(iamOwnerCtx, instancePermissionV2, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, org", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createOrganizationAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin2 := createOrganizationAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin3 := createOrganizationAdministrator(iamOwnerCtx, instancePermissionV2, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin3 + response.Administrators[1] = admin2 + response.Administrators[2] = admin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list single id, project", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createProjectAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, project", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createProjectAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin2 := createProjectAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin3 := createProjectAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin3 + response.Administrators[1] = admin2 + response.Administrators[2] = admin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list single id, project grant", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createProjectGrantAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, project grant", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createProjectGrantAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + admin2 := createProjectGrantAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + admin3 := createProjectGrantAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin3 + response.Administrators[1] = admin2 + response.Administrators[2] = admin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, instance owner", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin2 := createOrganizationAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin3 := createProjectAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin4 := createProjectGrantAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId(), admin4.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin4 + response.Administrators[1] = admin3 + response.Administrators[2] = admin2 + response.Administrators[3] = admin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, org owner", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin2 := createOrganizationAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin3 := createProjectAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin4 := createProjectGrantAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId(), admin4.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin4 + response.Administrators[1] = admin3 + response.Administrators[2] = admin2 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, project owner", + args: args{ + ctx: projectOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin2 := createOrganizationAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin3 := createProjectAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin4 := createProjectGrantAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId(), admin4.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin4 + response.Administrators[1] = admin3 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, + }, + }, + }, + // TODO: correct when permission check is added for project grants https://github.com/zitadel/zitadel/issues/9972 + { + name: "list multiple id, project grant owner", + args: args{ + ctx: projectGrantOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin2 := createOrganizationAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin3 := createProjectAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin4 := createProjectGrantAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId(), admin4.GetUser().GetId()}, + }, + } + // response.Administrators[0] = admin4 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{}, + }, + }, + } + 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 := instancePermissionV2.Client.InternalPermissionv2Beta.ListAdministrators(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.Administrators, len(tt.want.Administrators)) { + for i := range tt.want.Administrators { + assert.EqualExportedValues(ttt, tt.want.Administrators[i], got.Administrators[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} diff --git a/internal/api/grpc/internal_permission/v2beta/integration/server_test.go b/internal/api/grpc/internal_permission/v2beta/integration/server_test.go new file mode 100644 index 0000000000..59d9745222 --- /dev/null +++ b/internal/api/grpc/internal_permission/v2beta/integration/server_test.go @@ -0,0 +1,63 @@ +//go:build integration + +package project_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" +) + +var ( + CTX context.Context + instance *integration.Instance + instancePermissionV2 *integration.Instance +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + CTX = ctx + instance = integration.NewInstance(ctx) + instancePermissionV2 = integration.NewInstance(CTX) + return m.Run() + }()) +} + +func ensureFeaturePermissionV2Enabled(t *testing.T, instance *integration.Instance) { + ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + require.NoError(t, err) + if f.PermissionCheckV2.GetEnabled() { + return + } + _, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{ + PermissionCheckV2: gu.Ptr(true), + }) + require.NoError(t, err) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) + require.EventuallyWithT(t, + func(ttt *assert.CollectT) { + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + assert.NoError(ttt, err) + if f.PermissionCheckV2.GetEnabled() { + return + } + }, + retryDuration, + tick, + "timed out waiting for ensuring instance feature") +} diff --git a/internal/api/grpc/internal_permission/v2beta/query.go b/internal/api/grpc/internal_permission/v2beta/query.go new file mode 100644 index 0000000000..3a8de83292 --- /dev/null +++ b/internal/api/grpc/internal_permission/v2beta/query.go @@ -0,0 +1,192 @@ +package internal_permission + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + filter "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" + internal_permission "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta" +) + +func (s *Server) ListAdministrators(ctx context.Context, req *connect.Request[internal_permission.ListAdministratorsRequest]) (*connect.Response[internal_permission.ListAdministratorsResponse], error) { + queries, err := s.listAdministratorsRequestToModel(req.Msg) + if err != nil { + return nil, err + } + resp, err := s.query.SearchAdministrators(ctx, queries, s.checkPermission) + if err != nil { + return nil, err + } + return connect.NewResponse(&internal_permission.ListAdministratorsResponse{ + Administrators: administratorsToPb(resp.Administrators), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), + }), nil +} + +func (s *Server) listAdministratorsRequestToModel(req *internal_permission.ListAdministratorsRequest) (*query.MembershipSearchQuery, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := administratorSearchFiltersToQuery(req.Filters) + if err != nil { + return nil, err + } + return &query.MembershipSearchQuery{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: administratorFieldNameToSortingColumn(req.GetSortingColumn()), + }, + Queries: queries, + }, nil +} + +func administratorFieldNameToSortingColumn(field internal_permission.AdministratorFieldName) query.Column { + switch field { + case internal_permission.AdministratorFieldName_ADMINISTRATOR_FIELD_NAME_CREATION_DATE: + return query.MembershipCreationDate + case internal_permission.AdministratorFieldName_ADMINISTRATOR_FIELD_NAME_USER_ID: + return query.MembershipUserID + case internal_permission.AdministratorFieldName_ADMINISTRATOR_FIELD_NAME_CHANGE_DATE: + return query.MembershipChangeDate + case internal_permission.AdministratorFieldName_ADMINISTRATOR_FIELD_NAME_UNSPECIFIED: + return query.MembershipCreationDate + default: + return query.MembershipCreationDate + } +} + +func administratorSearchFiltersToQuery(queries []*internal_permission.AdministratorSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, qry := range queries { + q[i], err = administratorFilterToModel(qry) + if err != nil { + return nil, err + } + } + return q, nil +} + +func administratorFilterToModel(filter *internal_permission.AdministratorSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *internal_permission.AdministratorSearchFilter_InUserIdsFilter: + return inUserIDsFilterToQuery(q.InUserIdsFilter) + case *internal_permission.AdministratorSearchFilter_CreationDate: + return creationDateFilterToQuery(q.CreationDate) + case *internal_permission.AdministratorSearchFilter_ChangeDate: + return changeDateFilterToQuery(q.ChangeDate) + case *internal_permission.AdministratorSearchFilter_UserOrganizationId: + return userResourceOwnerFilterToQuery(q.UserOrganizationId) + case *internal_permission.AdministratorSearchFilter_UserPreferredLoginName: + return userLoginNameFilterToQuery(q.UserPreferredLoginName) + case *internal_permission.AdministratorSearchFilter_UserDisplayName: + return userDisplayNameFilterToQuery(q.UserDisplayName) + case *internal_permission.AdministratorSearchFilter_Resource: + return resourceFilterToQuery(q.Resource) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-vR9nC", "List.Query.Invalid") + } +} + +func inUserIDsFilterToQuery(q *filter_pb.InIDsFilter) (query.SearchQuery, error) { + return query.NewMemberInUserIDsSearchQuery(q.GetIds()) +} + +func userResourceOwnerFilterToQuery(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewAdministratorUserResourceOwnerSearchQuery(q.GetId()) +} + +func userLoginNameFilterToQuery(q *internal_permission.UserPreferredLoginNameFilter) (query.SearchQuery, error) { + return query.NewAdministratorUserLoginNameSearchQuery(q.GetPreferredLoginName()) +} + +func userDisplayNameFilterToQuery(q *internal_permission.UserDisplayNameFilter) (query.SearchQuery, error) { + return query.NewAdministratorUserDisplayNameSearchQuery(q.GetDisplayName()) +} + +func creationDateFilterToQuery(q *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewMembershipCreationDateQuery(q.GetTimestamp().AsTime(), filter.TimestampMethodPbToQuery(q.Method)) +} + +func changeDateFilterToQuery(q *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewMembershipChangeDateQuery(q.GetTimestamp().AsTime(), filter.TimestampMethodPbToQuery(q.Method)) +} + +func resourceFilterToQuery(q *internal_permission.ResourceFilter) (query.SearchQuery, error) { + switch q.GetResource().(type) { + case *internal_permission.ResourceFilter_Instance: + if q.GetInstance() { + return query.NewMembershipIsIAMQuery() + } + case *internal_permission.ResourceFilter_OrganizationId: + return query.NewMembershipOrgIDQuery(q.GetOrganizationId()) + case *internal_permission.ResourceFilter_ProjectId: + return query.NewMembershipProjectIDQuery(q.GetProjectId()) + case *internal_permission.ResourceFilter_ProjectGrantId: + return query.NewMembershipProjectGrantIDQuery(q.GetProjectGrantId()) + } + return nil, nil +} + +func administratorsToPb(administrators []*query.Administrator) []*internal_permission.Administrator { + a := make([]*internal_permission.Administrator, len(administrators)) + for i, admin := range administrators { + a[i] = administratorToPb(admin) + } + return a +} + +func administratorToPb(admin *query.Administrator) *internal_permission.Administrator { + var resource internal_permission.Resource + if admin.Instance != nil { + resource = &internal_permission.Administrator_Instance{Instance: true} + } + if admin.Org != nil { + resource = &internal_permission.Administrator_Organization{ + Organization: &internal_permission.Organization{ + Id: admin.Org.OrgID, + Name: admin.Org.Name, + }, + } + } + if admin.Project != nil { + resource = &internal_permission.Administrator_Project{ + Project: &internal_permission.Project{ + Id: admin.Project.ProjectID, + Name: admin.Project.Name, + OrganizationId: admin.Project.ResourceOwner, + }, + } + } + if admin.ProjectGrant != nil { + resource = &internal_permission.Administrator_ProjectGrant{ + ProjectGrant: &internal_permission.ProjectGrant{ + Id: admin.ProjectGrant.GrantID, + ProjectId: admin.ProjectGrant.ProjectID, + ProjectName: admin.ProjectGrant.ProjectName, + OrganizationId: admin.ProjectGrant.ResourceOwner, + GrantedOrganizationId: admin.ProjectGrant.GrantedOrgID, + }, + } + } + + return &internal_permission.Administrator{ + CreationDate: timestamppb.New(admin.CreationDate), + ChangeDate: timestamppb.New(admin.ChangeDate), + User: &internal_permission.User{ + Id: admin.User.UserID, + PreferredLoginName: admin.User.LoginName, + DisplayName: admin.User.DisplayName, + OrganizationId: admin.User.ResourceOwner, + }, + Resource: resource, + Roles: admin.Roles, + } +} diff --git a/internal/api/grpc/internal_permission/v2beta/server.go b/internal/api/grpc/internal_permission/v2beta/server.go new file mode 100644 index 0000000000..bc1a999faa --- /dev/null +++ b/internal/api/grpc/internal_permission/v2beta/server.go @@ -0,0 +1,66 @@ +package internal_permission + +import ( + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" + + "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" + internal_permission "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta/internal_permissionconnect" +) + +var _ internal_permissionconnect.InternalPermissionServiceHandler = (*Server)(nil) + +type Server struct { + systemDefaults systemdefaults.SystemDefaults + command *command.Commands + query *query.Queries + checkPermission domain.PermissionCheck +} + +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, + } +} + +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return internal_permissionconnect.NewInternalPermissionServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return internal_permission.File_zitadel_internal_permission_v2beta_internal_permission_service_proto +} + +func (s *Server) AppName() string { + return internal_permission.InternalPermissionService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return internal_permission.InternalPermissionService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return internal_permission.InternalPermissionService_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return internal_permission.RegisterInternalPermissionServiceHandler +} diff --git a/internal/api/grpc/management/org.go b/internal/api/grpc/management/org.go index 70f509a4d7..ee4f5eb633 100644 --- a/internal/api/grpc/management/org.go +++ b/internal/api/grpc/management/org.go @@ -278,28 +278,28 @@ func (s *Server) ListOrgMembers(ctx context.Context, req *mgmt_pb.ListOrgMembers } func (s *Server) AddOrgMember(ctx context.Context, req *mgmt_pb.AddOrgMemberRequest) (*mgmt_pb.AddOrgMemberResponse, error) { - addedMember, err := s.command.AddOrgMember(ctx, authz.GetCtxData(ctx).OrgID, req.UserId, req.Roles...) + addedMember, err := s.command.AddOrgMember(ctx, AddOrgMemberRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } return &mgmt_pb.AddOrgMemberResponse{ Details: object.AddToDetailsPb( addedMember.Sequence, - addedMember.ChangeDate, + addedMember.EventDate, addedMember.ResourceOwner, ), }, nil } func (s *Server) UpdateOrgMember(ctx context.Context, req *mgmt_pb.UpdateOrgMemberRequest) (*mgmt_pb.UpdateOrgMemberResponse, error) { - changedMember, err := s.command.ChangeOrgMember(ctx, UpdateOrgMemberRequestToDomain(ctx, req)) + changedMember, err := s.command.ChangeOrgMember(ctx, UpdateOrgMemberRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } return &mgmt_pb.UpdateOrgMemberResponse{ Details: object.ChangeToDetailsPb( changedMember.Sequence, - changedMember.ChangeDate, + changedMember.EventDate, changedMember.ResourceOwner, ), }, nil diff --git a/internal/api/grpc/management/org_converter.go b/internal/api/grpc/management/org_converter.go index 03de84cdf4..07c772189c 100644 --- a/internal/api/grpc/management/org_converter.go +++ b/internal/api/grpc/management/org_converter.go @@ -8,6 +8,7 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/metadata" "github.com/zitadel/zitadel/internal/api/grpc/object" org_grpc "github.com/zitadel/zitadel/internal/api/grpc/org" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/query" @@ -67,8 +68,20 @@ func SetPrimaryOrgDomainRequestToDomain(ctx context.Context, req *mgmt_pb.SetPri } } -func UpdateOrgMemberRequestToDomain(ctx context.Context, req *mgmt_pb.UpdateOrgMemberRequest) *domain.Member { - return domain.NewMember(authz.GetCtxData(ctx).OrgID, req.UserId, req.Roles...) +func AddOrgMemberRequestToCommand(req *mgmt_pb.AddOrgMemberRequest, orgID string) *command.AddOrgMember { + return &command.AddOrgMember{ + OrgID: orgID, + UserID: req.UserId, + Roles: req.Roles, + } +} + +func UpdateOrgMemberRequestToCommand(req *mgmt_pb.UpdateOrgMemberRequest, orgID string) *command.ChangeOrgMember { + return &command.ChangeOrgMember{ + OrgID: orgID, + UserID: req.UserId, + Roles: req.Roles, + } } func ListOrgMembersRequestToModel(ctx context.Context, req *mgmt_pb.ListOrgMembersRequest) (*query.OrgMembersQuery, error) { diff --git a/internal/api/grpc/management/project.go b/internal/api/grpc/management/project.go index f3af8dbf86..be196d14ce 100644 --- a/internal/api/grpc/management/project.go +++ b/internal/api/grpc/management/project.go @@ -227,7 +227,7 @@ func (s *Server) RemoveProject(ctx context.Context, req *mgmt_pb.RemoveProjectRe } grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{projectQuery}, - }, true) + }, true, nil) if err != nil { return nil, err } @@ -312,7 +312,7 @@ func (s *Server) RemoveProjectRole(ctx context.Context, req *mgmt_pb.RemoveProje } userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{projectQuery, rolesQuery}, - }, false) + }, false, nil) if err != nil { return nil, err @@ -354,24 +354,28 @@ func (s *Server) ListProjectMembers(ctx context.Context, req *mgmt_pb.ListProjec } func (s *Server) AddProjectMember(ctx context.Context, req *mgmt_pb.AddProjectMemberRequest) (*mgmt_pb.AddProjectMemberResponse, error) { - member, err := s.command.AddProjectMember(ctx, AddProjectMemberRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + member, err := s.command.AddProjectMember(ctx, AddProjectMemberRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } return &mgmt_pb.AddProjectMemberResponse{ - Details: object_grpc.AddToDetailsPb(member.Sequence, member.ChangeDate, member.ResourceOwner), + Details: object_grpc.AddToDetailsPb( + member.Sequence, + member.EventDate, + member.ResourceOwner, + ), }, nil } func (s *Server) UpdateProjectMember(ctx context.Context, req *mgmt_pb.UpdateProjectMemberRequest) (*mgmt_pb.UpdateProjectMemberResponse, error) { - member, err := s.command.ChangeProjectMember(ctx, UpdateProjectMemberRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + member, err := s.command.ChangeProjectMember(ctx, UpdateProjectMemberRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } return &mgmt_pb.UpdateProjectMemberResponse{ Details: object_grpc.ChangeToDetailsPb( member.Sequence, - member.ChangeDate, + member.EventDate, member.ResourceOwner, ), }, nil diff --git a/internal/api/grpc/management/project_converter.go b/internal/api/grpc/management/project_converter.go index 83a8246feb..8ea0014800 100644 --- a/internal/api/grpc/management/project_converter.go +++ b/internal/api/grpc/management/project_converter.go @@ -104,12 +104,22 @@ func ProjectGrantsToIDs(projectGrants *query.ProjectGrants) []string { return converted } -func AddProjectMemberRequestToDomain(req *mgmt_pb.AddProjectMemberRequest) *domain.Member { - return domain.NewMember(req.ProjectId, req.UserId, req.Roles...) +func AddProjectMemberRequestToCommand(req *mgmt_pb.AddProjectMemberRequest, orgID string) *command.AddProjectMember { + return &command.AddProjectMember{ + ResourceOwner: orgID, + ProjectID: req.ProjectId, + UserID: req.UserId, + Roles: req.Roles, + } } -func UpdateProjectMemberRequestToDomain(req *mgmt_pb.UpdateProjectMemberRequest) *domain.Member { - return domain.NewMember(req.ProjectId, req.UserId, req.Roles...) +func UpdateProjectMemberRequestToCommand(req *mgmt_pb.UpdateProjectMemberRequest, orgID string) *command.ChangeProjectMember { + return &command.ChangeProjectMember{ + ResourceOwner: orgID, + ProjectID: req.ProjectId, + UserID: req.UserId, + Roles: req.Roles, + } } func listProjectRequestToModel(req *mgmt_pb.ListProjectsRequest) (*query.ProjectSearchQueries, error) { diff --git a/internal/api/grpc/management/project_grant.go b/internal/api/grpc/management/project_grant.go index d84375818d..26f51f6851 100644 --- a/internal/api/grpc/management/project_grant.go +++ b/internal/api/grpc/management/project_grant.go @@ -91,7 +91,7 @@ func (s *Server) UpdateProjectGrant(ctx context.Context, req *mgmt_pb.UpdateProj } grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{projectQuery, grantQuery}, - }, true) + }, true, nil) if err != nil { return nil, err } @@ -139,7 +139,7 @@ func (s *Server) RemoveProjectGrant(ctx context.Context, req *mgmt_pb.RemoveProj } userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{projectQuery, grantQuery}, - }, false) + }, false, nil) if err != nil { return nil, err } @@ -176,28 +176,28 @@ func (s *Server) ListProjectGrantMembers(ctx context.Context, req *mgmt_pb.ListP } func (s *Server) AddProjectGrantMember(ctx context.Context, req *mgmt_pb.AddProjectGrantMemberRequest) (*mgmt_pb.AddProjectGrantMemberResponse, error) { - member, err := s.command.AddProjectGrantMember(ctx, AddProjectGrantMemberRequestToDomain(req)) + member, err := s.command.AddProjectGrantMember(ctx, AddProjectGrantMemberRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } return &mgmt_pb.AddProjectGrantMemberResponse{ Details: object_grpc.AddToDetailsPb( member.Sequence, - member.ChangeDate, + member.EventDate, member.ResourceOwner, ), }, nil } func (s *Server) UpdateProjectGrantMember(ctx context.Context, req *mgmt_pb.UpdateProjectGrantMemberRequest) (*mgmt_pb.UpdateProjectGrantMemberResponse, error) { - member, err := s.command.ChangeProjectGrantMember(ctx, UpdateProjectGrantMemberRequestToDomain(req)) + member, err := s.command.ChangeProjectGrantMember(ctx, UpdateProjectGrantMemberRequestToCommand(req)) if err != nil { return nil, err } return &mgmt_pb.UpdateProjectGrantMemberResponse{ Details: object_grpc.ChangeToDetailsPb( member.Sequence, - member.ChangeDate, + member.EventDate, member.ResourceOwner, ), }, nil diff --git a/internal/api/grpc/management/project_grant_converter.go b/internal/api/grpc/management/project_grant_converter.go index 04bc35301f..0523eed13a 100644 --- a/internal/api/grpc/management/project_grant_converter.go +++ b/internal/api/grpc/management/project_grant_converter.go @@ -7,7 +7,6 @@ import ( member_grpc "github.com/zitadel/zitadel/internal/api/grpc/member" "github.com/zitadel/zitadel/internal/api/grpc/object" "github.com/zitadel/zitadel/internal/command" - "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" @@ -146,24 +145,21 @@ func ListProjectGrantMembersRequestToModel(ctx context.Context, req *mgmt_pb.Lis }, nil } -func AddProjectGrantMemberRequestToDomain(req *mgmt_pb.AddProjectGrantMemberRequest) *domain.ProjectGrantMember { - return &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: req.ProjectId, - }, - GrantID: req.GrantId, - UserID: req.UserId, - Roles: req.Roles, +func AddProjectGrantMemberRequestToCommand(req *mgmt_pb.AddProjectGrantMemberRequest, orgID string) *command.AddProjectGrantMember { + return &command.AddProjectGrantMember{ + ResourceOwner: orgID, + ProjectID: req.ProjectId, + GrantID: req.GrantId, + UserID: req.UserId, + Roles: req.Roles, } } -func UpdateProjectGrantMemberRequestToDomain(req *mgmt_pb.UpdateProjectGrantMemberRequest) *domain.ProjectGrantMember { - return &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: req.ProjectId, - }, - GrantID: req.GrantId, - UserID: req.UserId, - Roles: req.Roles, +func UpdateProjectGrantMemberRequestToCommand(req *mgmt_pb.UpdateProjectGrantMemberRequest) *command.ChangeProjectGrantMember { + return &command.ChangeProjectGrantMember{ + ProjectID: req.ProjectId, + GrantID: req.GrantId, + UserID: req.UserId, + Roles: req.Roles, } } diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 09b9faa756..f40a29868e 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -142,7 +142,7 @@ func (s *Server) ListUserMetadata(ctx context.Context, req *mgmt_pb.ListUserMeta if err != nil { return nil, err } - res, err := s.query.SearchUserMetadata(ctx, true, req.Id, metadataQueries, false) + res, err := s.query.SearchUserMetadata(ctx, true, req.Id, metadataQueries, nil) if err != nil { return nil, err } @@ -369,7 +369,7 @@ func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]* } grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{userGrantUserQuery}, - }, true) + }, true, nil) if err != nil { return nil, nil, err } diff --git a/internal/api/grpc/management/user_grant.go b/internal/api/grpc/management/user_grant.go index 8589b49e30..3a894c8c29 100644 --- a/internal/api/grpc/management/user_grant.go +++ b/internal/api/grpc/management/user_grant.go @@ -33,7 +33,7 @@ func (s *Server) ListUserGrants(ctx context.Context, req *mgmt_pb.ListUserGrantR if err != nil { return nil, err } - res, err := s.query.UserGrants(ctx, queries, false) + res, err := s.query.UserGrants(ctx, queries, false, nil) if err != nil { return nil, err } @@ -44,11 +44,11 @@ func (s *Server) ListUserGrants(ctx context.Context, req *mgmt_pb.ListUserGrantR } func (s *Server) AddUserGrant(ctx context.Context, req *mgmt_pb.AddUserGrantRequest) (*mgmt_pb.AddUserGrantResponse, error) { - grant := AddUserGrantRequestToDomain(req) + grant := AddUserGrantRequestToDomain(req, authz.GetCtxData(ctx).OrgID) if err := checkExplicitProjectPermission(ctx, grant.ProjectGrantID, grant.ProjectID); err != nil { return nil, err } - grant, err := s.command.AddUserGrant(ctx, grant, authz.GetCtxData(ctx).OrgID) + grant, err := s.command.AddUserGrant(ctx, grant, nil) if err != nil { return nil, err } @@ -63,7 +63,7 @@ func (s *Server) AddUserGrant(ctx context.Context, req *mgmt_pb.AddUserGrantRequ } func (s *Server) UpdateUserGrant(ctx context.Context, req *mgmt_pb.UpdateUserGrantRequest) (*mgmt_pb.UpdateUserGrantResponse, error) { - grant, err := s.command.ChangeUserGrant(ctx, UpdateUserGrantRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + grant, err := s.command.ChangeUserGrant(ctx, UpdateUserGrantRequestToDomain(req, authz.GetCtxData(ctx).OrgID), false, false, nil) if err != nil { return nil, err } @@ -77,7 +77,7 @@ func (s *Server) UpdateUserGrant(ctx context.Context, req *mgmt_pb.UpdateUserGra } func (s *Server) DeactivateUserGrant(ctx context.Context, req *mgmt_pb.DeactivateUserGrantRequest) (*mgmt_pb.DeactivateUserGrantResponse, error) { - objectDetails, err := s.command.DeactivateUserGrant(ctx, req.GrantId, authz.GetCtxData(ctx).OrgID) + objectDetails, err := s.command.DeactivateUserGrant(ctx, req.GrantId, authz.GetCtxData(ctx).OrgID, nil) if err != nil { return nil, err } @@ -87,7 +87,7 @@ func (s *Server) DeactivateUserGrant(ctx context.Context, req *mgmt_pb.Deactivat } func (s *Server) ReactivateUserGrant(ctx context.Context, req *mgmt_pb.ReactivateUserGrantRequest) (*mgmt_pb.ReactivateUserGrantResponse, error) { - objectDetails, err := s.command.ReactivateUserGrant(ctx, req.GrantId, authz.GetCtxData(ctx).OrgID) + objectDetails, err := s.command.ReactivateUserGrant(ctx, req.GrantId, authz.GetCtxData(ctx).OrgID, nil) if err != nil { return nil, err } @@ -97,7 +97,7 @@ func (s *Server) ReactivateUserGrant(ctx context.Context, req *mgmt_pb.Reactivat } func (s *Server) RemoveUserGrant(ctx context.Context, req *mgmt_pb.RemoveUserGrantRequest) (*mgmt_pb.RemoveUserGrantResponse, error) { - objectDetails, err := s.command.RemoveUserGrant(ctx, req.GrantId, authz.GetCtxData(ctx).OrgID) + objectDetails, err := s.command.RemoveUserGrant(ctx, req.GrantId, authz.GetCtxData(ctx).OrgID, false, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/user_grant_converter.go b/internal/api/grpc/management/user_grant_converter.go index 17d992d251..ad1b4acdf6 100644 --- a/internal/api/grpc/management/user_grant_converter.go +++ b/internal/api/grpc/management/user_grant_converter.go @@ -49,19 +49,23 @@ func shouldAppendUserGrantOwnerQuery(queries []*user.UserGrantQuery) bool { return true } -func AddUserGrantRequestToDomain(req *mgmt_pb.AddUserGrantRequest) *domain.UserGrant { +func AddUserGrantRequestToDomain(req *mgmt_pb.AddUserGrantRequest, resourceowner string) *domain.UserGrant { return &domain.UserGrant{ UserID: req.UserId, ProjectID: req.ProjectId, ProjectGrantID: req.ProjectGrantId, RoleKeys: req.RoleKeys, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: resourceowner, + }, } } -func UpdateUserGrantRequestToDomain(req *mgmt_pb.UpdateUserGrantRequest) *domain.UserGrant { +func UpdateUserGrantRequestToDomain(req *mgmt_pb.UpdateUserGrantRequest, resourceowner string) *domain.UserGrant { return &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.GrantId, + AggregateID: req.GrantId, + ResourceOwner: resourceowner, }, UserID: req.UserId, RoleKeys: req.RoleKeys, diff --git a/internal/api/grpc/metadata/v2/metadata.go b/internal/api/grpc/metadata/v2/metadata.go new file mode 100644 index 0000000000..f50ad57f64 --- /dev/null +++ b/internal/api/grpc/metadata/v2/metadata.go @@ -0,0 +1,47 @@ +package metadata + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + filter_v2 "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + meta_pb "github.com/zitadel/zitadel/pkg/grpc/metadata/v2" +) + +func UserMetadataListToPb(dataList []*query.UserMetadata) []*meta_pb.Metadata { + mds := make([]*meta_pb.Metadata, len(dataList)) + for i, data := range dataList { + mds[i] = UserMetadataToPb(data) + } + return mds +} + +func UserMetadataToPb(data *query.UserMetadata) *meta_pb.Metadata { + return &meta_pb.Metadata{ + Key: data.Key, + Value: data.Value, + CreationDate: timestamppb.New(data.CreationDate), + ChangeDate: timestamppb.New(data.ChangeDate), + } +} + +func UserMetadataFiltersToQuery(queries []*meta_pb.MetadataSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = UserMetadataFilterToQuery(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func UserMetadataFilterToQuery(filter *meta_pb.MetadataSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *meta_pb.MetadataSearchFilter_KeyFilter: + return query.NewUserMetadataKeySearchQuery(q.KeyFilter.Key, filter_v2.TextMethodPbToQuery(q.KeyFilter.Method)) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "METAD-fdg23", "List.Query.Invalid") + } +} diff --git a/internal/api/grpc/project/v2beta/integration/query_test.go b/internal/api/grpc/project/v2beta/integration/query_test.go index b648e8c1d7..f8159226c7 100644 --- a/internal/api/grpc/project/v2beta/integration/query_test.go +++ b/internal/api/grpc/project/v2beta/integration/query_test.go @@ -389,7 +389,8 @@ func TestServer_ListProjects(t *testing.T) { resp2 := createProject(iamOwnerCtx, instance, t, orgID, true, false) resp3 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, true) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &filter.InIDsFilter{Ids: []string{resp1.GetId(), resp2.GetId(), resp3.GetId(), projectResp.GetId()}}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp1.GetId(), resp2.GetId(), resp3.GetId(), projectResp.GetId()}}, } response.Projects[0] = grantedProjectResp response.Projects[1] = projectResp @@ -1225,7 +1226,8 @@ func TestServer_ListProjectGrants(t *testing.T) { project2Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false) project3Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &filter.InIDsFilter{Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId(), projectResp.GetId()}}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId(), projectResp.GetId()}}, } createProjectGrant(iamOwnerCtx, instance, t, orgID, project1Resp.GetId(), name1) diff --git a/internal/api/grpc/project/v2beta/project.go b/internal/api/grpc/project/v2beta/project.go index b3294f1ea6..95f08ea61e 100644 --- a/internal/api/grpc/project/v2beta/project.go +++ b/internal/api/grpc/project/v2beta/project.go @@ -118,7 +118,7 @@ func (s *Server) userGrantsFromProject(ctx context.Context, projectID string) ([ } userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{projectQuery}, - }, false) + }, false, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/project/v2beta/project_grant.go b/internal/api/grpc/project/v2beta/project_grant.go index 555d4bfd27..c2ccef1751 100644 --- a/internal/api/grpc/project/v2beta/project_grant.go +++ b/internal/api/grpc/project/v2beta/project_grant.go @@ -119,7 +119,7 @@ func (s *Server) userGrantsFromProjectGrant(ctx context.Context, projectID, gran } userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{projectQuery, grantQuery}, - }, false) + }, false, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/project/v2beta/project_role.go b/internal/api/grpc/project/v2beta/project_role.go index 2316ef4028..b7105b38c6 100644 --- a/internal/api/grpc/project/v2beta/project_role.go +++ b/internal/api/grpc/project/v2beta/project_role.go @@ -109,7 +109,7 @@ func (s *Server) userGrantsFromProjectAndRole(ctx context.Context, projectID, ro } userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{projectQuery, rolesQuery}, - }, false) + }, false, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/user/v2/human.go b/internal/api/grpc/user/v2/human.go index 06414d12cb..03af0f75be 100644 --- a/internal/api/grpc/user/v2/human.go +++ b/internal/api/grpc/user/v2/human.go @@ -16,6 +16,13 @@ import ( ) func (s *Server) createUserTypeHuman(ctx context.Context, humanPb *user.CreateUserRequest_Human, orgId string, userName, userId *string) (*connect.Response[user.CreateUserResponse], error) { + metadataEntries := make([]*user.SetMetadataEntry, len(humanPb.Metadata)) + for i, metadataEntry := range humanPb.Metadata { + metadataEntries[i] = &user.SetMetadataEntry{ + Key: metadataEntry.GetKey(), + Value: metadataEntry.GetValue(), + } + } addHumanPb := &user.AddHumanUserRequest{ Username: userName, UserId: userId, @@ -27,6 +34,7 @@ func (s *Server) createUserTypeHuman(ctx context.Context, humanPb *user.CreateUs Phone: humanPb.Phone, IdpLinks: humanPb.IdpLinks, TotpSecret: humanPb.TotpSecret, + Metadata: metadataEntries, } switch pwType := humanPb.GetPasswordType().(type) { case *user.CreateUserRequest_Human_HashedPassword: diff --git a/internal/api/grpc/user/v2/integration_test/email_test.go b/internal/api/grpc/user/v2/integration_test/email_test.go index ad68ef5c5a..87f8a04dd8 100644 --- a/internal/api/grpc/user/v2/integration_test/email_test.go +++ b/internal/api/grpc/user/v2/integration_test/email_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" - + "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/object/v2" "github.com/zitadel/zitadel/pkg/grpc/user/v2" diff --git a/internal/api/grpc/user/v2/integration_test/key_test.go b/internal/api/grpc/user/v2/integration_test/key_test.go index e85903b2cb..bb4f8657fa 100644 --- a/internal/api/grpc/user/v2/integration_test/key_test.go +++ b/internal/api/grpc/user/v2/integration_test/key_test.go @@ -22,7 +22,7 @@ import ( ) func TestServer_AddKey(t *testing.T) { - resp := Instance.CreateUserTypeMachine(IamCTX) + resp := Instance.CreateUserTypeMachine(IamCTX, Instance.DefaultOrg.Id) userId := resp.GetId() expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24)) type args struct { @@ -108,7 +108,7 @@ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ExpirationDate: expirationDate, }, func(request *user.AddKeyRequest) error { - resp := Instance.CreateUserTypeHuman(IamCTX) + resp := Instance.CreateUserTypeHuman(IamCTX, gofakeit.Email()) request.UserId = resp.Id return nil }, @@ -220,7 +220,7 @@ func TestServer_AddKey_Permission(t *testing.T) { } func TestServer_RemoveKey(t *testing.T) { - resp := Instance.CreateUserTypeMachine(IamCTX) + resp := Instance.CreateUserTypeMachine(IamCTX, Instance.DefaultOrg.Id) userId := resp.GetId() expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24)) type args struct { @@ -388,7 +388,7 @@ func TestServer_ListKeys(t *testing.T) { }) require.NoError(t, err) otherOrgUserId := otherOrgUser.GetId() - otherUserId := Instance.CreateUserTypeMachine(SystemCTX).GetId() + otherUserId := Instance.CreateUserTypeMachine(SystemCTX, Instance.DefaultOrg.Id).GetId() onlySinceTestStartFilter := &user.KeysSearchFilter{Filter: &user.KeysSearchFilter_CreatedDateFilter{CreatedDateFilter: &filter.TimestampFilter{ Timestamp: timestamppb.Now(), Method: filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS, diff --git a/internal/api/grpc/user/v2/integration_test/metadata_test.go b/internal/api/grpc/user/v2/integration_test/metadata_test.go new file mode 100644 index 0000000000..c1e3bced0a --- /dev/null +++ b/internal/api/grpc/user/v2/integration_test/metadata_test.go @@ -0,0 +1,403 @@ +//go:build integration + +package user_test + +import ( + "context" + "encoding/base64" + "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/v2" + metadata "github.com/zitadel/zitadel/pkg/grpc/metadata/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func TestServer_SetUserMetadata(t *testing.T) { + iamOwnerCTX := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + tests := []struct { + name string + ctx context.Context + dep func(request *user.SetUserMetadataRequest) + req *user.SetUserMetadataRequest + setDate bool + wantErr bool + }{ + { + name: "missing permission", + ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeNoPermission), + dep: func(req *user.SetUserMetadataRequest) { + req.UserId = Instance.CreateUserTypeHuman(CTX, gofakeit.Email()).GetId() + }, + req: &user.SetUserMetadataRequest{ + Metadata: []*user.Metadata{{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))}}, + }, + wantErr: true, + }, + { + name: "set user metadata", + ctx: iamOwnerCTX, + dep: func(req *user.SetUserMetadataRequest) { + req.UserId = Instance.CreateUserTypeHuman(CTX, gofakeit.Email()).GetId() + }, + req: &user.SetUserMetadataRequest{ + Metadata: []*user.Metadata{{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))}}, + }, + setDate: true, + }, + { + name: "set user metadata, multiple", + ctx: iamOwnerCTX, + dep: func(req *user.SetUserMetadataRequest) { + req.UserId = Instance.CreateUserTypeHuman(CTX, gofakeit.Email()).GetId() + }, + req: &user.SetUserMetadataRequest{ + Metadata: []*user.Metadata{ + {Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))}, + {Key: "key2", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))}, + {Key: "key3", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value3")))}, + }, + }, + setDate: true, + }, + { + name: "set user metadata on non existent user", + ctx: iamOwnerCTX, + req: &user.SetUserMetadataRequest{ + UserId: "notexisting", + Metadata: []*user.Metadata{{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))}}, + }, + wantErr: true, + }, + { + name: "update user metadata", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + dep: func(req *user.SetUserMetadataRequest) { + req.UserId = Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId() + Instance.SetUserMetadata(iamOwnerCTX, req.UserId, "key1", "value1") + }, + req: &user.SetUserMetadataRequest{ + Metadata: []*user.Metadata{{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))}}, + }, + setDate: true, + }, + { + name: "update user metadata with same value", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + dep: func(req *user.SetUserMetadataRequest) { + req.UserId = Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId() + Instance.SetUserMetadata(iamOwnerCTX, req.UserId, "key1", "value1") + }, + req: &user.SetUserMetadataRequest{ + Metadata: []*user.Metadata{{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))}}, + }, + setDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + if tt.dep != nil { + tt.dep(tt.req) + } + got, err := Client.SetUserMetadata(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + assertSetUserMetadataResponse(t, creationDate, changeDate, tt.setDate, got) + }) + } +} + +func assertSetUserMetadataResponse(t *testing.T, creationDate, changeDate time.Time, expectedSetDat bool, actualResp *user.SetUserMetadataResponse) { + if expectedSetDat { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.SetDate) + } +} + +func TestServer_ListUserMetadata(t *testing.T) { + iamOwnerCTX := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + type args struct { + ctx context.Context + dep func(context.Context, *user.ListUserMetadataRequest, *user.ListUserMetadataResponse) + req *user.ListUserMetadataRequest + } + + tests := []struct { + name string + args args + want *user.ListUserMetadataResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeNoPermission), + dep: func(ctx context.Context, request *user.ListUserMetadataRequest, response *user.ListUserMetadataResponse) { + userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId() + request.UserId = userID + Instance.SetUserMetadata(iamOwnerCTX, userID, "key1", "value1") + }, + req: &user.ListUserMetadataRequest{}, + }, + wantErr: true, + }, + { + name: "list request", + args: args{ + ctx: iamOwnerCTX, + dep: func(ctx context.Context, request *user.ListUserMetadataRequest, response *user.ListUserMetadataResponse) { + userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId() + request.UserId = userID + metadataResp := Instance.SetUserMetadata(iamOwnerCTX, userID, "key1", "value1") + + response.Metadata[0] = &metadata.Metadata{ + CreationDate: metadataResp.GetSetDate(), + ChangeDate: metadataResp.GetSetDate(), + Key: "key1", + Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1"))), + } + }, + req: &user.ListUserMetadataRequest{}, + }, + want: &user.ListUserMetadataResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Metadata: []*metadata.Metadata{ + {}, + }, + }, + }, + { + name: "list request single key", + args: args{ + ctx: iamOwnerCTX, + dep: func(ctx context.Context, request *user.ListUserMetadataRequest, response *user.ListUserMetadataResponse) { + userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId() + request.UserId = userID + key := "key1" + response.Metadata[0] = setUserMetadata(iamOwnerCTX, userID, key, "value1") + Instance.SetUserMetadata(iamOwnerCTX, userID, "key2", "value2") + Instance.SetUserMetadata(iamOwnerCTX, userID, "key3", "value3") + request.Filters[0] = &metadata.MetadataSearchFilter{ + Filter: &metadata.MetadataSearchFilter_KeyFilter{KeyFilter: &metadata.MetadataKeyFilter{Key: key}}, + } + }, + req: &user.ListUserMetadataRequest{ + Filters: []*metadata.MetadataSearchFilter{{}}, + }, + }, + want: &user.ListUserMetadataResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Metadata: []*metadata.Metadata{ + {}, + }, + }, + }, + { + name: "list multiple keys", + args: args{ + ctx: iamOwnerCTX, + dep: func(ctx context.Context, request *user.ListUserMetadataRequest, response *user.ListUserMetadataResponse) { + userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId() + request.UserId = userID + + response.Metadata[2] = setUserMetadata(iamOwnerCTX, userID, "key1", "value1") + response.Metadata[1] = setUserMetadata(iamOwnerCTX, userID, "key2", "value2") + response.Metadata[0] = setUserMetadata(iamOwnerCTX, userID, "key3", "value3") + }, + req: &user.ListUserMetadataRequest{}, + }, + want: &user.ListUserMetadataResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Metadata: []*metadata.Metadata{ + {}, {}, {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.ctx, tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCTX, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := Instance.Client.UserV2.ListUserMetadata(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.Metadata, len(tt.want.Metadata)) { + assert.EqualExportedValues(ttt, got.Metadata, tt.want.Metadata) + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func setUserMetadata(ctx context.Context, userID, key, value string) *metadata.Metadata { + metadataResp := Instance.SetUserMetadata(ctx, userID, key, value) + return &metadata.Metadata{ + CreationDate: metadataResp.GetSetDate(), + ChangeDate: metadataResp.GetSetDate(), + Key: key, + Value: []byte(base64.StdEncoding.EncodeToString([]byte(value))), + } +} + +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) +} + +func TestServer_DeleteUserMetadata(t *testing.T) { + iamOwnerCTX := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + tests := []struct { + name string + ctx context.Context + prepare func(request *user.DeleteUserMetadataRequest) (time.Time, time.Time) + req *user.DeleteUserMetadataRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "empty id", + ctx: iamOwnerCTX, + req: &user.DeleteUserMetadataRequest{ + UserId: "", + }, + wantErr: true, + }, + { + name: "delete, user not existing", + ctx: iamOwnerCTX, + req: &user.DeleteUserMetadataRequest{ + UserId: "notexisting", + }, + wantErr: true, + }, + { + name: "delete", + ctx: iamOwnerCTX, + prepare: func(request *user.DeleteUserMetadataRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId() + request.UserId = userID + key := "key1" + Instance.SetUserMetadata(iamOwnerCTX, userID, key, "value1") + request.Keys = []string{key} + return creationDate, time.Time{} + }, + req: &user.DeleteUserMetadataRequest{}, + wantDeletionDate: true, + }, + { + name: "delete, empty list", + ctx: iamOwnerCTX, + prepare: func(request *user.DeleteUserMetadataRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId() + request.UserId = userID + key := "key1" + Instance.SetUserMetadata(iamOwnerCTX, userID, key, "value1") + Instance.DeleteUserMetadata(iamOwnerCTX, userID, key) + return creationDate, time.Now().UTC() + }, + req: &user.DeleteUserMetadataRequest{}, + wantErr: true, + }, + { + name: "delete, already removed", + ctx: iamOwnerCTX, + prepare: func(request *user.DeleteUserMetadataRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId() + request.UserId = userID + key := "key1" + Instance.SetUserMetadata(iamOwnerCTX, userID, key, "value1") + Instance.DeleteUserMetadata(iamOwnerCTX, userID, key) + request.Keys = []string{key} + return creationDate, time.Now().UTC() + }, + req: &user.DeleteUserMetadataRequest{}, + wantErr: true, + }, + { + name: "delete, multiple", + ctx: iamOwnerCTX, + prepare: func(request *user.DeleteUserMetadataRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId() + request.UserId = userID + key1 := "key1" + Instance.SetUserMetadata(iamOwnerCTX, userID, key1, "value1") + key2 := "key2" + Instance.SetUserMetadata(iamOwnerCTX, userID, key2, "value1") + key3 := "key3" + Instance.SetUserMetadata(iamOwnerCTX, userID, key3, "value1") + request.Keys = []string{key1, key2, key3} + return creationDate, time.Time{} + }, + req: &user.DeleteUserMetadataRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := Instance.Client.UserV2.DeleteUserMetadata(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteProjectResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func assertDeleteProjectResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *user.DeleteUserMetadataResponse) { + 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/user/v2/integration_test/pat_test.go b/internal/api/grpc/user/v2/integration_test/pat_test.go index ce974e0407..8ca6d80139 100644 --- a/internal/api/grpc/user/v2/integration_test/pat_test.go +++ b/internal/api/grpc/user/v2/integration_test/pat_test.go @@ -22,7 +22,7 @@ import ( ) func TestServer_AddPersonalAccessToken(t *testing.T) { - resp := Instance.CreateUserTypeMachine(IamCTX) + resp := Instance.CreateUserTypeMachine(IamCTX, Instance.DefaultOrg.Id) userId := resp.GetId() expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24)) type args struct { @@ -64,7 +64,7 @@ func TestServer_AddPersonalAccessToken(t *testing.T) { ExpirationDate: expirationDate, }, func(request *user.AddPersonalAccessTokenRequest) error { - resp := Instance.CreateUserTypeHuman(IamCTX) + resp := Instance.CreateUserTypeHuman(IamCTX, gofakeit.Email()) request.UserId = resp.Id return nil }, @@ -172,7 +172,7 @@ func TestServer_AddPersonalAccessToken_Permission(t *testing.T) { } func TestServer_RemovePersonalAccessToken(t *testing.T) { - resp := Instance.CreateUserTypeMachine(IamCTX) + resp := Instance.CreateUserTypeMachine(IamCTX, Instance.DefaultOrg.Id) userId := resp.GetId() expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24)) type args struct { @@ -339,7 +339,7 @@ func TestServer_ListPersonalAccessTokens(t *testing.T) { }) require.NoError(t, err) otherOrgUserId := otherOrgUser.GetId() - otherUserId := Instance.CreateUserTypeMachine(SystemCTX).GetId() + otherUserId := Instance.CreateUserTypeMachine(SystemCTX, Instance.DefaultOrg.Id).GetId() onlySinceTestStartFilter := &user.PersonalAccessTokensSearchFilter{Filter: &user.PersonalAccessTokensSearchFilter_CreatedDateFilter{CreatedDateFilter: &filter.TimestampFilter{ Timestamp: timestamppb.Now(), Method: filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS, diff --git a/internal/api/grpc/user/v2/integration_test/secret_test.go b/internal/api/grpc/user/v2/integration_test/secret_test.go index 8ff537b1fd..4296e8e599 100644 --- a/internal/api/grpc/user/v2/integration_test/secret_test.go +++ b/internal/api/grpc/user/v2/integration_test/secret_test.go @@ -43,7 +43,7 @@ func TestServer_AddSecret(t *testing.T) { CTX, &user.AddSecretRequest{}, func(request *user.AddSecretRequest) error { - resp := Instance.CreateUserTypeMachine(CTX) + resp := Instance.CreateUserTypeMachine(CTX, Instance.DefaultOrg.Id) request.UserId = resp.GetId() return nil }, @@ -55,7 +55,7 @@ func TestServer_AddSecret(t *testing.T) { CTX, &user.AddSecretRequest{}, func(request *user.AddSecretRequest) error { - resp := Instance.CreateUserTypeMachine(CTX) + resp := Instance.CreateUserTypeMachine(CTX, Instance.DefaultOrg.Id) request.UserId = resp.GetId() return nil }, @@ -67,7 +67,7 @@ func TestServer_AddSecret(t *testing.T) { CTX, &user.AddSecretRequest{}, func(request *user.AddSecretRequest) error { - resp := Instance.CreateUserTypeMachine(CTX) + resp := Instance.CreateUserTypeMachine(CTX, Instance.DefaultOrg.Id) request.UserId = resp.GetId() _, err := Client.AddSecret(CTX, &user.AddSecretRequest{ UserId: resp.GetId(), @@ -202,7 +202,7 @@ func TestServer_RemoveSecret(t *testing.T) { CTX, &user.RemoveSecretRequest{}, func(request *user.RemoveSecretRequest) error { - resp := Instance.CreateUserTypeMachine(CTX) + resp := Instance.CreateUserTypeMachine(CTX, Instance.DefaultOrg.Id) request.UserId = resp.GetId() return nil }, @@ -215,7 +215,7 @@ func TestServer_RemoveSecret(t *testing.T) { CTX, &user.RemoveSecretRequest{}, func(request *user.RemoveSecretRequest) error { - resp := Instance.CreateUserTypeMachine(CTX) + resp := Instance.CreateUserTypeMachine(CTX, Instance.DefaultOrg.Id) request.UserId = resp.GetId() _, err := Instance.Client.UserV2.AddSecret(CTX, &user.AddSecretRequest{ UserId: resp.GetId(), diff --git a/internal/api/grpc/user/v2/integration_test/user_test.go b/internal/api/grpc/user/v2/integration_test/user_test.go index 1776c57fcb..452de6720c 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -4,6 +4,7 @@ package user_test import ( "context" + "encoding/base64" "fmt" "net/url" "os" @@ -1818,7 +1819,7 @@ func TestServer_DeleteUser(t *testing.T) { request.UserId = resp.GetUserId() Instance.CreateProjectUserGrant(t, CTX, projectResp.GetId(), request.UserId) Instance.CreateProjectMembership(t, CTX, projectResp.GetId(), request.UserId) - Instance.CreateOrgMembership(t, CTX, request.UserId) + Instance.CreateOrgMembership(t, CTX, Instance.DefaultOrg.Id, request.UserId) return CTX }, }, @@ -3945,6 +3946,44 @@ func TestServer_CreateUser(t *testing.T) { } }, }, + { + name: "with metadata", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + Metadata: []*user.Metadata{ + {Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))}, + {Key: "key2", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))}, + {Key: "key3", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value3")))}, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, { name: "with idp", testCase: func(runId string) testCase { @@ -4890,7 +4929,7 @@ func TestServer_UpdateUserTypeHuman(t *testing.T) { t.Run(tt.name, func(t *testing.T) { now := time.Now() runId := fmt.Sprint(now.UnixNano() + int64(i)) - userId := Instance.CreateUserTypeHuman(CTX).GetId() + userId := Instance.CreateUserTypeHuman(CTX, gofakeit.Email()).GetId() test := tt.testCase(runId, userId) got, err := Client.UpdateUser(test.args.ctx, test.args.req) if test.wantErr { @@ -4972,7 +5011,7 @@ func TestServer_UpdateUserTypeMachine(t *testing.T) { t.Run(tt.name, func(t *testing.T) { now := time.Now() runId := fmt.Sprint(now.UnixNano() + int64(i)) - userId := Instance.CreateUserTypeMachine(CTX).GetId() + userId := Instance.CreateUserTypeMachine(CTX, Instance.DefaultOrg.Id).GetId() test := tt.testCase(runId, userId) got, err := Client.UpdateUser(test.args.ctx, test.args.req) if test.wantErr { diff --git a/internal/api/grpc/user/v2/metadata.go b/internal/api/grpc/user/v2/metadata.go new file mode 100644 index 0000000000..338ce9fc45 --- /dev/null +++ b/internal/api/grpc/user/v2/metadata.go @@ -0,0 +1,80 @@ +package user + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + "github.com/zitadel/zitadel/internal/api/grpc/metadata/v2" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) ListUserMetadata(ctx context.Context, req *connect.Request[user.ListUserMetadataRequest]) (*connect.Response[user.ListUserMetadataResponse], error) { + metadataQueries, err := s.listUserMetadataRequestToModel(req.Msg) + if err != nil { + return nil, err + } + res, err := s.query.SearchUserMetadata(ctx, true, req.Msg.UserId, metadataQueries, s.checkPermission) + if err != nil { + return nil, err + } + return connect.NewResponse(&user.ListUserMetadataResponse{ + Metadata: metadata.UserMetadataListToPb(res.Metadata), + Pagination: filter.QueryToPaginationPb(metadataQueries.SearchRequest, res.SearchResponse), + }), nil +} + +func (s *Server) listUserMetadataRequestToModel(req *user.ListUserMetadataRequest) (*query.UserMetadataSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := metadata.UserMetadataFiltersToQuery(req.Filters) + if err != nil { + return nil, err + } + return &query.UserMetadataSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: query.UserMetadataCreationDateCol, + }, + Queries: queries, + }, nil +} + +func (s *Server) SetUserMetadata(ctx context.Context, req *connect.Request[user.SetUserMetadataRequest]) (*connect.Response[user.SetUserMetadataResponse], error) { + result, err := s.command.BulkSetUserMetadata(ctx, req.Msg.UserId, "", setUserMetadataToDomain(req.Msg)...) + if err != nil { + return nil, err + } + return connect.NewResponse(&user.SetUserMetadataResponse{ + SetDate: timestamppb.New(result.EventDate), + }), nil +} + +func setUserMetadataToDomain(req *user.SetUserMetadataRequest) []*domain.Metadata { + metadata := make([]*domain.Metadata, len(req.Metadata)) + for i, data := range req.Metadata { + metadata[i] = &domain.Metadata{ + Key: data.Key, + Value: data.Value, + } + } + return metadata +} + +func (s *Server) DeleteUserMetadata(ctx context.Context, req *connect.Request[user.DeleteUserMetadataRequest]) (*connect.Response[user.DeleteUserMetadataResponse], error) { + result, err := s.command.BulkRemoveUserMetadata(ctx, req.Msg.UserId, "", req.Msg.Keys...) + if err != nil { + return nil, err + } + return connect.NewResponse(&user.DeleteUserMetadataResponse{ + DeletionDate: timestamppb.New(result.EventDate), + }), nil +} diff --git a/internal/api/grpc/user/v2/server.go b/internal/api/grpc/user/v2/server.go index 1f94906853..e89d0a7d60 100644 --- a/internal/api/grpc/user/v2/server.go +++ b/internal/api/grpc/user/v2/server.go @@ -37,9 +37,9 @@ type Server struct { type Config struct{} func CreateServer( - systemDefaults systemdefaults.SystemDefaults, command *command.Commands, query *query.Queries, + systemDefaults systemdefaults.SystemDefaults, userCodeAlg crypto.EncryptionAlgorithm, idpAlg crypto.EncryptionAlgorithm, idpCallback func(ctx context.Context) string, @@ -48,7 +48,6 @@ func CreateServer( checkPermission domain.PermissionCheck, ) *Server { return &Server{ - systemDefaults: systemDefaults, command: command, query: query, userCodeAlg: userCodeAlg, @@ -57,6 +56,7 @@ func CreateServer( samlRootURL: samlRootURL, assetAPIPrefix: assetAPIPrefix, checkPermission: checkPermission, + systemDefaults: systemDefaults, } } diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go index 95c2883195..3eeda8da5f 100644 --- a/internal/api/grpc/user/v2/user.go +++ b/internal/api/grpc/user/v2/user.go @@ -204,7 +204,7 @@ func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]* } grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{userGrantUserQuery}, - }, true) + }, true, nil) if err != nil { return nil, nil, err } diff --git a/internal/api/grpc/user/v2beta/integration_test/user_test.go b/internal/api/grpc/user/v2beta/integration_test/user_test.go index 7b02f7da70..077ed02d0e 100644 --- a/internal/api/grpc/user/v2beta/integration_test/user_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/user_test.go @@ -1773,7 +1773,7 @@ func TestServer_DeleteUser(t *testing.T) { request.UserId = resp.GetUserId() Instance.CreateProjectUserGrant(t, CTX, projectResp.GetId(), request.UserId) Instance.CreateProjectMembership(t, CTX, projectResp.GetId(), request.UserId) - Instance.CreateOrgMembership(t, CTX, request.UserId) + Instance.CreateOrgMembership(t, CTX, Instance.DefaultOrg.Id, request.UserId) }, }, want: &user.DeleteUserResponse{ @@ -2125,7 +2125,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { if tt.want.url != "" && !tt.want.postForm { authUrl, err := url.Parse(got.GetAuthUrl()) require.NoError(t, err) - + assert.Equal(t, tt.want.url, authUrl.Scheme+"://"+authUrl.Host+authUrl.Path) require.Len(t, authUrl.Query(), len(tt.want.parametersEqual)+len(tt.want.parametersExisting)) diff --git a/internal/api/grpc/user/v2beta/user.go b/internal/api/grpc/user/v2beta/user.go index e5b2094d2c..3cde7b773e 100644 --- a/internal/api/grpc/user/v2beta/user.go +++ b/internal/api/grpc/user/v2beta/user.go @@ -296,7 +296,7 @@ func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]* } grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{userGrantUserQuery}, - }, true) + }, true, nil) if err != nil { return nil, nil, err } diff --git a/internal/api/saml/storage.go b/internal/api/saml/storage.go index 935e986c72..5a7f6cb576 100644 --- a/internal/api/saml/storage.go +++ b/internal/api/saml/storage.go @@ -309,7 +309,7 @@ func (p *Storage) getCustomAttributes(ctx context.Context, user *query.User, use true, user.ID, &query.UserMetadataSearchQueries{Queries: []query.SearchQuery{resourceOwnerQuery}}, - false, + nil, ) if err != nil { logging.WithError(err).Info("unable to get md in action") @@ -490,7 +490,7 @@ func (p *Storage) getGrants(ctx context.Context, userID, applicationID string) ( userIDQuery, activeQuery, }, - }, true) + }, true, nil) } type customAttribute struct { diff --git a/internal/api/scim/integration_test/bulk_test.go b/internal/api/scim/integration_test/bulk_test.go index 660b10f4fd..c2f430c1b7 100644 --- a/internal/api/scim/integration_test/bulk_test.go +++ b/internal/api/scim/integration_test/bulk_test.go @@ -17,6 +17,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/text/language" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "github.com/zitadel/zitadel/internal/api/scim/resources" "github.com/zitadel/zitadel/internal/api/scim/schemas" @@ -478,7 +480,7 @@ func TestBulk(t *testing.T) { }, { name: "fail on errors", - body: bulkFailOnErrorsJson, + body: withUsername(bulkFailOnErrorsJson, gofakeit.Username()), want: &scim.BulkResponse{ Schemas: []schemas.ScimSchemaType{schemas.IdBulkResponse}, Operations: []*scim.BulkResponseOperation{ @@ -579,7 +581,6 @@ func TestBulk(t *testing.T) { response, err := Instance.Client.SCIM.Bulk(ctx, orgID, tt.body) createdUserIDs := buildCreatedIDs(response) - defer deleteUsers(t, createdUserIDs) if tt.wantErr != nil { statusCode := tt.wantErr.status @@ -656,17 +657,6 @@ func buildCreatedIDs(response *scim.BulkResponse) []string { return createdIds } -func deleteUsers(t require.TestingT, ids []string) { - for _, id := range ids { - err := Instance.Client.SCIM.Users.Delete(CTX, Instance.DefaultOrg.Id, id) - - // only not found errors are ok (if the user is deleted in a later on bulk request) - if err != nil { - scim.RequireScimError(t, http.StatusNotFound, err) - } - } -} - func buildMinimalUpdateRequest(userID string) *scim.BulkRequest { return &scim.BulkRequest{ Schemas: []schemas.ScimSchemaType{schemas.IdBulkRequest}, @@ -690,7 +680,7 @@ func buildTooManyOperationsRequest() *scim.BulkRequest { req.Operations[i] = &scim.BulkRequestOperation{ Method: http.MethodPost, Path: "/Users", - Data: minimalUserJson, + Data: withUsername(minimalUserJson, gofakeit.Username()), } } @@ -720,8 +710,11 @@ func ensureMetadataProjected(t require.TestingT, userID, key, value string) { Id: userID, Key: key, }) - require.NoError(tt, err) - require.Equal(tt, value, string(md.Metadata.Value)) + if !assert.NoError(tt, err) { + require.Equal(tt, status.Code(err), codes.NotFound) + return + } + assert.Equal(tt, value, string(md.Metadata.Value)) }, retryDuration, tick) } diff --git a/internal/api/scim/integration_test/testdata/bulk_test_fail_on_errors.json b/internal/api/scim/integration_test/testdata/bulk_test_fail_on_errors.json index bc9ed1346a..d1d0c88fe6 100644 --- a/internal/api/scim/integration_test/testdata/bulk_test_fail_on_errors.json +++ b/internal/api/scim/integration_test/testdata/bulk_test_fail_on_errors.json @@ -32,14 +32,14 @@ "urn:ietf:params:scim:schemas:core:2.0:User" ], "externalId": "scim-bulk-created-user-0", - "userName": "scim-bulk-created-user-0", + "userName": "{{ .Username }}", "name": { "familyName": "scim-bulk-created-user-0-family-name", "givenName": "scim-bulk-created-user-0-given-name" }, "emails": [ { - "value": "scim-bulk-created-user-0@example.com", + "value": "{{ .Username }}@example.com", "primary": true } ], diff --git a/internal/api/scim/integration_test/testdata/users_create_test_full.json b/internal/api/scim/integration_test/testdata/users_create_test_full.json index 7879ecf160..cfc786a7e3 100644 --- a/internal/api/scim/integration_test/testdata/users_create_test_full.json +++ b/internal/api/scim/integration_test/testdata/users_create_test_full.json @@ -1,7 +1,7 @@ { "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "externalId": "701984", - "userName": "bjensen@example.com", + "userName": "{{ .Username }}@example.com", "name": { "formatted": "Ms. Barbara J Jensen, III", "familyName": "Jensen", @@ -15,12 +15,12 @@ "profileUrl": "http://login.example.com/bjensen", "emails": [ { - "value": "bjensen@example.com", + "value": "{{ .Username }}@example.com", "type": "work", "primary": true }, { - "value": "babs@jensen.org", + "value": "{{ .Username }}+1@example.com", "type": "home" } ], diff --git a/internal/api/scim/integration_test/testdata/users_create_test_minimal.json b/internal/api/scim/integration_test/testdata/users_create_test_minimal.json index c51f416bc7..bdceb3e45f 100644 --- a/internal/api/scim/integration_test/testdata/users_create_test_minimal.json +++ b/internal/api/scim/integration_test/testdata/users_create_test_minimal.json @@ -2,14 +2,14 @@ "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:User" ], - "userName": "acmeUser1", + "userName": "{{ .Username }}", "name": { "familyName": "Ross", "givenName": "Bethany" }, "emails": [ { - "value": "user1@example.com", + "value": "{{ .Username }}@example.com", "primary": true } ] diff --git a/internal/api/scim/integration_test/testdata/users_create_test_minimal_inactive.json b/internal/api/scim/integration_test/testdata/users_create_test_minimal_inactive.json index 11650674a6..95ca1246d5 100644 --- a/internal/api/scim/integration_test/testdata/users_create_test_minimal_inactive.json +++ b/internal/api/scim/integration_test/testdata/users_create_test_minimal_inactive.json @@ -2,14 +2,14 @@ "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:User" ], - "userName": "acmeUser1", + "userName": "acmeUser1-inactive", "name": { "familyName": "Ross", "givenName": "Bethany" }, "emails": [ { - "value": "user1@example.com", + "value": "user1-inactive@example.com", "primary": true } ], diff --git a/internal/api/scim/integration_test/testdata/users_create_test_no_primary_email_phone.json b/internal/api/scim/integration_test/testdata/users_create_test_no_primary_email_phone.json index 20c67c4715..f2b4bf2e4c 100644 --- a/internal/api/scim/integration_test/testdata/users_create_test_no_primary_email_phone.json +++ b/internal/api/scim/integration_test/testdata/users_create_test_no_primary_email_phone.json @@ -2,14 +2,14 @@ "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:User" ], - "userName": "acmeUser1", + "userName": "acmeUser1-no-primary-email-phone", "name": { "familyName": "Ross", "givenName": "Bethany" }, "emails": [ { - "value": "user1@example.com" + "value": "user1-no-primary-email-phone@example.com" } ], "phoneNumbers": [ diff --git a/internal/api/scim/integration_test/testdata/users_replace_test_minimal_with_email_type.json b/internal/api/scim/integration_test/testdata/users_replace_test_minimal_with_email_type.json index b7e8d87590..33d78a2e3a 100644 --- a/internal/api/scim/integration_test/testdata/users_replace_test_minimal_with_email_type.json +++ b/internal/api/scim/integration_test/testdata/users_replace_test_minimal_with_email_type.json @@ -2,14 +2,14 @@ "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:User" ], - "userName": "acmeUser1-minimal-replaced", + "userName": "{{ .Username }}", "name": { "familyName": "Ross-replaced", "givenName": "Bethany-replaced" }, "emails": [ { - "value": "user1-minimal-replaced@example.com", + "value": "{{ .Username }}@example.com", "primary": true, "type": "work" } diff --git a/internal/api/scim/integration_test/testdata/users_update_test_full.json b/internal/api/scim/integration_test/testdata/users_update_test_full.json index 23403f3e5b..f79a0b5332 100644 --- a/internal/api/scim/integration_test/testdata/users_update_test_full.json +++ b/internal/api/scim/integration_test/testdata/users_update_test_full.json @@ -7,7 +7,7 @@ "value": { "emails":[ { - "value":"babs@example.com", + "value":"{{ .Username }}+2@example.com", "type":"home", "primary": true } diff --git a/internal/api/scim/integration_test/users_create_test.go b/internal/api/scim/integration_test/users_create_test.go index 35d5297878..4d1d9268ce 100644 --- a/internal/api/scim/integration_test/users_create_test.go +++ b/internal/api/scim/integration_test/users_create_test.go @@ -3,17 +3,22 @@ package integration_test import ( + "bytes" "context" _ "embed" + "fmt" "net/http" "path" "testing" + "text/template" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/text/language" "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "github.com/zitadel/zitadel/internal/api/scim/resources" "github.com/zitadel/zitadel/internal/api/scim/schemas" @@ -157,7 +162,18 @@ var ( } ) +func withUsername(fixture []byte, username string) []byte { + buf := new(bytes.Buffer) + template.Must(template.New("").Parse(string(fixture))).Execute(buf, &struct { + Username string + }{ + Username: username, + }) + return buf.Bytes() +} + func TestCreateUser(t *testing.T) { + minimalUsername := gofakeit.Username() tests := []struct { name string body []byte @@ -171,16 +187,16 @@ func TestCreateUser(t *testing.T) { }{ { name: "minimal user", - body: minimalUserJson, + body: withUsername(minimalUserJson, minimalUsername), want: &resources.ScimUser{ - UserName: "acmeUser1", + UserName: minimalUsername, Name: &resources.ScimUserName{ FamilyName: "Ross", GivenName: "Bethany", }, Emails: []*resources.ScimEmail{ { - Value: "user1@example.com", + Value: minimalUsername + "@example.com", Primary: true, }, }, @@ -195,7 +211,7 @@ func TestCreateUser(t *testing.T) { }, { name: "full user", - body: fullUserJson, + body: withUsername(fullUserJson, "bjensen"), want: fullUser, }, { @@ -204,7 +220,7 @@ func TestCreateUser(t *testing.T) { want: &resources.ScimUser{ Emails: []*resources.ScimEmail{ { - Value: "user1@example.com", + Value: "user1-no-primary-email-phone@example.com", Primary: true, }, }, @@ -262,21 +278,21 @@ func TestCreateUser(t *testing.T) { }, { name: "not authenticated", - body: minimalUserJson, + body: withUsername(minimalUserJson, gofakeit.Username()), ctx: context.Background(), wantErr: true, errorStatus: http.StatusUnauthorized, }, { name: "no permissions", - body: minimalUserJson, + body: withUsername(minimalUserJson, gofakeit.Username()), ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), wantErr: true, errorStatus: http.StatusNotFound, }, { name: "another org", - body: minimalUserJson, + body: withUsername(minimalUserJson, gofakeit.Username()), orgID: SecondaryOrganization.OrganizationId, wantErr: true, errorStatus: http.StatusNotFound, @@ -315,11 +331,6 @@ func TestCreateUser(t *testing.T) { } assert.NotEmpty(t, createdUser.ID) - defer func() { - _, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID}) - assert.NoError(t, err) - }() - assert.EqualValues(t, []schemas.ScimSchemaType{"urn:ietf:params:scim:schemas:core:2.0:User"}, createdUser.Resource.Schemas) assert.Equal(t, schemas.ScimResourceTypeSingular("User"), createdUser.Resource.Meta.ResourceType) assert.Equal(t, "http://"+Instance.Host()+path.Join(schemas.HandlerPrefix, orgID, "Users", createdUser.ID), createdUser.Resource.Meta.Location) @@ -345,10 +356,11 @@ func TestCreateUser(t *testing.T) { } func TestCreateUser_duplicate(t *testing.T) { - createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, minimalUserJson) + parsedMinimalUserJson := withUsername(minimalUserJson, gofakeit.Username()) + createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, parsedMinimalUserJson) require.NoError(t, err) - _, err = Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, minimalUserJson) + _, err = Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, parsedMinimalUserJson) scimErr := scim.RequireScimError(t, http.StatusConflict, err) assert.Equal(t, "User already exists", scimErr.Error.Detail) assert.Equal(t, "uniqueness", scimErr.Error.ScimType) @@ -358,14 +370,10 @@ func TestCreateUser_duplicate(t *testing.T) { } func TestCreateUser_metadata(t *testing.T) { - createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) + username := gofakeit.Username() + createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) require.NoError(t, err) - defer func() { - _, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID}) - require.NoError(t, err) - }() - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) require.EventuallyWithT(t, func(tt *assert.CollectT) { md, err := Instance.Client.Mgmt.ListUserMetadata(CTX, &management.ListUserMetadataRequest{ @@ -391,38 +399,36 @@ func TestCreateUser_metadata(t *testing.T) { test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:locale", "en-US") test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:ims", `[{"value":"someaimhandle","type":"aim"},{"value":"twitterhandle","type":"X"}]`) test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:roles", `[{"value":"my-role-1","display":"Rolle 1","type":"main-role","primary":true},{"value":"my-role-2","display":"Rolle 2","type":"secondary-role"}]`) - test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", `[{"value":"bjensen@example.com","primary":true,"type":"work"},{"value":"babs@jensen.org","primary":false,"type":"home"}]`) + test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", fmt.Sprintf(`[{"value":"%s@example.com","primary":true,"type":"work"},{"value":"%s+1@example.com","primary":false,"type":"home"}]`, username, username)) }, retryDuration, tick) } func TestCreateUser_scopedExternalID(t *testing.T) { - setProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID, "fooBar") - - createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) + callingUserId, callingUserPat, err := Instance.CreateMachineUserPATWithMembership(CTX, "ORG_OWNER") require.NoError(t, err) - - defer func() { - _, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID}) - require.NoError(t, err) - - removeProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID) - }() - - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + ctx := integration.WithAuthorizationToken(CTX, callingUserPat) + setProvisioningDomain(t, callingUserId, "fooBar") + createdUser, err := Instance.Client.SCIM.Users.Create(ctx, Instance.DefaultOrg.Id, withUsername(fullUserJson, gofakeit.Username())) + require.NoError(t, err) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) require.EventuallyWithT(t, func(tt *assert.CollectT) { // unscoped externalID should not exist - _, err = Instance.Client.Mgmt.GetUserMetadata(CTX, &management.GetUserMetadataRequest{ + unscoped, err := Instance.Client.Mgmt.GetUserMetadata(ctx, &management.GetUserMetadataRequest{ Id: createdUser.ID, Key: "urn:zitadel:scim:externalId", }) integration.AssertGrpcStatus(tt, codes.NotFound, err) + unscoped = unscoped // scoped externalID should exist - md, err := Instance.Client.Mgmt.GetUserMetadata(CTX, &management.GetUserMetadataRequest{ + md, err := Instance.Client.Mgmt.GetUserMetadata(ctx, &management.GetUserMetadataRequest{ Id: createdUser.ID, Key: "urn:zitadel:scim:fooBar:externalId", }) - require.NoError(tt, err) + if !assert.NoError(tt, err) { + require.Equal(tt, status.Code(err), codes.NotFound) + return + } assert.Equal(tt, "701984", string(md.Metadata.Value)) }, retryDuration, tick) } diff --git a/internal/api/scim/integration_test/users_get_test.go b/internal/api/scim/integration_test/users_get_test.go index 8a1bab6c93..08e09946fb 100644 --- a/internal/api/scim/integration_test/users_get_test.go +++ b/internal/api/scim/integration_test/users_get_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/text/language" @@ -18,256 +19,260 @@ import ( "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/internal/integration/scim" "github.com/zitadel/zitadel/internal/test" - "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func TestGetUser(t *testing.T) { - tests := []struct { - name string - orgID string - buildUserID func() string - cleanup func(userID string) + type testCase struct { ctx context.Context + orgID string + userID string want *resources.ScimUser wantErr bool errorStatus int + } + tests := []struct { + name string + setup func(t *testing.T) testCase }{ { - name: "not authenticated", - ctx: context.Background(), - errorStatus: http.StatusUnauthorized, - wantErr: true, + name: "not authenticated", + setup: func(t *testing.T) testCase { + return testCase{ + ctx: context.Background(), + errorStatus: http.StatusUnauthorized, + wantErr: true, + } + }, }, { - name: "no permissions", - ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), - errorStatus: http.StatusNotFound, - wantErr: true, + name: "no permissions", + setup: func(t *testing.T) testCase { + return testCase{ + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + errorStatus: http.StatusNotFound, + wantErr: true, + } + }, }, { - name: "another org", - orgID: SecondaryOrganization.OrganizationId, - errorStatus: http.StatusNotFound, - wantErr: true, + name: "another org", + setup: func(t *testing.T) testCase { + return testCase{ + orgID: SecondaryOrganization.OrganizationId, + errorStatus: http.StatusNotFound, + wantErr: true, + } + }, }, { - name: "another org with permissions", - orgID: SecondaryOrganization.OrganizationId, - ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), - errorStatus: http.StatusNotFound, - wantErr: true, + name: "another org with permissions", + setup: func(t *testing.T) testCase { + return testCase{ + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + orgID: SecondaryOrganization.OrganizationId, + errorStatus: http.StatusNotFound, + wantErr: true, + } + }, }, { name: "unknown user id", - buildUserID: func() string { - return "unknown" + setup: func(t *testing.T) testCase { + return testCase{ + userID: "unknown", + errorStatus: http.StatusNotFound, + wantErr: true, + } }, - errorStatus: http.StatusNotFound, - wantErr: true, }, { name: "created via grpc", - want: &resources.ScimUser{ - Name: &resources.ScimUserName{ - FamilyName: "Mouse", - GivenName: "Mickey", - }, - PreferredLanguage: language.MustParse("nl"), - PhoneNumbers: []*resources.ScimPhoneNumber{ - { - Value: "+41791234567", - Primary: true, + setup: func(t *testing.T) testCase { + return testCase{ + want: &resources.ScimUser{ + Name: &resources.ScimUserName{ + FamilyName: "Mouse", + GivenName: "Mickey", + }, + PreferredLanguage: language.MustParse("nl"), + PhoneNumbers: []*resources.ScimPhoneNumber{ + { + Value: "+41791234567", + Primary: true, + }, + }, }, - }, + } }, }, { name: "created via scim", - buildUserID: func() string { - createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) require.NoError(t, err) - return createdUser.ID - }, - cleanup: func(userID string) { - _, err := Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: userID}) - require.NoError(t, err) - }, - want: &resources.ScimUser{ - ExternalID: "701984", - UserName: "bjensen@example.com", - Name: &resources.ScimUserName{ - Formatted: "Babs Jensen", // DisplayName takes precedence - FamilyName: "Jensen", - GivenName: "Barbara", - MiddleName: "Jane", - HonorificPrefix: "Ms.", - HonorificSuffix: "III", - }, - DisplayName: "Babs Jensen", - NickName: "Babs", - ProfileUrl: test.Must(schemas.ParseHTTPURL("http://login.example.com/bjensen")), - Title: "Tour Guide", - PreferredLanguage: language.Make("en-US"), - Locale: "en-US", - Timezone: "America/Los_Angeles", - Active: schemas.NewRelaxedBool(true), - Emails: []*resources.ScimEmail{ - { - Value: "bjensen@example.com", - Primary: true, - Type: "work", + return testCase{ + userID: createdUser.ID, + want: &resources.ScimUser{ + ExternalID: "701984", + UserName: username + "@example.com", + Name: &resources.ScimUserName{ + Formatted: "Babs Jensen", // DisplayName takes precedence + FamilyName: "Jensen", + GivenName: "Barbara", + MiddleName: "Jane", + HonorificPrefix: "Ms.", + HonorificSuffix: "III", + }, + DisplayName: "Babs Jensen", + NickName: "Babs", + ProfileUrl: test.Must(schemas.ParseHTTPURL("http://login.example.com/bjensen")), + Title: "Tour Guide", + PreferredLanguage: language.Make("en-US"), + Locale: "en-US", + Timezone: "America/Los_Angeles", + Active: schemas.NewRelaxedBool(true), + Emails: []*resources.ScimEmail{ + { + Value: username + "@example.com", + Primary: true, + Type: "work", + }, + }, + PhoneNumbers: []*resources.ScimPhoneNumber{ + { + Value: "+415555555555", + Primary: true, + }, + }, + Ims: []*resources.ScimIms{ + { + Value: "someaimhandle", + Type: "aim", + }, + { + Value: "twitterhandle", + Type: "X", + }, + }, + Addresses: []*resources.ScimAddress{ + { + Type: "work", + StreetAddress: "100 Universal City Plaza", + Locality: "Hollywood", + Region: "CA", + PostalCode: "91608", + Country: "USA", + Formatted: "100 Universal City Plaza\nHollywood, CA 91608 USA", + Primary: true, + }, + { + Type: "home", + StreetAddress: "456 Hollywood Blvd", + Locality: "Hollywood", + Region: "CA", + PostalCode: "91608", + Country: "USA", + Formatted: "456 Hollywood Blvd\nHollywood, CA 91608 USA", + }, + }, + Photos: []*resources.ScimPhoto{ + { + Value: *test.Must(schemas.ParseHTTPURL("https://photos.example.com/profilephoto/72930000000Ccne/F")), + Type: "photo", + }, + { + Value: *test.Must(schemas.ParseHTTPURL("https://photos.example.com/profilephoto/72930000000Ccne/T")), + Type: "thumbnail", + }, + }, + Roles: []*resources.ScimRole{ + { + Value: "my-role-1", + Display: "Rolle 1", + Type: "main-role", + Primary: true, + }, + { + Value: "my-role-2", + Display: "Rolle 2", + Type: "secondary-role", + Primary: false, + }, + }, + Entitlements: []*resources.ScimEntitlement{ + { + Value: "my-entitlement-1", + Display: "Entitlement 1", + Type: "main-entitlement", + Primary: true, + }, + { + Value: "my-entitlement-2", + Display: "Entitlement 2", + Type: "secondary-entitlement", + Primary: false, + }, + }, }, - }, - PhoneNumbers: []*resources.ScimPhoneNumber{ - { - Value: "+415555555555", - Primary: true, - }, - }, - Ims: []*resources.ScimIms{ - { - Value: "someaimhandle", - Type: "aim", - }, - { - Value: "twitterhandle", - Type: "X", - }, - }, - Addresses: []*resources.ScimAddress{ - { - Type: "work", - StreetAddress: "100 Universal City Plaza", - Locality: "Hollywood", - Region: "CA", - PostalCode: "91608", - Country: "USA", - Formatted: "100 Universal City Plaza\nHollywood, CA 91608 USA", - Primary: true, - }, - { - Type: "home", - StreetAddress: "456 Hollywood Blvd", - Locality: "Hollywood", - Region: "CA", - PostalCode: "91608", - Country: "USA", - Formatted: "456 Hollywood Blvd\nHollywood, CA 91608 USA", - }, - }, - Photos: []*resources.ScimPhoto{ - { - Value: *test.Must(schemas.ParseHTTPURL("https://photos.example.com/profilephoto/72930000000Ccne/F")), - Type: "photo", - }, - { - Value: *test.Must(schemas.ParseHTTPURL("https://photos.example.com/profilephoto/72930000000Ccne/T")), - Type: "thumbnail", - }, - }, - Roles: []*resources.ScimRole{ - { - Value: "my-role-1", - Display: "Rolle 1", - Type: "main-role", - Primary: true, - }, - { - Value: "my-role-2", - Display: "Rolle 2", - Type: "secondary-role", - Primary: false, - }, - }, - Entitlements: []*resources.ScimEntitlement{ - { - Value: "my-entitlement-1", - Display: "Entitlement 1", - Type: "main-entitlement", - Primary: true, - }, - { - Value: "my-entitlement-2", - Display: "Entitlement 2", - Type: "secondary-entitlement", - Primary: false, - }, - }, + } }, }, { name: "scoped externalID", - buildUserID: func() string { - // create user without provisioning domain - createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) + setup: func(t *testing.T) testCase { + createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, gofakeit.Username())) require.NoError(t, err) - - // set provisioning domain of service user - setProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID, "fooBar") - - // set externalID for provisioning domain + callingUserId, callingUserPat, err := Instance.CreateMachineUserPATWithMembership(CTX, "ORG_OWNER") + require.NoError(t, err) + setProvisioningDomain(t, callingUserId, "fooBar") setAndEnsureMetadata(t, createdUser.ID, "urn:zitadel:scim:fooBar:externalId", "100-scopedExternalId") - return createdUser.ID - }, - cleanup: func(userID string) { - _, err := Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: userID}) - require.NoError(t, err) - - removeProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID) - }, - want: &resources.ScimUser{ - ExternalID: "100-scopedExternalId", + return testCase{ + ctx: integration.WithAuthorizationToken(CTX, callingUserPat), + userID: createdUser.ID, + want: &resources.ScimUser{ + ExternalID: "100-scopedExternalId", + }, + } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx := tt.ctx - if ctx == nil { - ctx = CTX + ttt := tt.setup(t) + if ttt.userID == "" { + ttt.userID = Instance.CreateHumanUser(CTX).UserId } - - var userID string - if tt.buildUserID != nil { - userID = tt.buildUserID() - } else { - createUserResp := Instance.CreateHumanUser(CTX) - userID = createUserResp.UserId + if ttt.ctx == nil { + ttt.ctx = CTX } - - orgID := tt.orgID - if orgID == "" { - orgID = Instance.DefaultOrg.Id + if ttt.orgID == "" { + ttt.orgID = Instance.DefaultOrg.Id } - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) - var fetchedUser *resources.ScimUser - var err error - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - fetchedUser, err = Instance.Client.SCIM.Users.Get(ctx, orgID, userID) - if tt.wantErr { - statusCode := tt.errorStatus + require.EventuallyWithT(t, func(collect *assert.CollectT) { + fetchedUser, err := Instance.Client.SCIM.Users.Get(ttt.ctx, ttt.orgID, ttt.userID) + if ttt.wantErr { + statusCode := ttt.errorStatus if statusCode == 0 { statusCode = http.StatusBadRequest } - - scim.RequireScimError(ttt, statusCode, err) + scim.RequireScimError(collect, statusCode, err) return } - - assert.Equal(ttt, userID, fetchedUser.ID) - assert.EqualValues(ttt, []schemas.ScimSchemaType{"urn:ietf:params:scim:schemas:core:2.0:User"}, fetchedUser.Schemas) - assert.Equal(ttt, schemas.ScimResourceTypeSingular("User"), fetchedUser.Resource.Meta.ResourceType) - assert.Equal(ttt, "http://"+Instance.Host()+path.Join(schemas.HandlerPrefix, orgID, "Users", fetchedUser.ID), fetchedUser.Resource.Meta.Location) - assert.Nil(ttt, fetchedUser.Password) - if !test.PartiallyDeepEqual(tt.want, fetchedUser) { - ttt.Errorf("GetUser() got = %#v, want %#v", fetchedUser, tt.want) + if !assert.NoError(collect, err) { + scim.RequireScimError(collect, http.StatusNotFound, err) + return + } + assert.Equal(collect, ttt.userID, fetchedUser.ID) + assert.EqualValues(collect, []schemas.ScimSchemaType{"urn:ietf:params:scim:schemas:core:2.0:User"}, fetchedUser.Schemas) + assert.Equal(collect, schemas.ScimResourceTypeSingular("User"), fetchedUser.Resource.Meta.ResourceType) + assert.Equal(collect, "http://"+Instance.Host()+path.Join(schemas.HandlerPrefix, ttt.orgID, "Users", fetchedUser.ID), fetchedUser.Resource.Meta.Location) + assert.Nil(collect, fetchedUser.Password) + if !test.PartiallyDeepEqual(ttt.want, fetchedUser) { + collect.Errorf("GetUser() got = %#v, want %#v", fetchedUser, ttt.want) } }, retryDuration, tick) - - if tt.cleanup != nil { - tt.cleanup(fetchedUser.ID) - } }) } } diff --git a/internal/api/scim/integration_test/users_list_test.go b/internal/api/scim/integration_test/users_list_test.go index 8c6ccb80ef..81fbf1a5bc 100644 --- a/internal/api/scim/integration_test/users_list_test.go +++ b/internal/api/scim/integration_test/users_list_test.go @@ -23,15 +23,6 @@ var totalCountOfHumanUsers = 13 /* func TestListUser(t *testing.T) { createdUserIDs := createUsers(t, CTX, Instance.DefaultOrg.Id) - defer func() { - // only the full user needs to be deleted, all others have random identification data - // fullUser is always the first one. - _, err := Instance.Client.UserV2.DeleteUser(CTX, &user_v2.DeleteUserRequest{ - UserId: createdUserIDs[0], - }) - require.NoError(t, err) - }() - // secondary organization with same set of users, // these should never be modified. // This allows testing list requests without filters. @@ -451,7 +442,7 @@ func createUsers(t *testing.T, ctx context.Context, orgID string) []string { // create the full scim user if on primary org if orgID == Instance.DefaultOrg.Id { - fullUserCreatedResp, err := Instance.Client.SCIM.Users.Create(ctx, orgID, fullUserJson) + fullUserCreatedResp, err := Instance.Client.SCIM.Users.Create(ctx, orgID, withUsername(fullUserJson, gofakeit.Username())) require.NoError(t, err) createdUserIDs = append(createdUserIDs, fullUserCreatedResp.ID) count-- diff --git a/internal/api/scim/integration_test/users_replace_test.go b/internal/api/scim/integration_test/users_replace_test.go index 1c99592b01..7896ed4e00 100644 --- a/internal/api/scim/integration_test/users_replace_test.go +++ b/internal/api/scim/integration_test/users_replace_test.go @@ -5,11 +5,13 @@ package integration_test import ( "context" _ "embed" + "fmt" "net/http" "path" "testing" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/text/language" @@ -20,7 +22,6 @@ import ( "github.com/zitadel/zitadel/internal/integration/scim" "github.com/zitadel/zitadel/internal/test" "github.com/zitadel/zitadel/pkg/grpc/management" - "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) var ( @@ -199,28 +200,28 @@ func TestReplaceUser(t *testing.T) { }, { name: "not authenticated", - body: minimalUserJson, + body: withUsername(minimalUserJson, gofakeit.Username()), ctx: context.Background(), wantErr: true, errorStatus: http.StatusUnauthorized, }, { name: "no permissions", - body: minimalUserJson, + body: withUsername(minimalUserJson, gofakeit.Username()), ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), wantErr: true, errorStatus: http.StatusNotFound, }, { name: "another org", - body: minimalUserJson, + body: withUsername(minimalUserJson, gofakeit.Username()), replaceUserOrgID: SecondaryOrganization.OrganizationId, wantErr: true, errorStatus: http.StatusNotFound, }, { name: "another org with permissions", - body: minimalUserJson, + body: withUsername(minimalUserJson, gofakeit.Username()), replaceUserOrgID: SecondaryOrganization.OrganizationId, ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), wantErr: true, @@ -230,14 +231,9 @@ func TestReplaceUser(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // use iam owner => we don't want to test permissions of the create endpoint. - createdUser, err := Instance.Client.SCIM.Users.Create(Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), Instance.DefaultOrg.Id, fullUserJson) + createdUser, err := Instance.Client.SCIM.Users.Create(Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), Instance.DefaultOrg.Id, withUsername(fullUserJson, gofakeit.Username())) require.NoError(t, err) - defer func() { - _, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID}) - assert.NoError(t, err) - }() - ctx := tt.ctx if ctx == nil { ctx = CTX @@ -294,10 +290,11 @@ func TestReplaceUser(t *testing.T) { func TestReplaceUser_removeOldMetadata(t *testing.T) { // ensure old metadata is removed correctly - createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) + username := gofakeit.Username() + createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) require.NoError(t, err) - _, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, minimalUserJson) + _, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, withUsername(minimalUserJson, username)) require.NoError(t, err) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) @@ -312,20 +309,17 @@ func TestReplaceUser_removeOldMetadata(t *testing.T) { for i := range md.Result { mdMap[md.Result[i].Key] = string(md.Result[i].Value) } - - test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", "[{\"value\":\"user1@example.com\",\"primary\":true}]") + test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", fmt.Sprintf("[{\"value\":\"%s@example.com\",\"primary\":true}]", username)) }, retryDuration, tick) - - _, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID}) - require.NoError(t, err) } func TestReplaceUser_emailType(t *testing.T) { // ensure old metadata is removed correctly - createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) + createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, gofakeit.Username())) require.NoError(t, err) - _, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, minimalUserWithEmailTypeReplaceJson) + replacedUsername := gofakeit.Username() + _, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, withUsername(minimalUserWithEmailTypeReplaceJson, replacedUsername)) require.NoError(t, err) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) @@ -341,28 +335,26 @@ func TestReplaceUser_emailType(t *testing.T) { mdMap[md.Result[i].Key] = string(md.Result[i].Value) } - test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", "[{\"value\":\"user1-minimal-replaced@example.com\",\"primary\":true,\"type\":\"work\"}]") + test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", fmt.Sprintf("[{\"value\":\"%s@example.com\",\"primary\":true,\"type\":\"work\"}]", replacedUsername)) }, retryDuration, tick) - - _, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID}) - require.NoError(t, err) } func TestReplaceUser_scopedExternalID(t *testing.T) { - // create user without provisioning domain set - createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) + createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, gofakeit.Username())) require.NoError(t, err) - + callingUserId, callingUserPat, err := Instance.CreateMachineUserPATWithMembership(CTX, "ORG_OWNER") + require.NoError(t, err) + ctx := integration.WithAuthorizationToken(CTX, callingUserPat) // set provisioning domain of service user - setProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID, "fooBazz") + setProvisioningDomain(t, callingUserId, "fooBazz") // replace the user with provisioning domain set - _, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, minimalUserWithExternalIDJson) + _, err = Instance.Client.SCIM.Users.Replace(ctx, Instance.DefaultOrg.Id, createdUser.ID, minimalUserWithExternalIDJson) require.NoError(t, err) - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) require.EventuallyWithT(t, func(tt *assert.CollectT) { - md, err := Instance.Client.Mgmt.ListUserMetadata(CTX, &management.ListUserMetadataRequest{ + md, err := Instance.Client.Mgmt.ListUserMetadata(ctx, &management.ListUserMetadataRequest{ Id: createdUser.ID, }) require.NoError(tt, err) @@ -376,9 +368,4 @@ func TestReplaceUser_scopedExternalID(t *testing.T) { test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:externalId", "701984") test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:fooBazz:externalId", "replaced-external-id") }, retryDuration, tick) - - _, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID}) - require.NoError(t, err) - - removeProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID) } diff --git a/internal/api/scim/integration_test/users_update_test.go b/internal/api/scim/integration_test/users_update_test.go index 77e55bac60..f8a65a8a69 100644 --- a/internal/api/scim/integration_test/users_update_test.go +++ b/internal/api/scim/integration_test/users_update_test.go @@ -5,11 +5,13 @@ package integration_test import ( "context" _ "embed" + "encoding/json" "fmt" "net/http" "testing" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/text/language" @@ -19,7 +21,6 @@ import ( "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/internal/integration/scim" "github.com/zitadel/zitadel/internal/test" - "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) var ( @@ -34,15 +35,7 @@ func init() { } func TestUpdateUser(t *testing.T) { - fullUserCreated, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) - require.NoError(t, err) - - defer func() { - _, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: fullUserCreated.ID}) - require.NoError(t, err) - }() - - tests := []struct { + type testCase struct { name string body []byte ctx context.Context @@ -52,215 +45,297 @@ func TestUpdateUser(t *testing.T) { wantErr bool scimErrorType string errorStatus int + } + tests := []struct { + name string + setup func(t *testing.T) testCase }{ { - name: "not authenticated", - ctx: context.Background(), - body: minimalUserUpdateJson, - wantErr: true, - errorStatus: http.StatusUnauthorized, + name: "not authenticated", + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) + require.NoError(t, err) + return testCase{ + userID: created.ID, + ctx: context.Background(), + body: minimalUserUpdateJson, + wantErr: true, + errorStatus: http.StatusUnauthorized, + } + }, }, { - name: "no permissions", - ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), - body: minimalUserUpdateJson, - wantErr: true, - errorStatus: http.StatusNotFound, + name: "no permissions", + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) + require.NoError(t, err) + return testCase{ + userID: created.ID, + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + body: minimalUserUpdateJson, + wantErr: true, + errorStatus: http.StatusNotFound, + } + }, }, { - name: "other org", - orgID: SecondaryOrganization.OrganizationId, - body: minimalUserUpdateJson, - wantErr: true, - errorStatus: http.StatusNotFound, + name: "other org", + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) + require.NoError(t, err) + return testCase{ + userID: created.ID, + orgID: SecondaryOrganization.OrganizationId, + body: minimalUserUpdateJson, + wantErr: true, + errorStatus: http.StatusNotFound, + } + }, }, { - name: "other org with permissions", - ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), - orgID: SecondaryOrganization.OrganizationId, - body: minimalUserUpdateJson, - wantErr: true, - errorStatus: http.StatusNotFound, + name: "other org with permissions", + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) + require.NoError(t, err) + return testCase{ + userID: created.ID, + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + orgID: SecondaryOrganization.OrganizationId, + body: minimalUserUpdateJson, + wantErr: true, + errorStatus: http.StatusNotFound, + } + }, }, { - name: "invalid patch json", - body: simpleReplacePatchBody("nickname", "10"), - wantErr: true, - scimErrorType: "invalidValue", + name: "invalid patch json", + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) + require.NoError(t, err) + return testCase{ + userID: created.ID, + body: simpleReplacePatchBody("nickname", "10"), + wantErr: true, + scimErrorType: "invalidValue", + } + }, }, { - name: "password complexity violation", - body: simpleReplacePatchBody("password", `"fooBar"`), - wantErr: true, - scimErrorType: "invalidValue", + name: "password complexity violation", + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) + require.NoError(t, err) + return testCase{ + userID: created.ID, + body: simpleReplacePatchBody("password", `"fooBar"`), + wantErr: true, + scimErrorType: "invalidValue", + } + }, }, { - name: "invalid profile url", - body: simpleReplacePatchBody("profileUrl", `"ftp://example.com/profiles"`), - wantErr: true, - scimErrorType: "invalidValue", + name: "invalid profile url", + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) + require.NoError(t, err) + return testCase{ + userID: created.ID, + body: simpleReplacePatchBody("profileUrl", `"ftp://example.com/profiles"`), + wantErr: true, + scimErrorType: "invalidValue", + } + }, }, { - name: "invalid time zone", - body: simpleReplacePatchBody("timezone", `"foobar"`), - wantErr: true, - scimErrorType: "invalidValue", + name: "invalid time zone", + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) + require.NoError(t, err) + return testCase{ + userID: created.ID, + body: simpleReplacePatchBody("timezone", `"foobar"`), + wantErr: true, + scimErrorType: "invalidValue", + } + }, }, { - name: "invalid locale", - body: simpleReplacePatchBody("locale", `"foobar"`), - wantErr: true, - scimErrorType: "invalidValue", + name: "invalid locale", + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) + require.NoError(t, err) + return testCase{ + userID: created.ID, + body: simpleReplacePatchBody("locale", `"foobar"`), + wantErr: true, + scimErrorType: "invalidValue", + } + }, }, { - name: "unknown user id", - body: simpleReplacePatchBody("nickname", `"foo"`), - userID: "fooBar", - wantErr: true, - errorStatus: http.StatusNotFound, + name: "unknown user id", + setup: func(t *testing.T) testCase { + return testCase{ + body: simpleReplacePatchBody("nickname", `"foo"`), + userID: "fooBar", + wantErr: true, + errorStatus: http.StatusNotFound, + } + }, }, { name: "full", - body: fullUserUpdateJson, - want: &resources.ScimUser{ - ExternalID: "fooBAR", - UserName: "bjensen@example.com", - Name: &resources.ScimUserName{ - Formatted: "replaced-display-name", - FamilyName: "added-family-name", - GivenName: "added-given-name", - MiddleName: "added-middle-name-2", - HonorificPrefix: "added-honorific-prefix", - HonorificSuffix: "replaced-honorific-suffix", - }, - DisplayName: "replaced-display-name", - NickName: "", - ProfileUrl: test.Must(schemas.ParseHTTPURL("http://login.example.com/bjensen")), - Emails: []*resources.ScimEmail{ - { - Value: "bjensen@example.com", - Type: "work", + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) + require.NoError(t, err) + return testCase{ + userID: created.ID, + body: withUsername(fullUserUpdateJson, username), + want: &resources.ScimUser{ + ExternalID: "fooBAR", + UserName: username + "@example.com", + Name: &resources.ScimUserName{ + Formatted: "replaced-display-name", + FamilyName: "added-family-name", + GivenName: "added-given-name", + MiddleName: "added-middle-name-2", + HonorificPrefix: "added-honorific-prefix", + HonorificSuffix: "replaced-honorific-suffix", + }, + DisplayName: "replaced-display-name", + NickName: "", + ProfileUrl: test.Must(schemas.ParseHTTPURL("http://login.example.com/bjensen")), + Emails: []*resources.ScimEmail{ + { + Value: username + "@example.com", + Type: "work", + }, + { + Value: username + "+1@example.com", + Type: "home", + }, + { + Value: username + "+2@example.com", + Primary: true, + Type: "home", + }, + }, + Addresses: []*resources.ScimAddress{ + { + Type: "replaced-work", + StreetAddress: "replaced-100 Universal City Plaza", + Locality: "replaced-Hollywood", + Region: "replaced-CA", + PostalCode: "replaced-91608", + Country: "replaced-USA", + Formatted: "replaced-100 Universal City Plaza\nHollywood, CA 91608 USA", + Primary: true, + }, + }, + PhoneNumbers: []*resources.ScimPhoneNumber{ + { + Value: "+41711234567", + Primary: true, + }, + }, + Ims: []*resources.ScimIms{ + { + Value: "someaimhandle", + Type: "aim", + }, + { + Value: "twitterhandle", + Type: "", + }, + }, + Photos: []*resources.ScimPhoto{ + { + Value: *test.Must(schemas.ParseHTTPURL("https://photos.example.com/profilephoto/72930000000Ccne/F")), + Type: "photo", + }, + }, + Roles: nil, + Entitlements: []*resources.ScimEntitlement{ + { + Value: "my-entitlement-1", + Display: "added-entitlement-1", + Type: "added-entitlement-1", + Primary: false, + }, + { + Value: "my-entitlement-2", + Display: "Entitlement 2", + Type: "secondary-entitlement", + Primary: false, + }, + { + Value: "added-entitlement-1", + Primary: false, + }, + { + Value: "added-entitlement-2", + Primary: false, + }, + { + Value: "added-entitlement-3", + Primary: true, + }, + }, + Title: "Tour Guide", + PreferredLanguage: language.MustParse("en"), + Locale: "en-US", + Timezone: "America/Los_Angeles", + Active: schemas.NewRelaxedBool(true), }, - { - Value: "babs@jensen.org", - Type: "home", - }, - { - Value: "babs@example.com", - Primary: true, - Type: "home", - }, - }, - Addresses: []*resources.ScimAddress{ - { - Type: "replaced-work", - StreetAddress: "replaced-100 Universal City Plaza", - Locality: "replaced-Hollywood", - Region: "replaced-CA", - PostalCode: "replaced-91608", - Country: "replaced-USA", - Formatted: "replaced-100 Universal City Plaza\nHollywood, CA 91608 USA", - Primary: true, - }, - }, - PhoneNumbers: []*resources.ScimPhoneNumber{ - { - Value: "+41711234567", - Primary: true, - }, - }, - Ims: []*resources.ScimIms{ - { - Value: "someaimhandle", - Type: "aim", - }, - { - Value: "twitterhandle", - Type: "", - }, - }, - Photos: []*resources.ScimPhoto{ - { - Value: *test.Must(schemas.ParseHTTPURL("https://photos.example.com/profilephoto/72930000000Ccne/F")), - Type: "photo", - }, - }, - Roles: nil, - Entitlements: []*resources.ScimEntitlement{ - { - Value: "my-entitlement-1", - Display: "added-entitlement-1", - Type: "added-entitlement-1", - Primary: false, - }, - { - Value: "my-entitlement-2", - Display: "Entitlement 2", - Type: "secondary-entitlement", - Primary: false, - }, - { - Value: "added-entitlement-1", - Primary: false, - }, - { - Value: "added-entitlement-2", - Primary: false, - }, - { - Value: "added-entitlement-3", - Primary: true, - }, - }, - Title: "Tour Guide", - PreferredLanguage: language.MustParse("en-US"), - Locale: "en-US", - Timezone: "America/Los_Angeles", - Active: schemas.NewRelaxedBool(true), + } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.ctx == nil { - tt.ctx = CTX + ttt := tt.setup(t) + if ttt.orgID == "" { + ttt.orgID = Instance.DefaultOrg.Id } - - if tt.orgID == "" { - tt.orgID = Instance.DefaultOrg.Id + if ttt.ctx == nil { + ttt.ctx = CTX } - - if tt.userID == "" { - tt.userID = fullUserCreated.ID - } - - err := Instance.Client.SCIM.Users.Update(tt.ctx, tt.orgID, tt.userID, tt.body) - - if tt.wantErr { + err := Instance.Client.SCIM.Users.Update(ttt.ctx, ttt.orgID, ttt.userID, ttt.body) + if ttt.wantErr { require.Error(t, err) - - statusCode := tt.errorStatus + statusCode := ttt.errorStatus if statusCode == 0 { statusCode = http.StatusBadRequest } - scimErr := scim.RequireScimError(t, statusCode, err) - assert.Equal(t, tt.scimErrorType, scimErr.Error.ScimType) + assert.Equal(t, ttt.scimErrorType, scimErr.Error.ScimType) return } require.NoError(t, err) - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - fetchedUser, err := Instance.Client.SCIM.Users.Get(tt.ctx, tt.orgID, fullUserCreated.ID) - require.NoError(ttt, err) - + require.EventuallyWithT(t, func(collect *assert.CollectT) { + fetchedUser, err := Instance.Client.SCIM.Users.Get(ttt.ctx, ttt.orgID, ttt.userID) + if !assert.NoError(collect, err) { + return + } fetchedUser.Resource = nil fetchedUser.ID = "" - if tt.want != nil && !test.PartiallyDeepEqual(tt.want, fetchedUser) { - ttt.Errorf("got = %#v, want = %#v", fetchedUser, tt.want) - } + fetched, err := json.Marshal(fetchedUser) + require.NoError(collect, err) + want, err := json.Marshal(ttt.want) + require.NoError(collect, err) + assert.JSONEq(collect, string(want), string(fetched)) }, retryDuration, tick) }) } diff --git a/internal/api/scim/resources/user.go b/internal/api/scim/resources/user.go index 6506ae35c7..dbf97e0eae 100644 --- a/internal/api/scim/resources/user.go +++ b/internal/api/scim/resources/user.go @@ -262,7 +262,7 @@ func (h *UsersHandler) queryUserDependencies(ctx context.Context, userID string) grants, err := h.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{userGrantUserQuery}, - }, true) + }, true, nil) if err != nil { return nil, nil, err } diff --git a/internal/api/scim/resources/user_metadata.go b/internal/api/scim/resources/user_metadata.go index 3e018507fe..b758117ce8 100644 --- a/internal/api/scim/resources/user_metadata.go +++ b/internal/api/scim/resources/user_metadata.go @@ -45,7 +45,7 @@ func (h *UsersHandler) queryMetadataForUsers(ctx context.Context, userIds []stri func (h *UsersHandler) queryMetadataForUser(ctx context.Context, id string) (map[metadata.ScopedKey][]byte, error) { queries := h.buildMetadataQueries(ctx) - md, err := h.query.SearchUserMetadata(ctx, false, id, queries, false) + md, err := h.query.SearchUserMetadata(ctx, false, id, queries, nil) if err != nil { return nil, err } diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index abd20088ba..967d79c1a9 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -1260,7 +1260,8 @@ func (l *Login) appendUserGrants(ctx context.Context, userGrants []*domain.UserG return nil } for _, grant := range userGrants { - _, err := l.command.AddUserGrant(setContext(ctx, resourceOwner), grant, resourceOwner) + grant.ResourceOwner = resourceOwner + _, err := l.command.AddUserGrant(setContext(ctx, resourceOwner), grant, nil) if err != nil { return err } diff --git a/internal/auth/repository/eventsourcing/repository.go b/internal/auth/repository/eventsourcing/repository.go index 16bee6e7a4..81dc8abd5c 100644 --- a/internal/auth/repository/eventsourcing/repository.go +++ b/internal/auth/repository/eventsourcing/repository.go @@ -125,7 +125,7 @@ func (q queryViewWrapper) UserGrantsByProjectAndUserID(ctx context.Context, proj return nil, err } queries := &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantUserID, userGrantProjectID, activeQuery}} - grants, err := q.Queries.UserGrants(ctx, queries, true) + grants, err := q.Queries.UserGrants(ctx, queries, true, nil) if err != nil { return nil, err } diff --git a/internal/command/instance.go b/internal/command/instance.go index 9e8f3d47c7..8a686262d7 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -690,7 +690,7 @@ func setupLoginClient(commands *Commands, validations *[]preparation.Validation, func setupAdminMembers(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, orgAgg *org.Aggregate, userID string) { *validations = append(*validations, - commands.AddOrgMemberCommand(orgAgg, userID, domain.RoleOrgOwner), + commands.AddOrgMemberCommand(&AddOrgMember{orgAgg.ID, userID, []string{domain.RoleOrgOwner}}), commands.AddInstanceMemberCommand(instanceAgg, userID, domain.RoleIAMOwner), ) } diff --git a/internal/command/instance_member.go b/internal/command/instance_member.go index a33635e8f5..0657170f75 100644 --- a/internal/command/instance_member.go +++ b/internal/command/instance_member.go @@ -2,7 +2,7 @@ package command import ( "context" - "reflect" + "slices" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command/preparation" @@ -69,9 +69,19 @@ func IsInstanceMember(ctx context.Context, filter preparation.FilterToQueryReduc return isMember, nil } -func (c *Commands) AddInstanceMember(ctx context.Context, userID string, roles ...string) (*domain.Member, error) { - instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.AddInstanceMemberCommand(instanceAgg, userID, roles...)) +type AddInstanceMember struct { + InstanceID string + UserID string + Roles []string +} + +func (c *Commands) AddInstanceMember(ctx context.Context, member *AddInstanceMember) (*domain.ObjectDetails, error) { + instanceAgg := instance.NewAggregate(member.InstanceID) + if err := c.checkPermissionUpdateInstanceMember(ctx, member.InstanceID); err != nil { + return nil, err + } + //nolint:staticcheck + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.AddInstanceMemberCommand(instanceAgg, member.UserID, member.Roles...)) if err != nil { return nil, err } @@ -79,33 +89,56 @@ func (c *Commands) AddInstanceMember(ctx context.Context, userID string, roles . if err != nil { return nil, err } - addedMember := NewInstanceMemberWriteModel(ctx, userID) + addedMember := NewInstanceMemberWriteModel(member.InstanceID, member.UserID) err = AppendAndReduce(addedMember, events...) if err != nil { return nil, err } - return memberWriteModelToMember(&addedMember.MemberWriteModel), nil + return writeModelToObjectDetails(&addedMember.WriteModel), nil +} + +type ChangeInstanceMember struct { + InstanceID string + UserID string + Roles []string +} + +func (i *ChangeInstanceMember) IsValid(zitadelRoles []authz.RoleMapping) error { + if i.InstanceID == "" || i.UserID == "" || len(i.Roles) == 0 { + return zerrors.ThrowInvalidArgument(nil, "INSTANCE-LiaZi", "Errors.IAM.MemberInvalid") + } + if len(domain.CheckForInvalidRoles(i.Roles, domain.IAMRolePrefix, zitadelRoles)) > 0 { + return zerrors.ThrowInvalidArgument(nil, "INSTANCE-3m9fs", "Errors.IAM.MemberInvalid") + } + return nil } // ChangeInstanceMember updates an existing member -func (c *Commands) ChangeInstanceMember(ctx context.Context, member *domain.Member) (*domain.Member, error) { - if !member.IsIAMValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-LiaZi", "Errors.IAM.MemberInvalid") - } - if len(domain.CheckForInvalidRoles(member.Roles, domain.IAMRolePrefix, c.zitadelRoles)) > 0 { - return nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-3m9fs", "Errors.IAM.MemberInvalid") - } - - existingMember, err := c.instanceMemberWriteModelByID(ctx, member.UserID) - if err != nil { +func (c *Commands) ChangeInstanceMember(ctx context.Context, member *ChangeInstanceMember) (*domain.ObjectDetails, error) { + if err := member.IsValid(c.zitadelRoles); err != nil { return nil, err } - if reflect.DeepEqual(existingMember.Roles, member.Roles) { - return nil, zerrors.ThrowPreconditionFailed(nil, "INSTANCE-LiaZi", "Errors.IAM.Member.RolesNotChanged") + existingMember, err := c.instanceMemberWriteModelByID(ctx, member.InstanceID, member.UserID) + if err != nil { + return nil, err } - instanceAgg := InstanceAggregateFromWriteModel(&existingMember.MemberWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, instance.NewMemberChangedEvent(ctx, instanceAgg, member.UserID, member.Roles...)) + if !existingMember.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "INSTANCE-D8JxR", "Errors.NotFound") + } + if err := c.checkPermissionUpdateInstanceMember(ctx, existingMember.AggregateID); err != nil { + return nil, err + } + if slices.Compare(existingMember.Roles, member.Roles) == 0 { + return writeModelToObjectDetails(&existingMember.WriteModel), nil + } + pushedEvents, err := c.eventstore.Push(ctx, + instance.NewMemberChangedEvent(ctx, + InstanceAggregateFromWriteModel(&existingMember.WriteModel), + member.UserID, + member.Roles..., + ), + ) if err != nil { return nil, err } @@ -114,34 +147,40 @@ func (c *Commands) ChangeInstanceMember(ctx context.Context, member *domain.Memb return nil, err } - return memberWriteModelToMember(&existingMember.MemberWriteModel), nil + return writeModelToObjectDetails(&existingMember.WriteModel), nil } -func (c *Commands) RemoveInstanceMember(ctx context.Context, userID string) (*domain.ObjectDetails, error) { +func (c *Commands) RemoveInstanceMember(ctx context.Context, instanceID, userID string) (*domain.ObjectDetails, error) { if userID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-LiaZi", "Errors.IDMissing") } - memberWriteModel, err := c.instanceMemberWriteModelByID(ctx, userID) - if err != nil && !zerrors.IsNotFound(err) { - return nil, err - } - if zerrors.IsNotFound(err) { - // empty response because we have no data that match the request - return &domain.ObjectDetails{}, nil - } - - instanceAgg := InstanceAggregateFromWriteModel(&memberWriteModel.MemberWriteModel.WriteModel) - removeEvent := c.removeInstanceMember(ctx, instanceAgg, userID, false) - pushedEvents, err := c.eventstore.Push(ctx, removeEvent) + existingMember, err := c.instanceMemberWriteModelByID(ctx, instanceID, userID) if err != nil { return nil, err } - err = AppendAndReduce(memberWriteModel, pushedEvents...) + if err := c.checkPermissionDeleteInstanceMember(ctx, instanceID); err != nil { + return nil, err + } + if !existingMember.State.Exists() { + return writeModelToObjectDetails(&existingMember.WriteModel), nil + } + + pushedEvents, err := c.eventstore.Push(ctx, + c.removeInstanceMember(ctx, + InstanceAggregateFromWriteModel(&existingMember.WriteModel), + userID, + false, + ), + ) + if err != nil { + return nil, err + } + err = AppendAndReduce(existingMember, pushedEvents...) if err != nil { return nil, err } - return writeModelToObjectDetails(&memberWriteModel.MemberWriteModel.WriteModel), nil + return writeModelToObjectDetails(&existingMember.WriteModel), nil } func (c *Commands) removeInstanceMember(ctx context.Context, instanceAgg *eventstore.Aggregate, userID string, cascade bool) eventstore.Command { @@ -155,19 +194,15 @@ func (c *Commands) removeInstanceMember(ctx context.Context, instanceAgg *events } } -func (c *Commands) instanceMemberWriteModelByID(ctx context.Context, userID string) (member *InstanceMemberWriteModel, err error) { +func (c *Commands) instanceMemberWriteModelByID(ctx context.Context, instanceID, userID string) (member *InstanceMemberWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - writeModel := NewInstanceMemberWriteModel(ctx, userID) + writeModel := NewInstanceMemberWriteModel(instanceID, userID) err = c.eventstore.FilterToQueryReducer(ctx, writeModel) if err != nil { return nil, err } - if writeModel.State == domain.MemberStateUnspecified || writeModel.State == domain.MemberStateRemoved { - return nil, zerrors.ThrowNotFound(nil, "INSTANCE-D8JxR", "Errors.NotFound") - } - return writeModel, nil } diff --git a/internal/command/instance_member_model.go b/internal/command/instance_member_model.go index e987a41d97..9cd2ca26a0 100644 --- a/internal/command/instance_member_model.go +++ b/internal/command/instance_member_model.go @@ -1,9 +1,6 @@ package command import ( - "context" - - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/instance" ) @@ -12,12 +9,12 @@ type InstanceMemberWriteModel struct { MemberWriteModel } -func NewInstanceMemberWriteModel(ctx context.Context, userID string) *InstanceMemberWriteModel { +func NewInstanceMemberWriteModel(instanceID, userID string) *InstanceMemberWriteModel { return &InstanceMemberWriteModel{ MemberWriteModel{ WriteModel: eventstore.WriteModel{ - AggregateID: authz.GetInstance(ctx).InstanceID(), - ResourceOwner: authz.GetInstance(ctx).InstanceID(), + AggregateID: instanceID, + ResourceOwner: instanceID, }, UserID: userID, }, diff --git a/internal/command/instance_member_test.go b/internal/command/instance_member_test.go index 8d254c56bc..264e7d16aa 100644 --- a/internal/command/instance_member_test.go +++ b/internal/command/instance_member_test.go @@ -10,24 +10,22 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" ) -func TestCommandSide_AddIAMMember(t *testing.T) { +func TestCommandSide_AddInstanceMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - zitadelRoles []authz.RoleMapping + eventstore func(t *testing.T) *eventstore.Eventstore + zitadelRoles []authz.RoleMapping + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - userID string - roles []string + member *AddInstanceMember } type res struct { - want *domain.Member + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -39,28 +37,28 @@ func TestCommandSide_AddIAMMember(t *testing.T) { { name: "invalid member, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), + member: &AddInstanceMember{}, }, res: res{ - err: zerrors.IsErrorInvalidArgument, + err: zerrors.IsInternal, }, }, { name: "invalid roles, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - userID: "user1", - roles: []string{"IAM_OWNER"}, + member: &AddInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -69,10 +67,10 @@ func TestCommandSide_AddIAMMember(t *testing.T) { { name: "user not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "IAM_OWNER", @@ -80,9 +78,11 @@ func TestCommandSide_AddIAMMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - userID: "user1", - roles: []string{"IAM_OWNER"}, + member: &AddInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, }, res: res{ err: zerrors.IsPreconditionFailed, @@ -91,8 +91,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) { { name: "member already exists, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -118,6 +117,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "IAM_OWNER", @@ -125,9 +125,11 @@ func TestCommandSide_AddIAMMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - userID: "user1", - roles: []string{"IAM_OWNER"}, + member: &AddInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, }, res: res{ err: zerrors.IsErrorAlreadyExists, @@ -136,8 +138,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) { { name: "member add uniqueconstraint err, already exists", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -163,6 +164,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "IAM_OWNER", @@ -170,9 +172,11 @@ func TestCommandSide_AddIAMMember(t *testing.T) { }, }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - userID: "user1", - roles: []string{"IAM_OWNER"}, + member: &AddInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, }, res: res{ err: zerrors.IsErrorAlreadyExists, @@ -181,8 +185,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) { { name: "member add, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusherWithInstanceID( "INSTANCE", @@ -209,6 +212,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "IAM_OWNER", @@ -216,30 +220,49 @@ func TestCommandSide_AddIAMMember(t *testing.T) { }, }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - userID: "user1", - roles: []string{"IAM_OWNER"}, + member: &AddInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, }, res: res{ - want: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - InstanceID: "INSTANCE", - ResourceOwner: "INSTANCE", - AggregateID: "INSTANCE", - }, - UserID: "user1", - Roles: []string{"IAM_OWNER"}, + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", }, }, }, + { + name: "member add, no permission", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckNotAllowed(), + zitadelRoles: []authz.RoleMapping{ + { + Role: "IAM_OWNER", + }, + }, + }, + args: args{ + member: &AddInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - zitadelRoles: tt.fields.zitadelRoles, + eventstore: tt.fields.eventstore(t), + zitadelRoles: tt.fields.zitadelRoles, + checkPermission: tt.fields.checkPermission, } - got, err := r.AddInstanceMember(tt.args.ctx, tt.args.userID, tt.args.roles...) + got, err := r.AddInstanceMember(context.Background(), tt.args.member) if tt.res.err == nil { assert.NoError(t, err) } @@ -247,24 +270,23 @@ func TestCommandSide_AddIAMMember(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } } -func TestCommandSide_ChangeIAMMember(t *testing.T) { +func TestCommandSide_ChangeInstanceMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - zitadelRoles []authz.RoleMapping + eventstore func(t *testing.T) *eventstore.Eventstore + zitadelRoles []authz.RoleMapping + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - instanceID string - member *domain.Member + member *ChangeInstanceMember } type res struct { - want *domain.Member + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -276,13 +298,11 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) { { name: "invalid member, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.Member{}, + member: &ChangeInstanceMember{}, }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -291,15 +311,14 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) { { name: "invalid roles, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - UserID: "user1", - Roles: []string{"IAM_OWNER"}, + member: &ChangeInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER"}, }, }, res: res{ @@ -309,10 +328,10 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) { { name: "member not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "IAM_OWNER", @@ -320,10 +339,10 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - UserID: "user1", - Roles: []string{"IAM_OWNER"}, + member: &ChangeInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER"}, }, }, res: res{ @@ -333,8 +352,7 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) { { name: "member not changed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewMemberAddedEvent(context.Background(), @@ -345,6 +363,7 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleIAMOwner, @@ -352,21 +371,22 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - UserID: "user1", - Roles: []string{"IAM_OWNER"}, + member: &ChangeInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER"}, }, }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, }, }, { name: "member change, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewMemberAddedEvent(context.Background(), @@ -384,6 +404,7 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "IAM_OWNER", @@ -394,32 +415,62 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - UserID: "user1", - Roles: []string{"IAM_OWNER", "IAM_OWNER_VIEWER"}, + member: &ChangeInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER", "IAM_OWNER_VIEWER"}, }, }, res: res{ - want: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - ResourceOwner: "INSTANCE", - AggregateID: "INSTANCE", - InstanceID: "INSTANCE", - }, - UserID: "user1", - Roles: []string{"IAM_OWNER", "IAM_OWNER_VIEWER"}, + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", }, }, }, + { + name: "member change, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewMemberAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "user1", + []string{"IAM_OWNER"}..., + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + zitadelRoles: []authz.RoleMapping{ + { + Role: "IAM_OWNER", + }, + { + Role: "IAM_OWNER_VIEWER", + }, + }, + }, + args: args{ + member: &ChangeInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - zitadelRoles: tt.fields.zitadelRoles, + eventstore: tt.fields.eventstore(t), + zitadelRoles: tt.fields.zitadelRoles, + checkPermission: tt.fields.checkPermission, } - got, err := r.ChangeInstanceMember(tt.args.ctx, tt.args.member) + got, err := r.ChangeInstanceMember(context.Background(), tt.args.member) if tt.res.err == nil { assert.NoError(t, err) } @@ -427,18 +478,18 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } } -func TestCommandSide_RemoveIAMMember(t *testing.T) { +func TestCommandSide_RemoveInstanceMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context instanceID string userID string } @@ -455,13 +506,12 @@ func TestCommandSide_RemoveIAMMember(t *testing.T) { { name: "invalid member userid missing, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - userID: "", + instanceID: "INSTANCE", + userID: "", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -470,24 +520,25 @@ func TestCommandSide_RemoveIAMMember(t *testing.T) { { name: "member not existing, empty object details result", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - userID: "user1", + instanceID: "INSTANCE", + userID: "user1", }, res: res{ - want: &domain.ObjectDetails{}, + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, }, }, { name: "member remove, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewMemberAddedEvent(context.Background(), @@ -504,10 +555,11 @@ func TestCommandSide_RemoveIAMMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - userID: "user1", + instanceID: "INSTANCE", + userID: "user1", }, res: res{ want: &domain.ObjectDetails{ @@ -515,13 +567,38 @@ func TestCommandSide_RemoveIAMMember(t *testing.T) { }, }, }, + { + name: "member remove, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewMemberAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "user1", + []string{"IAM_OWNER"}..., + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + instanceID: "INSTANCE", + userID: "user1", + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } - got, err := r.RemoveInstanceMember(tt.args.ctx, tt.args.userID) + got, err := r.RemoveInstanceMember(context.Background(), tt.args.instanceID, tt.args.userID) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/org.go b/internal/command/org.go index 876c256a0a..ff0208e5e2 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -123,7 +123,7 @@ func (c *Commands) newOrgSetupCommands(ctx context.Context, orgID string, orgSet func (c *orgSetupCommands) setupOrgAdmin(admin *OrgSetupAdmin, allowInitialMail bool) error { if admin.ID != "" { - c.validations = append(c.validations, c.commands.AddOrgMemberCommand(c.aggregate, admin.ID, orgAdminRoles(admin.Roles)...)) + c.validations = append(c.validations, c.commands.AddOrgMemberCommand(&AddOrgMember{OrgID: c.aggregate.ID, UserID: admin.ID, Roles: orgAdminRoles(admin.Roles)})) return nil } @@ -147,7 +147,7 @@ func (c *orgSetupCommands) setupOrgAdmin(admin *OrgSetupAdmin, allowInitialMail return err } } - c.validations = append(c.validations, c.commands.AddOrgMemberCommand(c.aggregate, userID, orgAdminRoles(admin.Roles)...)) + c.validations = append(c.validations, c.commands.AddOrgMemberCommand(&AddOrgMember{OrgID: c.aggregate.ID, UserID: userID, Roles: orgAdminRoles(admin.Roles)})) return nil } @@ -359,16 +359,15 @@ func (c *Commands) addOrgWithIDAndMember(ctx context.Context, name, userID, reso if err != nil { return nil, err } - err = c.checkUserExists(ctx, userID, resourceOwner) + _, err = c.checkUserExists(ctx, userID, resourceOwner) if err != nil { return nil, err } - addedMember := NewOrgMemberWriteModel(addedOrg.AggregateID, userID) - orgMemberEvent, err := c.addOrgMember(ctx, orgAgg, addedMember, domain.NewMember(orgAgg.ID, userID, domain.RoleOrgOwner)) - if err != nil { + addMember := &AddOrgMember{OrgID: orgAgg.ID, UserID: userID, Roles: []string{domain.RoleOrgOwner}} + if err := addMember.IsValid(c.zitadelRoles); err != nil { return nil, err } - events = append(events, orgMemberEvent) + events = append(events, org.NewMemberAddedEvent(ctx, orgAgg, addMember.UserID, addMember.Roles...)) if setOrgInactive { deactivateOrgEvent := org.NewOrgDeactivatedEvent(ctx, orgAgg) events = append(events, deactivateOrgEvent) diff --git a/internal/command/org_member.go b/internal/command/org_member.go index bf1ae91d8a..a384145e50 100644 --- a/internal/command/org_member.go +++ b/internal/command/org_member.go @@ -2,8 +2,9 @@ package command import ( "context" - "reflect" + "slices" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -12,29 +13,22 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddOrgMemberCommand(a *org.Aggregate, userID string, roles ...string) preparation.Validation { +func (c *Commands) AddOrgMemberCommand(member *AddOrgMember) preparation.Validation { return func() (preparation.CreateCommands, error) { - if userID == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument") - } - if len(roles) == 0 { - return nil, zerrors.ThrowInvalidArgument(nil, "V2-PfYhb", "Errors.Invalid.Argument") - } - - if len(domain.CheckForInvalidRoles(roles, domain.OrgRolePrefix, c.zitadelRoles)) > 0 && len(domain.CheckForInvalidRoles(roles, domain.RoleSelfManagementGlobal, c.zitadelRoles)) > 0 { - return nil, zerrors.ThrowInvalidArgument(nil, "Org-4N8es", "Errors.Org.MemberInvalid") + if err := member.IsValid(c.zitadelRoles); err != nil { + return nil, err } return func(ctx context.Context, filter preparation.FilterToQueryReducer) (_ []eventstore.Command, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if exists, err := ExistsUser(ctx, filter, userID, "", false); err != nil || !exists { + if exists, err := ExistsUser(ctx, filter, member.UserID, "", false); err != nil || !exists { return nil, zerrors.ThrowPreconditionFailed(err, "ORG-GoXOn", "Errors.User.NotFound") } - if isMember, err := IsOrgMember(ctx, filter, a.ID, userID); err != nil || isMember { + if isMember, err := IsOrgMember(ctx, filter, member.OrgID, member.UserID); err != nil || isMember { return nil, zerrors.ThrowAlreadyExists(err, "ORG-poWwe", "Errors.Org.Member.AlreadyExists") } - return []eventstore.Command{org.NewMemberAddedEvent(ctx, &a.Aggregate, userID, roles...)}, nil + return []eventstore.Command{org.NewMemberAddedEvent(ctx, &org.NewAggregate(member.OrgID).Aggregate, member.UserID, member.Roles...)}, nil }, nil } @@ -76,12 +70,33 @@ func IsOrgMember(ctx context.Context, filter preparation.FilterToQueryReducer, o return isMember, nil } -func (c *Commands) AddOrgMember(ctx context.Context, orgID, userID string, roles ...string) (_ *domain.Member, err error) { +type AddOrgMember struct { + OrgID string + UserID string + Roles []string +} + +func (m *AddOrgMember) IsValid(zitadelRoles []authz.RoleMapping) error { + if m.UserID == "" || m.OrgID == "" || len(m.Roles) == 0 { + return zerrors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument") + } + if len(domain.CheckForInvalidRoles(m.Roles, domain.OrgRolePrefix, zitadelRoles)) > 0 && len(domain.CheckForInvalidRoles(m.Roles, domain.RoleSelfManagementGlobal, zitadelRoles)) > 0 { + return zerrors.ThrowInvalidArgument(nil, "Org-4N8es", "Errors.Org.MemberInvalid") + } + return nil +} + +func (c *Commands) AddOrgMember(ctx context.Context, member *AddOrgMember) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - - orgAgg := org.NewAggregate(orgID) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.AddOrgMemberCommand(orgAgg, userID, roles...)) + if err := c.checkOrgExists(ctx, member.OrgID); err != nil { + return nil, err + } + if err := c.checkPermissionUpdateOrgMember(ctx, member.OrgID, member.OrgID); err != nil { + return nil, err + } + //nolint:staticcheck + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.AddOrgMemberCommand(member)) if err != nil { return nil, err } @@ -89,51 +104,59 @@ func (c *Commands) AddOrgMember(ctx context.Context, orgID, userID string, roles if err != nil { return nil, err } - addedMember := NewOrgMemberWriteModel(orgID, userID) + addedMember := NewOrgMemberWriteModel(member.OrgID, member.UserID) err = AppendAndReduce(addedMember, events...) if err != nil { return nil, err } - return memberWriteModelToMember(&addedMember.MemberWriteModel), nil + return writeModelToObjectDetails(&addedMember.WriteModel), nil } -func (c *Commands) addOrgMember(ctx context.Context, orgAgg *eventstore.Aggregate, addedMember *OrgMemberWriteModel, member *domain.Member) (eventstore.Command, error) { - if !member.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "Org-W8m4l", "Errors.Org.MemberInvalid") +type ChangeOrgMember struct { + OrgID string + UserID string + Roles []string +} + +func (c *ChangeOrgMember) IsValid(zitadelRoles []authz.RoleMapping) error { + if c.OrgID == "" || c.UserID == "" || len(c.Roles) == 0 { + return zerrors.ThrowInvalidArgument(nil, "Org-LiaZi", "Errors.Org.MemberInvalid") } - if len(domain.CheckForInvalidRoles(member.Roles, domain.OrgRolePrefix, c.zitadelRoles)) > 0 && len(domain.CheckForInvalidRoles(member.Roles, domain.RoleSelfManagementGlobal, c.zitadelRoles)) > 0 { - return nil, zerrors.ThrowInvalidArgument(nil, "Org-4N8es", "Errors.Org.MemberInvalid") - } - err := c.eventstore.FilterToQueryReducer(ctx, addedMember) - if err != nil { - return nil, err - } - if addedMember.State == domain.MemberStateActive { - return nil, zerrors.ThrowAlreadyExists(nil, "Org-PtXi1", "Errors.Org.Member.AlreadyExists") + if len(domain.CheckForInvalidRoles(c.Roles, domain.OrgRolePrefix, zitadelRoles)) > 0 { + return zerrors.ThrowInvalidArgument(nil, "IAM-m9fG8", "Errors.Org.MemberInvalid") } - return org.NewMemberAddedEvent(ctx, orgAgg, member.UserID, member.Roles...), nil + return nil } // ChangeOrgMember updates an existing member -func (c *Commands) ChangeOrgMember(ctx context.Context, member *domain.Member) (*domain.Member, error) { - if !member.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "Org-LiaZi", "Errors.Org.MemberInvalid") - } - if len(domain.CheckForInvalidRoles(member.Roles, domain.OrgRolePrefix, c.zitadelRoles)) > 0 { - return nil, zerrors.ThrowInvalidArgument(nil, "IAM-m9fG8", "Errors.Org.MemberInvalid") - } - - existingMember, err := c.orgMemberWriteModelByID(ctx, member.AggregateID, member.UserID) - if err != nil { +func (c *Commands) ChangeOrgMember(ctx context.Context, member *ChangeOrgMember) (*domain.ObjectDetails, error) { + if err := member.IsValid(c.zitadelRoles); err != nil { return nil, err } - if reflect.DeepEqual(existingMember.Roles, member.Roles) { - return nil, zerrors.ThrowPreconditionFailed(nil, "Org-LiaZi", "Errors.Org.Member.RolesNotChanged") + existingMember, err := c.orgMemberWriteModelByID(ctx, member.OrgID, member.UserID) + if err != nil { + return nil, err } - orgAgg := OrgAggregateFromWriteModel(&existingMember.MemberWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, org.NewMemberChangedEvent(ctx, orgAgg, member.UserID, member.Roles...)) + if !existingMember.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "Org-D8JxR", "Errors.NotFound") + } + if err := c.checkPermissionUpdateOrgMember(ctx, existingMember.ResourceOwner, existingMember.AggregateID); err != nil { + return nil, err + } + + if slices.Compare(existingMember.Roles, member.Roles) == 0 { + return writeModelToObjectDetails(&existingMember.WriteModel), nil + } + + pushedEvents, err := c.eventstore.Push(ctx, + org.NewMemberChangedEvent(ctx, + OrgAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel), + member.UserID, + member.Roles..., + ), + ) if err != nil { return nil, err } @@ -142,30 +165,39 @@ func (c *Commands) ChangeOrgMember(ctx context.Context, member *domain.Member) ( return nil, err } - return memberWriteModelToMember(&existingMember.MemberWriteModel), nil + return writeModelToObjectDetails(&existingMember.WriteModel), nil } func (c *Commands) RemoveOrgMember(ctx context.Context, orgID, userID string) (*domain.ObjectDetails, error) { - m, err := c.orgMemberWriteModelByID(ctx, orgID, userID) - if err != nil && !zerrors.IsNotFound(err) { + if orgID == "" || userID == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "Org-LiaZi", "Errors.Org.MemberInvalid") + } + existingMember, err := c.orgMemberWriteModelByID(ctx, orgID, userID) + if err != nil { return nil, err } - if zerrors.IsNotFound(err) { - // empty response because we have no data that match the request - return &domain.ObjectDetails{}, nil + if !existingMember.State.Exists() { + return writeModelToObjectDetails(&existingMember.WriteModel), nil + } + if err := c.checkPermissionDeleteOrgMember(ctx, existingMember.ResourceOwner, existingMember.AggregateID); err != nil { + return nil, err } - orgAgg := OrgAggregateFromWriteModel(&m.MemberWriteModel.WriteModel) - removeEvent := c.removeOrgMember(ctx, orgAgg, userID, false) - pushedEvents, err := c.eventstore.Push(ctx, removeEvent) + pushedEvents, err := c.eventstore.Push(ctx, + c.removeOrgMember(ctx, + OrgAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel), + userID, + false, + ), + ) if err != nil { return nil, err } - err = AppendAndReduce(m, pushedEvents...) + err = AppendAndReduce(existingMember, pushedEvents...) if err != nil { return nil, err } - return writeModelToObjectDetails(&m.WriteModel), nil + return writeModelToObjectDetails(&existingMember.WriteModel), nil } func (c *Commands) removeOrgMember(ctx context.Context, orgAgg *eventstore.Aggregate, userID string, cascade bool) eventstore.Command { @@ -189,9 +221,5 @@ func (c *Commands) orgMemberWriteModelByID(ctx context.Context, orgID, userID st return nil, err } - if writeModel.State == domain.MemberStateUnspecified || writeModel.State == domain.MemberStateRemoved { - return nil, zerrors.ThrowNotFound(nil, "Org-D8JxR", "Errors.NotFound") - } - return writeModel, nil } diff --git a/internal/command/org_member_test.go b/internal/command/org_member_test.go index 4e2e926b52..25dd3b818a 100644 --- a/internal/command/org_member_test.go +++ b/internal/command/org_member_test.go @@ -11,24 +11,19 @@ import ( "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/repository/org" - "github.com/zitadel/zitadel/internal/repository/project" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" ) func TestAddMember(t *testing.T) { type args struct { - a *org.Aggregate - userID string - roles []string + member *AddOrgMember zitadelRoles []authz.RoleMapping filter preparation.FilterToQueryReducer } ctx := context.Background() - agg := org.NewAggregate("test") tests := []struct { name string @@ -38,8 +33,10 @@ func TestAddMember(t *testing.T) { { name: "no user id", args: args{ - a: agg, - userID: "", + member: &AddOrgMember{ + OrgID: "test", + UserID: "", + }, }, want: Want{ ValidationErr: zerrors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument"), @@ -48,19 +45,23 @@ func TestAddMember(t *testing.T) { { name: "no roles", args: args{ - a: agg, - userID: "12342", + member: &AddOrgMember{ + OrgID: "test", + UserID: "12342", + }, }, want: Want{ - ValidationErr: zerrors.ThrowInvalidArgument(nil, "V2-PfYhb", "Errors.Invalid.Argument"), + ValidationErr: zerrors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument"), }, }, { name: "TODO: invalid roles", args: args{ - a: agg, - userID: "123", - roles: []string{"ORG_OWNER"}, + member: &AddOrgMember{ + OrgID: "test", + UserID: "12342", + Roles: []string{"ORG_OWNER"}, + }, }, want: Want{ ValidationErr: zerrors.ThrowInvalidArgument(nil, "Org-4N8es", ""), @@ -69,9 +70,11 @@ func TestAddMember(t *testing.T) { { name: "user not exists", args: args{ - a: agg, - userID: "userID", - roles: []string{"ORG_OWNER"}, + member: &AddOrgMember{ + OrgID: "test", + UserID: "userID", + Roles: []string{"ORG_OWNER"}, + }, zitadelRoles: []authz.RoleMapping{ { Role: "ORG_OWNER", @@ -89,9 +92,11 @@ func TestAddMember(t *testing.T) { { name: "already member", args: args{ - a: agg, - userID: "userID", - roles: []string{"ORG_OWNER"}, + member: &AddOrgMember{ + OrgID: "test", + UserID: "userID", + Roles: []string{"ORG_OWNER"}, + }, zitadelRoles: []authz.RoleMapping{ { Role: "ORG_OWNER", @@ -129,9 +134,11 @@ func TestAddMember(t *testing.T) { { name: "correct", args: args{ - a: agg, - userID: "userID", - roles: []string{"ORG_OWNER"}, + member: &AddOrgMember{ + OrgID: "test", + UserID: "userID", + Roles: []string{"ORG_OWNER"}, + }, zitadelRoles: []authz.RoleMapping{ { Role: "ORG_OWNER", @@ -158,14 +165,14 @@ func TestAddMember(t *testing.T) { }, want: Want{ Commands: []eventstore.Command{ - org.NewMemberAddedEvent(ctx, &agg.Aggregate, "userID", "ORG_OWNER"), + org.NewMemberAddedEvent(ctx, &org.NewAggregate("test").Aggregate, "userID", "ORG_OWNER"), }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - AssertValidation(t, context.Background(), (&Commands{zitadelRoles: tt.args.zitadelRoles}).AddOrgMemberCommand(tt.args.a, tt.args.userID, tt.args.roles...), tt.args.filter, tt.want) + AssertValidation(t, context.Background(), (&Commands{zitadelRoles: tt.args.zitadelRoles}).AddOrgMemberCommand(tt.args.member), tt.args.filter, tt.want) }) } } @@ -287,17 +294,15 @@ func TestIsMember(t *testing.T) { func TestCommandSide_AddOrgMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - zitadelRoles []authz.RoleMapping + checkPermission domain.PermissionCheck + eventstore func(t *testing.T) *eventstore.Eventstore + zitadelRoles []authz.RoleMapping } type args struct { - ctx context.Context - userID string - orgID string - roles []string + member *AddOrgMember } type res struct { - want *domain.Member + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -309,13 +314,22 @@ func TestCommandSide_AddOrgMember(t *testing.T) { { name: "invalid member, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org", + ), + ), + ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - orgID: "org1", + member: &AddOrgMember{ + OrgID: "org1", + }, }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -324,15 +338,24 @@ func TestCommandSide_AddOrgMember(t *testing.T) { { name: "invalid roles, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org", + ), + ), + ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - orgID: "org1", - userID: "user1", - roles: []string{"ORG_OWNER"}, + member: &AddOrgMember{ + OrgID: "org1", + UserID: "user1", + Roles: []string{"ORG_OWNER"}, + }, }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -341,10 +364,18 @@ func TestCommandSide_AddOrgMember(t *testing.T) { { name: "user not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org", + ), + ), + ), expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleOrgOwner, @@ -352,10 +383,11 @@ func TestCommandSide_AddOrgMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - orgID: "org1", - userID: "user1", - roles: []string{domain.RoleOrgOwner}, + member: &AddOrgMember{ + OrgID: "org1", + UserID: "user1", + Roles: []string{"ORG_OWNER"}, + }, }, res: res{ err: zerrors.IsPreconditionFailed, @@ -364,8 +396,15 @@ func TestCommandSide_AddOrgMember(t *testing.T) { { name: "member already exists, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org", + ), + ), + ), expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -391,6 +430,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleOrgOwner, @@ -398,10 +438,11 @@ func TestCommandSide_AddOrgMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - orgID: "org1", - userID: "user1", - roles: []string{"ORG_OWNER"}, + member: &AddOrgMember{ + OrgID: "org1", + UserID: "user1", + Roles: []string{"ORG_OWNER"}, + }, }, res: res{ err: zerrors.IsErrorAlreadyExists, @@ -410,8 +451,15 @@ func TestCommandSide_AddOrgMember(t *testing.T) { { name: "member add uniqueconstraint err, already exists", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org", + ), + ), + ), expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -437,6 +485,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleOrgOwner, @@ -444,10 +493,11 @@ func TestCommandSide_AddOrgMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - orgID: "org1", - userID: "user1", - roles: []string{"ORG_OWNER"}, + member: &AddOrgMember{ + OrgID: "org1", + UserID: "user1", + Roles: []string{"ORG_OWNER"}, + }, }, res: res{ err: zerrors.IsErrorAlreadyExists, @@ -456,8 +506,15 @@ func TestCommandSide_AddOrgMember(t *testing.T) { { name: "member add, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org", + ), + ), + ), expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -483,6 +540,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleOrgOwner, @@ -490,30 +548,58 @@ func TestCommandSide_AddOrgMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - orgID: "org1", - userID: "user1", - roles: []string{"ORG_OWNER"}, + member: &AddOrgMember{ + OrgID: "org1", + UserID: "user1", + Roles: []string{"ORG_OWNER"}, + }, }, res: res{ - want: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - ResourceOwner: "org1", - AggregateID: "org1", - }, - UserID: "user1", - Roles: []string{domain.RoleOrgOwner}, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, + { + name: "member add, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + zitadelRoles: []authz.RoleMapping{ + { + Role: domain.RoleOrgOwner, + }, + }, + }, + args: args{ + member: &AddOrgMember{ + OrgID: "org1", + UserID: "user1", + Roles: []string{"ORG_OWNER"}, + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - zitadelRoles: tt.fields.zitadelRoles, + eventstore: tt.fields.eventstore(t), + zitadelRoles: tt.fields.zitadelRoles, + checkPermission: tt.fields.checkPermission, } - got, err := r.AddOrgMember(tt.args.ctx, tt.args.orgID, tt.args.userID, tt.args.roles...) + got, err := r.AddOrgMember(context.Background(), tt.args.member) if tt.res.err == nil { assert.NoError(t, err) } @@ -521,7 +607,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -529,15 +615,15 @@ func TestCommandSide_AddOrgMember(t *testing.T) { func TestCommandSide_ChangeOrgMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - zitadelRoles []authz.RoleMapping + checkPermission domain.PermissionCheck + eventstore func(t *testing.T) *eventstore.Eventstore + zitadelRoles []authz.RoleMapping } type args struct { - ctx context.Context - member *domain.Member + member *ChangeOrgMember } type res struct { - want *domain.Member + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -549,16 +635,12 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { { name: "invalid member, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "org1", - }, + member: &ChangeOrgMember{ + OrgID: "org1", }, }, res: res{ @@ -568,16 +650,12 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { { name: "invalid roles, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "org1", - }, + member: &ChangeOrgMember{ + OrgID: "org1", UserID: "user1", Roles: []string{"PROJECT_OWNER"}, }, @@ -589,10 +667,10 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { { name: "member not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleOrgOwner, @@ -600,11 +678,8 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "org1", - }, + member: &ChangeOrgMember{ + OrgID: "org1", UserID: "user1", Roles: []string{"ORG_OWNER"}, }, @@ -614,10 +689,9 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { }, }, { - name: "member not changed, precondition error", + name: "member not changed, no change", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewMemberAddedEvent(context.Background(), @@ -628,6 +702,7 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleOrgOwner, @@ -635,24 +710,22 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "org1", - }, + member: &ChangeOrgMember{ + OrgID: "org1", UserID: "user1", Roles: []string{"ORG_OWNER"}, }, }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "member change, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewMemberAddedEvent(context.Background(), @@ -670,6 +743,7 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "ORG_OWNER", @@ -680,160 +754,210 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "org1", - }, + member: &ChangeOrgMember{ + OrgID: "org1", UserID: "user1", Roles: []string{"ORG_OWNER", "ORG_OWNER_VIEWER"}, }, }, - res: res{ - want: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - ResourceOwner: "org1", - AggregateID: "org1", - }, - UserID: "user1", - Roles: []string{"ORG_OWNER", "ORG_OWNER_VIEWER"}, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := &Commands{ - eventstore: tt.fields.eventstore, - zitadelRoles: tt.fields.zitadelRoles, - } - got, err := r.ChangeOrgMember(tt.args.ctx, tt.args.member) - 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_RemoveOrgMember(t *testing.T) { - type fields struct { - eventstore *eventstore.Eventstore - } - type args struct { - ctx context.Context - projectID string - userID string - resourceOwner string - } - type res struct { - want *domain.ObjectDetails - err func(error) bool - } - tests := []struct { - name string - fields fields - args args - res res - }{ - { - name: "invalid member projectid missing, error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - projectID: "", - userID: "user1", - resourceOwner: "org1", - }, - res: res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - name: "invalid member userid missing, error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - projectID: "project1", - userID: "", - resourceOwner: "org1", - }, - res: res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - name: "member not existing, empty object details result", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter(), - ), - }, - args: args{ - ctx: context.Background(), - projectID: "project1", - userID: "user1", - resourceOwner: "org1", - }, - res: res{ - want: &domain.ObjectDetails{}, - }, - }, - { - name: "member remove, ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - project.NewProjectMemberAddedEvent(context.Background(), - &project.NewAggregate("project1", "org1").Aggregate, - "user1", - []string{"PROJECT_OWNER"}..., - ), - ), - ), - expectPush( - project.NewProjectMemberRemovedEvent(context.Background(), - &project.NewAggregate("project1", "org1").Aggregate, - "user1", - ), - ), - ), - }, - args: args{ - ctx: context.Background(), - projectID: "project1", - userID: "user1", - resourceOwner: "org1", - }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, + { + name: "member change, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewMemberAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "user1", + []string{"ORG_OWNER"}..., + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + zitadelRoles: []authz.RoleMapping{ + { + Role: "ORG_OWNER", + }, + { + Role: "ORG_OWNER_VIEWER", + }, + }, + }, + args: args{ + member: &ChangeOrgMember{ + OrgID: "org1", + UserID: "user1", + Roles: []string{"ORG_OWNER", "ORG_OWNER_VIEWER"}, + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + zitadelRoles: tt.fields.zitadelRoles, + checkPermission: tt.fields.checkPermission, } - got, err := r.RemoveProjectMember(tt.args.ctx, tt.args.projectID, tt.args.userID, tt.args.resourceOwner) + got, err := r.ChangeOrgMember(context.Background(), tt.args.member) + 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 { + assertObjectDetails(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemoveOrgMember(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + ctx context.Context + orgID string + userID string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid member orgID missing, error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "", + userID: "user1", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "invalid member userid missing, error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "member not existing, empty object details result", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "member remove, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewMemberAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "user1", + []string{"PROJECT_OWNER"}..., + ), + ), + ), + expectPush( + org.NewMemberRemovedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "user1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "member remove, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewMemberAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "user1", + []string{"PROJECT_OWNER"}..., + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + 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.RemoveOrgMember(tt.args.ctx, tt.args.orgID, tt.args.userID) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/org_model.go b/internal/command/org_model.go index 2661af9bd9..9b39805609 100644 --- a/internal/command/org_model.go +++ b/internal/command/org_model.go @@ -1,6 +1,8 @@ package command import ( + "context" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/org" @@ -63,3 +65,7 @@ func (wm *OrgWriteModel) Query() *eventstore.SearchQueryBuilder { func OrgAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate { return eventstore.AggregateFromWriteModel(wm, org.AggregateType, org.AggregateVersion) } + +func OrgAggregateFromWriteModelWithCTX(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate { + return org.AggregateFromWriteModel(ctx, wm) +} diff --git a/internal/command/org_test.go b/internal/command/org_test.go index 4239be760a..a5d00d9bfd 100644 --- a/internal/command/org_test.go +++ b/internal/command/org_test.go @@ -73,7 +73,7 @@ func TestAddOrg(t *testing.T) { func TestCommandSide_AddOrg(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator zitadelRoles []authz.RoleMapping } @@ -97,9 +97,7 @@ func TestCommandSide_AddOrg(t *testing.T) { { name: "invalid org, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -113,9 +111,7 @@ func TestCommandSide_AddOrg(t *testing.T) { { name: "invalid org (spaces), error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -130,8 +126,7 @@ func TestCommandSide_AddOrg(t *testing.T) { { name: "user removed, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilterOrgDomainNotFound(), expectFilter( eventFromEventPusher( @@ -174,8 +169,7 @@ func TestCommandSide_AddOrg(t *testing.T) { { name: "push failed unique constraint, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilterOrgDomainNotFound(), expectFilter( eventFromEventPusher( @@ -193,7 +187,6 @@ func TestCommandSide_AddOrg(t *testing.T) { ), ), ), - expectFilterOrgMemberNotFound(), expectPushFailed(zerrors.ThrowAlreadyExists(nil, "id", "internal"), org.NewOrgAddedEvent( context.Background(), @@ -242,8 +235,7 @@ func TestCommandSide_AddOrg(t *testing.T) { { name: "push failed, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilterOrgDomainNotFound(), expectFilter( eventFromEventPusher( @@ -261,7 +253,6 @@ func TestCommandSide_AddOrg(t *testing.T) { ), ), ), - expectFilterOrgMemberNotFound(), expectPushFailed(zerrors.ThrowInternal(nil, "id", "internal"), org.NewOrgAddedEvent( context.Background(), @@ -310,8 +301,7 @@ func TestCommandSide_AddOrg(t *testing.T) { { name: "add org, no error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilterOrgDomainNotFound(), expectFilter( eventFromEventPusher( @@ -329,7 +319,6 @@ func TestCommandSide_AddOrg(t *testing.T) { ), ), ), - expectFilterOrgMemberNotFound(), expectPush( org.NewOrgAddedEvent(context.Background(), &org.NewAggregate("org2").Aggregate, @@ -381,8 +370,7 @@ func TestCommandSide_AddOrg(t *testing.T) { { name: "add org (remove spaces), no error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilterOrgDomainNotFound(), expectFilter( eventFromEventPusher( @@ -400,7 +388,6 @@ func TestCommandSide_AddOrg(t *testing.T) { ), ), ), - expectFilterOrgMemberNotFound(), expectPush( org.NewOrgAddedEvent(context.Background(), &org.NewAggregate("org2").Aggregate, @@ -453,7 +440,7 @@ func TestCommandSide_AddOrg(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), idGenerator: tt.fields.idGenerator, zitadelRoles: tt.fields.zitadelRoles, } @@ -473,7 +460,7 @@ func TestCommandSide_AddOrg(t *testing.T) { func TestCommandSide_ChangeOrg(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -492,9 +479,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { { name: "empty name, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -507,9 +492,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { { name: "empty name (spaces), invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -523,8 +506,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { { name: "org not found, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -540,8 +522,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { { name: "no change (spaces), error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -563,8 +544,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { { name: "push failed, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -593,8 +573,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { { name: "change org name verified, not primary", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -645,8 +624,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { { name: "change org name verified, with primary", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -705,8 +683,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { { name: "change org name case verified, with primary", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -754,7 +731,7 @@ func TestCommandSide_ChangeOrg(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.ChangeOrg(tt.args.ctx, tt.args.orgID, tt.args.name) if tt.res.err == nil { @@ -769,7 +746,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { func TestCommandSide_DeactivateOrg(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator iamDomain string } @@ -790,8 +767,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) { { name: "org not found, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -806,8 +782,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) { { name: "org already inactive, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -832,8 +807,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) { { name: "push failed, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -860,8 +834,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) { { name: "deactivate org", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -886,7 +859,7 @@ func TestCommandSide_DeactivateOrg(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), idGenerator: tt.fields.idGenerator, } _, err := r.DeactivateOrg(tt.args.ctx, tt.args.orgID) @@ -902,7 +875,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) { func TestCommandSide_ReactivateOrg(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator iamDomain string } @@ -923,8 +896,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) { { name: "org not found, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -939,8 +911,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) { { name: "org already active, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -961,8 +932,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) { { name: "push failed, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -994,8 +964,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) { { name: "reactivate org", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -1024,7 +993,7 @@ func TestCommandSide_ReactivateOrg(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), idGenerator: tt.fields.idGenerator, } _, err := r.ReactivateOrg(tt.args.ctx, tt.args.orgID) @@ -1040,7 +1009,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) { func TestCommandSide_RemoveOrg(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator } type args struct { @@ -1059,9 +1028,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) { { name: "default org, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: authz.WithInstance(context.Background(), &mockInstance{}), @@ -1074,8 +1041,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) { { name: "zitadel org, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -1100,8 +1066,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) { { name: "org not found, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), // zitadel project check expectFilter(), ), @@ -1117,8 +1082,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) { { name: "push failed, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), // zitadel project check expectFilter( eventFromEventPusher( @@ -1160,8 +1124,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) { { name: "remove org", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), // zitadel project check expectFilter( eventFromEventPusher( @@ -1200,8 +1163,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) { { name: "remove org with usernames and domains", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), // zitadel project check expectFilter( eventFromEventPusher( @@ -1291,7 +1253,7 @@ func TestCommandSide_RemoveOrg(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), idGenerator: tt.fields.idGenerator, } _, err := r.RemoveOrg(tt.args.ctx, tt.args.orgID) diff --git a/internal/command/permission_checks.go b/internal/command/permission_checks.go index 3f978b6618..128880341a 100644 --- a/internal/command/permission_checks.go +++ b/internal/command/permission_checks.go @@ -6,6 +6,8 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/repository/project" "github.com/zitadel/zitadel/internal/v2/user" "github.com/zitadel/zitadel/internal/zerrors" @@ -13,6 +15,8 @@ import ( type PermissionCheck func(resourceOwner, aggregateID string) error +type UserGrantPermissionCheck func(projectID, projectGrantID string) PermissionCheck + func (c *Commands) newPermissionCheck(ctx context.Context, permission string, aggregateType eventstore.AggregateType) PermissionCheck { return func(resourceOwner, aggregateID string) error { if aggregateID == "" { @@ -93,3 +97,62 @@ func (c *Commands) checkPermissionUpdateApplication(ctx context.Context, resourc func (c *Commands) checkPermissionDeleteApp(ctx context.Context, resourceOwner, appID string) error { return c.newPermissionCheck(ctx, domain.PermissionProjectAppDelete, project.AggregateType)(resourceOwner, appID) } + +func (c *Commands) checkPermissionUpdateInstanceMember(ctx context.Context, instanceID string) error { + return c.newPermissionCheck(ctx, domain.PermissionInstanceMemberWrite, instance.AggregateType)(instanceID, instanceID) +} + +func (c *Commands) checkPermissionDeleteInstanceMember(ctx context.Context, instanceID string) error { + return c.newPermissionCheck(ctx, domain.PermissionInstanceMemberDelete, instance.AggregateType)(instanceID, instanceID) +} + +func (c *Commands) checkPermissionUpdateOrgMember(ctx context.Context, instanceID, orgID string) error { + return c.newPermissionCheck(ctx, domain.PermissionOrgMemberWrite, org.AggregateType)(instanceID, orgID) +} +func (c *Commands) checkPermissionDeleteOrgMember(ctx context.Context, instanceID, orgID string) error { + return c.newPermissionCheck(ctx, domain.PermissionOrgMemberDelete, org.AggregateType)(instanceID, orgID) +} + +func (c *Commands) checkPermissionUpdateProjectMember(ctx context.Context, resourceOwner, projectID string) error { + return c.newPermissionCheck(ctx, domain.PermissionProjectMemberWrite, project.AggregateType)(resourceOwner, projectID) +} + +func (c *Commands) checkPermissionDeleteProjectMember(ctx context.Context, resourceOwner, projectID string) error { + return c.newPermissionCheck(ctx, domain.PermissionProjectMemberDelete, project.AggregateType)(resourceOwner, projectID) +} + +func (c *Commands) checkPermissionUpdateProjectGrantMember(ctx context.Context, grantedOrgID, projectGrantID string) (err error) { + // TODO: add permission check for project grant owners + //if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantMemberWrite, project.AggregateType)(resourceOwner, projectGrantID); err != nil { + return c.newPermissionCheck(ctx, domain.PermissionProjectGrantMemberWrite, project.AggregateType)(grantedOrgID, projectGrantID) + //} + //return nil +} + +func (c *Commands) checkPermissionDeleteProjectGrantMember(ctx context.Context, grantedOrgID, projectGrantID string) (err error) { + // TODO: add permission check for project grant owners + //if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantMemberDelete, project.AggregateType)(resourceOwner, projectGrantID); err != nil { + return c.newPermissionCheck(ctx, domain.PermissionProjectGrantMemberDelete, project.AggregateType)(grantedOrgID, projectGrantID) + //} + //return nil +} + +func (c *Commands) newUserGrantPermissionCheck(ctx context.Context, permission string) UserGrantPermissionCheck { + check := c.newPermissionCheck(ctx, permission, project.AggregateType) + return func(projectID, projectGrantID string) PermissionCheck { + return func(resourceOwner, _ string) error { + if projectGrantID != "" { + return check(resourceOwner, projectGrantID) + } + return check(resourceOwner, projectID) + } + } +} + +func (c *Commands) NewPermissionCheckUserGrantWrite(ctx context.Context) UserGrantPermissionCheck { + return c.newUserGrantPermissionCheck(ctx, domain.PermissionUserGrantWrite) +} + +func (c *Commands) NewPermissionCheckUserGrantDelete(ctx context.Context) UserGrantPermissionCheck { + return c.newUserGrantPermissionCheck(ctx, domain.PermissionUserGrantDelete) +} diff --git a/internal/command/project.go b/internal/command/project.go index 40aa79f186..4cdf1b7373 100644 --- a/internal/command/project.go +++ b/internal/command/project.go @@ -346,7 +346,7 @@ func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner s } for _, grantID := range cascadingUserGrantIDs { - event, _, err := c.removeUserGrant(ctx, grantID, "", true) + event, _, err := c.removeUserGrant(ctx, grantID, "", true, false, nil) if err != nil { logging.WithFields("id", "COMMAND-b8Djf", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") continue @@ -398,7 +398,7 @@ func (c *Commands) DeleteProject(ctx context.Context, id, resourceOwner string, ), } for _, grantID := range cascadingUserGrantIDs { - event, _, err := c.removeUserGrant(ctx, grantID, "", true) + event, _, err := c.removeUserGrant(ctx, grantID, "", true, false, nil) if err != nil { logging.WithFields("id", "COMMAND-b8Djf", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") continue diff --git a/internal/command/project_grant.go b/internal/command/project_grant.go index b613974b7e..2a0e83a6e8 100644 --- a/internal/command/project_grant.go +++ b/internal/command/project_grant.go @@ -234,6 +234,17 @@ func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantI return writeModelToObjectDetails(&existingGrant.WriteModel), nil } +func (c *Commands) checkProjectGrantExists(ctx context.Context, grantID, grantedOrgID, projectID, resourceOwner string) (string, string, error) { + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner) + if err != nil { + return "", "", err + } + if !existingGrant.State.Exists() { + return "", "", zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") + } + return existingGrant.GrantedOrgID, existingGrant.ResourceOwner, nil +} + func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string) (details *domain.ObjectDetails, err error) { if (grantID == "" && grantedOrgID == "") || projectID == "" { return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing") @@ -302,16 +313,17 @@ func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, r ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), existingGrant.GrantID, existingGrant.GrantedOrgID, - ), - ) + )) for _, userGrantID := range cascadeUserGrantIDs { - event, _, err := c.removeUserGrant(ctx, userGrantID, "", true) + event, _, err := c.removeUserGrant(ctx, userGrantID, "", true, true, nil) if err != nil { logging.WithFields("id", "COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") continue } - events = append(events, event) + if event != nil { + events = append(events, event) + } } pushedEvents, err := c.eventstore.Push(ctx, events...) if err != nil { @@ -348,12 +360,14 @@ func (c *Commands) DeleteProjectGrant(ctx context.Context, projectID, grantID, g ) for _, userGrantID := range cascadeUserGrantIDs { - event, _, err := c.removeUserGrant(ctx, userGrantID, "", true) + event, _, err := c.removeUserGrant(ctx, userGrantID, "", true, true, nil) if err != nil { logging.WithFields("id", "COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") continue } - events = append(events, event) + if event != nil { + events = append(events, event) + } } pushedEvents, err := c.eventstore.Push(ctx, events...) if err != nil { diff --git a/internal/command/project_grant_member.go b/internal/command/project_grant_member.go index f7ea887475..611b897c61 100644 --- a/internal/command/project_grant_member.go +++ b/internal/command/project_grant_member.go @@ -2,8 +2,9 @@ package command import ( "context" - "reflect" + "slices" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/project" @@ -11,25 +12,66 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddProjectGrantMember(ctx context.Context, member *domain.ProjectGrantMember) (_ *domain.ProjectGrantMember, err error) { +type AddProjectGrantMember struct { + ResourceOwner string + UserID string + GrantID string + ProjectID string + Roles []string +} + +func (i *AddProjectGrantMember) IsValid(zitadelRoles []authz.RoleMapping) error { + if i.ProjectID == "" || i.GrantID == "" || i.UserID == "" || len(i.Roles) == 0 { + return zerrors.ThrowInvalidArgument(nil, "PROJECT-8fi7G", "Errors.Project.Grant.Member.Invalid") + } + if len(domain.CheckForInvalidRoles(i.Roles, domain.ProjectGrantRolePrefix, zitadelRoles)) > 0 { + return zerrors.ThrowInvalidArgument(nil, "PROJECT-m9gKK", "Errors.Project.Grant.Member.Invalid") + } + return nil +} + +func (c *Commands) AddProjectGrantMember(ctx context.Context, member *AddProjectGrantMember) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if !member.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-8fi7G", "Errors.Project.Grant.Member.Invalid") + if err := member.IsValid(c.zitadelRoles); err != nil { + return nil, err } - if len(domain.CheckForInvalidRoles(member.Roles, domain.ProjectGrantRolePrefix, c.zitadelRoles)) > 0 { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-m9gKK", "Errors.Project.Grant.Member.Invalid") - } - err = c.checkUserExists(ctx, member.UserID, "") + _, err = c.checkUserExists(ctx, member.UserID, "") if err != nil { return nil, err } - addedMember := NewProjectGrantMemberWriteModel(member.AggregateID, member.UserID, member.GrantID) - projectAgg := ProjectAggregateFromWriteModel(&addedMember.WriteModel) + grantedOrgID, projectGrantResourceOwner, err := c.checkProjectGrantExists(ctx, member.GrantID, "", member.ProjectID, "") + if err != nil { + return nil, err + } + if member.ResourceOwner == "" { + member.ResourceOwner = projectGrantResourceOwner + } + addedMember, err := c.projectGrantMemberWriteModelByID(ctx, member.ProjectID, member.UserID, member.GrantID, member.ResourceOwner) + if err != nil { + return nil, err + } + // TODO: change e2e tests to use correct resourceowner, wrong resource owner is corrected through aggregate + // error if provided resourceowner is not equal to the resourceowner of the project grant + //if projectGrantResourceOwner != addedMember.ResourceOwner { + // return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-0l10S9OmZV", "Errors.Project.Grant.Invalid") + //} + if addedMember.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-37fug", "Errors.AlreadyExists") + } + if err := c.checkPermissionUpdateProjectGrantMember(ctx, grantedOrgID, addedMember.GrantID); err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.Push( ctx, - project.NewProjectGrantMemberAddedEvent(ctx, projectAgg, member.UserID, member.GrantID, member.Roles...)) + project.NewProjectGrantMemberAddedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &addedMember.WriteModel), + member.UserID, + member.GrantID, + member.Roles..., + )) if err != nil { return nil, err } @@ -38,30 +80,58 @@ func (c *Commands) AddProjectGrantMember(ctx context.Context, member *domain.Pro return nil, err } - return memberWriteModelToProjectGrantMember(addedMember), nil + return writeModelToObjectDetails(&addedMember.WriteModel), nil +} + +type ChangeProjectGrantMember struct { + UserID string + GrantID string + ProjectID string + Roles []string +} + +func (i *ChangeProjectGrantMember) IsValid(zitadelRoles []authz.RoleMapping) error { + if i.ProjectID == "" || i.GrantID == "" || i.UserID == "" || len(i.Roles) == 0 { + return zerrors.ThrowInvalidArgument(nil, "PROJECT-109fs", "Errors.Project.Grant.Member.Invalid") + } + if len(domain.CheckForInvalidRoles(i.Roles, domain.ProjectGrantRolePrefix, zitadelRoles)) > 0 { + return zerrors.ThrowInvalidArgument(nil, "PROJECT-m0sDf", "Errors.Project.Grant.Member.Invalid") + } + return nil } // ChangeProjectGrantMember updates an existing member -func (c *Commands) ChangeProjectGrantMember(ctx context.Context, member *domain.ProjectGrantMember) (*domain.ProjectGrantMember, error) { - if !member.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-109fs", "Errors.Project.Member.Invalid") +func (c *Commands) ChangeProjectGrantMember(ctx context.Context, member *ChangeProjectGrantMember) (*domain.ObjectDetails, error) { + if err := member.IsValid(c.zitadelRoles); err != nil { + return nil, err } - if len(domain.CheckForInvalidRoles(member.Roles, domain.ProjectGrantRolePrefix, c.zitadelRoles)) > 0 { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-m0sDf", "Errors.Project.Member.Invalid") - } - - existingMember, err := c.projectGrantMemberWriteModelByID(ctx, member.AggregateID, member.UserID, member.GrantID) + existingGrant, err := c.projectGrantWriteModelByID(ctx, member.GrantID, "", member.ProjectID, "") if err != nil { return nil, err } - - if reflect.DeepEqual(existingMember.Roles, member.Roles) { - return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-2n8vx", "Errors.Project.Member.RolesNotChanged") + existingMember, err := c.projectGrantMemberWriteModelByID(ctx, member.ProjectID, member.UserID, member.GrantID, existingGrant.ResourceOwner) + if err != nil { + return nil, err } - projectAgg := ProjectAggregateFromWriteModel(&existingMember.WriteModel) + if !existingMember.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-37fug", "Errors.NotFound") + } + + if err := c.checkPermissionUpdateProjectGrantMember(ctx, existingGrant.GrantedOrgID, existingMember.GrantID); err != nil { + return nil, err + } + if slices.Compare(existingMember.Roles, member.Roles) == 0 { + return writeModelToObjectDetails(&existingMember.WriteModel), nil + } + pushedEvents, err := c.eventstore.Push( ctx, - project.NewProjectGrantMemberChangedEvent(ctx, projectAgg, member.UserID, member.GrantID, member.Roles...)) + project.NewProjectGrantMemberChangedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel), + member.UserID, + member.GrantID, + member.Roles..., + )) if err != nil { return nil, err } @@ -70,29 +140,43 @@ func (c *Commands) ChangeProjectGrantMember(ctx context.Context, member *domain. return nil, err } - return memberWriteModelToProjectGrantMember(existingMember), nil + return writeModelToObjectDetails(&existingMember.WriteModel), nil } func (c *Commands) RemoveProjectGrantMember(ctx context.Context, projectID, userID, grantID string) (*domain.ObjectDetails, error) { if projectID == "" || userID == "" || grantID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-66mHd", "Errors.Project.Member.Invalid") } - m, err := c.projectGrantMemberWriteModelByID(ctx, projectID, userID, grantID) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, "", projectID, "") if err != nil { return nil, err } + existingMember, err := c.projectGrantMemberWriteModelByID(ctx, projectID, userID, grantID, existingGrant.ResourceOwner) + if err != nil { + return nil, err + } + if !existingMember.State.Exists() { + return writeModelToObjectDetails(&existingMember.WriteModel), nil + } + if err := c.checkPermissionDeleteProjectGrantMember(ctx, existingGrant.GrantedOrgID, existingMember.GrantID); err != nil { + return nil, err + } - projectAgg := ProjectAggregateFromWriteModel(&m.WriteModel) - removeEvent := c.removeProjectGrantMember(ctx, projectAgg, userID, grantID, false) + removeEvent := c.removeProjectGrantMember(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel), + userID, + grantID, + false, + ) pushedEvents, err := c.eventstore.Push(ctx, removeEvent) if err != nil { return nil, err } - err = AppendAndReduce(m, pushedEvents...) + err = AppendAndReduce(existingMember, pushedEvents...) if err != nil { return nil, err } - return writeModelToObjectDetails(&m.WriteModel), nil + return writeModelToObjectDetails(&existingMember.WriteModel), nil } func (c *Commands) removeProjectGrantMember(ctx context.Context, projectAgg *eventstore.Aggregate, userID, grantID string, cascade bool) eventstore.Command { @@ -107,19 +191,15 @@ func (c *Commands) removeProjectGrantMember(ctx context.Context, projectAgg *eve } } -func (c *Commands) projectGrantMemberWriteModelByID(ctx context.Context, projectID, userID, grantID string) (member *ProjectGrantMemberWriteModel, err error) { +func (c *Commands) projectGrantMemberWriteModelByID(ctx context.Context, projectID, userID, grantID, resourceOwner string) (member *ProjectGrantMemberWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - writeModel := NewProjectGrantMemberWriteModel(projectID, userID, grantID) + writeModel := NewProjectGrantMemberWriteModel(projectID, userID, grantID, resourceOwner) err = c.eventstore.FilterToQueryReducer(ctx, writeModel) if err != nil { return nil, err } - if writeModel.State == domain.MemberStateUnspecified || writeModel.State == domain.MemberStateRemoved { - return nil, zerrors.ThrowNotFound(nil, "PROJECT-37fug", "Errors.NotFound") - } - return writeModel, nil } diff --git a/internal/command/project_grant_member_model.go b/internal/command/project_grant_member_model.go index 6c089ba1c8..1d9ab02478 100644 --- a/internal/command/project_grant_member_model.go +++ b/internal/command/project_grant_member_model.go @@ -16,10 +16,11 @@ type ProjectGrantMemberWriteModel struct { State domain.MemberState } -func NewProjectGrantMemberWriteModel(projectID, userID, grantID string) *ProjectGrantMemberWriteModel { +func NewProjectGrantMemberWriteModel(projectID, userID, grantID, resourceOwner string) *ProjectGrantMemberWriteModel { return &ProjectGrantMemberWriteModel{ WriteModel: eventstore.WriteModel{ - AggregateID: projectID, + AggregateID: projectID, + ResourceOwner: resourceOwner, }, UserID: userID, GrantID: grantID, @@ -66,6 +67,7 @@ func (wm *ProjectGrantMemberWriteModel) Reduce() error { case *project.GrantMemberAddedEvent: wm.Roles = e.Roles wm.State = domain.MemberStateActive + wm.ResourceOwner = e.Aggregate().ResourceOwner case *project.GrantMemberChangedEvent: wm.Roles = e.Roles case *project.GrantMemberRemovedEvent: @@ -80,7 +82,8 @@ func (wm *ProjectGrantMemberWriteModel) Reduce() error { } func (wm *ProjectGrantMemberWriteModel) Query() *eventstore.SearchQueryBuilder { - return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). AddQuery(). AggregateTypes(project.AggregateType). AggregateIDs(wm.AggregateID). @@ -92,4 +95,5 @@ func (wm *ProjectGrantMemberWriteModel) Query() *eventstore.SearchQueryBuilder { project.GrantRemovedType, project.ProjectRemovedType). Builder() + return query } diff --git a/internal/command/project_grant_member_test.go b/internal/command/project_grant_member_test.go index 189d1b9911..3cd8ecdd82 100644 --- a/internal/command/project_grant_member_test.go +++ b/internal/command/project_grant_member_test.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/repository/project" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" @@ -18,15 +17,15 @@ import ( func TestCommandSide_AddProjectGrantMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - zitadelRoles []authz.RoleMapping + eventstore func(t *testing.T) *eventstore.Eventstore + zitadelRoles []authz.RoleMapping + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - member *domain.ProjectGrantMember + member *AddProjectGrantMember } type res struct { - want *domain.ProjectGrantMember + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -38,16 +37,12 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { { name: "invalid member, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, + member: &AddProjectGrantMember{ + ProjectID: "project1", }, }, res: res{ @@ -57,19 +52,15 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { { name: "invalid roles, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + member: &AddProjectGrantMember{ + ProjectID: "project1", + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, }, }, res: res{ @@ -79,10 +70,10 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { { name: "user not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "PROJECT_GRANT_OWNER", @@ -90,14 +81,11 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + member: &AddProjectGrantMember{ + ProjectID: "project1", + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, }, }, res: res{ @@ -107,8 +95,7 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { { name: "member add uniqueconstraint err, already exists", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -125,15 +112,28 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + ), + ), + ), + ), + expectFilter(), expectPushFailed(zerrors.ThrowAlreadyExists(nil, "ERROR", "internal"), project.NewProjectGrantMemberAddedEvent(context.Background(), - &project.NewAggregate("project1", "").Aggregate, + &project.NewAggregate("project1", "org1").Aggregate, "user1", "projectgrant1", []string{"PROJECT_GRANT_OWNER"}..., ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "PROJECT_GRANT_OWNER", @@ -141,14 +141,11 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + member: &AddProjectGrantMember{ + ProjectID: "project1", + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, }, }, res: res{ @@ -158,8 +155,7 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { { name: "member add, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -176,15 +172,28 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + ), + ), + ), + ), + expectFilter(), expectPush( project.NewProjectGrantMemberAddedEvent(context.Background(), - &project.NewAggregate("project1", "").Aggregate, + &project.NewAggregate("project1", "org1").Aggregate, "user1", "projectgrant1", []string{"PROJECT_GRANT_OWNER"}..., ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "PROJECT_GRANT_OWNER", @@ -192,35 +201,81 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - UserID: "user1", - GrantID: "projectgrant1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + member: &AddProjectGrantMember{ + ProjectID: "project1", + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, }, }, res: res{ - want: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "project1", }, }, }, + { + name: "member add, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + ), + ), + ), + ), + expectFilter(), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + zitadelRoles: []authz.RoleMapping{ + { + Role: "PROJECT_GRANT_OWNER", + }, + }, + }, + args: args{ + member: &AddProjectGrantMember{ + ProjectID: "project1", + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - zitadelRoles: tt.fields.zitadelRoles, + eventstore: tt.fields.eventstore(t), + zitadelRoles: tt.fields.zitadelRoles, + checkPermission: tt.fields.checkPermission, } - got, err := r.AddProjectGrantMember(tt.args.ctx, tt.args.member) + got, err := r.AddProjectGrantMember(context.Background(), tt.args.member) if tt.res.err == nil { assert.NoError(t, err) } @@ -236,15 +291,15 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - zitadelRoles []authz.RoleMapping + eventstore func(t *testing.T) *eventstore.Eventstore + zitadelRoles []authz.RoleMapping + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - member *domain.ProjectGrantMember + member *ChangeProjectGrantMember } type res struct { - want *domain.ProjectGrantMember + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -256,16 +311,12 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { { name: "invalid member, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, + member: &ChangeProjectGrantMember{ + ProjectID: "project1", }, }, res: res{ @@ -275,19 +326,15 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { { name: "invalid roles, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_OWNER"}, + member: &ChangeProjectGrantMember{ + ProjectID: "project1", + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, }, }, res: res{ @@ -297,10 +344,20 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { { name: "member not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "org2", + []string{"rol1", "role2"}, + ), + ), + ), expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "PROJECT_GRANT_OWNER", @@ -308,14 +365,11 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + member: &ChangeProjectGrantMember{ + ProjectID: "project1", + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, }, }, res: res{ @@ -325,8 +379,17 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { { name: "member not changed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "org2", + []string{"rol1", "role2"}, + ), + ), + ), expectFilter( eventFromEventPusher( project.NewProjectGrantMemberAddedEvent(context.Background(), @@ -338,6 +401,7 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "PROJECT_GRANT_OWNER", @@ -345,25 +409,33 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + member: &ChangeProjectGrantMember{ + ProjectID: "project1", + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, }, }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "member change, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "org2", + []string{"rol1", "role2"}, + ), + ), + ), expectFilter( eventFromEventPusher( project.NewProjectGrantMemberAddedEvent(context.Background(), @@ -383,6 +455,7 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "PROJECT_GRANT_OWNER", @@ -393,36 +466,75 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"}, + member: &ChangeProjectGrantMember{ + ProjectID: "project1", + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"}, }, }, res: res{ - want: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - ResourceOwner: "org1", - AggregateID: "project1", - }, - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"}, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, + { + name: "member change, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "org2", + []string{"rol1", "role2"}, + ), + ), + ), + expectFilter( + eventFromEventPusher( + project.NewProjectGrantMemberAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + "projectgrant1", + []string{"PROJECT_GRANT_OWNER"}..., + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + zitadelRoles: []authz.RoleMapping{ + { + Role: "PROJECT_GRANT_OWNER", + }, + { + Role: "PROJECT_GRANT_VIEWER", + }, + }, + }, + args: args{ + member: &ChangeProjectGrantMember{ + ProjectID: "project1", + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"}, + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - zitadelRoles: tt.fields.zitadelRoles, + eventstore: tt.fields.eventstore(t), + zitadelRoles: tt.fields.zitadelRoles, + checkPermission: tt.fields.checkPermission, } - got, err := r.ChangeProjectGrantMember(tt.args.ctx, tt.args.member) + got, err := r.ChangeProjectGrantMember(context.Background(), tt.args.member) if tt.res.err == nil { assert.NoError(t, err) } @@ -430,7 +542,7 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -438,7 +550,8 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { func TestCommandSide_RemoveProjectGrantMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -459,9 +572,8 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) { { name: "invalid member projectid missing, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -476,9 +588,8 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) { { name: "invalid member userid missing, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -493,9 +604,8 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) { { name: "invalid member grantid missing, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -508,12 +618,22 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) { }, }, { - name: "member not existing, not found err", + name: "member not existing, not found ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "org2", + []string{"rol1", "role2"}, + ), + ), + ), expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -522,14 +642,25 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) { grantID: "projectgrant1", }, res: res{ - err: zerrors.IsNotFound, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "member remove, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "org2", + []string{"rol1", "role2"}, + ), + ), + ), expectFilter( eventFromEventPusher( project.NewProjectGrantMemberAddedEvent(context.Background(), @@ -548,6 +679,7 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -561,11 +693,49 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) { }, }, }, + { + name: "member remove, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "org2", + []string{"rol1", "role2"}, + ), + ), + ), + expectFilter( + eventFromEventPusher( + project.NewProjectGrantMemberAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + "projectgrant1", + []string{"PROJECT_OWNER"}..., + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + userID: "user1", + grantID: "projectgrant1", + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.RemoveProjectGrantMember(tt.args.ctx, tt.args.projectID, tt.args.userID, tt.args.grantID) if tt.res.err == nil { diff --git a/internal/command/project_member.go b/internal/command/project_member.go index a2e4fae553..1bbd358490 100644 --- a/internal/command/project_member.go +++ b/internal/command/project_member.go @@ -2,8 +2,9 @@ package command import ( "context" - "reflect" + "slices" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/project" @@ -11,18 +12,64 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddProjectMember(ctx context.Context, member *domain.Member, resourceOwner string) (_ *domain.Member, err error) { +type AddProjectMember struct { + ResourceOwner string + ProjectID string + UserID string + Roles []string +} + +func (i *AddProjectMember) IsValid(zitadelRoles []authz.RoleMapping) error { + if i.ProjectID == "" || i.UserID == "" || len(i.Roles) == 0 { + return zerrors.ThrowInvalidArgument(nil, "PROJECT-W8m4l", "Errors.Project.Member.Invalid") + } + if len(domain.CheckForInvalidRoles(i.Roles, domain.ProjectRolePrefix, zitadelRoles)) > 0 { + return zerrors.ThrowInvalidArgument(nil, "PROJECT-3m9ds", "Errors.Project.Member.Invalid") + } + return nil +} + +func (c *Commands) AddProjectMember(ctx context.Context, member *AddProjectMember) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - addedMember := NewProjectMemberWriteModel(member.AggregateID, member.UserID, resourceOwner) - projectAgg := ProjectAggregateFromWriteModel(&addedMember.WriteModel) - event, err := c.addProjectMember(ctx, projectAgg, addedMember, member) + if err := member.IsValid(c.zitadelRoles); err != nil { + return nil, err + } + _, err = c.checkUserExists(ctx, member.UserID, "") if err != nil { return nil, err } + projectResourceOwner, err := c.checkProjectExists(ctx, member.ProjectID, member.ResourceOwner) + if err != nil { + return nil, err + } + // resourceowner of the member if not provided is the resourceowner of the project + if member.ResourceOwner == "" { + member.ResourceOwner = projectResourceOwner + } + addedMember, err := c.projectMemberWriteModelByID(ctx, member.ProjectID, member.UserID, member.ResourceOwner) + if err != nil { + return nil, err + } + // error if provided resourceowner is not equal to the resourceowner of the project + if projectResourceOwner != addedMember.ResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-0l10S9OmZV", "Errors.Project.Member.Invalid") + } + if err := c.checkPermissionUpdateProjectMember(ctx, addedMember.ResourceOwner, addedMember.AggregateID); err != nil { + return nil, err + } + if addedMember.State.Exists() { + return nil, zerrors.ThrowAlreadyExists(nil, "PROJECT-PtXi1", "Errors.Project.Member.AlreadyExists") + } - pushedEvents, err := c.eventstore.Push(ctx, event) + pushedEvents, err := c.eventstore.Push(ctx, + project.NewProjectMemberAddedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &addedMember.WriteModel), + member.UserID, + member.Roles..., + ), + ) if err != nil { return nil, err } @@ -31,53 +78,46 @@ func (c *Commands) AddProjectMember(ctx context.Context, member *domain.Member, return nil, err } - return memberWriteModelToMember(&addedMember.MemberWriteModel), nil + return writeModelToObjectDetails(&addedMember.WriteModel), nil } -func (c *Commands) addProjectMember(ctx context.Context, projectAgg *eventstore.Aggregate, addedMember *ProjectMemberWriteModel, member *domain.Member) (_ eventstore.Command, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() +type ChangeProjectMember struct { + ResourceOwner string + ProjectID string + UserID string + Roles []string +} - if !member.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-W8m4l", "Errors.Project.Member.Invalid") +func (i *ChangeProjectMember) IsValid(zitadelRoles []authz.RoleMapping) error { + if i.ProjectID == "" || i.UserID == "" || len(i.Roles) == 0 { + return zerrors.ThrowInvalidArgument(nil, "PROJECT-LiaZi", "Errors.Project.Member.Invalid") } - if len(domain.CheckForInvalidRoles(member.Roles, domain.ProjectRolePrefix, c.zitadelRoles)) > 0 { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-3m9ds", "Errors.Project.Member.Invalid") + if len(domain.CheckForInvalidRoles(i.Roles, domain.ProjectRolePrefix, zitadelRoles)) > 0 { + return zerrors.ThrowInvalidArgument(nil, "PROJECT-3m9d", "Errors.Project.Member.Invalid") } - - err = c.checkUserExists(ctx, addedMember.UserID, "") - if err != nil { - return nil, err - } - err = c.eventstore.FilterToQueryReducer(ctx, addedMember) - if err != nil { - return nil, err - } - if addedMember.State == domain.MemberStateActive { - return nil, zerrors.ThrowAlreadyExists(nil, "PROJECT-PtXi1", "Errors.Project.Member.AlreadyExists") - } - - return project.NewProjectMemberAddedEvent(ctx, projectAgg, member.UserID, member.Roles...), nil + return nil } // ChangeProjectMember updates an existing member -func (c *Commands) ChangeProjectMember(ctx context.Context, member *domain.Member, resourceOwner string) (*domain.Member, error) { - if !member.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-LiaZi", "Errors.Project.Member.Invalid") - } - if len(domain.CheckForInvalidRoles(member.Roles, domain.ProjectRolePrefix, c.zitadelRoles)) > 0 { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-3m9d", "Errors.Project.Member.Invalid") - } - - existingMember, err := c.projectMemberWriteModelByID(ctx, member.AggregateID, member.UserID, resourceOwner) - if err != nil { +func (c *Commands) ChangeProjectMember(ctx context.Context, member *ChangeProjectMember) (*domain.ObjectDetails, error) { + if err := member.IsValid(c.zitadelRoles); err != nil { return nil, err } - if reflect.DeepEqual(existingMember.Roles, member.Roles) { - return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-LiaZi", "Errors.Project.Member.RolesNotChanged") + existingMember, err := c.projectMemberWriteModelByID(ctx, member.ProjectID, member.UserID, member.ResourceOwner) + if err != nil { + return nil, err } - projectAgg := ProjectAggregateFromWriteModel(&existingMember.MemberWriteModel.WriteModel) + if !existingMember.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.NotFound") + } + if err := c.checkPermissionUpdateProjectMember(ctx, existingMember.ResourceOwner, existingMember.AggregateID); err != nil { + return nil, err + } + if slices.Compare(existingMember.Roles, member.Roles) == 0 { + return writeModelToObjectDetails(&existingMember.WriteModel), nil + } + projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel) pushedEvents, err := c.eventstore.Push(ctx, project.NewProjectMemberChangedEvent(ctx, projectAgg, member.UserID, member.Roles...)) if err != nil { return nil, err @@ -88,33 +128,35 @@ func (c *Commands) ChangeProjectMember(ctx context.Context, member *domain.Membe return nil, err } - return memberWriteModelToMember(&existingMember.MemberWriteModel), nil + return writeModelToObjectDetails(&existingMember.WriteModel), nil } func (c *Commands) RemoveProjectMember(ctx context.Context, projectID, userID, resourceOwner string) (*domain.ObjectDetails, error) { if projectID == "" || userID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-66mHd", "Errors.Project.Member.Invalid") } - m, err := c.projectMemberWriteModelByID(ctx, projectID, userID, resourceOwner) - if err != nil && !zerrors.IsNotFound(err) { + existingMember, err := c.projectMemberWriteModelByID(ctx, projectID, userID, resourceOwner) + if err != nil { return nil, err } - if zerrors.IsNotFound(err) { - // empty response because we have no data that match the request - return &domain.ObjectDetails{}, nil + if !existingMember.State.Exists() { + return writeModelToObjectDetails(&existingMember.WriteModel), nil + } + if err := c.checkPermissionDeleteProjectMember(ctx, existingMember.ResourceOwner, existingMember.AggregateID); err != nil { + return nil, err } - projectAgg := ProjectAggregateFromWriteModel(&m.MemberWriteModel.WriteModel) + projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel) removeEvent := c.removeProjectMember(ctx, projectAgg, userID, false) pushedEvents, err := c.eventstore.Push(ctx, removeEvent) if err != nil { return nil, err } - err = AppendAndReduce(m, pushedEvents...) + err = AppendAndReduce(existingMember, pushedEvents...) if err != nil { return nil, err } - return writeModelToObjectDetails(&m.WriteModel), nil + return writeModelToObjectDetails(&existingMember.WriteModel), nil } func (c *Commands) removeProjectMember(ctx context.Context, projectAgg *eventstore.Aggregate, userID string, cascade bool) eventstore.Command { @@ -138,9 +180,5 @@ func (c *Commands) projectMemberWriteModelByID(ctx context.Context, projectID, u return nil, err } - if writeModel.State == domain.MemberStateUnspecified || writeModel.State == domain.MemberStateRemoved { - return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.NotFound") - } - return writeModel, nil } diff --git a/internal/command/project_member_test.go b/internal/command/project_member_test.go index 88b52f63f8..1bd06e0b99 100644 --- a/internal/command/project_member_test.go +++ b/internal/command/project_member_test.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/repository/project" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" @@ -18,16 +17,15 @@ import ( func TestCommandSide_AddProjectMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - zitadelRoles []authz.RoleMapping + eventstore func(t *testing.T) *eventstore.Eventstore + zitadelRoles []authz.RoleMapping + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - member *domain.Member - resourceOwner string + member *AddProjectMember } type res struct { - want *domain.Member + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -39,18 +37,14 @@ func TestCommandSide_AddProjectMember(t *testing.T) { { name: "invalid member, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, + member: &AddProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -59,20 +53,16 @@ func TestCommandSide_AddProjectMember(t *testing.T) { { name: "invalid roles, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{"PROJECT_OWNER"}, + member: &AddProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -81,10 +71,10 @@ func TestCommandSide_AddProjectMember(t *testing.T) { { name: "user not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleProjectOwner, @@ -92,15 +82,12 @@ func TestCommandSide_AddProjectMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{"PROJECT_OWNER"}, + member: &AddProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -109,8 +96,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) { { name: "member already exists, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -127,6 +113,19 @@ func TestCommandSide_AddProjectMember(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + false, + false, + false, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), expectFilter( eventFromEventPusher( project.NewProjectMemberAddedEvent(context.Background(), @@ -136,6 +135,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleProjectOwner, @@ -143,15 +143,12 @@ func TestCommandSide_AddProjectMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{"PROJECT_OWNER"}, + member: &AddProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorAlreadyExists, @@ -160,8 +157,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) { { name: "member add uniqueconstraint err, already exists", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -178,6 +174,19 @@ func TestCommandSide_AddProjectMember(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + false, + false, + false, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), expectFilter(), expectPushFailed(zerrors.ThrowAlreadyExists(nil, "ERROR", "internal"), project.NewProjectMemberAddedEvent(context.Background(), @@ -187,6 +196,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleProjectOwner, @@ -194,15 +204,12 @@ func TestCommandSide_AddProjectMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{"PROJECT_OWNER"}, + member: &AddProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorAlreadyExists, @@ -211,8 +218,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) { { name: "member add, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -229,6 +235,19 @@ func TestCommandSide_AddProjectMember(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + false, + false, + false, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), expectFilter(), expectPush( project.NewProjectMemberAddedEvent(context.Background(), @@ -238,6 +257,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleProjectOwner, @@ -245,35 +265,81 @@ func TestCommandSide_AddProjectMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{"PROJECT_OWNER"}, + member: &AddProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, }, - resourceOwner: "org1", }, res: res{ - want: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - ResourceOwner: "org1", - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{domain.RoleProjectOwner}, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, + }, { + name: "member add, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + false, + false, + false, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), + expectFilter(), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + zitadelRoles: []authz.RoleMapping{ + { + Role: domain.RoleProjectOwner, + }, + }, + }, + args: args{ + member: &AddProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - zitadelRoles: tt.fields.zitadelRoles, + eventstore: tt.fields.eventstore(t), + zitadelRoles: tt.fields.zitadelRoles, + checkPermission: tt.fields.checkPermission, } - got, err := r.AddProjectMember(tt.args.ctx, tt.args.member, tt.args.resourceOwner) + got, err := r.AddProjectMember(context.Background(), tt.args.member) if tt.res.err == nil { assert.NoError(t, err) } @@ -281,7 +347,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -289,16 +355,15 @@ func TestCommandSide_AddProjectMember(t *testing.T) { func TestCommandSide_ChangeProjectMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - zitadelRoles []authz.RoleMapping + eventstore func(t *testing.T) *eventstore.Eventstore + zitadelRoles []authz.RoleMapping + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - member *domain.Member - resourceOwner string + member *ChangeProjectMember } type res struct { - want *domain.Member + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -310,18 +375,14 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { { name: "invalid member, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, + member: &ChangeProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -330,20 +391,16 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { { name: "invalid roles, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{"PROJECT_OWNER"}, + member: &ChangeProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -352,10 +409,10 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { { name: "member not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleProjectOwner, @@ -363,15 +420,12 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{"PROJECT_OWNER"}, + member: &ChangeProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsNotFound, @@ -380,8 +434,7 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { { name: "member not changed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectMemberAddedEvent(context.Background(), @@ -392,6 +445,7 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleProjectOwner, @@ -399,25 +453,23 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{"PROJECT_OWNER"}, + member: &ChangeProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, }, - resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "member change, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectMemberAddedEvent(context.Background(), @@ -435,6 +487,7 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleProjectOwner, @@ -445,35 +498,64 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{"PROJECT_OWNER", "PROJECT_VIEWER"}, + member: &ChangeProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER", "PROJECT_VIEWER"}, }, - resourceOwner: "org1", }, res: res{ - want: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - ResourceOwner: "org1", - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{domain.RoleProjectOwner, "PROJECT_VIEWER"}, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, + { + name: "member change, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectMemberAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + []string{"PROJECT_OWNER"}..., + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + zitadelRoles: []authz.RoleMapping{ + { + Role: domain.RoleProjectOwner, + }, + { + Role: "PROJECT_VIEWER", + }, + }, + }, + args: args{ + member: &ChangeProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER", "PROJECT_VIEWER"}, + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - zitadelRoles: tt.fields.zitadelRoles, + eventstore: tt.fields.eventstore(t), + zitadelRoles: tt.fields.zitadelRoles, + checkPermission: tt.fields.checkPermission, } - got, err := r.ChangeProjectMember(tt.args.ctx, tt.args.member, tt.args.resourceOwner) + got, err := r.ChangeProjectMember(context.Background(), tt.args.member) if tt.res.err == nil { assert.NoError(t, err) } @@ -481,7 +563,7 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -489,10 +571,10 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { func TestCommandSide_RemoveProjectMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context projectID string userID string resourceOwner string @@ -510,12 +592,10 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) { { name: "invalid member projectid missing, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), projectID: "", userID: "user1", resourceOwner: "org1", @@ -527,12 +607,10 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) { { name: "invalid member userid missing, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), projectID: "project1", userID: "", resourceOwner: "org1", @@ -544,26 +622,26 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) { { name: "member not existing, empty object details result", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), projectID: "project1", userID: "user1", resourceOwner: "org1", }, res: res{ - want: &domain.ObjectDetails{}, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "member remove, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectMemberAddedEvent(context.Background(), @@ -580,9 +658,9 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), projectID: "project1", userID: "user1", resourceOwner: "org1", @@ -593,13 +671,39 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) { }, }, }, + { + name: "member remove, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectMemberAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + []string{"PROJECT_OWNER"}..., + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + projectID: "project1", + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } - got, err := r.RemoveProjectMember(tt.args.ctx, tt.args.projectID, tt.args.userID, tt.args.resourceOwner) + got, err := r.RemoveProjectMember(context.Background(), tt.args.projectID, tt.args.userID, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/user.go b/internal/command/user.go index 0db4fda328..d834169f8a 100644 --- a/internal/command/user.go +++ b/internal/command/user.go @@ -194,7 +194,7 @@ func (c *Commands) RemoveUser(ctx context.Context, userID, resourceOwner string, events = append(events, user.NewUserRemovedEvent(ctx, userAgg, existingUser.UserName, existingUser.IDPLinks, domainPolicy.UserLoginMustBeDomain)) for _, grantID := range cascadingGrantIDs { - removeEvent, _, err := c.removeUserGrant(ctx, grantID, "", true) + removeEvent, _, err := c.removeUserGrant(ctx, grantID, "", true, false, nil) if err != nil { logging.WithFields("usergrantid", grantID).WithError(err).Warn("could not cascade remove role on user grant") continue @@ -327,18 +327,18 @@ func (c *Commands) UserDomainClaimedSent(ctx context.Context, orgID, userID stri return err } -func (c *Commands) checkUserExists(ctx context.Context, userID, resourceOwner string) (err error) { +func (c *Commands) checkUserExists(ctx context.Context, userID, resourceOwner string) (_ string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() existingUser, err := c.userWriteModelByID(ctx, userID, resourceOwner) if err != nil { - return err + return "", err } if !isUserStateExists(existingUser.UserState) { - return zerrors.ThrowPreconditionFailed(nil, "COMMAND-uXHNj", "Errors.User.NotFound") + return "", zerrors.ThrowPreconditionFailed(nil, "COMMAND-uXHNj", "Errors.User.NotFound") } - return nil + return existingUser.ResourceOwner, nil } func (c *Commands) userWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *UserWriteModel, err error) { diff --git a/internal/command/user_grant.go b/internal/command/user_grant.go index 6bb4a20b0a..2b0ff4cf18 100644 --- a/internal/command/user_grant.go +++ b/internal/command/user_grant.go @@ -2,7 +2,7 @@ package command import ( "context" - "reflect" + "slices" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" @@ -15,11 +15,14 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddUserGrant(ctx context.Context, usergrant *domain.UserGrant, resourceOwner string) (_ *domain.UserGrant, err error) { +// AddUserGrant authorizes a user for a project with the given role keys. +// The project must be owned by or granted to the resourceOwner. +// If the resourceOwner is nil, the project must be owned by the project that belongs to usergrant.ProjectID. +func (c *Commands) AddUserGrant(ctx context.Context, usergrant *domain.UserGrant, check UserGrantPermissionCheck) (_ *domain.UserGrant, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - event, addedUserGrant, err := c.addUserGrant(ctx, usergrant, resourceOwner) + event, addedUserGrant, err := c.addUserGrant(ctx, usergrant, check) if err != nil { return nil, err } @@ -35,11 +38,11 @@ func (c *Commands) AddUserGrant(ctx context.Context, usergrant *domain.UserGrant return userGrantWriteModelToUserGrant(addedUserGrant), nil } -func (c *Commands) addUserGrant(ctx context.Context, userGrant *domain.UserGrant, resourceOwner string) (command eventstore.Command, _ *UserGrantWriteModel, err error) { +func (c *Commands) addUserGrant(ctx context.Context, userGrant *domain.UserGrant, check UserGrantPermissionCheck) (command eventstore.Command, _ *UserGrantWriteModel, err error) { if !userGrant.IsValid() { return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-kVfMa", "Errors.UserGrant.Invalid") } - err = c.checkUserGrantPreCondition(ctx, userGrant, resourceOwner) + err = c.checkUserGrantPreCondition(ctx, userGrant, check) if err != nil { return nil, nil, err } @@ -48,7 +51,7 @@ func (c *Commands) addUserGrant(ctx context.Context, userGrant *domain.UserGrant return nil, nil, err } - addedUserGrant := NewUserGrantWriteModel(userGrant.AggregateID, resourceOwner) + addedUserGrant := NewUserGrantWriteModel(userGrant.AggregateID, userGrant.ResourceOwner) userGrantAgg := UserGrantAggregateFromWriteModel(&addedUserGrant.WriteModel) command = usergrant.NewUserGrantAddedEvent( ctx, @@ -61,54 +64,51 @@ func (c *Commands) addUserGrant(ctx context.Context, userGrant *domain.UserGrant return command, addedUserGrant, nil } -func (c *Commands) ChangeUserGrant(ctx context.Context, userGrant *domain.UserGrant, resourceOwner string) (_ *domain.UserGrant, err error) { - event, changedUserGrant, err := c.changeUserGrant(ctx, userGrant, resourceOwner, false) +func (c *Commands) ChangeUserGrant(ctx context.Context, userGrant *domain.UserGrant, cascade, ignoreUnchanged bool, check UserGrantPermissionCheck) (_ *domain.UserGrant, err error) { + if userGrant.AggregateID == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-3M0sd", "Errors.UserGrant.Invalid") + } + existingUserGrant, err := c.userGrantWriteModelByID(ctx, userGrant.AggregateID, "") if err != nil { return nil, err } + if existingUserGrant.State == domain.UserGrantStateUnspecified || existingUserGrant.State == domain.UserGrantStateRemoved { + return nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.UserGrant.NotFound") + } + + grantUnchanged := slices.Equal(existingUserGrant.RoleKeys, userGrant.RoleKeys) + if grantUnchanged { + if ignoreUnchanged { + return userGrantWriteModelToUserGrant(existingUserGrant), nil + } + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Rs8fy", "Errors.UserGrant.NotChanged") + } + userGrant.UserID = existingUserGrant.UserID + userGrant.ProjectID = existingUserGrant.ProjectID + userGrant.ProjectGrantID = existingUserGrant.ProjectGrantID + userGrant.ResourceOwner = existingUserGrant.ResourceOwner + + err = c.checkUserGrantPreCondition(ctx, userGrant, check) + if err != nil { + return nil, err + } + + changedUserGrant := NewUserGrantWriteModel(userGrant.AggregateID, userGrant.ResourceOwner) + userGrantAgg := UserGrantAggregateFromWriteModel(&changedUserGrant.WriteModel) + + var event eventstore.Command = usergrant.NewUserGrantChangedEvent(ctx, userGrantAgg, existingUserGrant.UserID, userGrant.RoleKeys) + if cascade { + event = usergrant.NewUserGrantCascadeChangedEvent(ctx, userGrantAgg, userGrant.RoleKeys) + } pushedEvents, err := c.eventstore.Push(ctx, event) if err != nil { return nil, err } - err = AppendAndReduce(changedUserGrant, pushedEvents...) + err = AppendAndReduce(existingUserGrant, pushedEvents...) if err != nil { return nil, err } - return userGrantWriteModelToUserGrant(changedUserGrant), nil -} - -func (c *Commands) changeUserGrant(ctx context.Context, userGrant *domain.UserGrant, resourceOwner string, cascade bool) (_ eventstore.Command, _ *UserGrantWriteModel, err error) { - if userGrant.AggregateID == "" { - return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-3M0sd", "Errors.UserGrant.Invalid") - } - existingUserGrant, err := c.userGrantWriteModelByID(ctx, userGrant.AggregateID, userGrant.ResourceOwner) - if err != nil { - return nil, nil, err - } - err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID) - if err != nil { - return nil, nil, err - } - if existingUserGrant.State == domain.UserGrantStateUnspecified || existingUserGrant.State == domain.UserGrantStateRemoved { - return nil, nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.UserGrant.NotFound") - } - if reflect.DeepEqual(existingUserGrant.RoleKeys, userGrant.RoleKeys) { - return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Rs8fy", "Errors.UserGrant.NotChanged") - } - userGrant.ProjectID = existingUserGrant.ProjectID - userGrant.ProjectGrantID = existingUserGrant.ProjectGrantID - err = c.checkUserGrantPreCondition(ctx, userGrant, resourceOwner) - if err != nil { - return nil, nil, err - } - - changedUserGrant := NewUserGrantWriteModel(userGrant.AggregateID, resourceOwner) - userGrantAgg := UserGrantAggregateFromWriteModel(&changedUserGrant.WriteModel) - - if cascade { - return usergrant.NewUserGrantCascadeChangedEvent(ctx, userGrantAgg, userGrant.RoleKeys), existingUserGrant, nil - } - return usergrant.NewUserGrantChangedEvent(ctx, userGrantAgg, existingUserGrant.UserID, userGrant.RoleKeys), existingUserGrant, nil + return userGrantWriteModelToUserGrant(existingUserGrant), nil } func (c *Commands) removeRoleFromUserGrant(ctx context.Context, userGrantID string, roleKeys []string, cascade bool) (_ eventstore.Command, err error) { @@ -144,8 +144,8 @@ func (c *Commands) removeRoleFromUserGrant(ctx context.Context, userGrantID stri return usergrant.NewUserGrantChangedEvent(ctx, userGrantAgg, existingUserGrant.UserID, existingUserGrant.RoleKeys), nil } -func (c *Commands) DeactivateUserGrant(ctx context.Context, grantID, resourceOwner string) (objectDetails *domain.ObjectDetails, err error) { - if grantID == "" || resourceOwner == "" { +func (c *Commands) DeactivateUserGrant(ctx context.Context, grantID string, resourceOwner string, check UserGrantPermissionCheck) (objectDetails *domain.ObjectDetails, err error) { + if grantID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-N2OhG", "Errors.UserGrant.IDMissing") } @@ -157,14 +157,17 @@ func (c *Commands) DeactivateUserGrant(ctx context.Context, grantID, resourceOwn return nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.UserGrant.NotFound") } if existingUserGrant.State != domain.UserGrantStateActive { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-1S9gx", "Errors.UserGrant.NotActive") + return writeModelToObjectDetails(&existingUserGrant.WriteModel), nil + } + if check != nil { + err = check(existingUserGrant.ProjectID, existingUserGrant.ProjectGrantID)(existingUserGrant.ResourceOwner, "") + } else { + err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID) } - err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID) if err != nil { return nil, err } - - deactivateUserGrant := NewUserGrantWriteModel(grantID, resourceOwner) + deactivateUserGrant := NewUserGrantWriteModel(grantID, existingUserGrant.ResourceOwner) userGrantAgg := UserGrantAggregateFromWriteModel(&deactivateUserGrant.WriteModel) pushedEvents, err := c.eventstore.Push(ctx, usergrant.NewUserGrantDeactivatedEvent(ctx, userGrantAgg)) if err != nil { @@ -177,8 +180,8 @@ func (c *Commands) DeactivateUserGrant(ctx context.Context, grantID, resourceOwn return writeModelToObjectDetails(&existingUserGrant.WriteModel), nil } -func (c *Commands) ReactivateUserGrant(ctx context.Context, grantID, resourceOwner string) (objectDetails *domain.ObjectDetails, err error) { - if grantID == "" || resourceOwner == "" { +func (c *Commands) ReactivateUserGrant(ctx context.Context, grantID string, resourceOwner string, check UserGrantPermissionCheck) (objectDetails *domain.ObjectDetails, err error) { + if grantID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Qxy8v", "Errors.UserGrant.IDMissing") } @@ -190,13 +193,17 @@ func (c *Commands) ReactivateUserGrant(ctx context.Context, grantID, resourceOwn return nil, zerrors.ThrowNotFound(nil, "COMMAND-Lp0gs", "Errors.UserGrant.NotFound") } if existingUserGrant.State != domain.UserGrantStateInactive { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-1ML0v", "Errors.UserGrant.NotInactive") + return writeModelToObjectDetails(&existingUserGrant.WriteModel), nil + } + if check != nil { + err = check(existingUserGrant.ProjectID, existingUserGrant.ProjectGrantID)(existingUserGrant.ResourceOwner, "") + } else { + err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID) } - err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID) if err != nil { return nil, err } - deactivateUserGrant := NewUserGrantWriteModel(grantID, resourceOwner) + deactivateUserGrant := NewUserGrantWriteModel(grantID, existingUserGrant.ResourceOwner) userGrantAgg := UserGrantAggregateFromWriteModel(&deactivateUserGrant.WriteModel) pushedEvents, err := c.eventstore.Push(ctx, usergrant.NewUserGrantReactivatedEvent(ctx, userGrantAgg)) if err != nil { @@ -209,12 +216,14 @@ func (c *Commands) ReactivateUserGrant(ctx context.Context, grantID, resourceOwn return writeModelToObjectDetails(&existingUserGrant.WriteModel), nil } -func (c *Commands) RemoveUserGrant(ctx context.Context, grantID, resourceOwner string) (objectDetails *domain.ObjectDetails, err error) { - event, existingUserGrant, err := c.removeUserGrant(ctx, grantID, resourceOwner, false) +func (c *Commands) RemoveUserGrant(ctx context.Context, grantID string, resourceOwner string, ignoreNotFound bool, check UserGrantPermissionCheck) (objectDetails *domain.ObjectDetails, err error) { + event, existingUserGrant, err := c.removeUserGrant(ctx, grantID, resourceOwner, false, ignoreNotFound, check) if err != nil { return nil, err } - + if event == nil { + return writeModelToObjectDetails(&existingUserGrant.WriteModel), nil + } pushedEvents, err := c.eventstore.Push(ctx, event) if err != nil { return nil, err @@ -232,7 +241,7 @@ func (c *Commands) BulkRemoveUserGrant(ctx context.Context, grantIDs []string, r } events := make([]eventstore.Command, len(grantIDs)) for i, grantID := range grantIDs { - event, _, err := c.removeUserGrant(ctx, grantID, resourceOwner, false) + event, _, err := c.removeUserGrant(ctx, grantID, resourceOwner, false, false, nil) if err != nil { return err } @@ -242,7 +251,7 @@ func (c *Commands) BulkRemoveUserGrant(ctx context.Context, grantIDs []string, r return err } -func (c *Commands) removeUserGrant(ctx context.Context, grantID, resourceOwner string, cascade bool) (_ eventstore.Command, writeModel *UserGrantWriteModel, err error) { +func (c *Commands) removeUserGrant(ctx context.Context, grantID string, resourceOwner string, cascade, ignoreNotFound bool, check UserGrantPermissionCheck) (_ eventstore.Command, writeModel *UserGrantWriteModel, err error) { if grantID == "" { return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-J9sc5", "Errors.UserGrant.IDMissing") } @@ -252,15 +261,22 @@ func (c *Commands) removeUserGrant(ctx context.Context, grantID, resourceOwner s return nil, nil, err } if existingUserGrant.State == domain.UserGrantStateUnspecified || existingUserGrant.State == domain.UserGrantStateRemoved { + if ignoreNotFound { + return nil, existingUserGrant, nil + } return nil, nil, zerrors.ThrowNotFound(nil, "COMMAND-1My0t", "Errors.UserGrant.NotFound") } - if !cascade { + if !cascade && check == nil { err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID) if err != nil { return nil, nil, err } } - + if check != nil { + if err = check(existingUserGrant.ProjectID, existingUserGrant.ProjectGrantID)(existingUserGrant.ResourceOwner, ""); err != nil { + return nil, nil, err + } + } removeUserGrant := NewUserGrantWriteModel(grantID, existingUserGrant.ResourceOwner) userGrantAgg := UserGrantAggregateFromWriteModel(&removeUserGrant.WriteModel) if cascade { @@ -279,7 +295,7 @@ func (c *Commands) removeUserGrant(ctx context.Context, grantID, resourceOwner s existingUserGrant.ProjectGrantID), existingUserGrant, nil } -func (c *Commands) userGrantWriteModelByID(ctx context.Context, userGrantID, resourceOwner string) (writeModel *UserGrantWriteModel, err error) { +func (c *Commands) userGrantWriteModelByID(ctx context.Context, userGrantID string, resourceOwner string) (writeModel *UserGrantWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -291,31 +307,46 @@ func (c *Commands) userGrantWriteModelByID(ctx context.Context, userGrantID, res return writeModel, nil } -func (c *Commands) checkUserGrantPreCondition(ctx context.Context, usergrant *domain.UserGrant, resourceOwner string) (err error) { +func (c *Commands) checkUserGrantPreCondition(ctx context.Context, usergrant *domain.UserGrant, check UserGrantPermissionCheck) (err error) { if !authz.GetFeatures(ctx).ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeUserGrant) { - return c.checkUserGrantPreConditionOld(ctx, usergrant, resourceOwner) + return c.checkUserGrantPreConditionOld(ctx, usergrant, check) } ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if err := c.checkUserExists(ctx, usergrant.UserID, ""); err != nil { + if _, err := c.checkUserExists(ctx, usergrant.UserID, ""); err != nil { return err } - existingRoleKeys, err := c.searchUserGrantPreConditionState(ctx, usergrant, resourceOwner) + if usergrant.ProjectGrantID != "" || usergrant.ResourceOwner == "" { + projectOwner, grantID, err := c.searchProjectOwnerAndGrantID(ctx, usergrant.ProjectID, "") + if err != nil { + return err + } + if usergrant.ResourceOwner == "" { + usergrant.ResourceOwner = projectOwner + } + if usergrant.ProjectGrantID == "" { + usergrant.ProjectGrantID = grantID + } + } + existingRoleKeys, err := c.searchUserGrantPreConditionState(ctx, usergrant) if err != nil { return err } if usergrant.HasInvalidRoles(existingRoleKeys) { return zerrors.ThrowPreconditionFailed(err, "COMMAND-mm9F4", "Errors.Project.Role.NotFound") } - return nil + if check != nil { + return check(usergrant.ProjectID, usergrant.ProjectGrantID)(usergrant.ResourceOwner, "") + } + return checkExplicitProjectPermission(ctx, usergrant.ProjectGrantID, usergrant.ProjectID) } // this code needs to be rewritten anyways as soon as we improved the fields handling // //nolint:gocognit -func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGrant *domain.UserGrant, resourceOwner string) (existingRoleKeys []string, err error) { +func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGrant *domain.UserGrant) (existingRoleKeys []string, err error) { criteria := []map[eventstore.FieldType]any{ // project state query { @@ -327,7 +358,7 @@ func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGra // granted org query { eventstore.FieldTypeAggregateType: org.AggregateType, - eventstore.FieldTypeAggregateID: resourceOwner, + eventstore.FieldTypeAggregateID: userGrant.ResourceOwner, eventstore.FieldTypeFieldName: org.OrgStateSearchField, eventstore.FieldTypeObjectType: org.OrgSearchType, }, @@ -386,7 +417,7 @@ func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGra case project.ProjectGrantGrantedOrgIDSearchField: var orgID string err := result.Value.Unmarshal(&orgID) - if err != nil || orgID != resourceOwner { + if err != nil || orgID != userGrant.ResourceOwner { return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound") } case project.ProjectGrantStateSearchField: @@ -425,26 +456,63 @@ func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGra return existingRoleKeys, nil } -func (c *Commands) checkUserGrantPreConditionOld(ctx context.Context, usergrant *domain.UserGrant, resourceOwner string) (err error) { +func (c *Commands) checkUserGrantPreConditionOld(ctx context.Context, usergrant *domain.UserGrant, check UserGrantPermissionCheck) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - preConditions := NewUserGrantPreConditionReadModel(usergrant.UserID, usergrant.ProjectID, usergrant.ProjectGrantID, resourceOwner) + preConditions := NewUserGrantPreConditionReadModel(usergrant.UserID, usergrant.ProjectID, usergrant.ProjectGrantID, usergrant.ResourceOwner) err = c.eventstore.FilterToQueryReducer(ctx, preConditions) if err != nil { return err } + if usergrant.ResourceOwner == "" { + usergrant.ResourceOwner = preConditions.ProjectResourceOwner + } + if usergrant.ProjectGrantID == "" { + usergrant.ProjectGrantID = preConditions.ProjectGrantID + } if !preConditions.UserExists { return zerrors.ThrowPreconditionFailed(err, "COMMAND-4f8sg", "Errors.User.NotFound") } - if usergrant.ProjectGrantID == "" && !preConditions.ProjectExists { + projectIsOwned := usergrant.ResourceOwner == "" || usergrant.ResourceOwner == preConditions.ProjectResourceOwner + if projectIsOwned && !preConditions.ProjectExists { return zerrors.ThrowPreconditionFailed(err, "COMMAND-3n77S", "Errors.Project.NotFound") } - if usergrant.ProjectGrantID != "" && !preConditions.ProjectGrantExists { + if !projectIsOwned && !preConditions.ProjectGrantExists { return zerrors.ThrowPreconditionFailed(err, "COMMAND-4m9ff", "Errors.Project.Grant.NotFound") } if usergrant.HasInvalidRoles(preConditions.ExistingRoleKeys) { return zerrors.ThrowPreconditionFailed(err, "COMMAND-mm9F4", "Errors.Project.Role.NotFound") } - return nil + if check != nil { + return check(usergrant.ProjectID, usergrant.ProjectGrantID)(usergrant.ResourceOwner, "") + } + return checkExplicitProjectPermission(ctx, usergrant.ProjectGrantID, usergrant.ProjectID) +} + +func (c *Commands) searchProjectOwnerAndGrantID(ctx context.Context, projectID string, grantedOrgID string) (projectOwner string, grantID string, err error) { + grantIDQuery := map[eventstore.FieldType]any{ + eventstore.FieldTypeAggregateType: project.AggregateType, + eventstore.FieldTypeAggregateID: projectID, + eventstore.FieldTypeFieldName: project.ProjectGrantGrantedOrgIDSearchField, + } + if grantedOrgID != "" { + grantIDQuery[eventstore.FieldTypeValue] = grantedOrgID + grantIDQuery[eventstore.FieldTypeObjectType] = project.ProjectGrantSearchType + + } + results, err := c.eventstore.Search(ctx, grantIDQuery) + if err != nil { + return "", "", err + } + for _, result := range results { + projectOwner = result.Aggregate.ResourceOwner + if grantedOrgID != "" && grantedOrgID == projectOwner { + return projectOwner, "", nil + } + if result.Object.Type == project.ProjectGrantSearchType { + return projectOwner, result.Object.ID, nil + } + } + return projectOwner, grantID, err } diff --git a/internal/command/user_grant_model.go b/internal/command/user_grant_model.go index b2490177d9..b35c96a2d5 100644 --- a/internal/command/user_grant_model.go +++ b/internal/command/user_grant_model.go @@ -36,6 +36,7 @@ func (wm *UserGrantWriteModel) Reduce() error { wm.ProjectGrantID = e.ProjectGrantID wm.RoleKeys = e.RoleKeys wm.State = domain.UserGrantStateActive + wm.ResourceOwner = e.Aggregate().ResourceOwner case *usergrant.UserGrantChangedEvent: wm.RoleKeys = e.RoleKeys case *usergrant.UserGrantCascadeChangedEvent: @@ -86,17 +87,18 @@ func UserGrantAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Agg type UserGrantPreConditionReadModel struct { eventstore.WriteModel - UserID string - ProjectID string - ProjectGrantID string - ResourceOwner string - UserExists bool - ProjectExists bool - ProjectGrantExists bool - ExistingRoleKeys []string + UserID string + ProjectID string + ProjectResourceOwner string + ProjectGrantID string + ResourceOwner string + UserExists bool + ProjectExists bool + ProjectGrantExists bool + ExistingRoleKeys []string } -func NewUserGrantPreConditionReadModel(userID, projectID, projectGrantID, resourceOwner string) *UserGrantPreConditionReadModel { +func NewUserGrantPreConditionReadModel(userID, projectID, projectGrantID string, resourceOwner string) *UserGrantPreConditionReadModel { return &UserGrantPreConditionReadModel{ UserID: userID, ProjectID: projectID, @@ -117,15 +119,19 @@ func (wm *UserGrantPreConditionReadModel) Reduce() error { case *user.UserRemovedEvent: wm.UserExists = false case *project.ProjectAddedEvent: - if wm.ProjectGrantID == "" && wm.ResourceOwner == e.Aggregate().ResourceOwner { + if wm.ResourceOwner == "" || wm.ResourceOwner == e.Aggregate().ResourceOwner { wm.ProjectExists = true } + wm.ProjectResourceOwner = e.Aggregate().ResourceOwner case *project.ProjectRemovedEvent: wm.ProjectExists = false case *project.GrantAddedEvent: - if wm.ProjectGrantID == e.GrantID && wm.ResourceOwner == e.GrantedOrgID { + if (wm.ProjectGrantID == e.GrantID || wm.ProjectGrantID == "") && wm.ResourceOwner != "" && wm.ResourceOwner == e.GrantedOrgID { wm.ProjectGrantExists = true wm.ExistingRoleKeys = e.RoleKeys + if wm.ProjectGrantID == "" { + wm.ProjectGrantID = e.GrantID + } } case *project.GrantChangedEvent: if wm.ProjectGrantID == e.GrantID { diff --git a/internal/command/user_grant_test.go b/internal/command/user_grant_test.go index dec5903fe8..b12e190d82 100644 --- a/internal/command/user_grant_test.go +++ b/internal/command/user_grant_test.go @@ -2,6 +2,7 @@ package command import ( "context" + "errors" "testing" "github.com/stretchr/testify/assert" @@ -19,15 +20,27 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) +var ( + errMockedPermissionCheck = errors.New("mocked permission check error") + isMockedPermissionCheckErr = func(err error) bool { + return errors.Is(err, errMockedPermissionCheck) + } + succeedingUserGrantPermissionCheck = func(_, _ string) PermissionCheck { + return func(_, _ string) error { return nil } + } + failingUserGrantPermissionCheck = func(_, _ string) PermissionCheck { + return func(_, _ string) error { return errMockedPermissionCheck } + } +) + func TestCommandSide_AddUserGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - idGenerator id.Generator + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator func(t *testing.T) id.Generator } type args struct { - ctx context.Context - userGrant *domain.UserGrant - resourceOwner string + ctx context.Context + userGrant *domain.UserGrant } type res struct { want *domain.UserGrant @@ -42,16 +55,16 @@ func TestCommandSide_AddUserGrant(t *testing.T) { { name: "invalid usergrant, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: authz.NewMockContextWithPermissions("", "org", "user", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ UserID: "user1", + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -60,8 +73,7 @@ func TestCommandSide_AddUserGrant(t *testing.T) { { name: "user removed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -94,8 +106,10 @@ func TestCommandSide_AddUserGrant(t *testing.T) { userGrant: &domain.UserGrant{ UserID: "user1", ProjectID: "project1", + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -104,8 +118,7 @@ func TestCommandSide_AddUserGrant(t *testing.T) { { name: "project removed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -143,8 +156,10 @@ func TestCommandSide_AddUserGrant(t *testing.T) { userGrant: &domain.UserGrant{ UserID: "user1", ProjectID: "project1", + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -153,8 +168,7 @@ func TestCommandSide_AddUserGrant(t *testing.T) { { name: "project on other org, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -185,8 +199,10 @@ func TestCommandSide_AddUserGrant(t *testing.T) { userGrant: &domain.UserGrant{ UserID: "user1", ProjectID: "project1", + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org2", + }, }, - resourceOwner: "org2", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -195,8 +211,7 @@ func TestCommandSide_AddUserGrant(t *testing.T) { { name: "project roles not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -228,8 +243,10 @@ func TestCommandSide_AddUserGrant(t *testing.T) { UserID: "user1", ProjectID: "project1", RoleKeys: []string{"roleKey"}, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -238,8 +255,7 @@ func TestCommandSide_AddUserGrant(t *testing.T) { { name: "project grant not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -272,8 +288,10 @@ func TestCommandSide_AddUserGrant(t *testing.T) { ProjectID: "project1", ProjectGrantID: "projectgrant1", RoleKeys: []string{"roleKey"}, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -282,8 +300,7 @@ func TestCommandSide_AddUserGrant(t *testing.T) { { name: "project grant roles not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -332,8 +349,10 @@ func TestCommandSide_AddUserGrant(t *testing.T) { ProjectID: "project1", ProjectGrantID: "projectgrant1", RoleKeys: []string{"roleKey"}, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -342,8 +361,7 @@ func TestCommandSide_AddUserGrant(t *testing.T) { { name: "project grant on other org, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -392,8 +410,10 @@ func TestCommandSide_AddUserGrant(t *testing.T) { ProjectID: "project1", ProjectGrantID: "projectgrant1", RoleKeys: []string{"rolekey1"}, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org2", + }, }, - resourceOwner: "org2", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -402,8 +422,7 @@ func TestCommandSide_AddUserGrant(t *testing.T) { { name: "usergrant for project, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -445,7 +464,82 @@ func TestCommandSide_AddUserGrant(t *testing.T) { ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "usergrant1"), + idGenerator: func(t *testing.T) id.Generator { + return id_mock.NewIDGeneratorExpectIDs(t, "usergrant1") + }, + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1"}, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + }, + }, + res: res{ + want: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1"}, + State: domain.UserGrantStateActive, + }, + }, + }, + { + name: "usergrant without resource owner on project, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + ), + expectPush( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", + []string{"rolekey1"}, + ), + ), + ), + idGenerator: func(t *testing.T) id.Generator { + return id_mock.NewIDGeneratorExpectIDs(t, "usergrant1") + }, }, args: args{ ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), @@ -454,7 +548,6 @@ func TestCommandSide_AddUserGrant(t *testing.T) { ProjectID: "project1", RoleKeys: []string{"rolekey1"}, }, - resourceOwner: "org1", }, res: res{ want: &domain.UserGrant{ @@ -472,8 +565,90 @@ func TestCommandSide_AddUserGrant(t *testing.T) { { name: "usergrant for projectgrant, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org2").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org2").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org2").Aggregate, + "projectgrant1", + "org1", + []string{"rolekey1"}, + ), + ), + ), + expectPush( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", + []string{"rolekey1"}, + ), + ), + ), + idGenerator: func(t *testing.T) id.Generator { + return id_mock.NewIDGeneratorExpectIDs(t, "usergrant1") + }, + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + UserID: "user1", + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + RoleKeys: []string{"rolekey1"}, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + }, + }, + res: res{ + want: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + RoleKeys: []string{"rolekey1"}, + State: domain.UserGrantStateActive, + }, + }, + }, + { + name: "usergrant for granted resource owner, ok", + fields: fields{ + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -523,17 +698,20 @@ func TestCommandSide_AddUserGrant(t *testing.T) { ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "usergrant1"), + idGenerator: func(t *testing.T) id.Generator { + return id_mock.NewIDGeneratorExpectIDs(t, "usergrant1") + }, }, args: args{ ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ - UserID: "user1", - ProjectID: "project1", - ProjectGrantID: "projectgrant1", - RoleKeys: []string{"rolekey1"}, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1"}, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, }, - resourceOwner: "org1", }, res: res{ want: &domain.UserGrant{ @@ -550,34 +728,111 @@ func TestCommandSide_AddUserGrant(t *testing.T) { }, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, - } - got, err := r.AddUserGrant(tt.args.ctx, tt.args.userGrant, tt.args.resourceOwner) - 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) - } - }) - } + t.Run("without permission check", func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + } + if tt.fields.idGenerator != nil { + r.idGenerator = tt.fields.idGenerator(t) + } + got, err := r.AddUserGrant(tt.args.ctx, tt.args.userGrant, nil) + 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) + } + }) + } + }) + t.Run("with succeeding permission check", func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + } + if tt.fields.idGenerator != nil { + r.idGenerator = tt.fields.idGenerator(t) + } + // we use an empty context and only rely on the permission check implementation + got, err := r.AddUserGrant(context.Background(), tt.args.userGrant, succeedingUserGrantPermissionCheck) + 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) + } + }) + } + }) + t.Run("with failing permission check", func(t *testing.T) { + r := &Commands{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + ), + ), + } + // we use an empty context and only rely on the permission check implementation + _, err := r.AddUserGrant(context.Background(), &domain.UserGrant{ + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1"}, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + }, failingUserGrantPermissionCheck) + assert.ErrorIs(t, err, errMockedPermissionCheck) + }) } func TestCommandSide_ChangeUserGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { - ctx context.Context - userGrant *domain.UserGrant - resourceOwner string + ctx context.Context + userGrant *domain.UserGrant + permissionCheck UserGrantPermissionCheck + cascade bool + ignoreUnchanged bool } type res struct { want *domain.UserGrant @@ -592,16 +847,16 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "invalid usergrant, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: authz.NewMockContextWithPermissions("", "org", "user", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ UserID: "user1", + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -610,28 +865,66 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "invalid permissions, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), - &usergrant.NewAggregate("usergrant1", "org").Aggregate, + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, "user1", "project1", "", []string{"rolekey1"}), ), ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey2", + "rolekey 2", + "", + ), + ), + ), ), }, args: args{ ctx: context.Background(), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPermissionDenied, @@ -640,8 +933,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "usergrant not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -649,13 +941,13 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", ProjectID: "project1", RoleKeys: []string{"rolekey1"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsNotFound, @@ -664,8 +956,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "usergrant not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -673,13 +964,13 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", ProjectID: "project1", RoleKeys: []string{"rolekey1"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsNotFound, @@ -688,8 +979,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "usergrant roles not changed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -705,13 +995,13 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", ProjectID: "project1", RoleKeys: []string{"rolekey1"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -720,8 +1010,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "user removed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -762,12 +1051,12 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { ctx: authz.NewMockContextWithPermissions("", "org", "user", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", ProjectID: "project1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -776,8 +1065,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "project removed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -823,12 +1111,12 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { ctx: authz.NewMockContextWithPermissions("", "org", "user", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", ProjectID: "project1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -837,8 +1125,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "project roles not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -877,13 +1164,13 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { ctx: authz.NewMockContextWithPermissions("", "org", "user", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", ProjectID: "project1", RoleKeys: []string{"roleKey"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -892,8 +1179,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "project grant not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -932,14 +1218,14 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { ctx: authz.NewMockContextWithPermissions("", "org", "user", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", ProjectID: "project1", ProjectGrantID: "projectgrant1", RoleKeys: []string{"roleKey"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -948,8 +1234,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "project grant roles not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1004,14 +1289,14 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { ctx: authz.NewMockContextWithPermissions("", "org", "user", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", ProjectID: "project1", ProjectGrantID: "projectgrant1", RoleKeys: []string{"roleKey"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -1020,8 +1305,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "usergrant for project, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1083,13 +1367,13 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", ProjectID: "project1", RoleKeys: []string{"rolekey1", "rolekey2"}, }, - resourceOwner: "org1", }, res: res{ want: &domain.UserGrant{ @@ -1104,11 +1388,94 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { }, }, }, + { + name: "usergrant for project cascade, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey2", + "rolekey 2", + "", + ), + ), + ), + expectPush( + usergrant.NewUserGrantCascadeChangedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + []string{"rolekey1", "rolekey2"}, + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + RoleKeys: []string{"rolekey1", "rolekey2"}, + }, + cascade: true, + }, + res: res{ + want: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + State: domain.UserGrantStateActive, + RoleKeys: []string{"rolekey1", "rolekey2"}, + }, + }, + }, { name: "usergrant for projectgrant, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1178,14 +1545,14 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", ProjectID: "project1", ProjectGrantID: "projectgrant1", RoleKeys: []string{"rolekey1", "rolekey2"}, }, - resourceOwner: "org1", }, res: res{ want: &domain.UserGrant{ @@ -1201,13 +1568,307 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { }, }, }, + { + name: "usergrant for project without resource owner, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey2", + "rolekey 2", + "", + ), + ), + ), + expectPush( + usergrant.NewUserGrantChangedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + []string{"rolekey1", "rolekey2"}, + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1", "rolekey2"}, + }, + }, + res: res{ + want: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1", "rolekey2"}, + State: domain.UserGrantStateActive, + }, + }, + }, + { + name: "usergrant for project with passed succeeding permission check, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey2", + "rolekey 2", + "", + ), + ), + ), + expectPush( + usergrant.NewUserGrantChangedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + []string{"rolekey1", "rolekey2"}, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrant: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1", "rolekey2"}, + }, + permissionCheck: succeedingUserGrantPermissionCheck, + }, + res: res{ + want: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1", "rolekey2"}, + State: domain.UserGrantStateActive, + }, + }, + }, + { + name: "usergrant for project with passed failing permission check, error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey2", + "rolekey 2", + "", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrant: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1", "rolekey2"}, + }, + permissionCheck: failingUserGrantPermissionCheck, + }, + res: res{ + want: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1", "rolekey2"}, + State: domain.UserGrantStateActive, + }, + err: isMockedPermissionCheckErr, + }, + }, + { + name: "usergrant roles not changed, ignore unchanged, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1", "rolekey2"}), + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1", "rolekey2"}, + }, + ignoreUnchanged: true, + }, + res: res{ + want: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1", "rolekey2"}, + State: domain.UserGrantStateActive, + }, + }, + }, } 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.ChangeUserGrant(tt.args.ctx, tt.args.userGrant, tt.args.resourceOwner) + got, err := r.ChangeUserGrant(tt.args.ctx, tt.args.userGrant, tt.args.cascade, tt.args.ignoreUnchanged, tt.args.permissionCheck) if tt.res.err == nil { assert.NoError(t, err) } @@ -1223,12 +1884,13 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { func TestCommandSide_DeactivateUserGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context userGrantID string resourceOwner string + check UserGrantPermissionCheck } type res struct { want *domain.ObjectDetails @@ -1243,12 +1905,10 @@ func TestCommandSide_DeactivateUserGrant(t *testing.T) { { name: "invalid usergrantID, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), resourceOwner: "org1", }, res: res{ @@ -1256,25 +1916,39 @@ func TestCommandSide_DeactivateUserGrant(t *testing.T) { }, }, { - name: "invalid resourceOwner, error", + name: "not provided resourceOwner, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + expectPush( + usergrant.NewUserGrantDeactivatedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + ), + ), ), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), userGrantID: "usergrant1", }, res: res{ - err: zerrors.IsErrorInvalidArgument, + want: &domain.ObjectDetails{ + ResourceOwner: "org", + }, }, }, { name: "usergrant not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -1290,8 +1964,7 @@ func TestCommandSide_DeactivateUserGrant(t *testing.T) { { name: "usergrant removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1320,14 +1993,13 @@ func TestCommandSide_DeactivateUserGrant(t *testing.T) { }, }, { - name: "no permissions, permisison denied error", + name: "no permissions, permission denied error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), - &usergrant.NewAggregate("usergrant1", "org").Aggregate, + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, "user1", "project1", "", []string{"rolekey1"}), @@ -1345,10 +2017,9 @@ func TestCommandSide_DeactivateUserGrant(t *testing.T) { }, }, { - name: "already deactivated, precondition error", + name: "already deactivated, ignore, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1370,14 +2041,15 @@ func TestCommandSide_DeactivateUserGrant(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org", + }, }, }, { name: "deactivated, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1405,13 +2077,70 @@ func TestCommandSide_DeactivateUserGrant(t *testing.T) { }, }, }, + { + name: "with passed succeeding permission check, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + expectPush( + usergrant.NewUserGrantDeactivatedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrantID: "usergrant1", + resourceOwner: "org1", + check: succeedingUserGrantPermissionCheck, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with passed failing permission check, error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrantID: "usergrant1", + resourceOwner: "org1", + check: failingUserGrantPermissionCheck, + }, + res: res{ + err: isMockedPermissionCheckErr, + }, + }, } 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.DeactivateUserGrant(tt.args.ctx, tt.args.userGrantID, tt.args.resourceOwner) + got, err := r.DeactivateUserGrant(tt.args.ctx, tt.args.userGrantID, tt.args.resourceOwner, tt.args.check) if tt.res.err == nil { assert.NoError(t, err) } @@ -1427,12 +2156,13 @@ func TestCommandSide_DeactivateUserGrant(t *testing.T) { func TestCommandSide_ReactivateUserGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context userGrantID string resourceOwner string + check UserGrantPermissionCheck } type res struct { want *domain.ObjectDetails @@ -1447,12 +2177,10 @@ func TestCommandSide_ReactivateUserGrant(t *testing.T) { { name: "invalid usergrantID, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), resourceOwner: "org1", }, res: res{ @@ -1460,25 +2188,43 @@ func TestCommandSide_ReactivateUserGrant(t *testing.T) { }, }, { - name: "invalid resourceOwner, error", + name: "not provided resourceOwner, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + eventFromEventPusher( + usergrant.NewUserGrantDeactivatedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate), + ), + ), + expectPush( + usergrant.NewUserGrantReactivatedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + ), + ), ), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), userGrantID: "usergrant1", }, res: res{ - err: zerrors.IsErrorInvalidArgument, + want: &domain.ObjectDetails{ + ResourceOwner: "org", + }, }, }, { name: "usergrant not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -1494,8 +2240,7 @@ func TestCommandSide_ReactivateUserGrant(t *testing.T) { { name: "usergrant removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1524,10 +2269,9 @@ func TestCommandSide_ReactivateUserGrant(t *testing.T) { }, }, { - name: "no permissions, permisison denied error", + name: "no permissions, permission denied error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1553,10 +2297,9 @@ func TestCommandSide_ReactivateUserGrant(t *testing.T) { }, }, { - name: "already active, precondition error", + name: "already active, ignore, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1569,19 +2312,20 @@ func TestCommandSide_ReactivateUserGrant(t *testing.T) { ), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), userGrantID: "usergrant1", resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org", + }, }, }, { name: "reactivated, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1613,13 +2357,78 @@ func TestCommandSide_ReactivateUserGrant(t *testing.T) { }, }, }, + { + name: "with passed succeeding permission check, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + eventFromEventPusher( + usergrant.NewUserGrantDeactivatedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate), + ), + ), + expectPush( + usergrant.NewUserGrantReactivatedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrantID: "usergrant1", + resourceOwner: "org1", + check: succeedingUserGrantPermissionCheck, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with passed failing permission check, error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + eventFromEventPusher( + usergrant.NewUserGrantDeactivatedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrantID: "usergrant1", + resourceOwner: "org1", + check: failingUserGrantPermissionCheck, + }, + res: res{ + err: isMockedPermissionCheckErr, + }, + }, } 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.ReactivateUserGrant(tt.args.ctx, tt.args.userGrantID, tt.args.resourceOwner) + got, err := r.ReactivateUserGrant(tt.args.ctx, tt.args.userGrantID, tt.args.resourceOwner, tt.args.check) if tt.res.err == nil { assert.NoError(t, err) } @@ -1635,12 +2444,14 @@ func TestCommandSide_ReactivateUserGrant(t *testing.T) { func TestCommandSide_RemoveUserGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { - ctx context.Context - userGrantID string - resourceOwner string + ctx context.Context + userGrantID string + resourceOwner string + ignoreNotFound bool + check UserGrantPermissionCheck } type res struct { want *domain.ObjectDetails @@ -1655,12 +2466,10 @@ func TestCommandSide_RemoveUserGrant(t *testing.T) { { name: "invalid usergrantID, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), resourceOwner: "org1", }, res: res{ @@ -1670,8 +2479,7 @@ func TestCommandSide_RemoveUserGrant(t *testing.T) { { name: "usergrant not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -1684,11 +2492,29 @@ func TestCommandSide_RemoveUserGrant(t *testing.T) { err: zerrors.IsNotFound, }, }, + { + name: "usergrant not existing, ignore, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), + userGrantID: "usergrant1", + resourceOwner: "org1", + ignoreNotFound: true, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, { name: "usergrant removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1717,10 +2543,9 @@ func TestCommandSide_RemoveUserGrant(t *testing.T) { }, }, { - name: "no permissions, permisison denied error", + name: "no permissions, permission denied error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1748,8 +2573,7 @@ func TestCommandSide_RemoveUserGrant(t *testing.T) { { name: "remove usergrant project, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1780,11 +2604,43 @@ func TestCommandSide_RemoveUserGrant(t *testing.T) { }, }, }, + { + name: "not provided resourceOwner, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + expectPush( + usergrant.NewUserGrantRemovedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), + userGrantID: "usergrant1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, { name: "remove usergrant projectgrant, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1815,13 +2671,73 @@ func TestCommandSide_RemoveUserGrant(t *testing.T) { }, }, }, + { + name: "with passed succeeding permission check, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", []string{"rolekey1"}), + ), + ), + expectPush( + usergrant.NewUserGrantRemovedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrantID: "usergrant1", + resourceOwner: "org1", + check: succeedingUserGrantPermissionCheck, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with passed failing permission check, error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", []string{"rolekey1"}), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrantID: "usergrant1", + resourceOwner: "org1", + check: failingUserGrantPermissionCheck, + }, + res: res{ + err: isMockedPermissionCheckErr, + }, + }, } 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.RemoveUserGrant(tt.args.ctx, tt.args.userGrantID, tt.args.resourceOwner) + got, err := r.RemoveUserGrant(tt.args.ctx, tt.args.userGrantID, tt.args.resourceOwner, tt.args.ignoreNotFound, tt.args.check) if tt.res.err == nil { assert.NoError(t, err) } @@ -1857,9 +2773,7 @@ func TestCommandSide_BulkRemoveUserGrant(t *testing.T) { { name: "empty usergrantid list, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: eventstoreExpect(t), }, args: args{ ctx: context.Background(), diff --git a/internal/command/user_human_otp.go b/internal/command/user_human_otp.go index 97596aabd8..fca762cbab 100644 --- a/internal/command/user_human_otp.go +++ b/internal/command/user_human_otp.go @@ -26,7 +26,7 @@ func (c *Commands) ImportHumanTOTP(ctx context.Context, userID, userAgentID, res if err != nil { return err } - if err = c.checkUserExists(ctx, userID, resourceOwner); err != nil { + if _, err = c.checkUserExists(ctx, userID, resourceOwner); err != nil { return err } diff --git a/internal/command/user_idp_link.go b/internal/command/user_idp_link.go index 432d2e0b90..e3484c9753 100644 --- a/internal/command/user_idp_link.go +++ b/internal/command/user_idp_link.go @@ -56,7 +56,7 @@ func (c *Commands) BulkAddedUserIDPLinks(ctx context.Context, userID, resourceOw return zerrors.ThrowInvalidArgument(nil, "COMMAND-Ek9s", "Errors.User.ExternalIDP.MinimumExternalIDPNeeded") } - if err := c.checkUserExists(ctx, userID, resourceOwner); err != nil { + if _, err := c.checkUserExists(ctx, userID, resourceOwner); err != nil { return err } diff --git a/internal/command/user_metadata.go b/internal/command/user_metadata.go index d47c5b61d0..294866e23b 100644 --- a/internal/command/user_metadata.go +++ b/internal/command/user_metadata.go @@ -1,6 +1,7 @@ package command import ( + "bytes" "context" "github.com/zitadel/zitadel/internal/domain" @@ -14,12 +15,25 @@ func (c *Commands) SetUserMetadata(ctx context.Context, metadata *domain.Metadat ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - err = c.checkUserExists(ctx, userID, resourceOwner) + userResourceOwner, err := c.checkUserExists(ctx, userID, resourceOwner) + if err != nil { + return nil, err + } + + if err := c.checkPermissionUpdateUser(ctx, userResourceOwner, userID); err != nil { + return nil, err + } + + setMetadata, err := c.getUserMetadataModelByID(ctx, userID, userResourceOwner, metadata.Key) if err != nil { return nil, err } - setMetadata := NewUserMetadataWriteModel(userID, resourceOwner, metadata.Key) userAgg := UserAggregateFromWriteModel(&setMetadata.WriteModel) + // return if no change in the metadata + if bytes.Equal(setMetadata.Value, metadata.Value) { + return writeModelToUserMetadata(setMetadata), nil + } + event, err := c.setUserMetadata(ctx, userAgg, metadata) if err != nil { return nil, err @@ -40,20 +54,35 @@ func (c *Commands) BulkSetUserMetadata(ctx context.Context, userID, resourceOwne if len(metadatas) == 0 { return nil, zerrors.ThrowPreconditionFailed(nil, "META-9mm2d", "Errors.Metadata.NoData") } - err = c.checkUserExists(ctx, userID, resourceOwner) + userResourceOwner, err := c.checkUserExists(ctx, userID, resourceOwner) if err != nil { return nil, err } - events := make([]eventstore.Command, len(metadatas)) - setMetadata := NewUserMetadataListWriteModel(userID, resourceOwner) + if err := c.checkPermissionUpdateUser(ctx, userResourceOwner, userID); err != nil { + return nil, err + } + + events := make([]eventstore.Command, 0) + setMetadata, err := c.getUserMetadataListModelByID(ctx, userID, userResourceOwner) + if err != nil { + return nil, err + } userAgg := UserAggregateFromWriteModel(&setMetadata.WriteModel) - for i, data := range metadatas { + for _, data := range metadatas { + // if no change to metadata no event has to be pushed + if existingValue, ok := setMetadata.metadataList[data.Key]; ok && bytes.Equal(existingValue, data.Value) { + continue + } event, err := c.setUserMetadata(ctx, userAgg, data) if err != nil { return nil, err } - events[i] = event + events = append(events, event) + } + // no changes for the metadata + if len(events) == 0 { + return writeModelToObjectDetails(&setMetadata.WriteModel), nil } pushedEvents, err := c.eventstore.Push(ctx, events...) @@ -84,11 +113,16 @@ func (c *Commands) RemoveUserMetadata(ctx context.Context, metadataKey, userID, if metadataKey == "" { return nil, zerrors.ThrowInvalidArgument(nil, "META-2n0fs", "Errors.Metadata.Invalid") } - err = c.checkUserExists(ctx, userID, resourceOwner) + userResourceOwner, err := c.checkUserExists(ctx, userID, resourceOwner) if err != nil { return nil, err } - removeMetadata, err := c.getUserMetadataModelByID(ctx, userID, resourceOwner, metadataKey) + + if err := c.checkPermissionUpdateUser(ctx, userResourceOwner, userID); err != nil { + return nil, err + } + + removeMetadata, err := c.getUserMetadataModelByID(ctx, userID, userResourceOwner, metadataKey) if err != nil { return nil, err } @@ -116,13 +150,17 @@ func (c *Commands) BulkRemoveUserMetadata(ctx context.Context, userID, resourceO if len(metadataKeys) == 0 { return nil, zerrors.ThrowPreconditionFailed(nil, "META-9mm2d", "Errors.Metadata.NoData") } - err = c.checkUserExists(ctx, userID, resourceOwner) + userResourceOwner, err := c.checkUserExists(ctx, userID, resourceOwner) if err != nil { return nil, err } + if err := c.checkPermissionUpdateUser(ctx, userResourceOwner, userID); err != nil { + return nil, err + } + events := make([]eventstore.Command, len(metadataKeys)) - removeMetadata, err := c.getUserMetadataListModelByID(ctx, userID, resourceOwner) + removeMetadata, err := c.getUserMetadataListModelByID(ctx, userID, userResourceOwner) if err != nil { return nil, err } @@ -153,24 +191,6 @@ func (c *Commands) BulkRemoveUserMetadata(ctx context.Context, userID, resourceO return writeModelToObjectDetails(&removeMetadata.WriteModel), nil } -func (c *Commands) removeUserMetadataFromOrg(ctx context.Context, resourceOwner string) ([]eventstore.Command, error) { - existingUserMetadata, err := c.getUserMetadataByOrgListModelByID(ctx, resourceOwner) - if err != nil { - return nil, err - } - if len(existingUserMetadata.UserMetadata) == 0 { - return nil, nil - } - events := make([]eventstore.Command, 0) - for key, value := range existingUserMetadata.UserMetadata { - if len(value) == 0 { - continue - } - events = append(events, user.NewMetadataRemovedAllEvent(ctx, &user.NewAggregate(key, resourceOwner).Aggregate)) - } - return events, nil -} - func (c *Commands) removeUserMetadata(ctx context.Context, userAgg *eventstore.Aggregate, metadataKey string) (command eventstore.Command, err error) { command = user.NewMetadataRemovedEvent( ctx, @@ -197,12 +217,3 @@ func (c *Commands) getUserMetadataListModelByID(ctx context.Context, userID, res } return userMetadataWriteModel, nil } - -func (c *Commands) getUserMetadataByOrgListModelByID(ctx context.Context, resourceOwner string) (*UserMetadataByOrgListWriteModel, error) { - userMetadataWriteModel := NewUserMetadataByOrgListWriteModel(resourceOwner) - err := c.eventstore.FilterToQueryReducer(ctx, userMetadataWriteModel) - if err != nil { - return nil, err - } - return userMetadataWriteModel, nil -} diff --git a/internal/command/user_metadata_test.go b/internal/command/user_metadata_test.go index b3ffa7b823..e8fe25acd9 100644 --- a/internal/command/user_metadata_test.go +++ b/internal/command/user_metadata_test.go @@ -16,7 +16,8 @@ import ( func TestCommandSide_SetUserMetadata(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type ( args struct { @@ -39,10 +40,10 @@ func TestCommandSide_SetUserMetadata(t *testing.T) { { name: "user not existing, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -60,8 +61,7 @@ func TestCommandSide_SetUserMetadata(t *testing.T) { { name: "invalid metadata, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -78,7 +78,17 @@ func TestCommandSide_SetUserMetadata(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + user.NewMetadataSetEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "key", + []byte("value"), + ), + ), + ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -93,10 +103,9 @@ func TestCommandSide_SetUserMetadata(t *testing.T) { }, }, { - name: "add metadata, ok", + name: "add metadata, no permission", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -113,6 +122,43 @@ func TestCommandSide_SetUserMetadata(t *testing.T) { ), ), ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + metadata: &domain.Metadata{ + Key: "key", + Value: []byte("value"), + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, + { + name: "add metadata, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + language.Und, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter(), expectPush( user.NewMetadataSetEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, @@ -121,6 +167,7 @@ func TestCommandSide_SetUserMetadata(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -143,11 +190,116 @@ func TestCommandSide_SetUserMetadata(t *testing.T) { }, }, }, + { + name: "add metadata, reset, invalid", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + language.Und, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewMetadataSetEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "key", + []byte("value"), + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + metadata: &domain.Metadata{ + Key: "key", + }, + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "add metadata, reset, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + language.Und, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewMetadataSetEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "key", + []byte("value"), + ), + ), + ), + expectPush( + user.NewMetadataSetEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "key", + []byte("value2"), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + metadata: &domain.Metadata{ + Key: "key", + Value: []byte("value2"), + }, + }, + res: res{ + want: &domain.Metadata{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Key: "key", + Value: []byte("value2"), + State: domain.MetadataStateActive, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.SetUserMetadata(tt.args.ctx, tt.args.metadata, tt.args.userID, tt.args.orgID) if tt.res.err == nil { @@ -165,7 +317,8 @@ func TestCommandSide_SetUserMetadata(t *testing.T) { func TestCommandSide_BulkSetUserMetadata(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type ( args struct { @@ -188,9 +341,7 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) { { name: "empty meta data list, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -204,8 +355,7 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) { { name: "user not existing, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -225,8 +375,7 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) { { name: "invalid metadata, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -243,7 +392,9 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) { ), ), ), + expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -259,10 +410,9 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) { }, }, { - name: "add metadata, ok", + name: "add metadata, no permission", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -279,6 +429,43 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) { ), ), ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + metadataList: []*domain.Metadata{ + {Key: "key", Value: []byte("value")}, + {Key: "key1", Value: []byte("value1")}, + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, + { + name: "add metadata, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + language.Und, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter(), expectPush( user.NewMetadataSetEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, @@ -292,6 +479,7 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -312,7 +500,8 @@ func TestCommandSide_BulkSetUserMetadata(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), + checkPermission: tt.fields.checkPermission, } got, err := r.BulkSetUserMetadata(tt.args.ctx, tt.args.userID, tt.args.orgID, tt.args.metadataList...) if tt.res.err == nil { @@ -330,7 +519,8 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) { func TestCommandSide_UserRemoveMetadata(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type ( args struct { @@ -353,8 +543,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) { { name: "user not existing, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -371,9 +560,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) { { name: "invalid metadata, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -388,8 +575,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) { { name: "meta data not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -408,6 +594,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) { ), expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -419,11 +606,43 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) { err: zerrors.IsNotFound, }, }, + { + name: "remove metadata, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + language.Und, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + metadataKey: "key", + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, { name: "remove metadata, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -456,6 +675,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -473,7 +693,8 @@ func TestCommandSide_UserRemoveMetadata(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), + checkPermission: tt.fields.checkPermission, } got, err := r.RemoveUserMetadata(tt.args.ctx, tt.args.metadataKey, tt.args.userID, tt.args.orgID) if tt.res.err == nil { @@ -491,7 +712,8 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) { func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type ( args struct { @@ -514,9 +736,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) { { name: "empty meta data list, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -530,8 +750,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) { { name: "user not existing, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -548,8 +767,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) { { name: "remove metadata keys not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -576,6 +794,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -590,8 +809,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) { { name: "invalid metadata, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -625,6 +843,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -636,11 +855,43 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) { err: zerrors.IsErrorInvalidArgument, }, }, + { + name: "remove metadata, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + language.Und, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + metadataList: []string{"key", "key1"}, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, { name: "remove metadata, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -684,6 +935,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -701,7 +953,8 @@ func TestCommandSide_BulkRemoveUserMetadata(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), + checkPermission: tt.fields.checkPermission, } got, err := r.BulkRemoveUserMetadata(tt.args.ctx, tt.args.userID, tt.args.orgID, tt.args.metadataList...) if tt.res.err == nil { diff --git a/internal/command/user_v2.go b/internal/command/user_v2.go index be10fd03fe..d6c5e7de53 100644 --- a/internal/command/user_v2.go +++ b/internal/command/user_v2.go @@ -2,7 +2,6 @@ package command import ( "context" - "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/domain" @@ -150,7 +149,7 @@ func (c *Commands) RemoveUserV2(ctx context.Context, userID, resourceOwner strin events = append(events, user.NewUserRemovedEvent(ctx, &existingUser.Aggregate().Aggregate, existingUser.UserName, existingUser.IDPLinks, domainPolicy.UserLoginMustBeDomain)) for _, grantID := range cascadingGrantIDs { - removeEvent, _, err := c.removeUserGrant(ctx, grantID, "", true) + removeEvent, _, err := c.removeUserGrant(ctx, grantID, "", true, true, nil) if err != nil { logging.WithFields("usergrantid", grantID).WithError(err).Warn("could not cascade remove role on user grant") continue diff --git a/internal/domain/member.go b/internal/domain/member.go index 821e65dfe3..76854a6568 100644 --- a/internal/domain/member.go +++ b/internal/domain/member.go @@ -25,10 +25,6 @@ func (i *Member) IsValid() bool { return i.AggregateID != "" && i.UserID != "" && len(i.Roles) != 0 } -func (i *Member) IsIAMValid() bool { - return i.UserID != "" && len(i.Roles) != 0 -} - type MemberState int32 const ( @@ -42,3 +38,7 @@ const ( func (f MemberState) Valid() bool { return f >= 0 && f < memberStateCount } + +func (f MemberState) Exists() bool { + return f != MemberStateRemoved && f != MemberStateUnspecified +} diff --git a/internal/domain/permission.go b/internal/domain/permission.go index 119e8c2d3e..1405991dae 100644 --- a/internal/domain/permission.go +++ b/internal/domain/permission.go @@ -27,29 +27,44 @@ func (p *Permissions) appendPermission(ctxID, permission string) { type PermissionCheck func(ctx context.Context, permission, resourceOwnerID, aggregateID string) (err error) const ( - PermissionUserWrite = "user.write" - PermissionUserRead = "user.read" - PermissionUserDelete = "user.delete" - PermissionUserCredentialWrite = "user.credential.write" - PermissionSessionWrite = "session.write" - PermissionSessionRead = "session.read" - PermissionSessionLink = "session.link" - PermissionSessionDelete = "session.delete" - PermissionOrgRead = "org.read" - PermissionIDPRead = "iam.idp.read" - PermissionOrgIDPRead = "org.idp.read" - PermissionProjectWrite = "project.write" - PermissionProjectRead = "project.read" - PermissionProjectDelete = "project.delete" - PermissionProjectGrantWrite = "project.grant.write" - PermissionProjectGrantRead = "project.grant.read" - PermissionProjectGrantDelete = "project.grant.delete" - PermissionProjectRoleWrite = "project.role.write" - PermissionProjectRoleRead = "project.role.read" - PermissionProjectRoleDelete = "project.role.delete" - PermissionProjectAppWrite = "project.app.write" - PermissionProjectAppDelete = "project.app.delete" - PermissionProjectAppRead = "project.app.read" + PermissionUserWrite = "user.write" + PermissionUserRead = "user.read" + PermissionUserDelete = "user.delete" + PermissionUserCredentialWrite = "user.credential.write" + PermissionSessionWrite = "session.write" + PermissionSessionRead = "session.read" + PermissionSessionLink = "session.link" + PermissionSessionDelete = "session.delete" + PermissionOrgRead = "org.read" + PermissionIDPRead = "iam.idp.read" + PermissionOrgIDPRead = "org.idp.read" + PermissionProjectWrite = "project.write" + PermissionProjectRead = "project.read" + PermissionProjectDelete = "project.delete" + PermissionProjectGrantWrite = "project.grant.write" + PermissionProjectGrantRead = "project.grant.read" + PermissionProjectGrantDelete = "project.grant.delete" + PermissionProjectRoleWrite = "project.role.write" + PermissionProjectRoleRead = "project.role.read" + PermissionProjectRoleDelete = "project.role.delete" + PermissionProjectAppWrite = "project.app.write" + PermissionProjectAppDelete = "project.app.delete" + PermissionProjectAppRead = "project.app.read" + PermissionInstanceMemberWrite = "iam.member.write" + PermissionInstanceMemberDelete = "iam.member.delete" + PermissionInstanceMemberRead = "iam.member.read" + PermissionOrgMemberWrite = "org.member.write" + PermissionOrgMemberDelete = "org.member.delete" + PermissionOrgMemberRead = "org.member.read" + PermissionProjectMemberWrite = "project.member.write" + PermissionProjectMemberDelete = "project.member.delete" + PermissionProjectMemberRead = "project.member.read" + PermissionProjectGrantMemberWrite = "project.grant.member.write" + PermissionProjectGrantMemberDelete = "project.grant.member.delete" + PermissionProjectGrantMemberRead = "project.grant.member.read" + PermissionUserGrantWrite = "user.grant.write" + PermissionUserGrantRead = "user.grant.read" + PermissionUserGrantDelete = "user.grant.delete" ) // 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 c4f639b4b8..a89e4fa621 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -2,6 +2,7 @@ package integration import ( "context" + "encoding/base64" "fmt" "sync" "testing" @@ -24,11 +25,13 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/admin" app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" "github.com/zitadel/zitadel/pkg/grpc/auth" + authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta" "github.com/zitadel/zitadel/pkg/grpc/feature/v2" feature_v2beta "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" "github.com/zitadel/zitadel/pkg/grpc/idp" idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2" instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" + internal_permission_v2beta "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" "github.com/zitadel/zitadel/pkg/grpc/object/v2" object_v3alpha "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" @@ -52,33 +55,35 @@ import ( ) type Client struct { - CC *grpc.ClientConn - Admin admin.AdminServiceClient - Mgmt mgmt.ManagementServiceClient - Auth auth.AuthServiceClient - UserV2beta user_v2beta.UserServiceClient - UserV2 user_v2.UserServiceClient - SessionV2beta session_v2beta.SessionServiceClient - SessionV2 session.SessionServiceClient - SettingsV2beta settings_v2beta.SettingsServiceClient - SettingsV2 settings.SettingsServiceClient - OIDCv2beta oidc_pb_v2beta.OIDCServiceClient - OIDCv2 oidc_pb.OIDCServiceClient - OrgV2beta org_v2beta.OrganizationServiceClient - OrgV2 org.OrganizationServiceClient - ActionV2beta action.ActionServiceClient - FeatureV2beta feature_v2beta.FeatureServiceClient - FeatureV2 feature.FeatureServiceClient - UserSchemaV3 userschema_v3alpha.ZITADELUserSchemasClient - WebKeyV2Beta webkey_v2beta.WebKeyServiceClient - WebKeyV2 webkey_v2.WebKeyServiceClient - IDPv2 idp_pb.IdentityProviderServiceClient - UserV3Alpha user_v3alpha.ZITADELUsersClient - SAMLv2 saml_pb.SAMLServiceClient - SCIM *scim.Client - Projectv2Beta project_v2beta.ProjectServiceClient - InstanceV2Beta instance.InstanceServiceClient - AppV2Beta app.AppServiceClient + CC *grpc.ClientConn + Admin admin.AdminServiceClient + Mgmt mgmt.ManagementServiceClient + Auth auth.AuthServiceClient + UserV2beta user_v2beta.UserServiceClient + UserV2 user_v2.UserServiceClient + SessionV2beta session_v2beta.SessionServiceClient + SessionV2 session.SessionServiceClient + SettingsV2beta settings_v2beta.SettingsServiceClient + SettingsV2 settings.SettingsServiceClient + OIDCv2beta oidc_pb_v2beta.OIDCServiceClient + OIDCv2 oidc_pb.OIDCServiceClient + OrgV2beta org_v2beta.OrganizationServiceClient + OrgV2 org.OrganizationServiceClient + ActionV2beta action.ActionServiceClient + FeatureV2beta feature_v2beta.FeatureServiceClient + FeatureV2 feature.FeatureServiceClient + UserSchemaV3 userschema_v3alpha.ZITADELUserSchemasClient + WebKeyV2 webkey_v2.WebKeyServiceClient + WebKeyV2Beta webkey_v2beta.WebKeyServiceClient + IDPv2 idp_pb.IdentityProviderServiceClient + UserV3Alpha user_v3alpha.ZITADELUsersClient + SAMLv2 saml_pb.SAMLServiceClient + SCIM *scim.Client + Projectv2Beta project_v2beta.ProjectServiceClient + InstanceV2Beta instance.InstanceServiceClient + AppV2Beta app.AppServiceClient + InternalPermissionv2Beta internal_permission_v2beta.InternalPermissionServiceClient + AuthorizationV2Beta authorization.AuthorizationServiceClient } func NewDefaultClient(ctx context.Context) (*Client, error) { @@ -93,33 +98,35 @@ func newClient(ctx context.Context, target string) (*Client, error) { return nil, err } client := &Client{ - CC: cc, - Admin: admin.NewAdminServiceClient(cc), - Mgmt: mgmt.NewManagementServiceClient(cc), - Auth: auth.NewAuthServiceClient(cc), - UserV2beta: user_v2beta.NewUserServiceClient(cc), - UserV2: user_v2.NewUserServiceClient(cc), - SessionV2beta: session_v2beta.NewSessionServiceClient(cc), - SessionV2: session.NewSessionServiceClient(cc), - SettingsV2beta: settings_v2beta.NewSettingsServiceClient(cc), - SettingsV2: settings.NewSettingsServiceClient(cc), - OIDCv2beta: oidc_pb_v2beta.NewOIDCServiceClient(cc), - OIDCv2: oidc_pb.NewOIDCServiceClient(cc), - OrgV2beta: org_v2beta.NewOrganizationServiceClient(cc), - OrgV2: org.NewOrganizationServiceClient(cc), - ActionV2beta: action.NewActionServiceClient(cc), - FeatureV2beta: feature_v2beta.NewFeatureServiceClient(cc), - FeatureV2: feature.NewFeatureServiceClient(cc), - UserSchemaV3: userschema_v3alpha.NewZITADELUserSchemasClient(cc), - WebKeyV2Beta: webkey_v2beta.NewWebKeyServiceClient(cc), - WebKeyV2: webkey_v2.NewWebKeyServiceClient(cc), - IDPv2: idp_pb.NewIdentityProviderServiceClient(cc), - UserV3Alpha: user_v3alpha.NewZITADELUsersClient(cc), - SAMLv2: saml_pb.NewSAMLServiceClient(cc), - SCIM: scim.NewScimClient(target), - Projectv2Beta: project_v2beta.NewProjectServiceClient(cc), - InstanceV2Beta: instance.NewInstanceServiceClient(cc), - AppV2Beta: app.NewAppServiceClient(cc), + CC: cc, + Admin: admin.NewAdminServiceClient(cc), + Mgmt: mgmt.NewManagementServiceClient(cc), + Auth: auth.NewAuthServiceClient(cc), + UserV2beta: user_v2beta.NewUserServiceClient(cc), + UserV2: user_v2.NewUserServiceClient(cc), + SessionV2beta: session_v2beta.NewSessionServiceClient(cc), + SessionV2: session.NewSessionServiceClient(cc), + SettingsV2beta: settings_v2beta.NewSettingsServiceClient(cc), + SettingsV2: settings.NewSettingsServiceClient(cc), + OIDCv2beta: oidc_pb_v2beta.NewOIDCServiceClient(cc), + OIDCv2: oidc_pb.NewOIDCServiceClient(cc), + OrgV2beta: org_v2beta.NewOrganizationServiceClient(cc), + OrgV2: org.NewOrganizationServiceClient(cc), + ActionV2beta: action.NewActionServiceClient(cc), + FeatureV2beta: feature_v2beta.NewFeatureServiceClient(cc), + FeatureV2: feature.NewFeatureServiceClient(cc), + UserSchemaV3: userschema_v3alpha.NewZITADELUserSchemasClient(cc), + WebKeyV2: webkey_v2.NewWebKeyServiceClient(cc), + WebKeyV2Beta: webkey_v2beta.NewWebKeyServiceClient(cc), + IDPv2: idp_pb.NewIdentityProviderServiceClient(cc), + UserV3Alpha: user_v3alpha.NewZITADELUsersClient(cc), + SAMLv2: saml_pb.NewSAMLServiceClient(cc), + SCIM: scim.NewScimClient(target), + Projectv2Beta: project_v2beta.NewProjectServiceClient(cc), + InstanceV2Beta: instance.NewInstanceServiceClient(cc), + AppV2Beta: app.NewAppServiceClient(cc), + InternalPermissionv2Beta: internal_permission_v2beta.NewInternalPermissionServiceClient(cc), + AuthorizationV2Beta: authorization.NewAuthorizationServiceClient(cc), } return client, client.pollHealth(ctx) } @@ -239,7 +246,29 @@ func (i *Instance) CreateHumanUserWithTOTP(ctx context.Context, secret string) * return resp } -func (i *Instance) CreateUserTypeHuman(ctx context.Context) *user_v2.CreateUserResponse { +func (i *Instance) SetUserMetadata(ctx context.Context, id, key, value string) *user_v2.SetUserMetadataResponse { + resp, err := i.Client.UserV2.SetUserMetadata(ctx, &user_v2.SetUserMetadataRequest{ + UserId: id, + Metadata: []*user_v2.Metadata{{ + Key: key, + Value: []byte(base64.StdEncoding.EncodeToString([]byte(value))), + }, + }, + }) + logging.OnError(err).Panic("set user metadata") + return resp +} + +func (i *Instance) DeleteUserMetadata(ctx context.Context, id, key string) *user_v2.DeleteUserMetadataResponse { + resp, err := i.Client.UserV2.DeleteUserMetadata(ctx, &user_v2.DeleteUserMetadataRequest{ + UserId: id, + Keys: []string{key}, + }) + logging.OnError(err).Panic("delete user metadata") + return resp +} + +func (i *Instance) CreateUserTypeHuman(ctx context.Context, email string) *user_v2.CreateUserResponse { resp, err := i.Client.UserV2.CreateUser(ctx, &user_v2.CreateUserRequest{ OrganizationId: i.DefaultOrg.GetId(), UserType: &user_v2.CreateUserRequest_Human_{ @@ -249,7 +278,7 @@ func (i *Instance) CreateUserTypeHuman(ctx context.Context) *user_v2.CreateUserR FamilyName: "Mouse", }, Email: &user_v2.SetHumanEmail{ - Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()), + Email: email, Verification: &user_v2.SetHumanEmail_ReturnCode{ ReturnCode: &user_v2.ReturnEmailVerificationCode{}, }, @@ -262,7 +291,7 @@ func (i *Instance) CreateUserTypeHuman(ctx context.Context) *user_v2.CreateUserR return resp } -func (i *Instance) CreateUserTypeMachine(ctx context.Context) *user_v2.CreateUserResponse { +func (i *Instance) CreateUserTypeMachine(ctx context.Context, orgId string) *user_v2.CreateUserResponse { resp, err := i.Client.UserV2.CreateUser(ctx, &user_v2.CreateUserRequest{ OrganizationId: i.DefaultOrg.GetId(), UserType: &user_v2.CreateUserRequest_Machine_{ @@ -629,14 +658,6 @@ func (i *Instance) AddOrgGenericOAuthProvider(ctx context.Context, name string) }, }) logging.OnError(err).Panic("create generic OAuth idp") - /* - mustAwait(func() error { - _, err := i.Client.Mgmt.GetProviderByID(ctx, &mgmt.GetProviderByIDRequest{ - Id: resp.GetId(), - }) - return err - }) - */ return resp } @@ -883,48 +904,107 @@ func (i *Instance) ActivateProjectGrant(ctx context.Context, t *testing.T, proje return resp } -func (i *Instance) CreateProjectUserGrant(t *testing.T, ctx context.Context, projectID, userID string) string { +func (i *Instance) CreateProjectUserGrant(t *testing.T, ctx context.Context, projectID, userID string) *mgmt.AddUserGrantResponse { resp, err := i.Client.Mgmt.AddUserGrant(ctx, &mgmt.AddUserGrantRequest{ UserId: userID, ProjectId: projectID, }) require.NoError(t, err) - return resp.GetUserGrantId() + return resp } -func (i *Instance) CreateProjectGrantUserGrant(ctx context.Context, orgID, projectID, projectGrantID, userID string) string { +func (i *Instance) CreateProjectGrantUserGrant(ctx context.Context, orgID, projectID, projectGrantID, userID string) *mgmt.AddUserGrantResponse { resp, err := i.Client.Mgmt.AddUserGrant(SetOrgID(ctx, orgID), &mgmt.AddUserGrantRequest{ UserId: userID, ProjectId: projectID, ProjectGrantId: projectGrantID, }) logging.OnError(err).Panic("create project grant user grant") - return resp.GetUserGrantId() + return resp } -func (i *Instance) CreateOrgMembership(t *testing.T, ctx context.Context, userID string) { - _, err := i.Client.Mgmt.AddOrgMember(ctx, &mgmt.AddOrgMemberRequest{ +func (i *Instance) CreateInstanceMembership(t *testing.T, ctx context.Context, userID string) *internal_permission_v2beta.CreateAdministratorResponse { + resp, err := i.Client.InternalPermissionv2Beta.CreateAdministrator(ctx, &internal_permission_v2beta.CreateAdministratorRequest{ + Resource: &internal_permission_v2beta.ResourceType{ + Resource: &internal_permission_v2beta.ResourceType_Instance{Instance: true}, + }, + UserId: userID, + Roles: []string{domain.RoleIAMOwner}, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) DeleteInstanceMembership(t *testing.T, ctx context.Context, userID string) { + _, err := i.Client.Admin.RemoveIAMMember(ctx, &admin.RemoveIAMMemberRequest{ + UserId: userID, + }) + require.NoError(t, err) +} + +func (i *Instance) CreateOrgMembership(t *testing.T, ctx context.Context, orgID, userID string) *internal_permission_v2beta.CreateAdministratorResponse { + resp, err := i.Client.InternalPermissionv2Beta.CreateAdministrator(ctx, &internal_permission_v2beta.CreateAdministratorRequest{ + Resource: &internal_permission_v2beta.ResourceType{ + Resource: &internal_permission_v2beta.ResourceType_OrganizationId{OrganizationId: orgID}, + }, UserId: userID, Roles: []string{domain.RoleOrgOwner}, }) require.NoError(t, err) + return resp } -func (i *Instance) CreateProjectMembership(t *testing.T, ctx context.Context, projectID, userID string) { - _, err := i.Client.Mgmt.AddProjectMember(ctx, &mgmt.AddProjectMemberRequest{ - ProjectId: projectID, - UserId: userID, - Roles: []string{domain.RoleProjectOwner}, +func (i *Instance) DeleteOrgMembership(t *testing.T, ctx context.Context, userID string) { + _, err := i.Client.Mgmt.RemoveOrgMember(ctx, &mgmt.RemoveOrgMemberRequest{ + UserId: userID, }) require.NoError(t, err) } -func (i *Instance) CreateProjectGrantMembership(t *testing.T, ctx context.Context, projectID, grantID, userID string) { - _, err := i.Client.Mgmt.AddProjectGrantMember(ctx, &mgmt.AddProjectGrantMemberRequest{ - ProjectId: projectID, - GrantId: grantID, - UserId: userID, - Roles: []string{domain.RoleProjectGrantOwner}, +func (i *Instance) CreateProjectMembership(t *testing.T, ctx context.Context, projectID, userID string) *internal_permission_v2beta.CreateAdministratorResponse { + resp, err := i.Client.InternalPermissionv2Beta.CreateAdministrator(ctx, &internal_permission_v2beta.CreateAdministratorRequest{ + Resource: &internal_permission_v2beta.ResourceType{ + Resource: &internal_permission_v2beta.ResourceType_ProjectId{ProjectId: projectID}, + }, + UserId: userID, + Roles: []string{domain.RoleProjectOwner}, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) DeleteProjectMembership(t *testing.T, ctx context.Context, projectID, userID string) { + _, err := i.Client.InternalPermissionv2Beta.DeleteAdministrator(ctx, &internal_permission_v2beta.DeleteAdministratorRequest{ + Resource: &internal_permission_v2beta.ResourceType{Resource: &internal_permission_v2beta.ResourceType_ProjectId{ProjectId: projectID}}, + UserId: userID, + }) + require.NoError(t, err) +} + +func (i *Instance) CreateProjectGrantMembership(t *testing.T, ctx context.Context, projectID, grantID, userID string) *internal_permission_v2beta.CreateAdministratorResponse { + resp, err := i.Client.InternalPermissionv2Beta.CreateAdministrator(ctx, &internal_permission_v2beta.CreateAdministratorRequest{ + Resource: &internal_permission_v2beta.ResourceType{ + Resource: &internal_permission_v2beta.ResourceType_ProjectGrant_{ProjectGrant: &internal_permission_v2beta.ResourceType_ProjectGrant{ + ProjectId: projectID, + ProjectGrantId: grantID, + }}, + }, + UserId: userID, + Roles: []string{domain.RoleProjectGrantOwner}, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) DeleteProjectGrantMembership(t *testing.T, ctx context.Context, projectID, grantID, userID string) { + _, err := i.Client.InternalPermissionv2Beta.DeleteAdministrator(ctx, &internal_permission_v2beta.DeleteAdministratorRequest{ + Resource: &internal_permission_v2beta.ResourceType{ + Resource: &internal_permission_v2beta.ResourceType_ProjectGrant_{ProjectGrant: &internal_permission_v2beta.ResourceType_ProjectGrant{ + ProjectId: projectID, + ProjectGrantId: grantID, + }}, + }, + UserId: userID, }) require.NoError(t, err) } diff --git a/internal/integration/feature.go b/internal/integration/feature.go new file mode 100644 index 0000000000..07942fcdcd --- /dev/null +++ b/internal/integration/feature.go @@ -0,0 +1,30 @@ +package integration + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" +) + +func EnsureInstanceFeature(t *testing.T, ctx context.Context, instance *Instance, features *feature.SetInstanceFeaturesRequest, assertFeatures func(t *assert.CollectT, got *feature.GetInstanceFeaturesResponse)) { + ctx = instance.WithAuthorizationToken(ctx, UserTypeIAMOwner) + _, err := instance.Client.FeatureV2.SetInstanceFeatures(ctx, features) + require.NoError(t, err) + retryDuration, tick := WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) + require.EventuallyWithT(t, + func(tt *assert.CollectT) { + got, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + require.NoError(tt, err) + assertFeatures(tt, got) + }, + retryDuration, + tick, + "timed out waiting for ensuring instance feature") +} diff --git a/internal/integration/instance.go b/internal/integration/instance.go index 66e2cf18ec..9e262cb3d9 100644 --- a/internal/integration/instance.go +++ b/internal/integration/instance.go @@ -294,11 +294,14 @@ func (i *Instance) createWebAuthNClient() { i.WebAuthN = webauthn.NewClient(i.Config.WebAuthNName, i.Domain, http_util.BuildOrigin(i.Host(), i.Config.Secure)) } +// Deprecated: WithAuthorization is misleading, as we have Zitadel resources called authorization now. +// It is aliased to WithAuthorizationToken, which sets the Authorization header with a Bearer token. +// Use WithAuthorizationToken directly instead. func (i *Instance) WithAuthorization(ctx context.Context, u UserType) context.Context { - return i.WithInstanceAuthorization(ctx, u) + return i.WithAuthorizationToken(ctx, u) } -func (i *Instance) WithInstanceAuthorization(ctx context.Context, u UserType) context.Context { +func (i *Instance) WithAuthorizationToken(ctx context.Context, u UserType) context.Context { return WithAuthorizationToken(ctx, i.Users.Get(u).Token) } diff --git a/internal/query/administrators.go b/internal/query/administrators.go new file mode 100644 index 0000000000..9ab81993ef --- /dev/null +++ b/internal/query/administrators.go @@ -0,0 +1,347 @@ +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/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type Administrators struct { + SearchResponse + Administrators []*Administrator +} + +type Administrator struct { + Roles database.TextArray[string] + CreationDate time.Time + ChangeDate time.Time + ResourceOwner string + + User *UserAdministrator + Org *OrgAdministrator + Instance *InstanceAdministrator + Project *ProjectAdministrator + ProjectGrant *ProjectGrantAdministrator +} + +type UserAdministrator struct { + UserID string + LoginName string + DisplayName string + ResourceOwner string +} +type OrgAdministrator struct { + OrgID string + Name string +} + +type InstanceAdministrator struct { + InstanceID string + Name string +} + +type ProjectAdministrator struct { + ProjectID string + Name string + ResourceOwner string +} + +type ProjectGrantAdministrator struct { + ProjectID string + ProjectName string + GrantID string + GrantedOrgID string + ResourceOwner string +} + +func NewAdministratorUserResourceOwnerSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(UserResourceOwnerCol, value, TextEquals) +} + +func NewAdministratorUserLoginNameSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(LoginNameNameCol, value, TextEquals) +} + +func NewAdministratorUserDisplayNameSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(HumanDisplayNameCol, value, TextEquals) +} + +func administratorInstancePermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + InstanceMemberResourceOwner, + domain.PermissionInstanceMemberRead, + OwnedRowsPermissionOption(InstanceMemberUserID), + ) + return query.JoinClause(join, args...) +} + +func administratorOrgPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + OrgMemberResourceOwner, + domain.PermissionOrgMemberRead, + OwnedRowsPermissionOption(OrgMemberUserID), + ) + return query.JoinClause(join, args...) +} + +func administratorProjectPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + ProjectMemberResourceOwner, + domain.PermissionProjectMemberRead, + WithProjectsPermissionOption(ProjectMemberProjectID), + OwnedRowsPermissionOption(ProjectMemberUserID), + ) + return query.JoinClause(join, args...) +} + +func administratorProjectGrantPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + ProjectGrantMemberResourceOwner, + domain.PermissionProjectGrantMemberRead, + WithProjectsPermissionOption(ProjectMemberProjectID), + OwnedRowsPermissionOption(ProjectGrantMemberUserID), + ) + return query.JoinClause(join, args...) +} + +func administratorsCheckPermission(ctx context.Context, administrators *Administrators, permissionCheck domain.PermissionCheck) { + selfUserID := authz.GetCtxData(ctx).UserID + administrators.Administrators = slices.DeleteFunc(administrators.Administrators, + func(administrator *Administrator) bool { + if administrator.User != nil && administrator.User.UserID == selfUserID { + return false + } + if administrator.ProjectGrant != nil { + return administratorProjectGrantCheckPermission(ctx, administrator.ProjectGrant.ResourceOwner, administrator.ProjectGrant.ProjectID, administrator.ProjectGrant.GrantID, administrator.ProjectGrant.GrantedOrgID, permissionCheck) != nil + } + if administrator.Project != nil { + return permissionCheck(ctx, domain.PermissionProjectMemberRead, administrator.Project.ResourceOwner, administrator.Project.ProjectID) != nil + } + if administrator.Org != nil { + return permissionCheck(ctx, domain.PermissionOrgMemberRead, administrator.Org.OrgID, administrator.Org.OrgID) != nil + } + if administrator.Instance != nil { + return permissionCheck(ctx, domain.PermissionInstanceMemberRead, administrator.Instance.InstanceID, administrator.Instance.InstanceID) != nil + } + return true + }, + ) +} + +func administratorProjectGrantCheckPermission(ctx context.Context, resourceOwner, projectID, grantID, grantedOrgID string, permissionCheck domain.PermissionCheck) error { + if err := permissionCheck(ctx, domain.PermissionProjectGrantMemberRead, resourceOwner, grantID); err != nil { + if err := permissionCheck(ctx, domain.PermissionProjectGrantMemberRead, grantedOrgID, grantID); err != nil { + if err := permissionCheck(ctx, domain.PermissionProjectGrantMemberRead, resourceOwner, projectID); err != nil { + return err + } + } + } + return nil +} + +func (q *Queries) SearchAdministrators(ctx context.Context, queries *MembershipSearchQuery, permissionCheck domain.PermissionCheck) (*Administrators, error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + admins, err := q.searchAdministrators(ctx, queries, permissionCheckV2) + if err != nil { + return nil, err + } + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + administratorsCheckPermission(ctx, admins, permissionCheck) + } + return admins, nil +} + +func (q *Queries) searchAdministrators(ctx context.Context, queries *MembershipSearchQuery, permissionCheckV2 bool) (administrators *Administrators, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + query, queryArgs, scan := prepareAdministratorsQuery(ctx, queries, permissionCheckV2) + 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") + } + latestState, err := q.latestState(ctx, orgMemberTable, instanceMemberTable, projectMemberTable, projectGrantMemberTable) + if err != nil { + return nil, err + } + queryArgs = append(queryArgs, args...) + + err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { + administrators, err = scan(rows) + return err + }, stmt, queryArgs...) + if err != nil { + return nil, err + } + administrators.State = latestState + return administrators, nil +} + +func prepareAdministratorsQuery(ctx context.Context, queries *MembershipSearchQuery, permissionV2 bool) (sq.SelectBuilder, []interface{}, func(*sql.Rows) (*Administrators, error)) { + query, args := getMembershipFromQuery(ctx, queries, permissionV2) + return sq.Select( + MembershipUserID.identifier(), + membershipRoles.identifier(), + MembershipCreationDate.identifier(), + MembershipChangeDate.identifier(), + membershipResourceOwner.identifier(), + membershipOrgID.identifier(), + membershipIAMID.identifier(), + membershipProjectID.identifier(), + membershipGrantID.identifier(), + ProjectGrantColumnGrantedOrgID.identifier(), + ProjectColumnResourceOwner.identifier(), + ProjectColumnName.identifier(), + OrgColumnName.identifier(), + InstanceColumnName.identifier(), + LoginNameNameCol.identifier(), + HumanDisplayNameCol.identifier(), + MachineNameCol.identifier(), + HumanAvatarURLCol.identifier(), + UserTypeCol.identifier(), + UserResourceOwnerCol.identifier(), + countColumn.identifier(), + ).From(query). + LeftJoin(join(ProjectColumnID, membershipProjectID)). + LeftJoin(join(ProjectGrantColumnGrantID, membershipGrantID)). + LeftJoin(join(OrgColumnID, membershipOrgID)). + LeftJoin(join(InstanceColumnID, membershipInstanceID)). + LeftJoin(join(HumanUserIDCol, OrgMemberUserID)). + LeftJoin(join(MachineUserIDCol, OrgMemberUserID)). + LeftJoin(join(UserIDCol, OrgMemberUserID)). + LeftJoin(join(LoginNameUserIDCol, OrgMemberUserID)). + Where( + sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, + ).PlaceholderFormat(sq.Dollar), + args, + func(rows *sql.Rows) (*Administrators, error) { + administrators := make([]*Administrator, 0) + var count uint64 + for rows.Next() { + + var ( + administrator = new(Administrator) + userID = sql.NullString{} + orgID = sql.NullString{} + instanceID = sql.NullString{} + projectID = sql.NullString{} + grantID = sql.NullString{} + grantedOrgID = sql.NullString{} + projectName = sql.NullString{} + orgName = sql.NullString{} + instanceName = sql.NullString{} + projectResourceOwner = sql.NullString{} + loginName = sql.NullString{} + displayName = sql.NullString{} + machineName = sql.NullString{} + avatarURL = sql.NullString{} + userType = sql.NullInt32{} + userResourceOwner = sql.NullString{} + ) + + err := rows.Scan( + &userID, + &administrator.Roles, + &administrator.CreationDate, + &administrator.ChangeDate, + &administrator.ResourceOwner, + &orgID, + &instanceID, + &projectID, + &grantID, + &grantedOrgID, + &projectResourceOwner, + &projectName, + &orgName, + &instanceName, + &loginName, + &displayName, + &machineName, + &avatarURL, + &userType, + &userResourceOwner, + &count, + ) + + if err != nil { + return nil, err + } + + if userID.Valid { + administrator.User = &UserAdministrator{ + UserID: userID.String, + LoginName: loginName.String, + DisplayName: displayName.String, + ResourceOwner: userResourceOwner.String, + } + } + + if orgID.Valid { + administrator.Org = &OrgAdministrator{ + OrgID: orgID.String, + Name: orgName.String, + } + } + if instanceID.Valid { + administrator.Instance = &InstanceAdministrator{ + InstanceID: instanceID.String, + Name: instanceName.String, + } + } + if projectID.Valid && grantID.Valid && grantedOrgID.Valid { + administrator.ProjectGrant = &ProjectGrantAdministrator{ + ProjectID: projectID.String, + ProjectName: projectName.String, + GrantID: grantID.String, + GrantedOrgID: grantedOrgID.String, + ResourceOwner: projectResourceOwner.String, + } + } else if projectID.Valid { + administrator.Project = &ProjectAdministrator{ + ProjectID: projectID.String, + Name: projectName.String, + ResourceOwner: projectResourceOwner.String, + } + } + + administrators = append(administrators, administrator) + } + + if err := rows.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-TODO", "Errors.Query.CloseRows") + } + + return &Administrators{ + Administrators: administrators, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil + } +} diff --git a/internal/query/member.go b/internal/query/member.go index 584ae15d1c..8a4ca3302d 100644 --- a/internal/query/member.go +++ b/internal/query/member.go @@ -35,8 +35,17 @@ func NewMemberLastNameSearchQuery(method TextComparison, value string) (SearchQu } func NewMemberUserIDSearchQuery(value string) (SearchQuery, error) { - return NewTextQuery(membershipUserID, value, TextEquals) + return NewTextQuery(MembershipUserID, value, TextEquals) } + +func NewMemberInUserIDsSearchQuery(ids []string) (SearchQuery, error) { + list := make([]interface{}, len(ids)) + for i, value := range ids { + list[i] = value + } + return NewListQuery(MembershipUserID, list, ListIn) +} + func NewMemberResourceOwnerSearchQuery(value string) (SearchQuery, error) { return NewTextQuery(membershipResourceOwner, value, TextEquals) } diff --git a/internal/query/project_grant_member.go b/internal/query/project_grant_member.go index a9cc49c498..2f469dce80 100644 --- a/internal/query/project_grant_member.go +++ b/internal/query/project_grant_member.go @@ -128,7 +128,7 @@ func prepareProjectGrantMembersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Memb LeftJoin(join(MachineUserIDCol, ProjectGrantMemberUserID)). LeftJoin(join(UserIDCol, ProjectGrantMemberUserID)). LeftJoin(join(LoginNameUserIDCol, ProjectGrantMemberUserID)). - LeftJoin(join(ProjectGrantColumnGrantID, ProjectGrantMemberGrantID)). + LeftJoin(join(ProjectGrantColumnGrantID, ProjectGrantMemberGrantID) + " AND " + ProjectGrantMemberProjectID.identifier() + " = " + ProjectGrantColumnProjectID.identifier()). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, ).PlaceholderFormat(sq.Dollar), diff --git a/internal/query/project_grant_member_test.go b/internal/query/project_grant_member_test.go index 23d1258b7c..be0f5f572e 100644 --- a/internal/query/project_grant_member_test.go +++ b/internal/query/project_grant_member_test.go @@ -46,6 +46,7 @@ var ( "LEFT JOIN projections.project_grants4 " + "ON members.grant_id = projections.project_grants4.grant_id " + "AND members.instance_id = projections.project_grants4.instance_id " + + "AND members.project_id = projections.project_grants4.project_id " + "WHERE projections.login_names3.is_primary = $1") projectGrantMembersColumns = []string{ "creation_date", diff --git a/internal/query/user_grant.go b/internal/query/user_grant.go index 05d80fe381..212972ea8c 100644 --- a/internal/query/user_grant.go +++ b/internal/query/user_grant.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" @@ -44,8 +45,9 @@ type UserGrant struct { OrgName string `json:"org_name,omitempty"` OrgPrimaryDomain string `json:"org_primary_domain,omitempty"` - ProjectID string `json:"project_id,omitempty"` - ProjectName string `json:"project_name,omitempty"` + ProjectResourceOwner string `json:"project_resource_owner,omitempty"` + ProjectID string `json:"project_id,omitempty"` + ProjectName string `json:"project_name,omitempty"` GrantedOrgID string `json:"granted_org_id,omitempty"` GrantedOrgName string `json:"granted_org_name,omitempty"` @@ -57,6 +59,27 @@ type UserGrants struct { UserGrants []*UserGrant } +func userGrantsCheckPermission(ctx context.Context, grants *UserGrants, permissionCheck domain.PermissionCheck) { + grants.UserGrants = slices.DeleteFunc(grants.UserGrants, + func(grant *UserGrant) bool { + return userGrantCheckPermission(ctx, grant.ResourceOwner, grant.ProjectID, grant.GrantID, grant.UserID, permissionCheck) != nil + }, + ) +} + +func userGrantCheckPermission(ctx context.Context, resourceOwner, projectID, grantID, userID string, permissionCheck domain.PermissionCheck) error { + // you should always be able to read your own permissions + if authz.GetCtxData(ctx).UserID == userID { + return nil + } + // check permission on the project grant + if grantID != "" { + return permissionCheck(ctx, domain.PermissionUserGrantRead, resourceOwner, grantID) + } + // check on project + return permissionCheck(ctx, domain.PermissionUserGrantRead, resourceOwner, projectID) +} + type UserGrantsQueries struct { SearchRequest Queries []SearchQuery @@ -70,6 +93,21 @@ func (q *UserGrantsQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { return query } +func userGrantPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *UserGrantsQueries) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + UserGrantResourceOwner, + domain.PermissionUserGrantRead, + SingleOrgPermissionOption(queries.Queries), + WithProjectsPermissionOption(UserGrantProjectID), + OwnedRowsPermissionOption(UserGrantUserID), + ) + return query.JoinClause(join, args...) +} + func NewUserGrantUserIDSearchQuery(id string) (SearchQuery, error) { return NewTextQuery(UserGrantUserID, id, TextEquals) } @@ -86,6 +124,10 @@ func NewUserGrantResourceOwnerSearchQuery(id string) (SearchQuery, error) { return NewTextQuery(UserGrantResourceOwner, id, TextEquals) } +func NewUserGrantUserResourceOwnerSearchQuery(id string) (SearchQuery, error) { + return NewTextQuery(UserResourceOwnerCol, id, TextEquals) +} + func NewUserGrantGrantIDSearchQuery(id string) (SearchQuery, error) { return NewTextQuery(UserGrantGrantID, id, TextEquals) } @@ -94,6 +136,14 @@ func NewUserGrantIDSearchQuery(id string) (SearchQuery, error) { return NewTextQuery(UserGrantID, id, TextEquals) } +func NewUserGrantInIDsSearchQuery(ids []string) (SearchQuery, error) { + list := make([]interface{}, len(ids)) + for i, value := range ids { + list[i] = value + } + return NewListQuery(UserGrantID, list, ListIn) +} + func NewUserGrantUserTypeQuery(typ domain.UserType) (SearchQuery, error) { return NewNumberQuery(UserTypeCol, typ, NumberEquals) } @@ -254,7 +304,19 @@ func (q *Queries) UserGrant(ctx context.Context, shouldTriggerBulk bool, queries return grant, err } -func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, shouldTriggerBulk bool) (grants *UserGrants, err error) { +func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, shouldTriggerBulk bool, permissionCheck domain.PermissionCheck) (*UserGrants, error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + grants, err := q.userGrants(ctx, queries, shouldTriggerBulk, permissionCheckV2) + if err != nil { + return nil, err + } + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + userGrantsCheckPermission(ctx, grants, permissionCheck) + } + return grants, nil +} + +func (q *Queries) userGrants(ctx context.Context, queries *UserGrantsQueries, shouldTriggerBulk bool, permissionCheckV2 bool) (grants *UserGrants, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -266,6 +328,7 @@ func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, sh } query, scan := prepareUserGrantsQuery() + query = userGrantPermissionCheckV2(ctx, query, permissionCheckV2, queries) eq := sq.Eq{UserGrantInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -316,6 +379,7 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro UserGrantProjectID.identifier(), ProjectColumnName.identifier(), + ProjectColumnResourceOwner.identifier(), GrantedOrgColumnId.identifier(), GrantedOrgColumnName.identifier(), @@ -326,7 +390,8 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro LeftJoin(join(HumanUserIDCol, UserGrantUserID)). LeftJoin(join(OrgColumnID, UserGrantResourceOwner)). LeftJoin(join(ProjectColumnID, UserGrantProjectID)). - LeftJoin(join(GrantedOrgColumnId, UserResourceOwnerCol)). + LeftJoin(join(ProjectGrantColumnGrantID, UserGrantGrantID) + " AND " + ProjectGrantColumnProjectID.identifier() + " = " + UserGrantProjectID.identifier()). + LeftJoin(join(GrantedOrgColumnId, ProjectGrantColumnGrantedOrgID)). LeftJoin(join(LoginNameUserIDCol, UserGrantUserID)). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, @@ -348,7 +413,8 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro orgName sql.NullString orgDomain sql.NullString - projectName sql.NullString + projectName sql.NullString + projectResourceOwner sql.NullString grantedOrgID sql.NullString grantedOrgName sql.NullString @@ -381,6 +447,7 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro &g.ProjectID, &projectName, + &projectResourceOwner, &grantedOrgID, &grantedOrgName, @@ -405,6 +472,7 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro g.OrgName = orgName.String g.OrgPrimaryDomain = orgDomain.String g.ProjectName = projectName.String + g.ProjectResourceOwner = projectResourceOwner.String g.GrantedOrgID = grantedOrgID.String g.GrantedOrgName = grantedOrgName.String g.GrantedOrgDomain = grantedOrgDomain.String @@ -439,6 +507,7 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e UserGrantProjectID.identifier(), ProjectColumnName.identifier(), + ProjectColumnResourceOwner.identifier(), GrantedOrgColumnId.identifier(), GrantedOrgColumnName.identifier(), @@ -451,7 +520,8 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e LeftJoin(join(HumanUserIDCol, UserGrantUserID)). LeftJoin(join(OrgColumnID, UserGrantResourceOwner)). LeftJoin(join(ProjectColumnID, UserGrantProjectID)). - LeftJoin(join(GrantedOrgColumnId, UserResourceOwnerCol)). + LeftJoin(join(ProjectGrantColumnGrantID, UserGrantGrantID) + " AND " + ProjectGrantColumnProjectID.identifier() + " = " + UserGrantProjectID.identifier()). + LeftJoin(join(GrantedOrgColumnId, ProjectGrantColumnGrantedOrgID)). LeftJoin(join(LoginNameUserIDCol, UserGrantUserID)). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, @@ -480,7 +550,8 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e grantedOrgName sql.NullString grantedOrgDomain sql.NullString - projectName sql.NullString + projectName sql.NullString + projectResourceOwner sql.NullString ) err := rows.Scan( @@ -509,6 +580,7 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e &g.ProjectID, &projectName, + &projectResourceOwner, &grantedOrgID, &grantedOrgName, @@ -532,6 +604,7 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e g.OrgName = orgName.String g.OrgPrimaryDomain = orgDomain.String g.ProjectName = projectName.String + g.ProjectResourceOwner = projectResourceOwner.String g.GrantedOrgID = grantedOrgID.String g.GrantedOrgName = grantedOrgName.String g.GrantedOrgDomain = grantedOrgDomain.String diff --git a/internal/query/user_grant_test.go b/internal/query/user_grant_test.go index 6a640c2ef2..dde04f2c88 100644 --- a/internal/query/user_grant_test.go +++ b/internal/query/user_grant_test.go @@ -37,6 +37,7 @@ var ( ", projections.orgs1.primary_domain" + ", projections.user_grants5.project_id" + ", projections.projects4.name" + + ", projections.projects4.resource_owner" + ", granted_orgs.id" + ", granted_orgs.name" + ", granted_orgs.primary_domain" + @@ -45,7 +46,8 @@ var ( " LEFT JOIN projections.users14_humans ON projections.user_grants5.user_id = projections.users14_humans.user_id AND projections.user_grants5.instance_id = projections.users14_humans.instance_id" + " LEFT JOIN projections.orgs1 ON projections.user_grants5.resource_owner = projections.orgs1.id AND projections.user_grants5.instance_id = projections.orgs1.instance_id" + " LEFT JOIN projections.projects4 ON projections.user_grants5.project_id = projections.projects4.id AND projections.user_grants5.instance_id = projections.projects4.instance_id" + - " LEFT JOIN projections.orgs1 AS granted_orgs ON projections.users14.resource_owner = granted_orgs.id AND projections.users14.instance_id = granted_orgs.instance_id" + + " LEFT JOIN projections.project_grants4 ON projections.user_grants5.grant_id = projections.project_grants4.grant_id AND projections.user_grants5.instance_id = projections.project_grants4.instance_id AND projections.project_grants4.project_id = projections.user_grants5.project_id" + + " LEFT JOIN projections.orgs1 AS granted_orgs ON projections.project_grants4.granted_org_id = granted_orgs.id AND projections.project_grants4.instance_id = granted_orgs.instance_id" + " LEFT JOIN projections.login_names3 ON projections.user_grants5.user_id = projections.login_names3.user_id AND projections.user_grants5.instance_id = projections.login_names3.instance_id" + " WHERE projections.login_names3.is_primary = $1") userGrantCols = []string{ @@ -71,6 +73,7 @@ var ( "primary_domain", "project_id", "name", // project name + "resource_owner", // project_grant resource owner "id", // granted org id "name", // granted org name "primary_domain", // granted org domain @@ -98,6 +101,7 @@ var ( ", projections.orgs1.primary_domain" + ", projections.user_grants5.project_id" + ", projections.projects4.name" + + ", projections.projects4.resource_owner" + ", granted_orgs.id" + ", granted_orgs.name" + ", granted_orgs.primary_domain" + @@ -107,7 +111,8 @@ var ( " LEFT JOIN projections.users14_humans ON projections.user_grants5.user_id = projections.users14_humans.user_id AND projections.user_grants5.instance_id = projections.users14_humans.instance_id" + " LEFT JOIN projections.orgs1 ON projections.user_grants5.resource_owner = projections.orgs1.id AND projections.user_grants5.instance_id = projections.orgs1.instance_id" + " LEFT JOIN projections.projects4 ON projections.user_grants5.project_id = projections.projects4.id AND projections.user_grants5.instance_id = projections.projects4.instance_id" + - " LEFT JOIN projections.orgs1 AS granted_orgs ON projections.users14.resource_owner = granted_orgs.id AND projections.users14.instance_id = granted_orgs.instance_id" + + " LEFT JOIN projections.project_grants4 ON projections.user_grants5.grant_id = projections.project_grants4.grant_id AND projections.user_grants5.instance_id = projections.project_grants4.instance_id AND projections.project_grants4.project_id = projections.user_grants5.project_id" + + " LEFT JOIN projections.orgs1 AS granted_orgs ON projections.project_grants4.granted_org_id = granted_orgs.id AND projections.project_grants4.instance_id = granted_orgs.instance_id" + " LEFT JOIN projections.login_names3 ON projections.user_grants5.user_id = projections.login_names3.user_id AND projections.user_grants5.instance_id = projections.login_names3.instance_id" + " WHERE projections.login_names3.is_primary = $1") userGrantsCols = append( @@ -175,6 +180,7 @@ func Test_UserGrantPrepares(t *testing.T) { "primary-domain", "project-id", "project-name", + "project-resource-owner", "granted-org-id", "granted-org-name", "granted-org-domain", @@ -182,31 +188,32 @@ func Test_UserGrantPrepares(t *testing.T) { ), }, object: &UserGrant{ - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeHuman, - UserResourceOwner: "resource-owner", - FirstName: "first-name", - LastName: "last-name", - Email: "email", - DisplayName: "display-name", - AvatarURL: "avatar-key", - PreferredLoginName: "login-name", - ResourceOwner: "ro", - OrgName: "org-name", - OrgPrimaryDomain: "primary-domain", - ProjectID: "project-id", - ProjectName: "project-name", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeHuman, + UserResourceOwner: "resource-owner", + FirstName: "first-name", + LastName: "last-name", + Email: "email", + DisplayName: "display-name", + AvatarURL: "avatar-key", + PreferredLoginName: "login-name", + ResourceOwner: "ro", + OrgName: "org-name", + OrgPrimaryDomain: "primary-domain", + ProjectID: "project-id", + ProjectName: "project-name", + ProjectResourceOwner: "project-resource-owner", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, { @@ -239,6 +246,7 @@ func Test_UserGrantPrepares(t *testing.T) { "primary-domain", "project-id", "project-name", + "project-resource-owner", "granted-org-id", "granted-org-name", "granted-org-domain", @@ -246,31 +254,32 @@ func Test_UserGrantPrepares(t *testing.T) { ), }, object: &UserGrant{ - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeMachine, - UserResourceOwner: "resource-owner", - FirstName: "", - LastName: "", - Email: "", - DisplayName: "", - AvatarURL: "", - PreferredLoginName: "login-name", - ResourceOwner: "ro", - OrgName: "org-name", - OrgPrimaryDomain: "primary-domain", - ProjectID: "project-id", - ProjectName: "project-name", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeMachine, + UserResourceOwner: "resource-owner", + FirstName: "", + LastName: "", + Email: "", + DisplayName: "", + AvatarURL: "", + PreferredLoginName: "login-name", + ResourceOwner: "ro", + OrgName: "org-name", + OrgPrimaryDomain: "primary-domain", + ProjectID: "project-id", + ProjectName: "project-name", + ProjectResourceOwner: "project-resource-owner", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, { @@ -303,6 +312,7 @@ func Test_UserGrantPrepares(t *testing.T) { nil, "project-id", "project-name", + "project-resource-owner", "granted-org-id", "granted-org-name", "granted-org-domain", @@ -310,31 +320,32 @@ func Test_UserGrantPrepares(t *testing.T) { ), }, object: &UserGrant{ - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeHuman, - UserResourceOwner: "resource-owner", - FirstName: "first-name", - LastName: "last-name", - Email: "email", - DisplayName: "display-name", - AvatarURL: "avatar-key", - PreferredLoginName: "login-name", - ResourceOwner: "ro", - OrgName: "", - OrgPrimaryDomain: "", - ProjectID: "project-id", - ProjectName: "project-name", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeHuman, + UserResourceOwner: "resource-owner", + FirstName: "first-name", + LastName: "last-name", + Email: "email", + DisplayName: "display-name", + AvatarURL: "avatar-key", + PreferredLoginName: "login-name", + ResourceOwner: "ro", + OrgName: "", + OrgPrimaryDomain: "", + ProjectID: "project-id", + ProjectName: "project-name", + ProjectResourceOwner: "project-resource-owner", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, { @@ -367,6 +378,7 @@ func Test_UserGrantPrepares(t *testing.T) { "primary-domain", "project-id", nil, + nil, "granted-org-id", "granted-org-name", "granted-org-domain", @@ -374,31 +386,32 @@ func Test_UserGrantPrepares(t *testing.T) { ), }, object: &UserGrant{ - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeHuman, - UserResourceOwner: "resource-owner", - FirstName: "first-name", - LastName: "last-name", - Email: "email", - DisplayName: "display-name", - AvatarURL: "avatar-key", - PreferredLoginName: "login-name", - ResourceOwner: "ro", - OrgName: "org-name", - OrgPrimaryDomain: "primary-domain", - ProjectID: "project-id", - ProjectName: "", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeHuman, + UserResourceOwner: "resource-owner", + FirstName: "first-name", + LastName: "last-name", + Email: "email", + DisplayName: "display-name", + AvatarURL: "avatar-key", + PreferredLoginName: "login-name", + ResourceOwner: "ro", + OrgName: "org-name", + OrgPrimaryDomain: "primary-domain", + ProjectID: "project-id", + ProjectName: "", + ProjectResourceOwner: "", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, { @@ -431,6 +444,7 @@ func Test_UserGrantPrepares(t *testing.T) { "primary-domain", "project-id", "project-name", + "project-resource-owner", "granted-org-id", "granted-org-name", "granted-org-domain", @@ -438,31 +452,32 @@ func Test_UserGrantPrepares(t *testing.T) { ), }, object: &UserGrant{ - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeHuman, - UserResourceOwner: "resource-owner", - FirstName: "first-name", - LastName: "last-name", - Email: "email", - DisplayName: "display-name", - AvatarURL: "avatar-key", - PreferredLoginName: "", - ResourceOwner: "ro", - OrgName: "org-name", - OrgPrimaryDomain: "primary-domain", - ProjectID: "project-id", - ProjectName: "project-name", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeHuman, + UserResourceOwner: "resource-owner", + FirstName: "first-name", + LastName: "last-name", + Email: "email", + DisplayName: "display-name", + AvatarURL: "avatar-key", + PreferredLoginName: "", + ResourceOwner: "ro", + OrgName: "org-name", + OrgPrimaryDomain: "primary-domain", + ProjectID: "project-id", + ProjectName: "project-name", + ProjectResourceOwner: "project-resource-owner", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, { @@ -525,6 +540,7 @@ func Test_UserGrantPrepares(t *testing.T) { "primary-domain", "project-id", "project-name", + "project-resource-owner", "granted-org-id", "granted-org-name", "granted-org-domain", @@ -538,31 +554,32 @@ func Test_UserGrantPrepares(t *testing.T) { }, UserGrants: []*UserGrant{ { - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeHuman, - UserResourceOwner: "resource-owner", - FirstName: "first-name", - LastName: "last-name", - Email: "email", - DisplayName: "display-name", - AvatarURL: "avatar-key", - PreferredLoginName: "login-name", - ResourceOwner: "ro", - OrgName: "org-name", - OrgPrimaryDomain: "primary-domain", - ProjectID: "project-id", - ProjectName: "project-name", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeHuman, + UserResourceOwner: "resource-owner", + FirstName: "first-name", + LastName: "last-name", + Email: "email", + DisplayName: "display-name", + AvatarURL: "avatar-key", + PreferredLoginName: "login-name", + ResourceOwner: "ro", + OrgName: "org-name", + OrgPrimaryDomain: "primary-domain", + ProjectID: "project-id", + ProjectName: "project-name", + ProjectResourceOwner: "project-resource-owner", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, }, @@ -598,6 +615,7 @@ func Test_UserGrantPrepares(t *testing.T) { "primary-domain", "project-id", "project-name", + "project-resource-owner", "granted-org-id", "granted-org-name", "granted-org-domain", @@ -611,31 +629,32 @@ func Test_UserGrantPrepares(t *testing.T) { }, UserGrants: []*UserGrant{ { - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeMachine, - UserResourceOwner: "resource-owner", - FirstName: "", - LastName: "", - Email: "", - DisplayName: "", - AvatarURL: "", - PreferredLoginName: "login-name", - ResourceOwner: "ro", - OrgName: "org-name", - OrgPrimaryDomain: "primary-domain", - ProjectID: "project-id", - ProjectName: "project-name", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeMachine, + UserResourceOwner: "resource-owner", + FirstName: "", + LastName: "", + Email: "", + DisplayName: "", + AvatarURL: "", + PreferredLoginName: "login-name", + ResourceOwner: "ro", + OrgName: "org-name", + OrgPrimaryDomain: "primary-domain", + ProjectID: "project-id", + ProjectName: "project-name", + ProjectResourceOwner: "project-resource-owner", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, }, @@ -671,6 +690,7 @@ func Test_UserGrantPrepares(t *testing.T) { nil, "project-id", "project-name", + "project-resource-owner", "granted-org-id", "granted-org-name", "granted-org-domain", @@ -684,31 +704,32 @@ func Test_UserGrantPrepares(t *testing.T) { }, UserGrants: []*UserGrant{ { - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeMachine, - UserResourceOwner: "resource-owner", - FirstName: "first-name", - LastName: "last-name", - Email: "email", - DisplayName: "display-name", - AvatarURL: "avatar-key", - PreferredLoginName: "login-name", - ResourceOwner: "ro", - OrgName: "", - OrgPrimaryDomain: "", - ProjectID: "project-id", - ProjectName: "project-name", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeMachine, + UserResourceOwner: "resource-owner", + FirstName: "first-name", + LastName: "last-name", + Email: "email", + DisplayName: "display-name", + AvatarURL: "avatar-key", + PreferredLoginName: "login-name", + ResourceOwner: "ro", + OrgName: "", + OrgPrimaryDomain: "", + ProjectID: "project-id", + ProjectName: "project-name", + ProjectResourceOwner: "project-resource-owner", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, }, @@ -744,6 +765,7 @@ func Test_UserGrantPrepares(t *testing.T) { "primary-domain", "project-id", nil, + nil, "granted-org-id", "granted-org-name", "granted-org-domain", @@ -757,31 +779,32 @@ func Test_UserGrantPrepares(t *testing.T) { }, UserGrants: []*UserGrant{ { - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeHuman, - UserResourceOwner: "resource-owner", - FirstName: "first-name", - LastName: "last-name", - Email: "email", - DisplayName: "display-name", - AvatarURL: "avatar-key", - PreferredLoginName: "login-name", - ResourceOwner: "ro", - OrgName: "org-name", - OrgPrimaryDomain: "primary-domain", - ProjectID: "project-id", - ProjectName: "", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeHuman, + UserResourceOwner: "resource-owner", + FirstName: "first-name", + LastName: "last-name", + Email: "email", + DisplayName: "display-name", + AvatarURL: "avatar-key", + PreferredLoginName: "login-name", + ResourceOwner: "ro", + OrgName: "org-name", + OrgPrimaryDomain: "primary-domain", + ProjectID: "project-id", + ProjectName: "", + ProjectResourceOwner: "", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, }, @@ -817,6 +840,7 @@ func Test_UserGrantPrepares(t *testing.T) { "primary-domain", "project-id", "project-name", + "project-resource-owner", "granted-org-id", "granted-org-name", "granted-org-domain", @@ -830,31 +854,32 @@ func Test_UserGrantPrepares(t *testing.T) { }, UserGrants: []*UserGrant{ { - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeHuman, - UserResourceOwner: "resource-owner", - FirstName: "first-name", - LastName: "last-name", - Email: "email", - DisplayName: "display-name", - AvatarURL: "avatar-key", - PreferredLoginName: "", - ResourceOwner: "ro", - OrgName: "org-name", - OrgPrimaryDomain: "primary-domain", - ProjectID: "project-id", - ProjectName: "project-name", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeHuman, + UserResourceOwner: "resource-owner", + FirstName: "first-name", + LastName: "last-name", + Email: "email", + DisplayName: "display-name", + AvatarURL: "avatar-key", + PreferredLoginName: "", + ResourceOwner: "ro", + OrgName: "org-name", + OrgPrimaryDomain: "primary-domain", + ProjectID: "project-id", + ProjectName: "project-name", + ProjectResourceOwner: "project-resource-owner", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, }, @@ -890,6 +915,7 @@ func Test_UserGrantPrepares(t *testing.T) { "primary-domain", "project-id", "project-name", + "project-resource-owner", "granted-org-id", "granted-org-name", "granted-org-domain", @@ -917,6 +943,7 @@ func Test_UserGrantPrepares(t *testing.T) { "primary-domain", "project-id", "project-name", + "project-resource-owner", "granted-org-id", "granted-org-name", "granted-org-domain", @@ -930,58 +957,60 @@ func Test_UserGrantPrepares(t *testing.T) { }, UserGrants: []*UserGrant{ { - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeHuman, - UserResourceOwner: "resource-owner", - FirstName: "first-name", - LastName: "last-name", - Email: "email", - DisplayName: "display-name", - AvatarURL: "avatar-key", - PreferredLoginName: "login-name", - ResourceOwner: "ro", - OrgName: "org-name", - OrgPrimaryDomain: "primary-domain", - ProjectID: "project-id", - ProjectName: "project-name", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeHuman, + UserResourceOwner: "resource-owner", + FirstName: "first-name", + LastName: "last-name", + Email: "email", + DisplayName: "display-name", + AvatarURL: "avatar-key", + PreferredLoginName: "login-name", + ResourceOwner: "ro", + OrgName: "org-name", + OrgPrimaryDomain: "primary-domain", + ProjectID: "project-id", + ProjectName: "project-name", + ProjectResourceOwner: "project-resource-owner", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, { - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeHuman, - UserResourceOwner: "resource-owner", - FirstName: "first-name", - LastName: "last-name", - Email: "email", - DisplayName: "display-name", - AvatarURL: "avatar-key", - PreferredLoginName: "login-name", - ResourceOwner: "ro", - OrgName: "org-name", - OrgPrimaryDomain: "primary-domain", - ProjectID: "project-id", - ProjectName: "project-name", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeHuman, + UserResourceOwner: "resource-owner", + FirstName: "first-name", + LastName: "last-name", + Email: "email", + DisplayName: "display-name", + AvatarURL: "avatar-key", + PreferredLoginName: "login-name", + ResourceOwner: "ro", + OrgName: "org-name", + OrgPrimaryDomain: "primary-domain", + ProjectID: "project-id", + ProjectName: "project-name", + ProjectResourceOwner: "project-resource-owner", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, }, diff --git a/internal/query/user_membership.go b/internal/query/user_membership.go index cb7588624f..069210a830 100644 --- a/internal/query/user_membership.go +++ b/internal/query/user_membership.go @@ -63,7 +63,15 @@ type MembershipSearchQuery struct { } func NewMembershipUserIDQuery(userID string) (SearchQuery, error) { - return NewTextQuery(membershipUserID.setTable(membershipAlias), userID, TextEquals) + return NewTextQuery(MembershipUserID.setTable(membershipAlias), userID, TextEquals) +} + +func NewMembershipCreationDateQuery(timestamp time.Time, comparison TimestampComparison) (SearchQuery, error) { + return NewTimestampQuery(MembershipCreationDate.setTable(membershipAlias), timestamp, comparison) +} + +func NewMembershipChangeDateQuery(timestamp time.Time, comparison TimestampComparison) (SearchQuery, error) { + return NewTimestampQuery(MembershipChangeDate.setTable(membershipAlias), timestamp, comparison) } func NewMembershipOrgIDQuery(value string) (SearchQuery, error) { @@ -137,7 +145,7 @@ func (q *Queries) Memberships(ctx context.Context, queries *MembershipSearchQuer wg.Wait() } - query, queryArgs, scan := prepareMembershipsQuery(queries) + query, queryArgs, scan := prepareMembershipsQuery(ctx, queries, false) eq := sq.Eq{membershipInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -166,7 +174,7 @@ var ( name: "members", instanceIDCol: projection.MemberInstanceID, } - membershipUserID = Column{ + MembershipUserID = Column{ name: projection.MemberUserIDCol, table: membershipAlias, } @@ -174,11 +182,11 @@ var ( name: projection.MemberRolesCol, table: membershipAlias, } - membershipCreationDate = Column{ + MembershipCreationDate = Column{ name: projection.MemberCreationDate, table: membershipAlias, } - membershipChangeDate = Column{ + MembershipChangeDate = Column{ name: projection.MemberChangeDate, table: membershipAlias, } @@ -216,11 +224,11 @@ var ( } ) -func getMembershipFromQuery(queries *MembershipSearchQuery) (string, []interface{}) { - orgMembers, orgMembersArgs := prepareOrgMember(queries) - iamMembers, iamMembersArgs := prepareIAMMember(queries) - projectMembers, projectMembersArgs := prepareProjectMember(queries) - projectGrantMembers, projectGrantMembersArgs := prepareProjectGrantMember(queries) +func getMembershipFromQuery(ctx context.Context, queries *MembershipSearchQuery, permissionV2 bool) (string, []interface{}) { + orgMembers, orgMembersArgs := prepareOrgMember(ctx, queries, permissionV2) + iamMembers, iamMembersArgs := prepareIAMMember(ctx, queries, permissionV2) + projectMembers, projectMembersArgs := prepareProjectMember(ctx, queries, permissionV2) + projectGrantMembers, projectGrantMembersArgs := prepareProjectGrantMember(ctx, queries, permissionV2) args := make([]interface{}, 0) args = append(append(append(append(args, orgMembersArgs...), iamMembersArgs...), projectMembersArgs...), projectGrantMembersArgs...) @@ -236,13 +244,13 @@ func getMembershipFromQuery(queries *MembershipSearchQuery) (string, []interface args } -func prepareMembershipsQuery(queries *MembershipSearchQuery) (sq.SelectBuilder, []interface{}, func(*sql.Rows) (*Memberships, error)) { - query, args := getMembershipFromQuery(queries) +func prepareMembershipsQuery(ctx context.Context, queries *MembershipSearchQuery, permissionV2 bool) (sq.SelectBuilder, []interface{}, func(*sql.Rows) (*Memberships, error)) { + query, args := getMembershipFromQuery(ctx, queries, permissionV2) return sq.Select( - membershipUserID.identifier(), + MembershipUserID.identifier(), membershipRoles.identifier(), - membershipCreationDate.identifier(), - membershipChangeDate.identifier(), + MembershipCreationDate.identifier(), + MembershipChangeDate.identifier(), membershipSequence.identifier(), membershipResourceOwner.identifier(), membershipOrgID.identifier(), @@ -257,7 +265,7 @@ func prepareMembershipsQuery(queries *MembershipSearchQuery) (sq.SelectBuilder, ).From(query). LeftJoin(join(ProjectColumnID, membershipProjectID)). LeftJoin(join(OrgColumnID, membershipOrgID)). - LeftJoin(join(ProjectGrantColumnGrantID, membershipGrantID)). + LeftJoin(join(ProjectGrantColumnGrantID, membershipGrantID) + " AND " + membershipProjectID.identifier() + " = " + ProjectGrantColumnProjectID.identifier()). LeftJoin(join(InstanceColumnID, membershipInstanceID)). PlaceholderFormat(sq.Dollar), args, @@ -340,7 +348,7 @@ func prepareMembershipsQuery(queries *MembershipSearchQuery) (sq.SelectBuilder, } } -func prepareOrgMember(query *MembershipSearchQuery) (string, []interface{}) { +func prepareOrgMember(ctx context.Context, query *MembershipSearchQuery, permissionV2 bool) (string, []interface{}) { builder := sq.Select( OrgMemberUserID.identifier(), OrgMemberRoles.identifier(), @@ -354,6 +362,7 @@ func prepareOrgMember(query *MembershipSearchQuery) (string, []interface{}) { "NULL::TEXT AS "+membershipProjectID.name, "NULL::TEXT AS "+membershipGrantID.name, ).From(orgMemberTable.identifier()) + builder = administratorOrgPermissionCheckV2(ctx, builder, permissionV2) for _, q := range query.Queries { if q.Col().table.name == membershipAlias.name || q.Col().table.name == orgMemberTable.name { @@ -363,7 +372,7 @@ func prepareOrgMember(query *MembershipSearchQuery) (string, []interface{}) { return builder.MustSql() } -func prepareIAMMember(query *MembershipSearchQuery) (string, []interface{}) { +func prepareIAMMember(ctx context.Context, query *MembershipSearchQuery, permissionV2 bool) (string, []interface{}) { builder := sq.Select( InstanceMemberUserID.identifier(), InstanceMemberRoles.identifier(), @@ -377,6 +386,7 @@ func prepareIAMMember(query *MembershipSearchQuery) (string, []interface{}) { "NULL::TEXT AS "+membershipProjectID.name, "NULL::TEXT AS "+membershipGrantID.name, ).From(instanceMemberTable.identifier()) + builder = administratorInstancePermissionCheckV2(ctx, builder, permissionV2) for _, q := range query.Queries { if q.Col().table.name == membershipAlias.name || q.Col().table.name == instanceMemberTable.name { @@ -386,7 +396,7 @@ func prepareIAMMember(query *MembershipSearchQuery) (string, []interface{}) { return builder.MustSql() } -func prepareProjectMember(query *MembershipSearchQuery) (string, []interface{}) { +func prepareProjectMember(ctx context.Context, query *MembershipSearchQuery, permissionV2 bool) (string, []interface{}) { builder := sq.Select( ProjectMemberUserID.identifier(), ProjectMemberRoles.identifier(), @@ -400,6 +410,7 @@ func prepareProjectMember(query *MembershipSearchQuery) (string, []interface{}) ProjectMemberProjectID.identifier(), "NULL::TEXT AS "+membershipGrantID.name, ).From(projectMemberTable.identifier()) + builder = administratorProjectPermissionCheckV2(ctx, builder, permissionV2) for _, q := range query.Queries { if q.Col().table.name == membershipAlias.name || q.Col().table.name == projectMemberTable.name { @@ -410,7 +421,7 @@ func prepareProjectMember(query *MembershipSearchQuery) (string, []interface{}) return builder.MustSql() } -func prepareProjectGrantMember(query *MembershipSearchQuery) (string, []interface{}) { +func prepareProjectGrantMember(ctx context.Context, query *MembershipSearchQuery, permissionV2 bool) (string, []interface{}) { builder := sq.Select( ProjectGrantMemberUserID.identifier(), ProjectGrantMemberRoles.identifier(), @@ -424,6 +435,7 @@ func prepareProjectGrantMember(query *MembershipSearchQuery) (string, []interfac ProjectGrantMemberProjectID.identifier(), ProjectGrantMemberGrantID.identifier(), ).From(projectGrantMemberTable.identifier()) + builder = administratorProjectGrantPermissionCheckV2(ctx, builder, permissionV2) for _, q := range query.Queries { if q.Col().table.name == membershipAlias.name || q.Col().table.name == projectMemberTable.name || q.Col().table.name == projectGrantMemberTable.name { diff --git a/internal/query/user_membership_test.go b/internal/query/user_membership_test.go index b0170182d1..ce48770fd8 100644 --- a/internal/query/user_membership_test.go +++ b/internal/query/user_membership_test.go @@ -1,6 +1,7 @@ package query import ( + "context" "database/sql" "database/sql/driver" "errors" @@ -85,7 +86,7 @@ var ( ") AS members" + " LEFT JOIN projections.projects4 ON members.project_id = projections.projects4.id AND members.instance_id = projections.projects4.instance_id" + " LEFT JOIN projections.orgs1 ON members.org_id = projections.orgs1.id AND members.instance_id = projections.orgs1.instance_id" + - " LEFT JOIN projections.project_grants4 ON members.grant_id = projections.project_grants4.grant_id AND members.instance_id = projections.project_grants4.instance_id" + + " LEFT JOIN projections.project_grants4 ON members.grant_id = projections.project_grants4.grant_id AND members.instance_id = projections.project_grants4.instance_id AND members.project_id = projections.project_grants4.project_id" + " LEFT JOIN projections.instances ON members.instance_id = projections.instances.id") membershipCols = []string{ "user_id", @@ -461,7 +462,7 @@ func Test_MembershipPrepares(t *testing.T) { func prepareMembershipWrapper() func() (sq.SelectBuilder, func(*sql.Rows) (*Memberships, error)) { return func() (sq.SelectBuilder, func(*sql.Rows) (*Memberships, error)) { - builder, _, fun := prepareMembershipsQuery(&MembershipSearchQuery{}) + builder, _, fun := prepareMembershipsQuery(context.Background(), &MembershipSearchQuery{}, false) return builder, fun } } diff --git a/internal/query/user_metadata.go b/internal/query/user_metadata.go index 534c707593..385c176e0a 100644 --- a/internal/query/user_metadata.go +++ b/internal/query/user_metadata.go @@ -4,12 +4,14 @@ import ( "context" "database/sql" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -36,6 +38,28 @@ type UserMetadataSearchQueries struct { Queries []SearchQuery } +func userMetadataCheckPermission(ctx context.Context, userMetadataList *UserMetadataList, permissionCheck domain.PermissionCheck) { + userMetadataList.Metadata = slices.DeleteFunc(userMetadataList.Metadata, + func(userMetadata *UserMetadata) bool { + return userCheckPermission(ctx, userMetadata.ResourceOwner, userMetadata.UserID, permissionCheck) != nil + }, + ) +} + +func userMetadataPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *UserMetadataSearchQueries) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + UserMetadataResourceOwnerCol, + domain.PermissionUserRead, + SingleOrgPermissionOption(queries.Queries), + OwnedRowsPermissionOption(UserMetadataUserIDCol), + ) + return query.JoinClause(join, args...) +} + var ( userMetadataTable = table{ name: projection.UserMetadataProjectionTable, @@ -139,7 +163,19 @@ func (q *Queries) SearchUserMetadataForUsers(ctx context.Context, shouldTriggerB return metadata, err } -func (q *Queries) SearchUserMetadata(ctx context.Context, shouldTriggerBulk bool, userID string, queries *UserMetadataSearchQueries, withOwnerRemoved bool) (metadata *UserMetadataList, err error) { +func (q *Queries) SearchUserMetadata(ctx context.Context, shouldTriggerBulk bool, userID string, queries *UserMetadataSearchQueries, permissionCheck domain.PermissionCheck) (metadata *UserMetadataList, err error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + users, err := q.searchUserMetadata(ctx, shouldTriggerBulk, userID, queries, permissionCheckV2) + if err != nil { + return nil, err + } + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + userMetadataCheckPermission(ctx, users, permissionCheck) + } + return users, nil +} + +func (q *Queries) searchUserMetadata(ctx context.Context, shouldTriggerBulk bool, userID string, queries *UserMetadataSearchQueries, permissionCheckV2 bool) (metadata *UserMetadataList, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -151,6 +187,7 @@ func (q *Queries) SearchUserMetadata(ctx context.Context, shouldTriggerBulk bool } query, scan := prepareUserMetadataListQuery() + query = userMetadataPermissionCheckV2(ctx, query, permissionCheckV2, queries) eq := sq.Eq{ UserMetadataUserIDCol.identifier(): userID, UserMetadataInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), diff --git a/internal/repository/org/aggregate.go b/internal/repository/org/aggregate.go index 58121afbcb..ff9be33085 100644 --- a/internal/repository/org/aggregate.go +++ b/internal/repository/org/aggregate.go @@ -1,6 +1,8 @@ package org import ( + "context" + "github.com/zitadel/zitadel/internal/eventstore" ) @@ -27,3 +29,7 @@ func NewAggregate(id string) *Aggregate { }, } } + +func AggregateFromWriteModel(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate { + return eventstore.AggregateFromWriteModelCtx(ctx, wm, AggregateType, AggregateVersion) +} diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index d58b2eb64a..5c7742b5f7 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -448,8 +448,6 @@ Errors: Invalid: Потребителското разрешение е невалидно NotChanged: Потребителското разрешение не е променено IDMissing: ID липсва - NotActive: Потребителското разрешение не е активно - NotInactive: Предоставянето на потребител не е деактивирано NoPermissionForProject: Потребителят няма разрешения за този проект RoleKeyNotFound: Ролята не е намерена Member: diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index d248ce4ca7..26e7c5b6f4 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -436,8 +436,6 @@ Errors: Invalid: Uživatelský grant je neplatný NotChanged: Uživatelský grant nebyl změněn IDMissing: Chybí Id - NotActive: Uživatelský grant není aktivní - NotInactive: Uživatelský grant není deaktivován NoPermissionForProject: Uživatel nemá na tomto projektu žádná oprávnění RoleKeyNotFound: Role nenalezena Member: diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 96edf57456..b7e36febf4 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -436,8 +436,6 @@ Errors: Invalid: Benutzer Berechtigung ist ungültig NotChanged: Benutzer Berechtigung wurde nicht verändert IDMissing: ID fehlt - NotActive: Benutzer Berechtigung ist nicht aktiv - NotInactive: Benutzer Berechtigung ist nicht deaktiviert NoPermissionForProject: Benutzer hat keine Rechte auf diesem Projekt RoleKeyNotFound: Rolle konnte nicht gefunden werden Member: diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 0f512defe4..7385a4a025 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -437,8 +437,6 @@ Errors: Invalid: User grant is invalid NotChanged: User grant has not been changed IDMissing: Id missing - NotActive: User grant is not active - NotInactive: User grant is not deactivated NoPermissionForProject: User has no permissions on this project RoleKeyNotFound: Role not found Member: diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index 8c901f8ebe..a41c7818c6 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -436,8 +436,6 @@ Errors: Invalid: La concesión de usuario no es válida NotChanged: La concesión de usuario no ha cambiado IDMissing: Falta Id - NotActive: La concesión de usuario no está activa - NotInactive: La concesión de usuario no está inactiva NoPermissionForProject: El usuario no tiene permisos en este proyecto RoleKeyNotFound: Rol no encontrado Member: diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 2a2a51d7c4..f414419216 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -436,8 +436,6 @@ Errors: Invalid: La subvention d'utilisateur n'est pas valide NotChanged: L'autorisation de l'utilisateur n'a pas été modifiée. IDMissing: Id manquant - NotActive: La subvention de l'utilisateur n'est pas active - NotInactive: La subvention à l'utilisateur n'est pas désactivée NoPermissionForProject: L'utilisateur n'a aucune autorisation pour ce projet RoleKeyNotFound: Rôle non trouvé Member: diff --git a/internal/static/i18n/hu.yaml b/internal/static/i18n/hu.yaml index a4cc908fa2..60f3d87074 100644 --- a/internal/static/i18n/hu.yaml +++ b/internal/static/i18n/hu.yaml @@ -436,8 +436,6 @@ Errors: Invalid: A felhasználói jogosultság érvénytelen NotChanged: A felhasználói jogosultság nem lett módosítva IDMissing: Hiányzó azonosító - NotActive: A felhasználói jogosultság nem aktív - NotInactive: A felhasználói jogosultság nincs kikapcsolva NoPermissionForProject: A felhasználónak nincs jogosultsága ebben a projektben RoleKeyNotFound: Szerepkör nem található Member: diff --git a/internal/static/i18n/id.yaml b/internal/static/i18n/id.yaml index c9187020f7..2a9a8ee2c3 100644 --- a/internal/static/i18n/id.yaml +++ b/internal/static/i18n/id.yaml @@ -436,8 +436,6 @@ Errors: Invalid: Hibah pengguna tidak valid NotChanged: Hibah pengguna belum diubah IDMissing: Aku hilang - NotActive: Hibah pengguna tidak aktif - NotInactive: Hibah pengguna tidak dinonaktifkan NoPermissionForProject: Pengguna tidak memiliki izin pada proyek ini RoleKeyNotFound: Peran tidak ditemukan Member: diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index d1dccef4c7..cbae5543e2 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -436,8 +436,6 @@ Errors: Invalid: User Grant non è valido NotChanged: User Grant non è stata cambiato IDMissing: ID mancante - NotActive: User Grant non è attivo - NotInactive: User Grant non è disattivato NoPermissionForProject: L'utente non ha permessi su questo progetto RoleKeyNotFound: Ruolo non trovato Member: diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index 4b0f2ea203..1d28dc16e5 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -437,8 +437,6 @@ Errors: Invalid: 無効なユーザーグラントです NotChanged: ユーザーグラントは変更されていません IDMissing: IDがありません - NotActive: ユーザーグラントはアクティブではありません - NotInactive: ユーザーグラントは非アクティブではありません NoPermissionForProject: ユーザーにはこのプロジェクトに許可がありません RoleKeyNotFound: ロールが見つかりません Member: diff --git a/internal/static/i18n/ko.yaml b/internal/static/i18n/ko.yaml index 2c87aa1f97..f0b19c6caa 100644 --- a/internal/static/i18n/ko.yaml +++ b/internal/static/i18n/ko.yaml @@ -437,8 +437,6 @@ Errors: Invalid: 사용자 권한이 유효하지 않습니다 NotChanged: 사용자 권한이 변경되지 않았습니다 IDMissing: ID가 누락되었습니다 - NotActive: 사용자 권한이 활성 상태가 아닙니다 - NotInactive: 사용자 권한이 비활성 상태가 아닙니다 NoPermissionForProject: 사용자가 이 프로젝트에 대한 권한이 없습니다 RoleKeyNotFound: 역할을 찾을 수 없습니다 Member: diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index 64ae87a618..7d03f93ee0 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -435,8 +435,6 @@ Errors: Invalid: Овластувањето на корисникот е невалидно NotChanged: Овластувањето на корисникот не е променето IDMissing: ID недостасува - NotActive: Овластувањето на корисникот не е активно - NotInactive: Овластувањето на корисникот не е неактивно NoPermissionForProject: Корисникот нема овластувања за овој проект RoleKeyNotFound: Улогата не е пронајдена Member: diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index dc9fd83721..2fb1b9ac3b 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -436,8 +436,6 @@ Errors: Invalid: Gebruikerstoekenning is ongeldig NotChanged: Gebruikerstoekenning is niet veranderd IDMissing: ID ontbreekt - NotActive: Gebruikerstoekenning is niet actief - NotInactive: Gebruikerstoekenning is niet gedeactiveerd NoPermissionForProject: Gebruiker heeft geen rechten op dit project RoleKeyNotFound: Rol niet gevonden Member: diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 4952345510..a45173cf63 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -436,8 +436,6 @@ Errors: Invalid: Uprawnienie użytkownika jest nieprawidłowe NotChanged: Uprawnienie użytkownika nie zostało zmienione IDMissing: Brak ID - NotActive: Uprawnienie użytkownika nie jest aktywne - NotInactive: Uprawnienie użytkownika nie jest dezaktywowane NoPermissionForProject: Użytkownik nie ma uprawnień do tego projektu RoleKeyNotFound: Rola nie znaleziona Member: diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index e5fc785d0c..866f041e6e 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -435,8 +435,6 @@ Errors: Invalid: A concessão de usuário é inválida NotChanged: A concessão de usuário não foi alterada IDMissing: ID faltando - NotActive: A concessão de usuário não está ativa - NotInactive: A concessão de usuário não está desativada NoPermissionForProject: O usuário não possui permissões neste projeto RoleKeyNotFound: Função não encontrada Member: diff --git a/internal/static/i18n/ro.yaml b/internal/static/i18n/ro.yaml index ece4680de6..f56dfd13d2 100644 --- a/internal/static/i18n/ro.yaml +++ b/internal/static/i18n/ro.yaml @@ -437,8 +437,6 @@ Errors: Invalid: Acordarea utilizatorului este invalidă NotChanged: Acordarea utilizatorului nu a fost schimbată IDMissing: Id lipsă - NotActive: Acordarea utilizatorului nu este activă - NotInactive: Acordarea utilizatorului nu este dezactivată NoPermissionForProject: Utilizatorul nu are permisiuni pentru acest proiect RoleKeyNotFound: Rolul nu a fost găsit Member: diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index a2efd25322..34eb6b9837 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -430,8 +430,6 @@ Errors: Invalid: Допуск пользователя недействителен NotChanged: Допуск пользователя не был изменён IDMissing: ID отсутствует - NotActive: Допуск пользователя неактивен - NotInactive: Допуск пользователя не деактивирован NoPermissionForProject: Пользователь не имеет прав доступа к данному проекту RoleKeyNotFound: Роль не найдена Member: diff --git a/internal/static/i18n/sv.yaml b/internal/static/i18n/sv.yaml index be40ceba3c..c8c13e683e 100644 --- a/internal/static/i18n/sv.yaml +++ b/internal/static/i18n/sv.yaml @@ -436,8 +436,6 @@ Errors: Invalid: Användarbeviljandet är ogiltigt NotChanged: Användarbeviljandet har inte ändrats IDMissing: Id saknas - NotActive: Användarbeviljandet är inte aktivt - NotInactive: Användarbeviljandet är inte inaktivt NoPermissionForProject: Användaren har inga behörigheter i detta projekt RoleKeyNotFound: Rollen hittades inte Member: diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 930fcaddae..1ef3daadcd 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -436,8 +436,6 @@ Errors: Invalid: 用户授权无效 NotChanged: 用户授权未更改 IDMissing: 没有 ID - NotActive: 用户授权不是启用状态 - NotInactive: 用户授权不是停用状态 NoPermissionForProject: 用户对此项目没有权限 RoleKeyNotFound: 角色不存在 Member: diff --git a/pkg/grpc/app/v2beta/application.go b/pkg/grpc/app/v2beta/application.go index bbce4289f9..5cef08db46 100644 --- a/pkg/grpc/app/v2beta/application.go +++ b/pkg/grpc/app/v2beta/application.go @@ -2,4 +2,4 @@ package app type ApplicationConfig = isApplication_Config -type MetaType = isUpdateSAMLApplicationConfigurationRequest_Metadata \ No newline at end of file +type MetaType = isUpdateSAMLApplicationConfigurationRequest_Metadata diff --git a/pkg/grpc/internal_permission/v2beta/resource.go b/pkg/grpc/internal_permission/v2beta/resource.go new file mode 100644 index 0000000000..a57f60242e --- /dev/null +++ b/pkg/grpc/internal_permission/v2beta/resource.go @@ -0,0 +1,3 @@ +package internal_permission + +type Resource = isAdministrator_Resource diff --git a/proto/zitadel/auth.proto b/proto/zitadel/auth.proto index 0ee6ad86d8..c995bce16a 100644 --- a/proto/zitadel/auth.proto +++ b/proto/zitadel/auth.proto @@ -859,6 +859,11 @@ service AuthService { }; } + // List My Authorizations / User Grants + // + // Deprecated: [List authorizations](apis/resources/authorization_service_v2/zitadel-authorization-v-2-beta-authorization-service-list-authorizations.api.mdx) and pass the user ID filter with your users ID to search for your authorizations on granted and owned projects. + // + // Returns a list of the authorizations/user grants the authenticated user has. User grants consist of an organization, a project and 1-n roles. rpc ListMyUserGrants(ListMyUserGrantsRequest) returns (ListMyUserGrantsResponse) { option (google.api.http) = { post: "/usergrants/me/_search" @@ -869,9 +874,8 @@ service AuthService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "User Authorizations/Grants" - summary: "List My Authorizations/Grants"; - description: "Returns a list of the authorizations/user grants the authenticated user has. User grants consist of an organization, a project and 1-n roles." + tags: "User Authorizations/Grants"; + deprecated: true; }; } @@ -908,6 +912,11 @@ service AuthService { }; } + // List My Project Roles + // + // Deprecated: [List authorizations](apis/resources/authorization_service_v2/zitadel-authorization-v-2-beta-authorization-service-list-authorizations.api.mdx) and pass the user ID filter with your users ID and the project ID filter to search for your authorizations on a granted and an owned project. + // + // Returns a list of roles for the authenticated user and for the requesting project. rpc ListMyProjectPermissions(ListMyProjectPermissionsRequest) returns (ListMyProjectPermissionsResponse) { option (google.api.http) = { post: "/permissions/me/_search" @@ -919,8 +928,7 @@ service AuthService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "User Authorizations/Grants" - summary: "List My Project Roles"; - description: "Returns a list of roles for the authenticated user and for the requesting project (based on the token)." + deprecated: true; }; } diff --git a/proto/zitadel/authorization/v2beta/authorization.proto b/proto/zitadel/authorization/v2beta/authorization.proto new file mode 100644 index 0000000000..aedd4c8b3c --- /dev/null +++ b/proto/zitadel/authorization/v2beta/authorization.proto @@ -0,0 +1,181 @@ +syntax = "proto3"; + +package zitadel.authorization.v2beta; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; + +import "zitadel/filter/v2beta/filter.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta;authorization"; + +message Authorization { + // ID is the unique identifier of the authorization. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + + // ID is the unique identifier of the project the user was granted the authorization for. + string project_id = 2; + // Name is the name of the project the user was granted the authorization for. + string project_name = 3; + // OrganizationID is the ID of the organization the project belongs to. + string project_organization_id = 4; + // ID of the granted project, only provided if it is a granted project. + optional string project_grant_id = 5; + // ID of the organization the project is granted to, only provided if it is a granted project. + optional string granted_organization_id = 6; + + // The unique identifier of the organization the authorization belongs to. + string organization_id = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // CreationDate is the timestamp when the authorization was created. + google.protobuf.Timestamp creation_date = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // ChangeDate is the timestamp when the authorization was last updated. + // In case the authorization was not updated, this field is equal to the creation date. + google.protobuf.Timestamp change_date = 9 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // State is the current state of the authorization. + State state = 10; + User user = 11; + // Roles contains the roles the user was granted for the project. + repeated string roles = 12; +} + +enum State { + STATE_UNSPECIFIED = 0; + // An active authorization grants the user access with the roles specified on the project. + STATE_ACTIVE = 1; + // An inactive authorization temporarily deactivates the granted access and roles. + // ZITADEL will not include the specific authorization in any authorization information like an access token. + // But the information can still be accessed using the API. + STATE_INACTIVE = 2; +} + +message User { + // ID represents the ID of the user who was granted the authorization. + string id = 1; + // PreferredLoginName represents the preferred login name of the granted user. + string preferred_login_name = 2; + // DisplayName represents the public display name of the granted user. + string display_name = 3; + // AvatarURL is the URL to the user's public avatar image. + string avatar_url = 4; + // The organization the user belong to. + // This does not have to correspond with the authorizations organization. + string organization_id = 5; +} + +message AuthorizationsSearchFilter { + oneof filter { + option (validate.required) = true; + + // Search for authorizations by their IDs. + zitadel.filter.v2beta.InIDsFilter authorization_ids = 1; + // Search for an organizations authorizations by its ID. + zitadel.filter.v2beta.IDFilter organization_id = 2; + // Search for authorizations by their state. + StateQuery state = 3; + // Search for authorizations by the ID of the user who was granted the authorization. + zitadel.filter.v2beta.IDFilter user_id = 4; + // Search for authorizations by the ID of the organisation the user is part of. + zitadel.filter.v2beta.IDFilter user_organization_id = 5; + // Search for authorizations by the preferred login name of the granted user. + UserPreferredLoginNameQuery user_preferred_login_name = 6; + // Search for authorizations by the public display name of the granted user. + UserDisplayNameQuery user_display_name = 7; + // Search for authorizations by the ID of the project the user was granted the authorization for. + // This will also include authorizations granted for project grants of the same project. + zitadel.filter.v2beta.IDFilter project_id = 8; + // Search for authorizations by the name of the project the user was granted the authorization for. + // This will also include authorizations granted for project grants of the same project. + ProjectNameQuery project_name = 9; + // Search for authorizations by the key of the role the user was granted. + RoleKeyQuery role_key = 10; + // Search for authorizations by the ID of the project grant the user was granted the authorization for. + // This will also include authorizations granted for project grants of the same project. + zitadel.filter.v2beta.IDFilter project_grant_id = 11; + } +} + +message StateQuery { + // Specify the state of the authorization to search for. + State state = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; +} + +message UserPreferredLoginNameQuery { + // Specify the preferred login name of the granted user to search for. + string login_name = 1 [(validate.rules).string = { + min_len: 1 + max_len: 200 + }]; + // Specify the method to search for the preferred login name. Default is EQUAL. + // For example, to search for all authorizations granted to a user with + // a preferred login name containing a specific string, use CONTAINS or CONTAINS_IGNORE_CASE. + zitadel.filter.v2beta.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true]; +} + +message UserDisplayNameQuery { + // Specify the public display name of the granted user to search for. + string display_name = 1 [(validate.rules).string = { + min_len: 1 + max_len: 200 + }]; + // Specify the method to search for the display name. Default is EQUAL. + // For example, to search for all authorizations granted to a user with + // a display name containing a specific string, use CONTAINS or CONTAINS_IGNORE_CASE. + zitadel.filter.v2beta.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true]; +} + +message ProjectNameQuery { + // Specify the name of the project the user was granted the authorization for to search for. + // Note that this will also include authorizations granted for project grants of the same project. + string name = 1 [(validate.rules).string = {max_len: 200}]; + // Specify the method to search for the project name. Default is EQUAL. + // For example, to search for all authorizations granted on a project with + // a name containing a specific string, use CONTAINS or CONTAINS_IGNORE_CASE. + zitadel.filter.v2beta.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true]; +} + +message OrganizationNameQuery { + // Specify the name of the organization the authorization was granted for to search for. + // This can either be the organization the project or the project grant is part of. + string name = 1 [(validate.rules).string = {max_len: 200}]; + // Specify the method to search for the organization name. Default is EQUAL. + // For example, to search for all authorizations with an organization name containing a specific string, + // use CONTAINS or CONTAINS_IGNORE_CASE. + zitadel.filter.v2beta.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true]; +} + +message RoleKeyQuery { + // Specify the key of the role the user was granted to search for. + string key = 1 [(validate.rules).string = {max_len: 200}]; + // Specify the method to search for the role key. Default is EQUAL. + // For example, to search for all authorizations starting with a specific role key, + // use STARTS_WITH or STARTS_WITH_IGNORE_CASE. + zitadel.filter.v2beta.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true]; +} + +enum AuthorizationFieldName { + AUTHORIZATION_FIELD_NAME_UNSPECIFIED = 0; + AUTHORIZATION_FIELD_NAME_CREATED_DATE = 1; + AUTHORIZATION_FIELD_NAME_CHANGED_DATE = 2; + AUTHORIZATION_FIELD_NAME_ID = 3; + AUTHORIZATION_FIELD_NAME_USER_ID = 4; + AUTHORIZATION_FIELD_NAME_PROJECT_ID = 5; + AUTHORIZATION_FIELD_NAME_ORGANIZATION_ID = 6; + AUTHORIZATION_FIELD_NAME_USER_ORGANIZATION_ID = 7; +} diff --git a/proto/zitadel/authorization/v2beta/authorization_service.proto b/proto/zitadel/authorization/v2beta/authorization_service.proto new file mode 100644 index 0000000000..5020154883 --- /dev/null +++ b/proto/zitadel/authorization/v2beta/authorization_service.proto @@ -0,0 +1,456 @@ +syntax = "proto3"; + +package zitadel.authorization.v2beta; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; +import "google/api/annotations.proto"; + +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/authorization/v2beta/authorization.proto"; +import "zitadel/filter/v2beta/filter.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta;authorization"; + +// AuthorizationService provides methods to manage authorizations for users within your projects and applications. +// +// For managing permissions and roles for ZITADEL internal resources, like organizations, projects, +// users, etc., please use the InternalPermissionService. +service AuthorizationService { + + // List Authorizations + // + // ListAuthorizations returns all authorizations matching the request and necessary permissions. + // + // Required permissions: + // - "user.grant.read" + // - no permissions required for listing own authorizations + rpc ListAuthorizations(ListAuthorizationsRequest) returns (ListAuthorizationsResponse) { + option (google.api.http) = { + // The only reason why it is used here is to avoid a conflict with the ListUsers endpoint, which already handles POST /v2/users. + post: "/v2beta/authorizations/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 authorizations matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } + + // Create Authorization + // + // CreateAuthorization creates a new authorization for a user in an owned or granted project. + // + // Required permissions: + // - "user.grant.write" + rpc CreateAuthorization(CreateAuthorizationRequest) returns (CreateAuthorizationResponse) { + option (google.api.http) = { + post: "/v2beta/authorizations" + 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: "The newly created authorization"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid create request"; + }; + }; + responses: { + key: "409" + value: { + description: "The authorization already exists."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + + // Update Authorization + // + // UpdateAuthorization updates the authorization. + // + // Note that any role keys previously granted to the user and not present in the request will be revoked. + // + // Required permissions: + // - "user.grant.write" + rpc UpdateAuthorization(UpdateAuthorizationRequest) returns (UpdateAuthorizationResponse) { + option (google.api.http) = { + patch: "/v2beta/authorizations/{id}" + 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: "OK"; + }; + }; + responses: { + key: "404"; + value: { + description: "Authorization or one of the roles do not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + } + }; + } + + // Delete Authorization + // + // DeleteAuthorization deletes the authorization. + // + // In case the authorization is not found, the request will return a successful response as + // the desired state is already achieved. + // You can check the deletion date in the response to verify if the authorization was deleted by the request. + // + // Required permissions: + // - "user.grant.delete" + rpc DeleteAuthorization(DeleteAuthorizationRequest) returns (DeleteAuthorizationResponse) { + option (google.api.http) = { + delete: "/v2beta/authorizations/{id}" + }; + + 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: "The authorization was deleted successfully."; + }; + }; + responses: { + key: "404"; + value: { + description: "Authorization not found."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + }; + }; + }; + } + + // Activate Authorization + // + // ActivateAuthorization activates an existing but inactive authorization. + // + // In case the authorization is already active, the request will return a successful response as + // the desired state is already achieved. + // You can check the change date in the response to verify if the authorization was activated by the request. + // + // Required permissions: + // - "user.grant.write" + rpc ActivateAuthorization(ActivateAuthorizationRequest) returns (ActivateAuthorizationResponse) { + option (google.api.http) = { + post: "/v2beta/authorizations/{id}/activate" + 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: "The authorization was activated successfully."; + }; + }; + responses: { + key: "404"; + value: { + description: "Authorization not found."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + }; + }; + }; + } + + // Deactivate Authorization + // + // DeactivateAuthorization deactivates an existing and active authorization. + // + // In case the authorization is already inactive, the request will return a successful response as + // the desired state is already achieved. + // You can check the change date in the response to verify if the authorization was deactivated by the request. + // + // Required permissions: + // - "user.grant.write" + rpc DeactivateAuthorization(DeactivateAuthorizationRequest) returns (DeactivateAuthorizationResponse) { + option (google.api.http) = { + post: "/v2beta/authorizations/{id}/deactivate" + 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: "The authorization was deactivated successfully."; + }; + }; + responses: { + key: "404"; + value: { + description: "Authorization not found."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + }; + }; + }; + } +} + +message ListAuthorizationsRequest { + // Paginate through the results using a limit, offset and sorting. + 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 AuthorizationFieldName sorting_column = 2 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"AUTHORIZATION_FIELD_NAME_CREATED_DATE\"" + } + ]; + // Define the criteria to query for. + repeated AuthorizationsSearchFilter filters = 3; +} + +message ListAuthorizationsResponse { + // Details contains the pagination information. + zitadel.filter.v2beta.PaginationResponse pagination = 1; + repeated Authorization authorizations = 2; +} + +message CreateAuthorizationRequest { + // UserID is the ID of the user who should be granted the authorization. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // Project ID is the ID of the project the user should be authorized for. + string project_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // OrganizationID is the ID of the organization on which the authorization should be created. + // The organization must either own the project or have a grant for the project. + // If omitted, the authorization is created on the projects organization. + optional string organization_id = 3 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // RoleKeys are the keys of the roles the user should be granted. + repeated string role_keys = 4 [ + (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: "[\"user\",\"admin\"]"; + } + ]; +} + +message CreateAuthorizationResponse { + // ID is the unique identifier of the newly created authorization. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // CreationDate is the timestamp when the authorization was created. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message UpdateAuthorizationRequest { + // ID is the unique identifier of the authorization. + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // RoleKeys are the keys of the roles the user should be granted. + // Note that any role keys previously granted to the user and not present in the list will be revoked. + repeated string role_keys = 2 [ + (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: "[\"user\",\"admin\"]"; + } + ]; +} + +message UpdateAuthorizationResponse { + // ChangeDate is the timestamp when the authorization was last updated. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message DeleteAuthorizationRequest { + // ID is the unique identifier of the authorization that should be deleted. + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; +} + +message DeleteAuthorizationResponse { + // DeletionDate is the timestamp when the authorization was deleted. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message ActivateAuthorizationRequest { + // ID is the unique identifier of the authorization that should be activated. + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; +} + +message ActivateAuthorizationResponse { + // ChangeDate is the last timestamp when the authorization was changed / activated. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message DeactivateAuthorizationRequest { + // ID is the unique identifier of the authorization that should be deactivated. + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; +} + +message DeactivateAuthorizationResponse { + // ChangeDate is the last timestamp when the authorization was changed / deactivated. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} diff --git a/proto/zitadel/filter/v2beta/filter.proto b/proto/zitadel/filter/v2beta/filter.proto index 2265fa4125..098808fed0 100644 --- a/proto/zitadel/filter/v2beta/filter.proto +++ b/proto/zitadel/filter/v2beta/filter.proto @@ -86,7 +86,18 @@ message TimestampFilter { 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/internal_permission/v2beta/internal_permission_service.proto b/proto/zitadel/internal_permission/v2beta/internal_permission_service.proto new file mode 100644 index 0000000000..3a27b89a4f --- /dev/null +++ b/proto/zitadel/internal_permission/v2beta/internal_permission_service.proto @@ -0,0 +1,384 @@ +syntax = "proto3"; + +package zitadel.internal_permission.v2beta; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +import "zitadel/protoc_gen_zitadel/v2/options.proto"; + +import "google/protobuf/timestamp.proto"; +import "zitadel/filter/v2beta/filter.proto"; +import "zitadel/internal_permission/v2beta/query.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta;internal_permission"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Internal Permission Service"; + version: "2.0-beta"; + description: "This API is intended to manage internal permissions in ZITADEL. This service is in beta state. It can AND will continue breaking until a stable version is released."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; + }; + }; + schemes: HTTPS; + schemes: HTTP; + + consumes: "application/json"; + consumes: "application/grpc"; + + produces: "application/json"; + produces: "application/grpc"; + + consumes: "application/grpc-web+proto"; + produces: "application/grpc-web+proto"; + + host: "$CUSTOM-DOMAIN"; + base_path: "/"; + + external_docs: { + description: "Detailed information about ZITADEL", + url: "https://zitadel.com/docs" + } + security_definitions: { + security: { + key: "OAuth2"; + value: { + type: TYPE_OAUTH2; + flow: FLOW_ACCESS_CODE; + authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; + token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; + scopes: { + scope: { + key: "openid"; + value: "openid"; + } + scope: { + key: "urn:zitadel:iam:org:project:id:zitadel:aud"; + value: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + } + } + security: { + security_requirement: { + key: "OAuth2"; + value: { + scope: "openid"; + scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "404"; + value: { + description: "Returned when the resource does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } +}; + + +// InternalPermissionService provides methods to manage permissions for resource +// and their management in ZITADEL itself. +// +// If you want to manage permissions and roles within your project or application, +// please use the AuthorizationsService. +service InternalPermissionService { + // ListAdministrators returns all administrators and its roles matching the request and necessary permissions. + // + // Required permissions depend on the resource type: + // - "iam.member.read" for instance administrators + // - "org.member.read" for organization administrators + // - "project.member.read" for project administrators + // - "project.grant.member.read" for project grant administrators + // - no permissions required for listing own administrator roles + rpc ListAdministrators(ListAdministratorsRequest) returns (ListAdministratorsResponse) { + option (google.api.http) = { + post: "/v2beta/administrators/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 administrators matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } + + // CreateAdministrator grants a administrator role to a user for a specific resource. + // + // Note that the roles are specific to the resource type. + // This means that if you want to grant a user the administrator role for an organization and a project, + // you need to create two administrator roles. + // + // Required permissions depend on the resource type: + // - "iam.member.write" for instance administrators + // - "org.member.write" for organization administrators + // - "project.member.write" for project administrators + // - "project.grant.member.write" for project grant administrators + rpc CreateAdministrator(CreateAdministratorRequest) returns (CreateAdministratorResponse) { + option (google.api.http) = { + post: "/v2beta/administrators" + 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: "Administrator created successfully"; + }; + }; + responses: { + key: "409" + value: { + description: "The administrator to create already exists."; + } + }; + }; + } + + // UpdateAdministrator updates the specific administrator role. + // + // Note that any role previously granted to the user and not present in the request will be revoked. + // + // Required permissions depend on the resource type: + // - "iam.member.write" for instance administrators + // - "org.member.write" for organization administrators + // - "project.member.write" for project administrators + // - "project.grant.member.write" for project grant administrators + rpc UpdateAdministrator(UpdateAdministratorRequest) returns (UpdateAdministratorResponse) { + option (google.api.http) = { + post: "/v2beta/administrators/{user_id}" + 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: "Administrator successfully updated or left unchanged"; + }; + }; + responses: { + key: "404" + value: { + description: "The administrator to update does not exist."; + } + }; + }; + } + + // DeleteAdministrator revokes a administrator role from a user. + // + // In case the administrator role is not found, the request will return a successful response as + // the desired state is already achieved. + // You can check the deletion date in the response to verify if the administrator role was deleted during the request. + // + // Required permissions depend on the resource type: + // - "iam.member.delete" for instance administrators + // - "org.member.delete" for organization administrators + // - "project.member.delete" for project administrators + // - "project.grant.member.delete" for project grant administrators + rpc DeleteAdministrator(DeleteAdministratorRequest) returns (DeleteAdministratorResponse) { + option (google.api.http) = { + delete: "/v2beta/administrators/{user_id}" + }; + + 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: "Administrator deleted successfully"; + }; + }; + }; + } +} + +message ListAdministratorsRequest { + // 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 AdministratorFieldName sorting_column = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"ADMINISTRATOR_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Filter the administrator roles to be returned. + repeated AdministratorSearchFilter filters = 3; +} + +message ListAdministratorsResponse { + zitadel.filter.v2beta.PaginationResponse pagination = 1; + repeated Administrator administrators = 2; +} + +message GetAdministratorRequest { + // ID is the unique identifier of the administrator. + string id = 1 [(validate.rules).string = { + min_len: 1 + max_len: 200 + }]; +} + +message GetAdministratorResponse { + Administrator administrator = 1; +} + +message CreateAdministratorRequest { + // UserID is the ID of the user who should be granted the administrator role. + string user_id = 1 [(validate.rules).string = { + min_len: 1 + max_len: 200 + }]; + // Resource is the type of the resource the administrator roles should be granted for. + ResourceType resource = 2; + + // Roles are the roles that should be granted to the user for the specified resource. + // Note that roles are currently specific to the resource type. + // This means that if you want to grant a user the administrator role for an organization and a project, + // you need to create two administrator roles. + repeated string roles = 3 [(validate.rules).repeated = { + unique: true + items: { + string: { + min_len: 1 + max_len: 200 + } + } + }]; +} + +message ResourceType { + message ProjectGrant { + // ProjectID is required to grant administrator privileges for a specific project. + string project_id = 1; + // ProjectGrantID is required to grant administrator privileges for a specific project grant. + string project_grant_id = 2; + } + + // Resource is the type of the resource the administrator roles should be granted for. + oneof resource { + option (validate.required) = true; + + // Instance is the resource type for granting administrator privileges on the instance level. + bool instance = 1 [(validate.rules).bool = {const: true}]; + // OrganizationID is required to grant administrator privileges for a specific organization. + string organization_id = 2; + // ProjectID is required to grant administrator privileges for a specific project. + string project_id = 3; + // ProjectGrantID is required to grant administrator privileges for a specific project grant. + ProjectGrant project_grant = 4; + } +} + +message CreateAdministratorResponse { + // CreationDate is the timestamp when the administrator role was created. + google.protobuf.Timestamp creation_date = 1; +} + +message UpdateAdministratorRequest { + // UserID is the ID of the user who should have his administrator roles update. + string user_id = 1 [(validate.rules).string = { + min_len: 1 + max_len: 200 + }]; + // Resource is the type of the resource the administrator roles should be granted for. + ResourceType resource = 2; + + // Roles are the roles that the user should be granted. + // Note that any role previously granted to the user and not present in the list will be revoked. + repeated string roles = 3 [(validate.rules).repeated = { + unique: true + items: { + string: { + min_len: 1 + max_len: 200 + } + } + }]; +} + +message UpdateAdministratorResponse { + // ChangeDate is the timestamp when the administrator role was last updated. + google.protobuf.Timestamp change_date = 1; +} + +message DeleteAdministratorRequest { + // UserID is the ID of the user who should have his administrator roles removed. + string user_id = 1 [(validate.rules).string = { + min_len: 1 + max_len: 200 + }]; + // Resource is the type of the resource the administrator roles should be removed for. + ResourceType resource = 2; +} + +message DeleteAdministratorResponse { + // DeletionDate is the timestamp when the administrator role was deleted. + // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request. + // In case the deletion occurred in a previous request, the deletion date might not be set. + google.protobuf.Timestamp deletion_date = 1; +} \ No newline at end of file diff --git a/proto/zitadel/internal_permission/v2beta/query.proto b/proto/zitadel/internal_permission/v2beta/query.proto new file mode 100644 index 0000000000..b23183cd50 --- /dev/null +++ b/proto/zitadel/internal_permission/v2beta/query.proto @@ -0,0 +1,166 @@ +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; +import "zitadel/filter/v2beta/filter.proto"; + +package zitadel.internal_permission.v2beta; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta;internal_permission"; + +message Administrator { + // CreationDate is the timestamp when the administrator role was granted. + google.protobuf.Timestamp creation_date = 1; + // ChangeDate is the timestamp when the administrator role was last updated. + // In case the administrator role was not updated, this field is equal to the creation date. + google.protobuf.Timestamp change_date = 2; + // User is the user who was granted the administrator role. + User user = 3; + // Resource is the type of the resource the administrator roles were granted for. + oneof resource { + // Instance is returned if the administrator roles were granted on the instance level. + bool instance = 4; + // Organization provides information about the organization the administrator roles were granted for. + Organization organization = 5; + // Project provides information about the project the administrator roles were granted for. + Project project = 6; + // ProjectGrant provides information about the project grant the administrator roles were granted for. + ProjectGrant project_grant = 7; + } + // Roles are the roles that were granted to the user for the specified resource. + repeated string roles = 8; +} + +message User { + // ID is the unique identifier of the user. + string id = 1; + // PreferredLoginName is the preferred login name of the user. This value is unique across the whole instance. + string preferred_login_name = 2; + // DisplayName is the public display name of the user. + // By default it's the user's given name and family name, their username or their email address. + string display_name = 3; + // The organization the user belong to. + string organization_id = 4; +} + +message Organization { + // ID is the unique identifier of the organization the user was granted the administrator role for. + string id = 1; + // Name is the name of the organization the user was granted the administrator role for. + string name = 2; +} +message Project { + // ID is the unique identifier of the project the user was granted the administrator role for. + string id = 1; + // Name is the name of the project the user was granted the administrator role for. + string name = 2; + // OrganizationID is the ID of the organization the project belongs to. + string organization_id = 3; +} +message ProjectGrant { + // ID is the unique identifier of the project grant the user was granted the administrator role for. + string id = 1; + // ProjectID is the ID of the project the project grant belongs to. + string project_id = 2; + // ProjectName is the name of the project the project grant belongs to. + string project_name = 3; + // OrganizationID is the ID of the organization the project grant belongs to. + string organization_id = 4; + // OrganizationID is the ID of the organization the project grant belongs to. + string granted_organization_id = 5; +} + +message AdministratorSearchFilter{ + oneof filter { + option (validate.required) = true; + // Search for administrator roles by their creation date. + zitadel.filter.v2beta.TimestampFilter creation_date = 1; + // Search for administrator roles by their change date. + zitadel.filter.v2beta.TimestampFilter change_date = 2; + // Search for administrators roles by the IDs of the users who was granted the administrator role. + zitadel.filter.v2beta.InIDsFilter in_user_ids_filter = 3; + // Search for administrators roles by the ID of the organization the user is part of. + zitadel.filter.v2beta.IDFilter user_organization_id = 4; + // Search for administrators roles by the preferred login name of the user. + UserPreferredLoginNameFilter user_preferred_login_name = 5; + // Search for administrators roles by the display name of the user. + UserDisplayNameFilter user_display_name = 6; + // Search for administrators roles granted for a specific resource. + ResourceFilter resource = 7; + // Search for administrators roles granted with a specific role. + RoleFilter role = 8; + + // Combine multiple authorization queries with an AND operation. + AndFilter and = 9; + // Combine multiple authorization queries with an OR operation. + // For example, to search for authorizations of multiple OrganizationIDs. + OrFilter or = 10; + // Negate an authorization query. + NotFilter not = 11; + } +} + +message UserPreferredLoginNameFilter { + // Search for administrators by the preferred login name of the user. + string preferred_login_name = 1 [(validate.rules).string = { + min_len: 1 + max_len: 200 + }]; + // Specify the method to search for the preferred login name. Default is EQUAL. + // For example, to search for all administrator roles of a user with a preferred login name + // containing a specific string, use CONTAINS or CONTAINS_IGNORE_CASE. + zitadel.filter.v2beta.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true]; +} + +message UserDisplayNameFilter { + // Search for administrators by the display name of the user. + string display_name = 1 [(validate.rules).string = { + min_len: 1 + max_len: 200 + }]; + // Specify the method to search for the display name. Default is EQUAL. + // For example, to search for all administrator roles of a user with a display name + // containing a specific string, use CONTAINS or CONTAINS_IGNORE_CASE. + zitadel.filter.v2beta.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true]; +} + +message ResourceFilter { + // Search for administrators by the granted resource. + oneof resource { + // Search for administrators granted on the instance level. + bool instance = 1; + // Search for administrators granted on a specific organization. + string organization_id = 2; + // Search for administrators granted on a specific project. + string project_id = 3; + // Search for administrators granted on a specific project grant. + string project_grant_id = 4; + } +} + +message RoleFilter { + // Search for administrators by the granted role. + string role_key = 1 [(validate.rules).string = { + min_len: 1 + max_len: 200 + }]; +} + +message AndFilter { + repeated AdministratorSearchFilter queries = 1; +} + +message OrFilter { + repeated AdministratorSearchFilter queries = 1; +} + +message NotFilter { + AdministratorSearchFilter query = 1; +} + +enum AdministratorFieldName { + ADMINISTRATOR_FIELD_NAME_UNSPECIFIED = 0; + ADMINISTRATOR_FIELD_NAME_USER_ID = 1; + ADMINISTRATOR_FIELD_NAME_CREATION_DATE = 2; + ADMINISTRATOR_FIELD_NAME_CHANGE_DATE = 3; +} diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index bb62e2eba6..09095e3b78 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -4177,6 +4177,11 @@ service ManagementService { }; } + // Get User Grant By ID + // + // Deprecated: [List authorizations](apis/resources/authorization_service_v2/zitadel-authorization-v-2-beta-authorization-service-list-authorizations.api.mdx) and filter by its ID. + // + // Returns a user grant per ID. A user grant is a role a user has for a specific project and organization. rpc GetUserGrantByID(GetUserGrantByIDRequest) returns (GetUserGrantByIDResponse) { option (google.api.http) = { get: "/users/{user_id}/grants/{grant_id}" @@ -4188,8 +4193,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "User Grants"; - summary: "User Grant By ID"; - description: "Returns a user grant per ID. A user grant is a role a user has for a specific project and organization." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -4201,6 +4205,11 @@ service ManagementService { }; } + // Search User Grants + // + // Deprecated: [List authorizations](apis/resources/authorization_service_v2/zitadel-authorization-v-2-beta-authorization-service-list-authorizations.api.mdx) and pass the user ID filter to search for a users grants on owned or granted projects. + // + // Returns a list of user grants that match the search queries. User grants are the roles users have for a specific project and organization. rpc ListUserGrants(ListUserGrantRequest) returns (ListUserGrantResponse) { option (google.api.http) = { post: "/users/grants/_search" @@ -4213,8 +4222,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "User Grants"; - summary: "Search User Grants"; - description: "Returns a list of user grants that match the search queries. User grants are the roles users have for a specific project and organization." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -4226,6 +4234,12 @@ service ManagementService { }; } + + // Add User Grant + // + // Deprecated: [Add an authorization](apis/resources/authorization_service_v2/zitadel-authorization-v-2-beta-authorization-service-create-authorization.api.mdx) to grant a user access to an owned or granted project. + // + // Add a user grant for a specific user. User grants are the roles users have for a specific project and organization. rpc AddUserGrant(AddUserGrantRequest) returns (AddUserGrantResponse) { option (google.api.http) = { post: "/users/{user_id}/grants" @@ -4238,8 +4252,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "User Grants"; - summary: "Add User Grant"; - description: "Add a user grant for a specific user. User grants are the roles users have for a specific project and organization." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -4251,6 +4264,11 @@ service ManagementService { }; } + // Update User Grant + // + // Deprecated: [Update an authorization](apis/resources/authorization_service_v2/zitadel-authorization-v-2-beta-authorization-service-update-authorization.api.mdx) to update a user's roles on an owned or granted project. + // + // Update the roles of a user grant. User grants are the roles users have for a specific project and organization. rpc UpdateUserGrant(UpdateUserGrantRequest) returns (UpdateUserGrantResponse) { option (google.api.http) = { put: "/users/{user_id}/grants/{grant_id}" @@ -4263,8 +4281,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "User Grants"; - summary: "Update User Grants"; - description: "Update the roles of a user grant. User grants are the roles users have for a specific project and organization." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -4276,6 +4293,11 @@ service ManagementService { }; } + // Deactivate User Grant + // + // Deprecated: [Deactivate an authorization](apis/resources/authorization_service_v2/zitadel-authorization-v-2-beta-authorization-service-deactivate-authorization.api.mdx) to disable a user's access to an owned or granted project. + // + // Deactivate the user grant. The user will not be able to use the granted project anymore. Also, the roles will not be included in the tokens when requested. An error will be returned if the user grant is already deactivated. rpc DeactivateUserGrant(DeactivateUserGrantRequest) returns (DeactivateUserGrantResponse) { option (google.api.http) = { post: "/users/{user_id}/grants/{grant_id}/_deactivate" @@ -4288,8 +4310,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "User Grants"; - summary: "Deactivate User Grant"; - description: "Deactivate the user grant. The user will not be able to use the granted project anymore. Also, the roles will not be included in the tokens when requested. An error will be returned if the user grant is already deactivated." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -4301,6 +4322,11 @@ service ManagementService { }; } + // Reactivate User Grant + // + // Deprecated: [Activate an authorization](apis/resources/authorization_service_v2/zitadel-authorization-v-2-beta-authorization-service-activate-authorization.api.mdx) to enable a user's access to an owned or granted project. + // + // Reactivate a deactivated user grant. The user will be able to use the granted project again. An error will be returned if the user grant is not deactivated. rpc ReactivateUserGrant(ReactivateUserGrantRequest) returns (ReactivateUserGrantResponse) { option (google.api.http) = { post: "/users/{user_id}/grants/{grant_id}/_reactivate" @@ -4313,8 +4339,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "User Grants"; - summary: "Reactivate User Grant"; - description: "Reactivate a deactivated user grant. The user will be able to use the granted project again. An error will be returned if the user grant is not deactivated." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -4326,6 +4351,11 @@ service ManagementService { }; } + // Remove User Grant + // + // Deprecated: [Delete an authorization](apis/resources/authorization_service_v2/zitadel-authorization-v-2-beta-authorization-service-delete-authorization.api.mdx) to remove a users access to an owned or granted project. + // + // Removes the user grant from the user. The user will not be able to use the granted project anymore. Also, the roles will not be included in the tokens when requested. rpc RemoveUserGrant(RemoveUserGrantRequest) returns (RemoveUserGrantResponse) { option (google.api.http) = { delete: "/users/{user_id}/grants/{grant_id}" @@ -4337,8 +4367,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "User Grants"; - summary: "Remove User Grant"; - description: "Removes the user grant from the user. The user will not be able to use the granted project anymore. Also, the roles will not be included in the tokens when requested." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -4350,6 +4379,11 @@ service ManagementService { }; } + // Bulk Remove User Grants + // + // Deprecated: [Delete authorizations one after the other](apis/resources/authorization_service_v2/zitadel-authorization-v-2-beta-authorization-service-delete-authorization.api.mdx) to remove access for multiple users on multiple owned or granted projects. + // + // Remove a list of user grants. The users will not be able to use the granted project anymore. Also, the roles will not be included in the tokens when requested. rpc BulkRemoveUserGrant(BulkRemoveUserGrantRequest) returns (BulkRemoveUserGrantResponse) { option (google.api.http) = { delete: "/user_grants/_bulk" @@ -4362,8 +4396,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "User Grants"; - summary: "Bulk Remove User Grants"; - description: "Remove a list of user grants. The users will not be able to use the granted project anymore. Also, the roles will not be included in the tokens when requested." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; diff --git a/proto/zitadel/metadata/v2/metadata.proto b/proto/zitadel/metadata/v2/metadata.proto new file mode 100644 index 0000000000..c04548ba4e --- /dev/null +++ b/proto/zitadel/metadata/v2/metadata.proto @@ -0,0 +1,57 @@ +syntax = "proto3"; + +import "zitadel/filter/v2/filter.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +package zitadel.metadata.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/metadata/v2"; + +message Metadata { + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + google.protobuf.Timestamp change_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + string key = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "metadata key", + example: "\"key1\""; + } + ]; + bytes value = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "metadata value is base64 encoded, make sure to decode to get the value", + example: "\"VGhpcyBpcyBteSBmaXJzdCB2YWx1ZQ==\""; + } + ]; +} + +message MetadataSearchFilter { + oneof filter { + option (validate.required) = true; + MetadataKeyFilter key_filter = 1; + } +} + +message MetadataKeyFilter { + string key = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"key\"" + } + ]; + zitadel.filter.v2.TextFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines which text equality method is used"; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/project/v2beta/project_service.proto b/proto/zitadel/project/v2beta/project_service.proto index cb7110bc91..66f221b911 100644 --- a/proto/zitadel/project/v2beta/project_service.proto +++ b/proto/zitadel/project/v2beta/project_service.proto @@ -451,7 +451,7 @@ service ProjectService { // - `project.role.read` rpc ListProjectRoles (ListProjectRolesRequest) returns (ListProjectRolesResponse) { option (google.api.http) = { - delete: "/v2beta/projects/{project_id}/roles/search" + post: "/v2beta/projects/{project_id}/roles/search" }; option (zitadel.protoc_gen_zitadel.v2.options) = { diff --git a/proto/zitadel/user/v2/user_service.proto b/proto/zitadel/user/v2/user_service.proto index 349f3c6c54..7ed12f0143 100644 --- a/proto/zitadel/user/v2/user_service.proto +++ b/proto/zitadel/user/v2/user_service.proto @@ -22,6 +22,7 @@ import "zitadel/user/v2/key.proto"; import "zitadel/user/v2/pat.proto"; import "zitadel/user/v2/query.proto"; import "zitadel/filter/v2/filter.proto"; +import "zitadel/metadata/v2/metadata.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; @@ -1793,6 +1794,84 @@ service UserService { } }; } + // Set User Metadata + // + // Sets a list of key value pairs. Existing metadata entries with matching keys are overwritten. Existing metadata entries without matching keys are untouched. To remove metadata entries, use [DeleteUserMetadata](apis/resources/user_service_v2/user-service-delete-user-metadata.api.mdx). For HTTP requests, make sure the bytes array value is base64 encoded. + // + // Required permission: + // - `user.write` + rpc SetUserMetadata(SetUserMetadataRequest) returns (SetUserMetadataResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/metadata" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + responses: { + key: "400" + value: { + description: "User not found"; + } + }; + }; + } + + // List User Metadata + // + // List metadata of an user filtered by query. + // + // Required permission: + // - `user.read` + rpc ListUserMetadata(ListUserMetadataRequest) returns (ListUserMetadataResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/metadata/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = {auth_option: { + permission: "user.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + } + + // Delete User Metadata + // + // Delete metadata objects from an user with a specific key. + // + // Required permission: + // - `user.write` + rpc DeleteUserMetadata(DeleteUserMetadataRequest) returns (DeleteUserMetadataResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}/metadata" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + } } message AddHumanUserRequest{ @@ -1890,6 +1969,13 @@ message CreateUserRequest{ example: "\"TJOPWSDYILLHXFV4MLKNNJOWFG7VSDCK\""; } ]; + + // Metadata to bet set. The values have to be base64 encoded. + repeated Metadata metadata = 9 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[{\"key\": \"test1\", \"value\": \"VGhpcyBpcyBteSBmaXJzdCB2YWx1ZQ==\"}, {\"key\": \"test2\", \"value\": \"VGhpcyBpcyBteSBzZWNvbmQgdmFsdWU=\"}]" + } + ]; } message Machine { // The machine users name is a human readable field that helps identifying the user. @@ -3456,3 +3542,79 @@ message ListPersonalAccessTokensResponse { zitadel.filter.v2.PaginationResponse pagination = 1; repeated PersonalAccessToken result = 2; } + +message Metadata { + // Key in the metadata key/value pair. + string key = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + // Value in the metadata key/value pair. + bytes value = 2 [(validate.rules).bytes = {min_len: 1, max_len: 500000}]; +} + +message SetUserMetadataRequest{ + // ID of the user under which the metadata gets set. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + // Metadata to bet set. The values have to be base64 encoded. + repeated Metadata metadata = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[{\"key\": \"test1\", \"value\": \"VGhpcyBpcyBteSBmaXJzdCB2YWx1ZQ==\"}, {\"key\": \"test2\", \"value\": \"VGhpcyBpcyBteSBzZWNvbmQgdmFsdWU=\"}]" + } + ]; +} + +message SetUserMetadataResponse{ + // The timestamp of the update of the user metadata. + google.protobuf.Timestamp set_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListUserMetadataRequest { + // ID of the user under which the metadata is to be listed. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + + // List limitations and ordering. + optional zitadel.filter.v2.PaginationRequest pagination = 2; + // Define the criteria to query for. + repeated zitadel.metadata.v2.MetadataSearchFilter filters = 3; +} + +message ListUserMetadataResponse { + // Pagination of the users metadata results. + zitadel.filter.v2.PaginationResponse pagination = 1; + // The user metadata requested. + repeated zitadel.metadata.v2.Metadata metadata = 2; +} + +message DeleteUserMetadataRequest { + // ID of the user which metadata is to be deleted is stored on. + string user_id = 1; + // The keys for the user metadata to be deleted. + repeated string keys = 2 [(validate.rules).repeated.items.string = {min_len: 1, max_len: 200}]; +} + +message DeleteUserMetadataResponse{ + // The timestamp of the deletion of the user metadata. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} \ No newline at end of file