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
@@ -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
}