feat: user api requests to resource API (#9794)

# Which Problems Are Solved

This pull request addresses a significant gap in the user service v2
API, which currently lacks methods for managing machine users.

# How the Problems Are Solved

This PR adds new API endpoints to the user service v2 to manage machine
users including their secret, keys and personal access tokens.
Additionally, there's now a CreateUser and UpdateUser endpoints which
allow to create either a human or machine user and update them. The
existing `CreateHumanUser` endpoint has been deprecated along the
corresponding management service endpoints. For details check the
additional context section.

# Additional Context

- Closes https://github.com/zitadel/zitadel/issues/9349

## More details
- API changes: https://github.com/zitadel/zitadel/pull/9680
- Implementation: https://github.com/zitadel/zitadel/pull/9763
- Tests: https://github.com/zitadel/zitadel/pull/9771

## Follow-ups

- Metadata: support managing user metadata using resource API
https://github.com/zitadel/zitadel/pull/10005
- Machine token type: support managing the machine token type (migrate
to new enum with zero value unspecified?)

---------

Co-authored-by: Copilot <[email protected]>
Co-authored-by: Livio Spring <[email protected]>
This commit is contained in:
Elio Bischof
2025-06-04 07:17:23 +00:00
committed by GitHub
co-authored by Copilot Livio Spring
parent e2a61a6002
commit 8fc11a7366
86 changed files with 7033 additions and 536 deletions
+3 -1
View File
@@ -206,6 +206,8 @@ The same applies to messages that are returned by multiple resources.
For example, information about the `User` might be different when managing the user resource itself than when it's returned
as part of an authorization or a manager role, where only limited information is needed.
On the other hand, types that always follow the same pattern and are used in multiple resources, such as `IDFilter`, `TimestampFilter` or `InIDsFilter` SHOULD be globalized and reused.
##### Re-using messages
Prevent reusing messages for the creation and the retrieval of a resource.
@@ -271,7 +273,7 @@ Additionally, state changes, specific actions or operations that do not fit into
The API uses OAuth 2 for authorization. There are corresponding middlewares that check the access token for validity and
automatically return an error if the token is invalid.
Permissions grated to the user might be organization specific and can therefore only be checked based on the queried resource.
Permissions granted to the user might be organization specific and can therefore only be checked based on the queried resource.
In such case, the API does not check the permissions itself but relies on the checks of the functions that are called by the API.
If the permission can be checked by the API itself, e.g. if the permission is instance wide, it can be annotated on the endpoint in the proto file (see below).
In any case, the required permissions need to be documented in the [API documentation](#documentation).
+1 -1
View File
@@ -461,7 +461,7 @@ func startAPIs(
if err := apis.RegisterService(ctx, user_v2beta.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, user_v2.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil {
if err := apis.RegisterService(ctx, user_v2.CreateServer(config.SystemDefaults, commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, session_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil {
+1
View File
@@ -34,6 +34,7 @@ require (
github.com/go-webauthn/webauthn v0.10.2
github.com/goccy/go-json v0.10.5
github.com/golang/protobuf v1.5.4
github.com/google/go-cmp v0.7.0
github.com/gorilla/csrf v1.7.2
github.com/gorilla/mux v1.8.1
github.com/gorilla/schema v1.4.1
+1 -1
View File
@@ -510,7 +510,7 @@ func importMachineUsers(ctx context.Context, s *Server, errors *[]*admin_pb.Impo
}
for _, user := range org.GetMachineUsers() {
logging.Debugf("import user: %s", user.GetUserId())
_, err := s.command.AddMachine(ctx, management.AddMachineUserRequestToCommand(user.GetUser(), org.GetOrgId()))
_, err := s.command.AddMachine(ctx, management.AddMachineUserRequestToCommand(user.GetUser(), org.GetOrgId()), nil)
if err != nil {
*errors = append(*errors, &admin_pb.ImportDataError{Type: "machine_user", Id: user.GetUserId(), Message: err.Error()})
if isCtxTimeout(ctx) {
+50
View File
@@ -0,0 +1,50 @@
package filter
import (
"fmt"
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
"github.com/zitadel/zitadel/pkg/grpc/filter/v2"
)
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_BEFORE:
return query.TimestampLess
case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER:
return query.TimestampGreater
case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_BEFORE_OR_EQUALS:
return query.TimestampLessOrEquals
case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_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 {
return 0, limit, asc, nil
}
offset = query.Offset
asc = query.Asc
if defaults.MaxQueryLimit > 0 && uint64(query.Limit) > defaults.MaxQueryLimit {
return 0, 0, false, zerrors.ThrowInvalidArgumentf(fmt.Errorf("given: %d, allowed: %d", query.Limit, defaults.MaxQueryLimit), "QUERY-4M0fs", "Errors.Query.LimitExceeded")
}
if query.Limit > 0 {
limit = uint64(query.Limit)
}
return offset, limit, asc, nil
}
func QueryToPaginationPb(request query.SearchRequest, response query.SearchResponse) *filter.PaginationResponse {
return &filter.PaginationResponse{
AppliedLimit: request.Limit,
TotalResult: response.Count,
}
}
@@ -271,7 +271,7 @@ func (s *Server) ListAppKeys(ctx context.Context, req *mgmt_pb.ListAppKeysReques
if err != nil {
return nil, err
}
keys, err := s.query.SearchAuthNKeys(ctx, queries, false)
keys, err := s.query.SearchAuthNKeys(ctx, queries, query.JoinFilterApp, nil)
if err != nil {
return nil, err
}
+6 -7
View File
@@ -297,7 +297,7 @@ func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUs
func (s *Server) AddMachineUser(ctx context.Context, req *mgmt_pb.AddMachineUserRequest) (*mgmt_pb.AddMachineUserResponse, error) {
machine := AddMachineUserRequestToCommand(req, authz.GetCtxData(ctx).OrgID)
objectDetails, err := s.command.AddMachine(ctx, machine)
objectDetails, err := s.command.AddMachine(ctx, machine, nil)
if err != nil {
return nil, err
}
@@ -752,11 +752,11 @@ func (s *Server) GetMachineKeyByIDs(ctx context.Context, req *mgmt_pb.GetMachine
}
func (s *Server) ListMachineKeys(ctx context.Context, req *mgmt_pb.ListMachineKeysRequest) (*mgmt_pb.ListMachineKeysResponse, error) {
query, err := ListMachineKeysRequestToQuery(ctx, req)
q, err := ListMachineKeysRequestToQuery(ctx, req)
if err != nil {
return nil, err
}
result, err := s.query.SearchAuthNKeys(ctx, query, false)
result, err := s.query.SearchAuthNKeys(ctx, q, query.JoinFilterUserMachine, nil)
if err != nil {
return nil, err
}
@@ -774,7 +774,6 @@ func (s *Server) AddMachineKey(ctx context.Context, req *mgmt_pb.AddMachineKeyRe
if err != nil {
return nil, err
}
// Return key details only if the pubkey wasn't supplied, otherwise the user already has
// private key locally
var keyDetails []byte
@@ -821,7 +820,7 @@ func (s *Server) GenerateMachineSecret(ctx context.Context, req *mgmt_pb.Generat
}
func (s *Server) RemoveMachineSecret(ctx context.Context, req *mgmt_pb.RemoveMachineSecretRequest) (*mgmt_pb.RemoveMachineSecretResponse, error) {
objectDetails, err := s.command.RemoveMachineSecret(ctx, req.UserId, authz.GetCtxData(ctx).OrgID)
objectDetails, err := s.command.RemoveMachineSecret(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, nil)
if err != nil {
return nil, err
}
@@ -839,7 +838,7 @@ func (s *Server) GetPersonalAccessTokenByIDs(ctx context.Context, req *mgmt_pb.G
if err != nil {
return nil, err
}
token, err := s.query.PersonalAccessTokenByID(ctx, true, req.TokenId, false, resourceOwner, aggregateID)
token, err := s.query.PersonalAccessTokenByID(ctx, true, req.TokenId, resourceOwner, aggregateID)
if err != nil {
return nil, err
}
@@ -853,7 +852,7 @@ func (s *Server) ListPersonalAccessTokens(ctx context.Context, req *mgmt_pb.List
if err != nil {
return nil, err
}
result, err := s.query.SearchPersonalAccessTokens(ctx, queries, false)
result, err := s.query.SearchPersonalAccessTokens(ctx, queries, nil)
if err != nil {
return nil, err
}
@@ -168,8 +168,8 @@ func TestServer_ListProjects(t *testing.T) {
orgID := instance.DefaultOrg.GetId()
resp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false)
request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{resp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{resp.GetId()},
},
}
},
@@ -188,8 +188,8 @@ func TestServer_ListProjects(t *testing.T) {
orgID := instance.DefaultOrg.GetId()
resp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false)
request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{resp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{resp.GetId()},
},
}
},
@@ -208,8 +208,8 @@ func TestServer_ListProjects(t *testing.T) {
orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email())
resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false)
request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{resp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{resp.GetId()},
},
}
},
@@ -232,8 +232,8 @@ func TestServer_ListProjects(t *testing.T) {
req: &project.ListProjectsRequest{
Filters: []*project.ProjectSearchFilter{
{Filter: &project.ProjectSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{"notfound"},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{"notfound"},
},
},
},
@@ -255,8 +255,8 @@ func TestServer_ListProjects(t *testing.T) {
orgID := instance.DefaultOrg.GetId()
response.Projects[0] = createProject(iamOwnerCtx, instance, t, orgID, false, false)
request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{response.Projects[0].GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{response.Projects[0].GetId()},
},
}
},
@@ -317,8 +317,8 @@ func TestServer_ListProjects(t *testing.T) {
response.Projects[1] = createProject(iamOwnerCtx, instance, t, orgID, true, false)
response.Projects[0] = createProject(iamOwnerCtx, instance, t, orgID, false, true)
request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{response.Projects[0].GetId(), response.Projects[1].GetId(), response.Projects[2].GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{response.Projects[0].GetId(), response.Projects[1].GetId(), response.Projects[2].GetId()},
},
}
},
@@ -349,8 +349,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: &project.InProjectIDsFilter{
ProjectIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()},
},
}
@@ -379,8 +379,8 @@ func TestServer_ListProjects(t *testing.T) {
projectResp := createProject(iamOwnerCtx, instance, t, orgID, true, true)
response.Projects[3] = projectResp
request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{projectResp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{projectResp.GetId()},
},
}
response.Projects[2] = createGrantedProject(iamOwnerCtx, instance, t, projectResp)
@@ -416,7 +416,7 @@ func TestServer_ListProjects(t *testing.T) {
response.Projects[1] = grantedProjectResp
response.Projects[0] = createProject(iamOwnerCtx, instance, t, *grantedProjectResp.GrantedOrganizationId, true, true)
request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectOrganizationIdFilter{
ProjectOrganizationIdFilter: &project.ProjectOrganizationIDFilter{ProjectOrganizationId: *grantedProjectResp.GrantedOrganizationId},
ProjectOrganizationIdFilter: &filter.IDFilter{Id: *grantedProjectResp.GrantedOrganizationId},
}
},
req: &project.ListProjectsRequest{
@@ -445,7 +445,7 @@ func TestServer_ListProjects(t *testing.T) {
grantedProjectResp := createGrantedProject(iamOwnerCtx, instance, t, projectResp)
response.Projects[0] = createProject(iamOwnerCtx, instance, t, *grantedProjectResp.GrantedOrganizationId, true, true)
request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectResourceOwnerFilter{
ProjectResourceOwnerFilter: &project.ProjectResourceOwnerFilter{ProjectResourceOwner: *grantedProjectResp.GrantedOrganizationId},
ProjectResourceOwnerFilter: &filter.IDFilter{Id: *grantedProjectResp.GrantedOrganizationId},
}
},
req: &project.ListProjectsRequest{
@@ -513,8 +513,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) {
dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) {
resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false)
request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{resp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{resp.GetId()},
},
}
},
@@ -531,8 +531,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) {
dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) {
resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false)
request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{resp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{resp.GetId()},
},
}
},
@@ -550,8 +550,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) {
orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email())
resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), false, false)
request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{resp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{resp.GetId()},
},
}
},
@@ -574,8 +574,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) {
req: &project.ListProjectsRequest{
Filters: []*project.ProjectSearchFilter{
{Filter: &project.ProjectSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{"notfound"},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{"notfound"},
},
},
},
@@ -596,8 +596,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) {
dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) {
response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false)
request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{response.Projects[0].GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{response.Projects[0].GetId()},
},
}
},
@@ -650,8 +650,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) {
response.Projects[1] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, false)
response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, true)
request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{response.Projects[0].GetId(), response.Projects[1].GetId(), response.Projects[2].GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{response.Projects[0].GetId(), response.Projects[1].GetId(), response.Projects[2].GetId()},
},
}
},
@@ -679,8 +679,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) {
projectResp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, true)
response.Projects[3] = projectResp
request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{projectResp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{projectResp.GetId()},
},
}
response.Projects[2] = createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp)
@@ -715,7 +715,7 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) {
response.Projects[1] = grantedProjectResp
response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, *grantedProjectResp.GrantedOrganizationId, true, true)
request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectOrganizationIdFilter{
ProjectOrganizationIdFilter: &project.ProjectOrganizationIDFilter{ProjectOrganizationId: *grantedProjectResp.GrantedOrganizationId},
ProjectOrganizationIdFilter: &filter.IDFilter{Id: *grantedProjectResp.GrantedOrganizationId},
}
},
req: &project.ListProjectsRequest{
@@ -743,7 +743,7 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) {
grantedProjectResp := createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp)
response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, *grantedProjectResp.GrantedOrganizationId, true, true)
request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectResourceOwnerFilter{
ProjectResourceOwnerFilter: &project.ProjectResourceOwnerFilter{ProjectResourceOwner: *grantedProjectResp.GrantedOrganizationId},
ProjectResourceOwnerFilter: &filter.IDFilter{Id: *grantedProjectResp.GrantedOrganizationId},
}
},
req: &project.ListProjectsRequest{
@@ -770,8 +770,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) {
resp2 := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, false)
resp3 := createProject(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), false, true)
request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()},
},
}
@@ -882,15 +882,13 @@ func TestServer_ListProjectGrants(t *testing.T) {
dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) {
projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false)
request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{projectResp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{projectResp.GetId()},
},
}
grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email())
request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{
ProjectGrantResourceOwnerFilter: &project.ProjectGrantResourceOwnerFilter{
ProjectGrantResourceOwner: grantedOrg.GetOrganizationId(),
},
ProjectGrantResourceOwnerFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()},
}
instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId())
@@ -908,15 +906,13 @@ func TestServer_ListProjectGrants(t *testing.T) {
dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) {
projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false)
request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{projectResp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{projectResp.GetId()},
},
}
grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email())
request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{
ProjectGrantResourceOwnerFilter: &project.ProjectGrantResourceOwnerFilter{
ProjectGrantResourceOwner: grantedOrg.GetOrganizationId(),
},
ProjectGrantResourceOwnerFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()},
}
instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId())
@@ -934,8 +930,8 @@ func TestServer_ListProjectGrants(t *testing.T) {
req: &project.ListProjectGrantsRequest{
Filters: []*project.ProjectGrantSearchFilter{
{Filter: &project.ProjectGrantSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{"notfound"},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{"notfound"},
},
},
},
@@ -958,8 +954,8 @@ func TestServer_ListProjectGrants(t *testing.T) {
orgID := instance.DefaultOrg.GetId()
projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false)
request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{projectResp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{projectResp.GetId()},
},
}
@@ -988,8 +984,8 @@ func TestServer_ListProjectGrants(t *testing.T) {
orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email())
projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false)
request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{projectResp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{projectResp.GetId()},
},
}
@@ -1016,8 +1012,8 @@ func TestServer_ListProjectGrants(t *testing.T) {
orgID := instance.DefaultOrg.GetId()
projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false)
request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{projectResp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{projectResp.GetId()},
},
}
@@ -1053,8 +1049,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: &project.InProjectIDsFilter{
ProjectIds: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId()},
},
}
@@ -1084,8 +1080,8 @@ func TestServer_ListProjectGrants(t *testing.T) {
orgID := instance.DefaultOrg.GetId()
projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false)
request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{projectResp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{projectResp.GetId()},
},
}
projectRoleResp := addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId())
@@ -1114,8 +1110,8 @@ func TestServer_ListProjectGrants(t *testing.T) {
orgID := instance.DefaultOrg.GetId()
projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false)
request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{projectResp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{projectResp.GetId()},
},
}
projectRoleResp := addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId())
@@ -1189,15 +1185,13 @@ func TestServer_ListProjectGrants_PermissionV2(t *testing.T) {
dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) {
projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), gofakeit.AppName(), false, false)
request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{projectResp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{projectResp.GetId()},
},
}
grantedOrg := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email())
request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{
ProjectGrantResourceOwnerFilter: &project.ProjectGrantResourceOwnerFilter{
ProjectGrantResourceOwner: grantedOrg.GetOrganizationId(),
},
ProjectGrantResourceOwnerFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()},
}
instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId())
@@ -1215,15 +1209,13 @@ func TestServer_ListProjectGrants_PermissionV2(t *testing.T) {
dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) {
projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), gofakeit.AppName(), false, false)
request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{projectResp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{projectResp.GetId()},
},
}
grantedOrg := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email())
request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{
ProjectGrantResourceOwnerFilter: &project.ProjectGrantResourceOwnerFilter{
ProjectGrantResourceOwner: grantedOrg.GetOrganizationId(),
},
ProjectGrantResourceOwnerFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()},
}
instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId())
@@ -1243,8 +1235,8 @@ func TestServer_ListProjectGrants_PermissionV2(t *testing.T) {
orgID := instancePermissionV2.DefaultOrg.GetId()
projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, name, false, false)
request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{projectResp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{projectResp.GetId()},
},
}
@@ -1273,8 +1265,8 @@ func TestServer_ListProjectGrants_PermissionV2(t *testing.T) {
orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email())
projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false)
request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{projectResp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{projectResp.GetId()},
},
}
@@ -1301,8 +1293,8 @@ func TestServer_ListProjectGrants_PermissionV2(t *testing.T) {
orgID := instancePermissionV2.DefaultOrg.GetId()
projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, name, false, false)
request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{projectResp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{projectResp.GetId()},
},
}
@@ -1338,8 +1330,8 @@ func TestServer_ListProjectGrants_PermissionV2(t *testing.T) {
project2Resp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false)
project3Resp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false)
request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &project.InProjectIDsFilter{
ProjectIds: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId()},
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId()},
},
}
+12 -11
View File
@@ -9,6 +9,7 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta"
project_pb "github.com/zitadel/zitadel/pkg/grpc/project/v2beta"
)
@@ -109,20 +110,20 @@ func projectNameFilterToQuery(q *project_pb.ProjectNameFilter) (query.SearchQuer
return query.NewGrantedProjectNameSearchQuery(filter.TextMethodPbToQuery(q.Method), q.GetProjectName())
}
func projectInIDsFilterToQuery(q *project_pb.InProjectIDsFilter) (query.SearchQuery, error) {
return query.NewGrantedProjectIDSearchQuery(q.ProjectIds)
func projectInIDsFilterToQuery(q *filter_pb.InIDsFilter) (query.SearchQuery, error) {
return query.NewGrantedProjectIDSearchQuery(q.Ids)
}
func projectResourceOwnerFilterToQuery(q *project_pb.ProjectResourceOwnerFilter) (query.SearchQuery, error) {
return query.NewGrantedProjectResourceOwnerSearchQuery(q.ProjectResourceOwner)
func projectResourceOwnerFilterToQuery(q *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewGrantedProjectResourceOwnerSearchQuery(q.Id)
}
func projectOrganizationIDFilterToQuery(q *project_pb.ProjectOrganizationIDFilter) (query.SearchQuery, error) {
return query.NewGrantedProjectOrganizationIDSearchQuery(q.ProjectOrganizationId)
func projectOrganizationIDFilterToQuery(q *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewGrantedProjectOrganizationIDSearchQuery(q.Id)
}
func projectGrantResourceOwnerFilterToQuery(q *project_pb.ProjectGrantResourceOwnerFilter) (query.SearchQuery, error) {
return query.NewGrantedProjectGrantResourceOwnerSearchQuery(q.ProjectGrantResourceOwner)
func projectGrantResourceOwnerFilterToQuery(q *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewGrantedProjectGrantResourceOwnerSearchQuery(q.Id)
}
func grantedProjectsToPb(projects []*query.GrantedProject) []*project_pb.Project {
@@ -283,11 +284,11 @@ func projectGrantFilterToModel(filter *project_pb.ProjectGrantSearchFilter) (que
case *project_pb.ProjectGrantSearchFilter_RoleKeyFilter:
return query.NewProjectGrantRoleKeySearchQuery(q.RoleKeyFilter.Key)
case *project_pb.ProjectGrantSearchFilter_InProjectIdsFilter:
return query.NewProjectGrantProjectIDsSearchQuery(q.InProjectIdsFilter.ProjectIds)
return query.NewProjectGrantProjectIDsSearchQuery(q.InProjectIdsFilter.Ids)
case *project_pb.ProjectGrantSearchFilter_ProjectResourceOwnerFilter:
return query.NewProjectGrantResourceOwnerSearchQuery(q.ProjectResourceOwnerFilter.ProjectResourceOwner)
return query.NewProjectGrantResourceOwnerSearchQuery(q.ProjectResourceOwnerFilter.Id)
case *project_pb.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter:
return query.NewProjectGrantGrantedOrgIDSearchQuery(q.ProjectGrantResourceOwnerFilter.ProjectGrantResourceOwner)
return query.NewProjectGrantGrantedOrgIDSearchQuery(q.ProjectGrantResourceOwnerFilter.Id)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-M099f", "List.Query.Invalid")
}
+187
View File
@@ -0,0 +1,187 @@
package user
import (
"context"
"io"
"golang.org/x/text/language"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
legacyobject "github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) createUserTypeHuman(ctx context.Context, humanPb *user.CreateUserRequest_Human, orgId string, userName, userId *string) (*user.CreateUserResponse, error) {
addHumanPb := &user.AddHumanUserRequest{
Username: userName,
UserId: userId,
Organization: &legacyobject.Organization{
Org: &legacyobject.Organization_OrgId{OrgId: orgId},
},
Profile: humanPb.Profile,
Email: humanPb.Email,
Phone: humanPb.Phone,
IdpLinks: humanPb.IdpLinks,
TotpSecret: humanPb.TotpSecret,
}
switch pwType := humanPb.GetPasswordType().(type) {
case *user.CreateUserRequest_Human_HashedPassword:
addHumanPb.PasswordType = &user.AddHumanUserRequest_HashedPassword{
HashedPassword: pwType.HashedPassword,
}
case *user.CreateUserRequest_Human_Password:
addHumanPb.PasswordType = &user.AddHumanUserRequest_Password{
Password: pwType.Password,
}
default:
// optional password is not set
}
newHuman, err := AddUserRequestToAddHuman(addHumanPb)
if err != nil {
return nil, err
}
if err = s.command.AddUserHuman(
ctx,
orgId,
newHuman,
false,
s.userCodeAlg,
); err != nil {
return nil, err
}
return &user.CreateUserResponse{
Id: newHuman.ID,
CreationDate: timestamppb.New(newHuman.Details.EventDate),
EmailCode: newHuman.EmailCode,
PhoneCode: newHuman.PhoneCode,
}, nil
}
func (s *Server) updateUserTypeHuman(ctx context.Context, humanPb *user.UpdateUserRequest_Human, userId string, userName *string) (*user.UpdateUserResponse, error) {
cmd, err := updateHumanUserToCommand(userId, userName, humanPb)
if err != nil {
return nil, err
}
if err = s.command.ChangeUserHuman(ctx, cmd, s.userCodeAlg); err != nil {
return nil, err
}
return &user.UpdateUserResponse{
ChangeDate: timestamppb.New(cmd.Details.EventDate),
EmailCode: cmd.EmailCode,
PhoneCode: cmd.PhoneCode,
}, nil
}
func updateHumanUserToCommand(userId string, userName *string, human *user.UpdateUserRequest_Human) (*command.ChangeHuman, error) {
phone := human.GetPhone()
if phone != nil && phone.Phone == "" && phone.GetVerification() != nil {
return nil, zerrors.ThrowInvalidArgument(nil, "USERv2-4f3d6", "Errors.User.Phone.VerifyingRemovalIsNotSupported")
}
email, err := setHumanEmailToEmail(human.Email, userId)
if err != nil {
return nil, err
}
return &command.ChangeHuman{
ID: userId,
Username: userName,
Profile: SetHumanProfileToProfile(human.Profile),
Email: email,
Phone: setHumanPhoneToPhone(human.Phone, true),
Password: setHumanPasswordToPassword(human.Password),
}, nil
}
func updateHumanUserRequestToChangeHuman(req *user.UpdateHumanUserRequest) (*command.ChangeHuman, error) {
email, err := setHumanEmailToEmail(req.Email, req.GetUserId())
if err != nil {
return nil, err
}
changeHuman := &command.ChangeHuman{
ID: req.GetUserId(),
Username: req.Username,
Email: email,
Phone: setHumanPhoneToPhone(req.Phone, false),
Password: setHumanPasswordToPassword(req.Password),
}
if profile := req.GetProfile(); profile != nil {
var firstName *string
if profile.GivenName != "" {
firstName = &profile.GivenName
}
var lastName *string
if profile.FamilyName != "" {
lastName = &profile.FamilyName
}
changeHuman.Profile = SetHumanProfileToProfile(&user.UpdateUserRequest_Human_Profile{
GivenName: firstName,
FamilyName: lastName,
NickName: profile.NickName,
DisplayName: profile.DisplayName,
PreferredLanguage: profile.PreferredLanguage,
Gender: profile.Gender,
})
}
return changeHuman, nil
}
func SetHumanProfileToProfile(profile *user.UpdateUserRequest_Human_Profile) *command.Profile {
if profile == nil {
return nil
}
return &command.Profile{
FirstName: profile.GivenName,
LastName: profile.FamilyName,
NickName: profile.NickName,
DisplayName: profile.DisplayName,
PreferredLanguage: ifNotNilPtr(profile.PreferredLanguage, language.Make),
Gender: ifNotNilPtr(profile.Gender, genderToDomain),
}
}
func setHumanEmailToEmail(email *user.SetHumanEmail, userID string) (*command.Email, error) {
if email == nil {
return nil, nil
}
var urlTemplate string
if email.GetSendCode() != nil && email.GetSendCode().UrlTemplate != nil {
urlTemplate = *email.GetSendCode().UrlTemplate
if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, userID, "code", "orgID"); err != nil {
return nil, err
}
}
return &command.Email{
Address: domain.EmailAddress(email.Email),
Verified: email.GetIsVerified(),
ReturnCode: email.GetReturnCode() != nil,
URLTemplate: urlTemplate,
}, nil
}
func setHumanPhoneToPhone(phone *user.SetHumanPhone, withRemove bool) *command.Phone {
if phone == nil {
return nil
}
number := phone.GetPhone()
return &command.Phone{
Number: domain.PhoneNumber(number),
Verified: phone.GetIsVerified(),
ReturnCode: phone.GetReturnCode() != nil,
Remove: withRemove && number == "",
}
}
func setHumanPasswordToPassword(password *user.SetPassword) *command.Password {
if password == nil {
return nil
}
return &command.Password{
PasswordCode: password.GetVerificationCode(),
OldPassword: password.GetCurrentPassword(),
Password: password.GetPassword().GetPassword(),
EncodedPasswordHash: password.GetHashedPassword().GetHash(),
ChangeRequired: password.GetPassword().GetChangeRequired() || password.GetHashedPassword().GetChangeRequired(),
}
}
+254
View File
@@ -0,0 +1,254 @@
package user
import (
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func Test_patchHumanUserToCommand(t *testing.T) {
type args struct {
userId string
userName *string
human *user.UpdateUserRequest_Human
}
tests := []struct {
name string
args args
want *command.ChangeHuman
wantErr assert.ErrorAssertionFunc
}{{
name: "single property",
args: args{
userId: "userId",
human: &user.UpdateUserRequest_Human{
Profile: &user.UpdateUserRequest_Human_Profile{
GivenName: gu.Ptr("givenName"),
},
},
},
want: &command.ChangeHuman{
ID: "userId",
Profile: &command.Profile{
FirstName: gu.Ptr("givenName"),
},
},
wantErr: assert.NoError,
}, {
name: "all properties",
args: args{
userId: "userId",
userName: gu.Ptr("userName"),
human: &user.UpdateUserRequest_Human{
Profile: &user.UpdateUserRequest_Human_Profile{
GivenName: gu.Ptr("givenName"),
FamilyName: gu.Ptr("familyName"),
NickName: gu.Ptr("nickName"),
DisplayName: gu.Ptr("displayName"),
PreferredLanguage: gu.Ptr("en-US"),
Gender: gu.Ptr(user.Gender_GENDER_FEMALE),
},
Email: &user.SetHumanEmail{
Email: "[email protected]",
Verification: &user.SetHumanEmail_IsVerified{
IsVerified: true,
},
},
Phone: &user.SetHumanPhone{
Phone: "+123456789",
Verification: &user.SetHumanPhone_IsVerified{
IsVerified: true,
},
},
Password: &user.SetPassword{
Verification: &user.SetPassword_CurrentPassword{
CurrentPassword: "currentPassword",
},
PasswordType: &user.SetPassword_Password{
Password: &user.Password{
Password: "newPassword",
ChangeRequired: true,
},
},
},
},
},
want: &command.ChangeHuman{
ID: "userId",
Username: gu.Ptr("userName"),
Profile: &command.Profile{
FirstName: gu.Ptr("givenName"),
LastName: gu.Ptr("familyName"),
NickName: gu.Ptr("nickName"),
DisplayName: gu.Ptr("displayName"),
PreferredLanguage: &language.AmericanEnglish,
Gender: gu.Ptr(domain.GenderFemale),
},
Email: &command.Email{
Address: "[email protected]",
Verified: true,
},
Phone: &command.Phone{
Number: "+123456789",
Verified: true,
},
Password: &command.Password{
OldPassword: "currentPassword",
Password: "newPassword",
ChangeRequired: true,
},
},
wantErr: assert.NoError,
}, {
name: "set email and request code",
args: args{
userId: "userId",
human: &user.UpdateUserRequest_Human{
Email: &user.SetHumanEmail{
Email: "[email protected]",
Verification: &user.SetHumanEmail_ReturnCode{
ReturnCode: &user.ReturnEmailVerificationCode{},
},
},
},
},
want: &command.ChangeHuman{
ID: "userId",
Email: &command.Email{
Address: "[email protected]",
ReturnCode: true,
},
},
wantErr: assert.NoError,
}, {
name: "set email and send code",
args: args{
userId: "userId",
human: &user.UpdateUserRequest_Human{
Email: &user.SetHumanEmail{
Email: "[email protected]",
Verification: &user.SetHumanEmail_SendCode{
SendCode: &user.SendEmailVerificationCode{},
},
},
},
},
want: &command.ChangeHuman{
ID: "userId",
Email: &command.Email{
Address: "[email protected]",
},
},
wantErr: assert.NoError,
}, {
name: "set email and send code with template",
args: args{
userId: "userId",
human: &user.UpdateUserRequest_Human{
Email: &user.SetHumanEmail{
Email: "[email protected]",
Verification: &user.SetHumanEmail_SendCode{
SendCode: &user.SendEmailVerificationCode{
UrlTemplate: gu.Ptr("Code: {{.Code}}"),
},
},
},
},
},
want: &command.ChangeHuman{
ID: "userId",
Email: &command.Email{
Address: "[email protected]",
URLTemplate: "Code: {{.Code}}",
},
},
wantErr: assert.NoError,
}, {
name: "set phone and request code",
args: args{
userId: "userId",
human: &user.UpdateUserRequest_Human{
Phone: &user.SetHumanPhone{
Phone: "+123456789",
Verification: &user.SetHumanPhone_ReturnCode{
ReturnCode: &user.ReturnPhoneVerificationCode{},
},
},
},
},
want: &command.ChangeHuman{
ID: "userId",
Phone: &command.Phone{
Number: "+123456789",
ReturnCode: true,
},
},
wantErr: assert.NoError,
}, {
name: "set phone and send code",
args: args{
userId: "userId",
human: &user.UpdateUserRequest_Human{
Phone: &user.SetHumanPhone{
Phone: "+123456789",
Verification: &user.SetHumanPhone_SendCode{
SendCode: &user.SendPhoneVerificationCode{},
},
},
},
},
want: &command.ChangeHuman{
ID: "userId",
Phone: &command.Phone{
Number: "+123456789",
},
},
wantErr: assert.NoError,
}, {
name: "remove phone, ok",
args: args{
userId: "userId",
human: &user.UpdateUserRequest_Human{
Phone: &user.SetHumanPhone{},
},
},
want: &command.ChangeHuman{
ID: "userId",
Phone: &command.Phone{
Remove: true,
},
},
wantErr: assert.NoError,
}, {
name: "remove phone with verification, error",
args: args{
userId: "userId",
human: &user.UpdateUserRequest_Human{
Phone: &user.SetHumanPhone{
Verification: &user.SetHumanPhone_ReturnCode{},
},
},
},
wantErr: assert.Error,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := updateHumanUserToCommand(tt.args.userId, tt.args.userName, tt.args.human)
if !tt.wantErr(t, err, fmt.Sprintf("patchHumanUserToCommand(%v, %v, %v)", tt.args.userId, tt.args.userName, tt.args.human)) {
return
}
if diff := cmp.Diff(tt.want, got, cmpopts.EquateComparable(language.Tag{})); diff != "" {
t.Errorf("patchHumanUserToCommand() mismatch (-want +got):\n%s", diff)
}
})
}
}
@@ -10,13 +10,13 @@ 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"
)
func TestServer_SetEmail(t *testing.T) {
func TestServer_Deprecated_SetEmail(t *testing.T) {
userID := Instance.CreateHumanUser(CTX).GetUserId()
tests := []struct {
@@ -0,0 +1,659 @@
//go:build integration
package user_test
import (
"context"
"fmt"
"slices"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/filter/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func TestServer_AddKey(t *testing.T) {
resp := Instance.CreateUserTypeMachine(IamCTX)
userId := resp.GetId()
expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24))
type args struct {
req *user.AddKeyRequest
prepare func(request *user.AddKeyRequest) error
}
tests := []struct {
name string
args args
wantErr bool
wantEmtpyKey bool
}{
{
name: "add key, user not existing",
args: args{
&user.AddKeyRequest{
UserId: "notexisting",
ExpirationDate: expirationDate,
},
func(request *user.AddKeyRequest) error { return nil },
},
wantErr: true,
},
{
name: "generate key pair, ok",
args: args{
&user.AddKeyRequest{
ExpirationDate: expirationDate,
},
func(request *user.AddKeyRequest) error {
request.UserId = userId
return nil
},
},
},
{
name: "add valid public key, ok",
args: args{
&user.AddKeyRequest{
ExpirationDate: expirationDate,
// This is the public key of the tester system user. This must be valid.
PublicKey: []byte(`
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzi+FFSJL7f5yw4KTwzgM
P34ePGycm/M+kT0M7V4Cgx5V3EaDIvTQKTLfBaEB45zb9LtjIXzDw0rXRoS2hO6t
h+CYQCz3KCvh09C0IzxZiB2IS3H/aT+5Bx9EFY+vnAkZjccbyG5YNRvmtOlnvIeI
H7qZ0tEwkPfF5GEZNPJPtmy3UGV7iofdVQS1xRj73+aMw5rvH4D8IdyiAC3VekIb
pt0Vj0SUX3DwKtog337BzTiPk3aXRF0sbFhQoqdJRI8NqgZjCwjq9yfI5tyxYswn
+JGzHGdHvW3idODlmwEt5K2pasiRIWK2OGfq+w0EcltQHabuqEPgZlmhCkRdNfix
BwIDAQAB
-----END PUBLIC KEY-----
`),
},
func(request *user.AddKeyRequest) error {
request.UserId = userId
return nil
},
},
wantEmtpyKey: true,
},
{
name: "add invalid public key, error",
args: args{
&user.AddKeyRequest{
ExpirationDate: expirationDate,
PublicKey: []byte(`
-----BEGIN PUBLIC KEY-----
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
-----END PUBLIC KEY-----
`),
},
func(request *user.AddKeyRequest) error {
request.UserId = userId
return nil
},
},
wantErr: true,
},
{
name: "add key human, error",
args: args{
&user.AddKeyRequest{
ExpirationDate: expirationDate,
},
func(request *user.AddKeyRequest) error {
resp := Instance.CreateUserTypeHuman(IamCTX)
request.UserId = resp.Id
return nil
},
},
wantErr: true,
},
{
name: "add another key, ok",
args: args{
&user.AddKeyRequest{
ExpirationDate: expirationDate,
},
func(request *user.AddKeyRequest) error {
request.UserId = userId
_, err := Client.AddKey(IamCTX, &user.AddKeyRequest{
ExpirationDate: expirationDate,
UserId: userId,
})
return err
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.AddKey(CTX, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, got.KeyId, "key id is empty")
if tt.wantEmtpyKey {
assert.Empty(t, got.KeyContent, "key content is not empty")
} else {
assert.NotEmpty(t, got.KeyContent, "key content is empty")
}
creationDate := got.CreationDate.AsTime()
assert.Greater(t, creationDate, now, "creation date is before the test started")
assert.Less(t, creationDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_AddKey_Permission(t *testing.T) {
OrgCTX := CTX
otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddKey-%s", gofakeit.AppName()), gofakeit.Email())
otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{
OrganizationId: otherOrg.OrganizationId,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: gofakeit.Name(),
},
},
})
require.NoError(t, err)
request := &user.AddKeyRequest{
ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)),
UserId: otherOrgUser.GetId(),
}
type args struct {
ctx context.Context
req *user.AddKeyRequest
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "system, ok",
args: args{SystemCTX, request},
},
{
name: "instance, ok",
args: args{IamCTX, request},
},
{
name: "org, error",
args: args{OrgCTX, request},
wantErr: true,
},
{
name: "user, error",
args: args{UserCTX, request},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
require.NoError(t, err)
got, err := Client.AddKey(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, got.KeyId, "key id is empty")
assert.NotEmpty(t, got.KeyContent, "key content is empty")
creationDate := got.CreationDate.AsTime()
assert.Greater(t, creationDate, now, "creation date is before the test started")
assert.Less(t, creationDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_RemoveKey(t *testing.T) {
resp := Instance.CreateUserTypeMachine(IamCTX)
userId := resp.GetId()
expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24))
type args struct {
req *user.RemoveKeyRequest
prepare func(request *user.RemoveKeyRequest) error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "remove key, user not existing",
args: args{
&user.RemoveKeyRequest{
UserId: "notexisting",
},
func(request *user.RemoveKeyRequest) error {
key, err := Client.AddKey(IamCTX, &user.AddKeyRequest{
ExpirationDate: expirationDate,
UserId: userId,
})
request.KeyId = key.GetKeyId()
return err
},
},
wantErr: true,
},
{
name: "remove key, not existing",
args: args{
&user.RemoveKeyRequest{
KeyId: "notexisting",
},
func(request *user.RemoveKeyRequest) error {
request.UserId = userId
return nil
},
},
wantErr: true,
},
{
name: "remove key, ok",
args: args{
&user.RemoveKeyRequest{},
func(request *user.RemoveKeyRequest) error {
key, err := Client.AddKey(IamCTX, &user.AddKeyRequest{
ExpirationDate: expirationDate,
UserId: userId,
})
request.KeyId = key.GetKeyId()
request.UserId = userId
return err
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.RemoveKey(CTX, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
deletionDate := got.DeletionDate.AsTime()
assert.Greater(t, deletionDate, now, "creation date is before the test started")
assert.Less(t, deletionDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_RemoveKey_Permission(t *testing.T) {
OrgCTX := CTX
otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("RemoveKey-%s", gofakeit.AppName()), gofakeit.Email())
otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{
OrganizationId: otherOrg.OrganizationId,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: gofakeit.Name(),
},
},
})
request := &user.RemoveKeyRequest{
UserId: otherOrgUser.GetId(),
}
prepare := func(request *user.RemoveKeyRequest) error {
key, err := Client.AddKey(IamCTX, &user.AddKeyRequest{
ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)),
UserId: otherOrgUser.GetId(),
})
request.KeyId = key.GetKeyId()
return err
}
require.NoError(t, err)
type args struct {
ctx context.Context
req *user.RemoveKeyRequest
prepare func(request *user.RemoveKeyRequest) error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "system, ok",
args: args{SystemCTX, request, prepare},
},
{
name: "instance, ok",
args: args{IamCTX, request, prepare},
},
{
name: "org, error",
args: args{OrgCTX, request, prepare},
wantErr: true,
},
{
name: "user, error",
args: args{UserCTX, request, prepare},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
require.NoError(t, tt.args.prepare(tt.args.req))
got, err := Client.RemoveKey(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, got.DeletionDate, "client key is empty")
creationDate := got.DeletionDate.AsTime()
assert.Greater(t, creationDate, now, "creation date is before the test started")
assert.Less(t, creationDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_ListKeys(t *testing.T) {
type args struct {
ctx context.Context
req *user.ListKeysRequest
}
type testCase struct {
name string
args args
want *user.ListKeysResponse
}
OrgCTX := CTX
otherOrg := Instance.CreateOrganization(SystemCTX, fmt.Sprintf("ListKeys-%s", gofakeit.AppName()), gofakeit.Email())
otherOrgUser, err := Client.CreateUser(SystemCTX, &user.CreateUserRequest{
OrganizationId: otherOrg.OrganizationId,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: gofakeit.Name(),
},
},
})
require.NoError(t, err)
otherOrgUserId := otherOrgUser.GetId()
otherUserId := Instance.CreateUserTypeMachine(SystemCTX).GetId()
onlySinceTestStartFilter := &user.KeysSearchFilter{Filter: &user.KeysSearchFilter_CreatedDateFilter{CreatedDateFilter: &filter.TimestampFilter{
Timestamp: timestamppb.Now(),
Method: filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS,
}}}
myOrgId := Instance.DefaultOrg.GetId()
myUserId := Instance.Users.Get(integration.UserTypeNoPermission).ID
expiresInADay := time.Now().Truncate(time.Hour).Add(time.Hour * 24)
myDataPoint := setupKeyDataPoint(t, myUserId, myOrgId, expiresInADay)
otherUserDataPoint := setupKeyDataPoint(t, otherUserId, myOrgId, expiresInADay)
otherOrgDataPointExpiringSoon := setupKeyDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, time.Now().Truncate(time.Hour).Add(time.Hour))
otherOrgDataPointExpiringLate := setupKeyDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, expiresInADay.Add(time.Hour*24*30))
sortingColumnExpirationDate := user.KeyFieldName_KEY_FIELD_NAME_KEY_EXPIRATION_DATE
awaitKeys(t, onlySinceTestStartFilter,
otherOrgDataPointExpiringSoon.GetId(),
otherOrgDataPointExpiringLate.GetId(),
otherUserDataPoint.GetId(),
myDataPoint.GetId(),
)
tests := []testCase{
{
name: "list all, instance",
args: args{
IamCTX,
&user.ListKeysRequest{Filters: []*user.KeysSearchFilter{onlySinceTestStartFilter}},
},
want: &user.ListKeysResponse{
Result: []*user.Key{
otherOrgDataPointExpiringLate,
otherOrgDataPointExpiringSoon,
otherUserDataPoint,
myDataPoint,
},
Pagination: &filter.PaginationResponse{
TotalResult: 4,
AppliedLimit: 100,
},
},
},
{
name: "list all, org",
args: args{
OrgCTX,
&user.ListKeysRequest{Filters: []*user.KeysSearchFilter{onlySinceTestStartFilter}},
},
want: &user.ListKeysResponse{
Result: []*user.Key{
otherUserDataPoint,
myDataPoint,
},
Pagination: &filter.PaginationResponse{
TotalResult: 2,
AppliedLimit: 100,
},
},
},
{
name: "list all, user",
args: args{
UserCTX,
&user.ListKeysRequest{Filters: []*user.KeysSearchFilter{onlySinceTestStartFilter}},
},
want: &user.ListKeysResponse{
Result: []*user.Key{
myDataPoint,
},
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
},
},
{
name: "list by id",
args: args{
IamCTX,
&user.ListKeysRequest{
Filters: []*user.KeysSearchFilter{
onlySinceTestStartFilter,
{
Filter: &user.KeysSearchFilter_KeyIdFilter{
KeyIdFilter: &filter.IDFilter{Id: otherOrgDataPointExpiringSoon.Id},
},
},
},
},
},
want: &user.ListKeysResponse{
Result: []*user.Key{
otherOrgDataPointExpiringSoon,
},
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
},
},
{
name: "list all from other org",
args: args{
IamCTX,
&user.ListKeysRequest{
Filters: []*user.KeysSearchFilter{
onlySinceTestStartFilter,
{
Filter: &user.KeysSearchFilter_OrganizationIdFilter{
OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId},
},
},
},
},
},
want: &user.ListKeysResponse{
Result: []*user.Key{
otherOrgDataPointExpiringLate,
otherOrgDataPointExpiringSoon,
},
Pagination: &filter.PaginationResponse{
TotalResult: 2,
AppliedLimit: 100,
},
},
},
{
name: "sort by next expiration dates",
args: args{
IamCTX,
&user.ListKeysRequest{
Pagination: &filter.PaginationRequest{
Asc: true,
},
SortingColumn: &sortingColumnExpirationDate,
Filters: []*user.KeysSearchFilter{
onlySinceTestStartFilter,
{Filter: &user.KeysSearchFilter_OrganizationIdFilter{OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId}}},
},
},
},
want: &user.ListKeysResponse{
Result: []*user.Key{
otherOrgDataPointExpiringSoon,
otherOrgDataPointExpiringLate,
},
Pagination: &filter.PaginationResponse{
TotalResult: 2,
AppliedLimit: 100,
},
},
},
{
name: "get page",
args: args{
IamCTX,
&user.ListKeysRequest{
Pagination: &filter.PaginationRequest{
Offset: 2,
Limit: 2,
Asc: true,
},
Filters: []*user.KeysSearchFilter{
onlySinceTestStartFilter,
},
},
},
want: &user.ListKeysResponse{
Result: []*user.Key{
otherOrgDataPointExpiringSoon,
otherOrgDataPointExpiringLate,
},
Pagination: &filter.PaginationResponse{
TotalResult: 4,
AppliedLimit: 2,
},
},
},
{
name: "empty list",
args: args{
UserCTX,
&user.ListKeysRequest{
Filters: []*user.KeysSearchFilter{
{
Filter: &user.KeysSearchFilter_KeyIdFilter{
KeyIdFilter: &filter.IDFilter{Id: otherUserDataPoint.Id},
},
},
},
},
},
want: &user.ListKeysResponse{
Result: []*user.Key{},
Pagination: &filter.PaginationResponse{
TotalResult: 0,
AppliedLimit: 100,
},
},
},
}
t.Run("with permission flag v2", func(t *testing.T) {
setPermissionCheckV2Flag(t, true)
defer setPermissionCheckV2Flag(t, false)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.ListKeys(tt.args.ctx, tt.args.req)
require.NoError(t, err)
assert.Len(t, got.Result, len(tt.want.Result))
if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" {
t.Errorf("ListKeys() mismatch (-want +got):\n%s", diff)
}
})
}
})
t.Run("without permission flag v2", func(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.ListKeys(tt.args.ctx, tt.args.req)
require.NoError(t, err)
assert.Len(t, got.Result, len(tt.want.Result))
// ignore the total result, as this is a known bug with the in-memory permission checks.
// The command can't know how many keys exist in the system if the SQL statement has a limit.
// This is fixed, once the in-memory permission checks are removed with https://github.com/zitadel/zitadel/issues/9188
tt.want.Pagination.TotalResult = got.Pagination.TotalResult
if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" {
t.Errorf("ListKeys() mismatch (-want +got):\n%s", diff)
}
})
}
})
}
func setupKeyDataPoint(t *testing.T, userId, orgId string, expirationDate time.Time) *user.Key {
expirationDatePb := timestamppb.New(expirationDate)
newKey, err := Client.AddKey(SystemCTX, &user.AddKeyRequest{
UserId: userId,
ExpirationDate: expirationDatePb,
PublicKey: nil,
})
require.NoError(t, err)
return &user.Key{
CreationDate: newKey.CreationDate,
ChangeDate: newKey.CreationDate,
Id: newKey.GetKeyId(),
UserId: userId,
OrganizationId: orgId,
ExpirationDate: expirationDatePb,
}
}
func awaitKeys(t *testing.T, sinceTestStartFilter *user.KeysSearchFilter, keyIds ...string) {
sortingColumn := user.KeyFieldName_KEY_FIELD_NAME_ID
slices.Sort(keyIds)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
result, err := Client.ListKeys(SystemCTX, &user.ListKeysRequest{
Filters: []*user.KeysSearchFilter{sinceTestStartFilter},
SortingColumn: &sortingColumn,
Pagination: &filter.PaginationRequest{
Asc: true,
},
})
require.NoError(t, err)
if !assert.Len(collect, result.Result, len(keyIds)) {
return
}
for i := range keyIds {
keyId := keyIds[i]
require.Equal(collect, keyId, result.Result[i].GetId())
}
}, 5*time.Second, time.Second, "key not created in time")
}
@@ -104,7 +104,7 @@ func TestServer_RequestPasswordReset(t *testing.T) {
}
}
func TestServer_SetPassword(t *testing.T) {
func TestServer_Deprecated_SetPassword(t *testing.T) {
type args struct {
ctx context.Context
req *user.SetPasswordRequest
@@ -0,0 +1,615 @@
//go:build integration
package user_test
import (
"context"
"fmt"
"slices"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/filter/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func TestServer_AddPersonalAccessToken(t *testing.T) {
resp := Instance.CreateUserTypeMachine(IamCTX)
userId := resp.GetId()
expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24))
type args struct {
req *user.AddPersonalAccessTokenRequest
prepare func(request *user.AddPersonalAccessTokenRequest) error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "add pat, user not existing",
args: args{
&user.AddPersonalAccessTokenRequest{
UserId: "notexisting",
ExpirationDate: expirationDate,
},
func(request *user.AddPersonalAccessTokenRequest) error { return nil },
},
wantErr: true,
},
{
name: "add pat, ok",
args: args{
&user.AddPersonalAccessTokenRequest{
ExpirationDate: expirationDate,
},
func(request *user.AddPersonalAccessTokenRequest) error {
request.UserId = userId
return nil
},
},
},
{
name: "add pat human, not ok",
args: args{
&user.AddPersonalAccessTokenRequest{
ExpirationDate: expirationDate,
},
func(request *user.AddPersonalAccessTokenRequest) error {
resp := Instance.CreateUserTypeHuman(IamCTX)
request.UserId = resp.Id
return nil
},
},
wantErr: true,
},
{
name: "add another pat, ok",
args: args{
&user.AddPersonalAccessTokenRequest{
ExpirationDate: expirationDate,
},
func(request *user.AddPersonalAccessTokenRequest) error {
request.UserId = userId
_, err := Client.AddPersonalAccessToken(IamCTX, &user.AddPersonalAccessTokenRequest{
ExpirationDate: expirationDate,
UserId: userId,
})
return err
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.AddPersonalAccessToken(CTX, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, got.TokenId, "id is empty")
assert.NotEmpty(t, got.Token, "token is empty")
creationDate := got.CreationDate.AsTime()
assert.Greater(t, creationDate, now, "creation date is before the test started")
assert.Less(t, creationDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_AddPersonalAccessToken_Permission(t *testing.T) {
OrgCTX := CTX
otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddPersonalAccessToken-%s", gofakeit.AppName()), gofakeit.Email())
otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{
OrganizationId: otherOrg.OrganizationId,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: gofakeit.Name(),
},
},
})
require.NoError(t, err)
request := &user.AddPersonalAccessTokenRequest{
ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)),
UserId: otherOrgUser.GetId(),
}
type args struct {
ctx context.Context
req *user.AddPersonalAccessTokenRequest
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "system, ok",
args: args{SystemCTX, request},
},
{
name: "instance, ok",
args: args{IamCTX, request},
},
{
name: "org, error",
args: args{OrgCTX, request},
wantErr: true,
},
{
name: "user, error",
args: args{UserCTX, request},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
require.NoError(t, err)
got, err := Client.AddPersonalAccessToken(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, got.TokenId, "id is empty")
assert.NotEmpty(t, got.Token, "token is empty")
creationDate := got.CreationDate.AsTime()
assert.Greater(t, creationDate, now, "creation date is before the test started")
assert.Less(t, creationDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_RemovePersonalAccessToken(t *testing.T) {
resp := Instance.CreateUserTypeMachine(IamCTX)
userId := resp.GetId()
expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24))
type args struct {
req *user.RemovePersonalAccessTokenRequest
prepare func(request *user.RemovePersonalAccessTokenRequest) error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "remove pat, user not existing",
args: args{
&user.RemovePersonalAccessTokenRequest{
UserId: "notexisting",
},
func(request *user.RemovePersonalAccessTokenRequest) error {
pat, err := Client.AddPersonalAccessToken(CTX, &user.AddPersonalAccessTokenRequest{
ExpirationDate: expirationDate,
UserId: userId,
})
request.TokenId = pat.GetTokenId()
return err
},
},
wantErr: true,
},
{
name: "remove pat, not existing",
args: args{
&user.RemovePersonalAccessTokenRequest{
TokenId: "notexisting",
},
func(request *user.RemovePersonalAccessTokenRequest) error {
request.UserId = userId
return nil
},
},
wantErr: true,
},
{
name: "remove pat, ok",
args: args{
&user.RemovePersonalAccessTokenRequest{},
func(request *user.RemovePersonalAccessTokenRequest) error {
pat, err := Client.AddPersonalAccessToken(CTX, &user.AddPersonalAccessTokenRequest{
ExpirationDate: expirationDate,
UserId: userId,
})
request.TokenId = pat.GetTokenId()
request.UserId = userId
return err
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.RemovePersonalAccessToken(CTX, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
deletionDate := got.DeletionDate.AsTime()
assert.Greater(t, deletionDate, now, "creation date is before the test started")
assert.Less(t, deletionDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_RemovePersonalAccessToken_Permission(t *testing.T) {
otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("RemovePersonalAccessToken-%s", gofakeit.AppName()), gofakeit.Email())
otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{
OrganizationId: otherOrg.OrganizationId,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: gofakeit.Name(),
},
},
})
request := &user.RemovePersonalAccessTokenRequest{
UserId: otherOrgUser.GetId(),
}
prepare := func(request *user.RemovePersonalAccessTokenRequest) error {
pat, err := Client.AddPersonalAccessToken(IamCTX, &user.AddPersonalAccessTokenRequest{
ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)),
UserId: otherOrgUser.GetId(),
})
request.TokenId = pat.GetTokenId()
return err
}
require.NoError(t, err)
type args struct {
ctx context.Context
req *user.RemovePersonalAccessTokenRequest
prepare func(request *user.RemovePersonalAccessTokenRequest) error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "system, ok",
args: args{SystemCTX, request, prepare},
},
{
name: "instance, ok",
args: args{IamCTX, request, prepare},
},
{
name: "org, error",
args: args{CTX, request, prepare},
wantErr: true,
},
{
name: "user, error",
args: args{UserCTX, request, prepare},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
require.NoError(t, tt.args.prepare(tt.args.req))
got, err := Client.RemovePersonalAccessToken(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, got.DeletionDate, "client pat is empty")
creationDate := got.DeletionDate.AsTime()
assert.Greater(t, creationDate, now, "creation date is before the test started")
assert.Less(t, creationDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_ListPersonalAccessTokens(t *testing.T) {
type args struct {
ctx context.Context
req *user.ListPersonalAccessTokensRequest
}
type testCase struct {
name string
args args
want *user.ListPersonalAccessTokensResponse
}
OrgCTX := CTX
otherOrg := Instance.CreateOrganization(SystemCTX, fmt.Sprintf("ListPersonalAccessTokens-%s", gofakeit.AppName()), gofakeit.Email())
otherOrgUser, err := Client.CreateUser(SystemCTX, &user.CreateUserRequest{
OrganizationId: otherOrg.OrganizationId,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: gofakeit.Name(),
},
},
})
require.NoError(t, err)
otherOrgUserId := otherOrgUser.GetId()
otherUserId := Instance.CreateUserTypeMachine(SystemCTX).GetId()
onlySinceTestStartFilter := &user.PersonalAccessTokensSearchFilter{Filter: &user.PersonalAccessTokensSearchFilter_CreatedDateFilter{CreatedDateFilter: &filter.TimestampFilter{
Timestamp: timestamppb.Now(),
Method: filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS,
}}}
myOrgId := Instance.DefaultOrg.GetId()
myUserId := Instance.Users.Get(integration.UserTypeNoPermission).ID
expiresInADay := time.Now().Truncate(time.Hour).Add(time.Hour * 24)
myDataPoint := setupPATDataPoint(t, myUserId, myOrgId, expiresInADay)
otherUserDataPoint := setupPATDataPoint(t, otherUserId, myOrgId, expiresInADay)
otherOrgDataPointExpiringSoon := setupPATDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, time.Now().Truncate(time.Hour).Add(time.Hour))
otherOrgDataPointExpiringLate := setupPATDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, expiresInADay.Add(time.Hour*24*30))
sortingColumnExpirationDate := user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_EXPIRATION_DATE
awaitPersonalAccessTokens(t,
onlySinceTestStartFilter,
otherOrgDataPointExpiringSoon.GetId(),
otherOrgDataPointExpiringLate.GetId(),
otherUserDataPoint.GetId(),
myDataPoint.GetId(),
)
tests := []testCase{
{
name: "list all, instance",
args: args{
IamCTX,
&user.ListPersonalAccessTokensRequest{
Filters: []*user.PersonalAccessTokensSearchFilter{onlySinceTestStartFilter},
},
},
want: &user.ListPersonalAccessTokensResponse{
Result: []*user.PersonalAccessToken{
otherOrgDataPointExpiringLate,
otherOrgDataPointExpiringSoon,
otherUserDataPoint,
myDataPoint,
},
Pagination: &filter.PaginationResponse{
TotalResult: 4,
AppliedLimit: 100,
},
},
},
{
name: "list all, org",
args: args{
OrgCTX,
&user.ListPersonalAccessTokensRequest{
Filters: []*user.PersonalAccessTokensSearchFilter{onlySinceTestStartFilter},
},
},
want: &user.ListPersonalAccessTokensResponse{
Result: []*user.PersonalAccessToken{
otherUserDataPoint,
myDataPoint,
},
Pagination: &filter.PaginationResponse{
TotalResult: 2,
AppliedLimit: 100,
},
},
},
{
name: "list all, user",
args: args{
UserCTX,
&user.ListPersonalAccessTokensRequest{
Filters: []*user.PersonalAccessTokensSearchFilter{onlySinceTestStartFilter},
},
},
want: &user.ListPersonalAccessTokensResponse{
Result: []*user.PersonalAccessToken{
myDataPoint,
},
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
},
},
{
name: "list by id",
args: args{
IamCTX,
&user.ListPersonalAccessTokensRequest{
Filters: []*user.PersonalAccessTokensSearchFilter{
onlySinceTestStartFilter,
{
Filter: &user.PersonalAccessTokensSearchFilter_TokenIdFilter{
TokenIdFilter: &filter.IDFilter{Id: otherOrgDataPointExpiringSoon.Id},
},
},
},
},
},
want: &user.ListPersonalAccessTokensResponse{
Result: []*user.PersonalAccessToken{
otherOrgDataPointExpiringSoon,
},
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
},
},
{
name: "list all from other org",
args: args{
IamCTX,
&user.ListPersonalAccessTokensRequest{
Filters: []*user.PersonalAccessTokensSearchFilter{
onlySinceTestStartFilter,
{
Filter: &user.PersonalAccessTokensSearchFilter_OrganizationIdFilter{
OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId},
},
}},
},
},
want: &user.ListPersonalAccessTokensResponse{
Result: []*user.PersonalAccessToken{
otherOrgDataPointExpiringLate,
otherOrgDataPointExpiringSoon,
},
Pagination: &filter.PaginationResponse{
TotalResult: 2,
AppliedLimit: 100,
},
},
},
{
name: "sort by next expiration dates",
args: args{
IamCTX,
&user.ListPersonalAccessTokensRequest{
Pagination: &filter.PaginationRequest{
Asc: true,
},
SortingColumn: &sortingColumnExpirationDate,
Filters: []*user.PersonalAccessTokensSearchFilter{
onlySinceTestStartFilter,
{Filter: &user.PersonalAccessTokensSearchFilter_OrganizationIdFilter{OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId}}},
},
},
},
want: &user.ListPersonalAccessTokensResponse{
Result: []*user.PersonalAccessToken{
otherOrgDataPointExpiringSoon,
otherOrgDataPointExpiringLate,
},
Pagination: &filter.PaginationResponse{
TotalResult: 2,
AppliedLimit: 100,
},
},
},
{
name: "get page",
args: args{
IamCTX,
&user.ListPersonalAccessTokensRequest{
Pagination: &filter.PaginationRequest{
Offset: 2,
Limit: 2,
Asc: true,
},
Filters: []*user.PersonalAccessTokensSearchFilter{
onlySinceTestStartFilter,
},
},
},
want: &user.ListPersonalAccessTokensResponse{
Result: []*user.PersonalAccessToken{
otherOrgDataPointExpiringSoon,
otherOrgDataPointExpiringLate,
},
Pagination: &filter.PaginationResponse{
TotalResult: 4,
AppliedLimit: 2,
},
},
},
{
name: "empty list",
args: args{
UserCTX,
&user.ListPersonalAccessTokensRequest{
Filters: []*user.PersonalAccessTokensSearchFilter{
{
Filter: &user.PersonalAccessTokensSearchFilter_TokenIdFilter{
TokenIdFilter: &filter.IDFilter{Id: otherUserDataPoint.Id},
},
},
},
},
},
want: &user.ListPersonalAccessTokensResponse{
Result: []*user.PersonalAccessToken{},
Pagination: &filter.PaginationResponse{
TotalResult: 0,
AppliedLimit: 100,
},
},
},
}
t.Run("with permission flag v2", func(t *testing.T) {
setPermissionCheckV2Flag(t, true)
defer setPermissionCheckV2Flag(t, false)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.ListPersonalAccessTokens(tt.args.ctx, tt.args.req)
require.NoError(t, err)
assert.Len(t, got.Result, len(tt.want.Result))
if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" {
t.Errorf("ListPersonalAccessTokens() mismatch (-want +got):\n%s", diff)
}
})
}
})
t.Run("without permission flag v2", func(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.ListPersonalAccessTokens(tt.args.ctx, tt.args.req)
require.NoError(t, err)
assert.Len(t, got.Result, len(tt.want.Result))
// ignore the total result, as this is a known bug with the in-memory permission checks.
// The command can't know how many keys exist in the system if the SQL statement has a limit.
// This is fixed, once the in-memory permission checks are removed with https://github.com/zitadel/zitadel/issues/9188
tt.want.Pagination.TotalResult = got.Pagination.TotalResult
if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" {
t.Errorf("ListPersonalAccessTokens() mismatch (-want +got):\n%s", diff)
}
})
}
})
}
func setupPATDataPoint(t *testing.T, userId, orgId string, expirationDate time.Time) *user.PersonalAccessToken {
expirationDatePb := timestamppb.New(expirationDate)
newPersonalAccessToken, err := Client.AddPersonalAccessToken(SystemCTX, &user.AddPersonalAccessTokenRequest{
UserId: userId,
ExpirationDate: expirationDatePb,
})
require.NoError(t, err)
return &user.PersonalAccessToken{
CreationDate: newPersonalAccessToken.CreationDate,
ChangeDate: newPersonalAccessToken.CreationDate,
Id: newPersonalAccessToken.GetTokenId(),
UserId: userId,
OrganizationId: orgId,
ExpirationDate: expirationDatePb,
}
}
func awaitPersonalAccessTokens(t *testing.T, sinceTestStartFilter *user.PersonalAccessTokensSearchFilter, patIds ...string) {
sortingColumn := user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_ID
slices.Sort(patIds)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
result, err := Client.ListPersonalAccessTokens(SystemCTX, &user.ListPersonalAccessTokensRequest{
Filters: []*user.PersonalAccessTokensSearchFilter{sinceTestStartFilter},
SortingColumn: &sortingColumn,
Pagination: &filter.PaginationRequest{
Asc: true,
},
})
require.NoError(t, err)
if !assert.Len(collect, result.Result, len(patIds)) {
return
}
for i := range patIds {
patId := patIds[i]
require.Equal(collect, patId, result.Result[i].GetId())
}
}, 5*time.Second, time.Second, "pat not created in time")
}
@@ -17,7 +17,7 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func TestServer_SetPhone(t *testing.T) {
func TestServer_Deprecated_SetPhone(t *testing.T) {
userID := Instance.CreateHumanUser(CTX).GetUserId()
tests := []struct {
@@ -249,7 +249,7 @@ func TestServer_VerifyPhone(t *testing.T) {
}
}
func TestServer_RemovePhone(t *testing.T) {
func TestServer_Deprecated_RemovePhone(t *testing.T) {
userResp := Instance.CreateHumanUser(CTX)
failResp := Instance.CreateHumanUserNoPhone(CTX)
otherUser := Instance.CreateHumanUser(CTX).GetUserId()
@@ -0,0 +1,347 @@
//go:build integration
package user_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func TestServer_AddSecret(t *testing.T) {
type args struct {
ctx context.Context
req *user.AddSecretRequest
prepare func(request *user.AddSecretRequest) error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "add secret, user not existing",
args: args{
CTX,
&user.AddSecretRequest{
UserId: "notexisting",
},
func(request *user.AddSecretRequest) error { return nil },
},
wantErr: true,
},
{
name: "add secret, ok",
args: args{
CTX,
&user.AddSecretRequest{},
func(request *user.AddSecretRequest) error {
resp := Instance.CreateUserTypeMachine(CTX)
request.UserId = resp.GetId()
return nil
},
},
},
{
name: "add secret human, not ok",
args: args{
CTX,
&user.AddSecretRequest{},
func(request *user.AddSecretRequest) error {
resp := Instance.CreateUserTypeMachine(CTX)
request.UserId = resp.GetId()
return nil
},
},
},
{
name: "overwrite secret, ok",
args: args{
CTX,
&user.AddSecretRequest{},
func(request *user.AddSecretRequest) error {
resp := Instance.CreateUserTypeMachine(CTX)
request.UserId = resp.GetId()
_, err := Client.AddSecret(CTX, &user.AddSecretRequest{
UserId: resp.GetId(),
})
return err
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.AddSecret(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, got.ClientSecret, "client secret is empty")
creationDate := got.CreationDate.AsTime()
assert.Greater(t, creationDate, now, "creation date is before the test started")
assert.Less(t, creationDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_AddSecret_Permission(t *testing.T) {
otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddSecret-%s", gofakeit.AppName()), gofakeit.Email())
otherOrgUser, err := Instance.Client.UserV2.CreateUser(IamCTX, &user.CreateUserRequest{
OrganizationId: otherOrg.OrganizationId,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: gofakeit.Name(),
},
},
})
require.NoError(t, err)
type args struct {
ctx context.Context
req *user.AddSecretRequest
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "system, ok",
args: args{
SystemCTX,
&user.AddSecretRequest{
UserId: otherOrgUser.GetId(),
},
},
},
{
name: "instance, ok",
args: args{
IamCTX,
&user.AddSecretRequest{
UserId: otherOrgUser.GetId(),
},
},
},
{
name: "org, error",
args: args{
CTX,
&user.AddSecretRequest{
UserId: otherOrgUser.GetId(),
},
},
wantErr: true,
},
{
name: "user, error",
args: args{
UserCTX,
&user.AddSecretRequest{
UserId: otherOrgUser.GetId(),
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
require.NoError(t, err)
got, err := Client.AddSecret(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, got.ClientSecret, "client secret is empty")
creationDate := got.CreationDate.AsTime()
assert.Greater(t, creationDate, now, "creation date is before the test started")
assert.Less(t, creationDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_RemoveSecret(t *testing.T) {
type args struct {
ctx context.Context
req *user.RemoveSecretRequest
prepare func(request *user.RemoveSecretRequest) error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "remove secret, user not existing",
args: args{
CTX,
&user.RemoveSecretRequest{
UserId: "notexisting",
},
func(request *user.RemoveSecretRequest) error { return nil },
},
wantErr: true,
},
{
name: "remove secret, not existing",
args: args{
CTX,
&user.RemoveSecretRequest{},
func(request *user.RemoveSecretRequest) error {
resp := Instance.CreateUserTypeMachine(CTX)
request.UserId = resp.GetId()
return nil
},
},
wantErr: true,
},
{
name: "remove secret, ok",
args: args{
CTX,
&user.RemoveSecretRequest{},
func(request *user.RemoveSecretRequest) error {
resp := Instance.CreateUserTypeMachine(CTX)
request.UserId = resp.GetId()
_, err := Instance.Client.UserV2.AddSecret(CTX, &user.AddSecretRequest{
UserId: resp.GetId(),
})
return err
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.RemoveSecret(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
deletionDate := got.DeletionDate.AsTime()
assert.Greater(t, deletionDate, now, "creation date is before the test started")
assert.Less(t, deletionDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_RemoveSecret_Permission(t *testing.T) {
otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("RemoveSecret-%s", gofakeit.AppName()), gofakeit.Email())
otherOrgUser, err := Instance.Client.UserV2.CreateUser(IamCTX, &user.CreateUserRequest{
OrganizationId: otherOrg.OrganizationId,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: gofakeit.Name(),
},
},
})
require.NoError(t, err)
type args struct {
ctx context.Context
req *user.RemoveSecretRequest
prepare func(request *user.RemoveSecretRequest) error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "system, ok",
args: args{
SystemCTX,
&user.RemoveSecretRequest{
UserId: otherOrgUser.GetId(),
},
func(request *user.RemoveSecretRequest) error {
_, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{
UserId: otherOrgUser.GetId(),
})
return err
},
},
},
{
name: "instance, ok",
args: args{
IamCTX,
&user.RemoveSecretRequest{
UserId: otherOrgUser.GetId(),
},
func(request *user.RemoveSecretRequest) error {
_, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{
UserId: otherOrgUser.GetId(),
})
return err
},
},
},
{
name: "org, error",
args: args{
CTX,
&user.RemoveSecretRequest{
UserId: otherOrgUser.GetId(),
},
func(request *user.RemoveSecretRequest) error {
_, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{
UserId: otherOrgUser.GetId(),
})
return err
},
},
wantErr: true,
},
{
name: "user, error",
args: args{
UserCTX,
&user.RemoveSecretRequest{
UserId: otherOrgUser.GetId(),
},
func(request *user.RemoveSecretRequest) error {
_, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{
UserId: otherOrgUser.GetId(),
})
return err
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
require.NoError(t, tt.args.prepare(tt.args.req))
got, err := Client.RemoveSecret(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, got.DeletionDate, "client secret is empty")
creationDate := got.DeletionDate.AsTime()
assert.Greater(t, creationDate, now, "creation date is before the test started")
assert.Less(t, creationDate, time.Now(), "creation date is in the future")
})
}
}
File diff suppressed because it is too large Load Diff
+62
View File
@@ -0,0 +1,62 @@
package user
import (
"context"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) AddKey(ctx context.Context, req *user.AddKeyRequest) (*user.AddKeyResponse, error) {
newMachineKey := &command.MachineKey{
ObjectRoot: models.ObjectRoot{
AggregateID: req.UserId,
},
ExpirationDate: req.GetExpirationDate().AsTime(),
Type: domain.AuthNKeyTypeJSON,
PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx),
}
newMachineKey.PublicKey = req.PublicKey
pubkeySupplied := len(newMachineKey.PublicKey) > 0
details, err := s.command.AddUserMachineKey(ctx, newMachineKey)
if err != nil {
return nil, err
}
// Return key details only if the pubkey wasn't supplied, otherwise the user already has
// private key locally
var keyDetails []byte
if !pubkeySupplied {
var err error
keyDetails, err = newMachineKey.Detail()
if err != nil {
return nil, err
}
}
return &user.AddKeyResponse{
KeyId: newMachineKey.KeyID,
KeyContent: keyDetails,
CreationDate: timestamppb.New(details.EventDate),
}, nil
}
func (s *Server) RemoveKey(ctx context.Context, req *user.RemoveKeyRequest) (*user.RemoveKeyResponse, error) {
machineKey := &command.MachineKey{
ObjectRoot: models.ObjectRoot{
AggregateID: req.UserId,
},
PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx),
KeyID: req.KeyId,
}
objectDetails, err := s.command.RemoveUserMachineKey(ctx, machineKey)
if err != nil {
return nil, err
}
return &user.RemoveKeyResponse{
DeletionDate: timestamppb.New(objectDetails.EventDate),
}, nil
}
+124
View File
@@ -0,0 +1,124 @@
package user
import (
"context"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc/filter/v2"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) ListKeys(ctx context.Context, req *user.ListKeysRequest) (*user.ListKeysResponse, error) {
offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination)
if err != nil {
return nil, err
}
filters, err := keyFiltersToQueries(req.Filters)
if err != nil {
return nil, err
}
search := &query.AuthNKeySearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: authnKeyFieldNameToSortingColumn(req.SortingColumn),
},
Queries: filters,
}
result, err := s.query.SearchAuthNKeys(ctx, search, query.JoinFilterUserMachine, s.checkPermission)
if err != nil {
return nil, err
}
resp := &user.ListKeysResponse{
Result: make([]*user.Key, len(result.AuthNKeys)),
Pagination: filter.QueryToPaginationPb(search.SearchRequest, result.SearchResponse),
}
for i, key := range result.AuthNKeys {
resp.Result[i] = &user.Key{
CreationDate: timestamppb.New(key.CreationDate),
ChangeDate: timestamppb.New(key.ChangeDate),
Id: key.ID,
UserId: key.AggregateID,
OrganizationId: key.ResourceOwner,
ExpirationDate: timestamppb.New(key.Expiration),
}
}
return resp, nil
}
func keyFiltersToQueries(filters []*user.KeysSearchFilter) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(filters))
for i, filter := range filters {
q[i], err = keyFilterToQuery(filter)
if err != nil {
return nil, err
}
}
return q, nil
}
func keyFilterToQuery(filter *user.KeysSearchFilter) (query.SearchQuery, error) {
switch q := filter.Filter.(type) {
case *user.KeysSearchFilter_CreatedDateFilter:
return authnKeyCreatedFilterToQuery(q.CreatedDateFilter)
case *user.KeysSearchFilter_ExpirationDateFilter:
return authnKeyExpirationFilterToQuery(q.ExpirationDateFilter)
case *user.KeysSearchFilter_KeyIdFilter:
return authnKeyIdFilterToQuery(q.KeyIdFilter)
case *user.KeysSearchFilter_UserIdFilter:
return authnKeyUserIdFilterToQuery(q.UserIdFilter)
case *user.KeysSearchFilter_OrganizationIdFilter:
return authnKeyOrgIdFilterToQuery(q.OrganizationIdFilter)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
}
func authnKeyIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewAuthNKeyIDQuery(f.Id)
}
func authnKeyUserIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewAuthNKeyIdentifyerQuery(f.Id)
}
func authnKeyOrgIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewAuthNKeyResourceOwnerQuery(f.Id)
}
func authnKeyCreatedFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) {
return query.NewAuthNKeyCreationDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method))
}
func authnKeyExpirationFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) {
return query.NewAuthNKeyExpirationDateDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method))
}
// authnKeyFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination
func authnKeyFieldNameToSortingColumn(field *user.KeyFieldName) query.Column {
if field == nil {
return query.AuthNKeyColumnCreationDate
}
switch *field {
case user.KeyFieldName_KEY_FIELD_NAME_UNSPECIFIED:
return query.AuthNKeyColumnCreationDate
case user.KeyFieldName_KEY_FIELD_NAME_ID:
return query.AuthNKeyColumnID
case user.KeyFieldName_KEY_FIELD_NAME_USER_ID:
return query.AuthNKeyColumnIdentifier
case user.KeyFieldName_KEY_FIELD_NAME_ORGANIZATION_ID:
return query.AuthNKeyColumnResourceOwner
case user.KeyFieldName_KEY_FIELD_NAME_CREATED_DATE:
return query.AuthNKeyColumnCreationDate
case user.KeyFieldName_KEY_FIELD_NAME_KEY_EXPIRATION_DATE:
return query.AuthNKeyColumnExpiration
default:
return query.AuthNKeyColumnCreationDate
}
}
+58
View File
@@ -0,0 +1,58 @@
package user
import (
"context"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) createUserTypeMachine(ctx context.Context, machinePb *user.CreateUserRequest_Machine, orgId, userName, userId string) (*user.CreateUserResponse, error) {
cmd := &command.Machine{
Username: userName,
Name: machinePb.Name,
Description: machinePb.GetDescription(),
AccessTokenType: domain.OIDCTokenTypeBearer,
ObjectRoot: models.ObjectRoot{
ResourceOwner: orgId,
AggregateID: userId,
},
}
details, err := s.command.AddMachine(
ctx,
cmd,
s.command.NewPermissionCheckUserWrite(ctx),
command.AddMachineWithUsernameToIDFallback(),
)
if err != nil {
return nil, err
}
return &user.CreateUserResponse{
Id: cmd.AggregateID,
CreationDate: timestamppb.New(details.EventDate),
}, nil
}
func (s *Server) updateUserTypeMachine(ctx context.Context, machinePb *user.UpdateUserRequest_Machine, userId string, userName *string) (*user.UpdateUserResponse, error) {
cmd := updateMachineUserToCommand(userId, userName, machinePb)
err := s.command.ChangeUserMachine(ctx, cmd)
if err != nil {
return nil, err
}
return &user.UpdateUserResponse{
ChangeDate: timestamppb.New(cmd.Details.EventDate),
}, nil
}
func updateMachineUserToCommand(userId string, userName *string, machine *user.UpdateUserRequest_Machine) *command.ChangeMachine {
return &command.ChangeMachine{
ID: userId,
Username: userName,
Name: machine.Name,
Description: machine.Description,
}
}
+62
View File
@@ -0,0 +1,62 @@
package user
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/muhlemmer/gu"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func Test_patchMachineUserToCommand(t *testing.T) {
type args struct {
userId string
userName *string
machine *user.UpdateUserRequest_Machine
}
tests := []struct {
name string
args args
want *command.ChangeMachine
}{{
name: "single property",
args: args{
userId: "userId",
machine: &user.UpdateUserRequest_Machine{
Name: gu.Ptr("name"),
},
},
want: &command.ChangeMachine{
ID: "userId",
Name: gu.Ptr("name"),
},
}, {
name: "all properties",
args: args{
userId: "userId",
userName: gu.Ptr("userName"),
machine: &user.UpdateUserRequest_Machine{
Name: gu.Ptr("name"),
Description: gu.Ptr("description"),
},
},
want: &command.ChangeMachine{
ID: "userId",
Username: gu.Ptr("userName"),
Name: gu.Ptr("name"),
Description: gu.Ptr("description"),
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := updateMachineUserToCommand(tt.args.userId, tt.args.userName, tt.args.machine)
if diff := cmp.Diff(tt.want, got, cmpopts.EquateComparable(language.Tag{})); diff != "" {
t.Errorf("patchMachineUserToCommand() mismatch (-want +got):\n%s", diff)
}
})
}
}
+56
View File
@@ -0,0 +1,56 @@
package user
import (
"context"
"github.com/zitadel/oidc/v3/pkg/oidc"
"google.golang.org/protobuf/types/known/timestamppb"
z_oidc "github.com/zitadel/zitadel/internal/api/oidc"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) AddPersonalAccessToken(ctx context.Context, req *user.AddPersonalAccessTokenRequest) (*user.AddPersonalAccessTokenResponse, error) {
newPat := &command.PersonalAccessToken{
ObjectRoot: models.ObjectRoot{
AggregateID: req.UserId,
},
PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx),
ExpirationDate: req.ExpirationDate.AsTime(),
Scopes: []string{
oidc.ScopeOpenID,
oidc.ScopeProfile,
z_oidc.ScopeUserMetaData,
z_oidc.ScopeResourceOwner,
},
AllowedUserType: domain.UserTypeMachine,
}
details, err := s.command.AddPersonalAccessToken(ctx, newPat)
if err != nil {
return nil, err
}
return &user.AddPersonalAccessTokenResponse{
CreationDate: timestamppb.New(details.EventDate),
TokenId: newPat.TokenID,
Token: newPat.Token,
}, nil
}
func (s *Server) RemovePersonalAccessToken(ctx context.Context, req *user.RemovePersonalAccessTokenRequest) (*user.RemovePersonalAccessTokenResponse, error) {
objectDetails, err := s.command.RemovePersonalAccessToken(ctx, &command.PersonalAccessToken{
TokenID: req.TokenId,
ObjectRoot: models.ObjectRoot{
AggregateID: req.UserId,
},
PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx),
})
if err != nil {
return nil, err
}
return &user.RemovePersonalAccessTokenResponse{
DeletionDate: timestamppb.New(objectDetails.EventDate),
}, nil
}
+123
View File
@@ -0,0 +1,123 @@
package user
import (
"context"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc/filter/v2"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) ListPersonalAccessTokens(ctx context.Context, req *user.ListPersonalAccessTokensRequest) (*user.ListPersonalAccessTokensResponse, error) {
offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination)
if err != nil {
return nil, err
}
filters, err := patFiltersToQueries(req.Filters)
if err != nil {
return nil, err
}
search := &query.PersonalAccessTokenSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: authnPersonalAccessTokenFieldNameToSortingColumn(req.SortingColumn),
},
Queries: filters,
}
result, err := s.query.SearchPersonalAccessTokens(ctx, search, s.checkPermission)
if err != nil {
return nil, err
}
resp := &user.ListPersonalAccessTokensResponse{
Result: make([]*user.PersonalAccessToken, len(result.PersonalAccessTokens)),
Pagination: filter.QueryToPaginationPb(search.SearchRequest, result.SearchResponse),
}
for i, pat := range result.PersonalAccessTokens {
resp.Result[i] = &user.PersonalAccessToken{
CreationDate: timestamppb.New(pat.CreationDate),
ChangeDate: timestamppb.New(pat.ChangeDate),
Id: pat.ID,
UserId: pat.UserID,
OrganizationId: pat.ResourceOwner,
ExpirationDate: timestamppb.New(pat.Expiration),
}
}
return resp, nil
}
func patFiltersToQueries(filters []*user.PersonalAccessTokensSearchFilter) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(filters))
for i, filter := range filters {
q[i], err = patFilterToQuery(filter)
if err != nil {
return nil, err
}
}
return q, nil
}
func patFilterToQuery(filter *user.PersonalAccessTokensSearchFilter) (query.SearchQuery, error) {
switch q := filter.Filter.(type) {
case *user.PersonalAccessTokensSearchFilter_CreatedDateFilter:
return authnPersonalAccessTokenCreatedFilterToQuery(q.CreatedDateFilter)
case *user.PersonalAccessTokensSearchFilter_ExpirationDateFilter:
return authnPersonalAccessTokenExpirationFilterToQuery(q.ExpirationDateFilter)
case *user.PersonalAccessTokensSearchFilter_TokenIdFilter:
return authnPersonalAccessTokenIdFilterToQuery(q.TokenIdFilter)
case *user.PersonalAccessTokensSearchFilter_UserIdFilter:
return authnPersonalAccessTokenUserIdFilterToQuery(q.UserIdFilter)
case *user.PersonalAccessTokensSearchFilter_OrganizationIdFilter:
return authnPersonalAccessTokenOrgIdFilterToQuery(q.OrganizationIdFilter)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
}
func authnPersonalAccessTokenIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewPersonalAccessTokenIDQuery(f.Id)
}
func authnPersonalAccessTokenUserIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewPersonalAccessTokenUserIDSearchQuery(f.Id)
}
func authnPersonalAccessTokenOrgIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewPersonalAccessTokenResourceOwnerSearchQuery(f.Id)
}
func authnPersonalAccessTokenCreatedFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) {
return query.NewPersonalAccessTokenCreationDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method))
}
func authnPersonalAccessTokenExpirationFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) {
return query.NewPersonalAccessTokenExpirationDateDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method))
}
// authnPersonalAccessTokenFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination
func authnPersonalAccessTokenFieldNameToSortingColumn(field *user.PersonalAccessTokenFieldName) query.Column {
if field == nil {
return query.PersonalAccessTokenColumnCreationDate
}
switch *field {
case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_UNSPECIFIED:
return query.PersonalAccessTokenColumnCreationDate
case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_ID:
return query.PersonalAccessTokenColumnID
case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_USER_ID:
return query.PersonalAccessTokenColumnUserID
case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_ORGANIZATION_ID:
return query.PersonalAccessTokenColumnResourceOwner
case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_CREATED_DATE:
return query.PersonalAccessTokenColumnCreationDate
case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_EXPIRATION_DATE:
return query.PersonalAccessTokenColumnExpiration
default:
return query.PersonalAccessTokenColumnCreationDate
}
}
+39
View File
@@ -0,0 +1,39 @@
package user
import (
"context"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) AddSecret(ctx context.Context, req *user.AddSecretRequest) (*user.AddSecretResponse, error) {
newSecret := &command.GenerateMachineSecret{
PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx),
}
details, err := s.command.GenerateMachineSecret(ctx, req.UserId, "", newSecret)
if err != nil {
return nil, err
}
return &user.AddSecretResponse{
CreationDate: timestamppb.New(details.EventDate),
ClientSecret: newSecret.ClientSecret,
}, nil
}
func (s *Server) RemoveSecret(ctx context.Context, req *user.RemoveSecretRequest) (*user.RemoveSecretResponse, error) {
details, err := s.command.RemoveMachineSecret(
ctx,
req.UserId,
"",
s.command.NewPermissionCheckUserWrite(ctx),
)
if err != nil {
return nil, err
}
return &user.RemoveSecretResponse{
DeletionDate: timestamppb.New(details.EventDate),
}, nil
}
+10 -6
View File
@@ -8,6 +8,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
@@ -18,12 +19,13 @@ var _ user.UserServiceServer = (*Server)(nil)
type Server struct {
user.UnimplementedUserServiceServer
command *command.Commands
query *query.Queries
userCodeAlg crypto.EncryptionAlgorithm
idpAlg crypto.EncryptionAlgorithm
idpCallback func(ctx context.Context) string
samlRootURL func(ctx context.Context, idpID string) string
systemDefaults systemdefaults.SystemDefaults
command *command.Commands
query *query.Queries
userCodeAlg crypto.EncryptionAlgorithm
idpAlg crypto.EncryptionAlgorithm
idpCallback func(ctx context.Context) string
samlRootURL func(ctx context.Context, idpID string) string
assetAPIPrefix func(context.Context) string
@@ -33,6 +35,7 @@ type Server struct {
type Config struct{}
func CreateServer(
systemDefaults systemdefaults.SystemDefaults,
command *command.Commands,
query *query.Queries,
userCodeAlg crypto.EncryptionAlgorithm,
@@ -43,6 +46,7 @@ func CreateServer(
checkPermission domain.PermissionCheck,
) *Server {
return &Server{
systemDefaults: systemDefaults,
command: command,
query: query,
userCodeAlg: userCodeAlg,
+24 -81
View File
@@ -11,6 +11,7 @@ import (
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
@@ -117,7 +118,7 @@ func genderToDomain(gender user.Gender) domain.Gender {
}
func (s *Server) UpdateHumanUser(ctx context.Context, req *user.UpdateHumanUserRequest) (_ *user.UpdateHumanUserResponse, err error) {
human, err := UpdateUserRequestToChangeHuman(req)
human, err := updateHumanUserRequestToChangeHuman(req)
if err != nil {
return nil, err
}
@@ -181,86 +182,6 @@ func ifNotNilPtr[v, p any](value *v, conv func(v) p) *p {
return &pVal
}
func UpdateUserRequestToChangeHuman(req *user.UpdateHumanUserRequest) (*command.ChangeHuman, error) {
email, err := SetHumanEmailToEmail(req.Email, req.GetUserId())
if err != nil {
return nil, err
}
return &command.ChangeHuman{
ID: req.GetUserId(),
Username: req.Username,
Profile: SetHumanProfileToProfile(req.Profile),
Email: email,
Phone: SetHumanPhoneToPhone(req.Phone),
Password: SetHumanPasswordToPassword(req.Password),
}, nil
}
func SetHumanProfileToProfile(profile *user.SetHumanProfile) *command.Profile {
if profile == nil {
return nil
}
var firstName *string
if profile.GivenName != "" {
firstName = &profile.GivenName
}
var lastName *string
if profile.FamilyName != "" {
lastName = &profile.FamilyName
}
return &command.Profile{
FirstName: firstName,
LastName: lastName,
NickName: profile.NickName,
DisplayName: profile.DisplayName,
PreferredLanguage: ifNotNilPtr(profile.PreferredLanguage, language.Make),
Gender: ifNotNilPtr(profile.Gender, genderToDomain),
}
}
func SetHumanEmailToEmail(email *user.SetHumanEmail, userID string) (*command.Email, error) {
if email == nil {
return nil, nil
}
var urlTemplate string
if email.GetSendCode() != nil && email.GetSendCode().UrlTemplate != nil {
urlTemplate = *email.GetSendCode().UrlTemplate
if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, userID, "code", "orgID"); err != nil {
return nil, err
}
}
return &command.Email{
Address: domain.EmailAddress(email.Email),
Verified: email.GetIsVerified(),
ReturnCode: email.GetReturnCode() != nil,
URLTemplate: urlTemplate,
}, nil
}
func SetHumanPhoneToPhone(phone *user.SetHumanPhone) *command.Phone {
if phone == nil {
return nil
}
return &command.Phone{
Number: domain.PhoneNumber(phone.GetPhone()),
Verified: phone.GetIsVerified(),
ReturnCode: phone.GetReturnCode() != nil,
}
}
func SetHumanPasswordToPassword(password *user.SetPassword) *command.Password {
if password == nil {
return nil
}
return &command.Password{
PasswordCode: password.GetVerificationCode(),
OldPassword: password.GetCurrentPassword(),
Password: password.GetPassword().GetPassword(),
EncodedPasswordHash: password.GetHashedPassword().GetHash(),
ChangeRequired: password.GetPassword().GetChangeRequired() || password.GetHashedPassword().GetChangeRequired(),
}
}
func (s *Server) DeleteUser(ctx context.Context, req *user.DeleteUserRequest) (_ *user.DeleteUserResponse, err error) {
memberships, grants, err := s.removeUserDependencies(ctx, req.GetUserId())
if err != nil {
@@ -482,3 +403,25 @@ func (s *Server) HumanMFAInitSkipped(ctx context.Context, req *user.HumanMFAInit
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) CreateUser(ctx context.Context, req *user.CreateUserRequest) (*user.CreateUserResponse, error) {
switch userType := req.GetUserType().(type) {
case *user.CreateUserRequest_Human_:
return s.createUserTypeHuman(ctx, userType.Human, req.OrganizationId, req.Username, req.UserId)
case *user.CreateUserRequest_Machine_:
return s.createUserTypeMachine(ctx, userType.Machine, req.OrganizationId, req.GetUsername(), req.GetUserId())
default:
return nil, zerrors.ThrowInternal(nil, "", "user type is not implemented")
}
}
func (s *Server) UpdateUser(ctx context.Context, req *user.UpdateUserRequest) (*user.UpdateUserResponse, error) {
switch userType := req.GetUserType().(type) {
case *user.UpdateUserRequest_Human_:
return s.updateUserTypeHuman(ctx, userType.Human, req.UserId, req.Username)
case *user.UpdateUserRequest_Machine_:
return s.updateUserTypeMachine(ctx, userType.Machine, req.UserId, req.Username)
default:
return nil, zerrors.ThrowUnimplemented(nil, "", "user type is not implemented")
}
}
-1
View File
@@ -203,7 +203,6 @@ func (h *UsersHandler) Delete(ctx context.Context, id string) error {
if err != nil {
return err
}
_, err = h.command.RemoveUserV2(ctx, id, authz.GetCtxData(ctx).OrgID, memberships, grants...)
return err
}
+1 -1
View File
@@ -22,7 +22,7 @@ func (c *Commands) AddInstanceMemberCommand(a *instance.Aggregate, userID string
return nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-4m0fS", "Errors.IAM.MemberInvalid")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
if exists, err := ExistsUser(ctx, filter, userID, ""); err != nil || !exists {
if exists, err := ExistsUser(ctx, filter, userID, "", false); err != nil || !exists {
return nil, zerrors.ThrowPreconditionFailed(err, "INSTA-GSXOn", "Errors.User.NotFound")
}
if isMember, err := IsInstanceMember(ctx, filter, a.ID, userID); err != nil || isMember {
+1 -1
View File
@@ -28,7 +28,7 @@ func (c *Commands) AddOrgMemberCommand(a *org.Aggregate, userID string, roles ..
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if exists, err := ExistsUser(ctx, filter, userID, ""); err != nil || !exists {
if exists, err := ExistsUser(ctx, filter, userID, "", false); err != nil || !exists {
return nil, zerrors.ThrowPreconditionFailed(err, "ORG-GoXOn", "Errors.User.NotFound")
}
if isMember, err := IsOrgMember(ctx, filter, a.ID, userID); err != nil || isMember {
+15 -9
View File
@@ -353,21 +353,27 @@ func (c *Commands) userWriteModelByID(ctx context.Context, userID, resourceOwner
return writeModel, nil
}
func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id, resourceOwner string) (exists bool, err error) {
func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id, resourceOwner string, machineOnly bool) (exists bool, err error) {
eventTypes := []eventstore.EventType{
user.MachineAddedEventType,
user.UserRemovedType,
}
if !machineOnly {
eventTypes = append(eventTypes,
user.HumanRegisteredType,
user.UserV1RegisteredType,
user.HumanAddedType,
user.UserV1AddedType,
)
}
events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(resourceOwner).
OrderAsc().
AddQuery().
AggregateTypes(user.AggregateType).
AggregateIDs(id).
EventTypes(
user.HumanRegisteredType,
user.UserV1RegisteredType,
user.HumanAddedType,
user.UserV1AddedType,
user.MachineAddedEventType,
user.UserRemovedType,
).Builder())
EventTypes(eventTypes...).
Builder())
if err != nil {
return false, err
}
+36 -12
View File
@@ -25,6 +25,7 @@ type Machine struct {
Name string
Description string
AccessTokenType domain.OIDCTokenType
PermissionCheck PermissionCheck
}
func (m *Machine) IsZero() bool {
@@ -33,8 +34,8 @@ func (m *Machine) IsZero() bool {
func AddMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validation {
return func() (_ preparation.CreateCommands, err error) {
if a.ResourceOwner == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-xiown2", "Errors.ResourceOwnerMissing")
if a.ResourceOwner == "" && machine.PermissionCheck == nil {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-xiown3", "Errors.ResourceOwnerMissing")
}
if a.ID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-p0p2mi", "Errors.User.UserIDMissing")
@@ -49,7 +50,7 @@ func AddMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validati
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter)
writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter, machine.PermissionCheck)
if err != nil {
return nil, err
}
@@ -67,7 +68,18 @@ func AddMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validati
}
}
func (c *Commands) AddMachine(ctx context.Context, machine *Machine) (_ *domain.ObjectDetails, err error) {
type addMachineOption func(context.Context, *Machine) error
func AddMachineWithUsernameToIDFallback() addMachineOption {
return func(ctx context.Context, m *Machine) error {
if m.Username == "" {
m.Username = m.AggregateID
}
return nil
}
}
func (c *Commands) AddMachine(ctx context.Context, machine *Machine, check PermissionCheck, options ...addMachineOption) (_ *domain.ObjectDetails, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -80,6 +92,16 @@ func (c *Commands) AddMachine(ctx context.Context, machine *Machine) (_ *domain.
}
agg := user.NewAggregate(machine.AggregateID, machine.ResourceOwner)
for _, option := range options {
if err = option(ctx, machine); err != nil {
return nil, err
}
}
if check != nil {
if err = check(machine.ResourceOwner, machine.AggregateID); err != nil {
return nil, err
}
}
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, AddMachineCommand(agg, machine))
if err != nil {
return nil, err
@@ -97,6 +119,7 @@ func (c *Commands) AddMachine(ctx context.Context, machine *Machine) (_ *domain.
}, nil
}
// Deprecated: use ChangeUserMachine instead
func (c *Commands) ChangeMachine(ctx context.Context, machine *Machine) (*domain.ObjectDetails, error) {
agg := user.NewAggregate(machine.AggregateID, machine.ResourceOwner)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, changeMachineCommand(agg, machine))
@@ -118,24 +141,21 @@ func (c *Commands) ChangeMachine(ctx context.Context, machine *Machine) (*domain
func changeMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validation {
return func() (_ preparation.CreateCommands, err error) {
if a.ResourceOwner == "" {
if a.ResourceOwner == "" && machine.PermissionCheck == nil {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-xiown3", "Errors.ResourceOwnerMissing")
}
if a.ID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-p0p3mi", "Errors.User.UserIDMissing")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter)
writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter, machine.PermissionCheck)
if err != nil {
return nil, err
}
if !isUserStateExists(writeModel.UserState) {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-5M0od", "Errors.User.NotFound")
}
changedEvent, hasChanged, err := writeModel.NewChangedEvent(ctx, &a.Aggregate, machine.Name, machine.Description, machine.AccessTokenType)
if err != nil {
return nil, err
}
changedEvent, hasChanged := writeModel.NewChangedEvent(ctx, &a.Aggregate, machine.Name, machine.Description, machine.AccessTokenType)
if !hasChanged {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-2n8vs", "Errors.User.NotChanged")
}
@@ -147,10 +167,9 @@ func changeMachineCommand(a *user.Aggregate, machine *Machine) preparation.Valid
}
}
func getMachineWriteModel(ctx context.Context, userID, resourceOwner string, filter preparation.FilterToQueryReducer) (_ *MachineWriteModel, err error) {
func getMachineWriteModel(ctx context.Context, userID, resourceOwner string, filter preparation.FilterToQueryReducer, permissionCheck PermissionCheck) (_ *MachineWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel := NewMachineWriteModel(userID, resourceOwner)
events, err := filter(ctx, writeModel.Query())
if err != nil {
@@ -161,5 +180,10 @@ func getMachineWriteModel(ctx context.Context, userID, resourceOwner string, fil
}
writeModel.AppendEvents(events...)
err = writeModel.Reduce()
if permissionCheck != nil {
if err := permissionCheck(writeModel.ResourceOwner, writeModel.AggregateID); err != nil {
return nil, err
}
}
return writeModel, err
}
+14 -10
View File
@@ -15,12 +15,14 @@ import (
)
type AddMachineKey struct {
Type domain.AuthNKeyType
ExpirationDate time.Time
Type domain.AuthNKeyType
ExpirationDate time.Time
PermissionCheck PermissionCheck
}
type MachineKey struct {
models.ObjectRoot
PermissionCheck PermissionCheck
KeyID string
Type domain.AuthNKeyType
@@ -64,7 +66,7 @@ func (key *MachineKey) Detail() ([]byte, error) {
}
func (key *MachineKey) content() error {
if key.ResourceOwner == "" {
if key.PermissionCheck == nil && key.ResourceOwner == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-kqpoix", "Errors.ResourceOwnerMissing")
}
if key.AggregateID == "" {
@@ -91,7 +93,7 @@ func (key *MachineKey) valid() (err error) {
}
func (key *MachineKey) checkAggregate(ctx context.Context, filter preparation.FilterToQueryReducer) error {
if exists, err := ExistsUser(ctx, filter, key.AggregateID, key.ResourceOwner); err != nil || !exists {
if exists, err := ExistsUser(ctx, filter, key.AggregateID, key.ResourceOwner, true); err != nil || !exists {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-bnipwm1", "Errors.User.NotFound")
}
return nil
@@ -142,7 +144,7 @@ func prepareAddUserMachineKey(machineKey *MachineKey, keySize int) preparation.V
return nil, err
}
}
writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner)
writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner, machineKey.PermissionCheck)
if err != nil {
return nil, err
}
@@ -186,7 +188,7 @@ func prepareRemoveUserMachineKey(machineKey *MachineKey) preparation.Validation
return nil, err
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner)
writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner, machineKey.PermissionCheck)
if err != nil {
return nil, err
}
@@ -204,16 +206,18 @@ func prepareRemoveUserMachineKey(machineKey *MachineKey) preparation.Validation
}
}
func getMachineKeyWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, keyID, resourceOwner string) (_ *MachineKeyWriteModel, err error) {
func getMachineKeyWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, keyID, resourceOwner string, permissionCheck PermissionCheck) (_ *MachineKeyWriteModel, err error) {
writeModel := NewMachineKeyWriteModel(userID, keyID, resourceOwner)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
if len(events) == 0 {
return writeModel, nil
}
writeModel.AppendEvents(events...)
err = writeModel.Reduce()
if permissionCheck != nil {
if err := permissionCheck(writeModel.ResourceOwner, writeModel.AggregateID); err != nil {
return nil, err
}
}
return writeModel, err
}
+4 -9
View File
@@ -2,7 +2,6 @@ package command
import (
"context"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
@@ -106,9 +105,8 @@ func (wm *MachineWriteModel) NewChangedEvent(
name,
description string,
accessTokenType domain.OIDCTokenType,
) (*user.MachineChangedEvent, bool, error) {
) (*user.MachineChangedEvent, bool) {
changes := make([]user.MachineChanges, 0)
var err error
if wm.Name != name {
changes = append(changes, user.ChangeName(name))
@@ -120,11 +118,8 @@ func (wm *MachineWriteModel) NewChangedEvent(
changes = append(changes, user.ChangeAccessTokenType(accessTokenType))
}
if len(changes) == 0 {
return nil, false, nil
return nil, false
}
changeEvent, err := user.NewMachineChangedEvent(ctx, aggregate, changes)
if err != nil {
return nil, false, err
}
return changeEvent, true, nil
changeEvent := user.NewMachineChangedEvent(ctx, aggregate, changes)
return changeEvent, true
}
+12 -10
View File
@@ -11,7 +11,8 @@ import (
)
type GenerateMachineSecret struct {
ClientSecret string
PermissionCheck PermissionCheck
ClientSecret string
}
func (c *Commands) GenerateMachineSecret(ctx context.Context, userID string, resourceOwner string, set *GenerateMachineSecret) (*domain.ObjectDetails, error) {
@@ -35,14 +36,14 @@ func (c *Commands) GenerateMachineSecret(ctx context.Context, userID string, res
func (c *Commands) prepareGenerateMachineSecret(a *user.Aggregate, set *GenerateMachineSecret) preparation.Validation {
return func() (_ preparation.CreateCommands, err error) {
if a.ResourceOwner == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-x0992n", "Errors.ResourceOwnerMissing")
if a.ResourceOwner == "" && set.PermissionCheck == nil {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-0qp2hus", "Errors.ResourceOwnerMissing")
}
if a.ID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-bzoqjs", "Errors.User.UserIDMissing")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter)
writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter, set.PermissionCheck)
if err != nil {
return nil, err
}
@@ -62,9 +63,10 @@ func (c *Commands) prepareGenerateMachineSecret(a *user.Aggregate, set *Generate
}
}
func (c *Commands) RemoveMachineSecret(ctx context.Context, userID string, resourceOwner string) (*domain.ObjectDetails, error) {
func (c *Commands) RemoveMachineSecret(ctx context.Context, userID string, resourceOwner string, permissionCheck PermissionCheck) (*domain.ObjectDetails, error) {
agg := user.NewAggregate(userID, resourceOwner)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareRemoveMachineSecret(agg))
//nolint:staticcheck
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareRemoveMachineSecret(agg, permissionCheck))
if err != nil {
return nil, err
}
@@ -81,16 +83,16 @@ func (c *Commands) RemoveMachineSecret(ctx context.Context, userID string, resou
}, nil
}
func prepareRemoveMachineSecret(a *user.Aggregate) preparation.Validation {
func prepareRemoveMachineSecret(a *user.Aggregate, check PermissionCheck) preparation.Validation {
return func() (_ preparation.CreateCommands, err error) {
if a.ResourceOwner == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-0qp2hus", "Errors.ResourceOwnerMissing")
if a.ResourceOwner == "" && check == nil {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-x0992n", "Errors.ResourceOwnerMissing")
}
if a.ID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-bzosjs", "Errors.User.UserIDMissing")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter)
writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter, check)
if err != nil {
return nil, err
}
+4 -4
View File
@@ -44,7 +44,7 @@ func TestCommandSide_GenerateMachineSecret(t *testing.T) {
ctx: context.Background(),
userID: "",
resourceOwner: "org1",
set: nil,
set: new(GenerateMachineSecret),
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -59,7 +59,7 @@ func TestCommandSide_GenerateMachineSecret(t *testing.T) {
ctx: context.Background(),
userID: "user1",
resourceOwner: "",
set: nil,
set: new(GenerateMachineSecret),
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -76,7 +76,7 @@ func TestCommandSide_GenerateMachineSecret(t *testing.T) {
ctx: context.Background(),
userID: "user1",
resourceOwner: "org1",
set: nil,
set: new(GenerateMachineSecret),
},
res: res{
err: zerrors.IsPreconditionFailed,
@@ -289,7 +289,7 @@ func TestCommandSide_RemoveMachineSecret(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.RemoveMachineSecret(tt.args.ctx, tt.args.userID, tt.args.resourceOwner)
got, err := r.RemoveMachineSecret(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, nil)
if tt.res.err == nil {
assert.NoError(t, err)
}
+234 -4
View File
@@ -24,6 +24,8 @@ func TestCommandSide_AddMachine(t *testing.T) {
type args struct {
ctx context.Context
machine *Machine
check PermissionCheck
options func(*Commands) []addMachineOption
}
type res struct {
want *domain.ObjectDetails
@@ -194,14 +196,242 @@ func TestCommandSide_AddMachine(t *testing.T) {
},
},
},
{
name: "with username fallback to given username",
fields: fields{
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "aggregateID"),
eventstore: eventstoreExpect(
t,
expectFilter(),
expectFilter(
eventFromEventPusher(
org.NewDomainPolicyAddedEvent(context.Background(),
&user.NewAggregate("aggregateID", "org1").Aggregate,
true,
true,
true,
),
),
),
expectPush(
user.NewMachineAddedEvent(context.Background(),
&user.NewAggregate("aggregateID", "org1").Aggregate,
"username",
"name",
"",
true,
domain.OIDCTokenTypeBearer,
),
),
),
},
args: args{
ctx: context.Background(),
machine: &Machine{
ObjectRoot: models.ObjectRoot{
ResourceOwner: "org1",
},
Name: "name",
Username: "username",
},
options: func(commands *Commands) []addMachineOption {
return []addMachineOption{
AddMachineWithUsernameToIDFallback(),
}
},
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "with username fallback to generated id",
fields: fields{
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "aggregateID"),
eventstore: eventstoreExpect(
t,
expectFilter(),
expectFilter(
eventFromEventPusher(
org.NewDomainPolicyAddedEvent(context.Background(),
&user.NewAggregate("aggregateID", "org1").Aggregate,
true,
true,
true,
),
),
),
expectPush(
user.NewMachineAddedEvent(context.Background(),
&user.NewAggregate("aggregateID", "org1").Aggregate,
"aggregateID",
"name",
"",
true,
domain.OIDCTokenTypeBearer,
),
),
),
},
args: args{
ctx: context.Background(),
machine: &Machine{
ObjectRoot: models.ObjectRoot{
ResourceOwner: "org1",
},
Name: "name",
},
options: func(commands *Commands) []addMachineOption {
return []addMachineOption{
AddMachineWithUsernameToIDFallback(),
}
},
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "with username fallback to given id",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectFilter(
eventFromEventPusher(
org.NewDomainPolicyAddedEvent(context.Background(),
&user.NewAggregate("aggregateID", "org1").Aggregate,
true,
true,
true,
),
),
),
expectPush(
user.NewMachineAddedEvent(context.Background(),
&user.NewAggregate("aggregateID", "org1").Aggregate,
"aggregateID",
"name",
"",
true,
domain.OIDCTokenTypeBearer,
),
),
),
},
args: args{
ctx: context.Background(),
machine: &Machine{
ObjectRoot: models.ObjectRoot{
ResourceOwner: "org1",
AggregateID: "aggregateID",
},
Name: "name",
},
options: func(commands *Commands) []addMachineOption {
return []addMachineOption{
AddMachineWithUsernameToIDFallback(),
}
},
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "with succeeding permission check, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectFilter(
eventFromEventPusher(
org.NewDomainPolicyAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
true,
true,
true,
),
),
),
expectPush(
user.NewMachineAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"name",
"description",
true,
domain.OIDCTokenTypeBearer,
),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
},
args: args{
ctx: context.Background(),
machine: &Machine{
ObjectRoot: models.ObjectRoot{
ResourceOwner: "org1",
},
Description: "description",
Name: "name",
Username: "username",
},
check: func(resourceOwner, aggregateID string) error {
return nil
},
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "with failing permission check, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
},
args: args{
ctx: context.Background(),
machine: &Machine{
ObjectRoot: models.ObjectRoot{
ResourceOwner: "org1",
},
Description: "description",
Name: "name",
Username: "username",
},
check: func(resourceOwner, aggregateID string) error {
return zerrors.ThrowPermissionDenied(nil, "", "")
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
idGenerator: tt.fields.idGenerator,
eventstore: tt.fields.eventstore,
idGenerator: tt.fields.idGenerator,
checkPermission: newMockPermissionCheckAllowed(),
}
got, err := r.AddMachine(tt.args.ctx, tt.args.machine)
var options []addMachineOption
if tt.args.options != nil {
options = tt.args.options(r)
}
got, err := r.AddMachine(tt.args.ctx, tt.args.machine, tt.args.check, options...)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -391,7 +621,7 @@ func TestCommandSide_ChangeMachine(t *testing.T) {
}
func newMachineChangedEvent(ctx context.Context, userID, resourceOwner, name, description string) *user.MachineChangedEvent {
event, _ := user.NewMachineChangedEvent(ctx,
event := user.NewMachineChangedEvent(ctx,
&user.NewAggregate(userID, resourceOwner).Aggregate,
[]user.MachineChanges{
user.ChangeName(name),
+11 -9
View File
@@ -21,6 +21,7 @@ type AddPat struct {
type PersonalAccessToken struct {
models.ObjectRoot
PermissionCheck PermissionCheck
ExpirationDate time.Time
Scopes []string
@@ -43,7 +44,7 @@ func NewPersonalAccessToken(resourceOwner string, userID string, expirationDate
}
func (pat *PersonalAccessToken) content() error {
if pat.ResourceOwner == "" {
if pat.ResourceOwner == "" && pat.PermissionCheck == nil {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-xs0k2n", "Errors.ResourceOwnerMissing")
}
if pat.AggregateID == "" {
@@ -109,11 +110,10 @@ func prepareAddPersonalAccessToken(pat *PersonalAccessToken, algorithm crypto.En
if err := pat.checkAggregate(ctx, filter); err != nil {
return nil, err
}
writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner)
writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner, pat.PermissionCheck)
if err != nil {
return nil, err
}
pat.Token, err = createToken(algorithm, writeModel.TokenID, writeModel.AggregateID)
if err != nil {
return nil, err
@@ -155,7 +155,7 @@ func prepareRemovePersonalAccessToken(pat *PersonalAccessToken) preparation.Vali
return nil, err
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) (_ []eventstore.Command, err error) {
writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner)
writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner, pat.PermissionCheck)
if err != nil {
return nil, err
}
@@ -181,16 +181,18 @@ func createToken(algorithm crypto.EncryptionAlgorithm, tokenID, userID string) (
return base64.RawURLEncoding.EncodeToString(encrypted), nil
}
func getPersonalAccessTokenWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, tokenID, resourceOwner string) (_ *PersonalAccessTokenWriteModel, err error) {
func getPersonalAccessTokenWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, tokenID, resourceOwner string, check PermissionCheck) (_ *PersonalAccessTokenWriteModel, err error) {
writeModel := NewPersonalAccessTokenWriteModel(userID, tokenID, resourceOwner)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
if len(events) == 0 {
return writeModel, nil
}
writeModel.AppendEvents(events...)
err = writeModel.Reduce()
if err = writeModel.Reduce(); err != nil {
return nil, err
}
if check != nil {
err = check(writeModel.ResourceOwner, writeModel.AggregateID)
}
return writeModel, err
}
+1 -1
View File
@@ -1813,7 +1813,7 @@ func TestExistsUser(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotExists, err := ExistsUser(context.Background(), tt.args.filter, tt.args.id, tt.args.resourceOwner)
gotExists, err := ExistsUser(context.Background(), tt.args.filter, tt.args.id, tt.args.resourceOwner, false)
if (err != nil) != tt.wantErr {
t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr)
return
-2
View File
@@ -132,7 +132,6 @@ func (c *Commands) RemoveUserV2(ctx context.Context, userID, resourceOwner strin
if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-vaipl7s13l", "Errors.User.UserIDMissing")
}
existingUser, err := c.userRemoveWriteModel(ctx, userID, resourceOwner)
if err != nil {
return nil, err
@@ -143,7 +142,6 @@ func (c *Commands) RemoveUserV2(ctx context.Context, userID, resourceOwner strin
if err := c.checkPermissionDeleteUser(ctx, existingUser.ResourceOwner, existingUser.AggregateID); err != nil {
return nil, err
}
domainPolicy, err := c.domainPolicyWriteModel(ctx, existingUser.ResourceOwner)
if err != nil {
return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-l40ykb3xh2", "Errors.Org.DomainPolicy.NotExisting")
+36 -28
View File
@@ -5,6 +5,7 @@ import (
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
@@ -121,7 +122,10 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human
if resourceOwner == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMA-095xh8fll1", "Errors.Internal")
}
if human.Details == nil {
human.Details = &domain.ObjectDetails{}
}
human.Details.ResourceOwner = resourceOwner
if err := human.Validate(c.userPasswordHasher); err != nil {
return err
}
@@ -132,7 +136,12 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human
return err
}
}
// check for permission to create user on resourceOwner
if !human.Register {
if err := c.checkPermissionUpdateUser(ctx, resourceOwner, human.ID); err != nil {
return err
}
}
// only check if user is already existing
existingHuman, err := c.userExistsWriteModel(
ctx,
@@ -144,12 +153,6 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human
if isUserStateExists(existingHuman.UserState) {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-7yiox1isql", "Errors.User.AlreadyExisting")
}
// check for permission to create user on resourceOwner
if !human.Register {
if err := c.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, human.ID); err != nil {
return err
}
}
// add resourceowner for the events with the aggregate
existingHuman.ResourceOwner = resourceOwner
@@ -161,6 +164,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human
if err = c.userValidateDomain(ctx, resourceOwner, human.Username, domainPolicy.UserLoginMustBeDomain); err != nil {
return err
}
var createCmd humanCreationCommand
if human.Register {
createCmd = user.NewHumanRegisteredEvent(
@@ -203,17 +207,33 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human
return err
}
cmds := make([]eventstore.Command, 0, 3)
cmds = append(cmds, createCmd)
cmds, err = c.addHumanCommandEmail(ctx, filter, cmds, existingHuman.Aggregate(), human, alg, allowInitMail)
cmds, err := c.addUserHumanCommands(ctx, filter, existingHuman, human, allowInitMail, alg, createCmd)
if err != nil {
return err
}
if len(cmds) == 0 {
human.Details = writeModelToObjectDetails(&existingHuman.WriteModel)
return nil
}
err = c.pushAppendAndReduce(ctx, existingHuman, cmds...)
if err != nil {
return err
}
human.Details = writeModelToObjectDetails(&existingHuman.WriteModel)
return nil
}
func (c *Commands) addUserHumanCommands(ctx context.Context, filter preparation.FilterToQueryReducer, existingHuman *UserV2WriteModel, human *AddHuman, allowInitMail bool, alg crypto.EncryptionAlgorithm, addUserCommand eventstore.Command) ([]eventstore.Command, error) {
cmds := []eventstore.Command{addUserCommand}
var err error
cmds, err = c.addHumanCommandEmail(ctx, filter, cmds, existingHuman.Aggregate(), human, alg, allowInitMail)
if err != nil {
return nil, err
}
cmds, err = c.addHumanCommandPhone(ctx, filter, cmds, existingHuman.Aggregate(), human, alg)
if err != nil {
return err
return nil, err
}
for _, metadataEntry := range human.Metadata {
@@ -227,7 +247,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human
for _, link := range human.Links {
cmd, err := addLink(ctx, filter, existingHuman.Aggregate(), link)
if err != nil {
return err
return nil, err
}
cmds = append(cmds, cmd)
}
@@ -235,7 +255,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human
if human.TOTPSecret != "" {
encryptedSecret, err := crypto.Encrypt([]byte(human.TOTPSecret), c.multifactors.OTP.CryptoMFA)
if err != nil {
return err
return nil, err
}
cmds = append(cmds,
user.NewHumanOTPAddedEvent(ctx, &existingHuman.Aggregate().Aggregate, encryptedSecret),
@@ -246,18 +266,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human
if human.SetInactive {
cmds = append(cmds, user.NewUserDeactivatedEvent(ctx, &existingHuman.Aggregate().Aggregate))
}
if len(cmds) == 0 {
human.Details = writeModelToObjectDetails(&existingHuman.WriteModel)
return nil
}
err = c.pushAppendAndReduce(ctx, existingHuman, cmds...)
if err != nil {
return err
}
human.Details = writeModelToObjectDetails(&existingHuman.WriteModel)
return nil
return cmds, nil
}
func (c *Commands) ChangeUserHuman(ctx context.Context, human *ChangeHuman, alg crypto.EncryptionAlgorithm) (err error) {
@@ -341,7 +350,6 @@ func (c *Commands) ChangeUserHuman(ctx context.Context, human *ChangeHuman, alg
if human.State != nil {
// only allow toggling between active and inactive
// any other target state is not supported
// the existing human's state has to be the
switch {
case isUserStateActive(*human.State):
if isUserStateActive(existingHuman.UserState) {
+2 -6
View File
@@ -302,9 +302,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
{
name: "add human (with initial code), no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckNotAllowed(),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
newCode: mockEncryptedCode("userinit", time.Hour),
@@ -326,9 +324,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
res: res{
err: func(err error) bool {
return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"))
},
err: zerrors.IsPermissionDenied,
},
},
{
+1
View File
@@ -352,6 +352,7 @@ func TestCommands_ResendInviteCode(t *testing.T) {
"user does not exist",
fields{
eventstore: expectEventstore(
// The write model doesn't query any events
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
+94
View File
@@ -0,0 +1,94 @@
package command
import (
"context"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
type ChangeMachine struct {
ID string
ResourceOwner string
Username *string
Name *string
Description *string
// Details are set after a successful execution of the command
Details *domain.ObjectDetails
}
func (h *ChangeMachine) Changed() bool {
if h.Username != nil {
return true
}
if h.Name != nil {
return true
}
if h.Description != nil {
return true
}
return false
}
func (c *Commands) ChangeUserMachine(ctx context.Context, machine *ChangeMachine) (err error) {
existingMachine, err := c.UserMachineWriteModel(
ctx,
machine.ID,
machine.ResourceOwner,
false,
)
if err != nil {
return err
}
if machine.Changed() {
if err := c.checkPermissionUpdateUser(ctx, existingMachine.ResourceOwner, existingMachine.AggregateID); err != nil {
return err
}
}
cmds := make([]eventstore.Command, 0)
if machine.Username != nil {
cmds, err = c.changeUsername(ctx, cmds, existingMachine, *machine.Username)
if err != nil {
return err
}
}
var machineChanges []user.MachineChanges
if machine.Name != nil && *machine.Name != existingMachine.Name {
machineChanges = append(machineChanges, user.ChangeName(*machine.Name))
}
if machine.Description != nil && *machine.Description != existingMachine.Description {
machineChanges = append(machineChanges, user.ChangeDescription(*machine.Description))
}
if len(machineChanges) > 0 {
cmds = append(cmds, user.NewMachineChangedEvent(ctx, &existingMachine.Aggregate().Aggregate, machineChanges))
}
if len(cmds) == 0 {
machine.Details = writeModelToObjectDetails(&existingMachine.WriteModel)
return nil
}
err = c.pushAppendAndReduce(ctx, existingMachine, cmds...)
if err != nil {
return err
}
machine.Details = writeModelToObjectDetails(&existingMachine.WriteModel)
return nil
}
func (c *Commands) UserMachineWriteModel(ctx context.Context, userID, resourceOwner string, metadataWM bool) (writeModel *UserV2WriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel = NewUserMachineWriteModel(userID, resourceOwner, metadataWM)
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
if !isUserStateExists(writeModel.UserState) {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-ugjs0upun6", "Errors.User.NotFound")
}
return writeModel, nil
}
+260
View File
@@ -0,0 +1,260 @@
package command
import (
"context"
"errors"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestCommandSide_ChangeUserMachine(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
orgID string
machine *ChangeMachine
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
userAgg := user.NewAggregate("user1", "org1")
userAddedEvent := user.NewMachineAddedEvent(context.Background(),
&userAgg.Aggregate,
"username",
"name",
"description",
true,
domain.OIDCTokenTypeBearer,
)
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "change machine username, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(userAddedEvent),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
machine: &ChangeMachine{
Username: gu.Ptr("changed"),
},
},
res: res{
err: func(err error) bool {
return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"))
},
},
},
{
name: "change machine username, not found",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
machine: &ChangeMachine{
Username: gu.Ptr("changed"),
},
},
res: res{
err: func(err error) bool {
return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-ugjs0upun6", "Errors.User.NotFound"))
},
},
},
{
name: "change machine username, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(userAddedEvent),
),
expectFilter(
eventFromEventPusher(
org.NewDomainPolicyAddedEvent(context.Background(),
&userAgg.Aggregate,
true,
true,
true,
),
),
),
expectPush(
user.NewUsernameChangedEvent(context.Background(),
&userAgg.Aggregate,
"username",
"changed",
true,
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
machine: &ChangeMachine{
Username: gu.Ptr("changed"),
},
},
res: res{
want: &domain.ObjectDetails{
Sequence: 0,
EventDate: time.Time{},
ResourceOwner: "org1",
},
},
},
{
name: "change machine username, no change",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(userAddedEvent),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
machine: &ChangeMachine{
Username: gu.Ptr("username"),
},
},
res: res{
want: &domain.ObjectDetails{
Sequence: 0,
EventDate: time.Time{},
ResourceOwner: "org1",
},
},
},
{
name: "change machine description, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(userAddedEvent),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
machine: &ChangeMachine{
Description: gu.Ptr("changed"),
},
},
res: res{
err: func(err error) bool {
return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"))
},
},
},
{
name: "change machine description, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(userAddedEvent),
),
expectPush(
user.NewMachineChangedEvent(context.Background(),
&userAgg.Aggregate,
[]user.MachineChanges{
user.ChangeDescription("changed"),
},
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
machine: &ChangeMachine{
Description: gu.Ptr("changed"),
},
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "change machine description, no change",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(userAddedEvent),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
machine: &ChangeMachine{
Description: gu.Ptr("description"),
},
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
err := r.ChangeUserMachine(tt.args.ctx, tt.args.machine)
if tt.res.err == nil {
if !assert.NoError(t, err) {
t.FailNow()
}
} else if !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
return
}
if tt.res.err == nil {
assertObjectDetails(t, tt.res.want, tt.args.machine.Details)
}
})
}
}
+8
View File
@@ -118,6 +118,14 @@ func NewUserHumanWriteModel(userID, resourceOwner string, profileWM, emailWM, ph
return newUserV2WriteModel(userID, resourceOwner, opts...)
}
func NewUserMachineWriteModel(userID, resourceOwner string, metadataListWM bool) *UserV2WriteModel {
opts := []UserV2WMOption{WithMachine(), WithState()}
if metadataListWM {
opts = append(opts, WithMetadata())
}
return newUserV2WriteModel(userID, resourceOwner, opts...)
}
func newUserV2WriteModel(userID, resourceOwner string, opts ...UserV2WMOption) *UserV2WriteModel {
wm := &UserV2WriteModel{
WriteModel: eventstore.WriteModel{
+1 -1
View File
@@ -24,7 +24,7 @@ func (p *Permissions) appendPermission(ctxID, permission string) {
p.Permissions = append(p.Permissions, permission)
}
type PermissionCheck func(ctx context.Context, permission, orgID, resourceID string) (err error)
type PermissionCheck func(ctx context.Context, permission, resourceOwnerID, aggregateID string) (err error)
const (
PermissionUserWrite = "user.write"
+16 -10
View File
@@ -1,6 +1,8 @@
package eventstore
import "time"
import (
"time"
)
// WriteModel is the minimum representation of a command side write model.
// It implements a basic reducer
@@ -27,21 +29,25 @@ func (wm *WriteModel) Reduce() error {
return nil
}
latestEvent := wm.Events[len(wm.Events)-1]
if wm.AggregateID == "" {
wm.AggregateID = wm.Events[0].Aggregate().ID
}
if wm.ResourceOwner == "" {
wm.ResourceOwner = wm.Events[0].Aggregate().ResourceOwner
}
if wm.InstanceID == "" {
wm.InstanceID = wm.Events[0].Aggregate().InstanceID
wm.AggregateID = latestEvent.Aggregate().ID
}
wm.ProcessedSequence = wm.Events[len(wm.Events)-1].Sequence()
wm.ChangeDate = wm.Events[len(wm.Events)-1].CreatedAt()
if wm.ResourceOwner == "" {
wm.ResourceOwner = latestEvent.Aggregate().ResourceOwner
}
if wm.InstanceID == "" {
wm.InstanceID = latestEvent.Aggregate().InstanceID
}
wm.ProcessedSequence = latestEvent.Sequence()
wm.ChangeDate = latestEvent.CreatedAt()
// all events processed and not needed anymore
wm.Events = nil
wm.Events = []Event{}
return nil
}
+40
View File
@@ -141,6 +141,7 @@ func (c *Client) pollHealth(ctx context.Context) (err error) {
}
}
// Deprecated: use CreateUserTypeHuman instead
func (i *Instance) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserResponse {
resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{
Organization: &object.Organization{
@@ -172,6 +173,7 @@ func (i *Instance) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserRes
return resp
}
// Deprecated: user CreateUserTypeHuman instead
func (i *Instance) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHumanUserResponse {
resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{
Organization: &object.Organization{
@@ -197,6 +199,7 @@ func (i *Instance) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHuman
return resp
}
// Deprecated: user CreateUserTypeHuman instead
func (i *Instance) CreateHumanUserWithTOTP(ctx context.Context, secret string) *user_v2.AddHumanUserResponse {
resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{
Organization: &object.Organization{
@@ -229,6 +232,43 @@ func (i *Instance) CreateHumanUserWithTOTP(ctx context.Context, secret string) *
return resp
}
func (i *Instance) CreateUserTypeHuman(ctx context.Context) *user_v2.CreateUserResponse {
resp, err := i.Client.UserV2.CreateUser(ctx, &user_v2.CreateUserRequest{
OrganizationId: i.DefaultOrg.GetId(),
UserType: &user_v2.CreateUserRequest_Human_{
Human: &user_v2.CreateUserRequest_Human{
Profile: &user_v2.SetHumanProfile{
GivenName: "Mickey",
FamilyName: "Mouse",
},
Email: &user_v2.SetHumanEmail{
Email: fmt.Sprintf("%[email protected]", time.Now().UnixNano()),
Verification: &user_v2.SetHumanEmail_ReturnCode{
ReturnCode: &user_v2.ReturnEmailVerificationCode{},
},
},
},
},
})
logging.OnError(err).Panic("create human user")
i.TriggerUserByID(ctx, resp.GetId())
return resp
}
func (i *Instance) CreateUserTypeMachine(ctx context.Context) *user_v2.CreateUserResponse {
resp, err := i.Client.UserV2.CreateUser(ctx, &user_v2.CreateUserRequest{
OrganizationId: i.DefaultOrg.GetId(),
UserType: &user_v2.CreateUserRequest_Machine_{
Machine: &user_v2.CreateUserRequest_Machine{
Name: "machine",
},
},
})
logging.OnError(err).Panic("create machine user")
i.TriggerUserByID(ctx, resp.GetId())
return resp
}
// TriggerUserByID makes sure the user projection gets triggered after creation.
func (i *Instance) TriggerUserByID(ctx context.Context, users ...string) {
var wg sync.WaitGroup
+105 -41
View File
@@ -5,6 +5,7 @@ import (
"database/sql"
_ "embed"
"errors"
"slices"
"time"
sq "github.com/Masterminds/squirrel"
@@ -18,6 +19,14 @@ import (
"github.com/zitadel/zitadel/internal/zerrors"
)
func keysCheckPermission(ctx context.Context, keys *AuthNKeys, permissionCheck domain.PermissionCheck) {
keys.AuthNKeys = slices.DeleteFunc(keys.AuthNKeys,
func(key *AuthNKey) bool {
return userCheckPermission(ctx, key.ResourceOwner, key.AggregateID, permissionCheck) != nil
},
)
}
var (
authNKeyTable = table{
name: projection.AuthNKeyTable,
@@ -84,6 +93,7 @@ type AuthNKeys struct {
type AuthNKey struct {
ID string
AggregateID string
CreationDate time.Time
ChangeDate time.Time
ResourceOwner string
@@ -124,12 +134,47 @@ func (q *AuthNKeySearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder
return query
}
func (q *Queries) SearchAuthNKeys(ctx context.Context, queries *AuthNKeySearchQueries, withOwnerRemoved bool) (authNKeys *AuthNKeys, err error) {
type JoinFilter int
const (
JoinFilterUnspecified JoinFilter = iota
JoinFilterApp
JoinFilterUserMachine
)
// SearchAuthNKeys returns machine or app keys, depending on the join filter.
// If permissionCheck is nil, the keys are not filtered.
// If permissionCheck is not nil and the PermissionCheckV2 feature flag is false, the returned keys are filtered in-memory by the given permission check.
// If permissionCheck is not nil and the PermissionCheckV2 feature flag is true, the returned keys are filtered in the database.
func (q *Queries) SearchAuthNKeys(ctx context.Context, queries *AuthNKeySearchQueries, filter JoinFilter, permissionCheck domain.PermissionCheck) (authNKeys *AuthNKeys, err error) {
permissionCheckV2 := PermissionV2(ctx, permissionCheck)
keys, err := q.searchAuthNKeys(ctx, queries, filter, permissionCheckV2)
if err != nil {
return nil, err
}
if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 {
keysCheckPermission(ctx, keys, permissionCheck)
}
return keys, nil
}
func (q *Queries) searchAuthNKeys(ctx context.Context, queries *AuthNKeySearchQueries, joinFilter JoinFilter, permissionCheckV2 bool) (authNKeys *AuthNKeys, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
query, scan := prepareAuthNKeysQuery()
query = queries.toQuery(query)
switch joinFilter {
case JoinFilterUnspecified:
// Select all authN keys
case JoinFilterApp:
joinCol := ProjectColumnID
query = query.Join(joinCol.table.identifier() + " ON " + AuthNKeyColumnIdentifier.identifier() + " = " + joinCol.identifier())
case JoinFilterUserMachine:
joinCol := MachineUserIDCol
query = query.Join(joinCol.table.identifier() + " ON " + AuthNKeyColumnIdentifier.identifier() + " = " + joinCol.identifier())
query = userPermissionCheckV2WithCustomColumns(ctx, query, permissionCheckV2, queries.Queries, AuthNKeyColumnResourceOwner, AuthNKeyColumnIdentifier)
}
eq := sq.Eq{
AuthNKeyColumnEnabled.identifier(): true,
AuthNKeyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
@@ -249,6 +294,22 @@ func NewAuthNKeyObjectIDQuery(id string) (SearchQuery, error) {
return NewTextQuery(AuthNKeyColumnObjectID, id, TextEquals)
}
func NewAuthNKeyIDQuery(id string) (SearchQuery, error) {
return NewTextQuery(AuthNKeyColumnID, id, TextEquals)
}
func NewAuthNKeyIdentifyerQuery(id string) (SearchQuery, error) {
return NewTextQuery(AuthNKeyColumnIdentifier, id, TextEquals)
}
func NewAuthNKeyCreationDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) {
return NewTimestampQuery(AuthNKeyColumnCreationDate, ts, compare)
}
func NewAuthNKeyExpirationDateDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) {
return NewTimestampQuery(AuthNKeyColumnExpiration, ts, compare)
}
//go:embed authn_key_user.sql
var authNKeyUserQuery string
@@ -288,49 +349,52 @@ func (q *Queries) GetAuthNKeyUser(ctx context.Context, keyID, userID string) (_
}
func prepareAuthNKeysQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeys, error)) {
return sq.Select(
AuthNKeyColumnID.identifier(),
AuthNKeyColumnCreationDate.identifier(),
AuthNKeyColumnChangeDate.identifier(),
AuthNKeyColumnResourceOwner.identifier(),
AuthNKeyColumnSequence.identifier(),
AuthNKeyColumnExpiration.identifier(),
AuthNKeyColumnType.identifier(),
countColumn.identifier(),
).From(authNKeyTable.identifier()).
PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) (*AuthNKeys, error) {
authNKeys := make([]*AuthNKey, 0)
var count uint64
for rows.Next() {
authNKey := new(AuthNKey)
err := rows.Scan(
&authNKey.ID,
&authNKey.CreationDate,
&authNKey.ChangeDate,
&authNKey.ResourceOwner,
&authNKey.Sequence,
&authNKey.Expiration,
&authNKey.Type,
&count,
)
if err != nil {
return nil, err
}
authNKeys = append(authNKeys, authNKey)
}
query := sq.Select(
AuthNKeyColumnID.identifier(),
AuthNKeyColumnAggregateID.identifier(),
AuthNKeyColumnCreationDate.identifier(),
AuthNKeyColumnChangeDate.identifier(),
AuthNKeyColumnResourceOwner.identifier(),
AuthNKeyColumnSequence.identifier(),
AuthNKeyColumnExpiration.identifier(),
AuthNKeyColumnType.identifier(),
countColumn.identifier(),
).From(authNKeyTable.identifier()).
PlaceholderFormat(sq.Dollar)
if err := rows.Close(); err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Dgfn3", "Errors.Query.CloseRows")
return query, func(rows *sql.Rows) (*AuthNKeys, error) {
authNKeys := make([]*AuthNKey, 0)
var count uint64
for rows.Next() {
authNKey := new(AuthNKey)
err := rows.Scan(
&authNKey.ID,
&authNKey.AggregateID,
&authNKey.CreationDate,
&authNKey.ChangeDate,
&authNKey.ResourceOwner,
&authNKey.Sequence,
&authNKey.Expiration,
&authNKey.Type,
&count,
)
if err != nil {
return nil, err
}
return &AuthNKeys{
AuthNKeys: authNKeys,
SearchResponse: SearchResponse{
Count: count,
},
}, nil
authNKeys = append(authNKeys, authNKey)
}
if err := rows.Close(); err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Dgfn3", "Errors.Query.CloseRows")
}
return &AuthNKeys{
AuthNKeys: authNKeys,
SearchResponse: SearchResponse{
Count: count,
},
}, nil
}
}
func prepareAuthNKeyQuery() (sq.SelectBuilder, func(row *sql.Row) (*AuthNKey, error)) {
+8
View File
@@ -19,6 +19,7 @@ import (
var (
prepareAuthNKeysStmt = `SELECT projections.authn_keys2.id,` +
` projections.authn_keys2.aggregate_id,` +
` projections.authn_keys2.creation_date,` +
` projections.authn_keys2.change_date,` +
` projections.authn_keys2.resource_owner,` +
@@ -29,6 +30,7 @@ var (
` FROM projections.authn_keys2`
prepareAuthNKeysCols = []string{
"id",
"aggregate_id",
"creation_date",
"change_date",
"resource_owner",
@@ -120,6 +122,7 @@ func Test_AuthNKeyPrepares(t *testing.T) {
[][]driver.Value{
{
"id",
"aggId",
testNow,
testNow,
"ro",
@@ -137,6 +140,7 @@ func Test_AuthNKeyPrepares(t *testing.T) {
AuthNKeys: []*AuthNKey{
{
ID: "id",
AggregateID: "aggId",
CreationDate: testNow,
ChangeDate: testNow,
ResourceOwner: "ro",
@@ -157,6 +161,7 @@ func Test_AuthNKeyPrepares(t *testing.T) {
[][]driver.Value{
{
"id-1",
"aggId-1",
testNow,
testNow,
"ro",
@@ -166,6 +171,7 @@ func Test_AuthNKeyPrepares(t *testing.T) {
},
{
"id-2",
"aggId-2",
testNow,
testNow,
"ro",
@@ -183,6 +189,7 @@ func Test_AuthNKeyPrepares(t *testing.T) {
AuthNKeys: []*AuthNKey{
{
ID: "id-1",
AggregateID: "aggId-1",
CreationDate: testNow,
ChangeDate: testNow,
ResourceOwner: "ro",
@@ -192,6 +199,7 @@ func Test_AuthNKeyPrepares(t *testing.T) {
},
{
ID: "id-2",
AggregateID: "aggId-2",
CreationDate: testNow,
ChangeDate: testNow,
ResourceOwner: "ro",
+3
View File
@@ -62,6 +62,9 @@ func (*authNKeyProjection) Init() *old_handler.Check {
handler.NewPrimaryKey(AuthNKeyInstanceIDCol, AuthNKeyIDCol),
handler.WithIndex(handler.NewIndex("enabled", []string{AuthNKeyEnabledCol})),
handler.WithIndex(handler.NewIndex("identifier", []string{AuthNKeyIdentifierCol})),
handler.WithIndex(handler.NewIndex("resource_owner", []string{AuthNKeyResourceOwnerCol})),
handler.WithIndex(handler.NewIndex("creation_date", []string{AuthNKeyCreationDateCol})),
handler.WithIndex(handler.NewIndex("expiration_date", []string{AuthNKeyExpirationCol})),
),
)
}
@@ -56,6 +56,8 @@ func (*personalAccessTokenProjection) Init() *old_handler.Check {
handler.WithIndex(handler.NewIndex("user_id", []string{PersonalAccessTokenColumnUserID})),
handler.WithIndex(handler.NewIndex("resource_owner", []string{PersonalAccessTokenColumnResourceOwner})),
handler.WithIndex(handler.NewIndex("owner_removed", []string{PersonalAccessTokenColumnOwnerRemoved})),
handler.WithIndex(handler.NewIndex("creation_date", []string{PersonalAccessTokenColumnCreationDate})),
handler.WithIndex(handler.NewIndex("expiration_date", []string{PersonalAccessTokenColumnExpiration})),
),
)
}
+9 -5
View File
@@ -132,16 +132,20 @@ func usersCheckPermission(ctx context.Context, users *Users, permissionCheck dom
)
}
func userPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *UserSearchQueries) sq.SelectBuilder {
func userPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, filters []SearchQuery) sq.SelectBuilder {
return userPermissionCheckV2WithCustomColumns(ctx, query, enabled, filters, UserResourceOwnerCol, UserIDCol)
}
func userPermissionCheckV2WithCustomColumns(ctx context.Context, query sq.SelectBuilder, enabled bool, filters []SearchQuery, userResourceOwnerCol, userID Column) sq.SelectBuilder {
if !enabled {
return query
}
join, args := PermissionClause(
ctx,
UserResourceOwnerCol,
userResourceOwnerCol,
domain.PermissionUserRead,
SingleOrgPermissionOption(queries.Queries),
OwnedRowsPermissionOption(UserIDCol),
SingleOrgPermissionOption(filters),
OwnedRowsPermissionOption(userID),
)
return query.JoinClause(join, args...)
}
@@ -637,7 +641,7 @@ func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries, p
defer func() { span.EndWithError(err) }()
query, scan := prepareUsersQuery()
query = userPermissionCheckV2(ctx, query, permissionCheckV2, queries)
query = userPermissionCheckV2(ctx, query, permissionCheckV2, queries.Queries)
stmt, args, err := queries.toQuery(query).Where(sq.Eq{
UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
}).ToSql()
+48 -12
View File
@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"errors"
"slices"
"time"
sq "github.com/Masterminds/squirrel"
@@ -11,12 +12,21 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/query/projection"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
func patsCheckPermission(ctx context.Context, tokens *PersonalAccessTokens, permissionCheck domain.PermissionCheck) {
tokens.PersonalAccessTokens = slices.DeleteFunc(tokens.PersonalAccessTokens,
func(token *PersonalAccessToken) bool {
return userCheckPermission(ctx, token.ResourceOwner, token.UserID, permissionCheck) != nil
},
)
}
var (
personalAccessTokensTable = table{
name: projection.PersonalAccessTokenProjectionTable,
@@ -86,7 +96,7 @@ type PersonalAccessTokenSearchQueries struct {
Queries []SearchQuery
}
func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk bool, id string, withOwnerRemoved bool, queries ...SearchQuery) (pat *PersonalAccessToken, err error) {
func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk bool, id string, queries ...SearchQuery) (pat *PersonalAccessToken, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -102,11 +112,9 @@ func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk
query = q.toQuery(query)
}
eq := sq.Eq{
PersonalAccessTokenColumnID.identifier(): id,
PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
}
if !withOwnerRemoved {
eq[PersonalAccessTokenColumnOwnerRemoved.identifier()] = false
PersonalAccessTokenColumnID.identifier(): id,
PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
PersonalAccessTokenColumnOwnerRemoved.identifier(): false,
}
stmt, args, err := query.Where(eq).ToSql()
if err != nil {
@@ -123,18 +131,34 @@ func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk
return pat, nil
}
func (q *Queries) SearchPersonalAccessTokens(ctx context.Context, queries *PersonalAccessTokenSearchQueries, withOwnerRemoved bool) (personalAccessTokens *PersonalAccessTokens, err error) {
// SearchPersonalAccessTokens returns personal access token resources.
// If permissionCheck is nil, the PATs are not filtered.
// If permissionCheck is not nil and the PermissionCheckV2 feature flag is false, the returned PATs are filtered in-memory by the given permission check.
// If permissionCheck is not nil and the PermissionCheckV2 feature flag is true, the returned PATs are filtered in the database.
func (q *Queries) SearchPersonalAccessTokens(ctx context.Context, queries *PersonalAccessTokenSearchQueries, permissionCheck domain.PermissionCheck) (authNKeys *PersonalAccessTokens, err error) {
permissionCheckV2 := PermissionV2(ctx, permissionCheck)
keys, err := q.searchPersonalAccessTokens(ctx, queries, permissionCheckV2)
if err != nil {
return nil, err
}
if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 {
patsCheckPermission(ctx, keys, permissionCheck)
}
return keys, nil
}
func (q *Queries) searchPersonalAccessTokens(ctx context.Context, queries *PersonalAccessTokenSearchQueries, permissionCheckV2 bool) (personalAccessTokens *PersonalAccessTokens, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
query, scan := preparePersonalAccessTokensQuery()
query = queries.toQuery(query)
query = userPermissionCheckV2WithCustomColumns(ctx, query, permissionCheckV2, queries.Queries, PersonalAccessTokenColumnResourceOwner, PersonalAccessTokenColumnUserID)
eq := sq.Eq{
PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
PersonalAccessTokenColumnOwnerRemoved.identifier(): false,
}
if !withOwnerRemoved {
eq[PersonalAccessTokenColumnOwnerRemoved.identifier()] = false
}
stmt, args, err := queries.toQuery(query).Where(eq).ToSql()
stmt, args, err := query.Where(eq).ToSql()
if err != nil {
return nil, zerrors.ThrowInvalidArgument(err, "QUERY-Hjw2w", "Errors.Query.InvalidRequest")
}
@@ -160,6 +184,18 @@ func NewPersonalAccessTokenUserIDSearchQuery(value string) (SearchQuery, error)
return NewTextQuery(PersonalAccessTokenColumnUserID, value, TextEquals)
}
func NewPersonalAccessTokenIDQuery(id string) (SearchQuery, error) {
return NewTextQuery(PersonalAccessTokenColumnID, id, TextEquals)
}
func NewPersonalAccessTokenCreationDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) {
return NewTimestampQuery(PersonalAccessTokenColumnCreationDate, ts, compare)
}
func NewPersonalAccessTokenExpirationDateDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) {
return NewTimestampQuery(PersonalAccessTokenColumnExpiration, ts, compare)
}
func (r *PersonalAccessTokenSearchQueries) AppendMyResourceOwnerQuery(orgID string) error {
query, err := NewPersonalAccessTokenResourceOwnerSearchQuery(orgID)
if err != nil {
+2 -5
View File
@@ -88,10 +88,7 @@ func NewMachineChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
changes []MachineChanges,
) (*MachineChangedEvent, error) {
if len(changes) == 0 {
return nil, zerrors.ThrowPreconditionFailed(nil, "USER-3M9fs", "Errors.NoChangesFound")
}
) *MachineChangedEvent {
changeEvent := &MachineChangedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
@@ -102,7 +99,7 @@ func NewMachineChangedEvent(
for _, change := range changes {
change(changeEvent)
}
return changeEvent, nil
return changeEvent
}
type MachineChanges func(event *MachineChangedEvent)
+1
View File
@@ -117,6 +117,7 @@ Errors:
AlreadyVerified: Телефонът вече е потвърден
Empty: Телефонът е празен
NotChanged: Телефонът не е сменен
VerifyingRemovalIsNotSupported: Премахването на проверката не се поддържа
Address:
NotFound: Адресът не е намерен
NotChanged: Адресът не е променен
+1
View File
@@ -115,6 +115,7 @@ Errors:
AlreadyVerified: Telefon již ověřen
Empty: Telefon je prázdný
NotChanged: Telefon nezměněn
VerifyingRemovalIsNotSupported: Ověření odstranění telefonu není podporováno
Address:
NotFound: Adresa nenalezena
NotChanged: Adresa nezměněna
+1
View File
@@ -115,6 +115,7 @@ Errors:
AlreadyVerified: Telefonnummer bereits verifiziert
Empty: Telefonnummer ist leer
NotChanged: Telefonnummer wurde nicht geändert
VerifyingRemovalIsNotSupported: Verifizieren der Telefonnummer Entfernung wird nicht unterstützt
Address:
NotFound: Adresse nicht gefunden
NotChanged: Adresse wurde nicht geändert
+1
View File
@@ -116,6 +116,7 @@ Errors:
AlreadyVerified: Phone already verified
Empty: Phone is empty
NotChanged: Phone not changed
VerifyingRemovalIsNotSupported: Verifying phone removal is not supported
Address:
NotFound: Address not found
NotChanged: Address not changed
+1
View File
@@ -115,6 +115,7 @@ Errors:
AlreadyVerified: El teléfono ya se verificó
Empty: El teléfono está vacío
NotChanged: El teléfono no ha cambiado
VerifyingRemovalIsNotSupported: La verificación de eliminación no está soportada
Address:
NotFound: Dirección no encontrada
NotChanged: La dirección no ha cambiado
+1
View File
@@ -115,6 +115,7 @@ Errors:
AlreadyVerified: Téléphone déjà vérifié
Empty: Téléphone est vide
NotChanged: Téléphone n'a pas changé
VerifyingRemovalIsNotSupported: La vérification de la suppression n'est pas prise en charge
Address:
NotFound: Adresse non trouvée
NotChanged: L'adresse n'a pas changé
+1
View File
@@ -115,6 +115,7 @@ Errors:
AlreadyVerified: Telefon már ellenőrizve
Empty: A telefon mező üres
NotChanged: Telefon nem lett megváltoztatva
VerifyingRemovalIsNotSupported: A telefon eltávolításának ellenőrzése nem támogatott
Address:
NotFound: Cím nem található
NotChanged: Cím nem lett megváltoztatva
+1
View File
@@ -115,6 +115,7 @@ Errors:
AlreadyVerified: Telepon sudah diverifikasi
Empty: Telepon kosong
NotChanged: Telepon tidak berubah
VerifyingRemovalIsNotSupported: Verifikasi penghapusan tidak didukung
Address:
NotFound: Alamat tidak ditemukan
NotChanged: Alamat tidak berubah
+1
View File
@@ -115,6 +115,7 @@ Errors:
AlreadyVerified: Telefono già verificato
Empty: Il telefono è vuoto
NotChanged: Telefono non cambiato
VerifyingRemovalIsNotSupported: La rimozione della verifica non è supportata
Address:
NotFound: Indirizzo non trovato
NotChanged: Indirizzo non cambiato
+1
View File
@@ -116,6 +116,7 @@ Errors:
AlreadyVerified: 電話番号はすでに認証済みです
Empty: 電話番号が空です
NotChanged: 電話番号が変更されていません
VerifyingRemovalIsNotSupported: 電話番号の削除を検証することはできません
Address:
NotFound: 住所が見つかりません
NotChanged: 住所は変更されていません
+1
View File
@@ -116,6 +116,7 @@ Errors:
AlreadyVerified: 전화번호가 이미 인증되었습니다
Empty: 전화번호가 비어 있습니다
NotChanged: 전화번호가 변경되지 않았습니다
VerifyingRemovalIsNotSupported: 전화번호 제거를 확인하는 것은 지원되지 않습니다
Address:
NotFound: 주소를 찾을 수 없습니다
NotChanged: 주소가 변경되지 않았습니다
+1
View File
@@ -115,6 +115,7 @@ Errors:
AlreadyVerified: Телефонскиот број веќе е верифициран
Empty: Телефонскиот број е празен
NotChanged: Телефонскиот број не е променет
VerifyingRemovalIsNotSupported: Отстранувањето на верификацијата не е поддржано
Address:
NotFound: Адресата не е пронајдена
NotChanged: Адресата не е променета
+1
View File
@@ -115,6 +115,7 @@ Errors:
AlreadyVerified: Telefoon is al geverifieerd
Empty: Telefoon is leeg
NotChanged: Telefoon niet veranderd
VerifyingRemovalIsNotSupported: Verwijderen van verificatie is niet ondersteund
Address:
NotFound: Adres niet gevonden
NotChanged: Adres niet veranderd
+1
View File
@@ -115,6 +115,7 @@ Errors:
AlreadyVerified: Numer telefonu już zweryfikowany
Empty: Numer telefonu jest pusty
NotChanged: Numer telefonu nie zmieniony
VerifyingRemovalIsNotSupported: Usunięcie weryfikacji nie jest obsługiwane
Address:
NotFound: Adres nie znaleziony
NotChanged: Adres nie zmieniony
+1
View File
@@ -115,6 +115,7 @@ Errors:
AlreadyVerified: O telefone já foi verificado
Empty: O telefone está vazio
NotChanged: Telefone não alterado
VerifyingRemovalIsNotSupported: Remoção de verificação não suportada
Address:
NotFound: Endereço não encontrado
NotChanged: Endereço não alterado
+1
View File
@@ -116,6 +116,7 @@ Errors:
AlreadyVerified: Numărul de telefon este deja verificat
Empty: Numărul de telefon este gol
NotChanged: Numărul de telefon nu a fost schimbat
VerifyingRemovalIsNotSupported: Verificarea eliminării nu este acceptată
Address:
NotFound: Adresa nu a fost găsită
NotChanged: Adresa nu a fost schimbată
+1
View File
@@ -116,6 +116,7 @@ Errors:
AlreadyVerified: Телефон уже подтверждён
Empty: Телефон пуст
NotChanged: Телефон не менялся
VerifyingRemovalIsNotSupported: Удаление телефона не поддерживается
Address:
NotFound: Адрес не найден
NotChanged: Адрес не изменён
+1
View File
@@ -115,6 +115,7 @@ Errors:
AlreadyVerified: Mobilnr redan verifierad
Empty: Mobilnr är tom
NotChanged: Mobilnr ändrades inte
VerifyingRemovalIsNotSupported: Verifiering av borttagning stöds inte
Address:
NotFound: Adress hittades inte
NotChanged: Adress ändrades inte
+1
View File
@@ -115,6 +115,7 @@ Errors:
AlreadyVerified: 手机号码已经验证
Empty: 电话号码是空的
NotChanged: 电话号码没有改变
VerifyingRemovalIsNotSupported: 验证手机号码删除不受支持
Address:
NotFound: 找不到地址
NotChanged: 地址没有改变
+1 -1
View File
@@ -40,4 +40,4 @@ lint:
- zitadel/system.proto
- zitadel/text.proto
- zitadel/user.proto
- zitadel/v1.proto
- zitadel/v1.proto
+96
View File
@@ -0,0 +1,96 @@
syntax = "proto3";
package zitadel.filter.v2;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/filter/v2;filter";
import "google/protobuf/timestamp.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
enum TextFilterMethod {
TEXT_FILTER_METHOD_EQUALS = 0;
TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE = 1;
TEXT_FILTER_METHOD_STARTS_WITH = 2;
TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE = 3;
TEXT_FILTER_METHOD_CONTAINS = 4;
TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE = 5;
TEXT_FILTER_METHOD_ENDS_WITH = 6;
TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE = 7;
}
enum ListFilterMethod {
LIST_FILTER_METHOD_IN = 0;
}
enum TimestampFilterMethod {
TIMESTAMP_FILTER_METHOD_EQUALS = 0;
TIMESTAMP_FILTER_METHOD_AFTER = 1;
TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS = 2;
TIMESTAMP_FILTER_METHOD_BEFORE = 3;
TIMESTAMP_FILTER_METHOD_BEFORE_OR_EQUALS = 4;
}
message PaginationRequest {
// Starting point for retrieval, in combination of offset used to query a set list of objects.
uint64 offset = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "0";
}
];
// limit is the maximum amount of objects returned. The default is set to 100
// with a maximum of 1000 in the runtime configuration.
// If the limit exceeds the maximum configured ZITADEL will throw an error.
// If no limit is present the default is taken.
uint32 limit = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "10";
}
];
// Asc is the sorting order. If true the list is sorted ascending, if false
// the list is sorted descending. The default is descending.
bool asc = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "false";
}
];
}
message PaginationResponse {
// Absolute number of objects matching the query, regardless of applied limit.
uint64 total_result = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "100";
}
];
// Applied limit from query, defines maximum amount of objects per request, to compare if all objects are returned.
uint64 applied_limit = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "100";
}
];
}
message IDFilter {
// Only return resources that belong to this id.
string id = 1 [
(validate.rules).string = {max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
max_length: 200;
example: "\"69629023906488337\"";
}
];
}
message TimestampFilter {
// Filter resources by timestamp.
google.protobuf.Timestamp timestamp = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2024-12-18T07:50:47.492Z\"";
}
];
// Defines the condition (e.g., equals, before, after) that the timestamp of the retrieved resources should match.
TimestampFilterMethod method = 2 [
(validate.rules).enum.defined_only = true
];
}
+35 -1
View File
@@ -6,6 +6,7 @@ option go_package = "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta;filter";
import "google/protobuf/timestamp.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
enum TextFilterMethod {
TEXT_FILTER_METHOD_EQUALS = 0;
@@ -56,4 +57,37 @@ message PaginationResponse {
example: "\"100\"";
}
];
}
}
message IDFilter {
// Only return resources that belong to this id.
string id = 1 [
(validate.rules).string = {max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
max_length: 200;
example: "\"69629023906488337\"";
}
];
}
message TimestampFilter {
// Filter resources by timestamp.
google.protobuf.Timestamp timestamp = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2024-12-18T07:50:47.492Z\"";
}
];
// Defines the condition (e.g., equals, before, after) that the timestamp of the retrieved resources should match.
TimestampFilterMethod method = 2 [
(validate.rules).enum.defined_only = true
];
}
message InIDsFilter {
// Defines the ids to query for.
repeated string ids = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"69629023906488334\",\"69622366012355662\"]";
}
];
}
+138 -63
View File
@@ -432,7 +432,11 @@ service ManagementService {
};
}
// Deprecated: use ImportHumanUser
// Create User (Human)
//
// Deprecated: use [ImportHumanUser](apis/resources/mgmt/management-service-import-human-user.api.mdx) instead.
//
// Create a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login.
rpc AddHumanUser(AddHumanUserRequest) returns (AddHumanUserResponse) {
option (google.api.http) = {
post: "/users/human"
@@ -444,10 +448,8 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Deprecated: Create User (Human)";
description: "Create a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login.\n\nDeprecated: use ImportHumanUser"
tags: "Users";
deprecated: true;
tags: "Users";
parameters: {
headers: {
name: "x-zitadel-orgid";
@@ -459,7 +461,11 @@ service ManagementService {
};
}
// Deprecated: please use user service v2 AddHumanUser
// Create/Import User (Human)
//
// Deprecated: use [UpdateHumanUser](apis/resources/user_service_v2/user-service-update-human-user.api.mdx) instead.
//
// Create/import a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login.
rpc ImportHumanUser(ImportHumanUserRequest) returns (ImportHumanUserResponse) {
option (google.api.http) = {
post: "/users/human/_import"
@@ -471,11 +477,9 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Create/Import User (Human)";
description: "Create/import a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login.\n\nDeprecated: please use user service v2 [AddHumanUser](apis/resources/user_service_v2/user-service-add-human-user.api.mdx)"
deprecated: true;
tags: "Users";
tags: "User Human"
deprecated: true;
parameters: {
headers: {
name: "x-zitadel-orgid";
@@ -487,6 +491,11 @@ service ManagementService {
};
}
// Create User (Machine)
//
// Deprecated: use [user service v2 CreateUser](apis/resources/user_service_v2/user-service-create-user.api.mdx) to create a user of type machine instead.
//
// Create a new user with the type machine for your API, service or device. These users are used for non-interactive authentication flows.
rpc AddMachineUser(AddMachineUserRequest) returns (AddMachineUserResponse) {
option (google.api.http) = {
post: "/users/machine"
@@ -498,8 +507,7 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Create User (Machine)";
description: "Create a new user with the type machine for your API, service or device. These users are used for non-interactive authentication flows."
deprecated: true;
tags: "Users";
tags: "User Machine";
responses: {
@@ -683,7 +691,11 @@ service ManagementService {
};
}
// Deprecated: please use user service v2 UpdateHumanUser
// Change user name
//
// Deprecated: use [user service v2 UpdateUser](apis/resources/user_service_v2/user-service-update-user.api.mdx) instead.
//
// Change the username of the user. Be aware that the user has to log in with the newly added username afterward
rpc UpdateUserName(UpdateUserNameRequest) returns (UpdateUserNameResponse) {
option (google.api.http) = {
put: "/users/{user_id}/username"
@@ -695,8 +707,6 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Change user name";
description: "Change the username of the user. Be aware that the user has to log in with the newly added username afterward.\n\nDeprecated: please use user service v2 UpdateHumanUser"
tags: "Users";
deprecated: true;
responses: {
@@ -903,7 +913,11 @@ service ManagementService {
};
}
// Deprecated: please use user service v2 UpdateHumanUser
// Update User Profile (Human)
//
// Deprecated: use [user service v2 UpdateHumanUser](apis/resources/user_service_v2/user-service-update-human-user.api.mdx) instead.
//
// Update the profile information from a user. The profile includes basic information like first_name and last_name.
rpc UpdateHumanProfile(UpdateHumanProfileRequest) returns (UpdateHumanProfileResponse) {
option (google.api.http) = {
put: "/users/{user_id}/profile"
@@ -915,11 +929,9 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Update User Profile (Human)";
description: "Update the profile information from a user. The profile includes basic information like first_name and last_name.\n\nDeprecated: please use user service v2 UpdateHumanUser"
deprecated: true;
tags: "Users";
tags: "User Human";
deprecated: true;
responses: {
key: "200"
value: {
@@ -970,7 +982,11 @@ service ManagementService {
};
}
// Deprecated: please use user service v2 SetEmail
// Update User Email (Human)
//
// Deprecated: use [user service v2 SetEmail](apis/resources/user_service_v2/user-service-set-email.api.mdx) instead.
//
// Change the email address of a user. If the state is set to not verified, the user will get a verification email.
rpc UpdateHumanEmail(UpdateHumanEmailRequest) returns (UpdateHumanEmailResponse) {
option (google.api.http) = {
put: "/users/{user_id}/email"
@@ -982,8 +998,6 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Update User Email (Human)";
description: "Change the email address of a user. If the state is set to not verified, the user will get a verification email.\n\nDeprecated: please use user service v2 SetEmail"
tags: "Users";
tags: "User Human";
deprecated: true;
@@ -1039,7 +1053,11 @@ service ManagementService {
};
}
// Deprecated: please use user service v2 ResendEmailCode
// Resend User Email Verification
//
// Deprecated: use [user service v2 ResendEmailCode](apis/resources/user_service_v2/user-service-resend-email-code.api.mdx) instead.
//
// Resend the email verification notification to the given email address of the user.
rpc ResendHumanEmailVerification(ResendHumanEmailVerificationRequest) returns (ResendHumanEmailVerificationResponse) {
option (google.api.http) = {
post: "/users/{user_id}/email/_resend_verification"
@@ -1051,8 +1069,6 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Resend User Email Verification";
description: "Resend the email verification notification to the given email address of the user.\n\nDeprecated: please use user service v2 ResendEmailCode"
tags: "Users";
tags: "User Human";
deprecated: true;
@@ -1106,7 +1122,11 @@ service ManagementService {
};
}
// Deprecated: please use user service v2 SetPhone
// Update User Phone (Human)
//
// Deprecated: use [user service v2 SetPhone](apis/resources/user_service_v2/user-service-update-user.api.mdx) instead.
//
// Change the phone number of a user. If the state is set to not verified, the user will get an SMS to verify (if a notification provider is configured). The phone number is only for informational purposes and to send messages, not for Authentication (2FA).
rpc UpdateHumanPhone(UpdateHumanPhoneRequest) returns (UpdateHumanPhoneResponse) {
option (google.api.http) = {
put: "/users/{user_id}/phone"
@@ -1118,8 +1138,6 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Update User Phone (Human)";
description: "Change the phone number of a user. If the state is set to not verified, the user will get an SMS to verify (if a notification provider is configured). The phone number is only for informational purposes and to send messages, not for Authentication (2FA).\n\nDeprecated: please use user service v2 SetPhone"
tags: "Users";
tags: "User Human";
deprecated: true;
@@ -1140,7 +1158,11 @@ service ManagementService {
};
}
// Deprecated: please use user service v2 SetPhone
// Remove User Phone (Human)
//
// Deprecated: use user service v2 [user service v2 SetPhone](apis/resources/user_service_v2/user-service-set-phone.api.mdx) instead.
//
// Remove the configured phone number of a user.
rpc RemoveHumanPhone(RemoveHumanPhoneRequest) returns (RemoveHumanPhoneResponse) {
option (google.api.http) = {
delete: "/users/{user_id}/phone"
@@ -1151,8 +1173,6 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Remove User Phone (Human)";
description: "Remove the configured phone number of a user.\n\nDeprecated: please use user service v2 SetPhone"
tags: "Users";
tags: "User Human";
deprecated: true;
@@ -1173,7 +1193,11 @@ service ManagementService {
};
}
// Deprecated: please use user service v2 ResendPhoneCode
// Resend User Phone Verification
//
// Deprecated: use user service v2 [user service v2 ResendPhoneCode](apis/resources/user_service_v2/user-service-resend-phone-code.api.mdx) instead.
//
// Resend the notification for the verification of the phone number, to the number stored on the user.
rpc ResendHumanPhoneVerification(ResendHumanPhoneVerificationRequest) returns (ResendHumanPhoneVerificationResponse) {
option (google.api.http) = {
post: "/users/{user_id}/phone/_resend_verification"
@@ -1185,8 +1209,6 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Resend User Phone Verification";
description: "Resend the notification for the verification of the phone number, to the number stored on the user.\n\nDeprecated: please use user service v2 ResendPhoneCode"
tags: "Users";
tags: "User Human";
deprecated: true;
@@ -1238,7 +1260,9 @@ service ManagementService {
};
}
// Deprecated: please use user service v2 SetPassword
// Set Human Initial Password
//
// Deprecated: use [user service v2 SetPassword](apis/resources/user_service_v2/user-service-set-password.api.mdx) instead.
rpc SetHumanInitialPassword(SetHumanInitialPasswordRequest) returns (SetHumanInitialPasswordResponse) {
option (google.api.http) = {
post: "/users/{user_id}/password/_initialize"
@@ -1252,7 +1276,6 @@ service ManagementService {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "Users";
tags: "User Human";
summary: "Set Human Initial Password\n\nDeprecated: please use user service v2 SetPassword";
deprecated: true;
parameters: {
headers: {
@@ -1265,7 +1288,9 @@ service ManagementService {
};
}
// Deprecated: please use user service v2 SetPassword
// Set User Password
//
// Deprecated: use [user service v2 SetPassword](apis/resources/user_service_v2/user-service-set-password.api.mdx) instead.
rpc SetHumanPassword(SetHumanPasswordRequest) returns (SetHumanPasswordResponse) {
option (google.api.http) = {
post: "/users/{user_id}/password"
@@ -1277,8 +1302,6 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Set User Password";
description: "Set a new password for a user. Per default, the user has to change the password on the next login. You can set no_change_required to true, to avoid the change on the next login.\n\nDeprecated: please use user service v2 SetPassword"
tags: "Users";
tags: "User Human";
deprecated: true;
@@ -1299,7 +1322,11 @@ service ManagementService {
};
}
// Deprecated: please use user service v2 PasswordReset
// Send Reset Password Notification
//
// Deprecated: use [user service v2 PasswordReset](apis/resources/user_service_v2/user-service-password-reset.api.mdx) instead.
//
// The user will receive an email with a link to change the password.
rpc SendHumanResetPasswordNotification(SendHumanResetPasswordNotificationRequest) returns (SendHumanResetPasswordNotificationResponse) {
option (google.api.http) = {
post: "/users/{user_id}/password/_reset"
@@ -1311,8 +1338,6 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Send Reset Password Notification";
description: "The user will receive an email with a link to change the password.\n\nDeprecated: please use user service v2 PasswordReset"
tags: "Users";
tags: "User Human";
deprecated: true;
@@ -1629,6 +1654,11 @@ service ManagementService {
};
}
// Update Machine User
//
// Deprecated: use [user service v2 UpdateUser](apis/resources/user_service_v2/user-service-update-user.api.mdx) to update a user of type machine instead.
//
// Change a service account/machine user. It is used for accounts with non-interactive authentication possibilities.
rpc UpdateMachine(UpdateMachineRequest) returns (UpdateMachineResponse) {
option (google.api.http) = {
put: "/users/{user_id}/machine"
@@ -1640,8 +1670,7 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Update Machine User";
description: "Change a service account/machine user. It is used for accounts with non-interactive authentication possibilities."
deprecated: true;
tags: "Users";
tags: "User Machine";
responses: {
@@ -1661,6 +1690,11 @@ service ManagementService {
};
}
// Create Secret for Machine User
//
// Deprecated: use [user service v2 AddSecret](apis/resources/user_service_v2/user-service-add-secret.api.mdx) instead.
//
// Create a new secret for a machine user/service account. It is used to authenticate the user (client credential grant).
rpc GenerateMachineSecret(GenerateMachineSecretRequest) returns (GenerateMachineSecretResponse) {
option (google.api.http) = {
put: "/users/{user_id}/secret"
@@ -1672,8 +1706,7 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Create Secret for Machine User";
description: "Create a new secret for a machine user/service account. It is used to authenticate the user (client credential grant)."
deprecated: true;
tags: "Users";
tags: "User Machine";
responses: {
@@ -1693,6 +1726,11 @@ service ManagementService {
};
}
// Delete Secret of Machine User
//
// Deprecated: use [user service v2 RemoveSecret](apis/resources/user_service_v2/user-service-remove-secret.api.mdx) instead.
//
// Delete a secret of a machine user/service account. The user will not be able to authenticate with the secret afterward.
rpc RemoveMachineSecret(RemoveMachineSecretRequest) returns (RemoveMachineSecretResponse) {
option (google.api.http) = {
delete: "/users/{user_id}/secret"
@@ -1703,8 +1741,7 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Delete Secret of Machine User";
description: "Delete a secret of a machine user/service account. The user will not be able to authenticate with the secret afterward."
deprecated: true;
tags: "Users";
tags: "User Machine";
responses: {
@@ -1724,6 +1761,11 @@ service ManagementService {
};
}
// Get Machine user Key By ID
//
// Deprecated: use [user service v2 ListKeys](apis/resources/user_service_v2/user-service-list-keys.api.mdx) instead.
//
// Get a specific Key of a machine user by its id. Machine keys are used to authenticate with jwt profile authentication.
rpc GetMachineKeyByIDs(GetMachineKeyByIDsRequest) returns (GetMachineKeyByIDsResponse) {
option (google.api.http) = {
get: "/users/{user_id}/keys/{key_id}"
@@ -1734,8 +1776,7 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Get Machine user Key By ID";
description: "Get a specific Key of a machine user by its id. Machine keys are used to authenticate with jwt profile authentication."
deprecated: true;
tags: "Users";
tags: "User Machine";
responses: {
@@ -1755,6 +1796,11 @@ service ManagementService {
};
}
// Get Machine user Key By ID
//
// Deprecated: use [user service v2 ListKeys](apis/resources/user_service_v2/user-service-list-keys.api.mdx) instead.
//
// Get the list of keys of a machine user. Machine keys are used to authenticate with jwt profile authentication.
rpc ListMachineKeys(ListMachineKeysRequest) returns (ListMachineKeysResponse) {
option (google.api.http) = {
post: "/users/{user_id}/keys/_search"
@@ -1766,8 +1812,7 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Get Machine user Key By ID";
description: "Get the list of keys of a machine user. Machine keys are used to authenticate with jwt profile authentication."
deprecated: true;
tags: "Users";
tags: "User Machine";
responses: {
@@ -1787,6 +1832,14 @@ service ManagementService {
};
}
// Create Key for machine user
//
// Deprecated: use [user service v2 AddKey](apis/resources/user_service_v2/user-service-add-key.api.mdx) instead.
//
// If a public key is not supplied, a new key is generated and will be returned in the response.
// Make sure to store the returned key.
// If an RSA public key is supplied, the private key is omitted from the response.
// Machine keys are used to authenticate with jwt profile.
rpc AddMachineKey(AddMachineKeyRequest) returns (AddMachineKeyResponse) {
option (google.api.http) = {
post: "/users/{user_id}/keys"
@@ -1798,8 +1851,7 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Create Key for machine user";
description: "If a public key is not supplied, a new key is generated and will be returned in the response. Make sure to store the returned key. If an RSA public key is supplied, the private key is omitted from the response. Machine keys are used to authenticate with jwt profile."
deprecated: true;
tags: "Users";
tags: "User Machine";
responses: {
@@ -1819,6 +1871,12 @@ service ManagementService {
};
}
// Delete Key for machine user
//
// Deprecated: use [user service v2 RemoveKey](apis/resources/user_service_v2/user-service-remove-key.api.mdx) instead.
//
// Delete a specific key from a user.
// The user will not be able to authenticate with that key afterward.
rpc RemoveMachineKey(RemoveMachineKeyRequest) returns (RemoveMachineKeyResponse) {
option (google.api.http) = {
delete: "/users/{user_id}/keys/{key_id}"
@@ -1829,8 +1887,7 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Delete Key for machine user";
description: "Delete a specific key from a user. The user will not be able to authenticate with that key afterward."
deprecated: true;
tags: "Users";
tags: "User Machine";
responses: {
@@ -1850,6 +1907,11 @@ service ManagementService {
};
}
// Get Personal-Access-Token (PAT) by ID
//
// Deprecated: use [user service v2 ListPersonalAccessTokens](apis/resources/user_service_v2/user-service-list-personal-access-tokens.api.mdx) instead.
//
// Returns the PAT for a user, currently only available for machine users/service accounts. PATs are ready-to-use tokens and can be sent directly in the authentication header.
rpc GetPersonalAccessTokenByIDs(GetPersonalAccessTokenByIDsRequest) returns (GetPersonalAccessTokenByIDsResponse) {
option (google.api.http) = {
get: "/users/{user_id}/pats/{token_id}"
@@ -1860,8 +1922,7 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Get a Personal-Access-Token (PAT) by ID";
description: "Returns the PAT for a user, currently only available for machine users/service accounts. PATs are ready-to-use tokens and can be sent directly in the authentication header."
deprecated: true;
tags: "Users";
tags: "User Machine";
responses: {
@@ -1881,6 +1942,11 @@ service ManagementService {
};
}
// List Personal-Access-Tokens (PATs)
//
// Deprecated: use [user service v2 ListPersonalAccessTokens](apis/resources/user_service_v2/user-service-list-personal-access-tokens.api.mdx) instead.
//
// Returns a list of PATs for a user, currently only available for machine users/service accounts. PATs are ready-to-use tokens and can be sent directly in the authentication header.
rpc ListPersonalAccessTokens(ListPersonalAccessTokensRequest) returns (ListPersonalAccessTokensResponse) {
option (google.api.http) = {
post: "/users/{user_id}/pats/_search"
@@ -1892,8 +1958,7 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Get a Personal-Access-Token (PAT) by ID";
description: "Returns a list of PATs for a user, currently only available for machine users/service accounts. PATs are ready-to-use tokens and can be sent directly in the authentication header."
deprecated: true;
tags: "Users";
tags: "User Machine";
responses: {
@@ -1913,6 +1978,13 @@ service ManagementService {
};
}
// Create a Personal-Access-Token (PAT)
//
// Deprecated: use [user service v2 AddPersonalAccessToken](apis/resources/user_service_v2/user-service-add-personal-access-token.api.mdx) instead.
//
// Generates a new PAT for the user. Currently only available for machine users.
// The token will be returned in the response, make sure to store it.
// PATs are ready-to-use tokens and can be sent directly in the authentication header.
rpc AddPersonalAccessToken(AddPersonalAccessTokenRequest) returns (AddPersonalAccessTokenResponse) {
option (google.api.http) = {
post: "/users/{user_id}/pats"
@@ -1924,8 +1996,7 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Create a Personal-Access-Token (PAT)";
description: "Generates a new PAT for the user. Currently only available for machine users. The token will be returned in the response, make sure to store it. PATs are ready-to-use tokens and can be sent directly in the authentication header."
deprecated: true;
tags: "Users";
tags: "User Machine";
responses: {
@@ -1945,6 +2016,11 @@ service ManagementService {
};
}
// Remove a Personal-Access-Token (PAT) by ID
//
// Deprecated: use [user service v2 RemovePersonalAccessToken](apis/resources/user_service_v2/user-service-remove-personal-access-token.api.mdx) instead.
//
// Delete a PAT from a user. Afterward, the user will not be able to authenticate with that token anymore.
rpc RemovePersonalAccessToken(RemovePersonalAccessTokenRequest) returns (RemovePersonalAccessTokenResponse) {
option (google.api.http) = {
delete: "/users/{user_id}/pats/{token_id}"
@@ -1955,8 +2031,7 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Get a Personal-Access-Token (PAT) by ID";
description: "Delete a PAT from a user. Afterward, the user will not be able to authenticate with that token anymore."
deprecated: true;
tags: "Users";
tags: "User Machine";
responses: {
@@ -2003,7 +2078,7 @@ service ManagementService {
};
}
// Deprecated: please use user service v2 RemoveLinkedIDP
// Deprecated: please use [user service v2 RemoveIDPLink](apis/resources/user_service_v2/user-service-remove-idp-link.api.mdx)
rpc RemoveHumanLinkedIDP(RemoveHumanLinkedIDPRequest) returns (RemoveHumanLinkedIDPResponse) {
option (google.api.http) = {
delete: "/users/{user_id}/idps/{idp_id}/{linked_user_id}"
+8 -58
View File
@@ -185,10 +185,10 @@ message ProjectSearchFilter {
option (validate.required) = true;
ProjectNameFilter project_name_filter = 1;
InProjectIDsFilter in_project_ids_filter = 2;
ProjectResourceOwnerFilter project_resource_owner_filter = 3;
ProjectGrantResourceOwnerFilter project_grant_resource_owner_filter = 4;
ProjectOrganizationIDFilter project_organization_id_filter = 5;
zitadel.filter.v2beta.InIDsFilter in_project_ids_filter = 2;
zitadel.filter.v2beta.IDFilter project_resource_owner_filter = 3;
zitadel.filter.v2beta.IDFilter project_grant_resource_owner_filter = 4;
zitadel.filter.v2beta.IDFilter project_organization_id_filter = 5;
}
}
@@ -210,68 +210,18 @@ message ProjectNameFilter {
];
}
message InProjectIDsFilter {
// Defines the ids to query for.
repeated string project_ids = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "the ids of the projects to include"
example: "[\"69629023906488334\",\"69622366012355662\"]";
}
];
}
message ProjectResourceOwnerFilter {
// Defines the ID of organization the project belongs to query for.
string project_resource_owner = 1 [
(validate.rules).string = {max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629023906488334\""
}
];
}
message ProjectGrantResourceOwnerFilter {
// Defines the ID of organization the project grant belongs to query for.
string project_grant_resource_owner = 1 [
(validate.rules).string = {max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629023906488334\""
}
];
}
message ProjectOrganizationIDFilter {
// Defines the ID of organization the project and granted project belong to query for.
string project_organization_id = 1 [
(validate.rules).string = {max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629023906488334\""
}
];
}
message ProjectGrantSearchFilter {
oneof filter {
option (validate.required) = true;
ProjectNameFilter project_name_filter = 1;
ProjectRoleKeyFilter role_key_filter = 2;
InProjectIDsFilter in_project_ids_filter = 3;
ProjectResourceOwnerFilter project_resource_owner_filter = 4;
ProjectGrantResourceOwnerFilter project_grant_resource_owner_filter = 5;
zitadel.filter.v2beta.InIDsFilter in_project_ids_filter = 3;
zitadel.filter.v2beta.IDFilter project_resource_owner_filter = 4;
zitadel.filter.v2beta.IDFilter project_grant_resource_owner_filter = 5;
}
}
message GrantedOrganizationIDFilter {
// Defines the ID of organization the project is granted to query for.
string granted_organization_id = 1 [
(validate.rules).string = {max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629023906488334\""
}
];
}
message ProjectRole {
// ID of the project.
string project_id = 1 [
@@ -344,4 +294,4 @@ message ProjectRoleDisplayNameFilter {
zitadel.filter.v2beta.TextFilterMethod method = 2 [
(validate.rules).enum.defined_only = true
];
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ message SetHumanEmail {
example: "\"mini@mouse.com\"";
}
];
// if no verification is specified, an email is sent with the default url
// If no verification is specified, an email is sent with the default url
oneof verification {
SendEmailVerificationCode send_code = 2;
ReturnEmailVerificationCode return_code = 3;
+69
View File
@@ -0,0 +1,69 @@
syntax = "proto3";
package zitadel.user.v2;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user";
import "protoc-gen-openapiv2/options/annotations.proto";
import "google/protobuf/timestamp.proto";
import "validate/validate.proto";
import "zitadel/filter/v2/filter.proto";
message Key {
// The timestamp of the key creation.
google.protobuf.Timestamp creation_date = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2024-12-18T07:50:47.492Z\"";
}
];
// The timestamp of the last change of the key.
google.protobuf.Timestamp change_date = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2024-12-18T07:50:47.492Z\"";
}
];
// The unique identifier of the key.
string id = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629012906488334\"";
}
];
// The unique identifier of the user the key belongs to.
string user_id = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629012906488334\"";
}
];
// The unique identifier of the organization the key belongs to.
string organization_id = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629012906488334\"";
}
];
// The keys expiration date.
google.protobuf.Timestamp expiration_date = 6 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2024-12-18T07:50:47.492Z\"";
}
];
}
message KeysSearchFilter {
oneof filter {
option (validate.required) = true;
zitadel.filter.v2.IDFilter key_id_filter = 1;
zitadel.filter.v2.IDFilter user_id_filter = 2;
zitadel.filter.v2.IDFilter organization_id_filter = 3;
zitadel.filter.v2.TimestampFilter created_date_filter = 4;
zitadel.filter.v2.TimestampFilter expiration_date_filter = 5;
}
}
enum KeyFieldName {
KEY_FIELD_NAME_UNSPECIFIED = 0;
KEY_FIELD_NAME_CREATED_DATE = 1;
KEY_FIELD_NAME_ID = 2;
KEY_FIELD_NAME_USER_ID = 3;
KEY_FIELD_NAME_ORGANIZATION_ID = 4;
KEY_FIELD_NAME_KEY_EXPIRATION_DATE = 5;
}
+70
View File
@@ -0,0 +1,70 @@
syntax = "proto3";
package zitadel.user.v2;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user";
import "protoc-gen-openapiv2/options/annotations.proto";
import "google/protobuf/timestamp.proto";
import "validate/validate.proto";
import "zitadel/filter/v2/filter.proto";
message PersonalAccessToken {
// The timestamp of the personal access token creation.
google.protobuf.Timestamp creation_date = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2024-12-18T07:50:47.492Z\"";
}
];
// The timestamp of the last change of the personal access token.
google.protobuf.Timestamp change_date = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2024-12-18T07:50:47.492Z\"";
}
];
// The unique identifier of the personal access token.
string id = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629012906488334\"";
}
];
// The unique identifier of the user the personal access token belongs to.
string user_id = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629012906488334\"";
}
];
// The unique identifier of the organization the personal access token belongs to.
string organization_id = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629012906488334\"";
}
];
// The personal access tokens expiration date.
google.protobuf.Timestamp expiration_date = 6 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2024-12-18T07:50:47.492Z\"";
}
];
}
message PersonalAccessTokensSearchFilter {
oneof filter {
option (validate.required) = true;
zitadel.filter.v2.IDFilter token_id_filter = 1;
zitadel.filter.v2.IDFilter user_id_filter = 2;
zitadel.filter.v2.IDFilter organization_id_filter = 3;
zitadel.filter.v2.TimestampFilter created_date_filter = 4;
zitadel.filter.v2.TimestampFilter expiration_date_filter = 5;
}
}
enum PersonalAccessTokenFieldName {
PERSONAL_ACCESS_TOKEN_FIELD_NAME_UNSPECIFIED = 0;
PERSONAL_ACCESS_TOKEN_FIELD_NAME_CREATED_DATE = 1;
PERSONAL_ACCESS_TOKEN_FIELD_NAME_ID = 2;
PERSONAL_ACCESS_TOKEN_FIELD_NAME_USER_ID = 3;
PERSONAL_ACCESS_TOKEN_FIELD_NAME_ORGANIZATION_ID = 4;
PERSONAL_ACCESS_TOKEN_FIELD_NAME_EXPIRATION_DATE = 5;
}
File diff suppressed because it is too large Load Diff