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
61 changed files with 5766 additions and 2247 deletions

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
}