diff --git a/cmd/start/start.go b/cmd/start/start.go index 02efdfaeba1..d886d31995e 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -41,6 +41,7 @@ import ( app_v2beta "github.com/zitadel/zitadel/internal/api/grpc/app/v2beta" application "github.com/zitadel/zitadel/internal/api/grpc/application/v2" "github.com/zitadel/zitadel/internal/api/grpc/auth" + authorization_v2 "github.com/zitadel/zitadel/internal/api/grpc/authorization/v2" 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" @@ -558,6 +559,9 @@ func startAPIs( if err := apis.RegisterService(ctx, authorization_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { return nil, err } + if err := apis.RegisterService(ctx, authorization_v2.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { + return nil, err + } if err := apis.RegisterService(ctx, app_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil { return nil, err } diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index fbe5f4c34d3..61b87b648d4 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -419,7 +419,7 @@ module.exports = { }, authorization_v2: { specPath: - ".artifacts/openapi3/zitadel/authorization/v2beta/authorization_service.openapi.yaml", + ".artifacts/openapi3/zitadel/authorization/v2/authorization_service.openapi.yaml", outputDir: "docs/apis/resources/authorization_service_v2", sidebarOptions: { groupPathsBy: "tag", diff --git a/docs/sidebars.js b/docs/sidebars.js index 9022f96109d..6b39fb62290 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -871,18 +871,16 @@ module.exports = { }, { type: "category", - label: "Authorizations (Beta)", + label: "Authorizations", link: { type: "generated-index", - title: "Authorization Service API (Beta)", + title: "Authorization Service API", 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" + "users, etc., please use the InternalPermissionService." }, items: sidebar_api_authorization_service_v2, }, diff --git a/internal/api/grpc/authorization/v2/authorization.go b/internal/api/grpc/authorization/v2/authorization.go new file mode 100644 index 00000000000..7b38f751800 --- /dev/null +++ b/internal/api/grpc/authorization/v2/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" + "github.com/zitadel/zitadel/pkg/grpc/authorization/v2" +) + +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/v2/integration_test/authorization_test.go b/internal/api/grpc/authorization/v2/integration_test/authorization_test.go new file mode 100644 index 00000000000..af47bf1efa4 --- /dev/null +++ b/internal/api/grpc/authorization/v2/integration_test/authorization_test.go @@ -0,0 +1,1034 @@ +//go:build integration + +package authorization_test + +import ( + "context" + "testing" + "time" + + "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" + "github.com/zitadel/zitadel/pkg/grpc/authorization/v2" + 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, integration.OrganizationName(), integration.Email()).OrganizationId + request.OrganizationId = selfOrgId + request.ProjectId = Instance.CreateProject(IAMCTX, t, selfOrgId, integration.ProjectName(), false, false).Id + request.RoleKeys = []string{integration.RoleKey()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], integration.RoleDisplayName(), "") + 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, error", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()).OrganizationId + request.ProjectId = Instance.CreateProject(IAMCTX, t, selfOrgId, integration.ProjectName(), false, false).Id + request.RoleKeys = []string{integration.RoleKey()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], integration.RoleDisplayName(), "") + 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 owned, ORG_OWNER, ok", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()).OrganizationId + request.OrganizationId = selfOrgId + request.ProjectId = Instance.CreateProject(IAMCTX, t, selfOrgId, integration.ProjectName(), false, false).Id + request.RoleKeys = []string{integration.RoleKey()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], integration.RoleDisplayName(), "") + 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, integration.OrganizationName(), integration.Email()).OrganizationId + request.OrganizationId = selfOrgId + request.ProjectId = Instance.CreateProject(IAMCTX, t, selfOrgId, integration.ProjectName(), false, false).Id + request.RoleKeys = []string{integration.RoleKey()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], integration.RoleDisplayName(), "") + 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, integration.OrganizationName(), integration.Email()).OrganizationId + request.OrganizationId = selfOrgId + request.ProjectId = Instance.CreateProject(IAMCTX, t, selfOrgId, integration.ProjectName(), false, false).Id + request.RoleKeys = []string{integration.RoleKey()} + 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, integration.OrganizationName(), integration.Email()).OrganizationId + request.OrganizationId = selfOrgId + request.ProjectId = "notexists" + 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, integration.OrganizationName(), integration.Email()).OrganizationId + request.OrganizationId = "notexists" + request.ProjectId = Instance.CreateProject(IAMCTX, t, selfOrgId, integration.ProjectName(), 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, integration.OrganizationName(), integration.Email()).OrganizationId + request.OrganizationId = selfOrgId + foreignOrg := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()) + request.ProjectId = Instance.CreateProject(IAMCTX, t, foreignOrg.OrganizationId, integration.ProjectName(), false, false).Id + request.RoleKeys = []string{integration.RoleKey()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], integration.RoleDisplayName(), "") + 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, integration.OrganizationName(), integration.Email()).OrganizationId + request.OrganizationId = selfOrgId + foreignOrg := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()) + request.ProjectId = Instance.CreateProject(IAMCTX, t, foreignOrg.OrganizationId, integration.ProjectName(), false, false).Id + request.RoleKeys = []string{integration.RoleKey()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], integration.RoleDisplayName(), "") + 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, integration.OrganizationName(), integration.Email()).OrganizationId + request.OrganizationId = selfOrgId + foreignOrg := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()) + projectID := Instance.CreateProject(IAMCTX, t, foreignOrg.OrganizationId, integration.ProjectName(), false, false).Id + request.ProjectId = projectID + request.RoleKeys = []string{integration.RoleKey()} + Instance.AddProjectRole(IAMCTX, t, projectID, request.RoleKeys[0], integration.RoleDisplayName(), "") + 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, integration.OrganizationName(), integration.Email()).OrganizationId + request.OrganizationId = selfOrgId + foreignOrg := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()) + request.ProjectId = Instance.CreateProject(IAMCTX, t, foreignOrg.OrganizationId, integration.ProjectName(), false, false).Id + request.RoleKeys = []string{integration.RoleKey()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], integration.RoleDisplayName(), "") + 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.AuthorizationV2.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, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + OrganizationId: selfOrgId, + }) + 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, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + OrganizationId: selfOrgId, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + request.RoleKeys = []string{projectRole1, projectRole2, "rolenotfound"} + 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, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + OrganizationId: selfOrgId, + }) + 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, integration.OrganizationName(), integration.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + projectRole3 := integration.RoleKey() + 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.AuthorizationV2.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, integration.OrganizationName(), integration.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + projectRole3 := integration.RoleKey() + 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.AuthorizationV2.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, integration.OrganizationName(), integration.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + projectRole3 := integration.RoleKey() + 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.AuthorizationV2.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.AuthorizationV2.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, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + OrganizationId: selfOrgId, + }) + 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, integration.OrganizationName(), integration.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + OrganizationId: foreignOrgId, + }) + 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, integration.OrganizationName(), integration.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + projectRole3 := integration.RoleKey() + 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.AuthorizationV2.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, integration.OrganizationName(), integration.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + projectRole3 := integration.RoleKey() + 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.AuthorizationV2.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, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + OrganizationId: selfOrgId, + }) + require.NoError(t, err) + _, err = Instance.Client.AuthorizationV2.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.AuthorizationV2.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, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + OrganizationId: selfOrgId, + }) + 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, integration.OrganizationName(), integration.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + OrganizationId: foreignOrgId, + }) + 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, integration.OrganizationName(), integration.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + projectRole3 := integration.RoleKey() + 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.AuthorizationV2.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, integration.OrganizationName(), integration.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + projectRole3 := integration.RoleKey() + 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.AuthorizationV2.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, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + OrganizationId: selfOrgId, + }) + require.NoError(t, err) + _, err = Instance.Client.AuthorizationV2.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.AuthorizationV2.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, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + OrganizationId: selfOrgId, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectId, callingUser.Id) + _, err = Instance.Client.AuthorizationV2.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, integration.OrganizationName(), integration.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + OrganizationId: foreignOrgId, + }) + 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.AuthorizationV2.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, integration.OrganizationName(), integration.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + projectRole3 := integration.RoleKey() + 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.AuthorizationV2.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.AuthorizationV2.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, integration.OrganizationName(), integration.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + projectRole3 := integration.RoleKey() + 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.AuthorizationV2.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.AuthorizationV2.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, integration.OrganizationName(), integration.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, integration.ProjectName(), false, false).Id + projectRole1 := integration.RoleKey() + projectRole2 := integration.RoleKey() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + OrganizationId: selfOrgId, + }) + 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.AuthorizationV2.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, integration.OrganizationName(), integration.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.AuthorizationV2.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/v2/integration_test/query_test.go b/internal/api/grpc/authorization/v2/integration_test/query_test.go new file mode 100644 index 00000000000..913ab531c2e --- /dev/null +++ b/internal/api/grpc/authorization/v2/integration_test/query_test.go @@ -0,0 +1,1101 @@ +//go:build integration + +package authorization_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/authorization/v2" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func TestServer_ListAuthorizations(t *testing.T) { + iamOwnerCtx := InstanceQuery.WithAuthorizationToken(EmptyCTX, integration.UserTypeIAMOwner) + + organizationName := integration.OrganizationName() + organization := InstanceQuery.CreateOrganization(iamOwnerCtx, organizationName, integration.Email()) + + projectName1 := integration.ProjectName() + project1 := InstanceQuery.CreateProject(iamOwnerCtx, t, organization.GetOrganizationId(), projectName1, false, false) + + projectName2 := integration.ProjectName() + project2 := InstanceQuery.CreateProject(iamOwnerCtx, t, organization.GetOrganizationId(), projectName2, false, false) + + roles := []*authorization.Role{ + {Key: "role1", DisplayName: "Role 1", Group: "group1"}, + {Key: "role2", DisplayName: "Role 2", Group: "group2"}, + {Key: "role3", DisplayName: "Role 3", Group: "group2"}} + for _, role := range roles { + InstanceQuery.AddProjectRole(iamOwnerCtx, t, project1.GetId(), role.Key, role.DisplayName, role.Group) + InstanceQuery.AddProjectRole(iamOwnerCtx, t, project2.GetId(), role.Key, role.DisplayName, role.Group) + } + + grantedOrganizationName := integration.OrganizationName() + grantedOrganization := InstanceQuery.CreateOrganization(iamOwnerCtx, grantedOrganizationName, integration.Email()) + + grantedRoles := roles[0:2] + grantedRoleKeys := make([]string, len(grantedRoles)) + for i, role := range grantedRoles { + grantedRoleKeys[i] = role.Key + } + + InstanceQuery.CreateProjectGrant(iamOwnerCtx, t, project2.GetId(), grantedOrganization.GetOrganizationId(), grantedRoleKeys...) + + user1 := InstanceQuery.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + user2 := InstanceQuery.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + user3 := InstanceQuery.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + + authorizationUser1Project1 := createAuthorizationForProject(iamOwnerCtx, InstanceQuery, t, organization.GetOrganizationId(), organizationName, user1.GetId(), project1.GetId(), projectName1, roles[0:1]) + authorizationUser2Project1 := createAuthorizationForProject(iamOwnerCtx, InstanceQuery, t, organization.GetOrganizationId(), organizationName, user2.GetId(), project1.GetId(), projectName1, roles[0:1]) + authorizationUser2Project2 := createAuthorizationForProject(iamOwnerCtx, InstanceQuery, t, organization.GetOrganizationId(), organizationName, user2.GetId(), project2.GetId(), projectName2, roles[1:2]) + authorizationUser3Project2 := createAuthorizationForProject(iamOwnerCtx, InstanceQuery, t, organization.GetOrganizationId(), organizationName, user3.GetId(), project2.GetId(), projectName2, roles[2:3]) + authorizationUser3ProjectGrant := createAuthorizationForProjectGrant(iamOwnerCtx, InstanceQuery, t, organization.GetOrganizationId(), user3.GetId(), project2.GetId(), projectName2, grantedOrganization.GetOrganizationId(), grantedOrganizationName, grantedRoles) + + organizationOwnerResp := InstanceQuery.CreateMachineUser(iamOwnerCtx) + organizationOwnerPatResp := InstanceQuery.CreatePersonalAccessToken(iamOwnerCtx, organizationOwnerResp.GetUserId()) + InstanceQuery.CreateOrgMembership(t, iamOwnerCtx, organization.GetOrganizationId(), organizationOwnerResp.GetUserId()) + organizationOwnerCtx := integration.WithAuthorizationToken(EmptyCTX, organizationOwnerPatResp.Token) + + projectOwnerResp := InstanceQuery.CreateMachineUser(iamOwnerCtx) + projectOwnerPatResp := InstanceQuery.CreatePersonalAccessToken(iamOwnerCtx, projectOwnerResp.GetUserId()) + InstanceQuery.CreateProjectMembership(t, iamOwnerCtx, project2.GetId(), projectOwnerResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(EmptyCTX, projectOwnerPatResp.Token) + + projectGrantOwnerResp := InstanceQuery.CreateMachineUser(iamOwnerCtx) + projectGrantOwnerPatResp := InstanceQuery.CreatePersonalAccessToken(iamOwnerCtx, projectGrantOwnerResp.GetUserId()) + InstanceQuery.CreateProjectGrantMembership(t, iamOwnerCtx, project2.GetId(), grantedOrganization.GetOrganizationId(), projectGrantOwnerResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(EmptyCTX, projectGrantOwnerPatResp.Token) + + type args struct { + ctx context.Context + req *authorization.ListAuthorizationsRequest + } + tests := []struct { + name string + args args + want *authorization.ListAuthorizationsResponse + wantErr bool + }{ + { + name: "unauthenticated", + args: args{ + ctx: EmptyCTX, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_InUserIds{ + InUserIds: &filter.InIDsFilter{ + Ids: []string{user1.GetId()}, + }, + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no permission", + args: args{ + ctx: InstanceQuery.WithAuthorizationToken(EmptyCTX, integration.UserTypeNoPermission), + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_InUserIds{ + InUserIds: &filter.InIDsFilter{ + Ids: []string{user1.GetId()}, + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: nil, + }, + }, + { + name: "not found", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_InUserIds{ + InUserIds: &filter.InIDsFilter{ + Ids: []string{"notexisting"}, + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "limit and pagination", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Pagination: &filter.PaginationRequest{ + Limit: 3, + Offset: 1, + Asc: true, + }, + SortingColumn: authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_CREATED_DATE, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 5, + AppliedLimit: 3, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser2Project1, + authorizationUser2Project2, + authorizationUser3Project2, + }, + }, + }, + { + name: "organization owner permission, only expect non granted", + args: args{ + ctx: organizationOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_InUserIds{ + InUserIds: &filter.InIDsFilter{ + Ids: []string{user3.GetId()}, + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, // count issue with permission check + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser3Project2, + }, + }, + }, + { + name: "project owner permission, only expect own project non granted", + args: args{ + ctx: projectOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_InUserIds{ + InUserIds: &filter.InIDsFilter{ + Ids: []string{user3.GetId()}, + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, // count issue with permission check + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser3Project2, + }, + }, + }, + { + name: "project grant owner permission, only expect granted", + args: args{ + ctx: projectGrantOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_InUserIds{ + InUserIds: &filter.InIDsFilter{ + Ids: []string{user3.GetId()}, + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, // count issue with permission check + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser3ProjectGrant, + }, + }, + }, + { + name: "filter by single authorizationID, expect one result", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_AuthorizationIds{ + AuthorizationIds: &filter.InIDsFilter{ + Ids: []string{authorizationUser1Project1.GetId()}, + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser1Project1, + }, + }, + }, + { + name: "filter by organization ID (expect only own / non granted)", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_OrganizationId{ + OrganizationId: &filter.IDFilter{ + Id: organization.GetOrganizationId(), + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser3Project2, + authorizationUser2Project2, + authorizationUser2Project1, + authorizationUser1Project1, + }, + }, + }, + { + name: "filter by organization ID (only project grant exists)", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_OrganizationId{ + OrganizationId: &filter.IDFilter{ + Id: grantedOrganization.GetOrganizationId(), + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser3ProjectGrant, + }, + }, + }, + { + name: "filter by userIDs", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_InUserIds{ + InUserIds: &filter.InIDsFilter{ + Ids: []string{user1.GetId(), user2.GetId()}, + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser2Project2, + authorizationUser2Project1, + authorizationUser1Project1, + }, + }, + }, + { + name: "list by userID (where project grant exists)", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_InUserIds{ + InUserIds: &filter.InIDsFilter{ + Ids: []string{user3.GetId()}, + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser3ProjectGrant, + authorizationUser3Project2, + }, + }, + }, + { + name: "filter by projectID (no project grant exists)", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_ProjectId{ + ProjectId: &filter.IDFilter{ + Id: project1.GetId(), + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser2Project1, + authorizationUser1Project1, + }, + }, + }, + { + name: "filter by projectID (project grant exists), expect own and granted", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_ProjectId{ + ProjectId: &filter.IDFilter{ + Id: project2.GetId(), + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser3ProjectGrant, + authorizationUser3Project2, + authorizationUser2Project2, + }, + }, + }, + { + name: "filter by project name (no project grant exists)", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_ProjectName{ + ProjectName: &authorization.ProjectNameQuery{ + Name: projectName1, + Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS, + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser2Project1, + authorizationUser1Project1, + }, + }, + }, + { + name: "filter role key, expect all with that role, own and granted", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_RoleKey{ + RoleKey: &authorization.RoleKeyQuery{ + Key: roles[0].Key, + Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS, + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser3ProjectGrant, + authorizationUser2Project1, + authorizationUser1Project1, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := InstanceQuery.Client.AuthorizationV2.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)) { + assert.EqualExportedValues(ttt, tt.want.Authorizations, got.Authorizations) + } + 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 createAuthorizationForProject(ctx context.Context, instance *integration.Instance, t *testing.T, organizationID, organizationName, userID, projectID, projectName string, roles []*authorization.Role) *authorization.Authorization { + userResp, err := instance.Client.UserV2.GetUserByID(ctx, &user.GetUserByIDRequest{UserId: userID}) + require.NoError(t, err) + + roleKeys := make([]string, len(roles)) + for i, role := range roles { + roleKeys[i] = role.Key + } + authResp := instance.CreateAuthorizationProject(t, ctx, projectID, userID, organizationID, roleKeys...) + return &authorization.Authorization{ + Id: authResp.GetId(), + CreationDate: authResp.GetCreationDate(), + ChangeDate: authResp.GetCreationDate(), + Project: &authorization.Project{ + Id: projectID, + Name: projectName, + OrganizationId: organizationID, + }, + Organization: &authorization.Organization{ + Id: organizationID, + Name: organizationName, + }, + 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(), + }, + State: 1, + Roles: roles, + } +} + +func createAuthorizationForProjectGrant(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, userID, projectID, projectName, grantedOrganizationID, grantedOrganizationName string, roles []*authorization.Role) *authorization.Authorization { + userResp, err := instance.Client.UserV2.GetUserByID(ctx, &user.GetUserByIDRequest{UserId: userID}) + require.NoError(t, err) + + roleKeys := make([]string, len(roles)) + for i, role := range roles { + roleKeys[i] = role.Key + } + authResp := instance.CreateAuthorizationProjectGrant(t, ctx, projectID, grantedOrganizationID, userID, roleKeys...) + return &authorization.Authorization{ + Id: authResp.GetId(), + CreationDate: authResp.GetCreationDate(), + ChangeDate: authResp.GetCreationDate(), + Project: &authorization.Project{ + Id: projectID, + Name: projectName, + OrganizationId: orgID, + }, + Organization: &authorization.Organization{ + Id: grantedOrganizationID, + Name: grantedOrganizationName, + }, + 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(), + }, + State: 1, + Roles: roles, + } +} + +func TestServer_ListAuthorizations_PermissionsV2(t *testing.T) { + ensureFeaturePermissionV2Enabled(t, InstancePermissionV2) + iamOwnerCtx := InstancePermissionV2.WithAuthorizationToken(EmptyCTX, integration.UserTypeIAMOwner) + + organizationName := integration.OrganizationName() + organization := InstancePermissionV2.CreateOrganization(iamOwnerCtx, organizationName, integration.Email()) + + projectName1 := integration.ProjectName() + project1 := InstancePermissionV2.CreateProject(iamOwnerCtx, t, organization.GetOrganizationId(), projectName1, false, false) + + projectName2 := integration.ProjectName() + project2 := InstancePermissionV2.CreateProject(iamOwnerCtx, t, organization.GetOrganizationId(), projectName2, false, false) + + roles := []*authorization.Role{ + {Key: "role1", DisplayName: "Role 1", Group: "group1"}, + {Key: "role2", DisplayName: "Role 2", Group: "group2"}, + {Key: "role3", DisplayName: "Role 3", Group: "group2"}} + for _, role := range roles { + InstancePermissionV2.AddProjectRole(iamOwnerCtx, t, project1.GetId(), role.Key, role.DisplayName, role.Group) + InstancePermissionV2.AddProjectRole(iamOwnerCtx, t, project2.GetId(), role.Key, role.DisplayName, role.Group) + } + + grantedOrganizationName := integration.OrganizationName() + grantedOrganization := InstancePermissionV2.CreateOrganization(iamOwnerCtx, grantedOrganizationName, integration.Email()) + + grantedRoles := roles[0:2] + grantedRoleKeys := make([]string, len(grantedRoles)) + for i, role := range grantedRoles { + grantedRoleKeys[i] = role.Key + } + + InstancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, project2.GetId(), grantedOrganization.GetOrganizationId(), grantedRoleKeys...) + + user1 := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + user2 := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + user3 := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + + authorizationUser1Project1 := createAuthorizationForProject(iamOwnerCtx, InstancePermissionV2, t, organization.GetOrganizationId(), organizationName, user1.GetId(), project1.GetId(), projectName1, roles[0:1]) + authorizationUser2Project1 := createAuthorizationForProject(iamOwnerCtx, InstancePermissionV2, t, organization.GetOrganizationId(), organizationName, user2.GetId(), project1.GetId(), projectName1, roles[0:1]) + authorizationUser2Project2 := createAuthorizationForProject(iamOwnerCtx, InstancePermissionV2, t, organization.GetOrganizationId(), organizationName, user2.GetId(), project2.GetId(), projectName2, roles[1:2]) + authorizationUser3Project2 := createAuthorizationForProject(iamOwnerCtx, InstancePermissionV2, t, organization.GetOrganizationId(), organizationName, user3.GetId(), project2.GetId(), projectName2, roles[2:3]) + authorizationUser3ProjectGrant := createAuthorizationForProjectGrant(iamOwnerCtx, InstancePermissionV2, t, organization.GetOrganizationId(), user3.GetId(), project2.GetId(), projectName2, grantedOrganization.GetOrganizationId(), grantedOrganizationName, grantedRoles) + + organizationOwnerResp := InstancePermissionV2.CreateMachineUser(iamOwnerCtx) + organizationOwnerPatResp := InstancePermissionV2.CreatePersonalAccessToken(iamOwnerCtx, organizationOwnerResp.GetUserId()) + InstancePermissionV2.CreateOrgMembership(t, iamOwnerCtx, organization.GetOrganizationId(), organizationOwnerResp.GetUserId()) + organizationOwnerCtx := integration.WithAuthorizationToken(EmptyCTX, organizationOwnerPatResp.Token) + + projectOwnerResp := InstancePermissionV2.CreateMachineUser(iamOwnerCtx) + projectOwnerPatResp := InstancePermissionV2.CreatePersonalAccessToken(iamOwnerCtx, projectOwnerResp.GetUserId()) + InstancePermissionV2.CreateProjectMembership(t, iamOwnerCtx, project2.GetId(), projectOwnerResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(EmptyCTX, projectOwnerPatResp.Token) + + projectGrantOwnerResp := InstancePermissionV2.CreateMachineUser(iamOwnerCtx) + projectGrantOwnerPatResp := InstancePermissionV2.CreatePersonalAccessToken(iamOwnerCtx, projectGrantOwnerResp.GetUserId()) + InstancePermissionV2.CreateProjectGrantMembership(t, iamOwnerCtx, project2.GetId(), grantedOrganization.GetOrganizationId(), projectGrantOwnerResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(EmptyCTX, projectGrantOwnerPatResp.Token) + + type args struct { + ctx context.Context + req *authorization.ListAuthorizationsRequest + } + tests := []struct { + name string + args args + want *authorization.ListAuthorizationsResponse + wantErr bool + }{ + { + name: "unauthenticated", + args: args{ + ctx: EmptyCTX, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_InUserIds{ + InUserIds: &filter.InIDsFilter{ + Ids: []string{user1.GetId()}, + }, + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no permission", + args: args{ + ctx: InstancePermissionV2.WithAuthorizationToken(EmptyCTX, integration.UserTypeNoPermission), + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_InUserIds{ + InUserIds: &filter.InIDsFilter{ + Ids: []string{user1.GetId()}, + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: nil, + }, + }, + { + name: "not found", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_InUserIds{ + InUserIds: &filter.InIDsFilter{ + Ids: []string{"notexisting"}, + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "limit and pagination", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Pagination: &filter.PaginationRequest{ + Limit: 3, + Offset: 1, + Asc: true, + }, + SortingColumn: authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_CREATED_DATE, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 5, + AppliedLimit: 3, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser2Project1, + authorizationUser2Project2, + authorizationUser3Project2, + }, + }, + }, + { + name: "organization owner permission, only expect non granted", + args: args{ + ctx: organizationOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_InUserIds{ + InUserIds: &filter.InIDsFilter{ + Ids: []string{user3.GetId()}, + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, // count issue with permission check + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser3Project2, + }, + }, + }, + { + name: "project owner permission, only expect own project non granted", + args: args{ + ctx: projectOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_InUserIds{ + InUserIds: &filter.InIDsFilter{ + Ids: []string{user3.GetId()}, + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, // count issue with permission check + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser3Project2, + }, + }, + }, + { + name: "project grant owner permission, only expect granted", + args: args{ + ctx: projectGrantOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_InUserIds{ + InUserIds: &filter.InIDsFilter{ + Ids: []string{user3.GetId()}, + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, // count issue with permission check + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser3ProjectGrant, + }, + }, + }, + { + name: "filter by single authorizationID, expect one result", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_AuthorizationIds{ + AuthorizationIds: &filter.InIDsFilter{ + Ids: []string{authorizationUser1Project1.GetId()}, + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser1Project1, + }, + }, + }, + { + name: "filter by organization ID (expect only own / non granted)", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_OrganizationId{ + OrganizationId: &filter.IDFilter{ + Id: organization.GetOrganizationId(), + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser3Project2, + authorizationUser2Project2, + authorizationUser2Project1, + authorizationUser1Project1, + }, + }, + }, + { + name: "filter by organization ID (only project grant exists)", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_OrganizationId{ + OrganizationId: &filter.IDFilter{ + Id: grantedOrganization.GetOrganizationId(), + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser3ProjectGrant, + }, + }, + }, + { + name: "filter by userIDs", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_InUserIds{ + InUserIds: &filter.InIDsFilter{ + Ids: []string{user1.GetId(), user2.GetId()}, + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser2Project2, + authorizationUser2Project1, + authorizationUser1Project1, + }, + }, + }, + { + name: "list by userID (where project grant exists)", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_InUserIds{ + InUserIds: &filter.InIDsFilter{ + Ids: []string{user3.GetId()}, + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser3ProjectGrant, + authorizationUser3Project2, + }, + }, + }, + { + name: "filter by projectID (no project grant exists)", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_ProjectId{ + ProjectId: &filter.IDFilter{ + Id: project1.GetId(), + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser2Project1, + authorizationUser1Project1, + }, + }, + }, + { + name: "filter by projectID (project grant exists), expect own and granted", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_ProjectId{ + ProjectId: &filter.IDFilter{ + Id: project2.GetId(), + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser3ProjectGrant, + authorizationUser3Project2, + authorizationUser2Project2, + }, + }, + }, + { + name: "filter by project name (no project grant exists)", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_ProjectName{ + ProjectName: &authorization.ProjectNameQuery{ + Name: projectName1, + Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS, + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser2Project1, + authorizationUser1Project1, + }, + }, + }, + { + name: "filter role key, expect all with that role, own and granted", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + { + Filter: &authorization.AuthorizationsSearchFilter_RoleKey{ + RoleKey: &authorization.RoleKeyQuery{ + Key: roles[0].Key, + Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS, + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + authorizationUser3ProjectGrant, + authorizationUser2Project1, + authorizationUser1Project1, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := InstancePermissionV2.Client.AuthorizationV2.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)) { + assert.EqualExportedValues(ttt, tt.want.Authorizations, got.Authorizations) + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} diff --git a/internal/api/grpc/authorization/v2/integration_test/server_test.go b/internal/api/grpc/authorization/v2/integration_test/server_test.go new file mode 100644 index 00000000000..ce91122da48 --- /dev/null +++ b/internal/api/grpc/authorization/v2/integration_test/server_test.go @@ -0,0 +1,67 @@ +//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 + InstanceQuery *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) + InstanceQuery = integration.NewInstance(ctx) // use a separate instance for queries to avoid side effects + 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/v2/query.go b/internal/api/grpc/authorization/v2/query.go new file mode 100644 index 00000000000..e4848441883 --- /dev/null +++ b/internal/api/grpc/authorization/v2/query.go @@ -0,0 +1,216 @@ +package authorization + +import ( + "context" + "errors" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/pkg/grpc/authorization/v2" + filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2" +) + +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_InUserIds: + return AuthorizationInUserIDsQueryToModel(q.InUserIds) + 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 AuthorizationInUserIDsQueryToModel(q *filter_pb.InIDsFilter) (query.SearchQuery, error) { + return query.NewUserGrantInUserIDsSearchQuery(q.Ids) +} + +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 { + return &authorization.Authorization{ + Id: userGrant.ID, + CreationDate: timestamppb.New(userGrant.CreationDate), + ChangeDate: timestamppb.New(userGrant.ChangeDate), + Project: &authorization.Project{ + Id: userGrant.ProjectID, + Name: userGrant.ProjectName, + OrganizationId: userGrant.ProjectResourceOwner, + }, + Organization: &authorization.Organization{ + Id: userGrant.ResourceOwner, + Name: userGrant.OrgName, + }, + User: &authorization.User{ + Id: userGrant.UserID, + PreferredLoginName: userGrant.PreferredLoginName, + DisplayName: userGrant.DisplayName, + AvatarUrl: userGrant.AvatarURL, + OrganizationId: userGrant.UserResourceOwner, + }, + State: userGrantStateToPb(userGrant.State), + Roles: rolesToPb(userGrant.RoleInformation), + } +} + +func rolesToPb(roles []query.Role) []*authorization.Role { + r := make([]*authorization.Role, len(roles)) + for i, role := range roles { + r[i] = &authorization.Role{ + Key: role.Key, + DisplayName: role.DisplayName, + Group: role.GroupName, + } + } + return r +} + +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/v2/server.go b/internal/api/grpc/authorization/v2/server.go new file mode 100644 index 00000000000..8fd241dc131 --- /dev/null +++ b/internal/api/grpc/authorization/v2/server.go @@ -0,0 +1,60 @@ +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/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/pkg/grpc/authorization/v2" + "github.com/zitadel/zitadel/pkg/grpc/authorization/v2/authorizationconnect" +) + +var _ authorizationconnect.AuthorizationServiceHandler = (*Server)(nil) + +type Server struct { + systemDefaults systemdefaults.SystemDefaults + command *command.Commands + query *query.Queries + + checkPermission domain.PermissionCheck +} + +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_v2_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 +} diff --git a/internal/api/grpc/authorization/v2beta/integration_test/query_test.go b/internal/api/grpc/authorization/v2beta/integration_test/query_test.go index 0bf40959403..69456a7af43 100644 --- a/internal/api/grpc/authorization/v2beta/integration_test/query_test.go +++ b/internal/api/grpc/authorization/v2beta/integration_test/query_test.go @@ -487,17 +487,17 @@ func createAuthorization(ctx context.Context, instance *integration.Instance, t 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 { +func createAuthorizationForProject(ctx context.Context, instance *integration.Instance, t *testing.T, organizationID, userID, projectName, projectID string) *authorization.Authorization { userResp, err := instance.Client.UserV2.GetUserByID(ctx, &user.GetUserByIDRequest{UserId: userID}) require.NoError(t, err) - authResp := instance.CreateAuthorizationProject(t, ctx, projectID, userID) + authResp := instance.CreateAuthorizationProject(t, ctx, projectID, userID, organizationID) return &authorization.Authorization{ Id: authResp.GetId(), ProjectId: projectID, ProjectName: projectName, - ProjectOrganizationId: orgID, - OrganizationId: orgID, + ProjectOrganizationId: organizationID, + OrganizationId: organizationID, CreationDate: authResp.GetCreationDate(), ChangeDate: authResp.GetCreationDate(), State: 1, diff --git a/internal/api/grpc/oidc/v2/integration_test/oidc_test.go b/internal/api/grpc/oidc/v2/integration_test/oidc_test.go index e2ddebe6ba8..57664af9d4d 100644 --- a/internal/api/grpc/oidc/v2/integration_test/oidc_test.go +++ b/internal/api/grpc/oidc/v2/integration_test/oidc_test.go @@ -961,12 +961,12 @@ func createProjectGrant(ctx context.Context, t *testing.T, projectID, grantedOrg }, retryDuration, tick) } -func createProjectUserGrant(ctx context.Context, t *testing.T, orgID, projectID, userID string) { - resp := Instance.CreateAuthorizationProject(t, ctx, projectID, userID) +func createProjectUserGrant(ctx context.Context, t *testing.T, organizationID, projectID, userID string) { + resp := Instance.CreateAuthorizationProject(t, ctx, projectID, userID, organizationID) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) require.EventuallyWithT(t, func(collect *assert.CollectT) { - _, err := Instance.Client.Mgmt.GetUserGrantByID(integration.SetOrgID(ctx, orgID), &mgmt.GetUserGrantByIDRequest{ + _, err := Instance.Client.Mgmt.GetUserGrantByID(integration.SetOrgID(ctx, organizationID), &mgmt.GetUserGrantByIDRequest{ UserId: userID, GrantId: resp.GetId(), }) diff --git a/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go b/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go index 0f248513dda..d50f8963480 100644 --- a/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go +++ b/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go @@ -692,12 +692,12 @@ func createOIDCApplication(ctx context.Context, t *testing.T, projectRoleCheck, return project.GetId(), clientV2.GetClientId() } -func createProjectUserGrant(ctx context.Context, t *testing.T, orgID, projectID, userID string) { - resp := Instance.CreateAuthorizationProject(t, ctx, projectID, userID) +func createProjectUserGrant(ctx context.Context, t *testing.T, organizationID, projectID, userID string) { + resp := Instance.CreateAuthorizationProject(t, ctx, projectID, userID, organizationID) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) require.EventuallyWithT(t, func(collect *assert.CollectT) { - _, err := Instance.Client.Mgmt.GetUserGrantByID(integration.SetOrgID(ctx, orgID), &mgmt.GetUserGrantByIDRequest{ + _, err := Instance.Client.Mgmt.GetUserGrantByID(integration.SetOrgID(ctx, organizationID), &mgmt.GetUserGrantByIDRequest{ UserId: userID, GrantId: resp.GetId(), }) diff --git a/internal/api/grpc/saml/v2/integration_test/saml_test.go b/internal/api/grpc/saml/v2/integration_test/saml_test.go index 70a63009d2a..9f7dfedc2d7 100644 --- a/internal/api/grpc/saml/v2/integration_test/saml_test.go +++ b/internal/api/grpc/saml/v2/integration_test/saml_test.go @@ -715,12 +715,12 @@ func createProjectGrant(ctx context.Context, t *testing.T, projectID, grantedOrg }, retryDuration, tick) } -func createProjectUserGrant(ctx context.Context, t *testing.T, orgID, projectID, userID string) { - resp := Instance.CreateAuthorizationProject(t, ctx, projectID, userID) +func createProjectUserGrant(ctx context.Context, t *testing.T, organizationID, projectID, userID string) { + resp := Instance.CreateAuthorizationProject(t, ctx, projectID, userID, organizationID) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) require.EventuallyWithT(t, func(collect *assert.CollectT) { - _, err := Instance.Client.Mgmt.GetUserGrantByID(integration.SetOrgID(ctx, orgID), &mgmt.GetUserGrantByIDRequest{ + _, err := Instance.Client.Mgmt.GetUserGrantByID(integration.SetOrgID(ctx, organizationID), &mgmt.GetUserGrantByIDRequest{ UserId: userID, GrantId: resp.GetId(), }) diff --git a/internal/integration/client.go b/internal/integration/client.go index 1a567cf6b2d..351eb1332bf 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -27,7 +27,8 @@ import ( app_v2beta "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" "github.com/zitadel/zitadel/pkg/grpc/application/v2" "github.com/zitadel/zitadel/pkg/grpc/auth" - authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta" + authorization_v2 "github.com/zitadel/zitadel/pkg/grpc/authorization/v2" + authorization_v2beta "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" @@ -89,7 +90,8 @@ type Client struct { ApplicationV2 application.ApplicationServiceClient InternalPermissionv2Beta internal_permission_v2beta.InternalPermissionServiceClient InternalPermissionV2 internal_permission_v2.InternalPermissionServiceClient - AuthorizationV2Beta authorization.AuthorizationServiceClient + AuthorizationV2Beta authorization_v2beta.AuthorizationServiceClient //nolint:staticcheck // deprecated, but still used in tests + AuthorizationV2 authorization_v2.AuthorizationServiceClient } func NewDefaultClient(ctx context.Context) (*Client, error) { @@ -135,7 +137,8 @@ func newClient(ctx context.Context, target string) (*Client, error) { ApplicationV2: application.NewApplicationServiceClient(cc), InternalPermissionv2Beta: internal_permission_v2beta.NewInternalPermissionServiceClient(cc), InternalPermissionV2: internal_permission_v2.NewInternalPermissionServiceClient(cc), - AuthorizationV2Beta: authorization.NewAuthorizationServiceClient(cc), + AuthorizationV2Beta: authorization_v2beta.NewAuthorizationServiceClient(cc), + AuthorizationV2: authorization_v2.NewAuthorizationServiceClient(cc), } return client, client.pollHealth(ctx) } @@ -944,20 +947,23 @@ func (i *Instance) ActivateProjectGrant(ctx context.Context, t *testing.T, proje return resp } -func (i *Instance) CreateAuthorizationProject(t *testing.T, ctx context.Context, projectID, userID string) *authorization.CreateAuthorizationResponse { - resp, err := i.Client.AuthorizationV2Beta.CreateAuthorization(ctx, &authorization.CreateAuthorizationRequest{ - UserId: userID, - ProjectId: projectID, +func (i *Instance) CreateAuthorizationProject(t *testing.T, ctx context.Context, projectID, userID, organizationID string, roles ...string) *authorization_v2.CreateAuthorizationResponse { + resp, err := i.Client.AuthorizationV2.CreateAuthorization(ctx, &authorization_v2.CreateAuthorizationRequest{ + UserId: userID, + ProjectId: projectID, + OrganizationId: organizationID, + RoleKeys: roles, }) require.NoError(t, err) return resp } -func (i *Instance) CreateAuthorizationProjectGrant(t *testing.T, ctx context.Context, projectID, orgID, userID string) *authorization.CreateAuthorizationResponse { - resp, err := i.Client.AuthorizationV2Beta.CreateAuthorization(ctx, &authorization.CreateAuthorizationRequest{ +func (i *Instance) CreateAuthorizationProjectGrant(t *testing.T, ctx context.Context, projectID, organizationID, userID string, roles ...string) *authorization_v2.CreateAuthorizationResponse { + resp, err := i.Client.AuthorizationV2.CreateAuthorization(ctx, &authorization_v2.CreateAuthorizationRequest{ UserId: userID, ProjectId: projectID, - OrganizationId: gu.Ptr(orgID), + OrganizationId: organizationID, + RoleKeys: roles, }) require.NoError(t, err) return resp diff --git a/internal/query/user_grant.go b/internal/query/user_grant.go index 6efc82f13b2..74fc344e925 100644 --- a/internal/query/user_grant.go +++ b/internal/query/user_grant.go @@ -3,7 +3,9 @@ package query import ( "context" "database/sql" + "encoding/json" "errors" + "fmt" "slices" "time" @@ -21,11 +23,12 @@ import ( type UserGrant struct { // ID represents the aggregate id (id of the user grant) - ID string `json:"id,omitempty"` - CreationDate time.Time `json:"creation_date,omitempty"` - ChangeDate time.Time `json:"change_date,omitempty"` - Sequence uint64 `json:"sequence,omitempty"` - Roles database.TextArray[string] `json:"roles,omitempty"` + ID string `json:"id,omitempty"` + CreationDate time.Time `json:"creation_date,omitempty"` + ChangeDate time.Time `json:"change_date,omitempty"` + Sequence uint64 `json:"sequence,omitempty"` + Roles database.TextArray[string] `json:"roles,omitempty"` + RoleInformation []Role `json:"-"` // GrantID represents the project grant id GrantID string `json:"grant_id,omitempty"` State domain.UserGrantState `json:"state,omitempty"` @@ -56,6 +59,12 @@ type UserGrant struct { GrantedOrgDomain string `json:"granted_org_domain,omitempty"` } +type Role struct { + Key string `json:"role_key,omitempty"` + DisplayName string `json:"display_name,omitempty"` + GroupName string `json:"group_name,omitempty"` +} + type UserGrants struct { SearchResponse UserGrants []*UserGrant @@ -390,6 +399,7 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro UserGrantSequence.identifier(), UserGrantGrantID.identifier(), UserGrantRoles.identifier(), + "roles.role_information", UserGrantState.identifier(), UserGrantUserID.identifier(), @@ -426,6 +436,7 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro LeftJoin(join(ProjectGrantColumnGrantID, UserGrantGrantID) + " AND " + ProjectGrantColumnProjectID.identifier() + " = " + UserGrantProjectID.identifier()). LeftJoin(join(GrantedOrgColumnId, ProjectGrantColumnGrantedOrgID)). LeftJoin(join(LoginNameUserIDCol, UserGrantUserID)). + LeftJoin("LATERAL (" + rolesInfoQuery + ") as roles ON true"). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, ).PlaceholderFormat(sq.Dollar), @@ -433,6 +444,8 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro g := new(UserGrant) var ( + roles []byte + username sql.NullString firstName sql.NullString userType sql.NullInt32 @@ -463,6 +476,7 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro &g.Sequence, &g.GrantID, &g.Roles, + &roles, &g.State, &g.UserID, @@ -496,6 +510,12 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro } return nil, zerrors.ThrowInternal(err, "QUERY-oQPcP", "Errors.Internal") } + if len(roles) > 0 { + err = json.Unmarshal(roles, &g.RoleInformation) + if err != nil { + return nil, err + } + } g.Username = username.String g.UserType = domain.UserType(userType.Int32) @@ -527,6 +547,7 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e UserGrantSequence.identifier(), UserGrantGrantID.identifier(), UserGrantRoles.identifier(), + "roles.role_information", UserGrantState.identifier(), UserGrantUserID.identifier(), @@ -565,6 +586,7 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e LeftJoin(join(ProjectGrantColumnGrantID, UserGrantGrantID) + " AND " + ProjectGrantColumnProjectID.identifier() + " = " + UserGrantProjectID.identifier()). LeftJoin(join(GrantedOrgColumnId, ProjectGrantColumnGrantedOrgID)). LeftJoin(join(LoginNameUserIDCol, UserGrantUserID)). + LeftJoin("LATERAL (" + rolesInfoQuery + ") as roles ON true"). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, ).PlaceholderFormat(sq.Dollar), @@ -575,6 +597,8 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e g := new(UserGrant) var ( + roles []byte + username sql.NullString userType sql.NullInt32 userOwner sql.NullString @@ -605,6 +629,7 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e &g.Sequence, &g.GrantID, &g.Roles, + &roles, &g.State, &g.UserID, @@ -637,6 +662,12 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e if err != nil { return nil, err } + if len(roles) > 0 { + err = json.Unmarshal(roles, &g.RoleInformation) + if err != nil { + return nil, err + } + } g.Username = username.String g.UserType = domain.UserType(userType.Int32) @@ -672,3 +703,21 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e }, nil } } + +var rolesInfoQuery = fmt.Sprintf( + `SELECT JSON_AGG( + JSON_BUILD_OBJECT( + 'role_key', pr.role_key, + 'display_name', pr.display_name, + 'group_name', pr.group_name + ) + ) as role_information + FROM %[1]s pr + WHERE pr.instance_id = %[2]s + AND pr.project_id = %[3]s + AND pr.role_key = ANY(%[4]s)`, + projectRolesTable.identifier(), + UserGrantInstanceID.identifier(), + UserGrantProjectID.identifier(), + UserGrantRoles.identifier(), +) diff --git a/internal/query/user_grant_test.go b/internal/query/user_grant_test.go index f79ae78a80e..0dc346529ef 100644 --- a/internal/query/user_grant_test.go +++ b/internal/query/user_grant_test.go @@ -21,6 +21,7 @@ var ( ", projections.user_grants5.sequence" + ", projections.user_grants5.grant_id" + ", projections.user_grants5.roles" + + ", roles.role_information" + ", projections.user_grants5.state" + ", projections.user_grants5.user_id" + ", projections.users14.username" + @@ -52,6 +53,7 @@ var ( " 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" + + " LEFT JOIN LATERAL (SELECT JSON_AGG( JSON_BUILD_OBJECT( 'role_key', pr.role_key, 'display_name', pr.display_name, 'group_name', pr.group_name ) ) as role_information FROM projections.project_roles4 pr WHERE pr.instance_id = projections.user_grants5.instance_id AND pr.project_id = projections.user_grants5.project_id AND pr.role_key = ANY(projections.user_grants5.roles)) as roles ON true " + " WHERE projections.login_names3.is_primary = $1") userGrantCols = []string{ "id", @@ -60,6 +62,7 @@ var ( "sequence", "grant_id", "roles", + "role_information", "state", "user_id", "username", @@ -90,6 +93,7 @@ var ( ", projections.user_grants5.sequence" + ", projections.user_grants5.grant_id" + ", projections.user_grants5.roles" + + ", roles.role_information" + ", projections.user_grants5.state" + ", projections.user_grants5.user_id" + ", projections.users14.username" + @@ -122,6 +126,7 @@ var ( " 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" + + " LEFT JOIN LATERAL (SELECT JSON_AGG( JSON_BUILD_OBJECT( 'role_key', pr.role_key, 'display_name', pr.display_name, 'group_name', pr.group_name ) ) as role_information FROM projections.project_roles4 pr WHERE pr.instance_id = projections.user_grants5.instance_id AND pr.project_id = projections.user_grants5.project_id AND pr.role_key = ANY(projections.user_grants5.roles)) as roles ON true " + " WHERE projections.login_names3.is_primary = $1") userGrantsCols = append( userGrantCols, @@ -172,6 +177,7 @@ func Test_UserGrantPrepares(t *testing.T) { 20211111, "grant-id", database.TextArray[string]{"role-key"}, + `[{"display_name":"displayName","group_name":"groupName","role_key":"role-key"}]`, domain.UserGrantStateActive, "user-id", "username", @@ -203,6 +209,7 @@ func Test_UserGrantPrepares(t *testing.T) { ChangeDate: testNow, Sequence: 20211111, Roles: database.TextArray[string]{"role-key"}, + RoleInformation: []Role{{Key: "role-key", DisplayName: "displayName", GroupName: "groupName"}}, GrantID: "grant-id", State: domain.UserGrantStateActive, UserID: "user-id", @@ -242,6 +249,7 @@ func Test_UserGrantPrepares(t *testing.T) { 20211111, "grant-id", database.TextArray[string]{"role-key"}, + `[{"display_name":"displayName","group_name":"groupName","role_key":"role-key"}]`, domain.UserGrantStateActive, "user-id", "username", @@ -273,6 +281,7 @@ func Test_UserGrantPrepares(t *testing.T) { ChangeDate: testNow, Sequence: 20211111, Roles: database.TextArray[string]{"role-key"}, + RoleInformation: []Role{{Key: "role-key", DisplayName: "displayName", GroupName: "groupName"}}, GrantID: "grant-id", State: domain.UserGrantStateActive, UserID: "user-id", @@ -312,6 +321,7 @@ func Test_UserGrantPrepares(t *testing.T) { 20211111, "grant-id", database.TextArray[string]{"role-key"}, + `[{"display_name":"displayName","group_name":"groupName","role_key":"role-key"}]`, domain.UserGrantStateActive, "user-id", "username", @@ -343,6 +353,7 @@ func Test_UserGrantPrepares(t *testing.T) { ChangeDate: testNow, Sequence: 20211111, Roles: database.TextArray[string]{"role-key"}, + RoleInformation: []Role{{Key: "role-key", DisplayName: "displayName", GroupName: "groupName"}}, GrantID: "grant-id", State: domain.UserGrantStateActive, UserID: "user-id", @@ -382,6 +393,7 @@ func Test_UserGrantPrepares(t *testing.T) { 20211111, "grant-id", database.TextArray[string]{"role-key"}, + `[{"display_name":"displayName","group_name":"groupName","role_key":"role-key"}]`, domain.UserGrantStateActive, "user-id", "username", @@ -413,6 +425,7 @@ func Test_UserGrantPrepares(t *testing.T) { ChangeDate: testNow, Sequence: 20211111, Roles: database.TextArray[string]{"role-key"}, + RoleInformation: []Role{{Key: "role-key", DisplayName: "displayName", GroupName: "groupName"}}, GrantID: "grant-id", State: domain.UserGrantStateActive, UserID: "user-id", @@ -452,6 +465,7 @@ func Test_UserGrantPrepares(t *testing.T) { 20211111, "grant-id", database.TextArray[string]{"role-key"}, + `[{"display_name":"displayName","group_name":"groupName","role_key":"role-key"}]`, domain.UserGrantStateActive, "user-id", "username", @@ -483,6 +497,7 @@ func Test_UserGrantPrepares(t *testing.T) { ChangeDate: testNow, Sequence: 20211111, Roles: database.TextArray[string]{"role-key"}, + RoleInformation: []Role{{Key: "role-key", DisplayName: "displayName", GroupName: "groupName"}}, GrantID: "grant-id", State: domain.UserGrantStateActive, UserID: "user-id", @@ -552,6 +567,7 @@ func Test_UserGrantPrepares(t *testing.T) { 20211111, "grant-id", database.TextArray[string]{"role-key"}, + `[{"display_name":"displayName","group_name":"groupName","role_key":"role-key"}]`, domain.UserGrantStateActive, "user-id", "username", @@ -589,6 +605,7 @@ func Test_UserGrantPrepares(t *testing.T) { ChangeDate: testNow, Sequence: 20211111, Roles: database.TextArray[string]{"role-key"}, + RoleInformation: []Role{{Key: "role-key", DisplayName: "displayName", GroupName: "groupName"}}, GrantID: "grant-id", State: domain.UserGrantStateActive, UserID: "user-id", @@ -631,6 +648,7 @@ func Test_UserGrantPrepares(t *testing.T) { 20211111, "grant-id", database.TextArray[string]{"role-key"}, + `[{"display_name":"displayName","group_name":"groupName","role_key":"role-key"}]`, domain.UserGrantStateActive, "user-id", "username", @@ -668,6 +686,7 @@ func Test_UserGrantPrepares(t *testing.T) { ChangeDate: testNow, Sequence: 20211111, Roles: database.TextArray[string]{"role-key"}, + RoleInformation: []Role{{Key: "role-key", DisplayName: "displayName", GroupName: "groupName"}}, GrantID: "grant-id", State: domain.UserGrantStateActive, UserID: "user-id", @@ -710,6 +729,7 @@ func Test_UserGrantPrepares(t *testing.T) { 20211111, "grant-id", database.TextArray[string]{"role-key"}, + `[{"display_name":"displayName","group_name":"groupName","role_key":"role-key"}]`, domain.UserGrantStateActive, "user-id", "username", @@ -747,6 +767,7 @@ func Test_UserGrantPrepares(t *testing.T) { ChangeDate: testNow, Sequence: 20211111, Roles: database.TextArray[string]{"role-key"}, + RoleInformation: []Role{{Key: "role-key", DisplayName: "displayName", GroupName: "groupName"}}, GrantID: "grant-id", State: domain.UserGrantStateActive, UserID: "user-id", @@ -789,6 +810,7 @@ func Test_UserGrantPrepares(t *testing.T) { 20211111, "grant-id", database.TextArray[string]{"role-key"}, + `[{"display_name":"displayName","group_name":"groupName","role_key":"role-key"}]`, domain.UserGrantStateActive, "user-id", "username", @@ -826,6 +848,7 @@ func Test_UserGrantPrepares(t *testing.T) { ChangeDate: testNow, Sequence: 20211111, Roles: database.TextArray[string]{"role-key"}, + RoleInformation: []Role{{Key: "role-key", DisplayName: "displayName", GroupName: "groupName"}}, GrantID: "grant-id", State: domain.UserGrantStateActive, UserID: "user-id", @@ -868,6 +891,7 @@ func Test_UserGrantPrepares(t *testing.T) { 20211111, "grant-id", database.TextArray[string]{"role-key"}, + `[{"display_name":"displayName","group_name":"groupName","role_key":"role-key"}]`, domain.UserGrantStateActive, "user-id", "username", @@ -905,6 +929,7 @@ func Test_UserGrantPrepares(t *testing.T) { ChangeDate: testNow, Sequence: 20211111, Roles: database.TextArray[string]{"role-key"}, + RoleInformation: []Role{{Key: "role-key", DisplayName: "displayName", GroupName: "groupName"}}, GrantID: "grant-id", State: domain.UserGrantStateActive, UserID: "user-id", @@ -947,6 +972,7 @@ func Test_UserGrantPrepares(t *testing.T) { 20211111, "grant-id", database.TextArray[string]{"role-key"}, + `[{"display_name":"displayName","group_name":"groupName","role_key":"role-key"}]`, domain.UserGrantStateActive, "user-id", "username", @@ -977,6 +1003,7 @@ func Test_UserGrantPrepares(t *testing.T) { 20211111, "grant-id", database.TextArray[string]{"role-key"}, + `[{"display_name":"displayName","group_name":"groupName","role_key":"role-key"}]`, domain.UserGrantStateActive, "user-id", "username", @@ -1014,6 +1041,7 @@ func Test_UserGrantPrepares(t *testing.T) { ChangeDate: testNow, Sequence: 20211111, Roles: database.TextArray[string]{"role-key"}, + RoleInformation: []Role{{Key: "role-key", DisplayName: "displayName", GroupName: "groupName"}}, GrantID: "grant-id", State: domain.UserGrantStateActive, UserID: "user-id", @@ -1044,6 +1072,7 @@ func Test_UserGrantPrepares(t *testing.T) { ChangeDate: testNow, Sequence: 20211111, Roles: database.TextArray[string]{"role-key"}, + RoleInformation: []Role{{Key: "role-key", DisplayName: "displayName", GroupName: "groupName"}}, GrantID: "grant-id", State: domain.UserGrantStateActive, UserID: "user-id", diff --git a/proto/zitadel/auth.proto b/proto/zitadel/auth.proto index c995bce16a2..d25c7661216 100644 --- a/proto/zitadel/auth.proto +++ b/proto/zitadel/auth.proto @@ -861,7 +861,7 @@ 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. + // Deprecated: [List authorizations](apis/resources/authorization_service_v2/zitadel-authorization-v-2-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) { @@ -914,7 +914,7 @@ 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. + // Deprecated: [List authorizations](apis/resources/authorization_service_v2/zitadel-authorization-v-2-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) { diff --git a/proto/zitadel/authorization/v2/authorization.proto b/proto/zitadel/authorization/v2/authorization.proto new file mode 100644 index 00000000000..e41c4b9a3c5 --- /dev/null +++ b/proto/zitadel/authorization/v2/authorization.proto @@ -0,0 +1,228 @@ +syntax = "proto3"; + +package zitadel.authorization.v2; + +import "google/api/field_behavior.proto"; +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/filter/v2/filter.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/authorization/v2;authorization"; + +message Authorization { + // ID is the unique identifier of the 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: "\"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 = 3 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"2025-01-23T10:34:18.051Z\""}]; + + // The project the user was granted the authorization for. + Project project = 4; + + // The organization the user was granted the authorization for. + // This does not have to correspond with the project or user's organization. + // But does represent the organization to which the authorization belongs and + // the user was given permissions for. + Organization organization = 5; + + // The user who was granted the authorization. + User user = 6; + + // State is the current state of the authorization. + State state = 7; + + // Roles contains the roles the user was granted for the project. + repeated Role roles = 8; +} + +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 Project { + // ID is the unique identifier of the project. + string id = 1; + + // Name is the name of the project. + string name = 2; + + // OrganizationID is the ID of the organization the project belongs to. + // This does not have to correspond with the authorizations organization. + // In case the authorization is given on a granted project, this is the organization + // that owns the project and granted it. + string organization_id = 3; +} + +message Organization { + // ID is the unique identifier of the organization. + string id = 1; + + // Name is the name of the organization. + string name = 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 Role { + // Key is the unique key of the role. It's the only relevant attribute for ZITADEL and + // will be used for authorization checks and as claim in tokens and user info responses. + string key = 1; + + // Human readable name for the role, which might be displayed to users. + string display_name = 2; + + // The group the role belongs to. This is used to group roles in the UI. + string group = 3; +} + +message AuthorizationsSearchFilter { + oneof filter { + option (validate.required) = true; + + // Search for authorizations by their IDs. + zitadel.filter.v2.InIDsFilter authorization_ids = 1; + + // Search for authorizations by the ID of the organization it was granted for. + zitadel.filter.v2.IDFilter organization_id = 2; + + // Search for authorizations by their state. + StateQuery state = 3; + + // Search for authorizations by the IDs of the users who were granted the authorizations. + zitadel.filter.v2.InIDsFilter in_user_ids = 4; + + // Search for authorizations by the ID of the organisation the user is part of. + zitadel.filter.v2.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.v2.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.v2.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 + }, + (google.api.field_behavior) = REQUIRED + ]; + + // 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.v2.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 + }, + (google.api.field_behavior) = REQUIRED + ]; + // 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.v2.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}, + (google.api.field_behavior) = REQUIRED + ]; + + // 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.v2.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.v2.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.v2.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/v2/authorization_service.proto b/proto/zitadel/authorization/v2/authorization_service.proto new file mode 100644 index 00000000000..45d464d59d5 --- /dev/null +++ b/proto/zitadel/authorization/v2/authorization_service.proto @@ -0,0 +1,317 @@ +syntax = "proto3"; + +package zitadel.authorization.v2; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/api/field_behavior.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/v2/authorization.proto"; +import "zitadel/filter/v2/filter.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/authorization/v2;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 (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // 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 (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // 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 (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // 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 (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // 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 (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // 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 (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } +} + +message ListAuthorizationsRequest { + // Paginate through the results using a limit, offset and sorting. + optional zitadel.filter.v2.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. + AuthorizationFieldName sorting_column = 2 [ + (validate.rules).enum = {defined_only: true} + ]; + + // Define the criteria to query for. + repeated AuthorizationsSearchFilter filters = 3; +} + +message ListAuthorizationsResponse { + // Contains the pagination information. + zitadel.filter.v2.PaginationResponse pagination = 1; + + // Authorizations contains the list of authorizations matching the request. + 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\""; + }, + (google.api.field_behavior) = REQUIRED + ]; + + // 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\""; + }, + (google.api.field_behavior) = REQUIRED + ]; + + // 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. + string organization_id = 3 [ + (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: "\"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\""; + }, + (google.api.field_behavior) = REQUIRED + ]; +} + +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\""; + }, + (google.api.field_behavior) = REQUIRED + ]; +} + +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\""; + }, + (google.api.field_behavior) = REQUIRED + ]; +} + +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/authorization/v2beta/authorization_service.proto b/proto/zitadel/authorization/v2beta/authorization_service.proto index 5020154883a..584a269f6fa 100644 --- a/proto/zitadel/authorization/v2beta/authorization_service.proto +++ b/proto/zitadel/authorization/v2beta/authorization_service.proto @@ -17,10 +17,14 @@ option go_package = "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta;au // // For managing permissions and roles for ZITADEL internal resources, like organizations, projects, // users, etc., please use the InternalPermissionService. +// +// Deprecated: use authorization service v2 instead. This service will be removed in the next major version of ZITADEL. service AuthorizationService { // List Authorizations // + // Deprecated: please move to the corresponding endpoint under authorization service v2. This endpoint will be removed with the next major version of ZITADEL. + // // ListAuthorizations returns all authorizations matching the request and necessary permissions. // // Required permissions: @@ -40,6 +44,7 @@ service AuthorizationService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -57,6 +62,8 @@ service AuthorizationService { // Create Authorization // + // Deprecated: please move to the corresponding endpoint under authorization service v2. This endpoint will be removed with the next major version of ZITADEL. + // // CreateAuthorization creates a new authorization for a user in an owned or granted project. // // Required permissions: @@ -74,6 +81,7 @@ service AuthorizationService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -102,6 +110,8 @@ service AuthorizationService { // Update Authorization // + // Deprecated: please move to the corresponding endpoint under authorization service v2. This endpoint will be removed with the next major version of ZITADEL. + // // UpdateAuthorization updates the authorization. // // Note that any role keys previously granted to the user and not present in the request will be revoked. @@ -121,6 +131,7 @@ service AuthorizationService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -143,6 +154,8 @@ service AuthorizationService { // Delete Authorization // + // Deprecated: please move to the corresponding endpoint under authorization service v2. This endpoint will be removed with the next major version of ZITADEL. + // // DeleteAuthorization deletes the authorization. // // In case the authorization is not found, the request will return a successful response as @@ -163,6 +176,7 @@ service AuthorizationService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -185,6 +199,8 @@ service AuthorizationService { // Activate Authorization // + // Deprecated: please move to the corresponding endpoint under authorization service v2. This endpoint will be removed with the next major version of ZITADEL. + // // ActivateAuthorization activates an existing but inactive authorization. // // In case the authorization is already active, the request will return a successful response as @@ -206,6 +222,7 @@ service AuthorizationService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -228,6 +245,8 @@ service AuthorizationService { // Deactivate Authorization // + // Deprecated: please move to the corresponding endpoint under authorization service v2. This endpoint will be removed with the next major version of ZITADEL. + // // DeactivateAuthorization deactivates an existing and active authorization. // // In case the authorization is already inactive, the request will return a successful response as @@ -249,6 +268,7 @@ service AuthorizationService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index e9037beb107..9056410d911 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -4509,7 +4509,7 @@ 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. + // Deprecated: [List authorizations](apis/resources/authorization_service_v2/zitadel-authorization-v-2-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) { @@ -4537,7 +4537,7 @@ 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. + // Deprecated: [List authorizations](apis/resources/authorization_service_v2/zitadel-authorization-v-2-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) { @@ -4567,7 +4567,7 @@ 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. + // Deprecated: [Add an authorization](apis/resources/authorization_service_v2/zitadel-authorization-v-2-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) { @@ -4596,7 +4596,7 @@ 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. + // Deprecated: [Update an authorization](apis/resources/authorization_service_v2/zitadel-authorization-v-2-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) { @@ -4625,7 +4625,7 @@ 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. + // Deprecated: [Deactivate an authorization](apis/resources/authorization_service_v2/zitadel-authorization-v-2-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) { @@ -4654,7 +4654,7 @@ 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. + // Deprecated: [Activate an authorization](apis/resources/authorization_service_v2/zitadel-authorization-v-2-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) { @@ -4683,7 +4683,7 @@ 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. + // Deprecated: [Delete an authorization](apis/resources/authorization_service_v2/zitadel-authorization-v-2-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) { @@ -4711,7 +4711,7 @@ 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. + // Deprecated: [Delete authorizations one after the other](apis/resources/authorization_service_v2/zitadel-authorization-v-2-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) {