feat: add schema user create and remove (#8494)

# Which Problems Are Solved

Added functionality that user with a userschema can be created and
removed.

# How the Problems Are Solved

Added logic and moved APIs so that everything is API v3 conform.

# Additional Changes

- move of user and userschema API to resources folder
- changed testing and parameters
- some renaming

# Additional Context

closes #7308

---------

Co-authored-by: Elio Bischof <elio@zitadel.com>
This commit is contained in:
Stefan Benz 2024-08-28 21:46:45 +02:00 committed by GitHub
parent 90b908c361
commit 41ae35f2ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 5766 additions and 2247 deletions

View File

@ -45,13 +45,14 @@ import (
org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2"
org_v2beta "github.com/zitadel/zitadel/internal/api/grpc/org/v2beta"
action_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/action/v3alpha"
user_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/user/v3alpha"
userschema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/userschema/v3alpha"
"github.com/zitadel/zitadel/internal/api/grpc/resources/webkey/v3"
session_v2 "github.com/zitadel/zitadel/internal/api/grpc/session/v2"
session_v2beta "github.com/zitadel/zitadel/internal/api/grpc/session/v2beta"
settings_v2 "github.com/zitadel/zitadel/internal/api/grpc/settings/v2"
settings_v2beta "github.com/zitadel/zitadel/internal/api/grpc/settings/v2beta"
"github.com/zitadel/zitadel/internal/api/grpc/system"
user_schema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/user/schema/v3alpha"
user_v2 "github.com/zitadel/zitadel/internal/api/grpc/user/v2"
user_v2beta "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta"
http_util "github.com/zitadel/zitadel/internal/api/http"
@ -444,7 +445,10 @@ func startAPIs(
if err := apis.RegisterService(ctx, action_v3_alpha.CreateServer(config.SystemDefaults, commands, queries, domain.AllFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, user_schema_v3_alpha.CreateServer(commands, queries)); err != nil {
if err := apis.RegisterService(ctx, userschema_v3_alpha.CreateServer(config.SystemDefaults, commands, queries)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, user_v3_alpha.CreateServer(commands, keys.User)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, webkey.CreateServer(commands, queries)); err != nil {

View File

@ -317,7 +317,7 @@ module.exports = {
},
},
user_schema_v3: {
specPath: ".artifacts/openapi/zitadel/user/schema/v3alpha/user_schema_service.swagger.json",
specPath: ".artifacts/openapi/zitadel/resources/userschema/v3alpha/user_schema_service.swagger.json",
outputDir: "docs/apis/resources/user_schema_service_v3",
sidebarOptions: {
groupPathsBy: "tag",
@ -325,7 +325,7 @@ module.exports = {
},
},
user_v3: {
specPath: ".artifacts/openapi/zitadel/user/v3alpha/user_service.swagger.json",
specPath: ".artifacts/openapi/zitadel/resources/user/v3alpha/user_service.swagger.json",
outputDir: "docs/apis/resources/user_service_v3",
sidebarOptions: {
groupPathsBy: "tag",

View File

@ -0,0 +1,51 @@
package user
import (
"google.golang.org/grpc"
"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/crypto"
user "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
)
var _ user.ZITADELUsersServer = (*Server)(nil)
type Server struct {
user.UnimplementedZITADELUsersServer
command *command.Commands
userCodeAlg crypto.EncryptionAlgorithm
}
type Config struct{}
func CreateServer(
command *command.Commands,
userCodeAlg crypto.EncryptionAlgorithm,
) *Server {
return &Server{
command: command,
userCodeAlg: userCodeAlg,
}
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
user.RegisterZITADELUsersServer(grpcServer, s)
}
func (s *Server) AppName() string {
return user.ZITADELUsers_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return user.ZITADELUsers_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return user.ZITADELUsers_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return user.RegisterZITADELUsersHandler
}

View File

@ -0,0 +1,72 @@
//go:build integration
package user_test
import (
"context"
"os"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
user "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
)
var (
IAMOwnerCTX, SystemCTX context.Context
UserCTX context.Context
Tester *integration.Tester
Client user.ZITADELUsersClient
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, _, cancel := integration.Contexts(time.Hour)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
IAMOwnerCTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser)
UserCTX = Tester.WithAuthorization(ctx, integration.Login)
Client = Tester.Client.UserV3Alpha
return m.Run()
}())
}
func ensureFeatureEnabled(t *testing.T, iamOwnerCTX context.Context) {
f, err := Tester.Client.FeatureV2.GetInstanceFeatures(iamOwnerCTX, &feature.GetInstanceFeaturesRequest{
Inheritance: true,
})
require.NoError(t, err)
if f.UserSchema.GetEnabled() {
return
}
_, err = Tester.Client.FeatureV2.SetInstanceFeatures(iamOwnerCTX, &feature.SetInstanceFeaturesRequest{
UserSchema: gu.Ptr(true),
})
require.NoError(t, err)
retryDuration := time.Minute
if ctxDeadline, ok := iamOwnerCTX.Deadline(); ok {
retryDuration = time.Until(ctxDeadline)
}
require.EventuallyWithT(t,
func(ttt *assert.CollectT) {
f, err := Tester.Client.FeatureV2.GetInstanceFeatures(iamOwnerCTX, &feature.GetInstanceFeaturesRequest{
Inheritance: true,
})
require.NoError(ttt, err)
if f.UserSchema.GetEnabled() {
return
}
},
retryDuration,
100*time.Millisecond,
"timed out waiting for ensuring instance feature")
}

View File

@ -0,0 +1,66 @@
package user
import (
"context"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/internal/api/authz"
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
"github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
)
func (s *Server) CreateUser(ctx context.Context, req *user.CreateUserRequest) (_ *user.CreateUserResponse, err error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
schemauser, err := createUserRequestToCreateSchemaUser(ctx, req)
if err != nil {
return nil, err
}
if err := s.command.CreateSchemaUser(ctx, schemauser, s.userCodeAlg); err != nil {
return nil, err
}
return &user.CreateUserResponse{
Details: resource_object.DomainToDetailsPb(schemauser.Details, object.OwnerType_OWNER_TYPE_ORG, schemauser.ResourceOwner),
EmailCode: gu.Ptr(schemauser.ReturnCodeEmail),
PhoneCode: gu.Ptr(schemauser.ReturnCodePhone),
}, nil
}
func createUserRequestToCreateSchemaUser(ctx context.Context, req *user.CreateUserRequest) (*command.CreateSchemaUser, error) {
data, err := req.GetUser().GetData().MarshalJSON()
if err != nil {
return nil, err
}
return &command.CreateSchemaUser{
ResourceOwner: authz.GetCtxData(ctx).OrgID,
SchemaID: req.GetUser().GetSchemaId(),
ID: req.GetUser().GetUserId(),
Data: data,
}, nil
}
func (s *Server) DeleteUser(ctx context.Context, req *user.DeleteUserRequest) (_ *user.DeleteUserResponse, err error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
details, err := s.command.DeleteSchemaUser(ctx, req.GetUserId())
if err != nil {
return nil, err
}
return &user.DeleteUserResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner),
}, nil
}
func checkUserSchemaEnabled(ctx context.Context) error {
if authz.GetInstance(ctx).Features().UserSchema {
return nil
}
return zerrors.ThrowPreconditionFailed(nil, "TODO", "Errors.UserSchema.NotEnabled")
}

View File

@ -0,0 +1,354 @@
//go:build integration
package user_test
import (
"context"
"testing"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/require"
"github.com/zitadel/logging"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
user "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
)
func TestServer_CreateUser(t *testing.T) {
ensureFeatureEnabled(t, IAMOwnerCTX)
schema := []byte(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}`)
schemaResp := Tester.CreateUserSchema(IAMOwnerCTX, schema)
permissionSchema := []byte(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"urn:zitadel:schema:permission": {
"owner": "r",
"self": "r"
},
"type": "string"
}
}
}`)
permissionSchemaResp := Tester.CreateUserSchema(IAMOwnerCTX, permissionSchema)
orgResp := Tester.CreateOrganization(IAMOwnerCTX, gofakeit.Name(), gofakeit.Email())
type res struct {
want *resource_object.Details
returnCodeEmail bool
returnCodePhone bool
}
tests := []struct {
name string
ctx context.Context
req *user.CreateUserRequest
res res
wantErr bool
}{
{
name: "user create, no schemaID",
ctx: IAMOwnerCTX,
req: &user.CreateUserRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
User: &user.CreateUser{Data: unmarshalJSON("{\"name\": \"user\"}")},
},
wantErr: true,
},
{
name: "user create, no context",
ctx: context.Background(),
req: &user.CreateUserRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
User: &user.CreateUser{
SchemaId: schemaResp.GetDetails().GetId(),
Data: unmarshalJSON("{\"name\": \"user\"}"),
},
},
wantErr: true,
},
{
name: "user create, no permission",
ctx: UserCTX,
req: &user.CreateUserRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
User: &user.CreateUser{
SchemaId: schemaResp.GetDetails().GetId(),
Data: unmarshalJSON("{\"name\": \"user\"}"),
},
},
wantErr: true,
},
{
name: "user create, invalid schema permission, owner",
ctx: IAMOwnerCTX,
req: &user.CreateUserRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
User: &user.CreateUser{
SchemaId: permissionSchemaResp.GetDetails().GetId(),
Data: unmarshalJSON("{\"name\": \"user\"}"),
},
},
wantErr: true,
},
{
name: "user create, no user data",
ctx: IAMOwnerCTX,
req: &user.CreateUserRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
User: &user.CreateUser{
SchemaId: schemaResp.GetDetails().GetId(),
},
},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
},
},
{
name: "user create, ok",
ctx: IAMOwnerCTX,
req: &user.CreateUserRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
User: &user.CreateUser{
SchemaId: schemaResp.GetDetails().GetId(),
Data: unmarshalJSON("{\"name\": \"user\"}"),
},
},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
},
}, {
name: "user create, full contact, ok",
ctx: IAMOwnerCTX,
req: &user.CreateUserRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
User: &user.CreateUser{
SchemaId: schemaResp.GetDetails().GetId(),
Data: unmarshalJSON("{\"name\": \"user\"}"),
Contact: &user.SetContact{
Email: &user.SetEmail{
Address: gofakeit.Email(),
Verification: &user.SetEmail_ReturnCode{ReturnCode: &user.ReturnEmailVerificationCode{}},
},
Phone: &user.SetPhone{
Number: gofakeit.Phone(),
Verification: &user.SetPhone_ReturnCode{ReturnCode: &user.ReturnPhoneVerificationCode{}},
},
},
},
},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
returnCodePhone: true,
returnCodeEmail: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Tester.Client.UserV3Alpha.CreateUser(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.res.want, got.Details)
if tt.res.returnCodeEmail {
require.NotNil(t, got.EmailCode)
}
if tt.res.returnCodePhone {
require.NotNil(t, got.PhoneCode)
}
})
}
}
func TestServer_DeleteUser(t *testing.T) {
ensureFeatureEnabled(t, IAMOwnerCTX)
schema := []byte(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}`)
schemaResp := Tester.CreateUserSchema(IAMOwnerCTX, schema)
orgResp := Tester.CreateOrganization(IAMOwnerCTX, gofakeit.Name(), gofakeit.Email())
tests := []struct {
name string
ctx context.Context
dep func(ctx context.Context, req *user.DeleteUserRequest) error
req *user.DeleteUserRequest
want *resource_object.Details
wantErr bool
}{
{
name: "user delete, no userID",
ctx: IAMOwnerCTX,
req: &user.DeleteUserRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
UserId: "",
},
wantErr: true,
},
{
name: "user delete, not existing",
ctx: IAMOwnerCTX,
req: &user.DeleteUserRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
UserId: "notexisting",
},
wantErr: true,
},
{
name: "user delete, no context",
ctx: context.Background(),
dep: func(ctx context.Context, req *user.DeleteUserRequest) error {
userResp := Tester.CreateSchemaUser(IAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.UserId = userResp.GetDetails().GetId()
return nil
},
req: &user.DeleteUserRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
},
wantErr: true,
},
{
name: "user delete, no permission",
ctx: UserCTX,
dep: func(ctx context.Context, req *user.DeleteUserRequest) error {
userResp := Tester.CreateSchemaUser(IAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.UserId = userResp.GetDetails().GetId()
return nil
},
req: &user.DeleteUserRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
},
wantErr: true,
},
{
name: "user delete, ok",
ctx: IAMOwnerCTX,
dep: func(ctx context.Context, req *user.DeleteUserRequest) error {
userResp := Tester.CreateSchemaUser(ctx, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.UserId = userResp.GetDetails().GetId()
return nil
},
req: &user.DeleteUserRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
},
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.dep != nil {
err := tt.dep(tt.ctx, tt.req)
require.NoError(t, err)
}
got, err := Tester.Client.UserV3Alpha.DeleteUser(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
})
}
}
func unmarshalJSON(data string) *structpb.Struct {
user := new(structpb.Struct)
err := user.UnmarshalJSON([]byte(data))
if err != nil {
logging.OnError(err).Fatal("unmarshalling user json")
}
return user
}

View File

@ -0,0 +1,251 @@
package userschema
import (
"context"
"google.golang.org/protobuf/types/known/structpb"
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
schema "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha"
)
func (s *Server) SearchUserSchemas(ctx context.Context, req *schema.SearchUserSchemasRequest) (*schema.SearchUserSchemasResponse, error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
queries, err := s.searchUserSchemaToModel(req)
if err != nil {
return nil, err
}
res, err := s.query.SearchUserSchema(ctx, queries)
if err != nil {
return nil, err
}
userSchemas, err := userSchemasToPb(res.UserSchemas)
if err != nil {
return nil, err
}
return &schema.SearchUserSchemasResponse{
Details: resource_object.ToSearchDetailsPb(queries.SearchRequest, res.SearchResponse),
Result: userSchemas,
}, nil
}
func (s *Server) GetUserSchemaByID(ctx context.Context, req *schema.GetUserSchemaByIDRequest) (*schema.GetUserSchemaByIDResponse, error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
res, err := s.query.GetUserSchemaByID(ctx, req.GetId())
if err != nil {
return nil, err
}
userSchema, err := userSchemaToPb(res)
if err != nil {
return nil, err
}
return &schema.GetUserSchemaByIDResponse{
Schema: userSchema,
}, nil
}
func (s *Server) searchUserSchemaToModel(req *schema.SearchUserSchemasRequest) (*query.UserSchemaSearchQueries, error) {
offset, limit, asc, err := resource_object.SearchQueryPbToQuery(s.systemDefaults, req.Query)
if err != nil {
return nil, err
}
queries, err := userSchemaFiltersToQuery(req.Filters, 0) // start at level 0
if err != nil {
return nil, err
}
return &query.UserSchemaSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: userSchemaFieldNameToSortingColumn(req.SortingColumn),
},
Queries: queries,
}, nil
}
func userSchemaFieldNameToSortingColumn(field *schema.FieldName) query.Column {
if field == nil {
return query.UserSchemaCreationDateCol
}
switch *field {
case schema.FieldName_FIELD_NAME_TYPE:
return query.UserSchemaTypeCol
case schema.FieldName_FIELD_NAME_STATE:
return query.UserSchemaStateCol
case schema.FieldName_FIELD_NAME_REVISION:
return query.UserSchemaRevisionCol
case schema.FieldName_FIELD_NAME_CHANGE_DATE:
return query.UserSchemaChangeDateCol
case schema.FieldName_FIELD_NAME_CREATION_DATE:
return query.UserSchemaCreationDateCol
case schema.FieldName_FIELD_NAME_UNSPECIFIED:
return query.UserSchemaIDCol
default:
return query.UserSchemaIDCol
}
}
func userSchemasToPb(schemas []*query.UserSchema) (_ []*schema.UserSchema, err error) {
userSchemas := make([]*schema.UserSchema, len(schemas))
for i, userSchema := range schemas {
userSchemas[i], err = userSchemaToPb(userSchema)
if err != nil {
return nil, err
}
}
return userSchemas, nil
}
func userSchemaToPb(userSchema *query.UserSchema) (*schema.UserSchema, error) {
s := new(structpb.Struct)
if err := s.UnmarshalJSON(userSchema.Schema); err != nil {
return nil, err
}
return &schema.UserSchema{
Details: resource_object.DomainToDetailsPb(&userSchema.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, userSchema.ResourceOwner),
Type: userSchema.Type,
State: userSchemaStateToPb(userSchema.State),
Revision: userSchema.Revision,
Schema: s,
PossibleAuthenticators: authenticatorTypesToPb(userSchema.PossibleAuthenticators),
}, nil
}
func authenticatorTypesToPb(authenticators []domain.AuthenticatorType) []schema.AuthenticatorType {
authTypes := make([]schema.AuthenticatorType, len(authenticators))
for i, authenticator := range authenticators {
authTypes[i] = authenticatorTypeToPb(authenticator)
}
return authTypes
}
func authenticatorTypeToPb(authenticator domain.AuthenticatorType) schema.AuthenticatorType {
switch authenticator {
case domain.AuthenticatorTypeUsername:
return schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME
case domain.AuthenticatorTypePassword:
return schema.AuthenticatorType_AUTHENTICATOR_TYPE_PASSWORD
case domain.AuthenticatorTypeWebAuthN:
return schema.AuthenticatorType_AUTHENTICATOR_TYPE_WEBAUTHN
case domain.AuthenticatorTypeTOTP:
return schema.AuthenticatorType_AUTHENTICATOR_TYPE_TOTP
case domain.AuthenticatorTypeOTPEmail:
return schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_EMAIL
case domain.AuthenticatorTypeOTPSMS:
return schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_SMS
case domain.AuthenticatorTypeAuthenticationKey:
return schema.AuthenticatorType_AUTHENTICATOR_TYPE_AUTHENTICATION_KEY
case domain.AuthenticatorTypeIdentityProvider:
return schema.AuthenticatorType_AUTHENTICATOR_TYPE_IDENTITY_PROVIDER
case domain.AuthenticatorTypeUnspecified:
return schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED
default:
return schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED
}
}
func userSchemaStateToPb(state domain.UserSchemaState) schema.State {
switch state {
case domain.UserSchemaStateActive:
return schema.State_STATE_ACTIVE
case domain.UserSchemaStateInactive:
return schema.State_STATE_INACTIVE
case domain.UserSchemaStateUnspecified,
domain.UserSchemaStateDeleted:
return schema.State_STATE_UNSPECIFIED
default:
return schema.State_STATE_UNSPECIFIED
}
}
func userSchemaStateToDomain(state schema.State) domain.UserSchemaState {
switch state {
case schema.State_STATE_ACTIVE:
return domain.UserSchemaStateActive
case schema.State_STATE_INACTIVE:
return domain.UserSchemaStateInactive
case schema.State_STATE_UNSPECIFIED:
return domain.UserSchemaStateUnspecified
default:
return domain.UserSchemaStateUnspecified
}
}
func userSchemaFiltersToQuery(queries []*schema.SearchFilter, level uint8) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = userSchemaFilterToQuery(query, level)
if err != nil {
return nil, err
}
}
return q, nil
}
func userSchemaFilterToQuery(query *schema.SearchFilter, level uint8) (query.SearchQuery, error) {
if level > 20 {
// can't go deeper than 20 levels of nesting.
return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-zsQ97", "Errors.Query.TooManyNestingLevels")
}
switch q := query.Filter.(type) {
case *schema.SearchFilter_StateFilter:
return stateQueryToQuery(q.StateFilter)
case *schema.SearchFilter_TypeFilter:
return typeQueryToQuery(q.TypeFilter)
case *schema.SearchFilter_IdFilter:
return idQueryToQuery(q.IdFilter)
case *schema.SearchFilter_OrFilter:
return orQueryToQuery(q.OrFilter, level)
case *schema.SearchFilter_AndFilter:
return andQueryToQuery(q.AndFilter, level)
case *schema.SearchFilter_NotFilter:
return notQueryToQuery(q.NotFilter, level)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-vR9nC", "List.Query.Invalid")
}
}
func stateQueryToQuery(q *schema.StateFilter) (query.SearchQuery, error) {
return query.NewUserSchemaStateSearchQuery(userSchemaStateToDomain(q.GetState()))
}
func typeQueryToQuery(q *schema.TypeFilter) (query.SearchQuery, error) {
return query.NewUserSchemaTypeSearchQuery(q.GetType(), resource_object.TextMethodPbToQuery(q.GetMethod()))
}
func idQueryToQuery(q *schema.IDFilter) (query.SearchQuery, error) {
return query.NewUserSchemaIDSearchQuery(q.GetId(), resource_object.TextMethodPbToQuery(q.GetMethod()))
}
func orQueryToQuery(q *schema.OrFilter, level uint8) (query.SearchQuery, error) {
mappedQueries, err := userSchemaFiltersToQuery(q.GetQueries(), level+1)
if err != nil {
return nil, err
}
return query.NewUserOrSearchQuery(mappedQueries)
}
func andQueryToQuery(q *schema.AndFilter, level uint8) (query.SearchQuery, error) {
mappedQueries, err := userSchemaFiltersToQuery(q.GetQueries(), level+1)
if err != nil {
return nil, err
}
return query.NewUserAndSearchQuery(mappedQueries)
}
func notQueryToQuery(q *schema.NotFilter, level uint8) (query.SearchQuery, error) {
mappedQuery, err := userSchemaFilterToQuery(q.GetFilter(), level+1)
if err != nil {
return nil, err
}
return query.NewUserNotSearchQuery(mappedQuery)
}

View File

@ -0,0 +1,302 @@
//go:build integration
package userschema_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
"github.com/zitadel/zitadel/internal/api/grpc"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
schema "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha"
)
func TestServer_ListUserSchemas(t *testing.T) {
ensureFeatureEnabled(t, IAMOwnerCTX)
userSchema := new(structpb.Struct)
err := userSchema.UnmarshalJSON([]byte(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {}
}`))
require.NoError(t, err)
type args struct {
ctx context.Context
req *schema.SearchUserSchemasRequest
prepare func(request *schema.SearchUserSchemasRequest, resp *schema.SearchUserSchemasResponse) error
}
tests := []struct {
name string
args args
want *schema.SearchUserSchemasResponse
wantErr bool
}{
{
name: "missing permission",
args: args{
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &schema.SearchUserSchemasRequest{},
},
wantErr: true,
},
{
name: "not found, error",
args: args{
ctx: IAMOwnerCTX,
req: &schema.SearchUserSchemasRequest{
Filters: []*schema.SearchFilter{
{
Filter: &schema.SearchFilter_IdFilter{
IdFilter: &schema.IDFilter{
Id: "notexisting",
},
},
},
},
},
},
want: &schema.SearchUserSchemasResponse{
Details: &object.ListDetails{
TotalResult: 0,
AppliedLimit: 100,
},
Result: []*schema.UserSchema{},
},
},
{
name: "single (id), ok",
args: args{
ctx: IAMOwnerCTX,
req: &schema.SearchUserSchemasRequest{},
prepare: func(request *schema.SearchUserSchemasRequest, resp *schema.SearchUserSchemasResponse) error {
schemaType := fmt.Sprint(time.Now().UnixNano() + 1)
createResp := Tester.CreateUserSchemaEmptyWithType(IAMOwnerCTX, schemaType)
request.Filters = []*schema.SearchFilter{
{
Filter: &schema.SearchFilter_IdFilter{
IdFilter: &schema.IDFilter{
Id: createResp.GetDetails().GetId(),
Method: object.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS,
},
},
},
}
resp.Result[0].Type = schemaType
resp.Result[0].Details = createResp.GetDetails()
// as schema is freshly created, the changed date is the created date
resp.Result[0].Details.Created = resp.Result[0].Details.GetChanged()
resp.Details.Timestamp = resp.Result[0].Details.GetChanged()
return nil
},
},
want: &schema.SearchUserSchemasResponse{
Details: &object.ListDetails{
TotalResult: 1,
AppliedLimit: 100,
},
Result: []*schema.UserSchema{
{
State: schema.State_STATE_ACTIVE,
Revision: 1,
Schema: userSchema,
PossibleAuthenticators: nil,
},
},
},
},
{
name: "multiple (type), ok",
args: args{
ctx: IAMOwnerCTX,
req: &schema.SearchUserSchemasRequest{},
prepare: func(request *schema.SearchUserSchemasRequest, resp *schema.SearchUserSchemasResponse) error {
schemaType := fmt.Sprint(time.Now().UnixNano())
schemaType1 := schemaType + "_1"
schemaType2 := schemaType + "_2"
createResp := Tester.CreateUserSchemaEmptyWithType(IAMOwnerCTX, schemaType1)
createResp2 := Tester.CreateUserSchemaEmptyWithType(IAMOwnerCTX, schemaType2)
request.SortingColumn = gu.Ptr(schema.FieldName_FIELD_NAME_TYPE)
request.Query = &object.SearchQuery{Desc: false}
request.Filters = []*schema.SearchFilter{
{
Filter: &schema.SearchFilter_TypeFilter{
TypeFilter: &schema.TypeFilter{
Type: schemaType,
Method: object.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH,
},
},
},
}
resp.Result[0].Type = schemaType1
resp.Result[0].Details = createResp.GetDetails()
resp.Result[1].Type = schemaType2
resp.Result[1].Details = createResp2.GetDetails()
return nil
},
},
want: &schema.SearchUserSchemasResponse{
Details: &object.ListDetails{
TotalResult: 2,
AppliedLimit: 100,
},
Result: []*schema.UserSchema{
{
State: schema.State_STATE_ACTIVE,
Revision: 1,
Schema: userSchema,
PossibleAuthenticators: nil,
},
{
State: schema.State_STATE_ACTIVE,
Revision: 1,
Schema: userSchema,
PossibleAuthenticators: nil,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.prepare != nil {
err := tt.args.prepare(tt.args.req, tt.want)
require.NoError(t, err)
}
retryDuration := 20 * time.Second
if ctxDeadline, ok := IAMOwnerCTX.Deadline(); ok {
retryDuration = time.Until(ctxDeadline)
}
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, err := Client.SearchUserSchemas(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(ttt, err)
return
}
assert.NoError(ttt, err)
// always first check length, otherwise its failed anyway
assert.Len(ttt, got.Result, len(tt.want.Result))
for i := range tt.want.Result {
want := tt.want.Result[i]
got := got.Result[i]
integration.AssertResourceDetails(t, want.GetDetails(), got.GetDetails())
want.Details = got.Details
grpc.AllFieldsEqual(t, want.ProtoReflect(), got.ProtoReflect(), grpc.CustomMappers)
}
integration.AssertListDetails(t, tt.want, got)
}, retryDuration, time.Millisecond*100, "timeout waiting for expected user schema result")
})
}
}
func TestServer_GetUserSchemaByID(t *testing.T) {
ensureFeatureEnabled(t, IAMOwnerCTX)
userSchema := new(structpb.Struct)
err := userSchema.UnmarshalJSON([]byte(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {}
}`))
require.NoError(t, err)
type args struct {
ctx context.Context
req *schema.GetUserSchemaByIDRequest
prepare func(request *schema.GetUserSchemaByIDRequest, resp *schema.GetUserSchemaByIDResponse) error
}
tests := []struct {
name string
args args
want *schema.GetUserSchemaByIDResponse
wantErr bool
}{
{
name: "missing permission",
args: args{
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &schema.GetUserSchemaByIDRequest{},
prepare: func(request *schema.GetUserSchemaByIDRequest, resp *schema.GetUserSchemaByIDResponse) error {
schemaType := fmt.Sprint(time.Now().UnixNano() + 1)
createResp := Tester.CreateUserSchemaEmptyWithType(IAMOwnerCTX, schemaType)
request.Id = createResp.GetDetails().GetId()
return nil
},
},
wantErr: true,
},
{
name: "not existing, error",
args: args{
ctx: IAMOwnerCTX,
req: &schema.GetUserSchemaByIDRequest{
Id: "notexisting",
},
},
wantErr: true,
},
{
name: "get, ok",
args: args{
ctx: IAMOwnerCTX,
req: &schema.GetUserSchemaByIDRequest{},
prepare: func(request *schema.GetUserSchemaByIDRequest, resp *schema.GetUserSchemaByIDResponse) error {
schemaType := fmt.Sprint(time.Now().UnixNano() + 1)
createResp := Tester.CreateUserSchemaEmptyWithType(IAMOwnerCTX, schemaType)
request.Id = createResp.GetDetails().GetId()
resp.Schema.Type = schemaType
resp.Schema.Details = createResp.GetDetails()
return nil
},
},
want: &schema.GetUserSchemaByIDResponse{
Schema: &schema.UserSchema{
State: schema.State_STATE_ACTIVE,
Revision: 1,
Schema: userSchema,
PossibleAuthenticators: nil,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.prepare != nil {
err := tt.args.prepare(tt.args.req, tt.want)
require.NoError(t, err)
}
retryDuration := 5 * time.Second
if ctxDeadline, ok := IAMOwnerCTX.Deadline(); ok {
retryDuration = time.Until(ctxDeadline)
}
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, err := Client.GetUserSchemaByID(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(ttt, err)
return
}
assert.NoError(ttt, err)
integration.AssertResourceDetails(t, tt.want.GetSchema().GetDetails(), got.GetSchema().GetDetails())
tt.want.Schema.Details = got.GetSchema().GetDetails()
grpc.AllFieldsEqual(t, tt.want.ProtoReflect(), got.ProtoReflect(), grpc.CustomMappers)
}, retryDuration, time.Millisecond*100, "timeout waiting for expected user schema result")
})
}
}

View File

@ -0,0 +1,65 @@
package userschema
import (
"context"
"google.golang.org/grpc"
"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/query"
"github.com/zitadel/zitadel/internal/zerrors"
schema "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha"
)
var _ schema.ZITADELUserSchemasServer = (*Server)(nil)
type Server struct {
schema.UnimplementedZITADELUserSchemasServer
systemDefaults systemdefaults.SystemDefaults
command *command.Commands
query *query.Queries
}
type Config struct{}
func CreateServer(
systemDefaults systemdefaults.SystemDefaults,
command *command.Commands,
query *query.Queries,
) *Server {
return &Server{
systemDefaults: systemDefaults,
command: command,
query: query,
}
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
schema.RegisterZITADELUserSchemasServer(grpcServer, s)
}
func (s *Server) AppName() string {
return schema.ZITADELUserSchemas_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return schema.ZITADELUserSchemas_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return schema.ZITADELUserSchemas_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return schema.RegisterZITADELUserSchemasHandler
}
func checkUserSchemaEnabled(ctx context.Context) error {
if authz.GetInstance(ctx).Features().UserSchema {
return nil
}
return zerrors.ThrowPreconditionFailed(nil, "SCHEMA-SFjk3", "Errors.UserSchema.NotEnabled")
}

View File

@ -0,0 +1,71 @@
//go:build integration
package userschema_test
import (
"context"
"os"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
schema "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha"
)
var (
IAMOwnerCTX, SystemCTX context.Context
Tester *integration.Tester
Client schema.ZITADELUserSchemasClient
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, _, cancel := integration.Contexts(5 * time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
IAMOwnerCTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser)
Client = Tester.Client.UserSchemaV3
return m.Run()
}())
}
func ensureFeatureEnabled(t *testing.T, iamOwnerCTX context.Context) {
f, err := Tester.Client.FeatureV2.GetInstanceFeatures(iamOwnerCTX, &feature.GetInstanceFeaturesRequest{
Inheritance: true,
})
require.NoError(t, err)
if f.UserSchema.GetEnabled() {
return
}
_, err = Tester.Client.FeatureV2.SetInstanceFeatures(iamOwnerCTX, &feature.SetInstanceFeaturesRequest{
UserSchema: gu.Ptr(true),
})
require.NoError(t, err)
retryDuration := time.Minute
if ctxDeadline, ok := iamOwnerCTX.Deadline(); ok {
retryDuration = time.Until(ctxDeadline)
}
require.EventuallyWithT(t,
func(ttt *assert.CollectT) {
f, err := Tester.Client.FeatureV2.GetInstanceFeatures(iamOwnerCTX, &feature.GetInstanceFeaturesRequest{
Inheritance: true,
})
require.NoError(ttt, err)
if f.UserSchema.GetEnabled() {
return
}
},
retryDuration,
100*time.Millisecond,
"timed out waiting for ensuring instance feature")
}

View File

@ -0,0 +1,149 @@
package userschema
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
schema "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha"
)
func (s *Server) CreateUserSchema(ctx context.Context, req *schema.CreateUserSchemaRequest) (*schema.CreateUserSchemaResponse, error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
instanceID := authz.GetInstance(ctx).InstanceID()
userSchema, err := createUserSchemaToCommand(req, instanceID)
if err != nil {
return nil, err
}
if err := s.command.CreateUserSchema(ctx, userSchema); err != nil {
return nil, err
}
return &schema.CreateUserSchemaResponse{
Details: resource_object.DomainToDetailsPb(userSchema.Details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
}, nil
}
func (s *Server) PatchUserSchema(ctx context.Context, req *schema.PatchUserSchemaRequest) (*schema.PatchUserSchemaResponse, error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
instanceID := authz.GetInstance(ctx).InstanceID()
userSchema, err := patchUserSchemaToCommand(req, instanceID)
if err != nil {
return nil, err
}
if err := s.command.ChangeUserSchema(ctx, userSchema); err != nil {
return nil, err
}
return &schema.PatchUserSchemaResponse{
Details: resource_object.DomainToDetailsPb(userSchema.Details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
}, nil
}
func (s *Server) DeactivateUserSchema(ctx context.Context, req *schema.DeactivateUserSchemaRequest) (*schema.DeactivateUserSchemaResponse, error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
instanceID := authz.GetInstance(ctx).InstanceID()
details, err := s.command.DeactivateUserSchema(ctx, req.GetId(), instanceID)
if err != nil {
return nil, err
}
return &schema.DeactivateUserSchemaResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
}, nil
}
func (s *Server) ReactivateUserSchema(ctx context.Context, req *schema.ReactivateUserSchemaRequest) (*schema.ReactivateUserSchemaResponse, error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
instanceID := authz.GetInstance(ctx).InstanceID()
details, err := s.command.ReactivateUserSchema(ctx, req.GetId(), instanceID)
if err != nil {
return nil, err
}
return &schema.ReactivateUserSchemaResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
}, nil
}
func (s *Server) DeleteUserSchema(ctx context.Context, req *schema.DeleteUserSchemaRequest) (*schema.DeleteUserSchemaResponse, error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
instanceID := authz.GetInstance(ctx).InstanceID()
details, err := s.command.DeleteUserSchema(ctx, req.GetId(), instanceID)
if err != nil {
return nil, err
}
return &schema.DeleteUserSchemaResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
}, nil
}
func createUserSchemaToCommand(req *schema.CreateUserSchemaRequest, resourceOwner string) (*command.CreateUserSchema, error) {
schema, err := req.GetUserSchema().GetSchema().MarshalJSON()
if err != nil {
return nil, err
}
return &command.CreateUserSchema{
ResourceOwner: resourceOwner,
Type: req.GetUserSchema().GetType(),
Schema: schema,
PossibleAuthenticators: authenticatorsToDomain(req.GetUserSchema().GetPossibleAuthenticators()),
}, nil
}
func patchUserSchemaToCommand(req *schema.PatchUserSchemaRequest, resourceOwner string) (*command.ChangeUserSchema, error) {
schema, err := req.GetSchema().MarshalJSON()
if err != nil {
return nil, err
}
return &command.ChangeUserSchema{
ID: req.GetId(),
ResourceOwner: resourceOwner,
Type: req.Type,
Schema: schema,
PossibleAuthenticators: authenticatorsToDomain(req.GetPossibleAuthenticators()),
}, nil
}
func authenticatorsToDomain(authenticators []schema.AuthenticatorType) []domain.AuthenticatorType {
types := make([]domain.AuthenticatorType, len(authenticators))
for i, authenticator := range authenticators {
types[i] = authenticatorTypeToDomain(authenticator)
}
return types
}
func authenticatorTypeToDomain(authenticator schema.AuthenticatorType) domain.AuthenticatorType {
switch authenticator {
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED:
return domain.AuthenticatorTypeUnspecified
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME:
return domain.AuthenticatorTypeUsername
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_PASSWORD:
return domain.AuthenticatorTypePassword
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_WEBAUTHN:
return domain.AuthenticatorTypeWebAuthN
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_TOTP:
return domain.AuthenticatorTypeTOTP
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_EMAIL:
return domain.AuthenticatorTypeOTPEmail
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_SMS:
return domain.AuthenticatorTypeOTPSMS
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_AUTHENTICATION_KEY:
return domain.AuthenticatorTypeAuthenticationKey
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_IDENTITY_PROVIDER:
return domain.AuthenticatorTypeIdentityProvider
default:
return domain.AuthenticatorTypeUnspecified
}
}

View File

@ -0,0 +1,811 @@
//go:build integration
package userschema_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
schema "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha"
)
func TestServer_CreateUserSchema(t *testing.T) {
ensureFeatureEnabled(t, IAMOwnerCTX)
tests := []struct {
name string
ctx context.Context
req *schema.CreateUserSchemaRequest
want *schema.CreateUserSchemaResponse
wantErr bool
}{
{
name: "missing permission, error",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &schema.CreateUserSchemaRequest{
UserSchema: &schema.CreateUserSchema{
Type: gofakeit.Name(),
},
},
wantErr: true,
},
{
name: "empty type",
ctx: IAMOwnerCTX,
req: &schema.CreateUserSchemaRequest{
UserSchema: &schema.CreateUserSchema{
Type: "",
},
},
wantErr: true,
},
{
name: "empty schema, error",
ctx: IAMOwnerCTX,
req: &schema.CreateUserSchemaRequest{
UserSchema: &schema.CreateUserSchema{
Type: gofakeit.Name(),
},
},
wantErr: true,
},
{
name: "invalid schema, error",
ctx: IAMOwnerCTX,
req: &schema.CreateUserSchemaRequest{
UserSchema: &schema.CreateUserSchema{
Type: gofakeit.Name(),
DataType: &schema.CreateUserSchema_Schema{
Schema: func() *structpb.Struct {
s := new(structpb.Struct)
err := s.UnmarshalJSON([]byte(`
{
"type": "object",
"properties": {
"name": {
"type": "string",
"required": true
},
"description": {
"type": "string"
}
}
}
`))
require.NoError(t, err)
return s
}(),
},
},
},
wantErr: true,
},
{
name: "no authenticators, ok",
ctx: IAMOwnerCTX,
req: &schema.CreateUserSchemaRequest{
UserSchema: &schema.CreateUserSchema{
Type: gofakeit.Name(),
DataType: &schema.CreateUserSchema_Schema{
Schema: func() *structpb.Struct {
s := new(structpb.Struct)
err := s.UnmarshalJSON([]byte(`
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": ["name"]
}
`))
require.NoError(t, err)
return s
}(),
},
},
},
want: &schema.CreateUserSchemaResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
{
name: "invalid authenticator, error",
ctx: IAMOwnerCTX,
req: &schema.CreateUserSchemaRequest{
UserSchema: &schema.CreateUserSchema{
Type: gofakeit.Name(),
DataType: &schema.CreateUserSchema_Schema{
Schema: func() *structpb.Struct {
s := new(structpb.Struct)
err := s.UnmarshalJSON([]byte(`
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": ["name"]
}
`))
require.NoError(t, err)
return s
}(),
},
PossibleAuthenticators: []schema.AuthenticatorType{
schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED,
},
},
},
wantErr: true,
},
{
name: "with authenticator, ok",
ctx: IAMOwnerCTX,
req: &schema.CreateUserSchemaRequest{
UserSchema: &schema.CreateUserSchema{
Type: gofakeit.Name(),
DataType: &schema.CreateUserSchema_Schema{
Schema: func() *structpb.Struct {
s := new(structpb.Struct)
err := s.UnmarshalJSON([]byte(`
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": ["name"]
}
`))
require.NoError(t, err)
return s
}(),
},
PossibleAuthenticators: []schema.AuthenticatorType{
schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME,
},
},
},
want: &schema.CreateUserSchemaResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
{
name: "with invalid permission, error",
ctx: IAMOwnerCTX,
req: &schema.CreateUserSchemaRequest{
UserSchema: &schema.CreateUserSchema{
Type: gofakeit.Name(),
DataType: &schema.CreateUserSchema_Schema{
Schema: func() *structpb.Struct {
s := new(structpb.Struct)
err := s.UnmarshalJSON([]byte(`
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string",
"urn:zitadel:schema:permission": "read"
}
},
"required": ["name"]
}
`))
require.NoError(t, err)
return s
}(),
},
PossibleAuthenticators: []schema.AuthenticatorType{
schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME,
},
},
},
wantErr: true,
},
{
name: "with valid permission, ok",
ctx: IAMOwnerCTX,
req: &schema.CreateUserSchemaRequest{
UserSchema: &schema.CreateUserSchema{
Type: gofakeit.Name(),
DataType: &schema.CreateUserSchema_Schema{
Schema: func() *structpb.Struct {
s := new(structpb.Struct)
err := s.UnmarshalJSON([]byte(`
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string",
"urn:zitadel:schema:permission": {
"owner": "rw",
"self": "r"
}
}
},
"required": ["name"]
}
`))
require.NoError(t, err)
return s
}(),
},
PossibleAuthenticators: []schema.AuthenticatorType{
schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME,
},
},
},
want: &schema.CreateUserSchemaResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.CreateUserSchema(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want.GetDetails(), got.GetDetails())
})
}
}
func TestServer_UpdateUserSchema(t *testing.T) {
ensureFeatureEnabled(t, IAMOwnerCTX)
type args struct {
ctx context.Context
req *schema.PatchUserSchemaRequest
}
tests := []struct {
name string
prepare func(request *schema.PatchUserSchemaRequest) error
args args
want *schema.PatchUserSchemaResponse
wantErr bool
}{
{
name: "missing permission, error",
prepare: func(request *schema.PatchUserSchemaRequest) error {
schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId()
request.Id = schemaID
return nil
},
args: args{
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &schema.PatchUserSchemaRequest{
Type: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
wantErr: true,
},
{
name: "missing id, error",
prepare: func(request *schema.PatchUserSchemaRequest) error {
return nil
},
args: args{
ctx: IAMOwnerCTX,
req: &schema.PatchUserSchemaRequest{},
},
wantErr: true,
},
{
name: "not existing, error",
prepare: func(request *schema.PatchUserSchemaRequest) error {
request.Id = "notexisting"
return nil
},
args: args{
ctx: IAMOwnerCTX,
req: &schema.PatchUserSchemaRequest{},
},
wantErr: true,
},
{
name: "empty type, error",
prepare: func(request *schema.PatchUserSchemaRequest) error {
schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId()
request.Id = schemaID
return nil
},
args: args{
ctx: IAMOwnerCTX,
req: &schema.PatchUserSchemaRequest{
Type: gu.Ptr(""),
},
},
wantErr: true,
},
{
name: "update type, ok",
prepare: func(request *schema.PatchUserSchemaRequest) error {
schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId()
request.Id = schemaID
return nil
},
args: args{
ctx: IAMOwnerCTX,
req: &schema.PatchUserSchemaRequest{
Type: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
want: &schema.PatchUserSchemaResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
{
name: "empty schema, ok",
prepare: func(request *schema.PatchUserSchemaRequest) error {
schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId()
request.Id = schemaID
return nil
},
args: args{
ctx: IAMOwnerCTX,
req: &schema.PatchUserSchemaRequest{
DataType: &schema.PatchUserSchemaRequest_Schema{},
},
},
want: &schema.PatchUserSchemaResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
{
name: "invalid schema, error",
prepare: func(request *schema.PatchUserSchemaRequest) error {
schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId()
request.Id = schemaID
return nil
},
args: args{
ctx: IAMOwnerCTX,
req: &schema.PatchUserSchemaRequest{
DataType: &schema.PatchUserSchemaRequest_Schema{
Schema: func() *structpb.Struct {
s := new(structpb.Struct)
err := s.UnmarshalJSON([]byte(`
{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string",
"required": true
},
"description": {
"type": "string"
}
}
}
`))
require.NoError(t, err)
return s
}(),
},
},
},
wantErr: true,
},
{
name: "update schema, ok",
prepare: func(request *schema.PatchUserSchemaRequest) error {
schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId()
request.Id = schemaID
return nil
},
args: args{
ctx: IAMOwnerCTX,
req: &schema.PatchUserSchemaRequest{
DataType: &schema.PatchUserSchemaRequest_Schema{
Schema: func() *structpb.Struct {
s := new(structpb.Struct)
err := s.UnmarshalJSON([]byte(`
{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": ["name"]
}
`))
require.NoError(t, err)
return s
}(),
},
},
},
want: &schema.PatchUserSchemaResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
{
name: "invalid authenticator, error",
prepare: func(request *schema.PatchUserSchemaRequest) error {
schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId()
request.Id = schemaID
return nil
},
args: args{
ctx: IAMOwnerCTX,
req: &schema.PatchUserSchemaRequest{
PossibleAuthenticators: []schema.AuthenticatorType{
schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED,
},
},
},
wantErr: true,
},
{
name: "update authenticator, ok",
prepare: func(request *schema.PatchUserSchemaRequest) error {
schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId()
request.Id = schemaID
return nil
},
args: args{
ctx: IAMOwnerCTX,
req: &schema.PatchUserSchemaRequest{
PossibleAuthenticators: []schema.AuthenticatorType{
schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME,
},
},
},
want: &schema.PatchUserSchemaResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
{
name: "inactive, error",
prepare: func(request *schema.PatchUserSchemaRequest) error {
schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId()
_, err := Client.DeactivateUserSchema(IAMOwnerCTX, &schema.DeactivateUserSchemaRequest{
Id: schemaID,
})
require.NoError(t, err)
request.Id = schemaID
return nil
},
args: args{
ctx: IAMOwnerCTX,
req: &schema.PatchUserSchemaRequest{
Type: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.PatchUserSchema(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want.GetDetails(), got.GetDetails())
})
}
}
func TestServer_DeactivateUserSchema(t *testing.T) {
ensureFeatureEnabled(t, IAMOwnerCTX)
type args struct {
ctx context.Context
req *schema.DeactivateUserSchemaRequest
prepare func(request *schema.DeactivateUserSchemaRequest) error
}
tests := []struct {
name string
args args
want *schema.DeactivateUserSchemaResponse
wantErr bool
}{
{
name: "not existing, error",
args: args{
IAMOwnerCTX,
&schema.DeactivateUserSchemaRequest{
Id: "notexisting",
},
func(request *schema.DeactivateUserSchemaRequest) error { return nil },
},
wantErr: true,
},
{
name: "active, ok",
args: args{
IAMOwnerCTX,
&schema.DeactivateUserSchemaRequest{},
func(request *schema.DeactivateUserSchemaRequest) error {
schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId()
request.Id = schemaID
return nil
},
},
want: &schema.DeactivateUserSchemaResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
{
name: "inactive, error",
args: args{
IAMOwnerCTX,
&schema.DeactivateUserSchemaRequest{},
func(request *schema.DeactivateUserSchemaRequest) error {
schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId()
request.Id = schemaID
_, err := Client.DeactivateUserSchema(IAMOwnerCTX, &schema.DeactivateUserSchemaRequest{
Id: schemaID,
})
return err
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.DeactivateUserSchema(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want.GetDetails(), got.GetDetails())
})
}
}
func TestServer_ReactivateUserSchema(t *testing.T) {
ensureFeatureEnabled(t, IAMOwnerCTX)
type args struct {
ctx context.Context
req *schema.ReactivateUserSchemaRequest
prepare func(request *schema.ReactivateUserSchemaRequest) error
}
tests := []struct {
name string
args args
want *schema.ReactivateUserSchemaResponse
wantErr bool
}{
{
name: "not existing, error",
args: args{
IAMOwnerCTX,
&schema.ReactivateUserSchemaRequest{
Id: "notexisting",
},
func(request *schema.ReactivateUserSchemaRequest) error { return nil },
},
wantErr: true,
},
{
name: "active, error",
args: args{
ctx: IAMOwnerCTX,
req: &schema.ReactivateUserSchemaRequest{},
prepare: func(request *schema.ReactivateUserSchemaRequest) error {
schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId()
request.Id = schemaID
return nil
},
},
wantErr: true,
},
{
name: "inactive, ok",
args: args{
ctx: IAMOwnerCTX,
req: &schema.ReactivateUserSchemaRequest{},
prepare: func(request *schema.ReactivateUserSchemaRequest) error {
schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId()
request.Id = schemaID
_, err := Client.DeactivateUserSchema(IAMOwnerCTX, &schema.DeactivateUserSchemaRequest{
Id: schemaID,
})
return err
},
},
want: &schema.ReactivateUserSchemaResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.ReactivateUserSchema(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want.GetDetails(), got.GetDetails())
})
}
}
func TestServer_DeleteUserSchema(t *testing.T) {
ensureFeatureEnabled(t, IAMOwnerCTX)
type args struct {
ctx context.Context
req *schema.DeleteUserSchemaRequest
prepare func(request *schema.DeleteUserSchemaRequest) error
}
tests := []struct {
name string
args args
want *schema.DeleteUserSchemaResponse
wantErr bool
}{
{
name: "not existing, error",
args: args{
IAMOwnerCTX,
&schema.DeleteUserSchemaRequest{
Id: "notexisting",
},
func(request *schema.DeleteUserSchemaRequest) error { return nil },
},
wantErr: true,
},
{
name: "delete, ok",
args: args{
ctx: IAMOwnerCTX,
req: &schema.DeleteUserSchemaRequest{},
prepare: func(request *schema.DeleteUserSchemaRequest) error {
schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId()
request.Id = schemaID
return nil
},
},
want: &schema.DeleteUserSchemaResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
{
name: "deleted, error",
args: args{
ctx: IAMOwnerCTX,
req: &schema.DeleteUserSchemaRequest{},
prepare: func(request *schema.DeleteUserSchemaRequest) error {
schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId()
request.Id = schemaID
_, err := Client.DeleteUserSchema(IAMOwnerCTX, &schema.DeleteUserSchemaRequest{
Id: schemaID,
})
return err
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.DeleteUserSchema(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want.GetDetails(), got.GetDetails())
})
}
}

View File

@ -1,386 +0,0 @@
package schema
import (
"context"
"google.golang.org/protobuf/types/known/structpb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha"
)
func (s *Server) CreateUserSchema(ctx context.Context, req *schema.CreateUserSchemaRequest) (*schema.CreateUserSchemaResponse, error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
userSchema, err := createUserSchemaToCommand(req, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
id, details, err := s.command.CreateUserSchema(ctx, userSchema)
if err != nil {
return nil, err
}
return &schema.CreateUserSchemaResponse{
Id: id,
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) UpdateUserSchema(ctx context.Context, req *schema.UpdateUserSchemaRequest) (*schema.UpdateUserSchemaResponse, error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
userSchema, err := updateUserSchemaToCommand(req, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
details, err := s.command.UpdateUserSchema(ctx, userSchema)
if err != nil {
return nil, err
}
return &schema.UpdateUserSchemaResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) DeactivateUserSchema(ctx context.Context, req *schema.DeactivateUserSchemaRequest) (*schema.DeactivateUserSchemaResponse, error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
details, err := s.command.DeactivateUserSchema(ctx, req.GetId(), authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
return &schema.DeactivateUserSchemaResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) ReactivateUserSchema(ctx context.Context, req *schema.ReactivateUserSchemaRequest) (*schema.ReactivateUserSchemaResponse, error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
details, err := s.command.ReactivateUserSchema(ctx, req.GetId(), authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
return &schema.ReactivateUserSchemaResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) DeleteUserSchema(ctx context.Context, req *schema.DeleteUserSchemaRequest) (*schema.DeleteUserSchemaResponse, error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
details, err := s.command.DeleteUserSchema(ctx, req.GetId(), authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
return &schema.DeleteUserSchemaResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) ListUserSchemas(ctx context.Context, req *schema.ListUserSchemasRequest) (*schema.ListUserSchemasResponse, error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
queries, err := listUserSchemaToQuery(req)
if err != nil {
return nil, err
}
res, err := s.query.SearchUserSchema(ctx, queries)
if err != nil {
return nil, err
}
userSchemas, err := userSchemasToPb(res.UserSchemas)
if err != nil {
return nil, err
}
return &schema.ListUserSchemasResponse{
Details: object.ToListDetails(res.SearchResponse),
Result: userSchemas,
}, nil
}
func (s *Server) GetUserSchemaByID(ctx context.Context, req *schema.GetUserSchemaByIDRequest) (*schema.GetUserSchemaByIDResponse, error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
res, err := s.query.GetUserSchemaByID(ctx, req.GetId())
if err != nil {
return nil, err
}
userSchema, err := userSchemaToPb(res)
if err != nil {
return nil, err
}
return &schema.GetUserSchemaByIDResponse{
Schema: userSchema,
}, nil
}
func userSchemasToPb(schemas []*query.UserSchema) (_ []*schema.UserSchema, err error) {
userSchemas := make([]*schema.UserSchema, len(schemas))
for i, userSchema := range schemas {
userSchemas[i], err = userSchemaToPb(userSchema)
if err != nil {
return nil, err
}
}
return userSchemas, nil
}
func userSchemaToPb(userSchema *query.UserSchema) (*schema.UserSchema, error) {
s := new(structpb.Struct)
if err := s.UnmarshalJSON(userSchema.Schema); err != nil {
return nil, err
}
return &schema.UserSchema{
Id: userSchema.ID,
Details: object.DomainToDetailsPb(&userSchema.ObjectDetails),
Type: userSchema.Type,
State: userSchemaStateToPb(userSchema.State),
Revision: userSchema.Revision,
Schema: s,
PossibleAuthenticators: authenticatorTypesToPb(userSchema.PossibleAuthenticators),
}, nil
}
func authenticatorTypesToPb(authenticators []domain.AuthenticatorType) []schema.AuthenticatorType {
authTypes := make([]schema.AuthenticatorType, len(authenticators))
for i, authenticator := range authenticators {
authTypes[i] = authenticatorTypeToPb(authenticator)
}
return authTypes
}
func authenticatorTypeToPb(authenticator domain.AuthenticatorType) schema.AuthenticatorType {
switch authenticator {
case domain.AuthenticatorTypeUsername:
return schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME
case domain.AuthenticatorTypePassword:
return schema.AuthenticatorType_AUTHENTICATOR_TYPE_PASSWORD
case domain.AuthenticatorTypeWebAuthN:
return schema.AuthenticatorType_AUTHENTICATOR_TYPE_WEBAUTHN
case domain.AuthenticatorTypeTOTP:
return schema.AuthenticatorType_AUTHENTICATOR_TYPE_TOTP
case domain.AuthenticatorTypeOTPEmail:
return schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_EMAIL
case domain.AuthenticatorTypeOTPSMS:
return schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_SMS
case domain.AuthenticatorTypeAuthenticationKey:
return schema.AuthenticatorType_AUTHENTICATOR_TYPE_AUTHENTICATION_KEY
case domain.AuthenticatorTypeIdentityProvider:
return schema.AuthenticatorType_AUTHENTICATOR_TYPE_IDENTITY_PROVIDER
case domain.AuthenticatorTypeUnspecified:
return schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED
default:
return schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED
}
}
func userSchemaStateToPb(state domain.UserSchemaState) schema.State {
switch state {
case domain.UserSchemaStateActive:
return schema.State_STATE_ACTIVE
case domain.UserSchemaStateInactive:
return schema.State_STATE_INACTIVE
case domain.UserSchemaStateUnspecified,
domain.UserSchemaStateDeleted:
return schema.State_STATE_UNSPECIFIED
default:
return schema.State_STATE_UNSPECIFIED
}
}
func listUserSchemaToQuery(req *schema.ListUserSchemasRequest) (*query.UserSchemaSearchQueries, error) {
offset, limit, asc := object.ListQueryToQuery(req.Query)
queries, err := userSchemaQueriesToQuery(req.Queries, 0) // start at level 0
if err != nil {
return nil, err
}
return &query.UserSchemaSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: userSchemaFieldNameToSortingColumn(req.SortingColumn),
},
Queries: queries,
}, nil
}
func userSchemaFieldNameToSortingColumn(column schema.FieldName) query.Column {
switch column {
case schema.FieldName_FIELD_NAME_TYPE:
return query.UserSchemaTypeCol
case schema.FieldName_FIELD_NAME_STATE:
return query.UserSchemaStateCol
case schema.FieldName_FIELD_NAME_REVISION:
return query.UserSchemaRevisionCol
case schema.FieldName_FIELD_NAME_CHANGE_DATE:
return query.UserSchemaChangeDateCol
case schema.FieldName_FIELD_NAME_UNSPECIFIED:
return query.UserSchemaIDCol
default:
return query.UserSchemaIDCol
}
}
func checkUserSchemaEnabled(ctx context.Context) error {
if authz.GetInstance(ctx).Features().UserSchema {
return nil
}
return zerrors.ThrowPreconditionFailed(nil, "SCHEMA-SFjk3", "Errors.UserSchema.NotEnabled")
}
func createUserSchemaToCommand(req *schema.CreateUserSchemaRequest, resourceOwner string) (*command.CreateUserSchema, error) {
schema, err := req.GetSchema().MarshalJSON()
if err != nil {
return nil, err
}
return &command.CreateUserSchema{
ResourceOwner: resourceOwner,
Type: req.GetType(),
Schema: schema,
PossibleAuthenticators: authenticatorsToDomain(req.GetPossibleAuthenticators()),
}, nil
}
func updateUserSchemaToCommand(req *schema.UpdateUserSchemaRequest, resourceOwner string) (*command.UpdateUserSchema, error) {
schema, err := req.GetSchema().MarshalJSON()
if err != nil {
return nil, err
}
return &command.UpdateUserSchema{
ID: req.GetId(),
ResourceOwner: resourceOwner,
Type: req.Type,
Schema: schema,
PossibleAuthenticators: authenticatorsToDomain(req.GetPossibleAuthenticators()),
}, nil
}
func authenticatorsToDomain(authenticators []schema.AuthenticatorType) []domain.AuthenticatorType {
types := make([]domain.AuthenticatorType, len(authenticators))
for i, authenticator := range authenticators {
types[i] = authenticatorTypeToDomain(authenticator)
}
return types
}
func authenticatorTypeToDomain(authenticator schema.AuthenticatorType) domain.AuthenticatorType {
switch authenticator {
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED:
return domain.AuthenticatorTypeUnspecified
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME:
return domain.AuthenticatorTypeUsername
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_PASSWORD:
return domain.AuthenticatorTypePassword
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_WEBAUTHN:
return domain.AuthenticatorTypeWebAuthN
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_TOTP:
return domain.AuthenticatorTypeTOTP
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_EMAIL:
return domain.AuthenticatorTypeOTPEmail
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_SMS:
return domain.AuthenticatorTypeOTPSMS
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_AUTHENTICATION_KEY:
return domain.AuthenticatorTypeAuthenticationKey
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_IDENTITY_PROVIDER:
return domain.AuthenticatorTypeIdentityProvider
default:
return domain.AuthenticatorTypeUnspecified
}
}
func userSchemaStateToDomain(state schema.State) domain.UserSchemaState {
switch state {
case schema.State_STATE_ACTIVE:
return domain.UserSchemaStateActive
case schema.State_STATE_INACTIVE:
return domain.UserSchemaStateInactive
case schema.State_STATE_UNSPECIFIED:
return domain.UserSchemaStateUnspecified
default:
return domain.UserSchemaStateUnspecified
}
}
func userSchemaQueriesToQuery(queries []*schema.SearchQuery, level uint8) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = userSchemaQueryToQuery(query, level)
if err != nil {
return nil, err
}
}
return q, nil
}
func userSchemaQueryToQuery(query *schema.SearchQuery, level uint8) (query.SearchQuery, error) {
if level > 20 {
// can't go deeper than 20 levels of nesting.
return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-zsQ97", "Errors.Query.TooManyNestingLevels")
}
switch q := query.Query.(type) {
case *schema.SearchQuery_StateQuery:
return stateQueryToQuery(q.StateQuery)
case *schema.SearchQuery_TypeQuery:
return typeQueryToQuery(q.TypeQuery)
case *schema.SearchQuery_IdQuery:
return idQueryToQuery(q.IdQuery)
case *schema.SearchQuery_OrQuery:
return orQueryToQuery(q.OrQuery, level)
case *schema.SearchQuery_AndQuery:
return andQueryToQuery(q.AndQuery, level)
case *schema.SearchQuery_NotQuery:
return notQueryToQuery(q.NotQuery, level)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-vR9nC", "List.Query.Invalid")
}
}
func stateQueryToQuery(q *schema.StateQuery) (query.SearchQuery, error) {
return query.NewUserSchemaStateSearchQuery(userSchemaStateToDomain(q.GetState()))
}
func typeQueryToQuery(q *schema.TypeQuery) (query.SearchQuery, error) {
return query.NewUserSchemaTypeSearchQuery(q.GetType(), object.TextMethodToQuery(q.GetMethod()))
}
func idQueryToQuery(q *schema.IDQuery) (query.SearchQuery, error) {
return query.NewUserSchemaIDSearchQuery(q.GetId(), object.TextMethodToQuery(q.GetMethod()))
}
func orQueryToQuery(q *schema.OrQuery, level uint8) (query.SearchQuery, error) {
mappedQueries, err := userSchemaQueriesToQuery(q.GetQueries(), level+1)
if err != nil {
return nil, err
}
return query.NewUserOrSearchQuery(mappedQueries)
}
func andQueryToQuery(q *schema.AndQuery, level uint8) (query.SearchQuery, error) {
mappedQueries, err := userSchemaQueriesToQuery(q.GetQueries(), level+1)
if err != nil {
return nil, err
}
return query.NewUserAndSearchQuery(mappedQueries)
}
func notQueryToQuery(q *schema.NotQuery, level uint8) (query.SearchQuery, error) {
mappedQuery, err := userSchemaQueryToQuery(q.GetQuery(), level+1)
if err != nil {
return nil, err
}
return query.NewUserNotSearchQuery(mappedQuery)
}

View File

@ -1,51 +0,0 @@
package schema
import (
"google.golang.org/grpc"
"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/query"
schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha"
)
var _ schema.UserSchemaServiceServer = (*Server)(nil)
type Server struct {
schema.UnimplementedUserSchemaServiceServer
command *command.Commands
query *query.Queries
}
type Config struct{}
func CreateServer(
command *command.Commands,
query *query.Queries,
) *Server {
return &Server{
command: command,
query: query,
}
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
schema.RegisterUserSchemaServiceServer(grpcServer, s)
}
func (s *Server) AppName() string {
return schema.UserSchemaService_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return schema.UserSchemaService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return schema.UserSchemaService_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return schema.RegisterUserSchemaServiceHandler
}

View File

@ -12,6 +12,8 @@ import (
)
type CreateUserSchema struct {
Details *domain.ObjectDetails
ResourceOwner string
Type string
Schema json.RawMessage
@ -33,7 +35,9 @@ func (s *CreateUserSchema) Valid() error {
return nil
}
type UpdateUserSchema struct {
type ChangeUserSchema struct {
Details *domain.ObjectDetails
ID string
ResourceOwner string
Type *string
@ -41,7 +45,7 @@ type UpdateUserSchema struct {
PossibleAuthenticators []domain.AuthenticatorType
}
func (s *UpdateUserSchema) Valid() error {
func (s *ChangeUserSchema) Valid() error {
if s.ID == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMA-H5421", "Errors.IDMissing")
}
@ -59,40 +63,43 @@ func (s *UpdateUserSchema) Valid() error {
return nil
}
func (c *Commands) CreateUserSchema(ctx context.Context, userSchema *CreateUserSchema) (string, *domain.ObjectDetails, error) {
func (c *Commands) CreateUserSchema(ctx context.Context, userSchema *CreateUserSchema) error {
if err := userSchema.Valid(); err != nil {
return "", nil, err
return err
}
if userSchema.ResourceOwner == "" {
return "", nil, zerrors.ThrowInvalidArgument(nil, "COMMA-J3hhj", "Errors.ResourceOwnerMissing")
return zerrors.ThrowInvalidArgument(nil, "COMMA-J3hhj", "Errors.ResourceOwnerMissing")
}
id, err := c.idGenerator.Next()
if err != nil {
return "", nil, err
return err
}
writeModel := NewUserSchemaWriteModel(id, userSchema.ResourceOwner)
err = c.pushAppendAndReduce(ctx, writeModel,
writeModel, err := c.getSchemaWriteModelByID(ctx, userSchema.ResourceOwner, id)
if err != nil {
return err
}
if err := c.pushAppendAndReduce(ctx, writeModel,
schema.NewCreatedEvent(ctx,
UserSchemaAggregateFromWriteModel(&writeModel.WriteModel),
userSchema.Type, userSchema.Schema, userSchema.PossibleAuthenticators,
),
)
if err != nil {
return "", nil, err
); err != nil {
return err
}
return id, writeModelToObjectDetails(&writeModel.WriteModel), nil
userSchema.Details = writeModelToObjectDetails(&writeModel.WriteModel)
return nil
}
func (c *Commands) UpdateUserSchema(ctx context.Context, userSchema *UpdateUserSchema) (*domain.ObjectDetails, error) {
func (c *Commands) ChangeUserSchema(ctx context.Context, userSchema *ChangeUserSchema) error {
if err := userSchema.Valid(); err != nil {
return nil, err
return err
}
writeModel := NewUserSchemaWriteModel(userSchema.ID, userSchema.ResourceOwner)
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
return nil, err
writeModel, err := c.getSchemaWriteModelByID(ctx, userSchema.ResourceOwner, userSchema.ID)
if err != nil {
return err
}
if writeModel.State != domain.UserSchemaStateActive {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-HB3e1", "Errors.UserSchema.NotActive")
return zerrors.ThrowPreconditionFailed(nil, "COMMA-HB3e1", "Errors.UserSchema.NotActive")
}
updatedEvent := writeModel.NewUpdatedEvent(
ctx,
@ -102,29 +109,30 @@ func (c *Commands) UpdateUserSchema(ctx context.Context, userSchema *UpdateUserS
userSchema.PossibleAuthenticators,
)
if updatedEvent == nil {
return writeModelToObjectDetails(&writeModel.WriteModel), nil
userSchema.Details = writeModelToObjectDetails(&writeModel.WriteModel)
return nil
}
if err := c.pushAppendAndReduce(ctx, writeModel, updatedEvent); err != nil {
return nil, err
return err
}
return writeModelToObjectDetails(&writeModel.WriteModel), nil
userSchema.Details = writeModelToObjectDetails(&writeModel.WriteModel)
return nil
}
func (c *Commands) DeactivateUserSchema(ctx context.Context, id, resourceOwner string) (*domain.ObjectDetails, error) {
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-Vvf3w", "Errors.IDMissing")
}
writeModel := NewUserSchemaWriteModel(id, resourceOwner)
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
writeModel, err := c.getSchemaWriteModelByID(ctx, resourceOwner, id)
if err != nil {
return nil, err
}
if writeModel.State != domain.UserSchemaStateActive {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-E4t4z", "Errors.UserSchema.NotActive")
}
err := c.pushAppendAndReduce(ctx, writeModel,
if err := c.pushAppendAndReduce(ctx, writeModel,
schema.NewDeactivatedEvent(ctx, UserSchemaAggregateFromWriteModel(&writeModel.WriteModel)),
)
if err != nil {
); err != nil {
return nil, err
}
return writeModelToObjectDetails(&writeModel.WriteModel), nil
@ -134,17 +142,16 @@ func (c *Commands) ReactivateUserSchema(ctx context.Context, id, resourceOwner s
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-wq3Gw", "Errors.IDMissing")
}
writeModel := NewUserSchemaWriteModel(id, resourceOwner)
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
writeModel, err := c.getSchemaWriteModelByID(ctx, resourceOwner, id)
if err != nil {
return nil, err
}
if writeModel.State != domain.UserSchemaStateInactive {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-DGzh5", "Errors.UserSchema.NotInactive")
}
err := c.pushAppendAndReduce(ctx, writeModel,
if err := c.pushAppendAndReduce(ctx, writeModel,
schema.NewReactivatedEvent(ctx, UserSchemaAggregateFromWriteModel(&writeModel.WriteModel)),
)
if err != nil {
); err != nil {
return nil, err
}
return writeModelToObjectDetails(&writeModel.WriteModel), nil
@ -154,18 +161,17 @@ func (c *Commands) DeleteUserSchema(ctx context.Context, id, resourceOwner strin
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-E22gg", "Errors.IDMissing")
}
writeModel := NewUserSchemaWriteModel(id, resourceOwner)
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
writeModel, err := c.getSchemaWriteModelByID(ctx, resourceOwner, id)
if err != nil {
return nil, err
}
if !writeModel.Exists() {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-Grg41", "Errors.UserSchema.NotExists")
}
// TODO: check for users based on that schema; this is only possible with / after https://github.com/zitadel/zitadel/issues/7308
err := c.pushAppendAndReduce(ctx, writeModel,
if err := c.pushAppendAndReduce(ctx, writeModel,
schema.NewDeletedEvent(ctx, UserSchemaAggregateFromWriteModel(&writeModel.WriteModel), writeModel.SchemaType),
)
if err != nil {
); err != nil {
return nil, err
}
return writeModelToObjectDetails(&writeModel.WriteModel), nil
@ -178,3 +184,11 @@ func validateUserSchema(userSchema json.RawMessage) error {
}
return nil
}
func (c *Commands) getSchemaWriteModelByID(ctx context.Context, resourceOwner, id string) (*UserSchemaWriteModel, error) {
writeModel := NewUserSchemaWriteModel(resourceOwner, id, "")
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
return nil, err
}
return writeModel, nil
}

View File

@ -19,14 +19,16 @@ type UserSchemaWriteModel struct {
Schema json.RawMessage
PossibleAuthenticators []domain.AuthenticatorType
State domain.UserSchemaState
Revision uint64
}
func NewUserSchemaWriteModel(schemaID, resourceOwner string) *UserSchemaWriteModel {
func NewUserSchemaWriteModel(resourceOwner, schemaID, ty string) *UserSchemaWriteModel {
return &UserSchemaWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: schemaID,
ResourceOwner: resourceOwner,
},
SchemaType: ty,
}
}
@ -38,10 +40,14 @@ func (wm *UserSchemaWriteModel) Reduce() error {
wm.Schema = e.Schema
wm.PossibleAuthenticators = e.PossibleAuthenticators
wm.State = domain.UserSchemaStateActive
wm.Revision = 1
case *schema.UpdatedEvent:
if e.SchemaType != nil {
wm.SchemaType = *e.SchemaType
}
if e.SchemaRevision != nil {
wm.Revision = *e.SchemaRevision
}
if len(e.Schema) > 0 {
wm.Schema = e.Schema
}
@ -60,7 +66,7 @@ func (wm *UserSchemaWriteModel) Reduce() error {
}
func (wm *UserSchemaWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(schema.AggregateType).
@ -71,8 +77,13 @@ func (wm *UserSchemaWriteModel) Query() *eventstore.SearchQueryBuilder {
schema.DeactivatedType,
schema.ReactivatedType,
schema.DeletedType,
).
Builder()
)
if wm.SchemaType != "" {
query = query.EventData(map[string]interface{}{"schemaType": wm.SchemaType})
}
return query.Builder()
}
func (wm *UserSchemaWriteModel) NewUpdatedEvent(
ctx context.Context,
@ -87,6 +98,8 @@ func (wm *UserSchemaWriteModel) NewUpdatedEvent(
}
if !bytes.Equal(wm.Schema, userSchema) {
changes = append(changes, schema.ChangeSchema(userSchema))
// change revision if the content of the schema changed
changes = append(changes, schema.IncreaseRevision(wm.Revision))
}
if len(possibleAuthenticators) > 0 && slices.Compare(wm.PossibleAuthenticators, possibleAuthenticators) != 0 {
changes = append(changes, schema.ChangePossibleAuthenticators(possibleAuthenticators))

View File

@ -27,7 +27,6 @@ func TestCommands_CreateUserSchema(t *testing.T) {
userSchema *CreateUserSchema
}
type res struct {
id string
details *domain.ObjectDetails
err error
}
@ -107,6 +106,7 @@ func TestCommands_CreateUserSchema(t *testing.T) {
"empty user schema created",
fields{
eventstore: expectEventstore(
expectFilter(),
expectPush(
schema.NewCreatedEvent(
context.Background(),
@ -131,8 +131,8 @@ func TestCommands_CreateUserSchema(t *testing.T) {
},
},
res{
id: "id1",
details: &domain.ObjectDetails{
ID: "id1",
ResourceOwner: "instanceID",
},
},
@ -141,6 +141,7 @@ func TestCommands_CreateUserSchema(t *testing.T) {
"user schema created",
fields{
eventstore: expectEventstore(
expectFilter(),
expectPush(
schema.NewCreatedEvent(
context.Background(),
@ -181,8 +182,8 @@ func TestCommands_CreateUserSchema(t *testing.T) {
},
},
res{
id: "id1",
details: &domain.ObjectDetails{
ID: "id1",
ResourceOwner: "instanceID",
},
},
@ -220,6 +221,7 @@ func TestCommands_CreateUserSchema(t *testing.T) {
"user schema with permission created",
fields{
eventstore: expectEventstore(
expectFilter(),
expectPush(
schema.NewCreatedEvent(
context.Background(),
@ -266,8 +268,8 @@ func TestCommands_CreateUserSchema(t *testing.T) {
},
},
res{
id: "id1",
details: &domain.ObjectDetails{
ID: "id1",
ResourceOwner: "instanceID",
},
},
@ -279,21 +281,20 @@ func TestCommands_CreateUserSchema(t *testing.T) {
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
}
gotID, gotDetails, err := c.CreateUserSchema(tt.args.ctx, tt.args.userSchema)
assert.Equal(t, tt.res.id, gotID)
assertObjectDetails(t, tt.res.details, gotDetails)
err := c.CreateUserSchema(tt.args.ctx, tt.args.userSchema)
assertObjectDetails(t, tt.res.details, tt.args.userSchema.Details)
assert.ErrorIs(t, err, tt.res.err)
})
}
}
func TestCommands_UpdateUserSchema(t *testing.T) {
func TestCommands_ChangeUserSchema(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
userSchema *UpdateUserSchema
userSchema *ChangeUserSchema
}
type res struct {
details *domain.ObjectDetails
@ -312,7 +313,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) {
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{},
userSchema: &ChangeUserSchema{},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-H5421", "Errors.IDMissing"),
@ -325,7 +326,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) {
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
userSchema: &ChangeUserSchema{
ID: "id1",
Type: gu.Ptr(""),
},
@ -341,7 +342,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) {
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
userSchema: &ChangeUserSchema{
ID: "id1",
},
},
@ -356,7 +357,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) {
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
userSchema: &ChangeUserSchema{
ID: "id1",
Schema: json.RawMessage(`{
"properties": {
@ -379,7 +380,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) {
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
userSchema: &ChangeUserSchema{
ID: "id1",
Schema: json.RawMessage(`{}`),
PossibleAuthenticators: []domain.AuthenticatorType{
@ -400,7 +401,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) {
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
userSchema: &ChangeUserSchema{
ID: "id1",
Type: gu.Ptr("type"),
Schema: json.RawMessage(`{}`),
@ -432,7 +433,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) {
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
userSchema: &ChangeUserSchema{
ID: "id1",
Type: gu.Ptr("type"),
Schema: json.RawMessage(`{}`),
@ -473,7 +474,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) {
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
userSchema: &ChangeUserSchema{
ID: "id1",
Schema: json.RawMessage(`{}`),
Type: gu.Ptr("newType"),
@ -515,7 +516,9 @@ func TestCommands_UpdateUserSchema(t *testing.T) {
schema.NewUpdatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
[]schema.Changes{schema.ChangeSchema(json.RawMessage(`{
[]schema.Changes{
schema.IncreaseRevision(1),
schema.ChangeSchema(json.RawMessage(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
@ -539,7 +542,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) {
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
userSchema: &ChangeUserSchema{
ID: "id1",
Schema: json.RawMessage(`{
"$schema": "urn:zitadel:schema:v1",
@ -597,7 +600,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) {
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
userSchema: &ChangeUserSchema{
ID: "id1",
Schema: json.RawMessage(`{}`),
PossibleAuthenticators: []domain.AuthenticatorType{
@ -618,9 +621,9 @@ func TestCommands_UpdateUserSchema(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore(t),
}
got, err := c.UpdateUserSchema(tt.args.ctx, tt.args.userSchema)
err := c.ChangeUserSchema(tt.args.ctx, tt.args.userSchema)
assert.ErrorIs(t, err, tt.res.err)
assertObjectDetails(t, tt.res.details, got)
assertObjectDetails(t, tt.res.details, tt.args.userSchema.Details)
})
}
}

220
internal/command/user_v3.go Normal file
View File

@ -0,0 +1,220 @@
package command
import (
"bytes"
"context"
"encoding/json"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
domain_schema "github.com/zitadel/zitadel/internal/domain/schema"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user/schemauser"
"github.com/zitadel/zitadel/internal/zerrors"
)
type CreateSchemaUser struct {
Details *domain.ObjectDetails
ResourceOwner string
SchemaID string
schemaRevision uint64
ID string
Data json.RawMessage
Email *Email
ReturnCodeEmail string
Phone *Phone
ReturnCodePhone string
}
func (s *CreateSchemaUser) Valid(ctx context.Context, c *Commands) (err error) {
if s.ResourceOwner == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-urEJKa1tJM", "Errors.ResourceOwnerMissing")
}
if s.SchemaID == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-TFo06JgnF2", "Errors.UserSchema.ID.Missing")
}
schemaWriteModel, err := c.getSchemaWriteModelByID(ctx, "", s.SchemaID)
if err != nil {
return err
}
if !schemaWriteModel.Exists() {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-N9QOuN4F7o", "Errors.UserSchema.NotExists")
}
s.schemaRevision = schemaWriteModel.Revision
if s.ID == "" {
s.ID, err = c.idGenerator.Next()
if err != nil {
return err
}
}
// get role for permission check in schema through extension
role, err := c.getSchemaRoleForWrite(ctx, s.ResourceOwner, s.ID)
if err != nil {
return err
}
schema, err := domain_schema.NewSchema(role, bytes.NewReader(schemaWriteModel.Schema))
if err != nil {
return err
}
var v interface{}
if err := json.Unmarshal(s.Data, &v); err != nil {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-7o3ZGxtXUz", "Errors.User.Invalid")
}
if err := schema.Validate(v); err != nil {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-SlKXqLSeL6", "Errors.UserSchema.Data.Invalid")
}
if s.Email != nil && s.Email.Address != "" {
if err := s.Email.Validate(); err != nil {
return err
}
}
if s.Phone != nil && s.Phone.Number != "" {
if s.Phone.Number, err = s.Phone.Number.Normalize(); err != nil {
return err
}
}
return nil
}
func (c *Commands) getSchemaRoleForWrite(ctx context.Context, resourceOwner, userID string) (domain_schema.Role, error) {
if userID == authz.GetCtxData(ctx).UserID {
return domain_schema.RoleSelf, nil
}
if err := c.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, userID); err != nil {
return domain_schema.RoleUnspecified, err
}
return domain_schema.RoleOwner, nil
}
func (c *Commands) CreateSchemaUser(ctx context.Context, user *CreateSchemaUser, alg crypto.EncryptionAlgorithm) (err error) {
if err := user.Valid(ctx, c); err != nil {
return err
}
writeModel, err := c.getSchemaUserExists(ctx, user.ResourceOwner, user.ID)
if err != nil {
return err
}
if writeModel.Exists() {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-Nn8CRVlkeZ", "Errors.User.AlreadyExists")
}
userAgg := UserV3AggregateFromWriteModel(&writeModel.WriteModel)
events := []eventstore.Command{
schemauser.NewCreatedEvent(ctx,
userAgg,
user.SchemaID, user.schemaRevision, user.Data,
),
}
if user.Email != nil {
events, user.ReturnCodeEmail, err = c.updateSchemaUserEmail(ctx, events, userAgg, user.Email, alg)
if err != nil {
return err
}
}
if user.Phone != nil {
events, user.ReturnCodePhone, err = c.updateSchemaUserPhone(ctx, events, userAgg, user.Phone, alg)
if err != nil {
return err
}
}
if err := c.pushAppendAndReduce(ctx, writeModel, events...); err != nil {
return err
}
user.Details = writeModelToObjectDetails(&writeModel.WriteModel)
return nil
}
func (c *Commands) DeleteSchemaUser(ctx context.Context, id string) (*domain.ObjectDetails, error) {
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Vs4wJCME7T", "Errors.IDMissing")
}
writeModel, err := c.getSchemaUserExists(ctx, "", id)
if err != nil {
return nil, err
}
if !writeModel.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-syHyCsGmvM", "Errors.User.NotFound")
}
if err := c.checkPermissionDeleteUser(ctx, writeModel.ResourceOwner, writeModel.AggregateID); err != nil {
return nil, err
}
if err := c.pushAppendAndReduce(ctx, writeModel,
schemauser.NewDeletedEvent(ctx, UserV3AggregateFromWriteModel(&writeModel.WriteModel)),
); err != nil {
return nil, err
}
return writeModelToObjectDetails(&writeModel.WriteModel), nil
}
func (c *Commands) updateSchemaUserEmail(ctx context.Context, events []eventstore.Command, agg *eventstore.Aggregate, email *Email, alg crypto.EncryptionAlgorithm) (_ []eventstore.Command, plainCode string, err error) {
events = append(events, schemauser.NewEmailUpdatedEvent(ctx,
agg,
email.Address,
))
if email.Verified {
events = append(events, schemauser.NewEmailVerifiedEvent(ctx, agg))
} else {
cryptoCode, err := c.newEmailCode(ctx, c.eventstore.Filter, alg) //nolint:staticcheck
if err != nil {
return nil, "", err
}
if email.ReturnCode {
plainCode = cryptoCode.Plain
}
events = append(events, schemauser.NewEmailCodeAddedEvent(ctx, agg,
cryptoCode.Crypted,
cryptoCode.Expiry,
email.URLTemplate,
email.ReturnCode,
))
}
return events, plainCode, nil
}
func (c *Commands) updateSchemaUserPhone(ctx context.Context, events []eventstore.Command, agg *eventstore.Aggregate, phone *Phone, alg crypto.EncryptionAlgorithm) (_ []eventstore.Command, plainCode string, err error) {
events = append(events, schemauser.NewPhoneChangedEvent(ctx,
agg,
phone.Number,
))
if phone.Verified {
events = append(events, schemauser.NewPhoneVerifiedEvent(ctx, agg))
} else {
cryptoCode, err := c.newPhoneCode(ctx, c.eventstore.Filter, alg) //nolint:staticcheck
if err != nil {
return nil, "", err
}
if phone.ReturnCode {
plainCode = cryptoCode.Plain
}
events = append(events, schemauser.NewPhoneCodeAddedEvent(ctx, agg,
cryptoCode.Crypted,
cryptoCode.Expiry,
phone.ReturnCode,
))
}
return events, plainCode, nil
}
func (c *Commands) getSchemaUserExists(ctx context.Context, resourceOwner, id string) (*UserV3WriteModel, error) {
writeModel := NewExistsUserV3WriteModel(resourceOwner, id)
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
return nil, err
}
return writeModel, nil
}

View File

@ -0,0 +1,174 @@
package command
import (
"bytes"
"context"
"encoding/json"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user/schemauser"
)
type UserV3WriteModel struct {
eventstore.WriteModel
PhoneWM bool
EmailWM bool
DataWM bool
SchemaID string
SchemaRevision uint64
Email string
IsEmailVerified bool
EmailVerifiedFailedCount int
Phone string
IsPhoneVerified bool
PhoneVerifiedFailedCount int
Data json.RawMessage
State domain.UserState
}
func NewExistsUserV3WriteModel(resourceOwner, userID string) *UserV3WriteModel {
return &UserV3WriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
PhoneWM: false,
EmailWM: false,
DataWM: false,
}
}
func NewUserV3WriteModel(resourceOwner, userID string) *UserV3WriteModel {
return &UserV3WriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
PhoneWM: true,
EmailWM: true,
DataWM: true,
}
}
func (wm *UserV3WriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *schemauser.CreatedEvent:
wm.SchemaID = e.SchemaID
wm.SchemaRevision = 1
wm.Data = e.Data
wm.State = domain.UserStateActive
case *schemauser.UpdatedEvent:
if e.SchemaID != nil {
wm.SchemaID = *e.SchemaID
}
if e.SchemaRevision != nil {
wm.SchemaRevision = *e.SchemaRevision
}
if len(e.Data) > 0 {
wm.Data = e.Data
}
case *schemauser.DeletedEvent:
wm.State = domain.UserStateDeleted
case *schemauser.EmailUpdatedEvent:
wm.Email = string(e.EmailAddress)
case *schemauser.EmailCodeAddedEvent:
wm.IsEmailVerified = false
wm.EmailVerifiedFailedCount = 0
case *schemauser.EmailVerifiedEvent:
wm.IsEmailVerified = true
wm.EmailVerifiedFailedCount = 0
case *schemauser.EmailVerificationFailedEvent:
wm.EmailVerifiedFailedCount += 1
case *schemauser.PhoneChangedEvent:
wm.Phone = string(e.PhoneNumber)
case *schemauser.PhoneCodeAddedEvent:
wm.IsPhoneVerified = false
wm.PhoneVerifiedFailedCount = 0
case *schemauser.PhoneVerifiedEvent:
wm.PhoneVerifiedFailedCount = 0
wm.IsPhoneVerified = true
case *schemauser.PhoneVerificationFailedEvent:
wm.PhoneVerifiedFailedCount += 1
}
}
return wm.WriteModel.Reduce()
}
func (wm *UserV3WriteModel) Query() *eventstore.SearchQueryBuilder {
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(schemauser.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
schemauser.CreatedType,
schemauser.DeletedType,
)
if wm.DataWM {
query = query.EventTypes(
schemauser.UpdatedType,
)
}
if wm.EmailWM {
query = query.EventTypes(
schemauser.EmailUpdatedType,
schemauser.EmailVerifiedType,
schemauser.EmailCodeAddedType,
schemauser.EmailVerificationFailedType,
)
}
if wm.PhoneWM {
query = query.EventTypes(
schemauser.PhoneUpdatedType,
schemauser.PhoneVerifiedType,
schemauser.PhoneCodeAddedType,
schemauser.PhoneVerificationFailedType,
)
}
return query.Builder()
}
func (wm *UserV3WriteModel) NewUpdatedEvent(
ctx context.Context,
agg *eventstore.Aggregate,
schemaID *string,
schemaRevision *uint64,
data json.RawMessage,
) *schemauser.UpdatedEvent {
changes := make([]schemauser.Changes, 0)
if schemaID != nil && wm.SchemaID != *schemaID {
changes = append(changes, schemauser.ChangeSchemaID(wm.SchemaID, *schemaID))
}
if schemaRevision != nil && wm.SchemaRevision != *schemaRevision {
changes = append(changes, schemauser.ChangeSchemaRevision(wm.SchemaRevision, *schemaRevision))
}
if !bytes.Equal(wm.Data, data) {
changes = append(changes, schemauser.ChangeData(data))
}
if len(changes) == 0 {
return nil
}
return schemauser.NewUpdatedEvent(ctx, agg, changes)
}
func UserV3AggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate {
return &eventstore.Aggregate{
ID: wm.AggregateID,
Type: schemauser.AggregateType,
ResourceOwner: wm.ResourceOwner,
InstanceID: wm.InstanceID,
Version: schemauser.AggregateVersion,
}
}
func (wm *UserV3WriteModel) Exists() bool {
return wm.State != domain.UserStateDeleted && wm.State != domain.UserStateUnspecified
}

File diff suppressed because it is too large Load Diff

View File

@ -20,16 +20,16 @@ const (
PermissionProperty = "urn:zitadel:schema:permission"
)
type role int32
type Role int32
const (
roleUnspecified role = iota
roleSelf
roleOwner
RoleUnspecified Role = iota
RoleSelf
RoleOwner
)
type permissionExtension struct {
role role
role Role
}
// Compile implements the [jsonschema.ExtCompiler] interface.
@ -57,14 +57,14 @@ func (c permissionExtension) Compile(ctx jsonschema.CompilerContext, m map[strin
return
}
default:
return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-GFjio", "invalid permission role")
return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-GFjio", "invalid permission Role")
}
}
return permissionExtensionConfig{c.role, perms}, nil
}
type permissionExtensionConfig struct {
role role
role Role
permissions *permissions
}
@ -72,17 +72,17 @@ type permissionExtensionConfig struct {
// It validates the fields of the json instance according to the permission schema.
func (s permissionExtensionConfig) Validate(ctx jsonschema.ValidationContext, v interface{}) error {
switch s.role {
case roleSelf:
case RoleSelf:
if s.permissions.self == nil || !s.permissions.self.write {
return ctx.Error("permission", "missing required permission")
}
return nil
case roleOwner:
case RoleOwner:
if s.permissions.owner == nil || !s.permissions.owner.write {
return ctx.Error("permission", "missing required permission")
}
return nil
case roleUnspecified:
case RoleUnspecified:
fallthrough
default:
return ctx.Error("permission", "missing required permission")

View File

@ -14,7 +14,7 @@ import (
func TestPermissionExtension(t *testing.T) {
type args struct {
role role
role Role
schema string
instance string
}
@ -83,7 +83,7 @@ func TestPermissionExtension(t *testing.T) {
},
},
{
"invalid role, compilation err",
"invalid Role, compilation err",
args{
schema: `{
"type": "object",
@ -98,13 +98,13 @@ func TestPermissionExtension(t *testing.T) {
}`,
},
want{
compilationErr: zerrors.ThrowInvalidArgument(nil, "SCHEMA-GFjio", "invalid permission role"),
compilationErr: zerrors.ThrowInvalidArgument(nil, "SCHEMA-GFjio", "invalid permission Role"),
},
},
{
"invalid permission self, validation err",
args{
role: roleSelf,
role: RoleSelf,
schema: `{
"type": "object",
"properties": {
@ -126,7 +126,7 @@ func TestPermissionExtension(t *testing.T) {
{
"invalid permission owner, validation err",
args{
role: roleOwner,
role: RoleOwner,
schema: `{
"type": "object",
"properties": {
@ -148,7 +148,7 @@ func TestPermissionExtension(t *testing.T) {
{
"valid permission self, ok",
args{
role: roleSelf,
role: RoleSelf,
schema: `{
"type": "object",
"properties": {
@ -170,7 +170,7 @@ func TestPermissionExtension(t *testing.T) {
{
"valid permission owner, ok",
args{
role: roleOwner,
role: RoleOwner,
schema: `{
"type": "object",
"properties": {
@ -190,9 +190,9 @@ func TestPermissionExtension(t *testing.T) {
},
},
{
"no role, validation err",
"no Role, validation err",
args{
role: roleUnspecified,
role: RoleUnspecified,
schema: `{
"type": "object",
"properties": {
@ -214,7 +214,7 @@ func TestPermissionExtension(t *testing.T) {
{
"no permission required, ok",
args{
role: roleSelf,
role: RoleSelf,
schema: `{
"type": "object",
"properties": {

View File

@ -19,7 +19,7 @@ const (
MetaSchemaID = "urn:zitadel:schema:v1"
)
func NewSchema(role role, r io.Reader) (*jsonschema.Schema, error) {
func NewSchema(role Role, r io.Reader) (*jsonschema.Schema, error) {
c := jsonschema.NewCompiler()
if err := c.AddResource(PermissionSchemaID, strings.NewReader(permissionJSON)); err != nil {
return nil, err
@ -31,11 +31,11 @@ func NewSchema(role role, r io.Reader) (*jsonschema.Schema, error) {
role,
})
if err := c.AddResource("schema.json", r); err != nil {
return nil, zerrors.ThrowInvalidArgument(err, "COMMA-Frh42", "Errors.UserSchema.Schema.Invalid")
return nil, zerrors.ThrowInvalidArgument(err, "COMMA-Frh42", "Errors.UserSchema.Invalid")
}
schema, err := c.Compile("schema.json")
if err != nil {
return nil, zerrors.ThrowInvalidArgument(err, "COMMA-W21tg", "Errors.UserSchema.Schema.Invalid")
return nil, zerrors.ThrowInvalidArgument(err, "COMMA-W21tg", "Errors.UserSchema.Invalid")
}
return schema, nil
}

View File

@ -5,7 +5,6 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
@ -378,7 +377,6 @@ func Test_CallTargets(t *testing.T) {
} else {
assert.NoError(t, err)
}
fmt.Println(respBody)
assert.Equal(t, tt.res.ret, respBody)
})
}

View File

@ -34,11 +34,14 @@ import (
idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2"
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
object_v3alpha "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
oidc_pb_v2beta "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
user_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
userschema_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha"
webkey_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
session_v2beta "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
@ -46,8 +49,7 @@ import (
settings_v2beta "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/system"
user_pb "github.com/zitadel/zitadel/pkg/grpc/user"
schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2"
user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
@ -57,7 +59,7 @@ type Client struct {
Mgmt mgmt.ManagementServiceClient
Auth auth.AuthServiceClient
UserV2beta user_v2beta.UserServiceClient
UserV2 user.UserServiceClient
UserV2 user_v2.UserServiceClient
SessionV2beta session_v2beta.SessionServiceClient
SessionV2 session.SessionServiceClient
SettingsV2beta settings_v2beta.SettingsServiceClient
@ -70,9 +72,10 @@ type Client struct {
ActionV3Alpha action.ZITADELActionsClient
FeatureV2beta feature_v2beta.FeatureServiceClient
FeatureV2 feature.FeatureServiceClient
UserSchemaV3 schema.UserSchemaServiceClient
UserSchemaV3 userschema_v3alpha.ZITADELUserSchemasClient
WebKeyV3Alpha webkey_v3alpha.ZITADELWebKeysClient
IDPv2 idp_pb.IdentityProviderServiceClient
UserV3Alpha user_v3alpha.ZITADELUsersClient
}
func newClient(cc *grpc.ClientConn) Client {
@ -82,7 +85,7 @@ func newClient(cc *grpc.ClientConn) Client {
Mgmt: mgmt.NewManagementServiceClient(cc),
Auth: auth.NewAuthServiceClient(cc),
UserV2beta: user_v2beta.NewUserServiceClient(cc),
UserV2: user.NewUserServiceClient(cc),
UserV2: user_v2.NewUserServiceClient(cc),
SessionV2beta: session_v2beta.NewSessionServiceClient(cc),
SessionV2: session.NewSessionServiceClient(cc),
SettingsV2beta: settings_v2beta.NewSettingsServiceClient(cc),
@ -95,9 +98,10 @@ func newClient(cc *grpc.ClientConn) Client {
ActionV3Alpha: action.NewZITADELActionsClient(cc),
FeatureV2beta: feature_v2beta.NewFeatureServiceClient(cc),
FeatureV2: feature.NewFeatureServiceClient(cc),
UserSchemaV3: schema.NewUserSchemaServiceClient(cc),
UserSchemaV3: userschema_v3alpha.NewZITADELUserSchemasClient(cc),
WebKeyV3Alpha: webkey_v3alpha.NewZITADELWebKeysClient(cc),
IDPv2: idp_pb.NewIdentityProviderServiceClient(cc),
UserV3Alpha: user_v3alpha.NewZITADELUsersClient(cc),
}
}
@ -148,29 +152,29 @@ func (t *Tester) UseIsolatedInstance(tt *testing.T, iamOwnerCtx, systemCtx conte
return primaryDomain, instanceId, adminUser.GetUserId(), t.updateInstanceAndOrg(newCtx, fmt.Sprintf("%s:%d", primaryDomain, t.Config.ExternalPort))
}
func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse {
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{
func (s *Tester) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserResponse {
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{
Organization: &object.Organization{
Org: &object.Organization_OrgId{
OrgId: s.Organisation.ID,
},
},
Profile: &user.SetHumanProfile{
Profile: &user_v2.SetHumanProfile{
GivenName: "Mickey",
FamilyName: "Mouse",
PreferredLanguage: gu.Ptr("nl"),
Gender: gu.Ptr(user.Gender_GENDER_MALE),
Gender: gu.Ptr(user_v2.Gender_GENDER_MALE),
},
Email: &user.SetHumanEmail{
Email: &user_v2.SetHumanEmail{
Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()),
Verification: &user.SetHumanEmail_ReturnCode{
ReturnCode: &user.ReturnEmailVerificationCode{},
Verification: &user_v2.SetHumanEmail_ReturnCode{
ReturnCode: &user_v2.ReturnEmailVerificationCode{},
},
},
Phone: &user.SetHumanPhone{
Phone: &user_v2.SetHumanPhone{
Phone: "+41791234567",
Verification: &user.SetHumanPhone_ReturnCode{
ReturnCode: &user.ReturnPhoneVerificationCode{},
Verification: &user_v2.SetHumanPhone_ReturnCode{
ReturnCode: &user_v2.ReturnPhoneVerificationCode{},
},
},
})
@ -178,23 +182,23 @@ func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse
return resp
}
func (s *Tester) CreateHumanUserNoPhone(ctx context.Context) *user.AddHumanUserResponse {
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{
func (s *Tester) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHumanUserResponse {
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{
Organization: &object.Organization{
Org: &object.Organization_OrgId{
OrgId: s.Organisation.ID,
},
},
Profile: &user.SetHumanProfile{
Profile: &user_v2.SetHumanProfile{
GivenName: "Mickey",
FamilyName: "Mouse",
PreferredLanguage: gu.Ptr("nl"),
Gender: gu.Ptr(user.Gender_GENDER_MALE),
Gender: gu.Ptr(user_v2.Gender_GENDER_MALE),
},
Email: &user.SetHumanEmail{
Email: &user_v2.SetHumanEmail{
Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()),
Verification: &user.SetHumanEmail_ReturnCode{
ReturnCode: &user.ReturnEmailVerificationCode{},
Verification: &user_v2.SetHumanEmail_ReturnCode{
ReturnCode: &user_v2.ReturnEmailVerificationCode{},
},
},
})
@ -202,29 +206,29 @@ func (s *Tester) CreateHumanUserNoPhone(ctx context.Context) *user.AddHumanUserR
return resp
}
func (s *Tester) CreateHumanUserWithTOTP(ctx context.Context, secret string) *user.AddHumanUserResponse {
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{
func (s *Tester) CreateHumanUserWithTOTP(ctx context.Context, secret string) *user_v2.AddHumanUserResponse {
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{
Organization: &object.Organization{
Org: &object.Organization_OrgId{
OrgId: s.Organisation.ID,
},
},
Profile: &user.SetHumanProfile{
Profile: &user_v2.SetHumanProfile{
GivenName: "Mickey",
FamilyName: "Mouse",
PreferredLanguage: gu.Ptr("nl"),
Gender: gu.Ptr(user.Gender_GENDER_MALE),
Gender: gu.Ptr(user_v2.Gender_GENDER_MALE),
},
Email: &user.SetHumanEmail{
Email: &user_v2.SetHumanEmail{
Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()),
Verification: &user.SetHumanEmail_ReturnCode{
ReturnCode: &user.ReturnEmailVerificationCode{},
Verification: &user_v2.SetHumanEmail_ReturnCode{
ReturnCode: &user_v2.ReturnEmailVerificationCode{},
},
},
Phone: &user.SetHumanPhone{
Phone: &user_v2.SetHumanPhone{
Phone: "+41791234567",
Verification: &user.SetHumanPhone_ReturnCode{
ReturnCode: &user.ReturnPhoneVerificationCode{},
Verification: &user_v2.SetHumanPhone_ReturnCode{
ReturnCode: &user_v2.ReturnPhoneVerificationCode{},
},
},
TotpSecret: gu.Ptr(secret),
@ -239,15 +243,15 @@ func (s *Tester) CreateOrganization(ctx context.Context, name, adminEmail string
Admins: []*org.AddOrganizationRequest_Admin{
{
UserType: &org.AddOrganizationRequest_Admin_Human{
Human: &user.AddHumanUserRequest{
Profile: &user.SetHumanProfile{
Human: &user_v2.AddHumanUserRequest{
Profile: &user_v2.SetHumanProfile{
GivenName: "firstname",
FamilyName: "lastname",
},
Email: &user.SetHumanEmail{
Email: &user_v2.SetHumanEmail{
Email: adminEmail,
Verification: &user.SetHumanEmail_ReturnCode{
ReturnCode: &user.ReturnEmailVerificationCode{},
Verification: &user_v2.SetHumanEmail_ReturnCode{
ReturnCode: &user_v2.ReturnEmailVerificationCode{},
},
},
},
@ -292,29 +296,29 @@ func (s *Tester) CreateOrganizationWithUserID(ctx context.Context, name, userID
return resp
}
func (s *Tester) CreateHumanUserVerified(ctx context.Context, org, email string) *user.AddHumanUserResponse {
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{
func (s *Tester) CreateHumanUserVerified(ctx context.Context, org, email string) *user_v2.AddHumanUserResponse {
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{
Organization: &object.Organization{
Org: &object.Organization_OrgId{
OrgId: org,
},
},
Profile: &user.SetHumanProfile{
Profile: &user_v2.SetHumanProfile{
GivenName: "Mickey",
FamilyName: "Mouse",
NickName: gu.Ptr("Mickey"),
PreferredLanguage: gu.Ptr("nl"),
Gender: gu.Ptr(user.Gender_GENDER_MALE),
Gender: gu.Ptr(user_v2.Gender_GENDER_MALE),
},
Email: &user.SetHumanEmail{
Email: &user_v2.SetHumanEmail{
Email: email,
Verification: &user.SetHumanEmail_IsVerified{
Verification: &user_v2.SetHumanEmail_IsVerified{
IsVerified: true,
},
},
Phone: &user.SetHumanPhone{
Phone: &user_v2.SetHumanPhone{
Phone: "+41791234567",
Verification: &user.SetHumanPhone_IsVerified{
Verification: &user_v2.SetHumanPhone_IsVerified{
IsVerified: true,
},
},
@ -334,12 +338,12 @@ func (s *Tester) CreateMachineUser(ctx context.Context) *mgmt.AddMachineUserResp
return resp
}
func (s *Tester) CreateUserIDPlink(ctx context.Context, userID, externalID, idpID, username string) *user.AddIDPLinkResponse {
func (s *Tester) CreateUserIDPlink(ctx context.Context, userID, externalID, idpID, username string) *user_v2.AddIDPLinkResponse {
resp, err := s.Client.UserV2.AddIDPLink(
ctx,
&user.AddIDPLinkRequest{
&user_v2.AddIDPLinkRequest{
UserId: userID,
IdpLink: &user.IDPLink{
IdpLink: &user_v2.IDPLink{
IdpId: idpID,
UserId: externalID,
UserName: username,
@ -351,13 +355,13 @@ func (s *Tester) CreateUserIDPlink(ctx context.Context, userID, externalID, idpI
}
func (s *Tester) RegisterUserPasskey(ctx context.Context, userID string) {
reg, err := s.Client.UserV2.CreatePasskeyRegistrationLink(ctx, &user.CreatePasskeyRegistrationLinkRequest{
reg, err := s.Client.UserV2.CreatePasskeyRegistrationLink(ctx, &user_v2.CreatePasskeyRegistrationLinkRequest{
UserId: userID,
Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{},
Medium: &user_v2.CreatePasskeyRegistrationLinkRequest_ReturnCode{},
})
logging.OnError(err).Fatal("create user passkey")
pkr, err := s.Client.UserV2.RegisterPasskey(ctx, &user.RegisterPasskeyRequest{
pkr, err := s.Client.UserV2.RegisterPasskey(ctx, &user_v2.RegisterPasskeyRequest{
UserId: userID,
Code: reg.GetCode(),
Domain: s.Config.ExternalDomain,
@ -366,7 +370,7 @@ func (s *Tester) RegisterUserPasskey(ctx context.Context, userID string) {
attestationResponse, err := s.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
logging.OnError(err).Fatal("create user passkey")
_, err = s.Client.UserV2.VerifyPasskeyRegistration(ctx, &user.VerifyPasskeyRegistrationRequest{
_, err = s.Client.UserV2.VerifyPasskeyRegistration(ctx, &user_v2.VerifyPasskeyRegistrationRequest{
UserId: userID,
PasskeyId: pkr.GetPasskeyId(),
PublicKeyCredential: attestationResponse,
@ -376,7 +380,7 @@ func (s *Tester) RegisterUserPasskey(ctx context.Context, userID string) {
}
func (s *Tester) RegisterUserU2F(ctx context.Context, userID string) {
pkr, err := s.Client.UserV2.RegisterU2F(ctx, &user.RegisterU2FRequest{
pkr, err := s.Client.UserV2.RegisterU2F(ctx, &user_v2.RegisterU2FRequest{
UserId: userID,
Domain: s.Config.ExternalDomain,
})
@ -384,7 +388,7 @@ func (s *Tester) RegisterUserU2F(ctx context.Context, userID string) {
attestationResponse, err := s.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
logging.OnError(err).Fatal("create user u2f")
_, err = s.Client.UserV2.VerifyU2FRegistration(ctx, &user.VerifyU2FRegistrationRequest{
_, err = s.Client.UserV2.VerifyU2FRegistration(ctx, &user_v2.VerifyU2FRegistrationRequest{
UserId: userID,
U2FId: pkr.GetU2FId(),
PublicKeyCredential: attestationResponse,
@ -394,9 +398,9 @@ func (s *Tester) RegisterUserU2F(ctx context.Context, userID string) {
}
func (s *Tester) SetUserPassword(ctx context.Context, userID, password string, changeRequired bool) *object.Details {
resp, err := s.Client.UserV2.SetPassword(ctx, &user.SetPasswordRequest{
resp, err := s.Client.UserV2.SetPassword(ctx, &user_v2.SetPasswordRequest{
UserId: userID,
NewPassword: &user.Password{
NewPassword: &user_v2.Password{
Password: password,
ChangeRequired: changeRequired,
},
@ -757,24 +761,57 @@ func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *action.Co
return target
}
func (s *Tester) CreateUserSchema(ctx context.Context, t *testing.T) *schema.CreateUserSchemaResponse {
return s.CreateUserSchemaWithType(ctx, t, fmt.Sprint(time.Now().UnixNano()+1))
func (s *Tester) CreateUserSchemaEmpty(ctx context.Context) *userschema_v3alpha.CreateUserSchemaResponse {
return s.CreateUserSchemaEmptyWithType(ctx, fmt.Sprint(time.Now().UnixNano()+1))
}
func (s *Tester) CreateUserSchemaWithType(ctx context.Context, t *testing.T, schemaType string) *schema.CreateUserSchemaResponse {
func (s *Tester) CreateUserSchema(ctx context.Context, schemaData []byte) *userschema_v3alpha.CreateUserSchemaResponse {
userSchema := new(structpb.Struct)
err := userSchema.UnmarshalJSON(schemaData)
logging.OnError(err).Fatal("create userschema unmarshal")
schema, err := s.Client.UserSchemaV3.CreateUserSchema(ctx, &userschema_v3alpha.CreateUserSchemaRequest{
UserSchema: &userschema_v3alpha.CreateUserSchema{
Type: fmt.Sprint(time.Now().UnixNano() + 1),
DataType: &userschema_v3alpha.CreateUserSchema_Schema{
Schema: userSchema,
},
},
})
logging.OnError(err).Fatal("create userschema")
return schema
}
func (s *Tester) CreateUserSchemaEmptyWithType(ctx context.Context, schemaType string) *userschema_v3alpha.CreateUserSchemaResponse {
userSchema := new(structpb.Struct)
err := userSchema.UnmarshalJSON([]byte(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {}
}`))
require.NoError(t, err)
target, err := s.Client.UserSchemaV3.CreateUserSchema(ctx, &schema.CreateUserSchemaRequest{
Type: schemaType,
DataType: &schema.CreateUserSchemaRequest_Schema{
Schema: userSchema,
logging.OnError(err).Fatal("create userschema unmarshal")
schema, err := s.Client.UserSchemaV3.CreateUserSchema(ctx, &userschema_v3alpha.CreateUserSchemaRequest{
UserSchema: &userschema_v3alpha.CreateUserSchema{
Type: schemaType,
DataType: &userschema_v3alpha.CreateUserSchema_Schema{
Schema: userSchema,
},
},
})
require.NoError(t, err)
return target
logging.OnError(err).Fatal("create userschema")
return schema
}
func (s *Tester) CreateSchemaUser(ctx context.Context, orgID string, schemaID string, data []byte) *user_v3alpha.CreateUserResponse {
userData := new(structpb.Struct)
err := userData.UnmarshalJSON(data)
logging.OnError(err).Fatal("create user unmarshal")
user, err := s.Client.UserV3Alpha.CreateUser(ctx, &user_v3alpha.CreateUserRequest{
Organization: &object_v3alpha.Organization{Property: &object_v3alpha.Organization_OrgId{OrgId: orgID}},
User: &user_v3alpha.CreateUser{
SchemaId: schemaID,
Data: userData,
},
})
logging.OnError(err).Fatal("create user")
return user
}

View File

@ -12,9 +12,10 @@ import (
)
const (
UserSchemaTable = "projections.user_schemas"
UserSchemaTable = "projections.user_schemas1"
UserSchemaIDCol = "id"
UserSchemaCreationDateCol = "creation_date"
UserSchemaChangeDateCol = "change_date"
UserSchemaSequenceCol = "sequence"
UserSchemaInstanceIDCol = "instance_id"
@ -39,6 +40,7 @@ func (*userSchemaProjection) Init() *old_handler.Check {
return handler.NewTableCheck(
handler.NewTable([]*handler.InitColumn{
handler.NewColumn(UserSchemaIDCol, handler.ColumnTypeText),
handler.NewColumn(UserSchemaCreationDateCol, handler.ColumnTypeTimestamp),
handler.NewColumn(UserSchemaChangeDateCol, handler.ColumnTypeTimestamp),
handler.NewColumn(UserSchemaSequenceCol, handler.ColumnTypeInt64),
handler.NewColumn(UserSchemaStateCol, handler.ColumnTypeEnum),
@ -102,6 +104,7 @@ func (p *userSchemaProjection) reduceCreated(event eventstore.Event) (*handler.S
event,
[]handler.Column{
handler.NewCol(UserSchemaIDCol, event.Aggregate().ID),
handler.NewCol(UserSchemaCreationDateCol, handler.OnlySetValueOnInsert(UserSchemaTable, e.CreationDate())),
handler.NewCol(UserSchemaChangeDateCol, event.CreatedAt()),
handler.NewCol(UserSchemaSequenceCol, event.Sequence()),
handler.NewCol(UserSchemaInstanceIDCol, event.Aggregate().InstanceID),
@ -130,7 +133,10 @@ func (p *userSchemaProjection) reduceUpdated(event eventstore.Event) (*handler.S
if len(e.Schema) > 0 {
cols = append(cols, handler.NewCol(UserSchemaSchemaCol, e.Schema))
cols = append(cols, handler.NewIncrementCol(UserSchemaRevisionCol, 1))
}
if e.SchemaRevision != nil {
cols = append(cols, handler.NewCol(UserSchemaRevisionCol, *e.SchemaRevision))
}
if len(e.PossibleAuthenticators) > 0 {

View File

@ -39,10 +39,11 @@ func TestUserSchemaProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.user_schemas (id, change_date, sequence, instance_id, state, type, revision, schema, possible_authenticators) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
expectedStmt: "INSERT INTO projections.user_schemas1 (id, creation_date, change_date, sequence, instance_id, state, type, revision, schema, possible_authenticators) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
expectedArgs: []interface{}{
"agg-id",
anyArg{},
anyArg{},
uint64(15),
"instance-id",
domain.UserSchemaStateActive,
@ -61,9 +62,9 @@ func TestUserSchemaProjection_reduces(t *testing.T) {
args: args{
event: getEvent(
testEvent(
schema.CreatedType,
schema.UpdatedType,
schema.AggregateType,
[]byte(`{"schemaType": "type", "schema": {"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}, "possibleAuthenticators": [1,2]}`),
[]byte(`{"schemaType": "type", "schemaRevision": 2, "schema": {"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}, "possibleAuthenticators": [1,2]}`),
), eventstore.GenericEventMapper[schema.UpdatedEvent]),
},
reduce: (&userSchemaProjection{}).reduceUpdated,
@ -73,13 +74,13 @@ func TestUserSchemaProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.user_schemas SET (change_date, sequence, type, schema, revision, possible_authenticators) = ($1, $2, $3, $4, revision + $5, $6) WHERE (id = $7) AND (instance_id = $8)",
expectedStmt: "UPDATE projections.user_schemas1 SET (change_date, sequence, type, schema, revision, possible_authenticators) = ($1, $2, $3, $4, $5, $6) WHERE (id = $7) AND (instance_id = $8)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
"type",
json.RawMessage(`{"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}`),
1,
uint64(2),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername, domain.AuthenticatorTypePassword},
"agg-id",
"instance-id",
@ -106,7 +107,7 @@ func TestUserSchemaProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.user_schemas SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedStmt: "UPDATE projections.user_schemas1 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@ -136,7 +137,7 @@ func TestUserSchemaProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.user_schemas SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedStmt: "UPDATE projections.user_schemas1 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@ -166,7 +167,7 @@ func TestUserSchemaProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.user_schemas WHERE (id = $1) AND (instance_id = $2)",
expectedStmt: "DELETE FROM projections.user_schemas1 WHERE (id = $1) AND (instance_id = $2)",
expectedArgs: []interface{}{
"agg-id",
"instance-id",
@ -193,7 +194,7 @@ func TestUserSchemaProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.user_schemas WHERE (instance_id = $1)",
expectedStmt: "DELETE FROM projections.user_schemas1 WHERE (instance_id = $1)",
expectedArgs: []interface{}{
"agg-id",
},

View File

@ -26,7 +26,6 @@ func (e *UserSchemas) SetState(s *State) {
}
type UserSchema struct {
ID string
domain.ObjectDetails
State domain.UserSchemaState
Type string
@ -49,6 +48,10 @@ var (
name: projection.UserSchemaIDCol,
table: userSchemaTable,
}
UserSchemaCreationDateCol = Column{
name: projection.UserSchemaCreationDateCol,
table: userSchemaTable,
}
UserSchemaChangeDateCol = Column{
name: projection.UserSchemaChangeDateCol,
table: userSchemaTable,
@ -131,6 +134,7 @@ func NewUserSchemaStateSearchQuery(value domain.UserSchemaState) (SearchQuery, e
func prepareUserSchemaQuery() (sq.SelectBuilder, func(*sql.Row) (*UserSchema, error)) {
return sq.Select(
UserSchemaIDCol.identifier(),
UserSchemaCreationDateCol.identifier(),
UserSchemaChangeDateCol.identifier(),
UserSchemaSequenceCol.identifier(),
UserSchemaInstanceIDCol.identifier(),
@ -147,6 +151,7 @@ func prepareUserSchemaQuery() (sq.SelectBuilder, func(*sql.Row) (*UserSchema, er
var schema database.ByteArray[byte]
err := row.Scan(
&u.ID,
&u.CreationDate,
&u.EventDate,
&u.Sequence,
&u.ResourceOwner,
@ -173,6 +178,7 @@ func prepareUserSchemaQuery() (sq.SelectBuilder, func(*sql.Row) (*UserSchema, er
func prepareUserSchemasQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserSchemas, error)) {
return sq.Select(
UserSchemaIDCol.identifier(),
UserSchemaCreationDateCol.identifier(),
UserSchemaChangeDateCol.identifier(),
UserSchemaSequenceCol.identifier(),
UserSchemaInstanceIDCol.identifier(),
@ -195,6 +201,7 @@ func prepareUserSchemasQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserSchemas,
u := new(UserSchema)
err := rows.Scan(
&u.ID,
&u.CreationDate,
&u.EventDate,
&u.Sequence,
&u.ResourceOwner,

View File

@ -15,19 +15,21 @@ import (
)
var (
prepareUserSchemasStmt = `SELECT projections.user_schemas.id,` +
` projections.user_schemas.change_date,` +
` projections.user_schemas.sequence,` +
` projections.user_schemas.instance_id,` +
` projections.user_schemas.state,` +
` projections.user_schemas.type,` +
` projections.user_schemas.revision,` +
` projections.user_schemas.schema,` +
` projections.user_schemas.possible_authenticators,` +
prepareUserSchemasStmt = `SELECT projections.user_schemas1.id,` +
` projections.user_schemas1.creation_date,` +
` projections.user_schemas1.change_date,` +
` projections.user_schemas1.sequence,` +
` projections.user_schemas1.instance_id,` +
` projections.user_schemas1.state,` +
` projections.user_schemas1.type,` +
` projections.user_schemas1.revision,` +
` projections.user_schemas1.schema,` +
` projections.user_schemas1.possible_authenticators,` +
` COUNT(*) OVER ()` +
` FROM projections.user_schemas`
prepareUserSchemasCols = []string{
"id",
"creation_date",
"change_date",
"sequence",
"instance_id",
@ -39,18 +41,20 @@ var (
"count",
}
prepareUserSchemaStmt = `SELECT projections.user_schemas.id,` +
` projections.user_schemas.change_date,` +
` projections.user_schemas.sequence,` +
` projections.user_schemas.instance_id,` +
` projections.user_schemas.state,` +
` projections.user_schemas.type,` +
` projections.user_schemas.revision,` +
` projections.user_schemas.schema,` +
` projections.user_schemas.possible_authenticators` +
prepareUserSchemaStmt = `SELECT projections.user_schemas1.id,` +
` projections.user_schemas1.creation_date,` +
` projections.user_schemas1.change_date,` +
` projections.user_schemas1.sequence,` +
` projections.user_schemas1.instance_id,` +
` projections.user_schemas1.state,` +
` projections.user_schemas1.type,` +
` projections.user_schemas1.revision,` +
` projections.user_schemas1.schema,` +
` projections.user_schemas1.possible_authenticators` +
` FROM projections.user_schemas`
prepareUserSchemaCols = []string{
"id",
"creation_date",
"change_date",
"sequence",
"instance_id",
@ -96,6 +100,7 @@ func Test_UserSchemaPrepares(t *testing.T) {
{
"id",
testNow,
testNow,
uint64(20211109),
"instance-id",
domain.UserSchemaStateActive,
@ -113,9 +118,10 @@ func Test_UserSchemaPrepares(t *testing.T) {
},
UserSchemas: []*UserSchema{
{
ID: "id",
ObjectDetails: domain.ObjectDetails{
ID: "id",
EventDate: testNow,
CreationDate: testNow,
Sequence: 20211109,
ResourceOwner: "instance-id",
},
@ -139,6 +145,7 @@ func Test_UserSchemaPrepares(t *testing.T) {
{
"id-1",
testNow,
testNow,
uint64(20211109),
"instance-id",
domain.UserSchemaStateActive,
@ -150,6 +157,7 @@ func Test_UserSchemaPrepares(t *testing.T) {
{
"id-2",
testNow,
testNow,
uint64(20211110),
"instance-id",
domain.UserSchemaStateInactive,
@ -167,9 +175,10 @@ func Test_UserSchemaPrepares(t *testing.T) {
},
UserSchemas: []*UserSchema{
{
ID: "id-1",
ObjectDetails: domain.ObjectDetails{
ID: "id-1",
EventDate: testNow,
CreationDate: testNow,
Sequence: 20211109,
ResourceOwner: "instance-id",
},
@ -180,9 +189,10 @@ func Test_UserSchemaPrepares(t *testing.T) {
PossibleAuthenticators: database.NumberArray[domain.AuthenticatorType]{domain.AuthenticatorTypeUsername, domain.AuthenticatorTypePassword},
},
{
ID: "id-2",
ObjectDetails: domain.ObjectDetails{
ID: "id-2",
EventDate: testNow,
CreationDate: testNow,
Sequence: 20211110,
ResourceOwner: "instance-id",
},
@ -240,6 +250,7 @@ func Test_UserSchemaPrepares(t *testing.T) {
[]driver.Value{
"id",
testNow,
testNow,
uint64(20211109),
"instance-id",
domain.UserSchemaStateActive,
@ -251,9 +262,10 @@ func Test_UserSchemaPrepares(t *testing.T) {
),
},
object: &UserSchema{
ID: "id",
ObjectDetails: domain.ObjectDetails{
ID: "id",
EventDate: testNow,
CreationDate: testNow,
Sequence: 20211109,
ResourceOwner: "instance-id",
},

View File

@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
)
@ -79,7 +81,9 @@ type UpdatedEvent struct {
SchemaType *string `json:"schemaType,omitempty"`
Schema json.RawMessage `json:"schema,omitempty"`
PossibleAuthenticators []domain.AuthenticatorType `json:"possibleAuthenticators,omitempty"`
SchemaRevision *uint64 `json:"schemaRevision,omitempty"`
oldSchemaType string
oldRevision uint64
}
func (e *UpdatedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
@ -139,6 +143,13 @@ func ChangePossibleAuthenticators(possibleAuthenticators []domain.AuthenticatorT
}
}
func IncreaseRevision(oldRevision uint64) func(event *UpdatedEvent) {
return func(e *UpdatedEvent) {
e.SchemaRevision = gu.Ptr(oldRevision + 1)
e.oldRevision = oldRevision
}
}
type DeactivatedEvent struct {
*eventstore.BaseEvent `json:"-"`
}

View File

@ -0,0 +1,25 @@
package schemauser
import (
"github.com/zitadel/zitadel/internal/eventstore"
)
const (
AggregateType = "user"
AggregateVersion = "v3"
)
type Aggregate struct {
eventstore.Aggregate
}
func NewAggregate(id, resourceOwner string) *Aggregate {
return &Aggregate{
Aggregate: eventstore.Aggregate{
Type: AggregateType,
Version: AggregateVersion,
ID: id,
ResourceOwner: resourceOwner,
},
}
}

View File

@ -0,0 +1,202 @@
package schemauser
import (
"context"
"time"
"github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
emailEventPrefix = eventPrefix + "email."
EmailUpdatedType = emailEventPrefix + "updated"
EmailVerifiedType = emailEventPrefix + "verified"
EmailVerificationFailedType = emailEventPrefix + "verification.failed"
EmailCodeAddedType = emailEventPrefix + "code.added"
EmailCodeSentType = emailEventPrefix + "code.sent"
)
type EmailUpdatedEvent struct {
eventstore.BaseEvent `json:"-"`
EmailAddress domain.EmailAddress `json:"email,omitempty"`
}
func (e *EmailUpdatedEvent) Payload() interface{} {
return e
}
func (e *EmailUpdatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewEmailUpdatedEvent(ctx context.Context, aggregate *eventstore.Aggregate, emailAddress domain.EmailAddress) *EmailUpdatedEvent {
return &EmailUpdatedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
EmailUpdatedType,
),
EmailAddress: emailAddress,
}
}
func EmailUpdatedEventMapper(event eventstore.Event) (eventstore.Event, error) {
emailChangedEvent := &EmailUpdatedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := event.Unmarshal(emailChangedEvent)
if err != nil {
return nil, zerrors.ThrowInternal(err, "USER-4M0sd", "unable to unmarshal human password changed")
}
return emailChangedEvent, nil
}
type EmailVerifiedEvent struct {
eventstore.BaseEvent `json:"-"`
IsEmailVerified bool `json:"-"`
}
func (e *EmailVerifiedEvent) Payload() interface{} {
return nil
}
func (e *EmailVerifiedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewEmailVerifiedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *EmailVerifiedEvent {
return &EmailVerifiedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
EmailVerifiedType,
),
}
}
func HumanVerifiedEventMapper(event eventstore.Event) (eventstore.Event, error) {
emailVerified := &EmailVerifiedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
IsEmailVerified: true,
}
return emailVerified, nil
}
type EmailVerificationFailedEvent struct {
eventstore.BaseEvent `json:"-"`
}
func (e *EmailVerificationFailedEvent) Payload() interface{} {
return nil
}
func (e *EmailVerificationFailedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewHumanEmailVerificationFailedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *EmailVerificationFailedEvent {
return &EmailVerificationFailedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
EmailVerificationFailedType,
),
}
}
func EmailVerificationFailedEventMapper(event eventstore.Event) (eventstore.Event, error) {
return &EmailVerificationFailedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}, nil
}
type EmailCodeAddedEvent struct {
eventstore.BaseEvent `json:"-"`
Code *crypto.CryptoValue `json:"code,omitempty"`
Expiry time.Duration `json:"expiry,omitempty"`
URLTemplate string `json:"url_template,omitempty"`
CodeReturned bool `json:"code_returned,omitempty"`
TriggeredAtOrigin string `json:"triggerOrigin,omitempty"`
}
func (e *EmailCodeAddedEvent) Payload() interface{} {
return e
}
func (e *EmailCodeAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func (e *EmailCodeAddedEvent) TriggerOrigin() string {
return e.TriggeredAtOrigin
}
func NewEmailCodeAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
code *crypto.CryptoValue,
expiry time.Duration,
urlTemplate string,
codeReturned bool,
) *EmailCodeAddedEvent {
return &EmailCodeAddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
EmailCodeAddedType,
),
Code: code,
Expiry: expiry,
URLTemplate: urlTemplate,
CodeReturned: codeReturned,
TriggeredAtOrigin: http.DomainContext(ctx).Origin(),
}
}
func EmailCodeAddedEventMapper(event eventstore.Event) (eventstore.Event, error) {
codeAdded := &EmailCodeAddedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := event.Unmarshal(codeAdded)
if err != nil {
return nil, zerrors.ThrowInternal(err, "USER-3M0sd", "unable to unmarshal human email code added")
}
return codeAdded, nil
}
type EmailCodeSentEvent struct {
eventstore.BaseEvent `json:"-"`
}
func (e *EmailCodeSentEvent) Payload() interface{} {
return nil
}
func (e *EmailCodeSentEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewHumanEmailCodeSentEvent(ctx context.Context, aggregate *eventstore.Aggregate) *EmailCodeSentEvent {
return &EmailCodeSentEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
EmailCodeSentType,
),
}
}
func EmailCodeSentEventMapper(event eventstore.Event) (eventstore.Event, error) {
return &EmailCodeSentEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}, nil
}

View File

@ -0,0 +1,9 @@
package schemauser
import "github.com/zitadel/zitadel/internal/eventstore"
func init() {
eventstore.RegisterFilterEventMapper(AggregateType, CreatedType, eventstore.GenericEventMapper[CreatedEvent])
eventstore.RegisterFilterEventMapper(AggregateType, UpdatedType, eventstore.GenericEventMapper[UpdatedEvent])
eventstore.RegisterFilterEventMapper(AggregateType, DeletedType, eventstore.GenericEventMapper[DeletedEvent])
}

View File

@ -0,0 +1,198 @@
package schemauser
import (
"context"
"time"
"github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
phoneEventPrefix = eventPrefix + "phone."
PhoneUpdatedType = phoneEventPrefix + "updated"
PhoneVerifiedType = phoneEventPrefix + "verified"
PhoneVerificationFailedType = phoneEventPrefix + "verification.failed"
PhoneCodeAddedType = phoneEventPrefix + "code.added"
PhoneCodeSentType = phoneEventPrefix + "code.sent"
)
type PhoneChangedEvent struct {
eventstore.BaseEvent `json:"-"`
PhoneNumber domain.PhoneNumber `json:"phone,omitempty"`
}
func (e *PhoneChangedEvent) Payload() interface{} {
return e
}
func (e *PhoneChangedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewPhoneChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, phone domain.PhoneNumber) *PhoneChangedEvent {
return &PhoneChangedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
PhoneUpdatedType,
),
PhoneNumber: phone,
}
}
func PhoneChangedEventMapper(event eventstore.Event) (eventstore.Event, error) {
phoneChangedEvent := &PhoneChangedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := event.Unmarshal(phoneChangedEvent)
if err != nil {
return nil, zerrors.ThrowInternal(err, "USER-5M0pd", "unable to unmarshal phone changed")
}
return phoneChangedEvent, nil
}
type PhoneVerifiedEvent struct {
eventstore.BaseEvent `json:"-"`
IsPhoneVerified bool `json:"-"`
}
func (e *PhoneVerifiedEvent) Payload() interface{} {
return nil
}
func (e *PhoneVerifiedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewPhoneVerifiedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *PhoneVerifiedEvent {
return &PhoneVerifiedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
PhoneVerifiedType,
),
}
}
func PhoneVerifiedEventMapper(event eventstore.Event) (eventstore.Event, error) {
return &PhoneVerifiedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
IsPhoneVerified: true,
}, nil
}
type PhoneVerificationFailedEvent struct {
eventstore.BaseEvent `json:"-"`
}
func (e *PhoneVerificationFailedEvent) Payload() interface{} {
return nil
}
func (e *PhoneVerificationFailedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewPhoneVerificationFailedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *PhoneVerificationFailedEvent {
return &PhoneVerificationFailedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
PhoneVerificationFailedType,
),
}
}
func PhoneVerificationFailedEventMapper(event eventstore.Event) (eventstore.Event, error) {
return &PhoneVerificationFailedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}, nil
}
type PhoneCodeAddedEvent struct {
eventstore.BaseEvent `json:"-"`
Code *crypto.CryptoValue `json:"code,omitempty"`
Expiry time.Duration `json:"expiry,omitempty"`
CodeReturned bool `json:"code_returned,omitempty"`
TriggeredAtOrigin string `json:"triggerOrigin,omitempty"`
}
func (e *PhoneCodeAddedEvent) Payload() interface{} {
return e
}
func (e *PhoneCodeAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func (e *PhoneCodeAddedEvent) TriggerOrigin() string {
return e.TriggeredAtOrigin
}
func NewPhoneCodeAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
code *crypto.CryptoValue,
expiry time.Duration,
codeReturned bool,
) *PhoneCodeAddedEvent {
return &PhoneCodeAddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
PhoneCodeAddedType,
),
Code: code,
Expiry: expiry,
CodeReturned: codeReturned,
TriggeredAtOrigin: http.DomainContext(ctx).Origin(),
}
}
func PhoneCodeAddedEventMapper(event eventstore.Event) (eventstore.Event, error) {
codeAdded := &PhoneCodeAddedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := event.Unmarshal(codeAdded)
if err != nil {
return nil, zerrors.ThrowInternal(err, "USER-6Ms9d", "unable to unmarshal phone code added")
}
return codeAdded, nil
}
type PhoneCodeSentEvent struct {
eventstore.BaseEvent `json:"-"`
}
func (e *PhoneCodeSentEvent) Payload() interface{} {
return e
}
func (e *PhoneCodeSentEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewPhoneCodeSentEvent(ctx context.Context, aggregate *eventstore.Aggregate) *PhoneCodeSentEvent {
return &PhoneCodeSentEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
PhoneCodeSentType,
),
}
}
func PhoneCodeSentEventMapper(event eventstore.Event) (eventstore.Event, error) {
return &PhoneCodeSentEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}, nil
}

View File

@ -0,0 +1,144 @@
package schemauser
import (
"context"
"encoding/json"
"github.com/zitadel/zitadel/internal/eventstore"
)
const (
eventPrefix = "user."
CreatedType = eventPrefix + "created"
UpdatedType = eventPrefix + "updated"
DeletedType = eventPrefix + "deleted"
)
type CreatedEvent struct {
*eventstore.BaseEvent `json:"-"`
ID string `json:"id"`
SchemaID string `json:"schemaID"`
SchemaRevision uint64 `json:"schemaRevision"`
Data json.RawMessage `json:"user,omitempty"`
}
func (e *CreatedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = event
}
func (e *CreatedEvent) Payload() interface{} {
return e
}
func (e *CreatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewCreatedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
schemaID string,
schemaRevision uint64,
data json.RawMessage,
) *CreatedEvent {
return &CreatedEvent{
BaseEvent: eventstore.NewBaseEventForPush(
ctx,
aggregate,
CreatedType,
),
SchemaID: schemaID,
SchemaRevision: schemaRevision,
Data: data,
}
}
type UpdatedEvent struct {
*eventstore.BaseEvent `json:"-"`
SchemaID *string `json:"schemaID,omitempty"`
SchemaRevision *uint64 `json:"schemaRevision,omitempty"`
Data json.RawMessage `json:"schema,omitempty"`
oldSchemaID string
oldRevision uint64
}
func (e *UpdatedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = event
}
func (e *UpdatedEvent) Payload() interface{} {
return e
}
func (e *UpdatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewUpdatedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
changes []Changes,
) *UpdatedEvent {
updatedEvent := &UpdatedEvent{
BaseEvent: eventstore.NewBaseEventForPush(
ctx,
aggregate,
UpdatedType,
),
}
for _, change := range changes {
change(updatedEvent)
}
return updatedEvent
}
type Changes func(event *UpdatedEvent)
func ChangeSchemaID(oldSchemaID, schemaID string) func(event *UpdatedEvent) {
return func(e *UpdatedEvent) {
e.SchemaID = &schemaID
e.oldSchemaID = oldSchemaID
}
}
func ChangeSchemaRevision(oldSchemaRevision, schemaRevision uint64) func(event *UpdatedEvent) {
return func(e *UpdatedEvent) {
e.SchemaRevision = &schemaRevision
e.oldRevision = oldSchemaRevision
}
}
func ChangeData(data json.RawMessage) func(event *UpdatedEvent) {
return func(e *UpdatedEvent) {
e.Data = data
}
}
type DeletedEvent struct {
*eventstore.BaseEvent `json:"-"`
}
func (e *DeletedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = event
}
func (e *DeletedEvent) Payload() interface{} {
return e
}
func (e *DeletedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewDeletedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
) *DeletedEvent {
return &DeletedEvent{
BaseEvent: eventstore.NewBaseEventForPush(
ctx,
aggregate,
DeletedType,
),
}
}

View File

@ -593,6 +593,11 @@ Errors:
NotActive: Потребителската схема не е активна
NotInactive: Потребителската схема не е неактивна
NotExists: Потребителската схема не съществува
ID:
Missing: Липсва идентификатор на потребителска схема
Invalid: Потребителската схема е невалидна
Data:
Invalid: Невалидни данни за потребителска схема
TokenExchange:
FeatureDisabled: Функцията Token Exchange е деактивирана за вашето копие. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
@ -1351,6 +1356,26 @@ EventTypes:
deactivated: Потребителската схема е деактивирана
reactivated: Потребителската схема е активирана отново
deleted: Потребителската схема е изтрита
user:
created: Потребител е създаден
updated: Потребителят е актуализиран
deleted: Потребителят е изтрит
email:
updated: Имейл адресът е променен
verified: Имейл адресът е потвърден
verification:
failed: Проверката на имейл адреса не бе успешна
code:
added: Генериран код за потвърждение на имейл адрес
sent: Кодът за потвърждение на имейл адреса е изпратен
phone:
updated: Телефонният номер е променен
verified: Телефонният номер е потвърден
verification:
failed: Неуспешна проверка на телефонния номер
code:
added: Генериран код за потвърждение на телефонен номер
sent: Кодът за потвърждение на телефонния номер е изпратен
web_key:
added: Добавен уеб ключ
activated: Уеб ключът е активиран

View File

@ -574,6 +574,11 @@ Errors:
NotActive: Uživatelské schéma není aktivní
NotInactive: Uživatelské schéma není neaktivní
NotExists: Uživatelské schéma neexistuje
ID:
Missing: Chybí ID schématu uživatele
Invalid: Uživatelské schéma je neplatné
Data:
Invalid: Data neplatná pro uživatelské schéma
TokenExchange:
FeatureDisabled: Funkce Token Exchange je pro vaši instanci zakázána. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
@ -1317,6 +1322,26 @@ EventTypes:
deactivated: Uživatelské schéma deaktivováno
reactivated: Uživatelské schéma bylo znovu aktivováno
deleted: Uživatelské schéma bylo smazáno
user:
created: Uživatel vytvořen
updated: Uživatel aktualizován
deleted: Uživatel byl smazán
email:
updated: E-mailová adresa změněna
verified: E-mailová adresa ověřena
verification:
failed: Ověření e-mailové adresy se nezdařilo
code:
added: Vygenerován ověřovací kód e-mailové adresy
sent: Ověřovací kód e-mailové adresy odeslán
phone:
updated: Telefonní číslo změněno
verified: Telefonní číslo ověřeno
verification:
failed: Ověření telefonního čísla se nezdařilo
code:
added: Byl vygenerován ověřovací kód telefonního čísla
sent: Ověřovací kód telefonního čísla odeslán
web_key:
added: Přidán webový klíč
activated: Web Key aktivován

View File

@ -576,6 +576,11 @@ Errors:
NotActive: Benutzerschema nicht aktiv
NotInactive: Benutzerschema nicht inaktiv
NotExists: Benutzerschema existiert nicht
ID:
Missing: BenutzerschemaID fehlt
Invalid: Benutzerschema ist ungültig
Data:
Invalid: Daten für Benutzerschema ungültig
TokenExchange:
FeatureDisabled: Die Token-Austauschfunktion ist für Ihre Instanz deaktiviert. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
@ -1319,6 +1324,26 @@ EventTypes:
deactivated: Benutzerschema deaktiviert
reactivated: Benutzerschema reaktiviert
deleted: Benutzerschema gelöscht
user:
created: Benutzer erstellt
updated: Benutzer aktualisiert
deleted: Benutzer gelöscht
email:
updated: E-Mail-Adresse geändert
verified: E-Mail-Adresse verifiziert
verification:
failed: E-Mail-Adressenverifizierung fehlgeschlagen
code:
added: E-Mail-Adressenverifizierungscode generiert
sent: E-Mail-Adressenverifizierungscode gesendet
phone:
updated: Telefonnummer geändert
verified: Telefonnummer verifiziert
verification:
failed: Telefonnummernverifizierung fehlgeschlagen
code:
added: Telefonnummernverifizierungscode generiert
sent: Telefonnummernverifizierungscode gesendet
web_key:
added: Web Key hinzugefügt
activated: Web Key aktiviert

View File

@ -576,6 +576,11 @@ Errors:
NotActive: User Schema not active
NotInactive: User Schema not inactive
NotExists: User Schema does not exist
ID:
Missing: User Schema ID missing
Invalid: User Schema invalid
Data:
Invalid: Data invalid for User Schema
TokenExchange:
FeatureDisabled: Token Exchange feature is disabled for your instance. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
@ -1319,6 +1324,28 @@ EventTypes:
deactivated: User Schema deactivated
reactivated: User Schema reactivated
deleted: User Schema deleted
user:
created: User created
updated: User updated
deleted: User deleted
email:
updated: Email address changed
verified: Email address verified
verification:
failed: Email address verification failed
code:
added: Email address verification code generated
sent: Email address verification code sent
phone:
updated: Phone number changed
verified: Phone number verified
verification:
failed: Phone number verification failed
code:
added: Phone number verification code generated
sent: Phone number verification code sent
web_key:
added: Web Key added
activated: Web Key activated

View File

@ -576,6 +576,11 @@ Errors:
NotActive: Esquema de usuario no activo
NotInactive: Esquema de usuario no inactivo
NotExists: El esquema de usuario no existe
ID:
Missing: Falta el ID del esquema de usuario
Invalid: Esquema de usuario no válido
Data:
Invalid: Datos no válidos para el esquema de usuario
TokenExchange:
FeatureDisabled: La función de intercambio de tokens está deshabilitada para su instancia. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
@ -1319,6 +1324,26 @@ EventTypes:
deactivated: Esquema de usuario desactivado
reactivated: Esquema de usuario reactivado
deleted: Esquema de usuario eliminado
user:
created: Usuario creado
updated: Usuario actualizado
deleted: Usuario eliminado
email:
updated: Dirección de correo electrónico modificada
verified: Dirección de correo electrónico verificada
verification:
failed: Error en la verificación de la dirección de correo electrónico
code:
added: Código de verificación de la dirección de correo electrónico generado
sent: Código de verificación de la dirección de correo electrónico enviado
phone:
updated: Número de teléfono modificado
verified: Número de teléfono verificado
verification:
failed: Error en la verificación del número de teléfono
code:
added: Código de verificación del número de teléfono generado
sent: Código de verificación del número de teléfono enviado
web_key:
added: Clave web añadida
activated: Clave web activada

View File

@ -576,6 +576,11 @@ Errors:
NotActive: Schéma utilisateur non actif
NotInactive: Le schéma utilisateur n'est pas inactif
NotExists: Le schéma utilisateur n'existe pas
ID:
Missing: ID de schéma utilisateur manquant
Invalid: Schéma utilisateur non valide
Data:
Invalid: Données non valides pour le schéma utilisateur
TokenExchange:
FeatureDisabled: La fonctionnalité Token Exchange est désactivée pour votre instance. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
@ -1314,6 +1319,26 @@ EventTypes:
deactivated: Schéma utilisateur désactivé
reactivated: Schéma utilisateur réactivé
deleted: Schéma utilisateur supprimé
user:
created: Utilisateur créé
updated: Utilisateur mis à jour
deleted: Utilisateur supprimé
email:
updated: Adresse e-mail modifiée
verified: Adresse e-mail vérifiée
verification:
failed: Échec de la vérification de l'adresse e-mail
code:
added: Code de vérification de l'adresse e-mail généré
sent: Code de vérification de l'adresse e-mail envoyé
phone:
updated: Numéro de téléphone modifié
verified: Numéro de téléphone vérifié
verification:
failed: Échec de la vérification du numéro de téléphone
code:
added: Code de vérification du numéro de téléphone généré
sent: Code de vérification du numéro de téléphone envoyé
web_key:
added: Clé Web ajoutée
activated: Clé Web activée

View File

@ -576,6 +576,11 @@ Errors:
NotActive: Schema utente non attivo
NotInactive: Schema utente non inattivo
NotExists: Lo schema utente non esiste
ID:
Missing: ID schema utente mancante
Invalid: Schema utente non valido
Data:
Invalid: Dati non validi per lo schema utente
TokenExchange:
FeatureDisabled: La funzionalità di scambio token è disabilitata per la tua istanza. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
@ -1315,6 +1320,26 @@ EventTypes:
deactivated: Schema utente disattivato
reactivated: Schema utente riattivato
deleted: Schema utente eliminato
user:
created: Utente creato
updated: Utente aggiornato
deleted: Utente eliminato
email:
updated: Indirizzo email modificato
verified: Indirizzo email verificato
verification:
failed: Verifica indirizzo email non riuscita
code:
added: Codice di verifica indirizzo email generato
sent: Codice di verifica indirizzo email inviato
phone:
updated: Numero di telefono modificato
verified: Numero di telefono verificato
verification:
failed: Verifica numero di telefono non riuscita
code:
added: Codice di verifica numero di telefono generato
sent: Codice di verifica numero di telefono inviato
web_key:
added: Web Key aggiunto
activated: Web Key attivato

View File

@ -565,6 +565,11 @@ Errors:
NotActive: ユーザースキーマがアクティブではありません
NotInactive: ユーザースキーマが非アクティブではありません
NotExists: ユーザースキーマが存在しません
ID:
Missing: ユーザー スキーマ ID がありません
Invalid: ユーザー スキーマが無効です
Data:
Invalid: ユーザー スキーマのデータが無効です
TokenExchange:
FeatureDisabled: インスタンスではトークン交換機能が無効になっています。 https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
@ -1305,6 +1310,26 @@ EventTypes:
deactivated: ユーザースキーマが非アクティブ化されました
reactivated: ユーザースキーマが再アクティブ化されました
deleted: ユーザースキーマが削除されました
user:
created: ユーザーが作成されました
updated: ユーザーが更新されました
deleted: ユーザーが削除されました
email:
updated: メールアドレスが変更されました
verified: メールアドレスが確認されました
verification:
failed: メールアドレスの確認に失敗しました
code:
added: メールアドレスの確認コードが生成されました
sent: メールアドレスの確認コードが送信されました
phone:
updated: 電話番号が変更されました
verified: 電話番号が確認されました
verification:
failed: 電話番号の確認に失敗しました
code:
added: 電話番号の確認コードが生成されました
sent: 電話番号の確認コードが送信されました
web_key:
added: Web キーが追加されました
activated: Web キーが有効化されました

View File

@ -575,6 +575,11 @@ Errors:
NotActive: Корисничката шема не е активна
NotInactive: Корисничката шема не е неактивна
NotExists: Корисничката шема не постои
ID:
Missing: Недостасува ID на корисничка шема
Invalid: Корисничката шема е неважечка
Data:
Invalid: Податоците не се валидни за корисничка шема
TokenExchange:
FeatureDisabled: Функцијата за размена на токени е оневозможена на вашиот пример. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
@ -1317,6 +1322,26 @@ EventTypes:
deactivated: Корисничката шема е деактивирана
reactivated: Корисничката шема е реактивирана
deleted: Корисничката шема е избришана
user:
created: Корисникот е создаден
updated: Корисникот е ажуриран
deleted: Корисникот е избришан
email:
updated: Адресата на е-пошта е променета
verified: Адресата на е-пошта е потврдена
verification:
failed: Потврдата на адресата на е-пошта не успеа
code:
added: Генериран е код за потврда на адресата на е-пошта
sent: Испратен е код за потврда на адресата на е-пошта
phone:
updated: Телефонскиот број е променет
verified: Телефонскиот број е потврден
verification:
failed: Потврдата на телефонскиот број не успеа
code:
added: Генериран е кодот за потврда на телефонскиот број
sent: Кодот за потврда на телефонскиот број е испратен
web_key:
added: Додаден е веб-клуч
activated: Веб-клучот е активиран

View File

@ -576,6 +576,11 @@ Errors:
NotActive: Gebruikersschema niet actief
NotInactive: Gebruikersschema niet inactief
NotExists: Gebruikersschema bestaat niet
ID:
Missing: Недостасува ID на корисничка шема
Invalid: Корисничката шема е неважечка
Data:
Invalid: Податоците не се валидни за корисничка шема
TokenExchange:
FeatureDisabled: De Token Exchange-functie is uitgeschakeld voor uw instantie. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
@ -1314,6 +1319,26 @@ EventTypes:
deactivated: Gebruikersschema gedeactiveerd
reactivated: Gebruikersschema opnieuw geactiveerd
deleted: Gebruikersschema verwijderd
user:
created: Gebruiker aangemaakt
updated: Gebruiker bijgewerkt
deleted: Gebruiker verwijderd
email:
updated: E-mailadres gewijzigd
verified: E-mailadres geverifieerd
verification:
failed: E-mailadres verificatie mislukt
code:
added: E-mailadres verificatiecode gegenereerd
sent: E-mailadres verificatiecode verzonden
phone:
updated: Telefoonnummer gewijzigd
verified: Telefoonnummer geverifieerd
verification:
failed: Telefoonnummer verificatie mislukt
code:
added: Telefoonnummer verificatiecode gegenereerd
sent: Telefoonnummer verificatiecode verzonden
web_key:
added: Web Key toegevoegd
activated: Web Key geactiveerd

View File

@ -576,6 +576,11 @@ Errors:
NotActive: Schemat użytkownika nieaktywny
NotInactive: Schemat użytkownika nie jest nieaktywny
NotExists: Schemat użytkownika nie istnieje
ID:
Missing: Brak identyfikatora schematu użytkownika
Invalid: Nieprawidłowy schemat użytkownika
Data:
Invalid: Nieprawidłowe dane dla schematu użytkownika
TokenExchange:
FeatureDisabled: Funkcja wymiany tokenów jest wyłączona dla Twojej instancji. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
@ -1319,6 +1324,26 @@ EventTypes:
deactivated: Schemat użytkownika dezaktywowany
reactivated: Schemat użytkownika został ponownie aktywowany
deleted: Schemat użytkownika został usunięty
user:
created: Użytkownik utworzony
updated: Użytkownik zaktualizowany
deleted: Użytkownik usunięty
email:
updated: Adres e-mail zmieniony
verified: Adres e-mail zweryfikowany
verification:
failed: Weryfikacja adresu e-mail nie powiodła się
code:
added: Wygenerowano kod weryfikacyjny adresu e-mail
sent: Wysłano kod weryfikacyjny adresu e-mail
phone:
updated: Numer telefonu zmieniony
verified: Numer telefonu zweryfikowany
verification:
failed: Weryfikacja numeru telefonu nie powiodła się
code:
added: Wygenerowano kod weryfikacyjny numeru telefonu
sent: Wysłano kod weryfikacyjny numeru telefonu
web_key:
added: Dodano klucz internetowy
activated: Klucz internetowy aktywowano

View File

@ -571,6 +571,11 @@ Errors:
NotActive: Esquema do usuário não ativo
NotInactive: Esquema do usuário não inativo
NotExists: O esquema do usuário não existe
ID:
Missing: ID do esquema do utilizador em falta
Invalid: Esquema de utilizador inválido
Data:
Invalid: Dados inválidos para o esquema do utilizador
TokenExchange:
FeatureDisabled: O recurso Token Exchange está desabilitado para sua instância. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
@ -1311,6 +1316,26 @@ EventTypes:
deactivated: Esquema de usuário desativado
reactivated: Esquema do usuário reativado
deleted: Esquema do usuário excluído
user:
created: Utilizador criado
updated: Utilizador atualizado
deleted: Utilizador excluído
email:
updated: Endereço de e-mail alterado
verified: Endereço de e-mail verificado
verification:
failed: Falha na verificação do endereço de e-mail
code:
added: Código de verificação do endereço de e-mail gerado
sent: Código de verificação do endereço de e-mail enviado
phone:
updated: Número de telefone alterado
verified: Número de telefone verificado
verification:
failed: Falha na verificação do número de telefone
code:
added: Código de verificação do número de telefone gerado
sent: Código de verificação do número de telefone enviado
web_key:
added: Chave Web adicionada
activated: Chave Web ativada

View File

@ -565,6 +565,11 @@ Errors:
NotActive: Пользовательская схема не активна
NotInactive: Пользовательская схема не неактивна
NotExists: Пользовательская схема не существует
ID:
Missing: Отсутствует идентификатор схемы пользователя
Invalid: Недействительная схема пользователя
Data:
Invalid: Данные недействительны для схемы пользователя
TokenExchange:
FeatureDisabled: Функция обмена токенами отключена для вашего экземпляра. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
@ -1305,6 +1310,26 @@ EventTypes:
deactivated: Пользовательская схема деактивирована
reactivated: Пользовательская схема повторно активирована
deleted: Пользовательская схема удалена
user:
created: Пользователь создан
updated: Пользователь обновлен
deleted: Пользователь удален
email:
updated: Адрес электронной почты изменен
verified: Адрес электронной почты проверен
verification:
failed: Проверка адреса электронной почты не удалась
code:
added: Сгенерирован код проверки адреса электронной почты
sent: Отправлен код проверки адреса электронной почты
phone:
updated: Номер телефона изменен
verified: Номер телефона проверен
verification:
failed: Проверка номера телефона не удалась
code:
added: Сгенерирован код проверки номера телефона
sent: Отправлен код проверки номера телефона
web_key:
added: Добавлен веб-ключ
activated: Веб-ключ активирован

View File

@ -576,6 +576,11 @@ Errors:
NotActive: Användarschema inte aktivt
NotInactive: Användarschema inte inaktivt
NotExists: Användarschema existerar inte
ID:
Missing: Användarschema-ID saknas
Invalid: Ogiltigt användarschema
Data:
Invalid: Data ogiltig för användarschema
TokenExchange:
FeatureDisabled: Token Exchange-funktionen är inaktiverad för din instans. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
@ -1319,6 +1324,26 @@ EventTypes:
deactivated: Användarschema avaktiverat
reactivated: Användarschema återaktiverat
deleted: Användarschema borttaget
user:
created: Användare skapad
updated: Användaren uppdaterad
deleted: Användare raderad
email:
updated: E-postadress ändrad
verified: E-postadress verifierad
verification:
failed: E-postadressverifiering misslyckades
code:
added: Verifieringskod för e-postadress genererad
sent: E-postadressens verifieringskod har skickats
phone:
updated: Telefonnummer ändrat
verified: Telefonnummer verifierat
verification:
failed: Verifiering av telefonnummer misslyckades
code:
added: Verifieringskod för telefonnummer genererad
sent: Verifieringskoden för telefonnummer har skickats
web_key:
added: Webbnyckel har lagts till
activated: Webbnyckel aktiverad

View File

@ -576,6 +576,11 @@ Errors:
NotActive: 用户架构未激活
NotInactive: 用户架构未处于非活动状态
NotExists: 用户架构不存在
ID:
Missing: 缺少用户架构 ID
Invalid: 用户架构无效
Data:
Invalid: 用户架构的数据无效
TokenExchange:
FeatureDisabled: 您的实例已禁用令牌交换功能。 https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
@ -1318,6 +1323,26 @@ EventTypes:
deactivated: 用户架构已停用
reactivated: 用户架构已重新激活
deleted: 用户架构已删除
user:
created: 用户已创建
updated: 用户已更新
deleted: 用户已删除
email:
updated: 电子邮件地址已更改
verified: 电子邮件地址已验证
verification:
failed: 电子邮件地址验证失败
code:
added: 电子邮件地址验证码已生成
sent: 电子邮件地址验证码已发送
phone:
updated: 电话号码已更改
verified: 电话号码已验证
verification:
failed: 电话号码验证失败
code:
added: 电话号码验证码已生成
sent: 电话号码验证码已发送
web_key:
added: 已添加 Web Key
activated: 已激活 Web Key

View File

@ -27,3 +27,11 @@ message Instance {
string domain = 2;
}
}
message Organization {
oneof property {
option (validate.required) = true;
string org_id = 1;
string org_domain = 2;
}
}

View File

@ -17,7 +17,6 @@ import "zitadel/resources/action/v3alpha/query.proto";
import "zitadel/resources/object/v3alpha/object.proto";
import "zitadel/object/v3alpha/object.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action";
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {

View File

@ -1,15 +1,15 @@
syntax = "proto3";
package zitadel.user.v3alpha;
package zitadel.resources.user.v3alpha;
import "google/api/field_behavior.proto";
import "google/protobuf/struct.proto";
import "google/protobuf/timestamp.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/object/v2/object.proto";
import "zitadel/resources/object/v3alpha/object.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v3alpha";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha;user";
message Authenticators {
// All of the user's usernames, which will be used for identification during authentication.
@ -109,6 +109,50 @@ message WebAuthN {
bool user_verified = 4;
}
message StartWebAuthNRegistration {
// Domain on which the user currently is or will be authenticated.
string domain = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"my-domain.zitadel.cloud\"";
}
];
// Optionally specify the authenticator type of the passkey device (platform or cross-platform).
// If none is provided, both values are allowed.
WebAuthNAuthenticatorType authenticator_type = 2;
// Optionally provide a one time code generated by ZITADEL.
// This is required to start the passkey registration without user authentication.
optional AuthenticatorRegistrationCode code = 3;
}
message VerifyWebAuthNRegistration {
// PublicKeyCredential Interface.
// Generated helper methods populate the field from JSON created by a WebAuthN client.
// See also: https://www.w3.org/TR/webauthn/#publickeycredential
google.protobuf.Struct public_key_credential = 1 [
(validate.rules).message.required = true,
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "{\"type\":\"public-key\",\"id\":\"pawVarF4xPxLFmfCnRkwXWeTrKGzabcAi92LEI1WC00\",\"rawId\":\"pawVarF4xPxLFmfCnRkwXWeTrKGzabcAi92LEI1WC00\",\"response\":{\"attestationObject\":\"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgRKS3VpeE9tfExXRzkoUKnG4rQWPvtSSt4YtDGgTx32oCIQDPey-2YJ4uIg-QCM4jj6aE2U3tgMFM_RP7Efx6xRu3JGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAADju76085Yhmlt1CEOHkwLQAIKWsFWqxeMT8SxZnwp0ZMF1nk6yhs2m3AIvdixCNVgtNpQECAyYgASFYIMGUDSP2FAQn2MIfPMy7cyB_Y30VqixVgGULTBtFjfRiIlggjUGfQo3_-CrMmH3S-ZQkFKWKnNBQEAMkFtG-9A4zqW0\",\"clientDataJSON\":\"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQlhXdHh0WGxJeFZZa0pHT1dVaUVmM25zby02aXZKdWw2YmNmWHdMVlFIayIsIm9yaWdpbiI6Imh0dHBzOi8vbG9jYWxob3N0OjgwODAifQ\"}}";
min_length: 55;
max_length: 1048576; //1 MB
}
];
// Provide a name for the WebAuthN device. This will help identify it in the future.
string web_auth_n_name = 2 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
example: "\"fido key\""
}
];
}
message OTPSMS {
// unique identifier of the one-time-password (OTP) SMS authenticator.
string otp_sms_id = 1 [
@ -167,7 +211,7 @@ message AuthenticationKey {
example: "\"69629023906488334\"";
}
];
zitadel.object.v2.Details details = 2;
zitadel.resources.object.v3alpha.Details details = 2;
// the file type of the key
AuthNKeyType type = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
@ -246,6 +290,30 @@ message SetPassword {
}
// Provide if the user needs to change the password on the next use.
bool change_required = 3;
// If neither, the current password nor a verification code generated by the PasswordReset is provided,
// the user must be granted permission to set a password.
oneof verification {
// Provide the current password to verify you're allowed to change the password.
string current_password = 4 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
example: "\"Secr3tP4ssw0rd!\"";
}
];
// Or provider the verification code generated during password reset request.
string verification_code = 5 [
(validate.rules).string = {min_len: 1, max_len: 20},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 20;
example: "\"SKJd342k\"";
}
];
}
}
message SendPasswordResetEmail {

View File

@ -1,8 +1,8 @@
syntax = "proto3";
package zitadel.user.v3alpha;
package zitadel.resources.user.v3alpha;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v3alpha";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha;user";
import "google/api/field_behavior.proto";
import "protoc-gen-openapiv2/options/annotations.proto";

View File

@ -1,72 +1,71 @@
syntax = "proto3";
package zitadel.user.v3alpha;
package zitadel.resources.user.v3alpha;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v3alpha";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha;user";
import "google/api/field_behavior.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/user/v3alpha/user.proto";
import "zitadel/object/v2/object.proto";
import "zitadel/resources/user/v3alpha/user.proto";
import "zitadel/resources/object/v3alpha/object.proto";
message SearchQuery {
oneof query {
message SearchFilter {
oneof Filter {
option (validate.required) = true;
// Union the results of each sub query ('OR').
OrQuery or_query = 1;
// Union the results of each sub filter ('OR').
OrFilter or_filter = 1;
// Limit the result to match all sub queries ('AND').
// Note that if you specify multiple queries, they will be implicitly used as andQueries.
// Use the andQuery in combination with orQuery and notQuery.
AndQuery and_query = 2;
// Use the andFilter in combination with orFilter and notFilter.
AndFilter and_filter = 2;
// Exclude / Negate the result of the sub query ('NOT').
NotQuery not_query = 3;
NotFilter not_filter = 3;
// Limit the result to a specific user ID.
UserIDQuery user_id_query = 4;
UserIDFilter user_id_filter = 4;
// Limit the result to a specific organization.
OrganizationIDQuery organization_id_query = 5;
OrganizationIDFilter organization_id_filter = 5;
// Limit the result to a specific username.
UsernameQuery username_query = 6;
UsernameFilter username_filter = 6;
// Limit the result to a specific contact email.
EmailQuery email_query = 7;
EmailFilter email_filter = 7;
// Limit the result to a specific contact phone.
PhoneQuery phone_query = 8;
PhoneFilter phone_filter = 8;
// Limit the result to a specific state of the user.
StateQuery state_query = 9;
StateFilter state_filter = 9;
// Limit the result to a specific schema ID.
SchemaIDQuery schema_ID_query = 10;
SchemaIDFilter schema_id_filter = 10;
// Limit the result to a specific schema type.
SchemaTypeQuery schema_type_query = 11;
SchemaTypeFilter schema_type_filter = 11;
}
}
message OrQuery {
repeated SearchQuery queries = 1 [
message OrFilter {
repeated SearchFilter queries = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[{\"userIdQuery\": {\"id\": \"163840776835432705\",\"method\": \"TEXT_QUERY_METHOD_EQUALS\"}},{\"userIdQuery\": {\"id\": \"163840776835943483\",\"method\": \"TEXT_QUERY_METHOD_EQUALS\"}}]"
example: "[{\"userIdFilter\": {\"id\": \"163840776835432705\",\"method\": \"TEXT_FILTER_METHOD_EQUALS\"}},{\"userIdFilter\": {\"id\": \"163840776835943483\",\"method\": \"TEXT_FILTER_METHOD_EQUALS\"}}]"
}
];
}
message AndQuery {
repeated SearchQuery queries = 1 [
message AndFilter {
repeated SearchFilter queries = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[{\"organizationIdQuery\": {\"id\": \"163840776835432705\",\"method\": \"TEXT_QUERY_METHOD_EQUALS\"}},{\"usernameQuery\": {\"username\": \"gigi\",\"method\": \"TEXT_QUERY_METHOD_EQUALS\"}}]"
example: "[{\"organizationIdFilter\": {\"id\": \"163840776835432705\",\"method\": \"TEXT_FILTER_METHOD_EQUALS\"}},{\"usernameFilter\": {\"username\": \"gigi\",\"method\": \"TEXT_FILTER_METHOD_EQUALS\"}}]"
}
];
}
message NotQuery {
SearchQuery query = 1 [
message NotFilter {
SearchFilter query = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "{\"schemaIDQuery\": {\"id\": \"163840776835432705\"}}"
example: "{\"schemaIDFilter\": {\"id\": \"163840776835432705\"}}"
}
];
}
message UserIDQuery {
message UserIDFilter {
// Defines the ID of the user to query for.
string id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
@ -78,12 +77,12 @@ message UserIDQuery {
}
];
// Defines which text comparison method used for the id query.
zitadel.object.v2.TextQueryMethod method = 2 [
zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [
(validate.rules).enum.defined_only = true
];
}
message OrganizationIDQuery {
message OrganizationIDFilter {
// Defines the ID of the organization to query for.
string id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
@ -95,12 +94,12 @@ message OrganizationIDQuery {
}
];
// Defines which text comparison method used for the id query.
zitadel.object.v2.TextQueryMethod method = 2 [
zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [
(validate.rules).enum.defined_only = true
];
}
message UsernameQuery {
message UsernameFilter {
// Defines the username to query for.
string username = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
@ -112,14 +111,14 @@ message UsernameQuery {
}
];
// Defines which text comparison method used for the username query.
zitadel.object.v2.TextQueryMethod method = 2 [
zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [
(validate.rules).enum.defined_only = true
];
// Defines that the username must only be unique in the organisation.
bool is_organization_specific = 3;
}
message EmailQuery {
message EmailFilter {
// Defines the email of the user to query for.
string address = 1 [
(validate.rules).string = {max_len: 200},
@ -131,12 +130,12 @@ message EmailQuery {
}
];
// Defines which text comparison method used for the email query.
zitadel.object.v2.TextQueryMethod method = 2 [
zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [
(validate.rules).enum.defined_only = true
];
}
message PhoneQuery {
message PhoneFilter {
// Defines the phone of the user to query for.
string number = 1 [
(validate.rules).string = {min_len: 1, max_len: 20},
@ -148,13 +147,13 @@ message PhoneQuery {
}
];
// Defines which text comparison method used for the phone query.
zitadel.object.v2.TextQueryMethod method = 2 [
zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [
(validate.rules).enum.defined_only = true
];
}
message StateQuery {
message StateFilter {
// Defines the state to query for.
State state = 1 [
(validate.rules).enum.defined_only = true,
@ -164,7 +163,7 @@ message StateQuery {
];
}
message SchemaIDQuery {
message SchemaIDFilter {
// Defines the ID of the schema to query for.
string id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
@ -177,7 +176,7 @@ message SchemaIDQuery {
];
}
message SchemaTypeQuery {
message SchemaTypeFilter {
// Defines which type to query for.
string type = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
@ -189,7 +188,7 @@ message SchemaTypeQuery {
}
];
// Defines which text comparison method used for the type query.
zitadel.object.v2.TextQueryMethod method = 2 [
zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [
(validate.rules).enum.defined_only = true
];
}

View File

@ -0,0 +1,112 @@
syntax = "proto3";
package zitadel.resources.user.v3alpha;
import "google/api/field_behavior.proto";
import "google/protobuf/struct.proto";
import "google/protobuf/timestamp.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/resources/object/v3alpha/object.proto";
import "zitadel/resources/user/v3alpha/authenticator.proto";
import "zitadel/resources/user/v3alpha/communication.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha;user";
message CreateUser {
// Define the schema the user's data schema by providing it's ID.
string schema_id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"69629026806489455\"";
}
];
google.protobuf.Struct data = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "{\"name\":\"Gigi\",\"description\":\"the giraffe\"}"
}
];
// Set the contact information (email, phone) for the user.
SetContact contact = 3;
// Set the initial authenticators of the user.
SetAuthenticators authenticators = 4;
// Optionally set a unique identifier of the user. If unset, ZITADEL will take care of it.
optional string user_id = 5 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"69629026806489455\"";
}
];
}
message PatchUser {
optional string schema_id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"69629026806489455\"";
}
];
optional google.protobuf.Struct data = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "{\"name\":\"Gigi\",\"description\":\"the giraffe\"}"
}
];
// Set the contact information (email, phone) for the user.
optional SetContact contact = 3;
// TODO: No SetAuthenticators?
}
message GetUser{
// Details provide some base information (such as the last change date) of the user.
zitadel.resources.object.v3alpha.Details details = 1;
// The schema the user and it's data is based on.
GetSchema schema = 2;
google.protobuf.Struct data = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "{\"name\":\"Gigi\",\"description\":\"the giraffe\"}"
}
];
// Contact information for the user. ZITADEL will use this in case of internal notifications.
Contact contact = 4;
// The user's authenticators. They are used to identify and authenticate the user
// during the authentication process.
Authenticators authenticators = 5;
// State of the user.
State state = 6;
}
enum State {
USER_STATE_UNSPECIFIED = 0;
USER_STATE_ACTIVE = 1;
USER_STATE_INACTIVE = 2;
USER_STATE_DELETED = 3;
USER_STATE_LOCKED = 4;
}
message GetSchema {
// The unique identifier of the user schema.
string id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629026806489455\""
}
];
// The human readable name of the user schema.
string type = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"employees\"";
}
];
// The revision the user's data is based on of the revision.
uint32 revision = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "7";
}
];
}

View File

@ -1,25 +1,18 @@
syntax = "proto3";
package zitadel.user.schema.v3alpha;
package zitadel.resources.userschema.v3alpha;
import "google/api/field_behavior.proto";
import "google/protobuf/struct.proto";
import "validate/validate.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "zitadel/object/v2/object.proto";
import "zitadel/resources/object/v3alpha/object.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha;userschema";
message UserSchema {
// ID is the read-only unique identifier of the schema.
string id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629012906488334\""
}
];
// Details provide some base information (such as the last change date) of the schema.
zitadel.object.v2.Details details = 2;
zitadel.resources.object.v3alpha.Details details = 2;
// Type is a human readable text describing the schema.
string type = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
@ -60,55 +53,56 @@ enum FieldName {
FIELD_NAME_STATE = 2;
FIELD_NAME_REVISION = 3;
FIELD_NAME_CHANGE_DATE = 4;
FIELD_NAME_CREATION_DATE = 5;
}
message SearchQuery {
oneof query {
message SearchFilter {
oneof Filter {
option (validate.required) = true;
// Union the results of each sub query ('OR').
OrQuery or_query = 1;
// Union the results of each sub filter ('OR').
OrFilter or_filter = 1;
// Limit the result to match all sub queries ('AND').
// Note that if you specify multiple queries, they will be implicitly used as andQueries.
// Use the andQuery in combination with orQuery and notQuery.
AndQuery and_query = 2;
// Exclude / Negate the result of the sub query ('NOT').
NotQuery not_query = 3;
// Use the andFilter in combination with orFilter and notFilter.
AndFilter and_filter = 2;
// Exclude / Negate the result of the sub filter ('NOT').
NotFilter not_filter = 3;
// Limit the result to a specific schema type.
TypeQuery type_query = 5;
TypeFilter type_filter = 5;
// Limit the result to a specific state of the schema.
StateQuery state_query = 6;
StateFilter state_filter = 6;
// Limit the result to a specific schema ID.
IDQuery id_query = 7;
IDFilter id_filter = 7;
}
}
message OrQuery {
repeated SearchQuery queries = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[{\"idQuery\": {\"id\": \"163840776835432705\",\"method\": \"TEXT_QUERY_METHOD_EQUALS\"}},{\"idQuery\": {\"id\": \"163840776835943483\",\"method\": \"TEXT_QUERY_METHOD_EQUALS\"}}]"
message OrFilter {
repeated SearchFilter queries = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[{\"idFilter\": {\"id\": \"163840776835432705\",\"method\": \"TEXT_FILTER_METHOD_EQUALS\"}},{\"idFilter\": {\"id\": \"163840776835943483\",\"method\": \"TEXT_FILTER_METHOD_EQUALS\"}}]"
}
];
}
message AndQuery {
repeated SearchQuery queries = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[{\"typeQuery\": {\"id\": \"employees\",\"method\": \"TEXT_QUERY_METHOD_STARTS_WITH\"}},{\"stateQuery\": {\"state\": \"STATE_ACTIVE\"}}]"
message AndFilter {
repeated SearchFilter queries = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[{\"typeFilter\": {\"id\": \"employees\",\"method\": \"TEXT_FILTER_METHOD_STARTS_WITH\"}},{\"stateFilter\": {\"state\": \"STATE_ACTIVE\"}}]"
}
];
}
message NotQuery {
SearchQuery query = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "{\"stateQuery\": {\"state\": \"STATE_ACTIVE\"}}"
message NotFilter {
SearchFilter filter = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "{\"stateFilter\": {\"state\": \"STATE_ACTIVE\"}}"
}
];
}
message IDQuery {
// Defines the ID of the user schema to query for.
message IDFilter {
// Defines the ID of the user schema to filter for.
string id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
@ -118,14 +112,14 @@ message IDQuery {
example: "\"163840776835432705\"";
}
];
// Defines which text comparison method used for the id query.
zitadel.object.v2.TextQueryMethod method = 2 [
// Defines which text comparison method used for the id filter.
zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [
(validate.rules).enum.defined_only = true
];
}
message TypeQuery {
// Defines which type to query for.
message TypeFilter {
// Defines which type to filter for.
string type = 1 [
(validate.rules).string = {max_len: 200},
(google.api.field_behavior) = REQUIRED,
@ -134,14 +128,14 @@ message TypeQuery {
example: "\"employees\"";
}
];
// Defines which text comparison method used for the type query.
zitadel.object.v2.TextQueryMethod method = 2 [
// Defines which text comparison method used for the type filter.
zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [
(validate.rules).enum.defined_only = true
];
}
message StateQuery {
// Defines the state to query for.
message StateFilter {
// Defines the state to filter for.
State state = 1 [
(validate.rules).enum.defined_only = true,
(google.api.field_behavior) = REQUIRED,

View File

@ -1,6 +1,6 @@
syntax = "proto3";
package zitadel.user.schema.v3alpha;
package zitadel.resources.userschema.v3alpha;
import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
@ -8,12 +8,12 @@ import "google/protobuf/duration.proto";
import "google/protobuf/struct.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/object/v2/object.proto";
import "zitadel/object/v3alpha/object.proto";
import "zitadel/resources/object/v3alpha/object.proto";
import "zitadel/protoc_gen_zitadel/v2/options.proto";
import "zitadel/user/schema/v3alpha/user_schema.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha";
import "zitadel/resources/userschema/v3alpha/user_schema.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha;userschema";
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
info: {
@ -103,12 +103,12 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
}
};
service UserSchemaService {
service ZITADELUserSchemas {
// List user schemas
// Search user schemas
//
// List all matching user schemas. By default, we will return all user schema of your instance. Make sure to include a limit and sorting for pagination.
rpc ListUserSchemas (ListUserSchemasRequest) returns (ListUserSchemasResponse) {
// Search all matching user schemas. By default, we will return all user schema of your instance. Make sure to include a limit and sorting for pagination.
rpc SearchUserSchemas (SearchUserSchemasRequest) returns (SearchUserSchemasResponse) {
option (google.api.http) = {
post: "/v3alpha/user_schemas/search"
body: "*"
@ -124,13 +124,13 @@ service UserSchemaService {
responses: {
key: "200";
value: {
description: "A list of all user schema matching the query";
description: "A list of all user schema matching the search";
};
};
responses: {
key: "400";
value: {
description: "invalid list query";
description: "invalid search";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
@ -198,12 +198,12 @@ service UserSchemaService {
};
}
// Update a user schema
// Patch a user schema
//
// Update an existing user schema to a new revision. Users based on the current revision will not be affected until they are updated.
rpc UpdateUserSchema (UpdateUserSchemaRequest) returns (UpdateUserSchemaResponse) {
// Patch an existing user schema to a new revision. Users based on the current revision will not be affected until they are updated.
rpc PatchUserSchema (PatchUserSchemaRequest) returns (PatchUserSchemaResponse) {
option (google.api.http) = {
put: "/v3alpha/user_schemas/{id}"
patch: "/v3alpha/user_schemas/{id}"
body: "*"
};
@ -297,26 +297,31 @@ service UserSchemaService {
}
message ListUserSchemasRequest {
// list limitations and ordering.
zitadel.object.v2.ListQuery query = 1;
// the field the result is sorted.
zitadel.user.schema.v3alpha.FieldName sorting_column = 2 [
message SearchUserSchemasRequest {
optional zitadel.object.v3alpha.Instance instance = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"FIELD_NAME_TYPE\"";
default: "\"domain from HOST or :authority header\""
}
];
// Define the criteria to query for.
repeated zitadel.user.schema.v3alpha.SearchQuery queries = 3;
// list limitations and ordering.
optional zitadel.resources.object.v3alpha.SearchQuery query = 2;
// The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent.
optional FieldName sorting_column = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
default: "\"TARGET_FIELD_NAME_CREATION_DATE\""
}
];
// Define the criteria to filter for.
repeated SearchFilter filters = 4;
}
message ListUserSchemasResponse {
message SearchUserSchemasResponse {
// Details provides information about the returned result including total amount found.
zitadel.object.v2.ListDetails details = 1;
zitadel.resources.object.v3alpha.ListDetails details = 1;
// States by which field the results are sorted.
zitadel.user.schema.v3alpha.FieldName sorting_column = 2;
FieldName sorting_column = 2;
// The result contains the user schemas, which matched the queries.
repeated zitadel.user.schema.v3alpha.UserSchema result = 3;
repeated UserSchema result = 3;
}
@ -334,11 +339,29 @@ message GetUserSchemaByIDRequest {
}
message GetUserSchemaByIDResponse {
zitadel.user.schema.v3alpha.UserSchema schema = 1;
zitadel.resources.userschema.v3alpha.UserSchema schema = 1;
}
message CreateUserSchemaRequest {
optional zitadel.object.v3alpha.Instance instance = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
default: "\"domain from HOST or :authority header\""
}
];
CreateUserSchema user_schema = 2 [
(validate.rules).message = {
required: true
}
];
}
message CreateUserSchema{
optional zitadel.object.v3alpha.Instance instance = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
default: "\"domain from HOST or :authority header\""
}
];
// Type is a human readable word describing the schema.
string type = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
@ -373,14 +396,17 @@ message CreateUserSchemaRequest {
}
message CreateUserSchemaResponse {
// ID is the read-only unique identifier of the schema.
string id = 1;
// Details provide some base information (such as the last change date) of the schema.
zitadel.object.v2.Details details = 2;
zitadel.resources.object.v3alpha.Details details = 2;
}
message UpdateUserSchemaRequest {
message PatchUserSchemaRequest {
optional zitadel.object.v3alpha.Instance instance = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
default: "\"domain from HOST or :authority header\""
}
];
// unique identifier of the schema.
string id = 1;
// Type is a human readable word describing the schema.
@ -414,39 +440,75 @@ message UpdateUserSchemaRequest {
];
}
message UpdateUserSchemaResponse {
message PatchUserSchemaResponse {
// Details provide some base information (such as the last change date) of the schema.
zitadel.object.v2.Details details = 1;
zitadel.resources.object.v3alpha.Details details = 1;
}
message DeactivateUserSchemaRequest {
optional zitadel.object.v3alpha.Instance instance = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
default: "\"domain from HOST or :authority header\""
}
];
// unique identifier of the schema.
string id = 1;
string id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"69629026806489455\"";
}
];
}
message DeactivateUserSchemaResponse {
// Details provide some base information (such as the last change date) of the schema.
zitadel.object.v2.Details details = 1;
zitadel.resources.object.v3alpha.Details details = 1;
}
message ReactivateUserSchemaRequest {
optional zitadel.object.v3alpha.Instance instance = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
default: "\"domain from HOST or :authority header\""
}
];
// unique identifier of the schema.
string id = 1;
string id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"69629026806489455\"";
}
];
}
message ReactivateUserSchemaResponse {
// Details provide some base information (such as the last change date) of the schema.
zitadel.object.v2.Details details = 1;
zitadel.resources.object.v3alpha.Details details = 1;
}
message DeleteUserSchemaRequest {
optional zitadel.object.v3alpha.Instance instance = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
default: "\"domain from HOST or :authority header\""
}
];
// unique identifier of the schema.
string id = 1;
string id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"69629026806489455\"";
}
];
}
message DeleteUserSchemaResponse {
// Details provide some base information (such as the last change date) of the schema.
zitadel.object.v2.Details details = 1;
zitadel.resources.object.v3alpha.Details details = 1;
}

View File

@ -1,66 +0,0 @@
syntax = "proto3";
package zitadel.user.v3alpha;
import "google/api/field_behavior.proto";
import "google/protobuf/struct.proto";
import "google/protobuf/timestamp.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/object/v2/object.proto";
import "zitadel/user/v3alpha/authenticator.proto";
import "zitadel/user/v3alpha/communication.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v3alpha";
message User {
// ID is the read-only unique identifier of the user.
string user_id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629012906488334\"";
}
];
// Details provide some base information (such as the last change date) of the user.
zitadel.object.v2.Details details = 2;
// The user's authenticators. They are used to identify and authenticate the user
// during the authentication process.
Authenticators authenticators = 3;
// Contact information for the user. ZITADEL will use this in case of internal notifications.
Contact contact = 4;
// State of the user.
State state = 5;
// The schema the user and it's data is based on.
Schema schema = 6;
// The user's data based on the provided schema.
google.protobuf.Struct data = 7;
}
enum State {
USER_STATE_UNSPECIFIED = 0;
USER_STATE_ACTIVE = 1;
USER_STATE_INACTIVE = 2;
USER_STATE_DELETED = 3;
USER_STATE_LOCKED = 4;
}
message Schema {
// The unique identifier of the user schema.
string id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629026806489455\""
}
];
// The human readable name of the user schema.
string type = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"employees\"";
}
];
// The revision the user's data is based on of the revision.
uint32 revision = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "7";
}
];
}