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
}