feat: user profile requests in resource APIs (#10151)

# Which Problems Are Solved

The commands for the resource based v2beta AuthorizationService API are
added.
Authorizations, previously knows as user grants, give a user in a
specific organization and project context roles.
The project can be owned or granted.
The given roles can be used to restrict access within the projects
applications.

The commands for the resource based v2beta InteralPermissionService API
are added.
Administrators, previously knows as memberships, give a user in a
specific organization and project context roles.
The project can be owned or granted.
The give roles give the user permissions to manage different resources
in Zitadel.

API definitions from https://github.com/zitadel/zitadel/issues/9165 are
implemented.

Contains endpoints for user metadata.

# How the Problems Are Solved

### New Methods

- CreateAuthorization
- UpdateAuthorization
- DeleteAuthorization
- ActivateAuthorization
- DeactivateAuthorization
- ListAuthorizations
- CreateAdministrator
- UpdateAdministrator
- DeleteAdministrator
- ListAdministrators
- SetUserMetadata to set metadata on a user
- DeleteUserMetadata to delete metadata on a user
- ListUserMetadata to query for metadata of a user

## Deprecated Methods

### v1.ManagementService
- GetUserGrantByID
- ListUserGrants
- AddUserGrant
- UpdateUserGrant
- DeactivateUserGrant
- ReactivateUserGrant
- RemoveUserGrant
- BulkRemoveUserGrant

### v1.AuthService
- ListMyUserGrants
- ListMyProjectPermissions

# Additional Changes

- Permission checks for metadata functionality on query and command side
- correct existence checks for resources, for example you can only be an
administrator on an existing project
- combined all member tables to singular query for the administrators
- add permission checks for command an query side functionality
- combined functions on command side where necessary for easier
maintainability

# Additional Context

Closes #9165

---------

Co-authored-by: Elio Bischof <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Livio Spring <[email protected]>
This commit is contained in:
Stefan Benz
2025-07-04 18:12:59 +02:00
committed by GitHub
co-authored by Elio Bischof Copilot Livio Spring
parent 9ebf2316c6
commit 5403be7c4b
142 changed files with 13223 additions and 2497 deletions
+2
View File
@@ -135,6 +135,8 @@ message CreateUserRequest {
```
Only allow providing a context where it is required. The context MUST not be provided if not required.
If the context is required but deferrable, the context can be defaulted.
For example, creating an Authorization without an organization id will default the organization id to the projects resource owner.
For example, when retrieving or updating a user, the `organization_id` is not required, since the user can be determined by the user's id.
However, it is possible to provide the `organization_id` as a filter to retrieve a list of users of a specific organization.
+9 -1
View File
@@ -38,10 +38,12 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/admin"
app "github.com/zitadel/zitadel/internal/api/grpc/app/v2beta"
"github.com/zitadel/zitadel/internal/api/grpc/auth"
authorization_v2beta "github.com/zitadel/zitadel/internal/api/grpc/authorization/v2beta"
feature_v2 "github.com/zitadel/zitadel/internal/api/grpc/feature/v2"
feature_v2beta "github.com/zitadel/zitadel/internal/api/grpc/feature/v2beta"
idp_v2 "github.com/zitadel/zitadel/internal/api/grpc/idp/v2"
instance "github.com/zitadel/zitadel/internal/api/grpc/instance/v2beta"
internal_permission_v2beta "github.com/zitadel/zitadel/internal/api/grpc/internal_permission/v2beta"
"github.com/zitadel/zitadel/internal/api/grpc/management"
oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2"
oidc_v2beta "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2beta"
@@ -474,7 +476,7 @@ func startAPIs(
if err := apis.RegisterService(ctx, user_v2beta.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, user_v2.CreateServer(config.SystemDefaults, commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil {
if err := apis.RegisterService(ctx, user_v2.CreateServer(commands, queries, config.SystemDefaults, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, session_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil {
@@ -510,6 +512,9 @@ func startAPIs(
if err := apis.RegisterService(ctx, project_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, internal_permission_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, userschema_v3_alpha.CreateServer(config.SystemDefaults, commands, queries)); err != nil {
return nil, err
}
@@ -525,6 +530,9 @@ func startAPIs(
if err := apis.RegisterService(ctx, debug_events.CreateServer(commands, queries)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, authorization_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, app.CreateServer(commands, queries, permissionCheck)); err != nil {
return nil, err
}
+18
View File
@@ -406,6 +406,24 @@ module.exports = {
categoryLinkSource: "auto",
},
},
authorization_v2: {
specPath:
".artifacts/openapi3/zitadel/authorization/v2beta/authorization_service.openapi.yaml",
outputDir: "docs/apis/resources/authorization_service_v2",
sidebarOptions: {
groupPathsBy: "tag",
categoryLinkSource: "auto",
},
},
internal_permission_v2: {
specPath:
".artifacts/openapi3/zitadel/internal_permission/v2beta/internal_permission_service.openapi.yaml",
outputDir: "docs/apis/resources/internal_permission_service_v2",
sidebarOptions: {
groupPathsBy: "tag",
categoryLinkSource: "auto",
},
},
},
},
],
+33
View File
@@ -16,6 +16,8 @@ const sidebar_api_actions_v2 = require("./docs/apis/resources/action_service_v2/
const sidebar_api_project_service_v2 = require("./docs/apis/resources/project_service_v2/sidebar.ts").default
const sidebar_api_webkey_service_v2 = require("./docs/apis/resources/webkey_service_v2/sidebar.ts").default
const sidebar_api_instance_service_v2 = require("./docs/apis/resources/instance_service_v2/sidebar.ts").default
const sidebar_api_authorization_service_v2 = require("./docs/apis/resources/authorization_service_v2/sidebar.ts").default
const sidebar_api_permission_service_v2 = require("./docs/apis/resources/internal_permission_service_v2/sidebar.ts").default
const sidebar_api_app_v2 = require("./docs/apis/resources/application_service_v2/sidebar.ts").default
module.exports = {
@@ -914,6 +916,37 @@ module.exports = {
},
items: sidebar_api_app_v2,
},
{
type: "category",
label: "Authorizations (Beta)",
link: {
type: "generated-index",
title: "Authorization Service API (Beta)",
slug: "/apis/resources/authorization_service_v2",
description:
"AuthorizationService provides methods to manage authorizations for users within your projects and applications.\n" +
"\n" +
"For managing permissions and roles for ZITADEL internal resources, like organizations, projects,\n" +
"users, etc., please use the InternalPermissionService."+
"\n"+
"This API is in beta state. It can AND will continue breaking until a stable version is released.\n"
},
items: sidebar_api_authorization_service_v2,
},
{
type: "category",
label: "Permissions (Beta)",
link: {
type: "generated-index",
title: "Permission Service API (Beta)",
slug: "/apis/resources/permission_service_v2",
description:
"This API is intended to manage internal permissions in ZITADEL.\n" +
"\n"+
"This API is in beta state. It can AND will continue breaking until a stable version is released.\n"
},
items: sidebar_api_permission_service_v2,
},
],
},
{
@@ -782,12 +782,3 @@ func TestServer_ListExecutions(t *testing.T) {
})
}
}
func containExecution(t *assert.CollectT, executionList []*action.Execution, execution *action.Execution) bool {
for _, exec := range executionList {
if assert.EqualExportedValues(t, execution, exec) {
return true
}
}
return false
}
+2 -2
View File
@@ -659,7 +659,7 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w
if err != nil {
return nil, nil, nil, nil, err
}
metadataList, err := s.query.SearchUserMetadata(ctx, false, user.ID, &query.UserMetadataSearchQueries{Queries: []query.SearchQuery{metadataOrgSearch}}, false)
metadataList, err := s.query.SearchUserMetadata(ctx, false, user.ID, &query.UserMetadataSearchQueries{Queries: []query.SearchQuery{metadataOrgSearch}}, nil)
metaspan.EndWithError(err)
if err != nil {
return nil, nil, nil, nil, err
@@ -984,7 +984,7 @@ func (s *Server) getNecessaryUserGrantsForOrg(ctx context.Context, org string, p
return nil, err
}
queriedUserGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantSearchOrg}}, true)
queriedUserGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantSearchOrg}}, true, nil)
if err != nil {
return nil, err
}
+6 -5
View File
@@ -4,6 +4,7 @@ import (
"context"
"time"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/member"
"github.com/zitadel/zitadel/internal/api/grpc/object"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
@@ -33,35 +34,35 @@ func (s *Server) ListIAMMembers(ctx context.Context, req *admin_pb.ListIAMMember
}
func (s *Server) AddIAMMember(ctx context.Context, req *admin_pb.AddIAMMemberRequest) (*admin_pb.AddIAMMemberResponse, error) {
member, err := s.command.AddInstanceMember(ctx, req.UserId, req.Roles...)
member, err := s.command.AddInstanceMember(ctx, AddIAMMemberToCommand(req, authz.GetInstance(ctx).InstanceID()))
if err != nil {
return nil, err
}
return &admin_pb.AddIAMMemberResponse{
Details: object.AddToDetailsPb(
member.Sequence,
member.ChangeDate,
member.EventDate,
member.ResourceOwner,
),
}, nil
}
func (s *Server) UpdateIAMMember(ctx context.Context, req *admin_pb.UpdateIAMMemberRequest) (*admin_pb.UpdateIAMMemberResponse, error) {
member, err := s.command.ChangeInstanceMember(ctx, UpdateIAMMemberToDomain(req))
member, err := s.command.ChangeInstanceMember(ctx, UpdateIAMMemberToCommand(req, authz.GetInstance(ctx).InstanceID()))
if err != nil {
return nil, err
}
return &admin_pb.UpdateIAMMemberResponse{
Details: object.ChangeToDetailsPb(
member.Sequence,
member.ChangeDate,
member.EventDate,
member.ResourceOwner,
),
}, nil
}
func (s *Server) RemoveIAMMember(ctx context.Context, req *admin_pb.RemoveIAMMemberRequest) (*admin_pb.RemoveIAMMemberResponse, error) {
objectDetails, err := s.command.RemoveInstanceMember(ctx, req.UserId)
objectDetails, err := s.command.RemoveInstanceMember(ctx, authz.GetInstance(ctx).InstanceID(), req.UserId)
if err != nil {
return nil, err
}
@@ -3,23 +3,25 @@ package admin
import (
member_grpc "github.com/zitadel/zitadel/internal/api/grpc/member"
"github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/query"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
member_pb "github.com/zitadel/zitadel/pkg/grpc/member"
)
func AddIAMMemberToDomain(req *admin_pb.AddIAMMemberRequest) *domain.Member {
return &domain.Member{
UserID: req.UserId,
Roles: req.Roles,
func AddIAMMemberToCommand(req *admin_pb.AddIAMMemberRequest, instanceID string) *command.AddInstanceMember {
return &command.AddInstanceMember{
InstanceID: instanceID,
UserID: req.UserId,
Roles: req.Roles,
}
}
func UpdateIAMMemberToDomain(req *admin_pb.UpdateIAMMemberRequest) *domain.Member {
return &domain.Member{
UserID: req.UserId,
Roles: req.Roles,
func UpdateIAMMemberToCommand(req *admin_pb.UpdateIAMMemberRequest, instanceID string) *command.ChangeInstanceMember {
return &command.ChangeInstanceMember{
InstanceID: instanceID,
UserID: req.UserId,
Roles: req.Roles,
}
}
@@ -27,7 +27,7 @@ func TestAddIAMMemberToDomain(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := AddIAMMemberToDomain(tt.args.req)
got := AddIAMMemberToCommand(tt.args.req, "INSTANCE")
test.AssertFieldsMapped(t, got, "ObjectRoot")
})
}
@@ -53,7 +53,7 @@ func TestUpdateIAMMemberToDomain(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := UpdateIAMMemberToDomain(tt.args.req)
got := UpdateIAMMemberToCommand(tt.args.req, "INSTANCE")
test.AssertFieldsMapped(t, got, "ObjectRoot")
})
}
+4 -4
View File
@@ -1046,7 +1046,7 @@ func importOrg2(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataEr
if org.UserGrants != nil {
for _, grant := range org.GetUserGrants() {
logging.Debugf("import usergrant: %s", grant.GetProjectId()+"_"+grant.GetUserId())
_, err := s.command.AddUserGrant(ctx, management.AddUserGrantRequestToDomain(grant), org.GetOrgId())
_, err := s.command.AddUserGrant(ctx, management.AddUserGrantRequestToDomain(grant, org.GetOrgId()), nil)
if err != nil {
*errors = append(*errors, &admin_pb.ImportDataError{Type: "user_grant", Id: org.GetOrgId() + "_" + grant.GetProjectId() + "_" + grant.GetUserId(), Message: err.Error()})
if isCtxTimeout(ctx) {
@@ -1088,7 +1088,7 @@ func importOrgMembers(ctx context.Context, s *Server, errors *[]*admin_pb.Import
}
for _, member := range org.GetOrgMembers() {
logging.Debugf("import orgmember: %s", member.GetUserId())
_, err := s.command.AddOrgMember(ctx, org.GetOrgId(), member.GetUserId(), member.GetRoles()...)
_, err := s.command.AddOrgMember(ctx, management.AddOrgMemberRequestToCommand(member, org.GetOrgId()))
if err != nil {
*errors = append(*errors, &admin_pb.ImportDataError{Type: "org_member", Id: org.GetOrgId() + "_" + member.GetUserId(), Message: err.Error()})
if isCtxTimeout(ctx) {
@@ -1112,7 +1112,7 @@ func importProjectGrantMembers(ctx context.Context, s *Server, errors *[]*admin_
}
for _, member := range org.GetProjectGrantMembers() {
logging.Debugf("import projectgrantmember: %s", member.GetProjectId()+"_"+member.GetGrantId()+"_"+member.GetUserId())
_, err := s.command.AddProjectGrantMember(ctx, management.AddProjectGrantMemberRequestToDomain(member))
_, err := s.command.AddProjectGrantMember(ctx, management.AddProjectGrantMemberRequestToCommand(member, org.GetOrgId()))
if err != nil {
*errors = append(*errors, &admin_pb.ImportDataError{Type: "project_grant_member", Id: org.GetOrgId() + "_" + member.GetProjectId() + "_" + member.GetGrantId() + "_" + member.GetUserId(), Message: err.Error()})
if isCtxTimeout(ctx) {
@@ -1136,7 +1136,7 @@ func importProjectMembers(ctx context.Context, s *Server, errors *[]*admin_pb.Im
}
for _, member := range org.GetProjectMembers() {
logging.Debugf("import orgmember: %s", member.GetProjectId()+"_"+member.GetUserId())
_, err := s.command.AddProjectMember(ctx, management.AddProjectMemberRequestToDomain(member), org.GetOrgId())
_, err := s.command.AddProjectMember(ctx, management.AddProjectMemberRequestToCommand(member, org.GetOrgId()))
if err != nil {
*errors = append(*errors, &admin_pb.ImportDataError{Type: "project_member", Id: org.GetOrgId() + "_" + member.GetProjectId() + "_" + member.GetUserId(), Message: err.Error()})
if isCtxTimeout(ctx) {
@@ -457,7 +457,7 @@ func TestServer_ImportData(t *testing.T) {
{
Type: "project_grant_member",
Id: orgIDs[5] + "_" + projectIDs[4] + "_" + grantIDs[5] + "_" + userIDs[2],
Message: "ID=V3-DKcYh Message=Errors.Project.Member.AlreadyExists Parent=(ERROR: duplicate key value violates unique constraint \"unique_constraints_pkey\" (SQLSTATE 23505))",
Message: "ID=PROJECT-37fug Message=Errors.AlreadyExists",
},
},
Success: &admin.ImportDataSuccess{
+4 -4
View File
@@ -32,7 +32,7 @@ func (s *Server) RemoveMyUser(ctx context.Context, _ *auth_pb.RemoveMyUserReques
return nil, err
}
queries := &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantUserID}}
grants, err := s.query.UserGrants(ctx, queries, true)
grants, err := s.query.UserGrants(ctx, queries, true, nil)
if err != nil {
return nil, err
}
@@ -97,7 +97,7 @@ func (s *Server) ListMyMetadata(ctx context.Context, req *auth_pb.ListMyMetadata
if err != nil {
return nil, err
}
res, err := s.query.SearchUserMetadata(ctx, true, authz.GetCtxData(ctx).UserID, queries, false)
res, err := s.query.SearchUserMetadata(ctx, true, authz.GetCtxData(ctx).UserID, queries, nil)
if err != nil {
return nil, err
}
@@ -151,7 +151,7 @@ func (s *Server) ListMyUserGrants(ctx context.Context, req *auth_pb.ListMyUserGr
if err != nil {
return nil, err
}
res, err := s.query.UserGrants(ctx, queries, false)
res, err := s.query.UserGrants(ctx, queries, false, nil)
if err != nil {
return nil, err
}
@@ -180,7 +180,7 @@ func (s *Server) ListMyProjectOrgs(ctx context.Context, req *auth_pb.ListMyProje
return nil, err
}
grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantProjectID, userGrantUserID}}, false)
grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantProjectID, userGrantUserID}}, false, nil)
if err != nil {
return nil, err
}
@@ -0,0 +1,76 @@
package authorization
import (
"context"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta"
)
func (s *Server) CreateAuthorization(ctx context.Context, req *connect.Request[authorization.CreateAuthorizationRequest]) (*connect.Response[authorization.CreateAuthorizationResponse], error) {
grant := &domain.UserGrant{
UserID: req.Msg.UserId,
ProjectID: req.Msg.ProjectId,
RoleKeys: req.Msg.RoleKeys,
ObjectRoot: models.ObjectRoot{
ResourceOwner: req.Msg.GetOrganizationId(),
},
}
grant, err := s.command.AddUserGrant(ctx, grant, s.command.NewPermissionCheckUserGrantWrite(ctx))
if err != nil {
return nil, err
}
return connect.NewResponse(&authorization.CreateAuthorizationResponse{
Id: grant.AggregateID,
CreationDate: timestamppb.New(grant.ChangeDate),
}), nil
}
func (s *Server) UpdateAuthorization(ctx context.Context, request *connect.Request[authorization.UpdateAuthorizationRequest]) (*connect.Response[authorization.UpdateAuthorizationResponse], error) {
userGrant, err := s.command.ChangeUserGrant(ctx, &domain.UserGrant{
ObjectRoot: models.ObjectRoot{
AggregateID: request.Msg.Id,
},
RoleKeys: request.Msg.RoleKeys,
}, true, true, s.command.NewPermissionCheckUserGrantWrite(ctx))
if err != nil {
return nil, err
}
return connect.NewResponse(&authorization.UpdateAuthorizationResponse{
ChangeDate: timestamppb.New(userGrant.ChangeDate),
}), nil
}
func (s *Server) DeleteAuthorization(ctx context.Context, request *connect.Request[authorization.DeleteAuthorizationRequest]) (*connect.Response[authorization.DeleteAuthorizationResponse], error) {
details, err := s.command.RemoveUserGrant(ctx, request.Msg.Id, "", true, s.command.NewPermissionCheckUserGrantDelete(ctx))
if err != nil {
return nil, err
}
return connect.NewResponse(&authorization.DeleteAuthorizationResponse{
DeletionDate: timestamppb.New(details.EventDate),
}), nil
}
func (s *Server) ActivateAuthorization(ctx context.Context, request *connect.Request[authorization.ActivateAuthorizationRequest]) (*connect.Response[authorization.ActivateAuthorizationResponse], error) {
details, err := s.command.ReactivateUserGrant(ctx, request.Msg.Id, "", s.command.NewPermissionCheckUserGrantWrite(ctx))
if err != nil {
return nil, err
}
return connect.NewResponse(&authorization.ActivateAuthorizationResponse{
ChangeDate: timestamppb.New(details.EventDate),
}), nil
}
func (s *Server) DeactivateAuthorization(ctx context.Context, request *connect.Request[authorization.DeactivateAuthorizationRequest]) (*connect.Response[authorization.DeactivateAuthorizationResponse], error) {
details, err := s.command.DeactivateUserGrant(ctx, request.Msg.Id, "", s.command.NewPermissionCheckUserGrantWrite(ctx))
if err != nil {
return nil, err
}
return connect.NewResponse(&authorization.DeactivateAuthorizationResponse{
ChangeDate: timestamppb.New(details.EventDate),
}), nil
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,971 @@
//go:build integration
package authorization_test
import (
"context"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta"
filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta"
project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func TestServer_ListAuthorizations(t *testing.T) {
iamOwnerCtx := Instance.WithAuthorizationToken(EmptyCTX, integration.UserTypeIAMOwner)
projectOwnerResp := Instance.CreateMachineUser(iamOwnerCtx)
projectOwnerPatResp := Instance.CreatePersonalAccessToken(iamOwnerCtx, projectOwnerResp.GetUserId())
projectResp := createProject(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), false, false)
Instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), projectOwnerResp.GetUserId())
projectOwnerCtx := integration.WithAuthorizationToken(EmptyCTX, projectOwnerPatResp.Token)
projectGrantOwnerResp := Instance.CreateMachineUser(iamOwnerCtx)
projectGrantOwnerPatResp := Instance.CreatePersonalAccessToken(iamOwnerCtx, projectGrantOwnerResp.GetUserId())
grantedProjectResp := createGrantedProject(iamOwnerCtx, Instance, t, projectResp)
Instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), grantedProjectResp.GetGrantedOrganizationId(), projectGrantOwnerResp.GetUserId())
projectGrantOwnerCtx := integration.WithAuthorizationToken(EmptyCTX, projectGrantOwnerPatResp.Token)
type args struct {
ctx context.Context
dep func(*authorization.ListAuthorizationsRequest, *authorization.ListAuthorizationsResponse)
req *authorization.ListAuthorizationsRequest
}
tests := []struct {
name string
args args
want *authorization.ListAuthorizationsResponse
wantErr bool
}{
{
name: "list by user id, unauthenticated",
args: args{
ctx: EmptyCTX,
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{
UserId: &filter.IDFilter{
Id: userResp.GetId(),
},
}
createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false)
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
wantErr: true,
},
{
name: "list by id, no permission",
args: args{
ctx: Instance.WithAuthorizationToken(EmptyCTX, integration.UserTypeNoPermission),
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{
UserId: &filter.IDFilter{
Id: userResp.GetId(),
},
}
createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false)
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{},
},
},
{
name: "list, not found",
args: args{
ctx: iamOwnerCtx,
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{
{Filter: &authorization.AuthorizationsSearchFilter_UserId{
UserId: &filter.IDFilter{
Id: "notexisting",
},
},
},
},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 0,
AppliedLimit: 100,
},
},
},
{
name: "list single id, project",
args: args{
ctx: iamOwnerCtx,
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{
UserId: &filter.IDFilter{
Id: userResp.GetId(),
},
}
response.Authorizations[0] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false)
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{
{},
},
},
},
{
name: "list single id",
args: args{
ctx: iamOwnerCtx,
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
resp := createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false)
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_AuthorizationIds{
AuthorizationIds: &filter.InIDsFilter{
Ids: []string{resp.GetId()},
},
}
response.Authorizations[0] = resp
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{
{},
},
},
},
{
name: "list single project id",
args: args{
ctx: iamOwnerCtx,
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
resp := createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false)
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_ProjectId{
ProjectId: &filter.IDFilter{
Id: resp.GetProjectId(),
},
}
response.Authorizations[0] = resp
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{
{},
},
},
},
{
name: "list single project name",
args: args{
ctx: iamOwnerCtx,
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
resp := createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false)
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_ProjectName{
ProjectName: &authorization.ProjectNameQuery{
Name: resp.GetProjectName(),
Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS,
},
}
response.Authorizations[0] = resp
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{
{},
},
},
},
{
name: "list single id, project grant",
args: args{
ctx: iamOwnerCtx,
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{
UserId: &filter.IDFilter{
Id: userResp.GetId(),
},
}
response.Authorizations[0] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), true)
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{
{},
},
},
},
{
name: "list single grant id, project grant",
args: args{
ctx: iamOwnerCtx,
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
resp := createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), true)
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_ProjectGrantId{
ProjectGrantId: &filter.IDFilter{
Id: resp.GetProjectGrantId(),
},
}
response.Authorizations[0] = resp
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{
{},
},
},
},
{
name: "list single id, project and project grant, multiple",
args: args{
ctx: iamOwnerCtx,
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{
UserId: &filter.IDFilter{
Id: userResp.GetId(),
},
}
response.Authorizations[5] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false)
response.Authorizations[4] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false)
response.Authorizations[3] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false)
response.Authorizations[2] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), true)
response.Authorizations[1] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), true)
response.Authorizations[0] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), true)
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 6,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{
{}, {}, {}, {}, {}, {},
},
},
},
{
name: "list single id, project and project grant, org owner",
args: args{
ctx: Instance.WithAuthorizationToken(EmptyCTX, integration.UserTypeOrgOwner),
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{
UserId: &filter.IDFilter{
Id: userResp.GetId(),
},
}
response.Authorizations[1] = createAuthorizationForProject(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId())
response.Authorizations[0] = createAuthorizationWithProjectGrant(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId())
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 2,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{
{}, {},
},
},
},
{
name: "list single id, project and project grant, project owner",
args: args{
ctx: projectOwnerCtx,
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{
UserId: &filter.IDFilter{
Id: userResp.GetId(),
},
}
response.Authorizations[0] = createAuthorizationForProject(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId())
createAuthorizationWithProjectGrant(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId())
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 2,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{
{},
},
},
},
{
name: "list single id, project and project grant, project grant owner",
args: args{
ctx: projectGrantOwnerCtx,
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{
UserId: &filter.IDFilter{
Id: userResp.GetId(),
},
}
createAuthorizationForProject(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId())
response.Authorizations[0] = createAuthorizationForProjectGrant(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId(), grantedProjectResp.GetGrantedOrganizationId())
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 2,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{
{},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.dep != nil {
tt.args.dep(tt.args.req, tt.want)
}
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, listErr := Instance.Client.AuthorizationV2Beta.ListAuthorizations(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(ttt, listErr)
return
}
require.NoError(ttt, listErr)
// always first check length, otherwise its failed anyway
if assert.Len(ttt, got.Authorizations, len(tt.want.Authorizations)) {
for i := range tt.want.Authorizations {
assert.EqualExportedValues(ttt, tt.want.Authorizations[i], got.Authorizations[i])
}
}
assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination)
}, retryDuration, tick, "timeout waiting for expected execution result")
})
}
}
func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationResponse, actual *filter.PaginationResponse) {
assert.Equal(t, expected.AppliedLimit, actual.AppliedLimit)
assert.Equal(t, expected.TotalResult, actual.TotalResult)
}
func createAuthorization(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, userID string, grant bool) *authorization.Authorization {
projectName := gofakeit.AppName()
projectResp := instance.CreateProject(ctx, t, orgID, projectName, false, false)
if grant {
return createAuthorizationWithProjectGrant(ctx, instance, t, orgID, userID, projectName, projectResp.GetId())
}
return createAuthorizationForProject(ctx, instance, t, orgID, userID, projectName, projectResp.GetId())
}
func createAuthorizationForProject(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, userID, projectName, projectID string) *authorization.Authorization {
userResp, err := instance.Client.UserV2.GetUserByID(ctx, &user.GetUserByIDRequest{UserId: userID})
require.NoError(t, err)
userGrantResp := instance.CreateProjectUserGrant(t, ctx, projectID, userID)
return &authorization.Authorization{
Id: userGrantResp.GetUserGrantId(),
ProjectId: projectID,
ProjectName: projectName,
ProjectOrganizationId: orgID,
OrganizationId: orgID,
CreationDate: userGrantResp.Details.GetCreationDate(),
ChangeDate: userGrantResp.Details.GetCreationDate(),
State: 1,
User: &authorization.User{
Id: userID,
PreferredLoginName: userResp.User.GetPreferredLoginName(),
DisplayName: userResp.User.GetHuman().GetProfile().GetDisplayName(),
AvatarUrl: userResp.User.GetHuman().GetProfile().GetAvatarUrl(),
OrganizationId: userResp.GetUser().GetDetails().GetResourceOwner(),
},
}
}
func createAuthorizationWithProjectGrant(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, userID, projectName, projectID string) *authorization.Authorization {
grantedOrgName := gofakeit.Company() + integration.RandString(10)
grantedOrg := instance.CreateOrganization(ctx, grantedOrgName, gofakeit.Email())
instance.CreateProjectGrant(ctx, t, projectID, grantedOrg.GetOrganizationId())
return createAuthorizationForProjectGrant(ctx, instance, t, orgID, userID, projectName, projectID, grantedOrg.GetOrganizationId())
}
func createAuthorizationForProjectGrant(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, userID, projectName, projectID, grantedOrgID string) *authorization.Authorization {
userResp, err := instance.Client.UserV2.GetUserByID(ctx, &user.GetUserByIDRequest{UserId: userID})
require.NoError(t, err)
userGrantResp := instance.CreateProjectGrantUserGrant(ctx, orgID, projectID, grantedOrgID, userID)
return &authorization.Authorization{
Id: userGrantResp.GetUserGrantId(),
ProjectId: projectID,
ProjectName: projectName,
ProjectOrganizationId: orgID,
ProjectGrantId: gu.Ptr(grantedOrgID),
GrantedOrganizationId: gu.Ptr(grantedOrgID),
OrganizationId: orgID,
CreationDate: userGrantResp.Details.GetCreationDate(),
ChangeDate: userGrantResp.Details.GetCreationDate(),
State: 1,
User: &authorization.User{
Id: userID,
PreferredLoginName: userResp.User.GetPreferredLoginName(),
DisplayName: userResp.User.GetHuman().GetProfile().GetDisplayName(),
AvatarUrl: userResp.User.GetHuman().GetProfile().GetAvatarUrl(),
OrganizationId: userResp.GetUser().GetDetails().GetResourceOwner(),
},
}
}
func createProject(ctx context.Context, instance *integration.Instance, t *testing.T, orgID string, projectRoleCheck, hasProjectCheck bool) *project.Project {
name := gofakeit.AppName()
resp := instance.CreateProject(ctx, t, orgID, name, projectRoleCheck, hasProjectCheck)
return &project.Project{
Id: resp.GetId(),
Name: name,
OrganizationId: orgID,
CreationDate: resp.GetCreationDate(),
ChangeDate: resp.GetCreationDate(),
State: 1,
ProjectRoleAssertion: false,
ProjectAccessRequired: hasProjectCheck,
AuthorizationRequired: projectRoleCheck,
PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED,
}
}
func createGrantedProject(ctx context.Context, instance *integration.Instance, t *testing.T, projectToGrant *project.Project) *project.Project {
grantedOrgName := gofakeit.AppName()
grantedOrg := instance.CreateOrganization(ctx, grantedOrgName, gofakeit.Email())
projectGrantResp := instance.CreateProjectGrant(ctx, t, projectToGrant.GetId(), grantedOrg.GetOrganizationId())
return &project.Project{
Id: projectToGrant.GetId(),
Name: projectToGrant.GetName(),
OrganizationId: projectToGrant.GetOrganizationId(),
CreationDate: projectGrantResp.GetCreationDate(),
ChangeDate: projectGrantResp.GetCreationDate(),
State: 1,
ProjectRoleAssertion: false,
ProjectAccessRequired: projectToGrant.GetProjectAccessRequired(),
AuthorizationRequired: projectToGrant.GetAuthorizationRequired(),
PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED,
GrantedOrganizationId: gu.Ptr(grantedOrg.GetOrganizationId()),
GrantedOrganizationName: gu.Ptr(grantedOrgName),
GrantedState: 1,
}
}
func TestServer_ListAuthorizations_PermissionsV2(t *testing.T) {
ensureFeaturePermissionV2Enabled(t, InstancePermissionV2)
iamOwnerCtx := InstancePermissionV2.WithAuthorizationToken(EmptyCTX, integration.UserTypeIAMOwner)
projectOwnerResp := InstancePermissionV2.CreateMachineUser(iamOwnerCtx)
projectOwnerPatResp := InstancePermissionV2.CreatePersonalAccessToken(iamOwnerCtx, projectOwnerResp.GetUserId())
projectResp := createProject(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), false, false)
InstancePermissionV2.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), projectOwnerResp.GetUserId())
projectOwnerCtx := integration.WithAuthorizationToken(EmptyCTX, projectOwnerPatResp.Token)
//projectGrantOwnerResp := InstancePermissionV2.CreateMachineUser(iamOwnerCtx)
//projectGrantOwnerPatResp := InstancePermissionV2.CreatePersonalAccessToken(iamOwnerCtx, projectGrantOwnerResp.GetUserId())
grantedProjectResp := createGrantedProject(iamOwnerCtx, InstancePermissionV2, t, projectResp)
//InstancePermissionV2.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), grantedProjectResp.GetGrantedOrganizationId(), projectGrantOwnerResp.GetUserId())
//projectGrantOwnerCtx := integration.WithAuthorizationToken(EmptyCTX, projectGrantOwnerPatResp.Token)
type args struct {
ctx context.Context
dep func(*authorization.ListAuthorizationsRequest, *authorization.ListAuthorizationsResponse)
req *authorization.ListAuthorizationsRequest
}
tests := []struct {
name string
args args
want *authorization.ListAuthorizationsResponse
wantErr bool
}{
{
name: "list by user id, unauthenticated",
args: args{
ctx: EmptyCTX,
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{
UserId: &filter.IDFilter{
Id: userResp.GetId(),
},
}
createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false)
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
wantErr: true,
},
{
name: "list by id, no permission",
args: args{
ctx: InstancePermissionV2.WithAuthorizationToken(EmptyCTX, integration.UserTypeNoPermission),
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{
UserId: &filter.IDFilter{
Id: userResp.GetId(),
},
}
createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false)
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 0,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{},
},
},
{
name: "list, not found",
args: args{
ctx: iamOwnerCtx,
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{
{Filter: &authorization.AuthorizationsSearchFilter_UserId{
UserId: &filter.IDFilter{
Id: "notexisting",
},
},
},
},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 0,
AppliedLimit: 100,
},
},
},
{
name: "list single id, project",
args: args{
ctx: iamOwnerCtx,
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{
UserId: &filter.IDFilter{
Id: userResp.GetId(),
},
}
response.Authorizations[0] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false)
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{
{},
},
},
},
{
name: "list single id",
args: args{
ctx: iamOwnerCtx,
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
resp := createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false)
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_AuthorizationIds{
AuthorizationIds: &filter.InIDsFilter{
Ids: []string{resp.GetId()},
},
}
response.Authorizations[0] = resp
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{
{},
},
},
},
{
name: "list single project id",
args: args{
ctx: iamOwnerCtx,
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
resp := createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false)
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_ProjectId{
ProjectId: &filter.IDFilter{
Id: resp.GetProjectId(),
},
}
response.Authorizations[0] = resp
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{
{},
},
},
},
{
name: "list single project name",
args: args{
ctx: iamOwnerCtx,
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
resp := createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false)
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_ProjectName{
ProjectName: &authorization.ProjectNameQuery{
Name: resp.GetProjectName(),
Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS,
},
}
response.Authorizations[0] = resp
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{
{},
},
},
},
{
name: "list single id, project grant",
args: args{
ctx: iamOwnerCtx,
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{
UserId: &filter.IDFilter{
Id: userResp.GetId(),
},
}
response.Authorizations[0] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), true)
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{
{},
},
},
},
{
name: "list single grant id, project grant",
args: args{
ctx: iamOwnerCtx,
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
resp := createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), true)
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_ProjectGrantId{
ProjectGrantId: &filter.IDFilter{
Id: resp.GetProjectGrantId(),
},
}
response.Authorizations[0] = resp
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{
{},
},
},
},
{
name: "list single id, project and project grant, multiple",
args: args{
ctx: iamOwnerCtx,
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{
UserId: &filter.IDFilter{
Id: userResp.GetId(),
},
}
response.Authorizations[5] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false)
response.Authorizations[4] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false)
response.Authorizations[3] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false)
response.Authorizations[2] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), true)
response.Authorizations[1] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), true)
response.Authorizations[0] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), true)
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 6,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{
{}, {}, {}, {}, {}, {},
},
},
},
{
name: "list single id, project and project grant, org owner",
args: args{
ctx: InstancePermissionV2.WithAuthorizationToken(EmptyCTX, integration.UserTypeOrgOwner),
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{
UserId: &filter.IDFilter{
Id: userResp.GetId(),
},
}
response.Authorizations[1] = createAuthorizationForProject(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId())
response.Authorizations[0] = createAuthorizationWithProjectGrant(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId())
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 2,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{
{}, {},
},
},
},
{
name: "list single id, project and project grant, project owner",
args: args{
ctx: projectOwnerCtx,
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{
UserId: &filter.IDFilter{
Id: userResp.GetId(),
},
}
response.Authorizations[1] = createAuthorizationForProject(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId())
response.Authorizations[0] = createAuthorizationWithProjectGrant(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId())
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 2,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{
{}, {},
},
},
},
/*
TODO: correct when permission check is added for project grants https://github.com/zitadel/zitadel/issues/9972
{
name: "list single id, project and project grant, project grant owner",
args: args{
ctx: projectGrantOwnerCtx,
dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) {
userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email())
request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{
UserId: &filter.IDFilter{
Id: userResp.GetId(),
},
}
createAuthorizationForProject(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId())
response.Authorizations[0] = createAuthorizationForProjectGrant(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId())
},
req: &authorization.ListAuthorizationsRequest{
Filters: []*authorization.AuthorizationsSearchFilter{{}},
},
},
want: &authorization.ListAuthorizationsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 2,
AppliedLimit: 100,
},
Authorizations: []*authorization.Authorization{
{},
},
},
},
*/
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.dep != nil {
tt.args.dep(tt.args.req, tt.want)
}
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, listErr := InstancePermissionV2.Client.AuthorizationV2Beta.ListAuthorizations(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(ttt, listErr)
return
}
require.NoError(ttt, listErr)
// always first check length, otherwise its failed anyway
if assert.Len(ttt, got.Authorizations, len(tt.want.Authorizations)) {
for i := range tt.want.Authorizations {
assert.EqualExportedValues(ttt, tt.want.Authorizations[i], got.Authorizations[i])
}
}
assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination)
}, retryDuration, tick, "timeout waiting for expected execution result")
})
}
}
@@ -0,0 +1,65 @@
//go:build integration
package authorization_test
import (
"context"
"os"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
)
var (
EmptyCTX context.Context
IAMCTX context.Context
Instance *integration.Instance
InstancePermissionV2 *integration.Instance
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
EmptyCTX = ctx
Instance = integration.NewInstance(ctx)
IAMCTX = Instance.WithAuthorizationToken(ctx, integration.UserTypeIAMOwner)
InstancePermissionV2 = integration.NewInstance(ctx)
return m.Run()
}())
}
func ensureFeaturePermissionV2Enabled(t *testing.T, instance *integration.Instance) {
ctx := instance.WithAuthorizationToken(EmptyCTX, integration.UserTypeIAMOwner)
f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{
Inheritance: true,
})
require.NoError(t, err)
if f.PermissionCheckV2.GetEnabled() {
return
}
_, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{
PermissionCheckV2: gu.Ptr(true),
})
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute)
require.EventuallyWithT(t,
func(ttt *assert.CollectT) {
f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{
Inheritance: true,
})
assert.NoError(ttt, err)
if f.PermissionCheckV2.GetEnabled() {
return
}
},
retryDuration,
tick,
"timed out waiting for ensuring instance feature")
}
@@ -0,0 +1,208 @@
package authorization
import (
"context"
"errors"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta"
filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta"
)
func (s *Server) ListAuthorizations(ctx context.Context, req *connect.Request[authorization.ListAuthorizationsRequest]) (*connect.Response[authorization.ListAuthorizationsResponse], error) {
queries, err := s.listAuthorizationsRequestToModel(req.Msg)
if err != nil {
return nil, err
}
resp, err := s.query.UserGrants(ctx, queries, false, s.checkPermission)
if err != nil {
return nil, err
}
return connect.NewResponse(&authorization.ListAuthorizationsResponse{
Authorizations: userGrantsToPb(resp.UserGrants),
Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse),
}), nil
}
func (s *Server) listAuthorizationsRequestToModel(req *authorization.ListAuthorizationsRequest) (*query.UserGrantsQueries, error) {
offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination)
if err != nil {
return nil, err
}
queries, err := AuthorizationQueriesToQuery(req.Filters)
if err != nil {
return nil, err
}
return &query.UserGrantsQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: authorizationFieldNameToSortingColumn(req.GetSortingColumn()),
},
Queries: queries,
}, nil
}
func authorizationFieldNameToSortingColumn(field authorization.AuthorizationFieldName) query.Column {
switch field {
case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_UNSPECIFIED:
return query.UserGrantCreationDate
case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_CREATED_DATE:
return query.UserGrantCreationDate
case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_CHANGED_DATE:
return query.UserGrantChangeDate
case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_ID:
return query.UserGrantID
case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_USER_ID:
return query.UserGrantUserID
case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_PROJECT_ID:
return query.UserGrantProjectID
case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_ORGANIZATION_ID:
return query.UserGrantResourceOwner
case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_USER_ORGANIZATION_ID:
return query.UserResourceOwnerCol
default:
return query.UserGrantCreationDate
}
}
func AuthorizationQueriesToQuery(queries []*authorization.AuthorizationsSearchFilter) (q []query.SearchQuery, err error) {
q = make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = AuthorizationSearchFilterToQuery(query)
if err != nil {
return nil, err
}
}
return q, nil
}
func AuthorizationSearchFilterToQuery(query *authorization.AuthorizationsSearchFilter) (query.SearchQuery, error) {
switch q := query.Filter.(type) {
case *authorization.AuthorizationsSearchFilter_AuthorizationIds:
return AuthorizationIDQueryToModel(q.AuthorizationIds)
case *authorization.AuthorizationsSearchFilter_OrganizationId:
return AuthorizationOrganizationIDQueryToModel(q.OrganizationId)
case *authorization.AuthorizationsSearchFilter_State:
return AuthorizationStateQueryToModel(q.State)
case *authorization.AuthorizationsSearchFilter_UserId:
return AuthorizationUserUserIDQueryToModel(q.UserId)
case *authorization.AuthorizationsSearchFilter_UserOrganizationId:
return AuthorizationUserOrganizationIDQueryToModel(q.UserOrganizationId)
case *authorization.AuthorizationsSearchFilter_UserPreferredLoginName:
return AuthorizationUserNameQueryToModel(q.UserPreferredLoginName)
case *authorization.AuthorizationsSearchFilter_UserDisplayName:
return AuthorizationDisplayNameQueryToModel(q.UserDisplayName)
case *authorization.AuthorizationsSearchFilter_ProjectId:
return AuthorizationProjectIDQueryToModel(q.ProjectId)
case *authorization.AuthorizationsSearchFilter_ProjectName:
return AuthorizationProjectNameQueryToModel(q.ProjectName)
case *authorization.AuthorizationsSearchFilter_RoleKey:
return AuthorizationRoleKeyQueryToModel(q.RoleKey)
case *authorization.AuthorizationsSearchFilter_ProjectGrantId:
return AuthorizationProjectGrantIDQueryToModel(q.ProjectGrantId)
default:
return nil, errors.New("invalid query")
}
}
func AuthorizationIDQueryToModel(q *filter_pb.InIDsFilter) (query.SearchQuery, error) {
return query.NewUserGrantInIDsSearchQuery(q.Ids)
}
func AuthorizationDisplayNameQueryToModel(q *authorization.UserDisplayNameQuery) (query.SearchQuery, error) {
return query.NewUserGrantDisplayNameQuery(q.DisplayName, filter.TextMethodPbToQuery(q.Method))
}
func AuthorizationOrganizationIDQueryToModel(q *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewUserGrantResourceOwnerSearchQuery(q.Id)
}
func AuthorizationProjectIDQueryToModel(q *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewUserGrantProjectIDSearchQuery(q.Id)
}
func AuthorizationProjectNameQueryToModel(q *authorization.ProjectNameQuery) (query.SearchQuery, error) {
return query.NewUserGrantProjectNameQuery(q.Name, filter.TextMethodPbToQuery(q.Method))
}
func AuthorizationProjectGrantIDQueryToModel(q *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewUserGrantGrantIDSearchQuery(q.Id)
}
func AuthorizationRoleKeyQueryToModel(q *authorization.RoleKeyQuery) (query.SearchQuery, error) {
return query.NewUserGrantRoleQuery(q.Key)
}
func AuthorizationUserNameQueryToModel(q *authorization.UserPreferredLoginNameQuery) (query.SearchQuery, error) {
return query.NewUserGrantUsernameQuery(q.LoginName, filter.TextMethodPbToQuery(q.Method))
}
func AuthorizationUserUserIDQueryToModel(q *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewUserGrantUserIDSearchQuery(q.Id)
}
func AuthorizationUserOrganizationIDQueryToModel(q *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewUserGrantUserResourceOwnerSearchQuery(q.Id)
}
func AuthorizationStateQueryToModel(q *authorization.StateQuery) (query.SearchQuery, error) {
return query.NewUserGrantStateQuery(domain.UserGrantState(q.State))
}
func userGrantsToPb(userGrants []*query.UserGrant) []*authorization.Authorization {
o := make([]*authorization.Authorization, len(userGrants))
for i, grant := range userGrants {
o[i] = userGrantToPb(grant)
}
return o
}
func userGrantToPb(userGrant *query.UserGrant) *authorization.Authorization {
var grantID, grantedOrgID *string
if userGrant.GrantID != "" {
grantID = &userGrant.GrantID
}
if userGrant.GrantedOrgID != "" {
grantedOrgID = &userGrant.GrantedOrgID
}
return &authorization.Authorization{
Id: userGrant.ID,
ProjectId: userGrant.ProjectID,
ProjectName: userGrant.ProjectName,
ProjectOrganizationId: userGrant.ProjectResourceOwner,
ProjectGrantId: grantID,
GrantedOrganizationId: grantedOrgID,
OrganizationId: userGrant.ResourceOwner,
CreationDate: timestamppb.New(userGrant.CreationDate),
ChangeDate: timestamppb.New(userGrant.ChangeDate),
State: userGrantStateToPb(userGrant.State),
User: &authorization.User{
Id: userGrant.UserID,
PreferredLoginName: userGrant.PreferredLoginName,
DisplayName: userGrant.DisplayName,
AvatarUrl: userGrant.AvatarURL,
OrganizationId: userGrant.UserResourceOwner,
},
Roles: userGrant.Roles,
}
}
func userGrantStateToPb(state domain.UserGrantState) authorization.State {
switch state {
case domain.UserGrantStateActive:
return authorization.State_STATE_ACTIVE
case domain.UserGrantStateInactive:
return authorization.State_STATE_INACTIVE
case domain.UserGrantStateUnspecified, domain.UserGrantStateRemoved:
return authorization.State_STATE_UNSPECIFIED
default:
return authorization.State_STATE_UNSPECIFIED
}
}
@@ -0,0 +1,67 @@
package authorization
import (
"net/http"
"connectrpc.com/connect"
"google.golang.org/protobuf/reflect/protoreflect"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta/authorizationconnect"
)
var _ authorizationconnect.AuthorizationServiceHandler = (*Server)(nil)
type Server struct {
systemDefaults systemdefaults.SystemDefaults
command *command.Commands
query *query.Queries
checkPermission domain.PermissionCheck
}
type Config struct{}
func CreateServer(
systemDefaults systemdefaults.SystemDefaults,
command *command.Commands,
query *query.Queries,
checkPermission domain.PermissionCheck,
) *Server {
return &Server{
systemDefaults: systemDefaults,
command: command,
query: query,
checkPermission: checkPermission,
}
}
func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) {
return authorizationconnect.NewAuthorizationServiceHandler(s, connect.WithInterceptors(interceptors...))
}
func (s *Server) FileDescriptor() protoreflect.FileDescriptor {
return authorization.File_zitadel_authorization_v2beta_authorization_service_proto
}
func (s *Server) AppName() string {
return authorization.AuthorizationService_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return authorization.AuthorizationService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return authorization.AuthorizationService_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return authorization.RegisterAuthorizationServiceHandler
}
+23 -23
View File
@@ -9,6 +9,29 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/filter/v2"
)
func TextMethodPbToQuery(method filter.TextFilterMethod) query.TextComparison {
switch method {
case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS:
return query.TextEquals
case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE:
return query.TextEqualsIgnoreCase
case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH:
return query.TextStartsWith
case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE:
return query.TextStartsWithIgnoreCase
case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS:
return query.TextContains
case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE:
return query.TextContainsIgnoreCase
case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH:
return query.TextEndsWith
case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE:
return query.TextEndsWithIgnoreCase
default:
return -1
}
}
func TimestampMethodPbToQuery(method filter.TimestampFilterMethod) query.TimestampComparison {
switch method {
case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_EQUALS:
@@ -48,26 +71,3 @@ func QueryToPaginationPb(request query.SearchRequest, response query.SearchRespo
TotalResult: response.Count,
}
}
func TextMethodPbToQuery(method filter.TextFilterMethod) query.TextComparison {
switch method {
case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS:
return query.TextEquals
case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE:
return query.TextEqualsIgnoreCase
case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH:
return query.TextStartsWith
case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE:
return query.TextStartsWithIgnoreCase
case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS:
return query.TextContains
case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE:
return query.TextContainsIgnoreCase
case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH:
return query.TextEndsWith
case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE:
return query.TextEndsWithIgnoreCase
default:
return -1
}
}
@@ -32,6 +32,23 @@ func TextMethodPbToQuery(method filter.TextFilterMethod) query.TextComparison {
}
}
func TimestampMethodPbToQuery(method filter.TimestampFilterMethod) query.TimestampComparison {
switch method {
case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_EQUALS:
return query.TimestampEquals
case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_LESS:
return query.TimestampLess
case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_GREATER:
return query.TimestampGreater
case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_LESS_OR_EQUALS:
return query.TimestampLessOrEquals
case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_GREATER_OR_EQUALS:
return query.TimestampGreaterOrEquals
default:
return -1
}
}
func PaginationPbToQuery(defaults systemdefaults.SystemDefaults, query *filter.PaginationRequest) (offset, limit uint64, asc bool, err error) {
limit = defaults.DefaultQueryLimit
if query == nil {
@@ -0,0 +1,220 @@
package internal_permission
import (
"context"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/zerrors"
internal_permission "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta"
)
func (s *Server) CreateAdministrator(ctx context.Context, req *connect.Request[internal_permission.CreateAdministratorRequest]) (*connect.Response[internal_permission.CreateAdministratorResponse], error) {
var creationDate *timestamppb.Timestamp
switch resource := req.Msg.GetResource().GetResource().(type) {
case *internal_permission.ResourceType_Instance:
if resource.Instance {
member, err := s.command.AddInstanceMember(ctx, createAdministratorInstanceToCommand(authz.GetInstance(ctx).InstanceID(), req.Msg.UserId, req.Msg.Roles))
if err != nil {
return nil, err
}
if !member.EventDate.IsZero() {
creationDate = timestamppb.New(member.EventDate)
}
}
case *internal_permission.ResourceType_OrganizationId:
member, err := s.command.AddOrgMember(ctx, createAdministratorOrganizationToCommand(resource, req.Msg.UserId, req.Msg.Roles))
if err != nil {
return nil, err
}
if !member.EventDate.IsZero() {
creationDate = timestamppb.New(member.EventDate)
}
case *internal_permission.ResourceType_ProjectId:
member, err := s.command.AddProjectMember(ctx, createAdministratorProjectToCommand(resource, req.Msg.UserId, req.Msg.Roles))
if err != nil {
return nil, err
}
if !member.EventDate.IsZero() {
creationDate = timestamppb.New(member.EventDate)
}
case *internal_permission.ResourceType_ProjectGrant_:
member, err := s.command.AddProjectGrantMember(ctx, createAdministratorProjectGrantToCommand(resource, req.Msg.UserId, req.Msg.Roles))
if err != nil {
return nil, err
}
if !member.EventDate.IsZero() {
creationDate = timestamppb.New(member.EventDate)
}
default:
return nil, zerrors.ThrowInvalidArgument(nil, "ADMIN-IbPp47HDP5", "Errors.Invalid.Argument")
}
return connect.NewResponse(&internal_permission.CreateAdministratorResponse{
CreationDate: creationDate,
}), nil
}
func createAdministratorInstanceToCommand(instanceID, userID string, roles []string) *command.AddInstanceMember {
return &command.AddInstanceMember{
InstanceID: instanceID,
UserID: userID,
Roles: roles,
}
}
func createAdministratorOrganizationToCommand(req *internal_permission.ResourceType_OrganizationId, userID string, roles []string) *command.AddOrgMember {
return &command.AddOrgMember{
OrgID: req.OrganizationId,
UserID: userID,
Roles: roles,
}
}
func createAdministratorProjectToCommand(req *internal_permission.ResourceType_ProjectId, userID string, roles []string) *command.AddProjectMember {
return &command.AddProjectMember{
ProjectID: req.ProjectId,
UserID: userID,
Roles: roles,
}
}
func createAdministratorProjectGrantToCommand(req *internal_permission.ResourceType_ProjectGrant_, userID string, roles []string) *command.AddProjectGrantMember {
return &command.AddProjectGrantMember{
GrantID: req.ProjectGrant.ProjectGrantId,
ProjectID: req.ProjectGrant.ProjectId,
UserID: userID,
Roles: roles,
}
}
func (s *Server) UpdateAdministrator(ctx context.Context, req *connect.Request[internal_permission.UpdateAdministratorRequest]) (*connect.Response[internal_permission.UpdateAdministratorResponse], error) {
var changeDate *timestamppb.Timestamp
switch resource := req.Msg.GetResource().GetResource().(type) {
case *internal_permission.ResourceType_Instance:
if resource.Instance {
member, err := s.command.ChangeInstanceMember(ctx, updateAdministratorInstanceToCommand(authz.GetInstance(ctx).InstanceID(), req.Msg.UserId, req.Msg.Roles))
if err != nil {
return nil, err
}
if !member.EventDate.IsZero() {
changeDate = timestamppb.New(member.EventDate)
}
}
case *internal_permission.ResourceType_OrganizationId:
member, err := s.command.ChangeOrgMember(ctx, updateAdministratorOrganizationToCommand(resource, req.Msg.UserId, req.Msg.Roles))
if err != nil {
return nil, err
}
if !member.EventDate.IsZero() {
changeDate = timestamppb.New(member.EventDate)
}
case *internal_permission.ResourceType_ProjectId:
member, err := s.command.ChangeProjectMember(ctx, updateAdministratorProjectToCommand(resource, req.Msg.UserId, req.Msg.Roles))
if err != nil {
return nil, err
}
if !member.EventDate.IsZero() {
changeDate = timestamppb.New(member.EventDate)
}
case *internal_permission.ResourceType_ProjectGrant_:
member, err := s.command.ChangeProjectGrantMember(ctx, updateAdministratorProjectGrantToCommand(resource, req.Msg.UserId, req.Msg.Roles))
if err != nil {
return nil, err
}
if !member.EventDate.IsZero() {
changeDate = timestamppb.New(member.EventDate)
}
default:
return nil, zerrors.ThrowInvalidArgument(nil, "ADMIN-i0V2IbdloZ", "Errors.Invalid.Argument")
}
return connect.NewResponse(&internal_permission.UpdateAdministratorResponse{
ChangeDate: changeDate,
}), nil
}
func updateAdministratorInstanceToCommand(instanceID, userID string, roles []string) *command.ChangeInstanceMember {
return &command.ChangeInstanceMember{
InstanceID: instanceID,
UserID: userID,
Roles: roles,
}
}
func updateAdministratorOrganizationToCommand(req *internal_permission.ResourceType_OrganizationId, userID string, roles []string) *command.ChangeOrgMember {
return &command.ChangeOrgMember{
OrgID: req.OrganizationId,
UserID: userID,
Roles: roles,
}
}
func updateAdministratorProjectToCommand(req *internal_permission.ResourceType_ProjectId, userID string, roles []string) *command.ChangeProjectMember {
return &command.ChangeProjectMember{
ProjectID: req.ProjectId,
UserID: userID,
Roles: roles,
}
}
func updateAdministratorProjectGrantToCommand(req *internal_permission.ResourceType_ProjectGrant_, userID string, roles []string) *command.ChangeProjectGrantMember {
return &command.ChangeProjectGrantMember{
GrantID: req.ProjectGrant.ProjectGrantId,
ProjectID: req.ProjectGrant.ProjectId,
UserID: userID,
Roles: roles,
}
}
func (s *Server) DeleteAdministrator(ctx context.Context, req *connect.Request[internal_permission.DeleteAdministratorRequest]) (*connect.Response[internal_permission.DeleteAdministratorResponse], error) {
var deletionDate *timestamppb.Timestamp
switch resource := req.Msg.GetResource().GetResource().(type) {
case *internal_permission.ResourceType_Instance:
if resource.Instance {
member, err := s.command.RemoveInstanceMember(ctx, authz.GetInstance(ctx).InstanceID(), req.Msg.UserId)
if err != nil {
return nil, err
}
if !member.EventDate.IsZero() {
deletionDate = timestamppb.New(member.EventDate)
}
}
case *internal_permission.ResourceType_OrganizationId:
member, err := s.command.RemoveOrgMember(ctx, resource.OrganizationId, req.Msg.UserId)
if err != nil {
return nil, err
}
if !member.EventDate.IsZero() {
deletionDate = timestamppb.New(member.EventDate)
}
case *internal_permission.ResourceType_ProjectId:
member, err := s.command.RemoveProjectMember(ctx, resource.ProjectId, req.Msg.UserId, "")
if err != nil {
return nil, err
}
if !member.EventDate.IsZero() {
deletionDate = timestamppb.New(member.EventDate)
}
case *internal_permission.ResourceType_ProjectGrant_:
member, err := s.command.RemoveProjectGrantMember(ctx, resource.ProjectGrant.ProjectId, req.Msg.UserId, resource.ProjectGrant.ProjectGrantId)
if err != nil {
return nil, err
}
if !member.EventDate.IsZero() {
deletionDate = timestamppb.New(member.EventDate)
}
default:
return nil, zerrors.ThrowInvalidArgument(nil, "ADMIN-3UOjLtuohh", "Errors.Invalid.Argument")
}
return connect.NewResponse(&internal_permission.DeleteAdministratorResponse{
DeletionDate: deletionDate,
}), nil
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,63 @@
//go:build integration
package project_test
import (
"context"
"os"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
)
var (
CTX context.Context
instance *integration.Instance
instancePermissionV2 *integration.Instance
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
CTX = ctx
instance = integration.NewInstance(ctx)
instancePermissionV2 = integration.NewInstance(CTX)
return m.Run()
}())
}
func ensureFeaturePermissionV2Enabled(t *testing.T, instance *integration.Instance) {
ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{
Inheritance: true,
})
require.NoError(t, err)
if f.PermissionCheckV2.GetEnabled() {
return
}
_, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{
PermissionCheckV2: gu.Ptr(true),
})
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute)
require.EventuallyWithT(t,
func(ttt *assert.CollectT) {
f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{
Inheritance: true,
})
assert.NoError(ttt, err)
if f.PermissionCheckV2.GetEnabled() {
return
}
},
retryDuration,
tick,
"timed out waiting for ensuring instance feature")
}
@@ -0,0 +1,192 @@
package internal_permission
import (
"context"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta"
internal_permission "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta"
)
func (s *Server) ListAdministrators(ctx context.Context, req *connect.Request[internal_permission.ListAdministratorsRequest]) (*connect.Response[internal_permission.ListAdministratorsResponse], error) {
queries, err := s.listAdministratorsRequestToModel(req.Msg)
if err != nil {
return nil, err
}
resp, err := s.query.SearchAdministrators(ctx, queries, s.checkPermission)
if err != nil {
return nil, err
}
return connect.NewResponse(&internal_permission.ListAdministratorsResponse{
Administrators: administratorsToPb(resp.Administrators),
Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse),
}), nil
}
func (s *Server) listAdministratorsRequestToModel(req *internal_permission.ListAdministratorsRequest) (*query.MembershipSearchQuery, error) {
offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination)
if err != nil {
return nil, err
}
queries, err := administratorSearchFiltersToQuery(req.Filters)
if err != nil {
return nil, err
}
return &query.MembershipSearchQuery{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: administratorFieldNameToSortingColumn(req.GetSortingColumn()),
},
Queries: queries,
}, nil
}
func administratorFieldNameToSortingColumn(field internal_permission.AdministratorFieldName) query.Column {
switch field {
case internal_permission.AdministratorFieldName_ADMINISTRATOR_FIELD_NAME_CREATION_DATE:
return query.MembershipCreationDate
case internal_permission.AdministratorFieldName_ADMINISTRATOR_FIELD_NAME_USER_ID:
return query.MembershipUserID
case internal_permission.AdministratorFieldName_ADMINISTRATOR_FIELD_NAME_CHANGE_DATE:
return query.MembershipChangeDate
case internal_permission.AdministratorFieldName_ADMINISTRATOR_FIELD_NAME_UNSPECIFIED:
return query.MembershipCreationDate
default:
return query.MembershipCreationDate
}
}
func administratorSearchFiltersToQuery(queries []*internal_permission.AdministratorSearchFilter) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, qry := range queries {
q[i], err = administratorFilterToModel(qry)
if err != nil {
return nil, err
}
}
return q, nil
}
func administratorFilterToModel(filter *internal_permission.AdministratorSearchFilter) (query.SearchQuery, error) {
switch q := filter.Filter.(type) {
case *internal_permission.AdministratorSearchFilter_InUserIdsFilter:
return inUserIDsFilterToQuery(q.InUserIdsFilter)
case *internal_permission.AdministratorSearchFilter_CreationDate:
return creationDateFilterToQuery(q.CreationDate)
case *internal_permission.AdministratorSearchFilter_ChangeDate:
return changeDateFilterToQuery(q.ChangeDate)
case *internal_permission.AdministratorSearchFilter_UserOrganizationId:
return userResourceOwnerFilterToQuery(q.UserOrganizationId)
case *internal_permission.AdministratorSearchFilter_UserPreferredLoginName:
return userLoginNameFilterToQuery(q.UserPreferredLoginName)
case *internal_permission.AdministratorSearchFilter_UserDisplayName:
return userDisplayNameFilterToQuery(q.UserDisplayName)
case *internal_permission.AdministratorSearchFilter_Resource:
return resourceFilterToQuery(q.Resource)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-vR9nC", "List.Query.Invalid")
}
}
func inUserIDsFilterToQuery(q *filter_pb.InIDsFilter) (query.SearchQuery, error) {
return query.NewMemberInUserIDsSearchQuery(q.GetIds())
}
func userResourceOwnerFilterToQuery(q *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewAdministratorUserResourceOwnerSearchQuery(q.GetId())
}
func userLoginNameFilterToQuery(q *internal_permission.UserPreferredLoginNameFilter) (query.SearchQuery, error) {
return query.NewAdministratorUserLoginNameSearchQuery(q.GetPreferredLoginName())
}
func userDisplayNameFilterToQuery(q *internal_permission.UserDisplayNameFilter) (query.SearchQuery, error) {
return query.NewAdministratorUserDisplayNameSearchQuery(q.GetDisplayName())
}
func creationDateFilterToQuery(q *filter_pb.TimestampFilter) (query.SearchQuery, error) {
return query.NewMembershipCreationDateQuery(q.GetTimestamp().AsTime(), filter.TimestampMethodPbToQuery(q.Method))
}
func changeDateFilterToQuery(q *filter_pb.TimestampFilter) (query.SearchQuery, error) {
return query.NewMembershipChangeDateQuery(q.GetTimestamp().AsTime(), filter.TimestampMethodPbToQuery(q.Method))
}
func resourceFilterToQuery(q *internal_permission.ResourceFilter) (query.SearchQuery, error) {
switch q.GetResource().(type) {
case *internal_permission.ResourceFilter_Instance:
if q.GetInstance() {
return query.NewMembershipIsIAMQuery()
}
case *internal_permission.ResourceFilter_OrganizationId:
return query.NewMembershipOrgIDQuery(q.GetOrganizationId())
case *internal_permission.ResourceFilter_ProjectId:
return query.NewMembershipProjectIDQuery(q.GetProjectId())
case *internal_permission.ResourceFilter_ProjectGrantId:
return query.NewMembershipProjectGrantIDQuery(q.GetProjectGrantId())
}
return nil, nil
}
func administratorsToPb(administrators []*query.Administrator) []*internal_permission.Administrator {
a := make([]*internal_permission.Administrator, len(administrators))
for i, admin := range administrators {
a[i] = administratorToPb(admin)
}
return a
}
func administratorToPb(admin *query.Administrator) *internal_permission.Administrator {
var resource internal_permission.Resource
if admin.Instance != nil {
resource = &internal_permission.Administrator_Instance{Instance: true}
}
if admin.Org != nil {
resource = &internal_permission.Administrator_Organization{
Organization: &internal_permission.Organization{
Id: admin.Org.OrgID,
Name: admin.Org.Name,
},
}
}
if admin.Project != nil {
resource = &internal_permission.Administrator_Project{
Project: &internal_permission.Project{
Id: admin.Project.ProjectID,
Name: admin.Project.Name,
OrganizationId: admin.Project.ResourceOwner,
},
}
}
if admin.ProjectGrant != nil {
resource = &internal_permission.Administrator_ProjectGrant{
ProjectGrant: &internal_permission.ProjectGrant{
Id: admin.ProjectGrant.GrantID,
ProjectId: admin.ProjectGrant.ProjectID,
ProjectName: admin.ProjectGrant.ProjectName,
OrganizationId: admin.ProjectGrant.ResourceOwner,
GrantedOrganizationId: admin.ProjectGrant.GrantedOrgID,
},
}
}
return &internal_permission.Administrator{
CreationDate: timestamppb.New(admin.CreationDate),
ChangeDate: timestamppb.New(admin.ChangeDate),
User: &internal_permission.User{
Id: admin.User.UserID,
PreferredLoginName: admin.User.LoginName,
DisplayName: admin.User.DisplayName,
OrganizationId: admin.User.ResourceOwner,
},
Resource: resource,
Roles: admin.Roles,
}
}
@@ -0,0 +1,66 @@
package internal_permission
import (
"net/http"
"connectrpc.com/connect"
"google.golang.org/protobuf/reflect/protoreflect"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
internal_permission "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta/internal_permissionconnect"
)
var _ internal_permissionconnect.InternalPermissionServiceHandler = (*Server)(nil)
type Server struct {
systemDefaults systemdefaults.SystemDefaults
command *command.Commands
query *query.Queries
checkPermission domain.PermissionCheck
}
type Config struct{}
func CreateServer(
systemDefaults systemdefaults.SystemDefaults,
command *command.Commands,
query *query.Queries,
checkPermission domain.PermissionCheck,
) *Server {
return &Server{
systemDefaults: systemDefaults,
command: command,
query: query,
checkPermission: checkPermission,
}
}
func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) {
return internal_permissionconnect.NewInternalPermissionServiceHandler(s, connect.WithInterceptors(interceptors...))
}
func (s *Server) FileDescriptor() protoreflect.FileDescriptor {
return internal_permission.File_zitadel_internal_permission_v2beta_internal_permission_service_proto
}
func (s *Server) AppName() string {
return internal_permission.InternalPermissionService_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return internal_permission.InternalPermissionService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return internal_permission.InternalPermissionService_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return internal_permission.RegisterInternalPermissionServiceHandler
}
+4 -4
View File
@@ -278,28 +278,28 @@ func (s *Server) ListOrgMembers(ctx context.Context, req *mgmt_pb.ListOrgMembers
}
func (s *Server) AddOrgMember(ctx context.Context, req *mgmt_pb.AddOrgMemberRequest) (*mgmt_pb.AddOrgMemberResponse, error) {
addedMember, err := s.command.AddOrgMember(ctx, authz.GetCtxData(ctx).OrgID, req.UserId, req.Roles...)
addedMember, err := s.command.AddOrgMember(ctx, AddOrgMemberRequestToCommand(req, authz.GetCtxData(ctx).OrgID))
if err != nil {
return nil, err
}
return &mgmt_pb.AddOrgMemberResponse{
Details: object.AddToDetailsPb(
addedMember.Sequence,
addedMember.ChangeDate,
addedMember.EventDate,
addedMember.ResourceOwner,
),
}, nil
}
func (s *Server) UpdateOrgMember(ctx context.Context, req *mgmt_pb.UpdateOrgMemberRequest) (*mgmt_pb.UpdateOrgMemberResponse, error) {
changedMember, err := s.command.ChangeOrgMember(ctx, UpdateOrgMemberRequestToDomain(ctx, req))
changedMember, err := s.command.ChangeOrgMember(ctx, UpdateOrgMemberRequestToCommand(req, authz.GetCtxData(ctx).OrgID))
if err != nil {
return nil, err
}
return &mgmt_pb.UpdateOrgMemberResponse{
Details: object.ChangeToDetailsPb(
changedMember.Sequence,
changedMember.ChangeDate,
changedMember.EventDate,
changedMember.ResourceOwner,
),
}, nil
+15 -2
View File
@@ -8,6 +8,7 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/metadata"
"github.com/zitadel/zitadel/internal/api/grpc/object"
org_grpc "github.com/zitadel/zitadel/internal/api/grpc/org"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/query"
@@ -67,8 +68,20 @@ func SetPrimaryOrgDomainRequestToDomain(ctx context.Context, req *mgmt_pb.SetPri
}
}
func UpdateOrgMemberRequestToDomain(ctx context.Context, req *mgmt_pb.UpdateOrgMemberRequest) *domain.Member {
return domain.NewMember(authz.GetCtxData(ctx).OrgID, req.UserId, req.Roles...)
func AddOrgMemberRequestToCommand(req *mgmt_pb.AddOrgMemberRequest, orgID string) *command.AddOrgMember {
return &command.AddOrgMember{
OrgID: orgID,
UserID: req.UserId,
Roles: req.Roles,
}
}
func UpdateOrgMemberRequestToCommand(req *mgmt_pb.UpdateOrgMemberRequest, orgID string) *command.ChangeOrgMember {
return &command.ChangeOrgMember{
OrgID: orgID,
UserID: req.UserId,
Roles: req.Roles,
}
}
func ListOrgMembersRequestToModel(ctx context.Context, req *mgmt_pb.ListOrgMembersRequest) (*query.OrgMembersQuery, error) {
+10 -6
View File
@@ -227,7 +227,7 @@ func (s *Server) RemoveProject(ctx context.Context, req *mgmt_pb.RemoveProjectRe
}
grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{
Queries: []query.SearchQuery{projectQuery},
}, true)
}, true, nil)
if err != nil {
return nil, err
}
@@ -312,7 +312,7 @@ func (s *Server) RemoveProjectRole(ctx context.Context, req *mgmt_pb.RemoveProje
}
userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{
Queries: []query.SearchQuery{projectQuery, rolesQuery},
}, false)
}, false, nil)
if err != nil {
return nil, err
@@ -354,24 +354,28 @@ func (s *Server) ListProjectMembers(ctx context.Context, req *mgmt_pb.ListProjec
}
func (s *Server) AddProjectMember(ctx context.Context, req *mgmt_pb.AddProjectMemberRequest) (*mgmt_pb.AddProjectMemberResponse, error) {
member, err := s.command.AddProjectMember(ctx, AddProjectMemberRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
member, err := s.command.AddProjectMember(ctx, AddProjectMemberRequestToCommand(req, authz.GetCtxData(ctx).OrgID))
if err != nil {
return nil, err
}
return &mgmt_pb.AddProjectMemberResponse{
Details: object_grpc.AddToDetailsPb(member.Sequence, member.ChangeDate, member.ResourceOwner),
Details: object_grpc.AddToDetailsPb(
member.Sequence,
member.EventDate,
member.ResourceOwner,
),
}, nil
}
func (s *Server) UpdateProjectMember(ctx context.Context, req *mgmt_pb.UpdateProjectMemberRequest) (*mgmt_pb.UpdateProjectMemberResponse, error) {
member, err := s.command.ChangeProjectMember(ctx, UpdateProjectMemberRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
member, err := s.command.ChangeProjectMember(ctx, UpdateProjectMemberRequestToCommand(req, authz.GetCtxData(ctx).OrgID))
if err != nil {
return nil, err
}
return &mgmt_pb.UpdateProjectMemberResponse{
Details: object_grpc.ChangeToDetailsPb(
member.Sequence,
member.ChangeDate,
member.EventDate,
member.ResourceOwner,
),
}, nil
@@ -104,12 +104,22 @@ func ProjectGrantsToIDs(projectGrants *query.ProjectGrants) []string {
return converted
}
func AddProjectMemberRequestToDomain(req *mgmt_pb.AddProjectMemberRequest) *domain.Member {
return domain.NewMember(req.ProjectId, req.UserId, req.Roles...)
func AddProjectMemberRequestToCommand(req *mgmt_pb.AddProjectMemberRequest, orgID string) *command.AddProjectMember {
return &command.AddProjectMember{
ResourceOwner: orgID,
ProjectID: req.ProjectId,
UserID: req.UserId,
Roles: req.Roles,
}
}
func UpdateProjectMemberRequestToDomain(req *mgmt_pb.UpdateProjectMemberRequest) *domain.Member {
return domain.NewMember(req.ProjectId, req.UserId, req.Roles...)
func UpdateProjectMemberRequestToCommand(req *mgmt_pb.UpdateProjectMemberRequest, orgID string) *command.ChangeProjectMember {
return &command.ChangeProjectMember{
ResourceOwner: orgID,
ProjectID: req.ProjectId,
UserID: req.UserId,
Roles: req.Roles,
}
}
func listProjectRequestToModel(req *mgmt_pb.ListProjectsRequest) (*query.ProjectSearchQueries, error) {
@@ -91,7 +91,7 @@ func (s *Server) UpdateProjectGrant(ctx context.Context, req *mgmt_pb.UpdateProj
}
grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{
Queries: []query.SearchQuery{projectQuery, grantQuery},
}, true)
}, true, nil)
if err != nil {
return nil, err
}
@@ -139,7 +139,7 @@ func (s *Server) RemoveProjectGrant(ctx context.Context, req *mgmt_pb.RemoveProj
}
userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{
Queries: []query.SearchQuery{projectQuery, grantQuery},
}, false)
}, false, nil)
if err != nil {
return nil, err
}
@@ -176,28 +176,28 @@ func (s *Server) ListProjectGrantMembers(ctx context.Context, req *mgmt_pb.ListP
}
func (s *Server) AddProjectGrantMember(ctx context.Context, req *mgmt_pb.AddProjectGrantMemberRequest) (*mgmt_pb.AddProjectGrantMemberResponse, error) {
member, err := s.command.AddProjectGrantMember(ctx, AddProjectGrantMemberRequestToDomain(req))
member, err := s.command.AddProjectGrantMember(ctx, AddProjectGrantMemberRequestToCommand(req, authz.GetCtxData(ctx).OrgID))
if err != nil {
return nil, err
}
return &mgmt_pb.AddProjectGrantMemberResponse{
Details: object_grpc.AddToDetailsPb(
member.Sequence,
member.ChangeDate,
member.EventDate,
member.ResourceOwner,
),
}, nil
}
func (s *Server) UpdateProjectGrantMember(ctx context.Context, req *mgmt_pb.UpdateProjectGrantMemberRequest) (*mgmt_pb.UpdateProjectGrantMemberResponse, error) {
member, err := s.command.ChangeProjectGrantMember(ctx, UpdateProjectGrantMemberRequestToDomain(req))
member, err := s.command.ChangeProjectGrantMember(ctx, UpdateProjectGrantMemberRequestToCommand(req))
if err != nil {
return nil, err
}
return &mgmt_pb.UpdateProjectGrantMemberResponse{
Details: object_grpc.ChangeToDetailsPb(
member.Sequence,
member.ChangeDate,
member.EventDate,
member.ResourceOwner,
),
}, nil
@@ -7,7 +7,6 @@ import (
member_grpc "github.com/zitadel/zitadel/internal/api/grpc/member"
"github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
@@ -146,24 +145,21 @@ func ListProjectGrantMembersRequestToModel(ctx context.Context, req *mgmt_pb.Lis
}, nil
}
func AddProjectGrantMemberRequestToDomain(req *mgmt_pb.AddProjectGrantMemberRequest) *domain.ProjectGrantMember {
return &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: req.ProjectId,
},
GrantID: req.GrantId,
UserID: req.UserId,
Roles: req.Roles,
func AddProjectGrantMemberRequestToCommand(req *mgmt_pb.AddProjectGrantMemberRequest, orgID string) *command.AddProjectGrantMember {
return &command.AddProjectGrantMember{
ResourceOwner: orgID,
ProjectID: req.ProjectId,
GrantID: req.GrantId,
UserID: req.UserId,
Roles: req.Roles,
}
}
func UpdateProjectGrantMemberRequestToDomain(req *mgmt_pb.UpdateProjectGrantMemberRequest) *domain.ProjectGrantMember {
return &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: req.ProjectId,
},
GrantID: req.GrantId,
UserID: req.UserId,
Roles: req.Roles,
func UpdateProjectGrantMemberRequestToCommand(req *mgmt_pb.UpdateProjectGrantMemberRequest) *command.ChangeProjectGrantMember {
return &command.ChangeProjectGrantMember{
ProjectID: req.ProjectId,
GrantID: req.GrantId,
UserID: req.UserId,
Roles: req.Roles,
}
}
+2 -2
View File
@@ -142,7 +142,7 @@ func (s *Server) ListUserMetadata(ctx context.Context, req *mgmt_pb.ListUserMeta
if err != nil {
return nil, err
}
res, err := s.query.SearchUserMetadata(ctx, true, req.Id, metadataQueries, false)
res, err := s.query.SearchUserMetadata(ctx, true, req.Id, metadataQueries, nil)
if err != nil {
return nil, err
}
@@ -369,7 +369,7 @@ func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]*
}
grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{
Queries: []query.SearchQuery{userGrantUserQuery},
}, true)
}, true, nil)
if err != nil {
return nil, nil, err
}
+7 -7
View File
@@ -33,7 +33,7 @@ func (s *Server) ListUserGrants(ctx context.Context, req *mgmt_pb.ListUserGrantR
if err != nil {
return nil, err
}
res, err := s.query.UserGrants(ctx, queries, false)
res, err := s.query.UserGrants(ctx, queries, false, nil)
if err != nil {
return nil, err
}
@@ -44,11 +44,11 @@ func (s *Server) ListUserGrants(ctx context.Context, req *mgmt_pb.ListUserGrantR
}
func (s *Server) AddUserGrant(ctx context.Context, req *mgmt_pb.AddUserGrantRequest) (*mgmt_pb.AddUserGrantResponse, error) {
grant := AddUserGrantRequestToDomain(req)
grant := AddUserGrantRequestToDomain(req, authz.GetCtxData(ctx).OrgID)
if err := checkExplicitProjectPermission(ctx, grant.ProjectGrantID, grant.ProjectID); err != nil {
return nil, err
}
grant, err := s.command.AddUserGrant(ctx, grant, authz.GetCtxData(ctx).OrgID)
grant, err := s.command.AddUserGrant(ctx, grant, nil)
if err != nil {
return nil, err
}
@@ -63,7 +63,7 @@ func (s *Server) AddUserGrant(ctx context.Context, req *mgmt_pb.AddUserGrantRequ
}
func (s *Server) UpdateUserGrant(ctx context.Context, req *mgmt_pb.UpdateUserGrantRequest) (*mgmt_pb.UpdateUserGrantResponse, error) {
grant, err := s.command.ChangeUserGrant(ctx, UpdateUserGrantRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
grant, err := s.command.ChangeUserGrant(ctx, UpdateUserGrantRequestToDomain(req, authz.GetCtxData(ctx).OrgID), false, false, nil)
if err != nil {
return nil, err
}
@@ -77,7 +77,7 @@ func (s *Server) UpdateUserGrant(ctx context.Context, req *mgmt_pb.UpdateUserGra
}
func (s *Server) DeactivateUserGrant(ctx context.Context, req *mgmt_pb.DeactivateUserGrantRequest) (*mgmt_pb.DeactivateUserGrantResponse, error) {
objectDetails, err := s.command.DeactivateUserGrant(ctx, req.GrantId, authz.GetCtxData(ctx).OrgID)
objectDetails, err := s.command.DeactivateUserGrant(ctx, req.GrantId, authz.GetCtxData(ctx).OrgID, nil)
if err != nil {
return nil, err
}
@@ -87,7 +87,7 @@ func (s *Server) DeactivateUserGrant(ctx context.Context, req *mgmt_pb.Deactivat
}
func (s *Server) ReactivateUserGrant(ctx context.Context, req *mgmt_pb.ReactivateUserGrantRequest) (*mgmt_pb.ReactivateUserGrantResponse, error) {
objectDetails, err := s.command.ReactivateUserGrant(ctx, req.GrantId, authz.GetCtxData(ctx).OrgID)
objectDetails, err := s.command.ReactivateUserGrant(ctx, req.GrantId, authz.GetCtxData(ctx).OrgID, nil)
if err != nil {
return nil, err
}
@@ -97,7 +97,7 @@ func (s *Server) ReactivateUserGrant(ctx context.Context, req *mgmt_pb.Reactivat
}
func (s *Server) RemoveUserGrant(ctx context.Context, req *mgmt_pb.RemoveUserGrantRequest) (*mgmt_pb.RemoveUserGrantResponse, error) {
objectDetails, err := s.command.RemoveUserGrant(ctx, req.GrantId, authz.GetCtxData(ctx).OrgID)
objectDetails, err := s.command.RemoveUserGrant(ctx, req.GrantId, authz.GetCtxData(ctx).OrgID, false, nil)
if err != nil {
return nil, err
}
@@ -49,19 +49,23 @@ func shouldAppendUserGrantOwnerQuery(queries []*user.UserGrantQuery) bool {
return true
}
func AddUserGrantRequestToDomain(req *mgmt_pb.AddUserGrantRequest) *domain.UserGrant {
func AddUserGrantRequestToDomain(req *mgmt_pb.AddUserGrantRequest, resourceowner string) *domain.UserGrant {
return &domain.UserGrant{
UserID: req.UserId,
ProjectID: req.ProjectId,
ProjectGrantID: req.ProjectGrantId,
RoleKeys: req.RoleKeys,
ObjectRoot: models.ObjectRoot{
ResourceOwner: resourceowner,
},
}
}
func UpdateUserGrantRequestToDomain(req *mgmt_pb.UpdateUserGrantRequest) *domain.UserGrant {
func UpdateUserGrantRequestToDomain(req *mgmt_pb.UpdateUserGrantRequest, resourceowner string) *domain.UserGrant {
return &domain.UserGrant{
ObjectRoot: models.ObjectRoot{
AggregateID: req.GrantId,
AggregateID: req.GrantId,
ResourceOwner: resourceowner,
},
UserID: req.UserId,
RoleKeys: req.RoleKeys,
+47
View File
@@ -0,0 +1,47 @@
package metadata
import (
"google.golang.org/protobuf/types/known/timestamppb"
filter_v2 "github.com/zitadel/zitadel/internal/api/grpc/filter/v2"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
meta_pb "github.com/zitadel/zitadel/pkg/grpc/metadata/v2"
)
func UserMetadataListToPb(dataList []*query.UserMetadata) []*meta_pb.Metadata {
mds := make([]*meta_pb.Metadata, len(dataList))
for i, data := range dataList {
mds[i] = UserMetadataToPb(data)
}
return mds
}
func UserMetadataToPb(data *query.UserMetadata) *meta_pb.Metadata {
return &meta_pb.Metadata{
Key: data.Key,
Value: data.Value,
CreationDate: timestamppb.New(data.CreationDate),
ChangeDate: timestamppb.New(data.ChangeDate),
}
}
func UserMetadataFiltersToQuery(queries []*meta_pb.MetadataSearchFilter) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = UserMetadataFilterToQuery(query)
if err != nil {
return nil, err
}
}
return q, nil
}
func UserMetadataFilterToQuery(filter *meta_pb.MetadataSearchFilter) (query.SearchQuery, error) {
switch q := filter.Filter.(type) {
case *meta_pb.MetadataSearchFilter_KeyFilter:
return query.NewUserMetadataKeySearchQuery(q.KeyFilter.Key, filter_v2.TextMethodPbToQuery(q.KeyFilter.Method))
default:
return nil, zerrors.ThrowInvalidArgument(nil, "METAD-fdg23", "List.Query.Invalid")
}
}
@@ -389,7 +389,8 @@ func TestServer_ListProjects(t *testing.T) {
resp2 := createProject(iamOwnerCtx, instance, t, orgID, true, false)
resp3 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, true)
request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &filter.InIDsFilter{Ids: []string{resp1.GetId(), resp2.GetId(), resp3.GetId(), projectResp.GetId()}},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{resp1.GetId(), resp2.GetId(), resp3.GetId(), projectResp.GetId()}},
}
response.Projects[0] = grantedProjectResp
response.Projects[1] = projectResp
@@ -1225,7 +1226,8 @@ func TestServer_ListProjectGrants(t *testing.T) {
project2Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false)
project3Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false)
request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &filter.InIDsFilter{Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId(), projectResp.GetId()}},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId(), projectResp.GetId()}},
}
createProjectGrant(iamOwnerCtx, instance, t, orgID, project1Resp.GetId(), name1)
+1 -1
View File
@@ -118,7 +118,7 @@ func (s *Server) userGrantsFromProject(ctx context.Context, projectID string) ([
}
userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{
Queries: []query.SearchQuery{projectQuery},
}, false)
}, false, nil)
if err != nil {
return nil, err
}
@@ -119,7 +119,7 @@ func (s *Server) userGrantsFromProjectGrant(ctx context.Context, projectID, gran
}
userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{
Queries: []query.SearchQuery{projectQuery, grantQuery},
}, false)
}, false, nil)
if err != nil {
return nil, err
}
@@ -109,7 +109,7 @@ func (s *Server) userGrantsFromProjectAndRole(ctx context.Context, projectID, ro
}
userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{
Queries: []query.SearchQuery{projectQuery, rolesQuery},
}, false)
}, false, nil)
if err != nil {
return nil, err
}
+8
View File
@@ -16,6 +16,13 @@ import (
)
func (s *Server) createUserTypeHuman(ctx context.Context, humanPb *user.CreateUserRequest_Human, orgId string, userName, userId *string) (*connect.Response[user.CreateUserResponse], error) {
metadataEntries := make([]*user.SetMetadataEntry, len(humanPb.Metadata))
for i, metadataEntry := range humanPb.Metadata {
metadataEntries[i] = &user.SetMetadataEntry{
Key: metadataEntry.GetKey(),
Value: metadataEntry.GetValue(),
}
}
addHumanPb := &user.AddHumanUserRequest{
Username: userName,
UserId: userId,
@@ -27,6 +34,7 @@ func (s *Server) createUserTypeHuman(ctx context.Context, humanPb *user.CreateUs
Phone: humanPb.Phone,
IdpLinks: humanPb.IdpLinks,
TotpSecret: humanPb.TotpSecret,
Metadata: metadataEntries,
}
switch pwType := humanPb.GetPasswordType().(type) {
case *user.CreateUserRequest_Human_HashedPassword:
@@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
@@ -22,7 +22,7 @@ import (
)
func TestServer_AddKey(t *testing.T) {
resp := Instance.CreateUserTypeMachine(IamCTX)
resp := Instance.CreateUserTypeMachine(IamCTX, Instance.DefaultOrg.Id)
userId := resp.GetId()
expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24))
type args struct {
@@ -108,7 +108,7 @@ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
ExpirationDate: expirationDate,
},
func(request *user.AddKeyRequest) error {
resp := Instance.CreateUserTypeHuman(IamCTX)
resp := Instance.CreateUserTypeHuman(IamCTX, gofakeit.Email())
request.UserId = resp.Id
return nil
},
@@ -220,7 +220,7 @@ func TestServer_AddKey_Permission(t *testing.T) {
}
func TestServer_RemoveKey(t *testing.T) {
resp := Instance.CreateUserTypeMachine(IamCTX)
resp := Instance.CreateUserTypeMachine(IamCTX, Instance.DefaultOrg.Id)
userId := resp.GetId()
expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24))
type args struct {
@@ -388,7 +388,7 @@ func TestServer_ListKeys(t *testing.T) {
})
require.NoError(t, err)
otherOrgUserId := otherOrgUser.GetId()
otherUserId := Instance.CreateUserTypeMachine(SystemCTX).GetId()
otherUserId := Instance.CreateUserTypeMachine(SystemCTX, Instance.DefaultOrg.Id).GetId()
onlySinceTestStartFilter := &user.KeysSearchFilter{Filter: &user.KeysSearchFilter_CreatedDateFilter{CreatedDateFilter: &filter.TimestampFilter{
Timestamp: timestamppb.Now(),
Method: filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS,
@@ -0,0 +1,403 @@
//go:build integration
package user_test
import (
"context"
"encoding/base64"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/filter/v2"
metadata "github.com/zitadel/zitadel/pkg/grpc/metadata/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func TestServer_SetUserMetadata(t *testing.T) {
iamOwnerCTX := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
tests := []struct {
name string
ctx context.Context
dep func(request *user.SetUserMetadataRequest)
req *user.SetUserMetadataRequest
setDate bool
wantErr bool
}{
{
name: "missing permission",
ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeNoPermission),
dep: func(req *user.SetUserMetadataRequest) {
req.UserId = Instance.CreateUserTypeHuman(CTX, gofakeit.Email()).GetId()
},
req: &user.SetUserMetadataRequest{
Metadata: []*user.Metadata{{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))}},
},
wantErr: true,
},
{
name: "set user metadata",
ctx: iamOwnerCTX,
dep: func(req *user.SetUserMetadataRequest) {
req.UserId = Instance.CreateUserTypeHuman(CTX, gofakeit.Email()).GetId()
},
req: &user.SetUserMetadataRequest{
Metadata: []*user.Metadata{{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))}},
},
setDate: true,
},
{
name: "set user metadata, multiple",
ctx: iamOwnerCTX,
dep: func(req *user.SetUserMetadataRequest) {
req.UserId = Instance.CreateUserTypeHuman(CTX, gofakeit.Email()).GetId()
},
req: &user.SetUserMetadataRequest{
Metadata: []*user.Metadata{
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
{Key: "key2", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))},
{Key: "key3", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value3")))},
},
},
setDate: true,
},
{
name: "set user metadata on non existent user",
ctx: iamOwnerCTX,
req: &user.SetUserMetadataRequest{
UserId: "notexisting",
Metadata: []*user.Metadata{{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))}},
},
wantErr: true,
},
{
name: "update user metadata",
ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner),
dep: func(req *user.SetUserMetadataRequest) {
req.UserId = Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId()
Instance.SetUserMetadata(iamOwnerCTX, req.UserId, "key1", "value1")
},
req: &user.SetUserMetadataRequest{
Metadata: []*user.Metadata{{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))}},
},
setDate: true,
},
{
name: "update user metadata with same value",
ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner),
dep: func(req *user.SetUserMetadataRequest) {
req.UserId = Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId()
Instance.SetUserMetadata(iamOwnerCTX, req.UserId, "key1", "value1")
},
req: &user.SetUserMetadataRequest{
Metadata: []*user.Metadata{{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))}},
},
setDate: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
creationDate := time.Now().UTC()
if tt.dep != nil {
tt.dep(tt.req)
}
got, err := Client.SetUserMetadata(tt.ctx, tt.req)
changeDate := time.Now().UTC()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assertSetUserMetadataResponse(t, creationDate, changeDate, tt.setDate, got)
})
}
}
func assertSetUserMetadataResponse(t *testing.T, creationDate, changeDate time.Time, expectedSetDat bool, actualResp *user.SetUserMetadataResponse) {
if expectedSetDat {
if !changeDate.IsZero() {
assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, changeDate)
} else {
assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, time.Now().UTC())
}
} else {
assert.Nil(t, actualResp.SetDate)
}
}
func TestServer_ListUserMetadata(t *testing.T) {
iamOwnerCTX := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
type args struct {
ctx context.Context
dep func(context.Context, *user.ListUserMetadataRequest, *user.ListUserMetadataResponse)
req *user.ListUserMetadataRequest
}
tests := []struct {
name string
args args
want *user.ListUserMetadataResponse
wantErr bool
}{
{
name: "missing permission",
args: args{
ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeNoPermission),
dep: func(ctx context.Context, request *user.ListUserMetadataRequest, response *user.ListUserMetadataResponse) {
userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId()
request.UserId = userID
Instance.SetUserMetadata(iamOwnerCTX, userID, "key1", "value1")
},
req: &user.ListUserMetadataRequest{},
},
wantErr: true,
},
{
name: "list request",
args: args{
ctx: iamOwnerCTX,
dep: func(ctx context.Context, request *user.ListUserMetadataRequest, response *user.ListUserMetadataResponse) {
userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId()
request.UserId = userID
metadataResp := Instance.SetUserMetadata(iamOwnerCTX, userID, "key1", "value1")
response.Metadata[0] = &metadata.Metadata{
CreationDate: metadataResp.GetSetDate(),
ChangeDate: metadataResp.GetSetDate(),
Key: "key1",
Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1"))),
}
},
req: &user.ListUserMetadataRequest{},
},
want: &user.ListUserMetadataResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Metadata: []*metadata.Metadata{
{},
},
},
},
{
name: "list request single key",
args: args{
ctx: iamOwnerCTX,
dep: func(ctx context.Context, request *user.ListUserMetadataRequest, response *user.ListUserMetadataResponse) {
userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId()
request.UserId = userID
key := "key1"
response.Metadata[0] = setUserMetadata(iamOwnerCTX, userID, key, "value1")
Instance.SetUserMetadata(iamOwnerCTX, userID, "key2", "value2")
Instance.SetUserMetadata(iamOwnerCTX, userID, "key3", "value3")
request.Filters[0] = &metadata.MetadataSearchFilter{
Filter: &metadata.MetadataSearchFilter_KeyFilter{KeyFilter: &metadata.MetadataKeyFilter{Key: key}},
}
},
req: &user.ListUserMetadataRequest{
Filters: []*metadata.MetadataSearchFilter{{}},
},
},
want: &user.ListUserMetadataResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Metadata: []*metadata.Metadata{
{},
},
},
},
{
name: "list multiple keys",
args: args{
ctx: iamOwnerCTX,
dep: func(ctx context.Context, request *user.ListUserMetadataRequest, response *user.ListUserMetadataResponse) {
userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId()
request.UserId = userID
response.Metadata[2] = setUserMetadata(iamOwnerCTX, userID, "key1", "value1")
response.Metadata[1] = setUserMetadata(iamOwnerCTX, userID, "key2", "value2")
response.Metadata[0] = setUserMetadata(iamOwnerCTX, userID, "key3", "value3")
},
req: &user.ListUserMetadataRequest{},
},
want: &user.ListUserMetadataResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 3,
AppliedLimit: 100,
},
Metadata: []*metadata.Metadata{
{}, {}, {},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.dep != nil {
tt.args.dep(tt.args.ctx, tt.args.req, tt.want)
}
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCTX, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, listErr := Instance.Client.UserV2.ListUserMetadata(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(ttt, listErr)
return
}
require.NoError(ttt, listErr)
// always first check length, otherwise its failed anyway
if assert.Len(ttt, got.Metadata, len(tt.want.Metadata)) {
assert.EqualExportedValues(ttt, got.Metadata, tt.want.Metadata)
}
assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination)
}, retryDuration, tick, "timeout waiting for expected execution result")
})
}
}
func setUserMetadata(ctx context.Context, userID, key, value string) *metadata.Metadata {
metadataResp := Instance.SetUserMetadata(ctx, userID, key, value)
return &metadata.Metadata{
CreationDate: metadataResp.GetSetDate(),
ChangeDate: metadataResp.GetSetDate(),
Key: key,
Value: []byte(base64.StdEncoding.EncodeToString([]byte(value))),
}
}
func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationResponse, actual *filter.PaginationResponse) {
assert.Equal(t, expected.AppliedLimit, actual.AppliedLimit)
assert.Equal(t, expected.TotalResult, actual.TotalResult)
}
func TestServer_DeleteUserMetadata(t *testing.T) {
iamOwnerCTX := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
tests := []struct {
name string
ctx context.Context
prepare func(request *user.DeleteUserMetadataRequest) (time.Time, time.Time)
req *user.DeleteUserMetadataRequest
wantDeletionDate bool
wantErr bool
}{
{
name: "empty id",
ctx: iamOwnerCTX,
req: &user.DeleteUserMetadataRequest{
UserId: "",
},
wantErr: true,
},
{
name: "delete, user not existing",
ctx: iamOwnerCTX,
req: &user.DeleteUserMetadataRequest{
UserId: "notexisting",
},
wantErr: true,
},
{
name: "delete",
ctx: iamOwnerCTX,
prepare: func(request *user.DeleteUserMetadataRequest) (time.Time, time.Time) {
creationDate := time.Now().UTC()
userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId()
request.UserId = userID
key := "key1"
Instance.SetUserMetadata(iamOwnerCTX, userID, key, "value1")
request.Keys = []string{key}
return creationDate, time.Time{}
},
req: &user.DeleteUserMetadataRequest{},
wantDeletionDate: true,
},
{
name: "delete, empty list",
ctx: iamOwnerCTX,
prepare: func(request *user.DeleteUserMetadataRequest) (time.Time, time.Time) {
creationDate := time.Now().UTC()
userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId()
request.UserId = userID
key := "key1"
Instance.SetUserMetadata(iamOwnerCTX, userID, key, "value1")
Instance.DeleteUserMetadata(iamOwnerCTX, userID, key)
return creationDate, time.Now().UTC()
},
req: &user.DeleteUserMetadataRequest{},
wantErr: true,
},
{
name: "delete, already removed",
ctx: iamOwnerCTX,
prepare: func(request *user.DeleteUserMetadataRequest) (time.Time, time.Time) {
creationDate := time.Now().UTC()
userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId()
request.UserId = userID
key := "key1"
Instance.SetUserMetadata(iamOwnerCTX, userID, key, "value1")
Instance.DeleteUserMetadata(iamOwnerCTX, userID, key)
request.Keys = []string{key}
return creationDate, time.Now().UTC()
},
req: &user.DeleteUserMetadataRequest{},
wantErr: true,
},
{
name: "delete, multiple",
ctx: iamOwnerCTX,
prepare: func(request *user.DeleteUserMetadataRequest) (time.Time, time.Time) {
creationDate := time.Now().UTC()
userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId()
request.UserId = userID
key1 := "key1"
Instance.SetUserMetadata(iamOwnerCTX, userID, key1, "value1")
key2 := "key2"
Instance.SetUserMetadata(iamOwnerCTX, userID, key2, "value1")
key3 := "key3"
Instance.SetUserMetadata(iamOwnerCTX, userID, key3, "value1")
request.Keys = []string{key1, key2, key3}
return creationDate, time.Time{}
},
req: &user.DeleteUserMetadataRequest{},
wantDeletionDate: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var creationDate, deletionDate time.Time
if tt.prepare != nil {
creationDate, deletionDate = tt.prepare(tt.req)
}
got, err := Instance.Client.UserV2.DeleteUserMetadata(tt.ctx, tt.req)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assertDeleteProjectResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got)
})
}
}
func assertDeleteProjectResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *user.DeleteUserMetadataResponse) {
if expectedDeletionDate {
if !deletionDate.IsZero() {
assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, deletionDate)
} else {
assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, time.Now().UTC())
}
} else {
assert.Nil(t, actualResp.DeletionDate)
}
}
@@ -22,7 +22,7 @@ import (
)
func TestServer_AddPersonalAccessToken(t *testing.T) {
resp := Instance.CreateUserTypeMachine(IamCTX)
resp := Instance.CreateUserTypeMachine(IamCTX, Instance.DefaultOrg.Id)
userId := resp.GetId()
expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24))
type args struct {
@@ -64,7 +64,7 @@ func TestServer_AddPersonalAccessToken(t *testing.T) {
ExpirationDate: expirationDate,
},
func(request *user.AddPersonalAccessTokenRequest) error {
resp := Instance.CreateUserTypeHuman(IamCTX)
resp := Instance.CreateUserTypeHuman(IamCTX, gofakeit.Email())
request.UserId = resp.Id
return nil
},
@@ -172,7 +172,7 @@ func TestServer_AddPersonalAccessToken_Permission(t *testing.T) {
}
func TestServer_RemovePersonalAccessToken(t *testing.T) {
resp := Instance.CreateUserTypeMachine(IamCTX)
resp := Instance.CreateUserTypeMachine(IamCTX, Instance.DefaultOrg.Id)
userId := resp.GetId()
expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24))
type args struct {
@@ -339,7 +339,7 @@ func TestServer_ListPersonalAccessTokens(t *testing.T) {
})
require.NoError(t, err)
otherOrgUserId := otherOrgUser.GetId()
otherUserId := Instance.CreateUserTypeMachine(SystemCTX).GetId()
otherUserId := Instance.CreateUserTypeMachine(SystemCTX, Instance.DefaultOrg.Id).GetId()
onlySinceTestStartFilter := &user.PersonalAccessTokensSearchFilter{Filter: &user.PersonalAccessTokensSearchFilter_CreatedDateFilter{CreatedDateFilter: &filter.TimestampFilter{
Timestamp: timestamppb.Now(),
Method: filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS,
@@ -43,7 +43,7 @@ func TestServer_AddSecret(t *testing.T) {
CTX,
&user.AddSecretRequest{},
func(request *user.AddSecretRequest) error {
resp := Instance.CreateUserTypeMachine(CTX)
resp := Instance.CreateUserTypeMachine(CTX, Instance.DefaultOrg.Id)
request.UserId = resp.GetId()
return nil
},
@@ -55,7 +55,7 @@ func TestServer_AddSecret(t *testing.T) {
CTX,
&user.AddSecretRequest{},
func(request *user.AddSecretRequest) error {
resp := Instance.CreateUserTypeMachine(CTX)
resp := Instance.CreateUserTypeMachine(CTX, Instance.DefaultOrg.Id)
request.UserId = resp.GetId()
return nil
},
@@ -67,7 +67,7 @@ func TestServer_AddSecret(t *testing.T) {
CTX,
&user.AddSecretRequest{},
func(request *user.AddSecretRequest) error {
resp := Instance.CreateUserTypeMachine(CTX)
resp := Instance.CreateUserTypeMachine(CTX, Instance.DefaultOrg.Id)
request.UserId = resp.GetId()
_, err := Client.AddSecret(CTX, &user.AddSecretRequest{
UserId: resp.GetId(),
@@ -202,7 +202,7 @@ func TestServer_RemoveSecret(t *testing.T) {
CTX,
&user.RemoveSecretRequest{},
func(request *user.RemoveSecretRequest) error {
resp := Instance.CreateUserTypeMachine(CTX)
resp := Instance.CreateUserTypeMachine(CTX, Instance.DefaultOrg.Id)
request.UserId = resp.GetId()
return nil
},
@@ -215,7 +215,7 @@ func TestServer_RemoveSecret(t *testing.T) {
CTX,
&user.RemoveSecretRequest{},
func(request *user.RemoveSecretRequest) error {
resp := Instance.CreateUserTypeMachine(CTX)
resp := Instance.CreateUserTypeMachine(CTX, Instance.DefaultOrg.Id)
request.UserId = resp.GetId()
_, err := Instance.Client.UserV2.AddSecret(CTX, &user.AddSecretRequest{
UserId: resp.GetId(),
@@ -4,6 +4,7 @@ package user_test
import (
"context"
"encoding/base64"
"fmt"
"net/url"
"os"
@@ -1818,7 +1819,7 @@ func TestServer_DeleteUser(t *testing.T) {
request.UserId = resp.GetUserId()
Instance.CreateProjectUserGrant(t, CTX, projectResp.GetId(), request.UserId)
Instance.CreateProjectMembership(t, CTX, projectResp.GetId(), request.UserId)
Instance.CreateOrgMembership(t, CTX, request.UserId)
Instance.CreateOrgMembership(t, CTX, Instance.DefaultOrg.Id, request.UserId)
return CTX
},
},
@@ -3945,6 +3946,44 @@ func TestServer_CreateUser(t *testing.T) {
}
},
},
{
name: "with metadata",
testCase: func(runId string) testCase {
username := fmt.Sprintf("donald.duck+%s", runId)
email := username + "@example.com"
return testCase{
args: args{
CTX,
&user.CreateUserRequest{
OrganizationId: Instance.DefaultOrg.Id,
Username: &username,
UserType: &user.CreateUserRequest_Human_{
Human: &user.CreateUserRequest_Human{
Profile: &user.SetHumanProfile{
GivenName: "Donald",
FamilyName: "Duck",
},
Email: &user.SetHumanEmail{
Email: email,
Verification: &user.SetHumanEmail_IsVerified{
IsVerified: true,
},
},
Metadata: []*user.Metadata{
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
{Key: "key2", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))},
{Key: "key3", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value3")))},
},
},
},
},
},
want: &user.CreateUserResponse{
Id: "is generated",
},
}
},
},
{
name: "with idp",
testCase: func(runId string) testCase {
@@ -4890,7 +4929,7 @@ func TestServer_UpdateUserTypeHuman(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
runId := fmt.Sprint(now.UnixNano() + int64(i))
userId := Instance.CreateUserTypeHuman(CTX).GetId()
userId := Instance.CreateUserTypeHuman(CTX, gofakeit.Email()).GetId()
test := tt.testCase(runId, userId)
got, err := Client.UpdateUser(test.args.ctx, test.args.req)
if test.wantErr {
@@ -4972,7 +5011,7 @@ func TestServer_UpdateUserTypeMachine(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
runId := fmt.Sprint(now.UnixNano() + int64(i))
userId := Instance.CreateUserTypeMachine(CTX).GetId()
userId := Instance.CreateUserTypeMachine(CTX, Instance.DefaultOrg.Id).GetId()
test := tt.testCase(runId, userId)
got, err := Client.UpdateUser(test.args.ctx, test.args.req)
if test.wantErr {
+80
View File
@@ -0,0 +1,80 @@
package user
import (
"context"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc/filter/v2"
"github.com/zitadel/zitadel/internal/api/grpc/metadata/v2"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) ListUserMetadata(ctx context.Context, req *connect.Request[user.ListUserMetadataRequest]) (*connect.Response[user.ListUserMetadataResponse], error) {
metadataQueries, err := s.listUserMetadataRequestToModel(req.Msg)
if err != nil {
return nil, err
}
res, err := s.query.SearchUserMetadata(ctx, true, req.Msg.UserId, metadataQueries, s.checkPermission)
if err != nil {
return nil, err
}
return connect.NewResponse(&user.ListUserMetadataResponse{
Metadata: metadata.UserMetadataListToPb(res.Metadata),
Pagination: filter.QueryToPaginationPb(metadataQueries.SearchRequest, res.SearchResponse),
}), nil
}
func (s *Server) listUserMetadataRequestToModel(req *user.ListUserMetadataRequest) (*query.UserMetadataSearchQueries, error) {
offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination)
if err != nil {
return nil, err
}
queries, err := metadata.UserMetadataFiltersToQuery(req.Filters)
if err != nil {
return nil, err
}
return &query.UserMetadataSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: query.UserMetadataCreationDateCol,
},
Queries: queries,
}, nil
}
func (s *Server) SetUserMetadata(ctx context.Context, req *connect.Request[user.SetUserMetadataRequest]) (*connect.Response[user.SetUserMetadataResponse], error) {
result, err := s.command.BulkSetUserMetadata(ctx, req.Msg.UserId, "", setUserMetadataToDomain(req.Msg)...)
if err != nil {
return nil, err
}
return connect.NewResponse(&user.SetUserMetadataResponse{
SetDate: timestamppb.New(result.EventDate),
}), nil
}
func setUserMetadataToDomain(req *user.SetUserMetadataRequest) []*domain.Metadata {
metadata := make([]*domain.Metadata, len(req.Metadata))
for i, data := range req.Metadata {
metadata[i] = &domain.Metadata{
Key: data.Key,
Value: data.Value,
}
}
return metadata
}
func (s *Server) DeleteUserMetadata(ctx context.Context, req *connect.Request[user.DeleteUserMetadataRequest]) (*connect.Response[user.DeleteUserMetadataResponse], error) {
result, err := s.command.BulkRemoveUserMetadata(ctx, req.Msg.UserId, "", req.Msg.Keys...)
if err != nil {
return nil, err
}
return connect.NewResponse(&user.DeleteUserMetadataResponse{
DeletionDate: timestamppb.New(result.EventDate),
}), nil
}
+2 -2
View File
@@ -37,9 +37,9 @@ type Server struct {
type Config struct{}
func CreateServer(
systemDefaults systemdefaults.SystemDefaults,
command *command.Commands,
query *query.Queries,
systemDefaults systemdefaults.SystemDefaults,
userCodeAlg crypto.EncryptionAlgorithm,
idpAlg crypto.EncryptionAlgorithm,
idpCallback func(ctx context.Context) string,
@@ -48,7 +48,6 @@ func CreateServer(
checkPermission domain.PermissionCheck,
) *Server {
return &Server{
systemDefaults: systemDefaults,
command: command,
query: query,
userCodeAlg: userCodeAlg,
@@ -57,6 +56,7 @@ func CreateServer(
samlRootURL: samlRootURL,
assetAPIPrefix: assetAPIPrefix,
checkPermission: checkPermission,
systemDefaults: systemDefaults,
}
}
+1 -1
View File
@@ -204,7 +204,7 @@ func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]*
}
grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{
Queries: []query.SearchQuery{userGrantUserQuery},
}, true)
}, true, nil)
if err != nil {
return nil, nil, err
}
@@ -1773,7 +1773,7 @@ func TestServer_DeleteUser(t *testing.T) {
request.UserId = resp.GetUserId()
Instance.CreateProjectUserGrant(t, CTX, projectResp.GetId(), request.UserId)
Instance.CreateProjectMembership(t, CTX, projectResp.GetId(), request.UserId)
Instance.CreateOrgMembership(t, CTX, request.UserId)
Instance.CreateOrgMembership(t, CTX, Instance.DefaultOrg.Id, request.UserId)
},
},
want: &user.DeleteUserResponse{
@@ -2125,7 +2125,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
if tt.want.url != "" && !tt.want.postForm {
authUrl, err := url.Parse(got.GetAuthUrl())
require.NoError(t, err)
assert.Equal(t, tt.want.url, authUrl.Scheme+"://"+authUrl.Host+authUrl.Path)
require.Len(t, authUrl.Query(), len(tt.want.parametersEqual)+len(tt.want.parametersExisting))
+1 -1
View File
@@ -296,7 +296,7 @@ func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]*
}
grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{
Queries: []query.SearchQuery{userGrantUserQuery},
}, true)
}, true, nil)
if err != nil {
return nil, nil, err
}
+2 -2
View File
@@ -309,7 +309,7 @@ func (p *Storage) getCustomAttributes(ctx context.Context, user *query.User, use
true,
user.ID,
&query.UserMetadataSearchQueries{Queries: []query.SearchQuery{resourceOwnerQuery}},
false,
nil,
)
if err != nil {
logging.WithError(err).Info("unable to get md in action")
@@ -490,7 +490,7 @@ func (p *Storage) getGrants(ctx context.Context, userID, applicationID string) (
userIDQuery,
activeQuery,
},
}, true)
}, true, nil)
}
type customAttribute struct {
@@ -17,6 +17,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/zitadel/zitadel/internal/api/scim/resources"
"github.com/zitadel/zitadel/internal/api/scim/schemas"
@@ -478,7 +480,7 @@ func TestBulk(t *testing.T) {
},
{
name: "fail on errors",
body: bulkFailOnErrorsJson,
body: withUsername(bulkFailOnErrorsJson, gofakeit.Username()),
want: &scim.BulkResponse{
Schemas: []schemas.ScimSchemaType{schemas.IdBulkResponse},
Operations: []*scim.BulkResponseOperation{
@@ -579,7 +581,6 @@ func TestBulk(t *testing.T) {
response, err := Instance.Client.SCIM.Bulk(ctx, orgID, tt.body)
createdUserIDs := buildCreatedIDs(response)
defer deleteUsers(t, createdUserIDs)
if tt.wantErr != nil {
statusCode := tt.wantErr.status
@@ -656,17 +657,6 @@ func buildCreatedIDs(response *scim.BulkResponse) []string {
return createdIds
}
func deleteUsers(t require.TestingT, ids []string) {
for _, id := range ids {
err := Instance.Client.SCIM.Users.Delete(CTX, Instance.DefaultOrg.Id, id)
// only not found errors are ok (if the user is deleted in a later on bulk request)
if err != nil {
scim.RequireScimError(t, http.StatusNotFound, err)
}
}
}
func buildMinimalUpdateRequest(userID string) *scim.BulkRequest {
return &scim.BulkRequest{
Schemas: []schemas.ScimSchemaType{schemas.IdBulkRequest},
@@ -690,7 +680,7 @@ func buildTooManyOperationsRequest() *scim.BulkRequest {
req.Operations[i] = &scim.BulkRequestOperation{
Method: http.MethodPost,
Path: "/Users",
Data: minimalUserJson,
Data: withUsername(minimalUserJson, gofakeit.Username()),
}
}
@@ -720,8 +710,11 @@ func ensureMetadataProjected(t require.TestingT, userID, key, value string) {
Id: userID,
Key: key,
})
require.NoError(tt, err)
require.Equal(tt, value, string(md.Metadata.Value))
if !assert.NoError(tt, err) {
require.Equal(tt, status.Code(err), codes.NotFound)
return
}
assert.Equal(tt, value, string(md.Metadata.Value))
}, retryDuration, tick)
}
@@ -32,14 +32,14 @@
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"externalId": "scim-bulk-created-user-0",
"userName": "scim-bulk-created-user-0",
"userName": "{{ .Username }}",
"name": {
"familyName": "scim-bulk-created-user-0-family-name",
"givenName": "scim-bulk-created-user-0-given-name"
},
"emails": [
{
"value": "scim-bulk-created-user-0@example.com",
"value": "{{ .Username }}@example.com",
"primary": true
}
],
@@ -1,7 +1,7 @@
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"externalId": "701984",
"userName": "bjensen@example.com",
"userName": "{{ .Username }}@example.com",
"name": {
"formatted": "Ms. Barbara J Jensen, III",
"familyName": "Jensen",
@@ -15,12 +15,12 @@
"profileUrl": "http://login.example.com/bjensen",
"emails": [
{
"value": "bjensen@example.com",
"value": "{{ .Username }}@example.com",
"type": "work",
"primary": true
},
{
"value": "[email protected]",
"value": "{{ .Username }}[email protected]",
"type": "home"
}
],
@@ -2,14 +2,14 @@
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"userName": "acmeUser1",
"userName": "{{ .Username }}",
"name": {
"familyName": "Ross",
"givenName": "Bethany"
},
"emails": [
{
"value": "user1@example.com",
"value": "{{ .Username }}@example.com",
"primary": true
}
]
@@ -2,14 +2,14 @@
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"userName": "acmeUser1",
"userName": "acmeUser1-inactive",
"name": {
"familyName": "Ross",
"givenName": "Bethany"
},
"emails": [
{
"value": "[email protected]",
"value": "user1-inactive@example.com",
"primary": true
}
],
@@ -2,14 +2,14 @@
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"userName": "acmeUser1",
"userName": "acmeUser1-no-primary-email-phone",
"name": {
"familyName": "Ross",
"givenName": "Bethany"
},
"emails": [
{
"value": "[email protected]"
"value": "user1-no-primary-email-phone@example.com"
}
],
"phoneNumbers": [
@@ -2,14 +2,14 @@
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"userName": "acmeUser1-minimal-replaced",
"userName": "{{ .Username }}",
"name": {
"familyName": "Ross-replaced",
"givenName": "Bethany-replaced"
},
"emails": [
{
"value": "user1-minimal-replaced@example.com",
"value": "{{ .Username }}@example.com",
"primary": true,
"type": "work"
}
@@ -7,7 +7,7 @@
"value": {
"emails":[
{
"value":"babs@example.com",
"value":"{{ .Username }}+2@example.com",
"type":"home",
"primary": true
}
@@ -3,17 +3,22 @@
package integration_test
import (
"bytes"
"context"
_ "embed"
"fmt"
"net/http"
"path"
"testing"
"text/template"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/zitadel/zitadel/internal/api/scim/resources"
"github.com/zitadel/zitadel/internal/api/scim/schemas"
@@ -157,7 +162,18 @@ var (
}
)
func withUsername(fixture []byte, username string) []byte {
buf := new(bytes.Buffer)
template.Must(template.New("").Parse(string(fixture))).Execute(buf, &struct {
Username string
}{
Username: username,
})
return buf.Bytes()
}
func TestCreateUser(t *testing.T) {
minimalUsername := gofakeit.Username()
tests := []struct {
name string
body []byte
@@ -171,16 +187,16 @@ func TestCreateUser(t *testing.T) {
}{
{
name: "minimal user",
body: minimalUserJson,
body: withUsername(minimalUserJson, minimalUsername),
want: &resources.ScimUser{
UserName: "acmeUser1",
UserName: minimalUsername,
Name: &resources.ScimUserName{
FamilyName: "Ross",
GivenName: "Bethany",
},
Emails: []*resources.ScimEmail{
{
Value: "user1@example.com",
Value: minimalUsername + "@example.com",
Primary: true,
},
},
@@ -195,7 +211,7 @@ func TestCreateUser(t *testing.T) {
},
{
name: "full user",
body: fullUserJson,
body: withUsername(fullUserJson, "bjensen"),
want: fullUser,
},
{
@@ -204,7 +220,7 @@ func TestCreateUser(t *testing.T) {
want: &resources.ScimUser{
Emails: []*resources.ScimEmail{
{
Value: "[email protected]",
Value: "user1-no-primary-email-phone@example.com",
Primary: true,
},
},
@@ -262,21 +278,21 @@ func TestCreateUser(t *testing.T) {
},
{
name: "not authenticated",
body: minimalUserJson,
body: withUsername(minimalUserJson, gofakeit.Username()),
ctx: context.Background(),
wantErr: true,
errorStatus: http.StatusUnauthorized,
},
{
name: "no permissions",
body: minimalUserJson,
body: withUsername(minimalUserJson, gofakeit.Username()),
ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
wantErr: true,
errorStatus: http.StatusNotFound,
},
{
name: "another org",
body: minimalUserJson,
body: withUsername(minimalUserJson, gofakeit.Username()),
orgID: SecondaryOrganization.OrganizationId,
wantErr: true,
errorStatus: http.StatusNotFound,
@@ -315,11 +331,6 @@ func TestCreateUser(t *testing.T) {
}
assert.NotEmpty(t, createdUser.ID)
defer func() {
_, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID})
assert.NoError(t, err)
}()
assert.EqualValues(t, []schemas.ScimSchemaType{"urn:ietf:params:scim:schemas:core:2.0:User"}, createdUser.Resource.Schemas)
assert.Equal(t, schemas.ScimResourceTypeSingular("User"), createdUser.Resource.Meta.ResourceType)
assert.Equal(t, "http://"+Instance.Host()+path.Join(schemas.HandlerPrefix, orgID, "Users", createdUser.ID), createdUser.Resource.Meta.Location)
@@ -345,10 +356,11 @@ func TestCreateUser(t *testing.T) {
}
func TestCreateUser_duplicate(t *testing.T) {
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, minimalUserJson)
parsedMinimalUserJson := withUsername(minimalUserJson, gofakeit.Username())
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, parsedMinimalUserJson)
require.NoError(t, err)
_, err = Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, minimalUserJson)
_, err = Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, parsedMinimalUserJson)
scimErr := scim.RequireScimError(t, http.StatusConflict, err)
assert.Equal(t, "User already exists", scimErr.Error.Detail)
assert.Equal(t, "uniqueness", scimErr.Error.ScimType)
@@ -358,14 +370,10 @@ func TestCreateUser_duplicate(t *testing.T) {
}
func TestCreateUser_metadata(t *testing.T) {
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson)
username := gofakeit.Username()
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
defer func() {
_, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID})
require.NoError(t, err)
}()
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) {
md, err := Instance.Client.Mgmt.ListUserMetadata(CTX, &management.ListUserMetadataRequest{
@@ -391,38 +399,36 @@ func TestCreateUser_metadata(t *testing.T) {
test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:locale", "en-US")
test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:ims", `[{"value":"someaimhandle","type":"aim"},{"value":"twitterhandle","type":"X"}]`)
test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:roles", `[{"value":"my-role-1","display":"Rolle 1","type":"main-role","primary":true},{"value":"my-role-2","display":"Rolle 2","type":"secondary-role"}]`)
test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", `[{"value":"bjensen@example.com","primary":true,"type":"work"},{"value":"[email protected]","primary":false,"type":"home"}]`)
test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", fmt.Sprintf(`[{"value":"%s@example.com","primary":true,"type":"work"},{"value":"%[email protected]","primary":false,"type":"home"}]`, username, username))
}, retryDuration, tick)
}
func TestCreateUser_scopedExternalID(t *testing.T) {
setProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID, "fooBar")
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson)
callingUserId, callingUserPat, err := Instance.CreateMachineUserPATWithMembership(CTX, "ORG_OWNER")
require.NoError(t, err)
defer func() {
_, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID})
require.NoError(t, err)
removeProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID)
}()
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
ctx := integration.WithAuthorizationToken(CTX, callingUserPat)
setProvisioningDomain(t, callingUserId, "fooBar")
createdUser, err := Instance.Client.SCIM.Users.Create(ctx, Instance.DefaultOrg.Id, withUsername(fullUserJson, gofakeit.Username()))
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) {
// unscoped externalID should not exist
_, err = Instance.Client.Mgmt.GetUserMetadata(CTX, &management.GetUserMetadataRequest{
unscoped, err := Instance.Client.Mgmt.GetUserMetadata(ctx, &management.GetUserMetadataRequest{
Id: createdUser.ID,
Key: "urn:zitadel:scim:externalId",
})
integration.AssertGrpcStatus(tt, codes.NotFound, err)
unscoped = unscoped
// scoped externalID should exist
md, err := Instance.Client.Mgmt.GetUserMetadata(CTX, &management.GetUserMetadataRequest{
md, err := Instance.Client.Mgmt.GetUserMetadata(ctx, &management.GetUserMetadataRequest{
Id: createdUser.ID,
Key: "urn:zitadel:scim:fooBar:externalId",
})
require.NoError(tt, err)
if !assert.NoError(tt, err) {
require.Equal(tt, status.Code(err), codes.NotFound)
return
}
assert.Equal(tt, "701984", string(md.Metadata.Value))
}, retryDuration, tick)
}
@@ -9,6 +9,7 @@ import (
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
@@ -18,256 +19,260 @@ import (
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/internal/integration/scim"
"github.com/zitadel/zitadel/internal/test"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func TestGetUser(t *testing.T) {
tests := []struct {
name string
orgID string
buildUserID func() string
cleanup func(userID string)
type testCase struct {
ctx context.Context
orgID string
userID string
want *resources.ScimUser
wantErr bool
errorStatus int
}
tests := []struct {
name string
setup func(t *testing.T) testCase
}{
{
name: "not authenticated",
ctx: context.Background(),
errorStatus: http.StatusUnauthorized,
wantErr: true,
name: "not authenticated",
setup: func(t *testing.T) testCase {
return testCase{
ctx: context.Background(),
errorStatus: http.StatusUnauthorized,
wantErr: true,
}
},
},
{
name: "no permissions",
ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
errorStatus: http.StatusNotFound,
wantErr: true,
name: "no permissions",
setup: func(t *testing.T) testCase {
return testCase{
ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
errorStatus: http.StatusNotFound,
wantErr: true,
}
},
},
{
name: "another org",
orgID: SecondaryOrganization.OrganizationId,
errorStatus: http.StatusNotFound,
wantErr: true,
name: "another org",
setup: func(t *testing.T) testCase {
return testCase{
orgID: SecondaryOrganization.OrganizationId,
errorStatus: http.StatusNotFound,
wantErr: true,
}
},
},
{
name: "another org with permissions",
orgID: SecondaryOrganization.OrganizationId,
ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
errorStatus: http.StatusNotFound,
wantErr: true,
name: "another org with permissions",
setup: func(t *testing.T) testCase {
return testCase{
ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
orgID: SecondaryOrganization.OrganizationId,
errorStatus: http.StatusNotFound,
wantErr: true,
}
},
},
{
name: "unknown user id",
buildUserID: func() string {
return "unknown"
setup: func(t *testing.T) testCase {
return testCase{
userID: "unknown",
errorStatus: http.StatusNotFound,
wantErr: true,
}
},
errorStatus: http.StatusNotFound,
wantErr: true,
},
{
name: "created via grpc",
want: &resources.ScimUser{
Name: &resources.ScimUserName{
FamilyName: "Mouse",
GivenName: "Mickey",
},
PreferredLanguage: language.MustParse("nl"),
PhoneNumbers: []*resources.ScimPhoneNumber{
{
Value: "+41791234567",
Primary: true,
setup: func(t *testing.T) testCase {
return testCase{
want: &resources.ScimUser{
Name: &resources.ScimUserName{
FamilyName: "Mouse",
GivenName: "Mickey",
},
PreferredLanguage: language.MustParse("nl"),
PhoneNumbers: []*resources.ScimPhoneNumber{
{
Value: "+41791234567",
Primary: true,
},
},
},
},
}
},
},
{
name: "created via scim",
buildUserID: func() string {
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson)
setup: func(t *testing.T) testCase {
username := gofakeit.Username()
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return createdUser.ID
},
cleanup: func(userID string) {
_, err := Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: userID})
require.NoError(t, err)
},
want: &resources.ScimUser{
ExternalID: "701984",
UserName: "[email protected]",
Name: &resources.ScimUserName{
Formatted: "Babs Jensen", // DisplayName takes precedence
FamilyName: "Jensen",
GivenName: "Barbara",
MiddleName: "Jane",
HonorificPrefix: "Ms.",
HonorificSuffix: "III",
},
DisplayName: "Babs Jensen",
NickName: "Babs",
ProfileUrl: test.Must(schemas.ParseHTTPURL("http://login.example.com/bjensen")),
Title: "Tour Guide",
PreferredLanguage: language.Make("en-US"),
Locale: "en-US",
Timezone: "America/Los_Angeles",
Active: schemas.NewRelaxedBool(true),
Emails: []*resources.ScimEmail{
{
Value: "[email protected]",
Primary: true,
Type: "work",
return testCase{
userID: createdUser.ID,
want: &resources.ScimUser{
ExternalID: "701984",
UserName: username + "@example.com",
Name: &resources.ScimUserName{
Formatted: "Babs Jensen", // DisplayName takes precedence
FamilyName: "Jensen",
GivenName: "Barbara",
MiddleName: "Jane",
HonorificPrefix: "Ms.",
HonorificSuffix: "III",
},
DisplayName: "Babs Jensen",
NickName: "Babs",
ProfileUrl: test.Must(schemas.ParseHTTPURL("http://login.example.com/bjensen")),
Title: "Tour Guide",
PreferredLanguage: language.Make("en-US"),
Locale: "en-US",
Timezone: "America/Los_Angeles",
Active: schemas.NewRelaxedBool(true),
Emails: []*resources.ScimEmail{
{
Value: username + "@example.com",
Primary: true,
Type: "work",
},
},
PhoneNumbers: []*resources.ScimPhoneNumber{
{
Value: "+415555555555",
Primary: true,
},
},
Ims: []*resources.ScimIms{
{
Value: "someaimhandle",
Type: "aim",
},
{
Value: "twitterhandle",
Type: "X",
},
},
Addresses: []*resources.ScimAddress{
{
Type: "work",
StreetAddress: "100 Universal City Plaza",
Locality: "Hollywood",
Region: "CA",
PostalCode: "91608",
Country: "USA",
Formatted: "100 Universal City Plaza\nHollywood, CA 91608 USA",
Primary: true,
},
{
Type: "home",
StreetAddress: "456 Hollywood Blvd",
Locality: "Hollywood",
Region: "CA",
PostalCode: "91608",
Country: "USA",
Formatted: "456 Hollywood Blvd\nHollywood, CA 91608 USA",
},
},
Photos: []*resources.ScimPhoto{
{
Value: *test.Must(schemas.ParseHTTPURL("https://photos.example.com/profilephoto/72930000000Ccne/F")),
Type: "photo",
},
{
Value: *test.Must(schemas.ParseHTTPURL("https://photos.example.com/profilephoto/72930000000Ccne/T")),
Type: "thumbnail",
},
},
Roles: []*resources.ScimRole{
{
Value: "my-role-1",
Display: "Rolle 1",
Type: "main-role",
Primary: true,
},
{
Value: "my-role-2",
Display: "Rolle 2",
Type: "secondary-role",
Primary: false,
},
},
Entitlements: []*resources.ScimEntitlement{
{
Value: "my-entitlement-1",
Display: "Entitlement 1",
Type: "main-entitlement",
Primary: true,
},
{
Value: "my-entitlement-2",
Display: "Entitlement 2",
Type: "secondary-entitlement",
Primary: false,
},
},
},
},
PhoneNumbers: []*resources.ScimPhoneNumber{
{
Value: "+415555555555",
Primary: true,
},
},
Ims: []*resources.ScimIms{
{
Value: "someaimhandle",
Type: "aim",
},
{
Value: "twitterhandle",
Type: "X",
},
},
Addresses: []*resources.ScimAddress{
{
Type: "work",
StreetAddress: "100 Universal City Plaza",
Locality: "Hollywood",
Region: "CA",
PostalCode: "91608",
Country: "USA",
Formatted: "100 Universal City Plaza\nHollywood, CA 91608 USA",
Primary: true,
},
{
Type: "home",
StreetAddress: "456 Hollywood Blvd",
Locality: "Hollywood",
Region: "CA",
PostalCode: "91608",
Country: "USA",
Formatted: "456 Hollywood Blvd\nHollywood, CA 91608 USA",
},
},
Photos: []*resources.ScimPhoto{
{
Value: *test.Must(schemas.ParseHTTPURL("https://photos.example.com/profilephoto/72930000000Ccne/F")),
Type: "photo",
},
{
Value: *test.Must(schemas.ParseHTTPURL("https://photos.example.com/profilephoto/72930000000Ccne/T")),
Type: "thumbnail",
},
},
Roles: []*resources.ScimRole{
{
Value: "my-role-1",
Display: "Rolle 1",
Type: "main-role",
Primary: true,
},
{
Value: "my-role-2",
Display: "Rolle 2",
Type: "secondary-role",
Primary: false,
},
},
Entitlements: []*resources.ScimEntitlement{
{
Value: "my-entitlement-1",
Display: "Entitlement 1",
Type: "main-entitlement",
Primary: true,
},
{
Value: "my-entitlement-2",
Display: "Entitlement 2",
Type: "secondary-entitlement",
Primary: false,
},
},
}
},
},
{
name: "scoped externalID",
buildUserID: func() string {
// create user without provisioning domain
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson)
setup: func(t *testing.T) testCase {
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, gofakeit.Username()))
require.NoError(t, err)
// set provisioning domain of service user
setProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID, "fooBar")
// set externalID for provisioning domain
callingUserId, callingUserPat, err := Instance.CreateMachineUserPATWithMembership(CTX, "ORG_OWNER")
require.NoError(t, err)
setProvisioningDomain(t, callingUserId, "fooBar")
setAndEnsureMetadata(t, createdUser.ID, "urn:zitadel:scim:fooBar:externalId", "100-scopedExternalId")
return createdUser.ID
},
cleanup: func(userID string) {
_, err := Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: userID})
require.NoError(t, err)
removeProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID)
},
want: &resources.ScimUser{
ExternalID: "100-scopedExternalId",
return testCase{
ctx: integration.WithAuthorizationToken(CTX, callingUserPat),
userID: createdUser.ID,
want: &resources.ScimUser{
ExternalID: "100-scopedExternalId",
},
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := tt.ctx
if ctx == nil {
ctx = CTX
ttt := tt.setup(t)
if ttt.userID == "" {
ttt.userID = Instance.CreateHumanUser(CTX).UserId
}
var userID string
if tt.buildUserID != nil {
userID = tt.buildUserID()
} else {
createUserResp := Instance.CreateHumanUser(CTX)
userID = createUserResp.UserId
if ttt.ctx == nil {
ttt.ctx = CTX
}
orgID := tt.orgID
if orgID == "" {
orgID = Instance.DefaultOrg.Id
if ttt.orgID == "" {
ttt.orgID = Instance.DefaultOrg.Id
}
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
var fetchedUser *resources.ScimUser
var err error
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
fetchedUser, err = Instance.Client.SCIM.Users.Get(ctx, orgID, userID)
if tt.wantErr {
statusCode := tt.errorStatus
require.EventuallyWithT(t, func(collect *assert.CollectT) {
fetchedUser, err := Instance.Client.SCIM.Users.Get(ttt.ctx, ttt.orgID, ttt.userID)
if ttt.wantErr {
statusCode := ttt.errorStatus
if statusCode == 0 {
statusCode = http.StatusBadRequest
}
scim.RequireScimError(ttt, statusCode, err)
scim.RequireScimError(collect, statusCode, err)
return
}
assert.Equal(ttt, userID, fetchedUser.ID)
assert.EqualValues(ttt, []schemas.ScimSchemaType{"urn:ietf:params:scim:schemas:core:2.0:User"}, fetchedUser.Schemas)
assert.Equal(ttt, schemas.ScimResourceTypeSingular("User"), fetchedUser.Resource.Meta.ResourceType)
assert.Equal(ttt, "http://"+Instance.Host()+path.Join(schemas.HandlerPrefix, orgID, "Users", fetchedUser.ID), fetchedUser.Resource.Meta.Location)
assert.Nil(ttt, fetchedUser.Password)
if !test.PartiallyDeepEqual(tt.want, fetchedUser) {
ttt.Errorf("GetUser() got = %#v, want %#v", fetchedUser, tt.want)
if !assert.NoError(collect, err) {
scim.RequireScimError(collect, http.StatusNotFound, err)
return
}
assert.Equal(collect, ttt.userID, fetchedUser.ID)
assert.EqualValues(collect, []schemas.ScimSchemaType{"urn:ietf:params:scim:schemas:core:2.0:User"}, fetchedUser.Schemas)
assert.Equal(collect, schemas.ScimResourceTypeSingular("User"), fetchedUser.Resource.Meta.ResourceType)
assert.Equal(collect, "http://"+Instance.Host()+path.Join(schemas.HandlerPrefix, ttt.orgID, "Users", fetchedUser.ID), fetchedUser.Resource.Meta.Location)
assert.Nil(collect, fetchedUser.Password)
if !test.PartiallyDeepEqual(ttt.want, fetchedUser) {
collect.Errorf("GetUser() got = %#v, want %#v", fetchedUser, ttt.want)
}
}, retryDuration, tick)
if tt.cleanup != nil {
tt.cleanup(fetchedUser.ID)
}
})
}
}
@@ -23,15 +23,6 @@ var totalCountOfHumanUsers = 13
/*
func TestListUser(t *testing.T) {
createdUserIDs := createUsers(t, CTX, Instance.DefaultOrg.Id)
defer func() {
// only the full user needs to be deleted, all others have random identification data
// fullUser is always the first one.
_, err := Instance.Client.UserV2.DeleteUser(CTX, &user_v2.DeleteUserRequest{
UserId: createdUserIDs[0],
})
require.NoError(t, err)
}()
// secondary organization with same set of users,
// these should never be modified.
// This allows testing list requests without filters.
@@ -451,7 +442,7 @@ func createUsers(t *testing.T, ctx context.Context, orgID string) []string {
// create the full scim user if on primary org
if orgID == Instance.DefaultOrg.Id {
fullUserCreatedResp, err := Instance.Client.SCIM.Users.Create(ctx, orgID, fullUserJson)
fullUserCreatedResp, err := Instance.Client.SCIM.Users.Create(ctx, orgID, withUsername(fullUserJson, gofakeit.Username()))
require.NoError(t, err)
createdUserIDs = append(createdUserIDs, fullUserCreatedResp.ID)
count--
@@ -5,11 +5,13 @@ package integration_test
import (
"context"
_ "embed"
"fmt"
"net/http"
"path"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
@@ -20,7 +22,6 @@ import (
"github.com/zitadel/zitadel/internal/integration/scim"
"github.com/zitadel/zitadel/internal/test"
"github.com/zitadel/zitadel/pkg/grpc/management"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
var (
@@ -199,28 +200,28 @@ func TestReplaceUser(t *testing.T) {
},
{
name: "not authenticated",
body: minimalUserJson,
body: withUsername(minimalUserJson, gofakeit.Username()),
ctx: context.Background(),
wantErr: true,
errorStatus: http.StatusUnauthorized,
},
{
name: "no permissions",
body: minimalUserJson,
body: withUsername(minimalUserJson, gofakeit.Username()),
ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
wantErr: true,
errorStatus: http.StatusNotFound,
},
{
name: "another org",
body: minimalUserJson,
body: withUsername(minimalUserJson, gofakeit.Username()),
replaceUserOrgID: SecondaryOrganization.OrganizationId,
wantErr: true,
errorStatus: http.StatusNotFound,
},
{
name: "another org with permissions",
body: minimalUserJson,
body: withUsername(minimalUserJson, gofakeit.Username()),
replaceUserOrgID: SecondaryOrganization.OrganizationId,
ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner),
wantErr: true,
@@ -230,14 +231,9 @@ func TestReplaceUser(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// use iam owner => we don't want to test permissions of the create endpoint.
createdUser, err := Instance.Client.SCIM.Users.Create(Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), Instance.DefaultOrg.Id, fullUserJson)
createdUser, err := Instance.Client.SCIM.Users.Create(Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), Instance.DefaultOrg.Id, withUsername(fullUserJson, gofakeit.Username()))
require.NoError(t, err)
defer func() {
_, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID})
assert.NoError(t, err)
}()
ctx := tt.ctx
if ctx == nil {
ctx = CTX
@@ -294,10 +290,11 @@ func TestReplaceUser(t *testing.T) {
func TestReplaceUser_removeOldMetadata(t *testing.T) {
// ensure old metadata is removed correctly
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson)
username := gofakeit.Username()
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
_, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, minimalUserJson)
_, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, withUsername(minimalUserJson, username))
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
@@ -312,20 +309,17 @@ func TestReplaceUser_removeOldMetadata(t *testing.T) {
for i := range md.Result {
mdMap[md.Result[i].Key] = string(md.Result[i].Value)
}
test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", "[{\"value\":\"[email protected]\",\"primary\":true}]")
test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", fmt.Sprintf("[{\"value\":\"%[email protected]\",\"primary\":true}]", username))
}, retryDuration, tick)
_, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID})
require.NoError(t, err)
}
func TestReplaceUser_emailType(t *testing.T) {
// ensure old metadata is removed correctly
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson)
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, gofakeit.Username()))
require.NoError(t, err)
_, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, minimalUserWithEmailTypeReplaceJson)
replacedUsername := gofakeit.Username()
_, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, withUsername(minimalUserWithEmailTypeReplaceJson, replacedUsername))
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
@@ -341,28 +335,26 @@ func TestReplaceUser_emailType(t *testing.T) {
mdMap[md.Result[i].Key] = string(md.Result[i].Value)
}
test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", "[{\"value\":\"user1-minimal-replaced@example.com\",\"primary\":true,\"type\":\"work\"}]")
test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", fmt.Sprintf("[{\"value\":\"%s@example.com\",\"primary\":true,\"type\":\"work\"}]", replacedUsername))
}, retryDuration, tick)
_, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID})
require.NoError(t, err)
}
func TestReplaceUser_scopedExternalID(t *testing.T) {
// create user without provisioning domain set
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson)
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, gofakeit.Username()))
require.NoError(t, err)
callingUserId, callingUserPat, err := Instance.CreateMachineUserPATWithMembership(CTX, "ORG_OWNER")
require.NoError(t, err)
ctx := integration.WithAuthorizationToken(CTX, callingUserPat)
// set provisioning domain of service user
setProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID, "fooBazz")
setProvisioningDomain(t, callingUserId, "fooBazz")
// replace the user with provisioning domain set
_, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, minimalUserWithExternalIDJson)
_, err = Instance.Client.SCIM.Users.Replace(ctx, Instance.DefaultOrg.Id, createdUser.ID, minimalUserWithExternalIDJson)
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) {
md, err := Instance.Client.Mgmt.ListUserMetadata(CTX, &management.ListUserMetadataRequest{
md, err := Instance.Client.Mgmt.ListUserMetadata(ctx, &management.ListUserMetadataRequest{
Id: createdUser.ID,
})
require.NoError(tt, err)
@@ -376,9 +368,4 @@ func TestReplaceUser_scopedExternalID(t *testing.T) {
test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:externalId", "701984")
test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:fooBazz:externalId", "replaced-external-id")
}, retryDuration, tick)
_, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID})
require.NoError(t, err)
removeProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID)
}
@@ -5,11 +5,13 @@ package integration_test
import (
"context"
_ "embed"
"encoding/json"
"fmt"
"net/http"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
@@ -19,7 +21,6 @@ import (
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/internal/integration/scim"
"github.com/zitadel/zitadel/internal/test"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
var (
@@ -34,15 +35,7 @@ func init() {
}
func TestUpdateUser(t *testing.T) {
fullUserCreated, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson)
require.NoError(t, err)
defer func() {
_, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: fullUserCreated.ID})
require.NoError(t, err)
}()
tests := []struct {
type testCase struct {
name string
body []byte
ctx context.Context
@@ -52,215 +45,297 @@ func TestUpdateUser(t *testing.T) {
wantErr bool
scimErrorType string
errorStatus int
}
tests := []struct {
name string
setup func(t *testing.T) testCase
}{
{
name: "not authenticated",
ctx: context.Background(),
body: minimalUserUpdateJson,
wantErr: true,
errorStatus: http.StatusUnauthorized,
name: "not authenticated",
setup: func(t *testing.T) testCase {
username := gofakeit.Username()
created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return testCase{
userID: created.ID,
ctx: context.Background(),
body: minimalUserUpdateJson,
wantErr: true,
errorStatus: http.StatusUnauthorized,
}
},
},
{
name: "no permissions",
ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
body: minimalUserUpdateJson,
wantErr: true,
errorStatus: http.StatusNotFound,
name: "no permissions",
setup: func(t *testing.T) testCase {
username := gofakeit.Username()
created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return testCase{
userID: created.ID,
ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
body: minimalUserUpdateJson,
wantErr: true,
errorStatus: http.StatusNotFound,
}
},
},
{
name: "other org",
orgID: SecondaryOrganization.OrganizationId,
body: minimalUserUpdateJson,
wantErr: true,
errorStatus: http.StatusNotFound,
name: "other org",
setup: func(t *testing.T) testCase {
username := gofakeit.Username()
created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return testCase{
userID: created.ID,
orgID: SecondaryOrganization.OrganizationId,
body: minimalUserUpdateJson,
wantErr: true,
errorStatus: http.StatusNotFound,
}
},
},
{
name: "other org with permissions",
ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner),
orgID: SecondaryOrganization.OrganizationId,
body: minimalUserUpdateJson,
wantErr: true,
errorStatus: http.StatusNotFound,
name: "other org with permissions",
setup: func(t *testing.T) testCase {
username := gofakeit.Username()
created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return testCase{
userID: created.ID,
ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner),
orgID: SecondaryOrganization.OrganizationId,
body: minimalUserUpdateJson,
wantErr: true,
errorStatus: http.StatusNotFound,
}
},
},
{
name: "invalid patch json",
body: simpleReplacePatchBody("nickname", "10"),
wantErr: true,
scimErrorType: "invalidValue",
name: "invalid patch json",
setup: func(t *testing.T) testCase {
username := gofakeit.Username()
created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return testCase{
userID: created.ID,
body: simpleReplacePatchBody("nickname", "10"),
wantErr: true,
scimErrorType: "invalidValue",
}
},
},
{
name: "password complexity violation",
body: simpleReplacePatchBody("password", `"fooBar"`),
wantErr: true,
scimErrorType: "invalidValue",
name: "password complexity violation",
setup: func(t *testing.T) testCase {
username := gofakeit.Username()
created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return testCase{
userID: created.ID,
body: simpleReplacePatchBody("password", `"fooBar"`),
wantErr: true,
scimErrorType: "invalidValue",
}
},
},
{
name: "invalid profile url",
body: simpleReplacePatchBody("profileUrl", `"ftp://example.com/profiles"`),
wantErr: true,
scimErrorType: "invalidValue",
name: "invalid profile url",
setup: func(t *testing.T) testCase {
username := gofakeit.Username()
created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return testCase{
userID: created.ID,
body: simpleReplacePatchBody("profileUrl", `"ftp://example.com/profiles"`),
wantErr: true,
scimErrorType: "invalidValue",
}
},
},
{
name: "invalid time zone",
body: simpleReplacePatchBody("timezone", `"foobar"`),
wantErr: true,
scimErrorType: "invalidValue",
name: "invalid time zone",
setup: func(t *testing.T) testCase {
username := gofakeit.Username()
created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return testCase{
userID: created.ID,
body: simpleReplacePatchBody("timezone", `"foobar"`),
wantErr: true,
scimErrorType: "invalidValue",
}
},
},
{
name: "invalid locale",
body: simpleReplacePatchBody("locale", `"foobar"`),
wantErr: true,
scimErrorType: "invalidValue",
name: "invalid locale",
setup: func(t *testing.T) testCase {
username := gofakeit.Username()
created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return testCase{
userID: created.ID,
body: simpleReplacePatchBody("locale", `"foobar"`),
wantErr: true,
scimErrorType: "invalidValue",
}
},
},
{
name: "unknown user id",
body: simpleReplacePatchBody("nickname", `"foo"`),
userID: "fooBar",
wantErr: true,
errorStatus: http.StatusNotFound,
name: "unknown user id",
setup: func(t *testing.T) testCase {
return testCase{
body: simpleReplacePatchBody("nickname", `"foo"`),
userID: "fooBar",
wantErr: true,
errorStatus: http.StatusNotFound,
}
},
},
{
name: "full",
body: fullUserUpdateJson,
want: &resources.ScimUser{
ExternalID: "fooBAR",
UserName: "[email protected]",
Name: &resources.ScimUserName{
Formatted: "replaced-display-name",
FamilyName: "added-family-name",
GivenName: "added-given-name",
MiddleName: "added-middle-name-2",
HonorificPrefix: "added-honorific-prefix",
HonorificSuffix: "replaced-honorific-suffix",
},
DisplayName: "replaced-display-name",
NickName: "",
ProfileUrl: test.Must(schemas.ParseHTTPURL("http://login.example.com/bjensen")),
Emails: []*resources.ScimEmail{
{
Value: "[email protected]",
Type: "work",
setup: func(t *testing.T) testCase {
username := gofakeit.Username()
created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return testCase{
userID: created.ID,
body: withUsername(fullUserUpdateJson, username),
want: &resources.ScimUser{
ExternalID: "fooBAR",
UserName: username + "@example.com",
Name: &resources.ScimUserName{
Formatted: "replaced-display-name",
FamilyName: "added-family-name",
GivenName: "added-given-name",
MiddleName: "added-middle-name-2",
HonorificPrefix: "added-honorific-prefix",
HonorificSuffix: "replaced-honorific-suffix",
},
DisplayName: "replaced-display-name",
NickName: "",
ProfileUrl: test.Must(schemas.ParseHTTPURL("http://login.example.com/bjensen")),
Emails: []*resources.ScimEmail{
{
Value: username + "@example.com",
Type: "work",
},
{
Value: username + "[email protected]",
Type: "home",
},
{
Value: username + "[email protected]",
Primary: true,
Type: "home",
},
},
Addresses: []*resources.ScimAddress{
{
Type: "replaced-work",
StreetAddress: "replaced-100 Universal City Plaza",
Locality: "replaced-Hollywood",
Region: "replaced-CA",
PostalCode: "replaced-91608",
Country: "replaced-USA",
Formatted: "replaced-100 Universal City Plaza\nHollywood, CA 91608 USA",
Primary: true,
},
},
PhoneNumbers: []*resources.ScimPhoneNumber{
{
Value: "+41711234567",
Primary: true,
},
},
Ims: []*resources.ScimIms{
{
Value: "someaimhandle",
Type: "aim",
},
{
Value: "twitterhandle",
Type: "",
},
},
Photos: []*resources.ScimPhoto{
{
Value: *test.Must(schemas.ParseHTTPURL("https://photos.example.com/profilephoto/72930000000Ccne/F")),
Type: "photo",
},
},
Roles: nil,
Entitlements: []*resources.ScimEntitlement{
{
Value: "my-entitlement-1",
Display: "added-entitlement-1",
Type: "added-entitlement-1",
Primary: false,
},
{
Value: "my-entitlement-2",
Display: "Entitlement 2",
Type: "secondary-entitlement",
Primary: false,
},
{
Value: "added-entitlement-1",
Primary: false,
},
{
Value: "added-entitlement-2",
Primary: false,
},
{
Value: "added-entitlement-3",
Primary: true,
},
},
Title: "Tour Guide",
PreferredLanguage: language.MustParse("en"),
Locale: "en-US",
Timezone: "America/Los_Angeles",
Active: schemas.NewRelaxedBool(true),
},
{
Value: "[email protected]",
Type: "home",
},
{
Value: "[email protected]",
Primary: true,
Type: "home",
},
},
Addresses: []*resources.ScimAddress{
{
Type: "replaced-work",
StreetAddress: "replaced-100 Universal City Plaza",
Locality: "replaced-Hollywood",
Region: "replaced-CA",
PostalCode: "replaced-91608",
Country: "replaced-USA",
Formatted: "replaced-100 Universal City Plaza\nHollywood, CA 91608 USA",
Primary: true,
},
},
PhoneNumbers: []*resources.ScimPhoneNumber{
{
Value: "+41711234567",
Primary: true,
},
},
Ims: []*resources.ScimIms{
{
Value: "someaimhandle",
Type: "aim",
},
{
Value: "twitterhandle",
Type: "",
},
},
Photos: []*resources.ScimPhoto{
{
Value: *test.Must(schemas.ParseHTTPURL("https://photos.example.com/profilephoto/72930000000Ccne/F")),
Type: "photo",
},
},
Roles: nil,
Entitlements: []*resources.ScimEntitlement{
{
Value: "my-entitlement-1",
Display: "added-entitlement-1",
Type: "added-entitlement-1",
Primary: false,
},
{
Value: "my-entitlement-2",
Display: "Entitlement 2",
Type: "secondary-entitlement",
Primary: false,
},
{
Value: "added-entitlement-1",
Primary: false,
},
{
Value: "added-entitlement-2",
Primary: false,
},
{
Value: "added-entitlement-3",
Primary: true,
},
},
Title: "Tour Guide",
PreferredLanguage: language.MustParse("en-US"),
Locale: "en-US",
Timezone: "America/Los_Angeles",
Active: schemas.NewRelaxedBool(true),
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.ctx == nil {
tt.ctx = CTX
ttt := tt.setup(t)
if ttt.orgID == "" {
ttt.orgID = Instance.DefaultOrg.Id
}
if tt.orgID == "" {
tt.orgID = Instance.DefaultOrg.Id
if ttt.ctx == nil {
ttt.ctx = CTX
}
if tt.userID == "" {
tt.userID = fullUserCreated.ID
}
err := Instance.Client.SCIM.Users.Update(tt.ctx, tt.orgID, tt.userID, tt.body)
if tt.wantErr {
err := Instance.Client.SCIM.Users.Update(ttt.ctx, ttt.orgID, ttt.userID, ttt.body)
if ttt.wantErr {
require.Error(t, err)
statusCode := tt.errorStatus
statusCode := ttt.errorStatus
if statusCode == 0 {
statusCode = http.StatusBadRequest
}
scimErr := scim.RequireScimError(t, statusCode, err)
assert.Equal(t, tt.scimErrorType, scimErr.Error.ScimType)
assert.Equal(t, ttt.scimErrorType, scimErr.Error.ScimType)
return
}
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
fetchedUser, err := Instance.Client.SCIM.Users.Get(tt.ctx, tt.orgID, fullUserCreated.ID)
require.NoError(ttt, err)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
fetchedUser, err := Instance.Client.SCIM.Users.Get(ttt.ctx, ttt.orgID, ttt.userID)
if !assert.NoError(collect, err) {
return
}
fetchedUser.Resource = nil
fetchedUser.ID = ""
if tt.want != nil && !test.PartiallyDeepEqual(tt.want, fetchedUser) {
ttt.Errorf("got = %#v, want = %#v", fetchedUser, tt.want)
}
fetched, err := json.Marshal(fetchedUser)
require.NoError(collect, err)
want, err := json.Marshal(ttt.want)
require.NoError(collect, err)
assert.JSONEq(collect, string(want), string(fetched))
}, retryDuration, tick)
})
}
+1 -1
View File
@@ -262,7 +262,7 @@ func (h *UsersHandler) queryUserDependencies(ctx context.Context, userID string)
grants, err := h.query.UserGrants(ctx, &query.UserGrantsQueries{
Queries: []query.SearchQuery{userGrantUserQuery},
}, true)
}, true, nil)
if err != nil {
return nil, nil, err
}
+1 -1
View File
@@ -45,7 +45,7 @@ func (h *UsersHandler) queryMetadataForUsers(ctx context.Context, userIds []stri
func (h *UsersHandler) queryMetadataForUser(ctx context.Context, id string) (map[metadata.ScopedKey][]byte, error) {
queries := h.buildMetadataQueries(ctx)
md, err := h.query.SearchUserMetadata(ctx, false, id, queries, false)
md, err := h.query.SearchUserMetadata(ctx, false, id, queries, nil)
if err != nil {
return nil, err
}
@@ -1260,7 +1260,8 @@ func (l *Login) appendUserGrants(ctx context.Context, userGrants []*domain.UserG
return nil
}
for _, grant := range userGrants {
_, err := l.command.AddUserGrant(setContext(ctx, resourceOwner), grant, resourceOwner)
grant.ResourceOwner = resourceOwner
_, err := l.command.AddUserGrant(setContext(ctx, resourceOwner), grant, nil)
if err != nil {
return err
}
@@ -125,7 +125,7 @@ func (q queryViewWrapper) UserGrantsByProjectAndUserID(ctx context.Context, proj
return nil, err
}
queries := &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantUserID, userGrantProjectID, activeQuery}}
grants, err := q.Queries.UserGrants(ctx, queries, true)
grants, err := q.Queries.UserGrants(ctx, queries, true, nil)
if err != nil {
return nil, err
}
+1 -1
View File
@@ -690,7 +690,7 @@ func setupLoginClient(commands *Commands, validations *[]preparation.Validation,
func setupAdminMembers(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, orgAgg *org.Aggregate, userID string) {
*validations = append(*validations,
commands.AddOrgMemberCommand(orgAgg, userID, domain.RoleOrgOwner),
commands.AddOrgMemberCommand(&AddOrgMember{orgAgg.ID, userID, []string{domain.RoleOrgOwner}}),
commands.AddInstanceMemberCommand(instanceAgg, userID, domain.RoleIAMOwner),
)
}
+77 -42
View File
@@ -2,7 +2,7 @@ package command
import (
"context"
"reflect"
"slices"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command/preparation"
@@ -69,9 +69,19 @@ func IsInstanceMember(ctx context.Context, filter preparation.FilterToQueryReduc
return isMember, nil
}
func (c *Commands) AddInstanceMember(ctx context.Context, userID string, roles ...string) (*domain.Member, error) {
instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.AddInstanceMemberCommand(instanceAgg, userID, roles...))
type AddInstanceMember struct {
InstanceID string
UserID string
Roles []string
}
func (c *Commands) AddInstanceMember(ctx context.Context, member *AddInstanceMember) (*domain.ObjectDetails, error) {
instanceAgg := instance.NewAggregate(member.InstanceID)
if err := c.checkPermissionUpdateInstanceMember(ctx, member.InstanceID); err != nil {
return nil, err
}
//nolint:staticcheck
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.AddInstanceMemberCommand(instanceAgg, member.UserID, member.Roles...))
if err != nil {
return nil, err
}
@@ -79,33 +89,56 @@ func (c *Commands) AddInstanceMember(ctx context.Context, userID string, roles .
if err != nil {
return nil, err
}
addedMember := NewInstanceMemberWriteModel(ctx, userID)
addedMember := NewInstanceMemberWriteModel(member.InstanceID, member.UserID)
err = AppendAndReduce(addedMember, events...)
if err != nil {
return nil, err
}
return memberWriteModelToMember(&addedMember.MemberWriteModel), nil
return writeModelToObjectDetails(&addedMember.WriteModel), nil
}
type ChangeInstanceMember struct {
InstanceID string
UserID string
Roles []string
}
func (i *ChangeInstanceMember) IsValid(zitadelRoles []authz.RoleMapping) error {
if i.InstanceID == "" || i.UserID == "" || len(i.Roles) == 0 {
return zerrors.ThrowInvalidArgument(nil, "INSTANCE-LiaZi", "Errors.IAM.MemberInvalid")
}
if len(domain.CheckForInvalidRoles(i.Roles, domain.IAMRolePrefix, zitadelRoles)) > 0 {
return zerrors.ThrowInvalidArgument(nil, "INSTANCE-3m9fs", "Errors.IAM.MemberInvalid")
}
return nil
}
// ChangeInstanceMember updates an existing member
func (c *Commands) ChangeInstanceMember(ctx context.Context, member *domain.Member) (*domain.Member, error) {
if !member.IsIAMValid() {
return nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-LiaZi", "Errors.IAM.MemberInvalid")
}
if len(domain.CheckForInvalidRoles(member.Roles, domain.IAMRolePrefix, c.zitadelRoles)) > 0 {
return nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-3m9fs", "Errors.IAM.MemberInvalid")
}
existingMember, err := c.instanceMemberWriteModelByID(ctx, member.UserID)
if err != nil {
func (c *Commands) ChangeInstanceMember(ctx context.Context, member *ChangeInstanceMember) (*domain.ObjectDetails, error) {
if err := member.IsValid(c.zitadelRoles); err != nil {
return nil, err
}
if reflect.DeepEqual(existingMember.Roles, member.Roles) {
return nil, zerrors.ThrowPreconditionFailed(nil, "INSTANCE-LiaZi", "Errors.IAM.Member.RolesNotChanged")
existingMember, err := c.instanceMemberWriteModelByID(ctx, member.InstanceID, member.UserID)
if err != nil {
return nil, err
}
instanceAgg := InstanceAggregateFromWriteModel(&existingMember.MemberWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, instance.NewMemberChangedEvent(ctx, instanceAgg, member.UserID, member.Roles...))
if !existingMember.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "INSTANCE-D8JxR", "Errors.NotFound")
}
if err := c.checkPermissionUpdateInstanceMember(ctx, existingMember.AggregateID); err != nil {
return nil, err
}
if slices.Compare(existingMember.Roles, member.Roles) == 0 {
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
pushedEvents, err := c.eventstore.Push(ctx,
instance.NewMemberChangedEvent(ctx,
InstanceAggregateFromWriteModel(&existingMember.WriteModel),
member.UserID,
member.Roles...,
),
)
if err != nil {
return nil, err
}
@@ -114,34 +147,40 @@ func (c *Commands) ChangeInstanceMember(ctx context.Context, member *domain.Memb
return nil, err
}
return memberWriteModelToMember(&existingMember.MemberWriteModel), nil
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
func (c *Commands) RemoveInstanceMember(ctx context.Context, userID string) (*domain.ObjectDetails, error) {
func (c *Commands) RemoveInstanceMember(ctx context.Context, instanceID, userID string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-LiaZi", "Errors.IDMissing")
}
memberWriteModel, err := c.instanceMemberWriteModelByID(ctx, userID)
if err != nil && !zerrors.IsNotFound(err) {
return nil, err
}
if zerrors.IsNotFound(err) {
// empty response because we have no data that match the request
return &domain.ObjectDetails{}, nil
}
instanceAgg := InstanceAggregateFromWriteModel(&memberWriteModel.MemberWriteModel.WriteModel)
removeEvent := c.removeInstanceMember(ctx, instanceAgg, userID, false)
pushedEvents, err := c.eventstore.Push(ctx, removeEvent)
existingMember, err := c.instanceMemberWriteModelByID(ctx, instanceID, userID)
if err != nil {
return nil, err
}
err = AppendAndReduce(memberWriteModel, pushedEvents...)
if err := c.checkPermissionDeleteInstanceMember(ctx, instanceID); err != nil {
return nil, err
}
if !existingMember.State.Exists() {
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
pushedEvents, err := c.eventstore.Push(ctx,
c.removeInstanceMember(ctx,
InstanceAggregateFromWriteModel(&existingMember.WriteModel),
userID,
false,
),
)
if err != nil {
return nil, err
}
err = AppendAndReduce(existingMember, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&memberWriteModel.MemberWriteModel.WriteModel), nil
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
func (c *Commands) removeInstanceMember(ctx context.Context, instanceAgg *eventstore.Aggregate, userID string, cascade bool) eventstore.Command {
@@ -155,19 +194,15 @@ func (c *Commands) removeInstanceMember(ctx context.Context, instanceAgg *events
}
}
func (c *Commands) instanceMemberWriteModelByID(ctx context.Context, userID string) (member *InstanceMemberWriteModel, err error) {
func (c *Commands) instanceMemberWriteModelByID(ctx context.Context, instanceID, userID string) (member *InstanceMemberWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel := NewInstanceMemberWriteModel(ctx, userID)
writeModel := NewInstanceMemberWriteModel(instanceID, userID)
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
if writeModel.State == domain.MemberStateUnspecified || writeModel.State == domain.MemberStateRemoved {
return nil, zerrors.ThrowNotFound(nil, "INSTANCE-D8JxR", "Errors.NotFound")
}
return writeModel, nil
}
+3 -6
View File
@@ -1,9 +1,6 @@
package command
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/instance"
)
@@ -12,12 +9,12 @@ type InstanceMemberWriteModel struct {
MemberWriteModel
}
func NewInstanceMemberWriteModel(ctx context.Context, userID string) *InstanceMemberWriteModel {
func NewInstanceMemberWriteModel(instanceID, userID string) *InstanceMemberWriteModel {
return &InstanceMemberWriteModel{
MemberWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: authz.GetInstance(ctx).InstanceID(),
ResourceOwner: authz.GetInstance(ctx).InstanceID(),
AggregateID: instanceID,
ResourceOwner: instanceID,
},
UserID: userID,
},
+197 -120
View File
@@ -10,24 +10,22 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestCommandSide_AddIAMMember(t *testing.T) {
func TestCommandSide_AddInstanceMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
eventstore func(t *testing.T) *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
userID string
roles []string
member *AddInstanceMember
}
type res struct {
want *domain.Member
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
@@ -39,28 +37,28 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
{
name: "invalid member, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &AddInstanceMember{},
},
res: res{
err: zerrors.IsErrorInvalidArgument,
err: zerrors.IsInternal,
},
},
{
name: "invalid roles, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
userID: "user1",
roles: []string{"IAM_OWNER"},
member: &AddInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER"},
},
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -69,10 +67,10 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "IAM_OWNER",
@@ -80,9 +78,11 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
userID: "user1",
roles: []string{"IAM_OWNER"},
member: &AddInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER"},
},
},
res: res{
err: zerrors.IsPreconditionFailed,
@@ -91,8 +91,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
{
name: "member already exists, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -118,6 +117,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "IAM_OWNER",
@@ -125,9 +125,11 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
userID: "user1",
roles: []string{"IAM_OWNER"},
member: &AddInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER"},
},
},
res: res{
err: zerrors.IsErrorAlreadyExists,
@@ -136,8 +138,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
{
name: "member add uniqueconstraint err, already exists",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -163,6 +164,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "IAM_OWNER",
@@ -170,9 +172,11 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
},
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
userID: "user1",
roles: []string{"IAM_OWNER"},
member: &AddInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER"},
},
},
res: res{
err: zerrors.IsErrorAlreadyExists,
@@ -181,8 +185,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
{
name: "member add, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusherWithInstanceID(
"INSTANCE",
@@ -209,6 +212,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "IAM_OWNER",
@@ -216,30 +220,49 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
},
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
userID: "user1",
roles: []string{"IAM_OWNER"},
member: &AddInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER"},
},
},
res: res{
want: &domain.Member{
ObjectRoot: models.ObjectRoot{
InstanceID: "INSTANCE",
ResourceOwner: "INSTANCE",
AggregateID: "INSTANCE",
},
UserID: "user1",
Roles: []string{"IAM_OWNER"},
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
{
name: "member add, no permission",
fields: fields{
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckNotAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "IAM_OWNER",
},
},
},
args: args{
member: &AddInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER"},
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
zitadelRoles: tt.fields.zitadelRoles,
eventstore: tt.fields.eventstore(t),
zitadelRoles: tt.fields.zitadelRoles,
checkPermission: tt.fields.checkPermission,
}
got, err := r.AddInstanceMember(tt.args.ctx, tt.args.userID, tt.args.roles...)
got, err := r.AddInstanceMember(context.Background(), tt.args.member)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -247,24 +270,23 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
assertObjectDetails(t, tt.res.want, got)
}
})
}
}
func TestCommandSide_ChangeIAMMember(t *testing.T) {
func TestCommandSide_ChangeInstanceMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
eventstore func(t *testing.T) *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
instanceID string
member *domain.Member
member *ChangeInstanceMember
}
type res struct {
want *domain.Member
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
@@ -276,13 +298,11 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) {
{
name: "invalid member, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.Member{},
member: &ChangeInstanceMember{},
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -291,15 +311,14 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) {
{
name: "invalid roles, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.Member{
UserID: "user1",
Roles: []string{"IAM_OWNER"},
member: &ChangeInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER"},
},
},
res: res{
@@ -309,10 +328,10 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) {
{
name: "member not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "IAM_OWNER",
@@ -320,10 +339,10 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
UserID: "user1",
Roles: []string{"IAM_OWNER"},
member: &ChangeInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER"},
},
},
res: res{
@@ -333,8 +352,7 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) {
{
name: "member not changed, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
instance.NewMemberAddedEvent(context.Background(),
@@ -345,6 +363,7 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleIAMOwner,
@@ -352,21 +371,22 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
UserID: "user1",
Roles: []string{"IAM_OWNER"},
member: &ChangeInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER"},
},
},
res: res{
err: zerrors.IsPreconditionFailed,
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
{
name: "member change, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
instance.NewMemberAddedEvent(context.Background(),
@@ -384,6 +404,7 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "IAM_OWNER",
@@ -394,32 +415,62 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
UserID: "user1",
Roles: []string{"IAM_OWNER", "IAM_OWNER_VIEWER"},
member: &ChangeInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER", "IAM_OWNER_VIEWER"},
},
},
res: res{
want: &domain.Member{
ObjectRoot: models.ObjectRoot{
ResourceOwner: "INSTANCE",
AggregateID: "INSTANCE",
InstanceID: "INSTANCE",
},
UserID: "user1",
Roles: []string{"IAM_OWNER", "IAM_OWNER_VIEWER"},
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
{
name: "member change, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
instance.NewMemberAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"user1",
[]string{"IAM_OWNER"}...,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "IAM_OWNER",
},
{
Role: "IAM_OWNER_VIEWER",
},
},
},
args: args{
member: &ChangeInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER"},
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
zitadelRoles: tt.fields.zitadelRoles,
eventstore: tt.fields.eventstore(t),
zitadelRoles: tt.fields.zitadelRoles,
checkPermission: tt.fields.checkPermission,
}
got, err := r.ChangeInstanceMember(tt.args.ctx, tt.args.member)
got, err := r.ChangeInstanceMember(context.Background(), tt.args.member)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -427,18 +478,18 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
assertObjectDetails(t, tt.res.want, got)
}
})
}
}
func TestCommandSide_RemoveIAMMember(t *testing.T) {
func TestCommandSide_RemoveInstanceMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
instanceID string
userID string
}
@@ -455,13 +506,12 @@ func TestCommandSide_RemoveIAMMember(t *testing.T) {
{
name: "invalid member userid missing, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
userID: "",
instanceID: "INSTANCE",
userID: "",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -470,24 +520,25 @@ func TestCommandSide_RemoveIAMMember(t *testing.T) {
{
name: "member not existing, empty object details result",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
userID: "user1",
instanceID: "INSTANCE",
userID: "user1",
},
res: res{
want: &domain.ObjectDetails{},
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
{
name: "member remove, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
instance.NewMemberAddedEvent(context.Background(),
@@ -504,10 +555,11 @@ func TestCommandSide_RemoveIAMMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
userID: "user1",
instanceID: "INSTANCE",
userID: "user1",
},
res: res{
want: &domain.ObjectDetails{
@@ -515,13 +567,38 @@ func TestCommandSide_RemoveIAMMember(t *testing.T) {
},
},
},
{
name: "member remove, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
instance.NewMemberAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"user1",
[]string{"IAM_OWNER"}...,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
instanceID: "INSTANCE",
userID: "user1",
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.RemoveInstanceMember(tt.args.ctx, tt.args.userID)
got, err := r.RemoveInstanceMember(context.Background(), tt.args.instanceID, tt.args.userID)
if tt.res.err == nil {
assert.NoError(t, err)
}
+6 -7
View File
@@ -123,7 +123,7 @@ func (c *Commands) newOrgSetupCommands(ctx context.Context, orgID string, orgSet
func (c *orgSetupCommands) setupOrgAdmin(admin *OrgSetupAdmin, allowInitialMail bool) error {
if admin.ID != "" {
c.validations = append(c.validations, c.commands.AddOrgMemberCommand(c.aggregate, admin.ID, orgAdminRoles(admin.Roles)...))
c.validations = append(c.validations, c.commands.AddOrgMemberCommand(&AddOrgMember{OrgID: c.aggregate.ID, UserID: admin.ID, Roles: orgAdminRoles(admin.Roles)}))
return nil
}
@@ -147,7 +147,7 @@ func (c *orgSetupCommands) setupOrgAdmin(admin *OrgSetupAdmin, allowInitialMail
return err
}
}
c.validations = append(c.validations, c.commands.AddOrgMemberCommand(c.aggregate, userID, orgAdminRoles(admin.Roles)...))
c.validations = append(c.validations, c.commands.AddOrgMemberCommand(&AddOrgMember{OrgID: c.aggregate.ID, UserID: userID, Roles: orgAdminRoles(admin.Roles)}))
return nil
}
@@ -359,16 +359,15 @@ func (c *Commands) addOrgWithIDAndMember(ctx context.Context, name, userID, reso
if err != nil {
return nil, err
}
err = c.checkUserExists(ctx, userID, resourceOwner)
_, err = c.checkUserExists(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
addedMember := NewOrgMemberWriteModel(addedOrg.AggregateID, userID)
orgMemberEvent, err := c.addOrgMember(ctx, orgAgg, addedMember, domain.NewMember(orgAgg.ID, userID, domain.RoleOrgOwner))
if err != nil {
addMember := &AddOrgMember{OrgID: orgAgg.ID, UserID: userID, Roles: []string{domain.RoleOrgOwner}}
if err := addMember.IsValid(c.zitadelRoles); err != nil {
return nil, err
}
events = append(events, orgMemberEvent)
events = append(events, org.NewMemberAddedEvent(ctx, orgAgg, addMember.UserID, addMember.Roles...))
if setOrgInactive {
deactivateOrgEvent := org.NewOrgDeactivatedEvent(ctx, orgAgg)
events = append(events, deactivateOrgEvent)
+90 -62
View File
@@ -2,8 +2,9 @@ package command
import (
"context"
"reflect"
"slices"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
@@ -12,29 +13,22 @@ import (
"github.com/zitadel/zitadel/internal/zerrors"
)
func (c *Commands) AddOrgMemberCommand(a *org.Aggregate, userID string, roles ...string) preparation.Validation {
func (c *Commands) AddOrgMemberCommand(member *AddOrgMember) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument")
}
if len(roles) == 0 {
return nil, zerrors.ThrowInvalidArgument(nil, "V2-PfYhb", "Errors.Invalid.Argument")
}
if len(domain.CheckForInvalidRoles(roles, domain.OrgRolePrefix, c.zitadelRoles)) > 0 && len(domain.CheckForInvalidRoles(roles, domain.RoleSelfManagementGlobal, c.zitadelRoles)) > 0 {
return nil, zerrors.ThrowInvalidArgument(nil, "Org-4N8es", "Errors.Org.MemberInvalid")
if err := member.IsValid(c.zitadelRoles); err != nil {
return nil, err
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) (_ []eventstore.Command, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if exists, err := ExistsUser(ctx, filter, userID, "", false); err != nil || !exists {
if exists, err := ExistsUser(ctx, filter, member.UserID, "", false); err != nil || !exists {
return nil, zerrors.ThrowPreconditionFailed(err, "ORG-GoXOn", "Errors.User.NotFound")
}
if isMember, err := IsOrgMember(ctx, filter, a.ID, userID); err != nil || isMember {
if isMember, err := IsOrgMember(ctx, filter, member.OrgID, member.UserID); err != nil || isMember {
return nil, zerrors.ThrowAlreadyExists(err, "ORG-poWwe", "Errors.Org.Member.AlreadyExists")
}
return []eventstore.Command{org.NewMemberAddedEvent(ctx, &a.Aggregate, userID, roles...)}, nil
return []eventstore.Command{org.NewMemberAddedEvent(ctx, &org.NewAggregate(member.OrgID).Aggregate, member.UserID, member.Roles...)}, nil
},
nil
}
@@ -76,12 +70,33 @@ func IsOrgMember(ctx context.Context, filter preparation.FilterToQueryReducer, o
return isMember, nil
}
func (c *Commands) AddOrgMember(ctx context.Context, orgID, userID string, roles ...string) (_ *domain.Member, err error) {
type AddOrgMember struct {
OrgID string
UserID string
Roles []string
}
func (m *AddOrgMember) IsValid(zitadelRoles []authz.RoleMapping) error {
if m.UserID == "" || m.OrgID == "" || len(m.Roles) == 0 {
return zerrors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument")
}
if len(domain.CheckForInvalidRoles(m.Roles, domain.OrgRolePrefix, zitadelRoles)) > 0 && len(domain.CheckForInvalidRoles(m.Roles, domain.RoleSelfManagementGlobal, zitadelRoles)) > 0 {
return zerrors.ThrowInvalidArgument(nil, "Org-4N8es", "Errors.Org.MemberInvalid")
}
return nil
}
func (c *Commands) AddOrgMember(ctx context.Context, member *AddOrgMember) (_ *domain.ObjectDetails, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
orgAgg := org.NewAggregate(orgID)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.AddOrgMemberCommand(orgAgg, userID, roles...))
if err := c.checkOrgExists(ctx, member.OrgID); err != nil {
return nil, err
}
if err := c.checkPermissionUpdateOrgMember(ctx, member.OrgID, member.OrgID); err != nil {
return nil, err
}
//nolint:staticcheck
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.AddOrgMemberCommand(member))
if err != nil {
return nil, err
}
@@ -89,51 +104,59 @@ func (c *Commands) AddOrgMember(ctx context.Context, orgID, userID string, roles
if err != nil {
return nil, err
}
addedMember := NewOrgMemberWriteModel(orgID, userID)
addedMember := NewOrgMemberWriteModel(member.OrgID, member.UserID)
err = AppendAndReduce(addedMember, events...)
if err != nil {
return nil, err
}
return memberWriteModelToMember(&addedMember.MemberWriteModel), nil
return writeModelToObjectDetails(&addedMember.WriteModel), nil
}
func (c *Commands) addOrgMember(ctx context.Context, orgAgg *eventstore.Aggregate, addedMember *OrgMemberWriteModel, member *domain.Member) (eventstore.Command, error) {
if !member.IsValid() {
return nil, zerrors.ThrowInvalidArgument(nil, "Org-W8m4l", "Errors.Org.MemberInvalid")
type ChangeOrgMember struct {
OrgID string
UserID string
Roles []string
}
func (c *ChangeOrgMember) IsValid(zitadelRoles []authz.RoleMapping) error {
if c.OrgID == "" || c.UserID == "" || len(c.Roles) == 0 {
return zerrors.ThrowInvalidArgument(nil, "Org-LiaZi", "Errors.Org.MemberInvalid")
}
if len(domain.CheckForInvalidRoles(member.Roles, domain.OrgRolePrefix, c.zitadelRoles)) > 0 && len(domain.CheckForInvalidRoles(member.Roles, domain.RoleSelfManagementGlobal, c.zitadelRoles)) > 0 {
return nil, zerrors.ThrowInvalidArgument(nil, "Org-4N8es", "Errors.Org.MemberInvalid")
}
err := c.eventstore.FilterToQueryReducer(ctx, addedMember)
if err != nil {
return nil, err
}
if addedMember.State == domain.MemberStateActive {
return nil, zerrors.ThrowAlreadyExists(nil, "Org-PtXi1", "Errors.Org.Member.AlreadyExists")
if len(domain.CheckForInvalidRoles(c.Roles, domain.OrgRolePrefix, zitadelRoles)) > 0 {
return zerrors.ThrowInvalidArgument(nil, "IAM-m9fG8", "Errors.Org.MemberInvalid")
}
return org.NewMemberAddedEvent(ctx, orgAgg, member.UserID, member.Roles...), nil
return nil
}
// ChangeOrgMember updates an existing member
func (c *Commands) ChangeOrgMember(ctx context.Context, member *domain.Member) (*domain.Member, error) {
if !member.IsValid() {
return nil, zerrors.ThrowInvalidArgument(nil, "Org-LiaZi", "Errors.Org.MemberInvalid")
}
if len(domain.CheckForInvalidRoles(member.Roles, domain.OrgRolePrefix, c.zitadelRoles)) > 0 {
return nil, zerrors.ThrowInvalidArgument(nil, "IAM-m9fG8", "Errors.Org.MemberInvalid")
}
existingMember, err := c.orgMemberWriteModelByID(ctx, member.AggregateID, member.UserID)
if err != nil {
func (c *Commands) ChangeOrgMember(ctx context.Context, member *ChangeOrgMember) (*domain.ObjectDetails, error) {
if err := member.IsValid(c.zitadelRoles); err != nil {
return nil, err
}
if reflect.DeepEqual(existingMember.Roles, member.Roles) {
return nil, zerrors.ThrowPreconditionFailed(nil, "Org-LiaZi", "Errors.Org.Member.RolesNotChanged")
existingMember, err := c.orgMemberWriteModelByID(ctx, member.OrgID, member.UserID)
if err != nil {
return nil, err
}
orgAgg := OrgAggregateFromWriteModel(&existingMember.MemberWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, org.NewMemberChangedEvent(ctx, orgAgg, member.UserID, member.Roles...))
if !existingMember.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "Org-D8JxR", "Errors.NotFound")
}
if err := c.checkPermissionUpdateOrgMember(ctx, existingMember.ResourceOwner, existingMember.AggregateID); err != nil {
return nil, err
}
if slices.Compare(existingMember.Roles, member.Roles) == 0 {
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
pushedEvents, err := c.eventstore.Push(ctx,
org.NewMemberChangedEvent(ctx,
OrgAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel),
member.UserID,
member.Roles...,
),
)
if err != nil {
return nil, err
}
@@ -142,30 +165,39 @@ func (c *Commands) ChangeOrgMember(ctx context.Context, member *domain.Member) (
return nil, err
}
return memberWriteModelToMember(&existingMember.MemberWriteModel), nil
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
func (c *Commands) RemoveOrgMember(ctx context.Context, orgID, userID string) (*domain.ObjectDetails, error) {
m, err := c.orgMemberWriteModelByID(ctx, orgID, userID)
if err != nil && !zerrors.IsNotFound(err) {
if orgID == "" || userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "Org-LiaZi", "Errors.Org.MemberInvalid")
}
existingMember, err := c.orgMemberWriteModelByID(ctx, orgID, userID)
if err != nil {
return nil, err
}
if zerrors.IsNotFound(err) {
// empty response because we have no data that match the request
return &domain.ObjectDetails{}, nil
if !existingMember.State.Exists() {
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
if err := c.checkPermissionDeleteOrgMember(ctx, existingMember.ResourceOwner, existingMember.AggregateID); err != nil {
return nil, err
}
orgAgg := OrgAggregateFromWriteModel(&m.MemberWriteModel.WriteModel)
removeEvent := c.removeOrgMember(ctx, orgAgg, userID, false)
pushedEvents, err := c.eventstore.Push(ctx, removeEvent)
pushedEvents, err := c.eventstore.Push(ctx,
c.removeOrgMember(ctx,
OrgAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel),
userID,
false,
),
)
if err != nil {
return nil, err
}
err = AppendAndReduce(m, pushedEvents...)
err = AppendAndReduce(existingMember, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&m.WriteModel), nil
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
func (c *Commands) removeOrgMember(ctx context.Context, orgAgg *eventstore.Aggregate, userID string, cascade bool) eventstore.Command {
@@ -189,9 +221,5 @@ func (c *Commands) orgMemberWriteModelByID(ctx context.Context, orgID, userID st
return nil, err
}
if writeModel.State == domain.MemberStateUnspecified || writeModel.State == domain.MemberStateRemoved {
return nil, zerrors.ThrowNotFound(nil, "Org-D8JxR", "Errors.NotFound")
}
return writeModel, nil
}
+379 -255
View File
@@ -11,24 +11,19 @@ import (
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestAddMember(t *testing.T) {
type args struct {
a *org.Aggregate
userID string
roles []string
member *AddOrgMember
zitadelRoles []authz.RoleMapping
filter preparation.FilterToQueryReducer
}
ctx := context.Background()
agg := org.NewAggregate("test")
tests := []struct {
name string
@@ -38,8 +33,10 @@ func TestAddMember(t *testing.T) {
{
name: "no user id",
args: args{
a: agg,
userID: "",
member: &AddOrgMember{
OrgID: "test",
UserID: "",
},
},
want: Want{
ValidationErr: zerrors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument"),
@@ -48,19 +45,23 @@ func TestAddMember(t *testing.T) {
{
name: "no roles",
args: args{
a: agg,
userID: "12342",
member: &AddOrgMember{
OrgID: "test",
UserID: "12342",
},
},
want: Want{
ValidationErr: zerrors.ThrowInvalidArgument(nil, "V2-PfYhb", "Errors.Invalid.Argument"),
ValidationErr: zerrors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument"),
},
},
{
name: "TODO: invalid roles",
args: args{
a: agg,
userID: "123",
roles: []string{"ORG_OWNER"},
member: &AddOrgMember{
OrgID: "test",
UserID: "12342",
Roles: []string{"ORG_OWNER"},
},
},
want: Want{
ValidationErr: zerrors.ThrowInvalidArgument(nil, "Org-4N8es", ""),
@@ -69,9 +70,11 @@ func TestAddMember(t *testing.T) {
{
name: "user not exists",
args: args{
a: agg,
userID: "userID",
roles: []string{"ORG_OWNER"},
member: &AddOrgMember{
OrgID: "test",
UserID: "userID",
Roles: []string{"ORG_OWNER"},
},
zitadelRoles: []authz.RoleMapping{
{
Role: "ORG_OWNER",
@@ -89,9 +92,11 @@ func TestAddMember(t *testing.T) {
{
name: "already member",
args: args{
a: agg,
userID: "userID",
roles: []string{"ORG_OWNER"},
member: &AddOrgMember{
OrgID: "test",
UserID: "userID",
Roles: []string{"ORG_OWNER"},
},
zitadelRoles: []authz.RoleMapping{
{
Role: "ORG_OWNER",
@@ -129,9 +134,11 @@ func TestAddMember(t *testing.T) {
{
name: "correct",
args: args{
a: agg,
userID: "userID",
roles: []string{"ORG_OWNER"},
member: &AddOrgMember{
OrgID: "test",
UserID: "userID",
Roles: []string{"ORG_OWNER"},
},
zitadelRoles: []authz.RoleMapping{
{
Role: "ORG_OWNER",
@@ -158,14 +165,14 @@ func TestAddMember(t *testing.T) {
},
want: Want{
Commands: []eventstore.Command{
org.NewMemberAddedEvent(ctx, &agg.Aggregate, "userID", "ORG_OWNER"),
org.NewMemberAddedEvent(ctx, &org.NewAggregate("test").Aggregate, "userID", "ORG_OWNER"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, context.Background(), (&Commands{zitadelRoles: tt.args.zitadelRoles}).AddOrgMemberCommand(tt.args.a, tt.args.userID, tt.args.roles...), tt.args.filter, tt.want)
AssertValidation(t, context.Background(), (&Commands{zitadelRoles: tt.args.zitadelRoles}).AddOrgMemberCommand(tt.args.member), tt.args.filter, tt.want)
})
}
}
@@ -287,17 +294,15 @@ func TestIsMember(t *testing.T) {
func TestCommandSide_AddOrgMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
checkPermission domain.PermissionCheck
eventstore func(t *testing.T) *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
}
type args struct {
ctx context.Context
userID string
orgID string
roles []string
member *AddOrgMember
}
type res struct {
want *domain.Member
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
@@ -309,13 +314,22 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
{
name: "invalid member, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"org",
),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
member: &AddOrgMember{
OrgID: "org1",
},
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -324,15 +338,24 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
{
name: "invalid roles, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"org",
),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
roles: []string{"ORG_OWNER"},
member: &AddOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"ORG_OWNER"},
},
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -341,10 +364,18 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"org",
),
),
),
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleOrgOwner,
@@ -352,10 +383,11 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
roles: []string{domain.RoleOrgOwner},
member: &AddOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"ORG_OWNER"},
},
},
res: res{
err: zerrors.IsPreconditionFailed,
@@ -364,8 +396,15 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
{
name: "member already exists, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"org",
),
),
),
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -391,6 +430,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleOrgOwner,
@@ -398,10 +438,11 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
roles: []string{"ORG_OWNER"},
member: &AddOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"ORG_OWNER"},
},
},
res: res{
err: zerrors.IsErrorAlreadyExists,
@@ -410,8 +451,15 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
{
name: "member add uniqueconstraint err, already exists",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"org",
),
),
),
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -437,6 +485,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleOrgOwner,
@@ -444,10 +493,11 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
roles: []string{"ORG_OWNER"},
member: &AddOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"ORG_OWNER"},
},
},
res: res{
err: zerrors.IsErrorAlreadyExists,
@@ -456,8 +506,15 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
{
name: "member add, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"org",
),
),
),
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -483,6 +540,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleOrgOwner,
@@ -490,30 +548,58 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
roles: []string{"ORG_OWNER"},
member: &AddOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"ORG_OWNER"},
},
},
res: res{
want: &domain.Member{
ObjectRoot: models.ObjectRoot{
ResourceOwner: "org1",
AggregateID: "org1",
},
UserID: "user1",
Roles: []string{domain.RoleOrgOwner},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member add, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"org",
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleOrgOwner,
},
},
},
args: args{
member: &AddOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"ORG_OWNER"},
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
zitadelRoles: tt.fields.zitadelRoles,
eventstore: tt.fields.eventstore(t),
zitadelRoles: tt.fields.zitadelRoles,
checkPermission: tt.fields.checkPermission,
}
got, err := r.AddOrgMember(tt.args.ctx, tt.args.orgID, tt.args.userID, tt.args.roles...)
got, err := r.AddOrgMember(context.Background(), tt.args.member)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -521,7 +607,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
assertObjectDetails(t, tt.res.want, got)
}
})
}
@@ -529,15 +615,15 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
func TestCommandSide_ChangeOrgMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
checkPermission domain.PermissionCheck
eventstore func(t *testing.T) *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
}
type args struct {
ctx context.Context
member *domain.Member
member *ChangeOrgMember
}
type res struct {
want *domain.Member
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
@@ -549,16 +635,12 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
{
name: "invalid member, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "org1",
},
member: &ChangeOrgMember{
OrgID: "org1",
},
},
res: res{
@@ -568,16 +650,12 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
{
name: "invalid roles, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "org1",
},
member: &ChangeOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
@@ -589,10 +667,10 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
{
name: "member not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleOrgOwner,
@@ -600,11 +678,8 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "org1",
},
member: &ChangeOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"ORG_OWNER"},
},
@@ -614,10 +689,9 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
},
},
{
name: "member not changed, precondition error",
name: "member not changed, no change",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewMemberAddedEvent(context.Background(),
@@ -628,6 +702,7 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleOrgOwner,
@@ -635,24 +710,22 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "org1",
},
member: &ChangeOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"ORG_OWNER"},
},
},
res: res{
err: zerrors.IsPreconditionFailed,
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member change, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewMemberAddedEvent(context.Background(),
@@ -670,6 +743,7 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "ORG_OWNER",
@@ -680,160 +754,210 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "org1",
},
member: &ChangeOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"ORG_OWNER", "ORG_OWNER_VIEWER"},
},
},
res: res{
want: &domain.Member{
ObjectRoot: models.ObjectRoot{
ResourceOwner: "org1",
AggregateID: "org1",
},
UserID: "user1",
Roles: []string{"ORG_OWNER", "ORG_OWNER_VIEWER"},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
zitadelRoles: tt.fields.zitadelRoles,
}
got, err := r.ChangeOrgMember(tt.args.ctx, tt.args.member)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
}
})
}
}
func TestCommandSide_RemoveOrgMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
projectID string
userID string
resourceOwner string
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "invalid member projectid missing, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
projectID: "",
userID: "user1",
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "invalid member userid missing, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
projectID: "project1",
userID: "",
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "member not existing, empty object details result",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
},
args: args{
ctx: context.Background(),
projectID: "project1",
userID: "user1",
resourceOwner: "org1",
},
res: res{
want: &domain.ObjectDetails{},
},
},
{
name: "member remove, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
project.NewProjectMemberAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"user1",
[]string{"PROJECT_OWNER"}...,
),
),
),
expectPush(
project.NewProjectMemberRemovedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"user1",
),
),
),
},
args: args{
ctx: context.Background(),
projectID: "project1",
userID: "user1",
resourceOwner: "org1",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member change, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewMemberAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"user1",
[]string{"ORG_OWNER"}...,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "ORG_OWNER",
},
{
Role: "ORG_OWNER_VIEWER",
},
},
},
args: args{
member: &ChangeOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"ORG_OWNER", "ORG_OWNER_VIEWER"},
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
zitadelRoles: tt.fields.zitadelRoles,
checkPermission: tt.fields.checkPermission,
}
got, err := r.RemoveProjectMember(tt.args.ctx, tt.args.projectID, tt.args.userID, tt.args.resourceOwner)
got, err := r.ChangeOrgMember(context.Background(), tt.args.member)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assertObjectDetails(t, tt.res.want, got)
}
})
}
}
func TestCommandSide_RemoveOrgMember(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
orgID string
userID string
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "invalid member orgID missing, error",
fields: fields{
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "",
userID: "user1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "invalid member userid missing, error",
fields: fields{
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "member not existing, empty object details result",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member remove, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewMemberAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"user1",
[]string{"PROJECT_OWNER"}...,
),
),
),
expectPush(
org.NewMemberRemovedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"user1",
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member remove, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewMemberAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"user1",
[]string{"PROJECT_OWNER"}...,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.RemoveOrgMember(tt.args.ctx, tt.args.orgID, tt.args.userID)
if tt.res.err == nil {
assert.NoError(t, err)
}
+6
View File
@@ -1,6 +1,8 @@
package command
import (
"context"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/org"
@@ -63,3 +65,7 @@ func (wm *OrgWriteModel) Query() *eventstore.SearchQueryBuilder {
func OrgAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate {
return eventstore.AggregateFromWriteModel(wm, org.AggregateType, org.AggregateVersion)
}
func OrgAggregateFromWriteModelWithCTX(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate {
return org.AggregateFromWriteModel(ctx, wm)
}
+39 -77
View File
@@ -73,7 +73,7 @@ func TestAddOrg(t *testing.T) {
func TestCommandSide_AddOrg(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
zitadelRoles []authz.RoleMapping
}
@@ -97,9 +97,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
{
name: "invalid org, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@@ -113,9 +111,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
{
name: "invalid org (spaces), error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@@ -130,8 +126,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
{
name: "user removed, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilterOrgDomainNotFound(),
expectFilter(
eventFromEventPusher(
@@ -174,8 +169,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
{
name: "push failed unique constraint, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilterOrgDomainNotFound(),
expectFilter(
eventFromEventPusher(
@@ -193,7 +187,6 @@ func TestCommandSide_AddOrg(t *testing.T) {
),
),
),
expectFilterOrgMemberNotFound(),
expectPushFailed(zerrors.ThrowAlreadyExists(nil, "id", "internal"),
org.NewOrgAddedEvent(
context.Background(),
@@ -242,8 +235,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
{
name: "push failed, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilterOrgDomainNotFound(),
expectFilter(
eventFromEventPusher(
@@ -261,7 +253,6 @@ func TestCommandSide_AddOrg(t *testing.T) {
),
),
),
expectFilterOrgMemberNotFound(),
expectPushFailed(zerrors.ThrowInternal(nil, "id", "internal"),
org.NewOrgAddedEvent(
context.Background(),
@@ -310,8 +301,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
{
name: "add org, no error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilterOrgDomainNotFound(),
expectFilter(
eventFromEventPusher(
@@ -329,7 +319,6 @@ func TestCommandSide_AddOrg(t *testing.T) {
),
),
),
expectFilterOrgMemberNotFound(),
expectPush(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("org2").Aggregate,
@@ -381,8 +370,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
{
name: "add org (remove spaces), no error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilterOrgDomainNotFound(),
expectFilter(
eventFromEventPusher(
@@ -400,7 +388,6 @@ func TestCommandSide_AddOrg(t *testing.T) {
),
),
),
expectFilterOrgMemberNotFound(),
expectPush(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("org2").Aggregate,
@@ -453,7 +440,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
zitadelRoles: tt.fields.zitadelRoles,
}
@@ -473,7 +460,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
func TestCommandSide_ChangeOrg(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@@ -492,9 +479,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
{
name: "empty name, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@@ -507,9 +492,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
{
name: "empty name (spaces), invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@@ -523,8 +506,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
{
name: "org not found, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@@ -540,8 +522,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
{
name: "no change (spaces), error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -563,8 +544,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
{
name: "push failed, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -593,8 +573,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
{
name: "change org name verified, not primary",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -645,8 +624,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
{
name: "change org name verified, with primary",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -705,8 +683,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
{
name: "change org name case verified, with primary",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -754,7 +731,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
}
_, err := r.ChangeOrg(tt.args.ctx, tt.args.orgID, tt.args.name)
if tt.res.err == nil {
@@ -769,7 +746,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
func TestCommandSide_DeactivateOrg(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
iamDomain string
}
@@ -790,8 +767,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) {
{
name: "org not found, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@@ -806,8 +782,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) {
{
name: "org already inactive, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -832,8 +807,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) {
{
name: "push failed, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -860,8 +834,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) {
{
name: "deactivate org",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -886,7 +859,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
}
_, err := r.DeactivateOrg(tt.args.ctx, tt.args.orgID)
@@ -902,7 +875,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) {
func TestCommandSide_ReactivateOrg(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
iamDomain string
}
@@ -923,8 +896,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) {
{
name: "org not found, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@@ -939,8 +911,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) {
{
name: "org already active, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -961,8 +932,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) {
{
name: "push failed, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -994,8 +964,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) {
{
name: "reactivate org",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -1024,7 +993,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
}
_, err := r.ReactivateOrg(tt.args.ctx, tt.args.orgID)
@@ -1040,7 +1009,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) {
func TestCommandSide_RemoveOrg(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
}
type args struct {
@@ -1059,9 +1028,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) {
{
name: "default org, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: authz.WithInstance(context.Background(), &mockInstance{}),
@@ -1074,8 +1041,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) {
{
name: "zitadel org, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(context.Background(),
@@ -1100,8 +1066,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) {
{
name: "org not found, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(), // zitadel project check
expectFilter(),
),
@@ -1117,8 +1082,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) {
{
name: "push failed, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(), // zitadel project check
expectFilter(
eventFromEventPusher(
@@ -1160,8 +1124,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) {
{
name: "remove org",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(), // zitadel project check
expectFilter(
eventFromEventPusher(
@@ -1200,8 +1163,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) {
{
name: "remove org with usernames and domains",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(), // zitadel project check
expectFilter(
eventFromEventPusher(
@@ -1291,7 +1253,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
}
_, err := r.RemoveOrg(tt.args.ctx, tt.args.orgID)
+63
View File
@@ -6,6 +6,8 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/v2/user"
"github.com/zitadel/zitadel/internal/zerrors"
@@ -13,6 +15,8 @@ import (
type PermissionCheck func(resourceOwner, aggregateID string) error
type UserGrantPermissionCheck func(projectID, projectGrantID string) PermissionCheck
func (c *Commands) newPermissionCheck(ctx context.Context, permission string, aggregateType eventstore.AggregateType) PermissionCheck {
return func(resourceOwner, aggregateID string) error {
if aggregateID == "" {
@@ -93,3 +97,62 @@ func (c *Commands) checkPermissionUpdateApplication(ctx context.Context, resourc
func (c *Commands) checkPermissionDeleteApp(ctx context.Context, resourceOwner, appID string) error {
return c.newPermissionCheck(ctx, domain.PermissionProjectAppDelete, project.AggregateType)(resourceOwner, appID)
}
func (c *Commands) checkPermissionUpdateInstanceMember(ctx context.Context, instanceID string) error {
return c.newPermissionCheck(ctx, domain.PermissionInstanceMemberWrite, instance.AggregateType)(instanceID, instanceID)
}
func (c *Commands) checkPermissionDeleteInstanceMember(ctx context.Context, instanceID string) error {
return c.newPermissionCheck(ctx, domain.PermissionInstanceMemberDelete, instance.AggregateType)(instanceID, instanceID)
}
func (c *Commands) checkPermissionUpdateOrgMember(ctx context.Context, instanceID, orgID string) error {
return c.newPermissionCheck(ctx, domain.PermissionOrgMemberWrite, org.AggregateType)(instanceID, orgID)
}
func (c *Commands) checkPermissionDeleteOrgMember(ctx context.Context, instanceID, orgID string) error {
return c.newPermissionCheck(ctx, domain.PermissionOrgMemberDelete, org.AggregateType)(instanceID, orgID)
}
func (c *Commands) checkPermissionUpdateProjectMember(ctx context.Context, resourceOwner, projectID string) error {
return c.newPermissionCheck(ctx, domain.PermissionProjectMemberWrite, project.AggregateType)(resourceOwner, projectID)
}
func (c *Commands) checkPermissionDeleteProjectMember(ctx context.Context, resourceOwner, projectID string) error {
return c.newPermissionCheck(ctx, domain.PermissionProjectMemberDelete, project.AggregateType)(resourceOwner, projectID)
}
func (c *Commands) checkPermissionUpdateProjectGrantMember(ctx context.Context, grantedOrgID, projectGrantID string) (err error) {
// TODO: add permission check for project grant owners
//if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantMemberWrite, project.AggregateType)(resourceOwner, projectGrantID); err != nil {
return c.newPermissionCheck(ctx, domain.PermissionProjectGrantMemberWrite, project.AggregateType)(grantedOrgID, projectGrantID)
//}
//return nil
}
func (c *Commands) checkPermissionDeleteProjectGrantMember(ctx context.Context, grantedOrgID, projectGrantID string) (err error) {
// TODO: add permission check for project grant owners
//if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantMemberDelete, project.AggregateType)(resourceOwner, projectGrantID); err != nil {
return c.newPermissionCheck(ctx, domain.PermissionProjectGrantMemberDelete, project.AggregateType)(grantedOrgID, projectGrantID)
//}
//return nil
}
func (c *Commands) newUserGrantPermissionCheck(ctx context.Context, permission string) UserGrantPermissionCheck {
check := c.newPermissionCheck(ctx, permission, project.AggregateType)
return func(projectID, projectGrantID string) PermissionCheck {
return func(resourceOwner, _ string) error {
if projectGrantID != "" {
return check(resourceOwner, projectGrantID)
}
return check(resourceOwner, projectID)
}
}
}
func (c *Commands) NewPermissionCheckUserGrantWrite(ctx context.Context) UserGrantPermissionCheck {
return c.newUserGrantPermissionCheck(ctx, domain.PermissionUserGrantWrite)
}
func (c *Commands) NewPermissionCheckUserGrantDelete(ctx context.Context) UserGrantPermissionCheck {
return c.newUserGrantPermissionCheck(ctx, domain.PermissionUserGrantDelete)
}
+2 -2
View File
@@ -346,7 +346,7 @@ func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner s
}
for _, grantID := range cascadingUserGrantIDs {
event, _, err := c.removeUserGrant(ctx, grantID, "", true)
event, _, err := c.removeUserGrant(ctx, grantID, "", true, false, nil)
if err != nil {
logging.WithFields("id", "COMMAND-b8Djf", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant")
continue
@@ -398,7 +398,7 @@ func (c *Commands) DeleteProject(ctx context.Context, id, resourceOwner string,
),
}
for _, grantID := range cascadingUserGrantIDs {
event, _, err := c.removeUserGrant(ctx, grantID, "", true)
event, _, err := c.removeUserGrant(ctx, grantID, "", true, false, nil)
if err != nil {
logging.WithFields("id", "COMMAND-b8Djf", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant")
continue
+20 -6
View File
@@ -234,6 +234,17 @@ func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantI
return writeModelToObjectDetails(&existingGrant.WriteModel), nil
}
func (c *Commands) checkProjectGrantExists(ctx context.Context, grantID, grantedOrgID, projectID, resourceOwner string) (string, string, error) {
existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner)
if err != nil {
return "", "", err
}
if !existingGrant.State.Exists() {
return "", "", zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound")
}
return existingGrant.GrantedOrgID, existingGrant.ResourceOwner, nil
}
func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string) (details *domain.ObjectDetails, err error) {
if (grantID == "" && grantedOrgID == "") || projectID == "" {
return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing")
@@ -302,16 +313,17 @@ func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, r
ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel),
existingGrant.GrantID,
existingGrant.GrantedOrgID,
),
)
))
for _, userGrantID := range cascadeUserGrantIDs {
event, _, err := c.removeUserGrant(ctx, userGrantID, "", true)
event, _, err := c.removeUserGrant(ctx, userGrantID, "", true, true, nil)
if err != nil {
logging.WithFields("id", "COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant")
continue
}
events = append(events, event)
if event != nil {
events = append(events, event)
}
}
pushedEvents, err := c.eventstore.Push(ctx, events...)
if err != nil {
@@ -348,12 +360,14 @@ func (c *Commands) DeleteProjectGrant(ctx context.Context, projectID, grantID, g
)
for _, userGrantID := range cascadeUserGrantIDs {
event, _, err := c.removeUserGrant(ctx, userGrantID, "", true)
event, _, err := c.removeUserGrant(ctx, userGrantID, "", true, true, nil)
if err != nil {
logging.WithFields("id", "COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant")
continue
}
events = append(events, event)
if event != nil {
events = append(events, event)
}
}
pushedEvents, err := c.eventstore.Push(ctx, events...)
if err != nil {
+117 -37
View File
@@ -2,8 +2,9 @@ package command
import (
"context"
"reflect"
"slices"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/project"
@@ -11,25 +12,66 @@ import (
"github.com/zitadel/zitadel/internal/zerrors"
)
func (c *Commands) AddProjectGrantMember(ctx context.Context, member *domain.ProjectGrantMember) (_ *domain.ProjectGrantMember, err error) {
type AddProjectGrantMember struct {
ResourceOwner string
UserID string
GrantID string
ProjectID string
Roles []string
}
func (i *AddProjectGrantMember) IsValid(zitadelRoles []authz.RoleMapping) error {
if i.ProjectID == "" || i.GrantID == "" || i.UserID == "" || len(i.Roles) == 0 {
return zerrors.ThrowInvalidArgument(nil, "PROJECT-8fi7G", "Errors.Project.Grant.Member.Invalid")
}
if len(domain.CheckForInvalidRoles(i.Roles, domain.ProjectGrantRolePrefix, zitadelRoles)) > 0 {
return zerrors.ThrowInvalidArgument(nil, "PROJECT-m9gKK", "Errors.Project.Grant.Member.Invalid")
}
return nil
}
func (c *Commands) AddProjectGrantMember(ctx context.Context, member *AddProjectGrantMember) (_ *domain.ObjectDetails, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if !member.IsValid() {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-8fi7G", "Errors.Project.Grant.Member.Invalid")
if err := member.IsValid(c.zitadelRoles); err != nil {
return nil, err
}
if len(domain.CheckForInvalidRoles(member.Roles, domain.ProjectGrantRolePrefix, c.zitadelRoles)) > 0 {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-m9gKK", "Errors.Project.Grant.Member.Invalid")
}
err = c.checkUserExists(ctx, member.UserID, "")
_, err = c.checkUserExists(ctx, member.UserID, "")
if err != nil {
return nil, err
}
addedMember := NewProjectGrantMemberWriteModel(member.AggregateID, member.UserID, member.GrantID)
projectAgg := ProjectAggregateFromWriteModel(&addedMember.WriteModel)
grantedOrgID, projectGrantResourceOwner, err := c.checkProjectGrantExists(ctx, member.GrantID, "", member.ProjectID, "")
if err != nil {
return nil, err
}
if member.ResourceOwner == "" {
member.ResourceOwner = projectGrantResourceOwner
}
addedMember, err := c.projectGrantMemberWriteModelByID(ctx, member.ProjectID, member.UserID, member.GrantID, member.ResourceOwner)
if err != nil {
return nil, err
}
// TODO: change e2e tests to use correct resourceowner, wrong resource owner is corrected through aggregate
// error if provided resourceowner is not equal to the resourceowner of the project grant
//if projectGrantResourceOwner != addedMember.ResourceOwner {
// return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-0l10S9OmZV", "Errors.Project.Grant.Invalid")
//}
if addedMember.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "PROJECT-37fug", "Errors.AlreadyExists")
}
if err := c.checkPermissionUpdateProjectGrantMember(ctx, grantedOrgID, addedMember.GrantID); err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(
ctx,
project.NewProjectGrantMemberAddedEvent(ctx, projectAgg, member.UserID, member.GrantID, member.Roles...))
project.NewProjectGrantMemberAddedEvent(ctx,
ProjectAggregateFromWriteModelWithCTX(ctx, &addedMember.WriteModel),
member.UserID,
member.GrantID,
member.Roles...,
))
if err != nil {
return nil, err
}
@@ -38,30 +80,58 @@ func (c *Commands) AddProjectGrantMember(ctx context.Context, member *domain.Pro
return nil, err
}
return memberWriteModelToProjectGrantMember(addedMember), nil
return writeModelToObjectDetails(&addedMember.WriteModel), nil
}
type ChangeProjectGrantMember struct {
UserID string
GrantID string
ProjectID string
Roles []string
}
func (i *ChangeProjectGrantMember) IsValid(zitadelRoles []authz.RoleMapping) error {
if i.ProjectID == "" || i.GrantID == "" || i.UserID == "" || len(i.Roles) == 0 {
return zerrors.ThrowInvalidArgument(nil, "PROJECT-109fs", "Errors.Project.Grant.Member.Invalid")
}
if len(domain.CheckForInvalidRoles(i.Roles, domain.ProjectGrantRolePrefix, zitadelRoles)) > 0 {
return zerrors.ThrowInvalidArgument(nil, "PROJECT-m0sDf", "Errors.Project.Grant.Member.Invalid")
}
return nil
}
// ChangeProjectGrantMember updates an existing member
func (c *Commands) ChangeProjectGrantMember(ctx context.Context, member *domain.ProjectGrantMember) (*domain.ProjectGrantMember, error) {
if !member.IsValid() {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-109fs", "Errors.Project.Member.Invalid")
func (c *Commands) ChangeProjectGrantMember(ctx context.Context, member *ChangeProjectGrantMember) (*domain.ObjectDetails, error) {
if err := member.IsValid(c.zitadelRoles); err != nil {
return nil, err
}
if len(domain.CheckForInvalidRoles(member.Roles, domain.ProjectGrantRolePrefix, c.zitadelRoles)) > 0 {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-m0sDf", "Errors.Project.Member.Invalid")
}
existingMember, err := c.projectGrantMemberWriteModelByID(ctx, member.AggregateID, member.UserID, member.GrantID)
existingGrant, err := c.projectGrantWriteModelByID(ctx, member.GrantID, "", member.ProjectID, "")
if err != nil {
return nil, err
}
if reflect.DeepEqual(existingMember.Roles, member.Roles) {
return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-2n8vx", "Errors.Project.Member.RolesNotChanged")
existingMember, err := c.projectGrantMemberWriteModelByID(ctx, member.ProjectID, member.UserID, member.GrantID, existingGrant.ResourceOwner)
if err != nil {
return nil, err
}
projectAgg := ProjectAggregateFromWriteModel(&existingMember.WriteModel)
if !existingMember.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "PROJECT-37fug", "Errors.NotFound")
}
if err := c.checkPermissionUpdateProjectGrantMember(ctx, existingGrant.GrantedOrgID, existingMember.GrantID); err != nil {
return nil, err
}
if slices.Compare(existingMember.Roles, member.Roles) == 0 {
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
pushedEvents, err := c.eventstore.Push(
ctx,
project.NewProjectGrantMemberChangedEvent(ctx, projectAgg, member.UserID, member.GrantID, member.Roles...))
project.NewProjectGrantMemberChangedEvent(ctx,
ProjectAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel),
member.UserID,
member.GrantID,
member.Roles...,
))
if err != nil {
return nil, err
}
@@ -70,29 +140,43 @@ func (c *Commands) ChangeProjectGrantMember(ctx context.Context, member *domain.
return nil, err
}
return memberWriteModelToProjectGrantMember(existingMember), nil
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
func (c *Commands) RemoveProjectGrantMember(ctx context.Context, projectID, userID, grantID string) (*domain.ObjectDetails, error) {
if projectID == "" || userID == "" || grantID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-66mHd", "Errors.Project.Member.Invalid")
}
m, err := c.projectGrantMemberWriteModelByID(ctx, projectID, userID, grantID)
existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, "", projectID, "")
if err != nil {
return nil, err
}
existingMember, err := c.projectGrantMemberWriteModelByID(ctx, projectID, userID, grantID, existingGrant.ResourceOwner)
if err != nil {
return nil, err
}
if !existingMember.State.Exists() {
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
if err := c.checkPermissionDeleteProjectGrantMember(ctx, existingGrant.GrantedOrgID, existingMember.GrantID); err != nil {
return nil, err
}
projectAgg := ProjectAggregateFromWriteModel(&m.WriteModel)
removeEvent := c.removeProjectGrantMember(ctx, projectAgg, userID, grantID, false)
removeEvent := c.removeProjectGrantMember(ctx,
ProjectAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel),
userID,
grantID,
false,
)
pushedEvents, err := c.eventstore.Push(ctx, removeEvent)
if err != nil {
return nil, err
}
err = AppendAndReduce(m, pushedEvents...)
err = AppendAndReduce(existingMember, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&m.WriteModel), nil
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
func (c *Commands) removeProjectGrantMember(ctx context.Context, projectAgg *eventstore.Aggregate, userID, grantID string, cascade bool) eventstore.Command {
@@ -107,19 +191,15 @@ func (c *Commands) removeProjectGrantMember(ctx context.Context, projectAgg *eve
}
}
func (c *Commands) projectGrantMemberWriteModelByID(ctx context.Context, projectID, userID, grantID string) (member *ProjectGrantMemberWriteModel, err error) {
func (c *Commands) projectGrantMemberWriteModelByID(ctx context.Context, projectID, userID, grantID, resourceOwner string) (member *ProjectGrantMemberWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel := NewProjectGrantMemberWriteModel(projectID, userID, grantID)
writeModel := NewProjectGrantMemberWriteModel(projectID, userID, grantID, resourceOwner)
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
if writeModel.State == domain.MemberStateUnspecified || writeModel.State == domain.MemberStateRemoved {
return nil, zerrors.ThrowNotFound(nil, "PROJECT-37fug", "Errors.NotFound")
}
return writeModel, nil
}
@@ -16,10 +16,11 @@ type ProjectGrantMemberWriteModel struct {
State domain.MemberState
}
func NewProjectGrantMemberWriteModel(projectID, userID, grantID string) *ProjectGrantMemberWriteModel {
func NewProjectGrantMemberWriteModel(projectID, userID, grantID, resourceOwner string) *ProjectGrantMemberWriteModel {
return &ProjectGrantMemberWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: projectID,
AggregateID: projectID,
ResourceOwner: resourceOwner,
},
UserID: userID,
GrantID: grantID,
@@ -66,6 +67,7 @@ func (wm *ProjectGrantMemberWriteModel) Reduce() error {
case *project.GrantMemberAddedEvent:
wm.Roles = e.Roles
wm.State = domain.MemberStateActive
wm.ResourceOwner = e.Aggregate().ResourceOwner
case *project.GrantMemberChangedEvent:
wm.Roles = e.Roles
case *project.GrantMemberRemovedEvent:
@@ -80,7 +82,8 @@ func (wm *ProjectGrantMemberWriteModel) Reduce() error {
}
func (wm *ProjectGrantMemberWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(project.AggregateType).
AggregateIDs(wm.AggregateID).
@@ -92,4 +95,5 @@ func (wm *ProjectGrantMemberWriteModel) Query() *eventstore.SearchQueryBuilder {
project.GrantRemovedType,
project.ProjectRemovedType).
Builder()
return query
}
+321 -151
View File
@@ -10,7 +10,6 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
@@ -18,15 +17,15 @@ import (
func TestCommandSide_AddProjectGrantMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
eventstore func(t *testing.T) *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
member *domain.ProjectGrantMember
member *AddProjectGrantMember
}
type res struct {
want *domain.ProjectGrantMember
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
@@ -38,16 +37,12 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
{
name: "invalid member, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
member: &AddProjectGrantMember{
ProjectID: "project1",
},
},
res: res{
@@ -57,19 +52,15 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
{
name: "invalid roles, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
member: &AddProjectGrantMember{
ProjectID: "project1",
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
},
},
res: res{
@@ -79,10 +70,10 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "PROJECT_GRANT_OWNER",
@@ -90,14 +81,11 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
member: &AddProjectGrantMember{
ProjectID: "project1",
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
},
},
res: res{
@@ -107,8 +95,7 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
{
name: "member add uniqueconstraint err, already exists",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -125,15 +112,28 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
eventFromEventPusher(project.NewGrantAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectgrant1",
"grantedorg1",
[]string{"key1"},
),
),
),
),
expectFilter(),
expectPushFailed(zerrors.ThrowAlreadyExists(nil, "ERROR", "internal"),
project.NewProjectGrantMemberAddedEvent(context.Background(),
&project.NewAggregate("project1", "").Aggregate,
&project.NewAggregate("project1", "org1").Aggregate,
"user1",
"projectgrant1",
[]string{"PROJECT_GRANT_OWNER"}...,
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "PROJECT_GRANT_OWNER",
@@ -141,14 +141,11 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
member: &AddProjectGrantMember{
ProjectID: "project1",
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
},
},
res: res{
@@ -158,8 +155,7 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
{
name: "member add, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -176,15 +172,28 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
eventFromEventPusher(project.NewGrantAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectgrant1",
"grantedorg1",
[]string{"key1"},
),
),
),
),
expectFilter(),
expectPush(
project.NewProjectGrantMemberAddedEvent(context.Background(),
&project.NewAggregate("project1", "").Aggregate,
&project.NewAggregate("project1", "org1").Aggregate,
"user1",
"projectgrant1",
[]string{"PROJECT_GRANT_OWNER"}...,
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "PROJECT_GRANT_OWNER",
@@ -192,35 +201,81 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
UserID: "user1",
GrantID: "projectgrant1",
Roles: []string{"PROJECT_GRANT_OWNER"},
member: &AddProjectGrantMember{
ProjectID: "project1",
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
},
},
res: res{
want: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
ID: "project1",
},
},
},
{
name: "member add, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username1",
"firstname1",
"lastname1",
"nickname1",
"displayname1",
language.German,
domain.GenderMale,
"email1",
true,
),
),
),
expectFilter(
eventFromEventPusher(
eventFromEventPusher(project.NewGrantAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectgrant1",
"grantedorg1",
[]string{"key1"},
),
),
),
),
expectFilter(),
),
checkPermission: newMockPermissionCheckNotAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "PROJECT_GRANT_OWNER",
},
},
},
args: args{
member: &AddProjectGrantMember{
ProjectID: "project1",
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
zitadelRoles: tt.fields.zitadelRoles,
eventstore: tt.fields.eventstore(t),
zitadelRoles: tt.fields.zitadelRoles,
checkPermission: tt.fields.checkPermission,
}
got, err := r.AddProjectGrantMember(tt.args.ctx, tt.args.member)
got, err := r.AddProjectGrantMember(context.Background(), tt.args.member)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -236,15 +291,15 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
eventstore func(t *testing.T) *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
member *domain.ProjectGrantMember
member *ChangeProjectGrantMember
}
type res struct {
want *domain.ProjectGrantMember
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
@@ -256,16 +311,12 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
{
name: "invalid member, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
member: &ChangeProjectGrantMember{
ProjectID: "project1",
},
},
res: res{
@@ -275,19 +326,15 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
{
name: "invalid roles, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
member: &ChangeProjectGrantMember{
ProjectID: "project1",
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
},
res: res{
@@ -297,10 +344,20 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
{
name: "member not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewGrantAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectgrant1",
"org2",
[]string{"rol1", "role2"},
),
),
),
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "PROJECT_GRANT_OWNER",
@@ -308,14 +365,11 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
member: &ChangeProjectGrantMember{
ProjectID: "project1",
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
},
},
res: res{
@@ -325,8 +379,17 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
{
name: "member not changed, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewGrantAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectgrant1",
"org2",
[]string{"rol1", "role2"},
),
),
),
expectFilter(
eventFromEventPusher(
project.NewProjectGrantMemberAddedEvent(context.Background(),
@@ -338,6 +401,7 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "PROJECT_GRANT_OWNER",
@@ -345,25 +409,33 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
member: &ChangeProjectGrantMember{
ProjectID: "project1",
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
},
},
res: res{
err: zerrors.IsPreconditionFailed,
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member change, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewGrantAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectgrant1",
"org2",
[]string{"rol1", "role2"},
),
),
),
expectFilter(
eventFromEventPusher(
project.NewProjectGrantMemberAddedEvent(context.Background(),
@@ -383,6 +455,7 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "PROJECT_GRANT_OWNER",
@@ -393,36 +466,75 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"},
member: &ChangeProjectGrantMember{
ProjectID: "project1",
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"},
},
},
res: res{
want: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
ResourceOwner: "org1",
AggregateID: "project1",
},
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member change, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewGrantAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectgrant1",
"org2",
[]string{"rol1", "role2"},
),
),
),
expectFilter(
eventFromEventPusher(
project.NewProjectGrantMemberAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"user1",
"projectgrant1",
[]string{"PROJECT_GRANT_OWNER"}...,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "PROJECT_GRANT_OWNER",
},
{
Role: "PROJECT_GRANT_VIEWER",
},
},
},
args: args{
member: &ChangeProjectGrantMember{
ProjectID: "project1",
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"},
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
zitadelRoles: tt.fields.zitadelRoles,
eventstore: tt.fields.eventstore(t),
zitadelRoles: tt.fields.zitadelRoles,
checkPermission: tt.fields.checkPermission,
}
got, err := r.ChangeProjectGrantMember(tt.args.ctx, tt.args.member)
got, err := r.ChangeProjectGrantMember(context.Background(), tt.args.member)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -430,7 +542,7 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
assertObjectDetails(t, tt.res.want, got)
}
})
}
@@ -438,7 +550,8 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
func TestCommandSide_RemoveProjectGrantMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
@@ -459,9 +572,8 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) {
{
name: "invalid member projectid missing, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -476,9 +588,8 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) {
{
name: "invalid member userid missing, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -493,9 +604,8 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) {
{
name: "invalid member grantid missing, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -508,12 +618,22 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) {
},
},
{
name: "member not existing, not found err",
name: "member not existing, not found ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewGrantAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectgrant1",
"org2",
[]string{"rol1", "role2"},
),
),
),
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -522,14 +642,25 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) {
grantID: "projectgrant1",
},
res: res{
err: zerrors.IsNotFound,
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member remove, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewGrantAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectgrant1",
"org2",
[]string{"rol1", "role2"},
),
),
),
expectFilter(
eventFromEventPusher(
project.NewProjectGrantMemberAddedEvent(context.Background(),
@@ -548,6 +679,7 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -561,11 +693,49 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) {
},
},
},
{
name: "member remove, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewGrantAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectgrant1",
"org2",
[]string{"rol1", "role2"},
),
),
),
expectFilter(
eventFromEventPusher(
project.NewProjectGrantMemberAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"user1",
"projectgrant1",
[]string{"PROJECT_OWNER"}...,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
ctx: context.Background(),
projectID: "project1",
userID: "user1",
grantID: "projectgrant1",
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.RemoveProjectGrantMember(tt.args.ctx, tt.args.projectID, tt.args.userID, tt.args.grantID)
if tt.res.err == nil {
+92 -54
View File
@@ -2,8 +2,9 @@ package command
import (
"context"
"reflect"
"slices"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/project"
@@ -11,18 +12,64 @@ import (
"github.com/zitadel/zitadel/internal/zerrors"
)
func (c *Commands) AddProjectMember(ctx context.Context, member *domain.Member, resourceOwner string) (_ *domain.Member, err error) {
type AddProjectMember struct {
ResourceOwner string
ProjectID string
UserID string
Roles []string
}
func (i *AddProjectMember) IsValid(zitadelRoles []authz.RoleMapping) error {
if i.ProjectID == "" || i.UserID == "" || len(i.Roles) == 0 {
return zerrors.ThrowInvalidArgument(nil, "PROJECT-W8m4l", "Errors.Project.Member.Invalid")
}
if len(domain.CheckForInvalidRoles(i.Roles, domain.ProjectRolePrefix, zitadelRoles)) > 0 {
return zerrors.ThrowInvalidArgument(nil, "PROJECT-3m9ds", "Errors.Project.Member.Invalid")
}
return nil
}
func (c *Commands) AddProjectMember(ctx context.Context, member *AddProjectMember) (_ *domain.ObjectDetails, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
addedMember := NewProjectMemberWriteModel(member.AggregateID, member.UserID, resourceOwner)
projectAgg := ProjectAggregateFromWriteModel(&addedMember.WriteModel)
event, err := c.addProjectMember(ctx, projectAgg, addedMember, member)
if err := member.IsValid(c.zitadelRoles); err != nil {
return nil, err
}
_, err = c.checkUserExists(ctx, member.UserID, "")
if err != nil {
return nil, err
}
projectResourceOwner, err := c.checkProjectExists(ctx, member.ProjectID, member.ResourceOwner)
if err != nil {
return nil, err
}
// resourceowner of the member if not provided is the resourceowner of the project
if member.ResourceOwner == "" {
member.ResourceOwner = projectResourceOwner
}
addedMember, err := c.projectMemberWriteModelByID(ctx, member.ProjectID, member.UserID, member.ResourceOwner)
if err != nil {
return nil, err
}
// error if provided resourceowner is not equal to the resourceowner of the project
if projectResourceOwner != addedMember.ResourceOwner {
return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-0l10S9OmZV", "Errors.Project.Member.Invalid")
}
if err := c.checkPermissionUpdateProjectMember(ctx, addedMember.ResourceOwner, addedMember.AggregateID); err != nil {
return nil, err
}
if addedMember.State.Exists() {
return nil, zerrors.ThrowAlreadyExists(nil, "PROJECT-PtXi1", "Errors.Project.Member.AlreadyExists")
}
pushedEvents, err := c.eventstore.Push(ctx, event)
pushedEvents, err := c.eventstore.Push(ctx,
project.NewProjectMemberAddedEvent(ctx,
ProjectAggregateFromWriteModelWithCTX(ctx, &addedMember.WriteModel),
member.UserID,
member.Roles...,
),
)
if err != nil {
return nil, err
}
@@ -31,53 +78,46 @@ func (c *Commands) AddProjectMember(ctx context.Context, member *domain.Member,
return nil, err
}
return memberWriteModelToMember(&addedMember.MemberWriteModel), nil
return writeModelToObjectDetails(&addedMember.WriteModel), nil
}
func (c *Commands) addProjectMember(ctx context.Context, projectAgg *eventstore.Aggregate, addedMember *ProjectMemberWriteModel, member *domain.Member) (_ eventstore.Command, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
type ChangeProjectMember struct {
ResourceOwner string
ProjectID string
UserID string
Roles []string
}
if !member.IsValid() {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-W8m4l", "Errors.Project.Member.Invalid")
func (i *ChangeProjectMember) IsValid(zitadelRoles []authz.RoleMapping) error {
if i.ProjectID == "" || i.UserID == "" || len(i.Roles) == 0 {
return zerrors.ThrowInvalidArgument(nil, "PROJECT-LiaZi", "Errors.Project.Member.Invalid")
}
if len(domain.CheckForInvalidRoles(member.Roles, domain.ProjectRolePrefix, c.zitadelRoles)) > 0 {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-3m9ds", "Errors.Project.Member.Invalid")
if len(domain.CheckForInvalidRoles(i.Roles, domain.ProjectRolePrefix, zitadelRoles)) > 0 {
return zerrors.ThrowInvalidArgument(nil, "PROJECT-3m9d", "Errors.Project.Member.Invalid")
}
err = c.checkUserExists(ctx, addedMember.UserID, "")
if err != nil {
return nil, err
}
err = c.eventstore.FilterToQueryReducer(ctx, addedMember)
if err != nil {
return nil, err
}
if addedMember.State == domain.MemberStateActive {
return nil, zerrors.ThrowAlreadyExists(nil, "PROJECT-PtXi1", "Errors.Project.Member.AlreadyExists")
}
return project.NewProjectMemberAddedEvent(ctx, projectAgg, member.UserID, member.Roles...), nil
return nil
}
// ChangeProjectMember updates an existing member
func (c *Commands) ChangeProjectMember(ctx context.Context, member *domain.Member, resourceOwner string) (*domain.Member, error) {
if !member.IsValid() {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-LiaZi", "Errors.Project.Member.Invalid")
}
if len(domain.CheckForInvalidRoles(member.Roles, domain.ProjectRolePrefix, c.zitadelRoles)) > 0 {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-3m9d", "Errors.Project.Member.Invalid")
}
existingMember, err := c.projectMemberWriteModelByID(ctx, member.AggregateID, member.UserID, resourceOwner)
if err != nil {
func (c *Commands) ChangeProjectMember(ctx context.Context, member *ChangeProjectMember) (*domain.ObjectDetails, error) {
if err := member.IsValid(c.zitadelRoles); err != nil {
return nil, err
}
if reflect.DeepEqual(existingMember.Roles, member.Roles) {
return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-LiaZi", "Errors.Project.Member.RolesNotChanged")
existingMember, err := c.projectMemberWriteModelByID(ctx, member.ProjectID, member.UserID, member.ResourceOwner)
if err != nil {
return nil, err
}
projectAgg := ProjectAggregateFromWriteModel(&existingMember.MemberWriteModel.WriteModel)
if !existingMember.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.NotFound")
}
if err := c.checkPermissionUpdateProjectMember(ctx, existingMember.ResourceOwner, existingMember.AggregateID); err != nil {
return nil, err
}
if slices.Compare(existingMember.Roles, member.Roles) == 0 {
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, project.NewProjectMemberChangedEvent(ctx, projectAgg, member.UserID, member.Roles...))
if err != nil {
return nil, err
@@ -88,33 +128,35 @@ func (c *Commands) ChangeProjectMember(ctx context.Context, member *domain.Membe
return nil, err
}
return memberWriteModelToMember(&existingMember.MemberWriteModel), nil
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
func (c *Commands) RemoveProjectMember(ctx context.Context, projectID, userID, resourceOwner string) (*domain.ObjectDetails, error) {
if projectID == "" || userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-66mHd", "Errors.Project.Member.Invalid")
}
m, err := c.projectMemberWriteModelByID(ctx, projectID, userID, resourceOwner)
if err != nil && !zerrors.IsNotFound(err) {
existingMember, err := c.projectMemberWriteModelByID(ctx, projectID, userID, resourceOwner)
if err != nil {
return nil, err
}
if zerrors.IsNotFound(err) {
// empty response because we have no data that match the request
return &domain.ObjectDetails{}, nil
if !existingMember.State.Exists() {
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
if err := c.checkPermissionDeleteProjectMember(ctx, existingMember.ResourceOwner, existingMember.AggregateID); err != nil {
return nil, err
}
projectAgg := ProjectAggregateFromWriteModel(&m.MemberWriteModel.WriteModel)
projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel)
removeEvent := c.removeProjectMember(ctx, projectAgg, userID, false)
pushedEvents, err := c.eventstore.Push(ctx, removeEvent)
if err != nil {
return nil, err
}
err = AppendAndReduce(m, pushedEvents...)
err = AppendAndReduce(existingMember, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&m.WriteModel), nil
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
func (c *Commands) removeProjectMember(ctx context.Context, projectAgg *eventstore.Aggregate, userID string, cascade bool) eventstore.Command {
@@ -138,9 +180,5 @@ func (c *Commands) projectMemberWriteModelByID(ctx context.Context, projectID, u
return nil, err
}
if writeModel.State == domain.MemberStateUnspecified || writeModel.State == domain.MemberStateRemoved {
return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.NotFound")
}
return writeModel, nil
}
+269 -165
View File
@@ -10,7 +10,6 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
@@ -18,16 +17,15 @@ import (
func TestCommandSide_AddProjectMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
eventstore func(t *testing.T) *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
member *domain.Member
resourceOwner string
member *AddProjectMember
}
type res struct {
want *domain.Member
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
@@ -39,18 +37,14 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
{
name: "invalid member, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
member: &AddProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -59,20 +53,16 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
{
name: "invalid roles, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
member: &AddProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -81,10 +71,10 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleProjectOwner,
@@ -92,15 +82,12 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
member: &AddProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsPreconditionFailed,
@@ -109,8 +96,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
{
name: "member already exists, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -127,6 +113,19 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(
context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project",
false,
false,
false,
domain.PrivateLabelingSettingUnspecified,
),
),
),
expectFilter(
eventFromEventPusher(
project.NewProjectMemberAddedEvent(context.Background(),
@@ -136,6 +135,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleProjectOwner,
@@ -143,15 +143,12 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
member: &AddProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorAlreadyExists,
@@ -160,8 +157,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
{
name: "member add uniqueconstraint err, already exists",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -178,6 +174,19 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(
context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project",
false,
false,
false,
domain.PrivateLabelingSettingUnspecified,
),
),
),
expectFilter(),
expectPushFailed(zerrors.ThrowAlreadyExists(nil, "ERROR", "internal"),
project.NewProjectMemberAddedEvent(context.Background(),
@@ -187,6 +196,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleProjectOwner,
@@ -194,15 +204,12 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
member: &AddProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorAlreadyExists,
@@ -211,8 +218,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
{
name: "member add, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -229,6 +235,19 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(
context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project",
false,
false,
false,
domain.PrivateLabelingSettingUnspecified,
),
),
),
expectFilter(),
expectPush(
project.NewProjectMemberAddedEvent(context.Background(),
@@ -238,6 +257,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleProjectOwner,
@@ -245,35 +265,81 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
member: &AddProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
resourceOwner: "org1",
},
res: res{
want: &domain.Member{
ObjectRoot: models.ObjectRoot{
ResourceOwner: "org1",
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{domain.RoleProjectOwner},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
}, {
name: "member add, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username1",
"firstname1",
"lastname1",
"nickname1",
"displayname1",
language.German,
domain.GenderMale,
"email1",
true,
),
),
),
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(
context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project",
false,
false,
false,
domain.PrivateLabelingSettingUnspecified,
),
),
),
expectFilter(),
),
checkPermission: newMockPermissionCheckNotAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleProjectOwner,
},
},
},
args: args{
member: &AddProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
zitadelRoles: tt.fields.zitadelRoles,
eventstore: tt.fields.eventstore(t),
zitadelRoles: tt.fields.zitadelRoles,
checkPermission: tt.fields.checkPermission,
}
got, err := r.AddProjectMember(tt.args.ctx, tt.args.member, tt.args.resourceOwner)
got, err := r.AddProjectMember(context.Background(), tt.args.member)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -281,7 +347,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
assertObjectDetails(t, tt.res.want, got)
}
})
}
@@ -289,16 +355,15 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
func TestCommandSide_ChangeProjectMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
eventstore func(t *testing.T) *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
member *domain.Member
resourceOwner string
member *ChangeProjectMember
}
type res struct {
want *domain.Member
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
@@ -310,18 +375,14 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
{
name: "invalid member, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
member: &ChangeProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -330,20 +391,16 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
{
name: "invalid roles, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
member: &ChangeProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -352,10 +409,10 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
{
name: "member not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleProjectOwner,
@@ -363,15 +420,12 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
member: &ChangeProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsNotFound,
@@ -380,8 +434,7 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
{
name: "member not changed, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewProjectMemberAddedEvent(context.Background(),
@@ -392,6 +445,7 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleProjectOwner,
@@ -399,25 +453,23 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
member: &ChangeProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsPreconditionFailed,
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member change, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewProjectMemberAddedEvent(context.Background(),
@@ -435,6 +487,7 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleProjectOwner,
@@ -445,35 +498,64 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{"PROJECT_OWNER", "PROJECT_VIEWER"},
member: &ChangeProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER", "PROJECT_VIEWER"},
},
resourceOwner: "org1",
},
res: res{
want: &domain.Member{
ObjectRoot: models.ObjectRoot{
ResourceOwner: "org1",
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{domain.RoleProjectOwner, "PROJECT_VIEWER"},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member change, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewProjectMemberAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"user1",
[]string{"PROJECT_OWNER"}...,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleProjectOwner,
},
{
Role: "PROJECT_VIEWER",
},
},
},
args: args{
member: &ChangeProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER", "PROJECT_VIEWER"},
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
zitadelRoles: tt.fields.zitadelRoles,
eventstore: tt.fields.eventstore(t),
zitadelRoles: tt.fields.zitadelRoles,
checkPermission: tt.fields.checkPermission,
}
got, err := r.ChangeProjectMember(tt.args.ctx, tt.args.member, tt.args.resourceOwner)
got, err := r.ChangeProjectMember(context.Background(), tt.args.member)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -481,7 +563,7 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
assertObjectDetails(t, tt.res.want, got)
}
})
}
@@ -489,10 +571,10 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
func TestCommandSide_RemoveProjectMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
projectID string
userID string
resourceOwner string
@@ -510,12 +592,10 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) {
{
name: "invalid member projectid missing, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
projectID: "",
userID: "user1",
resourceOwner: "org1",
@@ -527,12 +607,10 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) {
{
name: "invalid member userid missing, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
projectID: "project1",
userID: "",
resourceOwner: "org1",
@@ -544,26 +622,26 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) {
{
name: "member not existing, empty object details result",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
projectID: "project1",
userID: "user1",
resourceOwner: "org1",
},
res: res{
want: &domain.ObjectDetails{},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member remove, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewProjectMemberAddedEvent(context.Background(),
@@ -580,9 +658,9 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
projectID: "project1",
userID: "user1",
resourceOwner: "org1",
@@ -593,13 +671,39 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) {
},
},
},
{
name: "member remove, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewProjectMemberAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"user1",
[]string{"PROJECT_OWNER"}...,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
projectID: "project1",
userID: "user1",
resourceOwner: "org1",
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.RemoveProjectMember(tt.args.ctx, tt.args.projectID, tt.args.userID, tt.args.resourceOwner)
got, err := r.RemoveProjectMember(context.Background(), tt.args.projectID, tt.args.userID, tt.args.resourceOwner)
if tt.res.err == nil {
assert.NoError(t, err)
}
+5 -5
View File
@@ -194,7 +194,7 @@ func (c *Commands) RemoveUser(ctx context.Context, userID, resourceOwner string,
events = append(events, user.NewUserRemovedEvent(ctx, userAgg, existingUser.UserName, existingUser.IDPLinks, domainPolicy.UserLoginMustBeDomain))
for _, grantID := range cascadingGrantIDs {
removeEvent, _, err := c.removeUserGrant(ctx, grantID, "", true)
removeEvent, _, err := c.removeUserGrant(ctx, grantID, "", true, false, nil)
if err != nil {
logging.WithFields("usergrantid", grantID).WithError(err).Warn("could not cascade remove role on user grant")
continue
@@ -327,18 +327,18 @@ func (c *Commands) UserDomainClaimedSent(ctx context.Context, orgID, userID stri
return err
}
func (c *Commands) checkUserExists(ctx context.Context, userID, resourceOwner string) (err error) {
func (c *Commands) checkUserExists(ctx context.Context, userID, resourceOwner string) (_ string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
existingUser, err := c.userWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return err
return "", err
}
if !isUserStateExists(existingUser.UserState) {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-uXHNj", "Errors.User.NotFound")
return "", zerrors.ThrowPreconditionFailed(nil, "COMMAND-uXHNj", "Errors.User.NotFound")
}
return nil
return existingUser.ResourceOwner, nil
}
func (c *Commands) userWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *UserWriteModel, err error) {
+144 -76
View File
@@ -2,7 +2,7 @@ package command
import (
"context"
"reflect"
"slices"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
@@ -15,11 +15,14 @@ import (
"github.com/zitadel/zitadel/internal/zerrors"
)
func (c *Commands) AddUserGrant(ctx context.Context, usergrant *domain.UserGrant, resourceOwner string) (_ *domain.UserGrant, err error) {
// AddUserGrant authorizes a user for a project with the given role keys.
// The project must be owned by or granted to the resourceOwner.
// If the resourceOwner is nil, the project must be owned by the project that belongs to usergrant.ProjectID.
func (c *Commands) AddUserGrant(ctx context.Context, usergrant *domain.UserGrant, check UserGrantPermissionCheck) (_ *domain.UserGrant, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
event, addedUserGrant, err := c.addUserGrant(ctx, usergrant, resourceOwner)
event, addedUserGrant, err := c.addUserGrant(ctx, usergrant, check)
if err != nil {
return nil, err
}
@@ -35,11 +38,11 @@ func (c *Commands) AddUserGrant(ctx context.Context, usergrant *domain.UserGrant
return userGrantWriteModelToUserGrant(addedUserGrant), nil
}
func (c *Commands) addUserGrant(ctx context.Context, userGrant *domain.UserGrant, resourceOwner string) (command eventstore.Command, _ *UserGrantWriteModel, err error) {
func (c *Commands) addUserGrant(ctx context.Context, userGrant *domain.UserGrant, check UserGrantPermissionCheck) (command eventstore.Command, _ *UserGrantWriteModel, err error) {
if !userGrant.IsValid() {
return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-kVfMa", "Errors.UserGrant.Invalid")
}
err = c.checkUserGrantPreCondition(ctx, userGrant, resourceOwner)
err = c.checkUserGrantPreCondition(ctx, userGrant, check)
if err != nil {
return nil, nil, err
}
@@ -48,7 +51,7 @@ func (c *Commands) addUserGrant(ctx context.Context, userGrant *domain.UserGrant
return nil, nil, err
}
addedUserGrant := NewUserGrantWriteModel(userGrant.AggregateID, resourceOwner)
addedUserGrant := NewUserGrantWriteModel(userGrant.AggregateID, userGrant.ResourceOwner)
userGrantAgg := UserGrantAggregateFromWriteModel(&addedUserGrant.WriteModel)
command = usergrant.NewUserGrantAddedEvent(
ctx,
@@ -61,54 +64,51 @@ func (c *Commands) addUserGrant(ctx context.Context, userGrant *domain.UserGrant
return command, addedUserGrant, nil
}
func (c *Commands) ChangeUserGrant(ctx context.Context, userGrant *domain.UserGrant, resourceOwner string) (_ *domain.UserGrant, err error) {
event, changedUserGrant, err := c.changeUserGrant(ctx, userGrant, resourceOwner, false)
func (c *Commands) ChangeUserGrant(ctx context.Context, userGrant *domain.UserGrant, cascade, ignoreUnchanged bool, check UserGrantPermissionCheck) (_ *domain.UserGrant, err error) {
if userGrant.AggregateID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-3M0sd", "Errors.UserGrant.Invalid")
}
existingUserGrant, err := c.userGrantWriteModelByID(ctx, userGrant.AggregateID, "")
if err != nil {
return nil, err
}
if existingUserGrant.State == domain.UserGrantStateUnspecified || existingUserGrant.State == domain.UserGrantStateRemoved {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.UserGrant.NotFound")
}
grantUnchanged := slices.Equal(existingUserGrant.RoleKeys, userGrant.RoleKeys)
if grantUnchanged {
if ignoreUnchanged {
return userGrantWriteModelToUserGrant(existingUserGrant), nil
}
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Rs8fy", "Errors.UserGrant.NotChanged")
}
userGrant.UserID = existingUserGrant.UserID
userGrant.ProjectID = existingUserGrant.ProjectID
userGrant.ProjectGrantID = existingUserGrant.ProjectGrantID
userGrant.ResourceOwner = existingUserGrant.ResourceOwner
err = c.checkUserGrantPreCondition(ctx, userGrant, check)
if err != nil {
return nil, err
}
changedUserGrant := NewUserGrantWriteModel(userGrant.AggregateID, userGrant.ResourceOwner)
userGrantAgg := UserGrantAggregateFromWriteModel(&changedUserGrant.WriteModel)
var event eventstore.Command = usergrant.NewUserGrantChangedEvent(ctx, userGrantAgg, existingUserGrant.UserID, userGrant.RoleKeys)
if cascade {
event = usergrant.NewUserGrantCascadeChangedEvent(ctx, userGrantAgg, userGrant.RoleKeys)
}
pushedEvents, err := c.eventstore.Push(ctx, event)
if err != nil {
return nil, err
}
err = AppendAndReduce(changedUserGrant, pushedEvents...)
err = AppendAndReduce(existingUserGrant, pushedEvents...)
if err != nil {
return nil, err
}
return userGrantWriteModelToUserGrant(changedUserGrant), nil
}
func (c *Commands) changeUserGrant(ctx context.Context, userGrant *domain.UserGrant, resourceOwner string, cascade bool) (_ eventstore.Command, _ *UserGrantWriteModel, err error) {
if userGrant.AggregateID == "" {
return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-3M0sd", "Errors.UserGrant.Invalid")
}
existingUserGrant, err := c.userGrantWriteModelByID(ctx, userGrant.AggregateID, userGrant.ResourceOwner)
if err != nil {
return nil, nil, err
}
err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID)
if err != nil {
return nil, nil, err
}
if existingUserGrant.State == domain.UserGrantStateUnspecified || existingUserGrant.State == domain.UserGrantStateRemoved {
return nil, nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.UserGrant.NotFound")
}
if reflect.DeepEqual(existingUserGrant.RoleKeys, userGrant.RoleKeys) {
return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Rs8fy", "Errors.UserGrant.NotChanged")
}
userGrant.ProjectID = existingUserGrant.ProjectID
userGrant.ProjectGrantID = existingUserGrant.ProjectGrantID
err = c.checkUserGrantPreCondition(ctx, userGrant, resourceOwner)
if err != nil {
return nil, nil, err
}
changedUserGrant := NewUserGrantWriteModel(userGrant.AggregateID, resourceOwner)
userGrantAgg := UserGrantAggregateFromWriteModel(&changedUserGrant.WriteModel)
if cascade {
return usergrant.NewUserGrantCascadeChangedEvent(ctx, userGrantAgg, userGrant.RoleKeys), existingUserGrant, nil
}
return usergrant.NewUserGrantChangedEvent(ctx, userGrantAgg, existingUserGrant.UserID, userGrant.RoleKeys), existingUserGrant, nil
return userGrantWriteModelToUserGrant(existingUserGrant), nil
}
func (c *Commands) removeRoleFromUserGrant(ctx context.Context, userGrantID string, roleKeys []string, cascade bool) (_ eventstore.Command, err error) {
@@ -144,8 +144,8 @@ func (c *Commands) removeRoleFromUserGrant(ctx context.Context, userGrantID stri
return usergrant.NewUserGrantChangedEvent(ctx, userGrantAgg, existingUserGrant.UserID, existingUserGrant.RoleKeys), nil
}
func (c *Commands) DeactivateUserGrant(ctx context.Context, grantID, resourceOwner string) (objectDetails *domain.ObjectDetails, err error) {
if grantID == "" || resourceOwner == "" {
func (c *Commands) DeactivateUserGrant(ctx context.Context, grantID string, resourceOwner string, check UserGrantPermissionCheck) (objectDetails *domain.ObjectDetails, err error) {
if grantID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-N2OhG", "Errors.UserGrant.IDMissing")
}
@@ -157,14 +157,17 @@ func (c *Commands) DeactivateUserGrant(ctx context.Context, grantID, resourceOwn
return nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.UserGrant.NotFound")
}
if existingUserGrant.State != domain.UserGrantStateActive {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-1S9gx", "Errors.UserGrant.NotActive")
return writeModelToObjectDetails(&existingUserGrant.WriteModel), nil
}
if check != nil {
err = check(existingUserGrant.ProjectID, existingUserGrant.ProjectGrantID)(existingUserGrant.ResourceOwner, "")
} else {
err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID)
}
err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID)
if err != nil {
return nil, err
}
deactivateUserGrant := NewUserGrantWriteModel(grantID, resourceOwner)
deactivateUserGrant := NewUserGrantWriteModel(grantID, existingUserGrant.ResourceOwner)
userGrantAgg := UserGrantAggregateFromWriteModel(&deactivateUserGrant.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, usergrant.NewUserGrantDeactivatedEvent(ctx, userGrantAgg))
if err != nil {
@@ -177,8 +180,8 @@ func (c *Commands) DeactivateUserGrant(ctx context.Context, grantID, resourceOwn
return writeModelToObjectDetails(&existingUserGrant.WriteModel), nil
}
func (c *Commands) ReactivateUserGrant(ctx context.Context, grantID, resourceOwner string) (objectDetails *domain.ObjectDetails, err error) {
if grantID == "" || resourceOwner == "" {
func (c *Commands) ReactivateUserGrant(ctx context.Context, grantID string, resourceOwner string, check UserGrantPermissionCheck) (objectDetails *domain.ObjectDetails, err error) {
if grantID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Qxy8v", "Errors.UserGrant.IDMissing")
}
@@ -190,13 +193,17 @@ func (c *Commands) ReactivateUserGrant(ctx context.Context, grantID, resourceOwn
return nil, zerrors.ThrowNotFound(nil, "COMMAND-Lp0gs", "Errors.UserGrant.NotFound")
}
if existingUserGrant.State != domain.UserGrantStateInactive {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-1ML0v", "Errors.UserGrant.NotInactive")
return writeModelToObjectDetails(&existingUserGrant.WriteModel), nil
}
if check != nil {
err = check(existingUserGrant.ProjectID, existingUserGrant.ProjectGrantID)(existingUserGrant.ResourceOwner, "")
} else {
err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID)
}
err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID)
if err != nil {
return nil, err
}
deactivateUserGrant := NewUserGrantWriteModel(grantID, resourceOwner)
deactivateUserGrant := NewUserGrantWriteModel(grantID, existingUserGrant.ResourceOwner)
userGrantAgg := UserGrantAggregateFromWriteModel(&deactivateUserGrant.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, usergrant.NewUserGrantReactivatedEvent(ctx, userGrantAgg))
if err != nil {
@@ -209,12 +216,14 @@ func (c *Commands) ReactivateUserGrant(ctx context.Context, grantID, resourceOwn
return writeModelToObjectDetails(&existingUserGrant.WriteModel), nil
}
func (c *Commands) RemoveUserGrant(ctx context.Context, grantID, resourceOwner string) (objectDetails *domain.ObjectDetails, err error) {
event, existingUserGrant, err := c.removeUserGrant(ctx, grantID, resourceOwner, false)
func (c *Commands) RemoveUserGrant(ctx context.Context, grantID string, resourceOwner string, ignoreNotFound bool, check UserGrantPermissionCheck) (objectDetails *domain.ObjectDetails, err error) {
event, existingUserGrant, err := c.removeUserGrant(ctx, grantID, resourceOwner, false, ignoreNotFound, check)
if err != nil {
return nil, err
}
if event == nil {
return writeModelToObjectDetails(&existingUserGrant.WriteModel), nil
}
pushedEvents, err := c.eventstore.Push(ctx, event)
if err != nil {
return nil, err
@@ -232,7 +241,7 @@ func (c *Commands) BulkRemoveUserGrant(ctx context.Context, grantIDs []string, r
}
events := make([]eventstore.Command, len(grantIDs))
for i, grantID := range grantIDs {
event, _, err := c.removeUserGrant(ctx, grantID, resourceOwner, false)
event, _, err := c.removeUserGrant(ctx, grantID, resourceOwner, false, false, nil)
if err != nil {
return err
}
@@ -242,7 +251,7 @@ func (c *Commands) BulkRemoveUserGrant(ctx context.Context, grantIDs []string, r
return err
}
func (c *Commands) removeUserGrant(ctx context.Context, grantID, resourceOwner string, cascade bool) (_ eventstore.Command, writeModel *UserGrantWriteModel, err error) {
func (c *Commands) removeUserGrant(ctx context.Context, grantID string, resourceOwner string, cascade, ignoreNotFound bool, check UserGrantPermissionCheck) (_ eventstore.Command, writeModel *UserGrantWriteModel, err error) {
if grantID == "" {
return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-J9sc5", "Errors.UserGrant.IDMissing")
}
@@ -252,15 +261,22 @@ func (c *Commands) removeUserGrant(ctx context.Context, grantID, resourceOwner s
return nil, nil, err
}
if existingUserGrant.State == domain.UserGrantStateUnspecified || existingUserGrant.State == domain.UserGrantStateRemoved {
if ignoreNotFound {
return nil, existingUserGrant, nil
}
return nil, nil, zerrors.ThrowNotFound(nil, "COMMAND-1My0t", "Errors.UserGrant.NotFound")
}
if !cascade {
if !cascade && check == nil {
err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID)
if err != nil {
return nil, nil, err
}
}
if check != nil {
if err = check(existingUserGrant.ProjectID, existingUserGrant.ProjectGrantID)(existingUserGrant.ResourceOwner, ""); err != nil {
return nil, nil, err
}
}
removeUserGrant := NewUserGrantWriteModel(grantID, existingUserGrant.ResourceOwner)
userGrantAgg := UserGrantAggregateFromWriteModel(&removeUserGrant.WriteModel)
if cascade {
@@ -279,7 +295,7 @@ func (c *Commands) removeUserGrant(ctx context.Context, grantID, resourceOwner s
existingUserGrant.ProjectGrantID), existingUserGrant, nil
}
func (c *Commands) userGrantWriteModelByID(ctx context.Context, userGrantID, resourceOwner string) (writeModel *UserGrantWriteModel, err error) {
func (c *Commands) userGrantWriteModelByID(ctx context.Context, userGrantID string, resourceOwner string) (writeModel *UserGrantWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -291,31 +307,46 @@ func (c *Commands) userGrantWriteModelByID(ctx context.Context, userGrantID, res
return writeModel, nil
}
func (c *Commands) checkUserGrantPreCondition(ctx context.Context, usergrant *domain.UserGrant, resourceOwner string) (err error) {
func (c *Commands) checkUserGrantPreCondition(ctx context.Context, usergrant *domain.UserGrant, check UserGrantPermissionCheck) (err error) {
if !authz.GetFeatures(ctx).ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeUserGrant) {
return c.checkUserGrantPreConditionOld(ctx, usergrant, resourceOwner)
return c.checkUserGrantPreConditionOld(ctx, usergrant, check)
}
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if err := c.checkUserExists(ctx, usergrant.UserID, ""); err != nil {
if _, err := c.checkUserExists(ctx, usergrant.UserID, ""); err != nil {
return err
}
existingRoleKeys, err := c.searchUserGrantPreConditionState(ctx, usergrant, resourceOwner)
if usergrant.ProjectGrantID != "" || usergrant.ResourceOwner == "" {
projectOwner, grantID, err := c.searchProjectOwnerAndGrantID(ctx, usergrant.ProjectID, "")
if err != nil {
return err
}
if usergrant.ResourceOwner == "" {
usergrant.ResourceOwner = projectOwner
}
if usergrant.ProjectGrantID == "" {
usergrant.ProjectGrantID = grantID
}
}
existingRoleKeys, err := c.searchUserGrantPreConditionState(ctx, usergrant)
if err != nil {
return err
}
if usergrant.HasInvalidRoles(existingRoleKeys) {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-mm9F4", "Errors.Project.Role.NotFound")
}
return nil
if check != nil {
return check(usergrant.ProjectID, usergrant.ProjectGrantID)(usergrant.ResourceOwner, "")
}
return checkExplicitProjectPermission(ctx, usergrant.ProjectGrantID, usergrant.ProjectID)
}
// this code needs to be rewritten anyways as soon as we improved the fields handling
//
//nolint:gocognit
func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGrant *domain.UserGrant, resourceOwner string) (existingRoleKeys []string, err error) {
func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGrant *domain.UserGrant) (existingRoleKeys []string, err error) {
criteria := []map[eventstore.FieldType]any{
// project state query
{
@@ -327,7 +358,7 @@ func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGra
// granted org query
{
eventstore.FieldTypeAggregateType: org.AggregateType,
eventstore.FieldTypeAggregateID: resourceOwner,
eventstore.FieldTypeAggregateID: userGrant.ResourceOwner,
eventstore.FieldTypeFieldName: org.OrgStateSearchField,
eventstore.FieldTypeObjectType: org.OrgSearchType,
},
@@ -386,7 +417,7 @@ func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGra
case project.ProjectGrantGrantedOrgIDSearchField:
var orgID string
err := result.Value.Unmarshal(&orgID)
if err != nil || orgID != resourceOwner {
if err != nil || orgID != userGrant.ResourceOwner {
return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound")
}
case project.ProjectGrantStateSearchField:
@@ -425,26 +456,63 @@ func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGra
return existingRoleKeys, nil
}
func (c *Commands) checkUserGrantPreConditionOld(ctx context.Context, usergrant *domain.UserGrant, resourceOwner string) (err error) {
func (c *Commands) checkUserGrantPreConditionOld(ctx context.Context, usergrant *domain.UserGrant, check UserGrantPermissionCheck) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
preConditions := NewUserGrantPreConditionReadModel(usergrant.UserID, usergrant.ProjectID, usergrant.ProjectGrantID, resourceOwner)
preConditions := NewUserGrantPreConditionReadModel(usergrant.UserID, usergrant.ProjectID, usergrant.ProjectGrantID, usergrant.ResourceOwner)
err = c.eventstore.FilterToQueryReducer(ctx, preConditions)
if err != nil {
return err
}
if usergrant.ResourceOwner == "" {
usergrant.ResourceOwner = preConditions.ProjectResourceOwner
}
if usergrant.ProjectGrantID == "" {
usergrant.ProjectGrantID = preConditions.ProjectGrantID
}
if !preConditions.UserExists {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-4f8sg", "Errors.User.NotFound")
}
if usergrant.ProjectGrantID == "" && !preConditions.ProjectExists {
projectIsOwned := usergrant.ResourceOwner == "" || usergrant.ResourceOwner == preConditions.ProjectResourceOwner
if projectIsOwned && !preConditions.ProjectExists {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-3n77S", "Errors.Project.NotFound")
}
if usergrant.ProjectGrantID != "" && !preConditions.ProjectGrantExists {
if !projectIsOwned && !preConditions.ProjectGrantExists {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-4m9ff", "Errors.Project.Grant.NotFound")
}
if usergrant.HasInvalidRoles(preConditions.ExistingRoleKeys) {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-mm9F4", "Errors.Project.Role.NotFound")
}
return nil
if check != nil {
return check(usergrant.ProjectID, usergrant.ProjectGrantID)(usergrant.ResourceOwner, "")
}
return checkExplicitProjectPermission(ctx, usergrant.ProjectGrantID, usergrant.ProjectID)
}
func (c *Commands) searchProjectOwnerAndGrantID(ctx context.Context, projectID string, grantedOrgID string) (projectOwner string, grantID string, err error) {
grantIDQuery := map[eventstore.FieldType]any{
eventstore.FieldTypeAggregateType: project.AggregateType,
eventstore.FieldTypeAggregateID: projectID,
eventstore.FieldTypeFieldName: project.ProjectGrantGrantedOrgIDSearchField,
}
if grantedOrgID != "" {
grantIDQuery[eventstore.FieldTypeValue] = grantedOrgID
grantIDQuery[eventstore.FieldTypeObjectType] = project.ProjectGrantSearchType
}
results, err := c.eventstore.Search(ctx, grantIDQuery)
if err != nil {
return "", "", err
}
for _, result := range results {
projectOwner = result.Aggregate.ResourceOwner
if grantedOrgID != "" && grantedOrgID == projectOwner {
return projectOwner, "", nil
}
if result.Object.Type == project.ProjectGrantSearchType {
return projectOwner, result.Object.ID, nil
}
}
return projectOwner, grantID, err
}
+17 -11
View File
@@ -36,6 +36,7 @@ func (wm *UserGrantWriteModel) Reduce() error {
wm.ProjectGrantID = e.ProjectGrantID
wm.RoleKeys = e.RoleKeys
wm.State = domain.UserGrantStateActive
wm.ResourceOwner = e.Aggregate().ResourceOwner
case *usergrant.UserGrantChangedEvent:
wm.RoleKeys = e.RoleKeys
case *usergrant.UserGrantCascadeChangedEvent:
@@ -86,17 +87,18 @@ func UserGrantAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Agg
type UserGrantPreConditionReadModel struct {
eventstore.WriteModel
UserID string
ProjectID string
ProjectGrantID string
ResourceOwner string
UserExists bool
ProjectExists bool
ProjectGrantExists bool
ExistingRoleKeys []string
UserID string
ProjectID string
ProjectResourceOwner string
ProjectGrantID string
ResourceOwner string
UserExists bool
ProjectExists bool
ProjectGrantExists bool
ExistingRoleKeys []string
}
func NewUserGrantPreConditionReadModel(userID, projectID, projectGrantID, resourceOwner string) *UserGrantPreConditionReadModel {
func NewUserGrantPreConditionReadModel(userID, projectID, projectGrantID string, resourceOwner string) *UserGrantPreConditionReadModel {
return &UserGrantPreConditionReadModel{
UserID: userID,
ProjectID: projectID,
@@ -117,15 +119,19 @@ func (wm *UserGrantPreConditionReadModel) Reduce() error {
case *user.UserRemovedEvent:
wm.UserExists = false
case *project.ProjectAddedEvent:
if wm.ProjectGrantID == "" && wm.ResourceOwner == e.Aggregate().ResourceOwner {
if wm.ResourceOwner == "" || wm.ResourceOwner == e.Aggregate().ResourceOwner {
wm.ProjectExists = true
}
wm.ProjectResourceOwner = e.Aggregate().ResourceOwner
case *project.ProjectRemovedEvent:
wm.ProjectExists = false
case *project.GrantAddedEvent:
if wm.ProjectGrantID == e.GrantID && wm.ResourceOwner == e.GrantedOrgID {
if (wm.ProjectGrantID == e.GrantID || wm.ProjectGrantID == "") && wm.ResourceOwner != "" && wm.ResourceOwner == e.GrantedOrgID {
wm.ProjectGrantExists = true
wm.ExistingRoleKeys = e.RoleKeys
if wm.ProjectGrantID == "" {
wm.ProjectGrantID = e.GrantID
}
}
case *project.GrantChangedEvent:
if wm.ProjectGrantID == e.GrantID {
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -26,7 +26,7 @@ func (c *Commands) ImportHumanTOTP(ctx context.Context, userID, userAgentID, res
if err != nil {
return err
}
if err = c.checkUserExists(ctx, userID, resourceOwner); err != nil {
if _, err = c.checkUserExists(ctx, userID, resourceOwner); err != nil {
return err
}
+1 -1
View File
@@ -56,7 +56,7 @@ func (c *Commands) BulkAddedUserIDPLinks(ctx context.Context, userID, resourceOw
return zerrors.ThrowInvalidArgument(nil, "COMMAND-Ek9s", "Errors.User.ExternalIDP.MinimumExternalIDPNeeded")
}
if err := c.checkUserExists(ctx, userID, resourceOwner); err != nil {
if _, err := c.checkUserExists(ctx, userID, resourceOwner); err != nil {
return err
}
+49 -38
View File
@@ -1,6 +1,7 @@
package command
import (
"bytes"
"context"
"github.com/zitadel/zitadel/internal/domain"
@@ -14,12 +15,25 @@ func (c *Commands) SetUserMetadata(ctx context.Context, metadata *domain.Metadat
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
err = c.checkUserExists(ctx, userID, resourceOwner)
userResourceOwner, err := c.checkUserExists(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
if err := c.checkPermissionUpdateUser(ctx, userResourceOwner, userID); err != nil {
return nil, err
}
setMetadata, err := c.getUserMetadataModelByID(ctx, userID, userResourceOwner, metadata.Key)
if err != nil {
return nil, err
}
setMetadata := NewUserMetadataWriteModel(userID, resourceOwner, metadata.Key)
userAgg := UserAggregateFromWriteModel(&setMetadata.WriteModel)
// return if no change in the metadata
if bytes.Equal(setMetadata.Value, metadata.Value) {
return writeModelToUserMetadata(setMetadata), nil
}
event, err := c.setUserMetadata(ctx, userAgg, metadata)
if err != nil {
return nil, err
@@ -40,20 +54,35 @@ func (c *Commands) BulkSetUserMetadata(ctx context.Context, userID, resourceOwne
if len(metadatas) == 0 {
return nil, zerrors.ThrowPreconditionFailed(nil, "META-9mm2d", "Errors.Metadata.NoData")
}
err = c.checkUserExists(ctx, userID, resourceOwner)
userResourceOwner, err := c.checkUserExists(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
events := make([]eventstore.Command, len(metadatas))
setMetadata := NewUserMetadataListWriteModel(userID, resourceOwner)
if err := c.checkPermissionUpdateUser(ctx, userResourceOwner, userID); err != nil {
return nil, err
}
events := make([]eventstore.Command, 0)
setMetadata, err := c.getUserMetadataListModelByID(ctx, userID, userResourceOwner)
if err != nil {
return nil, err
}
userAgg := UserAggregateFromWriteModel(&setMetadata.WriteModel)
for i, data := range metadatas {
for _, data := range metadatas {
// if no change to metadata no event has to be pushed
if existingValue, ok := setMetadata.metadataList[data.Key]; ok && bytes.Equal(existingValue, data.Value) {
continue
}
event, err := c.setUserMetadata(ctx, userAgg, data)
if err != nil {
return nil, err
}
events[i] = event
events = append(events, event)
}
// no changes for the metadata
if len(events) == 0 {
return writeModelToObjectDetails(&setMetadata.WriteModel), nil
}
pushedEvents, err := c.eventstore.Push(ctx, events...)
@@ -84,11 +113,16 @@ func (c *Commands) RemoveUserMetadata(ctx context.Context, metadataKey, userID,
if metadataKey == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "META-2n0fs", "Errors.Metadata.Invalid")
}
err = c.checkUserExists(ctx, userID, resourceOwner)
userResourceOwner, err := c.checkUserExists(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
removeMetadata, err := c.getUserMetadataModelByID(ctx, userID, resourceOwner, metadataKey)
if err := c.checkPermissionUpdateUser(ctx, userResourceOwner, userID); err != nil {
return nil, err
}
removeMetadata, err := c.getUserMetadataModelByID(ctx, userID, userResourceOwner, metadataKey)
if err != nil {
return nil, err
}
@@ -116,13 +150,17 @@ func (c *Commands) BulkRemoveUserMetadata(ctx context.Context, userID, resourceO
if len(metadataKeys) == 0 {
return nil, zerrors.ThrowPreconditionFailed(nil, "META-9mm2d", "Errors.Metadata.NoData")
}
err = c.checkUserExists(ctx, userID, resourceOwner)
userResourceOwner, err := c.checkUserExists(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
if err := c.checkPermissionUpdateUser(ctx, userResourceOwner, userID); err != nil {
return nil, err
}
events := make([]eventstore.Command, len(metadataKeys))
removeMetadata, err := c.getUserMetadataListModelByID(ctx, userID, resourceOwner)
removeMetadata, err := c.getUserMetadataListModelByID(ctx, userID, userResourceOwner)
if err != nil {
return nil, err
}
@@ -153,24 +191,6 @@ func (c *Commands) BulkRemoveUserMetadata(ctx context.Context, userID, resourceO
return writeModelToObjectDetails(&removeMetadata.WriteModel), nil
}
func (c *Commands) removeUserMetadataFromOrg(ctx context.Context, resourceOwner string) ([]eventstore.Command, error) {
existingUserMetadata, err := c.getUserMetadataByOrgListModelByID(ctx, resourceOwner)
if err != nil {
return nil, err
}
if len(existingUserMetadata.UserMetadata) == 0 {
return nil, nil
}
events := make([]eventstore.Command, 0)
for key, value := range existingUserMetadata.UserMetadata {
if len(value) == 0 {
continue
}
events = append(events, user.NewMetadataRemovedAllEvent(ctx, &user.NewAggregate(key, resourceOwner).Aggregate))
}
return events, nil
}
func (c *Commands) removeUserMetadata(ctx context.Context, userAgg *eventstore.Aggregate, metadataKey string) (command eventstore.Command, err error) {
command = user.NewMetadataRemovedEvent(
ctx,
@@ -197,12 +217,3 @@ func (c *Commands) getUserMetadataListModelByID(ctx context.Context, userID, res
}
return userMetadataWriteModel, nil
}
func (c *Commands) getUserMetadataByOrgListModelByID(ctx context.Context, resourceOwner string) (*UserMetadataByOrgListWriteModel, error) {
userMetadataWriteModel := NewUserMetadataByOrgListWriteModel(resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, userMetadataWriteModel)
if err != nil {
return nil, err
}
return userMetadataWriteModel, nil
}
+298 -45
View File
@@ -16,7 +16,8 @@ import (
func TestCommandSide_SetUserMetadata(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type (
args struct {
@@ -39,10 +40,10 @@ func TestCommandSide_SetUserMetadata(t *testing.T) {
{
name: "user not existing, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -60,8 +61,7 @@ func TestCommandSide_SetUserMetadata(t *testing.T) {
{
name: "invalid metadata, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -78,7 +78,17 @@ func TestCommandSide_SetUserMetadata(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key",
[]byte("value"),
),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -93,10 +103,9 @@ func TestCommandSide_SetUserMetadata(t *testing.T) {
},
},
{
name: "add metadata, ok",
name: "add metadata, no permission",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -113,6 +122,43 @@ func TestCommandSide_SetUserMetadata(t *testing.T) {
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadata: &domain.Metadata{
Key: "key",
Value: []byte("value"),
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
{
name: "add metadata, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"[email protected]",
true,
),
),
),
expectFilter(),
expectPush(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
@@ -121,6 +167,7 @@ func TestCommandSide_SetUserMetadata(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -143,11 +190,116 @@ func TestCommandSide_SetUserMetadata(t *testing.T) {
},
},
},
{
name: "add metadata, reset, invalid",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"[email protected]",
true,
),
),
),
expectFilter(
eventFromEventPusher(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key",
[]byte("value"),
),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadata: &domain.Metadata{
Key: "key",
},
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "add metadata, reset, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"[email protected]",
true,
),
),
),
expectFilter(
eventFromEventPusher(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key",
[]byte("value"),
),
),
),
expectPush(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key",
[]byte("value2"),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadata: &domain.Metadata{
Key: "key",
Value: []byte("value2"),
},
},
res: res{
want: &domain.Metadata{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
ResourceOwner: "org1",
},
Key: "key",
Value: []byte("value2"),
State: domain.MetadataStateActive,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.SetUserMetadata(tt.args.ctx, tt.args.metadata, tt.args.userID, tt.args.orgID)
if tt.res.err == nil {
@@ -165,7 +317,8 @@ func TestCommandSide_SetUserMetadata(t *testing.T) {
func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type (
args struct {
@@ -188,9 +341,7 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
{
name: "empty meta data list, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@@ -204,8 +355,7 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
{
name: "user not existing, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@@ -225,8 +375,7 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
{
name: "invalid metadata, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -243,7 +392,9 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
),
),
),
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -259,10 +410,9 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
},
},
{
name: "add metadata, ok",
name: "add metadata, no permission",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -279,6 +429,43 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadataList: []*domain.Metadata{
{Key: "key", Value: []byte("value")},
{Key: "key1", Value: []byte("value1")},
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
{
name: "add metadata, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"[email protected]",
true,
),
),
),
expectFilter(),
expectPush(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
@@ -292,6 +479,7 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -312,7 +500,8 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.BulkSetUserMetadata(tt.args.ctx, tt.args.userID, tt.args.orgID, tt.args.metadataList...)
if tt.res.err == nil {
@@ -330,7 +519,8 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
func TestCommandSide_UserRemoveMetadata(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type (
args struct {
@@ -353,8 +543,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) {
{
name: "user not existing, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@@ -371,9 +560,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) {
{
name: "invalid metadata, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@@ -388,8 +575,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) {
{
name: "meta data not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -408,6 +594,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) {
),
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -419,11 +606,43 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) {
err: zerrors.IsNotFound,
},
},
{
name: "remove metadata, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"[email protected]",
true,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadataKey: "key",
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
{
name: "remove metadata, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -456,6 +675,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -473,7 +693,8 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.RemoveUserMetadata(tt.args.ctx, tt.args.metadataKey, tt.args.userID, tt.args.orgID)
if tt.res.err == nil {
@@ -491,7 +712,8 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) {
func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type (
args struct {
@@ -514,9 +736,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
{
name: "empty meta data list, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@@ -530,8 +750,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
{
name: "user not existing, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@@ -548,8 +767,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
{
name: "remove metadata keys not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -576,6 +794,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -590,8 +809,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
{
name: "invalid metadata, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -625,6 +843,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -636,11 +855,43 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "remove metadata, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"[email protected]",
true,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadataList: []string{"key", "key1"},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
{
name: "remove metadata, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -684,6 +935,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -701,7 +953,8 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.BulkRemoveUserMetadata(tt.args.ctx, tt.args.userID, tt.args.orgID, tt.args.metadataList...)
if tt.res.err == nil {
+1 -2
View File
@@ -2,7 +2,6 @@ package command
import (
"context"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/domain"
@@ -150,7 +149,7 @@ func (c *Commands) RemoveUserV2(ctx context.Context, userID, resourceOwner strin
events = append(events, user.NewUserRemovedEvent(ctx, &existingUser.Aggregate().Aggregate, existingUser.UserName, existingUser.IDPLinks, domainPolicy.UserLoginMustBeDomain))
for _, grantID := range cascadingGrantIDs {
removeEvent, _, err := c.removeUserGrant(ctx, grantID, "", true)
removeEvent, _, err := c.removeUserGrant(ctx, grantID, "", true, true, nil)
if err != nil {
logging.WithFields("usergrantid", grantID).WithError(err).Warn("could not cascade remove role on user grant")
continue
+4 -4
View File
@@ -25,10 +25,6 @@ func (i *Member) IsValid() bool {
return i.AggregateID != "" && i.UserID != "" && len(i.Roles) != 0
}
func (i *Member) IsIAMValid() bool {
return i.UserID != "" && len(i.Roles) != 0
}
type MemberState int32
const (
@@ -42,3 +38,7 @@ const (
func (f MemberState) Valid() bool {
return f >= 0 && f < memberStateCount
}
func (f MemberState) Exists() bool {
return f != MemberStateRemoved && f != MemberStateUnspecified
}
+38 -23
View File
@@ -27,29 +27,44 @@ func (p *Permissions) appendPermission(ctxID, permission string) {
type PermissionCheck func(ctx context.Context, permission, resourceOwnerID, aggregateID string) (err error)
const (
PermissionUserWrite = "user.write"
PermissionUserRead = "user.read"
PermissionUserDelete = "user.delete"
PermissionUserCredentialWrite = "user.credential.write"
PermissionSessionWrite = "session.write"
PermissionSessionRead = "session.read"
PermissionSessionLink = "session.link"
PermissionSessionDelete = "session.delete"
PermissionOrgRead = "org.read"
PermissionIDPRead = "iam.idp.read"
PermissionOrgIDPRead = "org.idp.read"
PermissionProjectWrite = "project.write"
PermissionProjectRead = "project.read"
PermissionProjectDelete = "project.delete"
PermissionProjectGrantWrite = "project.grant.write"
PermissionProjectGrantRead = "project.grant.read"
PermissionProjectGrantDelete = "project.grant.delete"
PermissionProjectRoleWrite = "project.role.write"
PermissionProjectRoleRead = "project.role.read"
PermissionProjectRoleDelete = "project.role.delete"
PermissionProjectAppWrite = "project.app.write"
PermissionProjectAppDelete = "project.app.delete"
PermissionProjectAppRead = "project.app.read"
PermissionUserWrite = "user.write"
PermissionUserRead = "user.read"
PermissionUserDelete = "user.delete"
PermissionUserCredentialWrite = "user.credential.write"
PermissionSessionWrite = "session.write"
PermissionSessionRead = "session.read"
PermissionSessionLink = "session.link"
PermissionSessionDelete = "session.delete"
PermissionOrgRead = "org.read"
PermissionIDPRead = "iam.idp.read"
PermissionOrgIDPRead = "org.idp.read"
PermissionProjectWrite = "project.write"
PermissionProjectRead = "project.read"
PermissionProjectDelete = "project.delete"
PermissionProjectGrantWrite = "project.grant.write"
PermissionProjectGrantRead = "project.grant.read"
PermissionProjectGrantDelete = "project.grant.delete"
PermissionProjectRoleWrite = "project.role.write"
PermissionProjectRoleRead = "project.role.read"
PermissionProjectRoleDelete = "project.role.delete"
PermissionProjectAppWrite = "project.app.write"
PermissionProjectAppDelete = "project.app.delete"
PermissionProjectAppRead = "project.app.read"
PermissionInstanceMemberWrite = "iam.member.write"
PermissionInstanceMemberDelete = "iam.member.delete"
PermissionInstanceMemberRead = "iam.member.read"
PermissionOrgMemberWrite = "org.member.write"
PermissionOrgMemberDelete = "org.member.delete"
PermissionOrgMemberRead = "org.member.read"
PermissionProjectMemberWrite = "project.member.write"
PermissionProjectMemberDelete = "project.member.delete"
PermissionProjectMemberRead = "project.member.read"
PermissionProjectGrantMemberWrite = "project.grant.member.write"
PermissionProjectGrantMemberDelete = "project.grant.member.delete"
PermissionProjectGrantMemberRead = "project.grant.member.read"
PermissionUserGrantWrite = "user.grant.write"
PermissionUserGrantRead = "user.grant.read"
PermissionUserGrantDelete = "user.grant.delete"
)
// ProjectPermissionCheck is used as a check for preconditions dependent on application, project, user resourceowner and usergrants.
+162 -82
View File
@@ -2,6 +2,7 @@ package integration
import (
"context"
"encoding/base64"
"fmt"
"sync"
"testing"
@@ -24,11 +25,13 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/admin"
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/auth"
authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
feature_v2beta "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/idp"
idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2"
instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta"
internal_permission_v2beta "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta"
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
object_v3alpha "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
@@ -52,33 +55,35 @@ import (
)
type Client struct {
CC *grpc.ClientConn
Admin admin.AdminServiceClient
Mgmt mgmt.ManagementServiceClient
Auth auth.AuthServiceClient
UserV2beta user_v2beta.UserServiceClient
UserV2 user_v2.UserServiceClient
SessionV2beta session_v2beta.SessionServiceClient
SessionV2 session.SessionServiceClient
SettingsV2beta settings_v2beta.SettingsServiceClient
SettingsV2 settings.SettingsServiceClient
OIDCv2beta oidc_pb_v2beta.OIDCServiceClient
OIDCv2 oidc_pb.OIDCServiceClient
OrgV2beta org_v2beta.OrganizationServiceClient
OrgV2 org.OrganizationServiceClient
ActionV2beta action.ActionServiceClient
FeatureV2beta feature_v2beta.FeatureServiceClient
FeatureV2 feature.FeatureServiceClient
UserSchemaV3 userschema_v3alpha.ZITADELUserSchemasClient
WebKeyV2Beta webkey_v2beta.WebKeyServiceClient
WebKeyV2 webkey_v2.WebKeyServiceClient
IDPv2 idp_pb.IdentityProviderServiceClient
UserV3Alpha user_v3alpha.ZITADELUsersClient
SAMLv2 saml_pb.SAMLServiceClient
SCIM *scim.Client
Projectv2Beta project_v2beta.ProjectServiceClient
InstanceV2Beta instance.InstanceServiceClient
AppV2Beta app.AppServiceClient
CC *grpc.ClientConn
Admin admin.AdminServiceClient
Mgmt mgmt.ManagementServiceClient
Auth auth.AuthServiceClient
UserV2beta user_v2beta.UserServiceClient
UserV2 user_v2.UserServiceClient
SessionV2beta session_v2beta.SessionServiceClient
SessionV2 session.SessionServiceClient
SettingsV2beta settings_v2beta.SettingsServiceClient
SettingsV2 settings.SettingsServiceClient
OIDCv2beta oidc_pb_v2beta.OIDCServiceClient
OIDCv2 oidc_pb.OIDCServiceClient
OrgV2beta org_v2beta.OrganizationServiceClient
OrgV2 org.OrganizationServiceClient
ActionV2beta action.ActionServiceClient
FeatureV2beta feature_v2beta.FeatureServiceClient
FeatureV2 feature.FeatureServiceClient
UserSchemaV3 userschema_v3alpha.ZITADELUserSchemasClient
WebKeyV2 webkey_v2.WebKeyServiceClient
WebKeyV2Beta webkey_v2beta.WebKeyServiceClient
IDPv2 idp_pb.IdentityProviderServiceClient
UserV3Alpha user_v3alpha.ZITADELUsersClient
SAMLv2 saml_pb.SAMLServiceClient
SCIM *scim.Client
Projectv2Beta project_v2beta.ProjectServiceClient
InstanceV2Beta instance.InstanceServiceClient
AppV2Beta app.AppServiceClient
InternalPermissionv2Beta internal_permission_v2beta.InternalPermissionServiceClient
AuthorizationV2Beta authorization.AuthorizationServiceClient
}
func NewDefaultClient(ctx context.Context) (*Client, error) {
@@ -93,33 +98,35 @@ func newClient(ctx context.Context, target string) (*Client, error) {
return nil, err
}
client := &Client{
CC: cc,
Admin: admin.NewAdminServiceClient(cc),
Mgmt: mgmt.NewManagementServiceClient(cc),
Auth: auth.NewAuthServiceClient(cc),
UserV2beta: user_v2beta.NewUserServiceClient(cc),
UserV2: user_v2.NewUserServiceClient(cc),
SessionV2beta: session_v2beta.NewSessionServiceClient(cc),
SessionV2: session.NewSessionServiceClient(cc),
SettingsV2beta: settings_v2beta.NewSettingsServiceClient(cc),
SettingsV2: settings.NewSettingsServiceClient(cc),
OIDCv2beta: oidc_pb_v2beta.NewOIDCServiceClient(cc),
OIDCv2: oidc_pb.NewOIDCServiceClient(cc),
OrgV2beta: org_v2beta.NewOrganizationServiceClient(cc),
OrgV2: org.NewOrganizationServiceClient(cc),
ActionV2beta: action.NewActionServiceClient(cc),
FeatureV2beta: feature_v2beta.NewFeatureServiceClient(cc),
FeatureV2: feature.NewFeatureServiceClient(cc),
UserSchemaV3: userschema_v3alpha.NewZITADELUserSchemasClient(cc),
WebKeyV2Beta: webkey_v2beta.NewWebKeyServiceClient(cc),
WebKeyV2: webkey_v2.NewWebKeyServiceClient(cc),
IDPv2: idp_pb.NewIdentityProviderServiceClient(cc),
UserV3Alpha: user_v3alpha.NewZITADELUsersClient(cc),
SAMLv2: saml_pb.NewSAMLServiceClient(cc),
SCIM: scim.NewScimClient(target),
Projectv2Beta: project_v2beta.NewProjectServiceClient(cc),
InstanceV2Beta: instance.NewInstanceServiceClient(cc),
AppV2Beta: app.NewAppServiceClient(cc),
CC: cc,
Admin: admin.NewAdminServiceClient(cc),
Mgmt: mgmt.NewManagementServiceClient(cc),
Auth: auth.NewAuthServiceClient(cc),
UserV2beta: user_v2beta.NewUserServiceClient(cc),
UserV2: user_v2.NewUserServiceClient(cc),
SessionV2beta: session_v2beta.NewSessionServiceClient(cc),
SessionV2: session.NewSessionServiceClient(cc),
SettingsV2beta: settings_v2beta.NewSettingsServiceClient(cc),
SettingsV2: settings.NewSettingsServiceClient(cc),
OIDCv2beta: oidc_pb_v2beta.NewOIDCServiceClient(cc),
OIDCv2: oidc_pb.NewOIDCServiceClient(cc),
OrgV2beta: org_v2beta.NewOrganizationServiceClient(cc),
OrgV2: org.NewOrganizationServiceClient(cc),
ActionV2beta: action.NewActionServiceClient(cc),
FeatureV2beta: feature_v2beta.NewFeatureServiceClient(cc),
FeatureV2: feature.NewFeatureServiceClient(cc),
UserSchemaV3: userschema_v3alpha.NewZITADELUserSchemasClient(cc),
WebKeyV2: webkey_v2.NewWebKeyServiceClient(cc),
WebKeyV2Beta: webkey_v2beta.NewWebKeyServiceClient(cc),
IDPv2: idp_pb.NewIdentityProviderServiceClient(cc),
UserV3Alpha: user_v3alpha.NewZITADELUsersClient(cc),
SAMLv2: saml_pb.NewSAMLServiceClient(cc),
SCIM: scim.NewScimClient(target),
Projectv2Beta: project_v2beta.NewProjectServiceClient(cc),
InstanceV2Beta: instance.NewInstanceServiceClient(cc),
AppV2Beta: app.NewAppServiceClient(cc),
InternalPermissionv2Beta: internal_permission_v2beta.NewInternalPermissionServiceClient(cc),
AuthorizationV2Beta: authorization.NewAuthorizationServiceClient(cc),
}
return client, client.pollHealth(ctx)
}
@@ -239,7 +246,29 @@ func (i *Instance) CreateHumanUserWithTOTP(ctx context.Context, secret string) *
return resp
}
func (i *Instance) CreateUserTypeHuman(ctx context.Context) *user_v2.CreateUserResponse {
func (i *Instance) SetUserMetadata(ctx context.Context, id, key, value string) *user_v2.SetUserMetadataResponse {
resp, err := i.Client.UserV2.SetUserMetadata(ctx, &user_v2.SetUserMetadataRequest{
UserId: id,
Metadata: []*user_v2.Metadata{{
Key: key,
Value: []byte(base64.StdEncoding.EncodeToString([]byte(value))),
},
},
})
logging.OnError(err).Panic("set user metadata")
return resp
}
func (i *Instance) DeleteUserMetadata(ctx context.Context, id, key string) *user_v2.DeleteUserMetadataResponse {
resp, err := i.Client.UserV2.DeleteUserMetadata(ctx, &user_v2.DeleteUserMetadataRequest{
UserId: id,
Keys: []string{key},
})
logging.OnError(err).Panic("delete user metadata")
return resp
}
func (i *Instance) CreateUserTypeHuman(ctx context.Context, email string) *user_v2.CreateUserResponse {
resp, err := i.Client.UserV2.CreateUser(ctx, &user_v2.CreateUserRequest{
OrganizationId: i.DefaultOrg.GetId(),
UserType: &user_v2.CreateUserRequest_Human_{
@@ -249,7 +278,7 @@ func (i *Instance) CreateUserTypeHuman(ctx context.Context) *user_v2.CreateUserR
FamilyName: "Mouse",
},
Email: &user_v2.SetHumanEmail{
Email: fmt.Sprintf("%[email protected]", time.Now().UnixNano()),
Email: email,
Verification: &user_v2.SetHumanEmail_ReturnCode{
ReturnCode: &user_v2.ReturnEmailVerificationCode{},
},
@@ -262,7 +291,7 @@ func (i *Instance) CreateUserTypeHuman(ctx context.Context) *user_v2.CreateUserR
return resp
}
func (i *Instance) CreateUserTypeMachine(ctx context.Context) *user_v2.CreateUserResponse {
func (i *Instance) CreateUserTypeMachine(ctx context.Context, orgId string) *user_v2.CreateUserResponse {
resp, err := i.Client.UserV2.CreateUser(ctx, &user_v2.CreateUserRequest{
OrganizationId: i.DefaultOrg.GetId(),
UserType: &user_v2.CreateUserRequest_Machine_{
@@ -629,14 +658,6 @@ func (i *Instance) AddOrgGenericOAuthProvider(ctx context.Context, name string)
},
})
logging.OnError(err).Panic("create generic OAuth idp")
/*
mustAwait(func() error {
_, err := i.Client.Mgmt.GetProviderByID(ctx, &mgmt.GetProviderByIDRequest{
Id: resp.GetId(),
})
return err
})
*/
return resp
}
@@ -883,48 +904,107 @@ func (i *Instance) ActivateProjectGrant(ctx context.Context, t *testing.T, proje
return resp
}
func (i *Instance) CreateProjectUserGrant(t *testing.T, ctx context.Context, projectID, userID string) string {
func (i *Instance) CreateProjectUserGrant(t *testing.T, ctx context.Context, projectID, userID string) *mgmt.AddUserGrantResponse {
resp, err := i.Client.Mgmt.AddUserGrant(ctx, &mgmt.AddUserGrantRequest{
UserId: userID,
ProjectId: projectID,
})
require.NoError(t, err)
return resp.GetUserGrantId()
return resp
}
func (i *Instance) CreateProjectGrantUserGrant(ctx context.Context, orgID, projectID, projectGrantID, userID string) string {
func (i *Instance) CreateProjectGrantUserGrant(ctx context.Context, orgID, projectID, projectGrantID, userID string) *mgmt.AddUserGrantResponse {
resp, err := i.Client.Mgmt.AddUserGrant(SetOrgID(ctx, orgID), &mgmt.AddUserGrantRequest{
UserId: userID,
ProjectId: projectID,
ProjectGrantId: projectGrantID,
})
logging.OnError(err).Panic("create project grant user grant")
return resp.GetUserGrantId()
return resp
}
func (i *Instance) CreateOrgMembership(t *testing.T, ctx context.Context, userID string) {
_, err := i.Client.Mgmt.AddOrgMember(ctx, &mgmt.AddOrgMemberRequest{
func (i *Instance) CreateInstanceMembership(t *testing.T, ctx context.Context, userID string) *internal_permission_v2beta.CreateAdministratorResponse {
resp, err := i.Client.InternalPermissionv2Beta.CreateAdministrator(ctx, &internal_permission_v2beta.CreateAdministratorRequest{
Resource: &internal_permission_v2beta.ResourceType{
Resource: &internal_permission_v2beta.ResourceType_Instance{Instance: true},
},
UserId: userID,
Roles: []string{domain.RoleIAMOwner},
})
require.NoError(t, err)
return resp
}
func (i *Instance) DeleteInstanceMembership(t *testing.T, ctx context.Context, userID string) {
_, err := i.Client.Admin.RemoveIAMMember(ctx, &admin.RemoveIAMMemberRequest{
UserId: userID,
})
require.NoError(t, err)
}
func (i *Instance) CreateOrgMembership(t *testing.T, ctx context.Context, orgID, userID string) *internal_permission_v2beta.CreateAdministratorResponse {
resp, err := i.Client.InternalPermissionv2Beta.CreateAdministrator(ctx, &internal_permission_v2beta.CreateAdministratorRequest{
Resource: &internal_permission_v2beta.ResourceType{
Resource: &internal_permission_v2beta.ResourceType_OrganizationId{OrganizationId: orgID},
},
UserId: userID,
Roles: []string{domain.RoleOrgOwner},
})
require.NoError(t, err)
return resp
}
func (i *Instance) CreateProjectMembership(t *testing.T, ctx context.Context, projectID, userID string) {
_, err := i.Client.Mgmt.AddProjectMember(ctx, &mgmt.AddProjectMemberRequest{
ProjectId: projectID,
UserId: userID,
Roles: []string{domain.RoleProjectOwner},
func (i *Instance) DeleteOrgMembership(t *testing.T, ctx context.Context, userID string) {
_, err := i.Client.Mgmt.RemoveOrgMember(ctx, &mgmt.RemoveOrgMemberRequest{
UserId: userID,
})
require.NoError(t, err)
}
func (i *Instance) CreateProjectGrantMembership(t *testing.T, ctx context.Context, projectID, grantID, userID string) {
_, err := i.Client.Mgmt.AddProjectGrantMember(ctx, &mgmt.AddProjectGrantMemberRequest{
ProjectId: projectID,
GrantId: grantID,
UserId: userID,
Roles: []string{domain.RoleProjectGrantOwner},
func (i *Instance) CreateProjectMembership(t *testing.T, ctx context.Context, projectID, userID string) *internal_permission_v2beta.CreateAdministratorResponse {
resp, err := i.Client.InternalPermissionv2Beta.CreateAdministrator(ctx, &internal_permission_v2beta.CreateAdministratorRequest{
Resource: &internal_permission_v2beta.ResourceType{
Resource: &internal_permission_v2beta.ResourceType_ProjectId{ProjectId: projectID},
},
UserId: userID,
Roles: []string{domain.RoleProjectOwner},
})
require.NoError(t, err)
return resp
}
func (i *Instance) DeleteProjectMembership(t *testing.T, ctx context.Context, projectID, userID string) {
_, err := i.Client.InternalPermissionv2Beta.DeleteAdministrator(ctx, &internal_permission_v2beta.DeleteAdministratorRequest{
Resource: &internal_permission_v2beta.ResourceType{Resource: &internal_permission_v2beta.ResourceType_ProjectId{ProjectId: projectID}},
UserId: userID,
})
require.NoError(t, err)
}
func (i *Instance) CreateProjectGrantMembership(t *testing.T, ctx context.Context, projectID, grantID, userID string) *internal_permission_v2beta.CreateAdministratorResponse {
resp, err := i.Client.InternalPermissionv2Beta.CreateAdministrator(ctx, &internal_permission_v2beta.CreateAdministratorRequest{
Resource: &internal_permission_v2beta.ResourceType{
Resource: &internal_permission_v2beta.ResourceType_ProjectGrant_{ProjectGrant: &internal_permission_v2beta.ResourceType_ProjectGrant{
ProjectId: projectID,
ProjectGrantId: grantID,
}},
},
UserId: userID,
Roles: []string{domain.RoleProjectGrantOwner},
})
require.NoError(t, err)
return resp
}
func (i *Instance) DeleteProjectGrantMembership(t *testing.T, ctx context.Context, projectID, grantID, userID string) {
_, err := i.Client.InternalPermissionv2Beta.DeleteAdministrator(ctx, &internal_permission_v2beta.DeleteAdministratorRequest{
Resource: &internal_permission_v2beta.ResourceType{
Resource: &internal_permission_v2beta.ResourceType_ProjectGrant_{ProjectGrant: &internal_permission_v2beta.ResourceType_ProjectGrant{
ProjectId: projectID,
ProjectGrantId: grantID,
}},
},
UserId: userID,
})
require.NoError(t, err)
}
+30
View File
@@ -0,0 +1,30 @@
package integration
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
)
func EnsureInstanceFeature(t *testing.T, ctx context.Context, instance *Instance, features *feature.SetInstanceFeaturesRequest, assertFeatures func(t *assert.CollectT, got *feature.GetInstanceFeaturesResponse)) {
ctx = instance.WithAuthorizationToken(ctx, UserTypeIAMOwner)
_, err := instance.Client.FeatureV2.SetInstanceFeatures(ctx, features)
require.NoError(t, err)
retryDuration, tick := WaitForAndTickWithMaxDuration(ctx, 5*time.Minute)
require.EventuallyWithT(t,
func(tt *assert.CollectT) {
got, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{
Inheritance: true,
})
require.NoError(tt, err)
assertFeatures(tt, got)
},
retryDuration,
tick,
"timed out waiting for ensuring instance feature")
}

Some files were not shown because too many files have changed in this diff Show More