feat: api v2beta to api v2 (#8283)

# Which Problems Are Solved

The v2beta services are stable but not GA.

# How the Problems Are Solved

The v2beta services are copied to v2. The corresponding v1 and v2beta
services are deprecated.

# Additional Context

Closes #7236

---------

Co-authored-by: Elio Bischof <elio@zitadel.com>
This commit is contained in:
Stefan Benz
2024-07-26 22:39:55 +02:00
committed by GitHub
parent bc16962aac
commit 7d2d85f57c
142 changed files with 15170 additions and 386 deletions

View File

@@ -7,7 +7,7 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/user/v2"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/zerrors"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
)
func (s *Server) AddOrganization(ctx context.Context, request *org.AddOrganizationRequest) (*org.AddOrganizationResponse, error) {

View File

@@ -14,8 +14,8 @@ import (
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
var (

View File

@@ -12,9 +12,9 @@ import (
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func Test_addOrganizationRequestToCommand(t *testing.T) {

View File

@@ -8,7 +8,7 @@ import (
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
)
var _ org.OrganizationServiceServer = (*Server)(nil)

View File

@@ -0,0 +1,83 @@
package org
import (
"context"
object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta"
user "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/zerrors"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
)
func (s *Server) AddOrganization(ctx context.Context, request *org.AddOrganizationRequest) (*org.AddOrganizationResponse, error) {
orgSetup, err := addOrganizationRequestToCommand(request)
if err != nil {
return nil, err
}
createdOrg, err := s.command.SetUpOrg(ctx, orgSetup, false)
if err != nil {
return nil, err
}
return createdOrganizationToPb(createdOrg)
}
func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*command.OrgSetup, error) {
admins, err := addOrganizationRequestAdminsToCommand(request.GetAdmins())
if err != nil {
return nil, err
}
return &command.OrgSetup{
Name: request.GetName(),
CustomDomain: "",
Admins: admins,
}, nil
}
func addOrganizationRequestAdminsToCommand(requestAdmins []*org.AddOrganizationRequest_Admin) (admins []*command.OrgSetupAdmin, err error) {
admins = make([]*command.OrgSetupAdmin, len(requestAdmins))
for i, admin := range requestAdmins {
admins[i], err = addOrganizationRequestAdminToCommand(admin)
if err != nil {
return nil, err
}
}
return admins, nil
}
func addOrganizationRequestAdminToCommand(admin *org.AddOrganizationRequest_Admin) (*command.OrgSetupAdmin, error) {
switch a := admin.GetUserType().(type) {
case *org.AddOrganizationRequest_Admin_UserId:
return &command.OrgSetupAdmin{
ID: a.UserId,
Roles: admin.GetRoles(),
}, nil
case *org.AddOrganizationRequest_Admin_Human:
human, err := user.AddUserRequestToAddHuman(a.Human)
if err != nil {
return nil, err
}
return &command.OrgSetupAdmin{
Human: human,
Roles: admin.GetRoles(),
}, nil
default:
return nil, zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", a)
}
}
func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *org.AddOrganizationResponse, err error) {
admins := make([]*org.AddOrganizationResponse_CreatedAdmin, len(createdOrg.CreatedAdmins))
for i, admin := range createdOrg.CreatedAdmins {
admins[i] = &org.AddOrganizationResponse_CreatedAdmin{
UserId: admin.ID,
EmailCode: admin.EmailCode,
PhoneCode: admin.PhoneCode,
}
}
return &org.AddOrganizationResponse{
Details: object.DomainToDetailsPb(createdOrg.ObjectDetails),
OrganizationId: createdOrg.ObjectDetails.ResourceOwner,
CreatedAdmins: admins,
}, nil
}

View File

@@ -0,0 +1,207 @@
//go:build integration
package org_test
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
var (
CTX context.Context
Tester *integration.Tester
Client org.OrganizationServiceClient
User *user.AddHumanUserResponse
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, errCtx, cancel := integration.Contexts(5 * time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
Client = Tester.Client.OrgV2beta
CTX, _ = Tester.WithAuthorization(ctx, integration.IAMOwner), errCtx
User = Tester.CreateHumanUser(CTX)
return m.Run()
}())
}
func TestServer_AddOrganization(t *testing.T) {
idpID := Tester.AddGenericOAuthProvider(t, CTX)
tests := []struct {
name string
ctx context.Context
req *org.AddOrganizationRequest
want *org.AddOrganizationResponse
wantErr bool
}{
{
name: "missing permission",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &org.AddOrganizationRequest{
Name: "name",
Admins: nil,
},
wantErr: true,
},
{
name: "empty name",
ctx: CTX,
req: &org.AddOrganizationRequest{
Name: "",
Admins: nil,
},
wantErr: true,
},
{
name: "invalid admin type",
ctx: CTX,
req: &org.AddOrganizationRequest{
Name: fmt.Sprintf("%d", time.Now().UnixNano()),
Admins: []*org.AddOrganizationRequest_Admin{
{},
},
},
wantErr: true,
},
{
name: "admin with init",
ctx: CTX,
req: &org.AddOrganizationRequest{
Name: fmt.Sprintf("%d", time.Now().UnixNano()),
Admins: []*org.AddOrganizationRequest_Admin{
{
UserType: &org.AddOrganizationRequest_Admin_Human{
Human: &user_v2beta.AddHumanUserRequest{
Profile: &user_v2beta.SetHumanProfile{
GivenName: "firstname",
FamilyName: "lastname",
},
Email: &user_v2beta.SetHumanEmail{
Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()),
Verification: &user_v2beta.SetHumanEmail_ReturnCode{
ReturnCode: &user_v2beta.ReturnEmailVerificationCode{},
},
},
},
},
},
},
},
want: &org.AddOrganizationResponse{
OrganizationId: integration.NotEmpty,
CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{
{
UserId: integration.NotEmpty,
EmailCode: gu.Ptr(integration.NotEmpty),
PhoneCode: nil,
},
},
},
},
{
name: "existing user and new human with idp",
ctx: CTX,
req: &org.AddOrganizationRequest{
Name: fmt.Sprintf("%d", time.Now().UnixNano()),
Admins: []*org.AddOrganizationRequest_Admin{
{
UserType: &org.AddOrganizationRequest_Admin_UserId{UserId: User.GetUserId()},
},
{
UserType: &org.AddOrganizationRequest_Admin_Human{
Human: &user_v2beta.AddHumanUserRequest{
Profile: &user_v2beta.SetHumanProfile{
GivenName: "firstname",
FamilyName: "lastname",
},
Email: &user_v2beta.SetHumanEmail{
Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()),
Verification: &user_v2beta.SetHumanEmail_IsVerified{
IsVerified: true,
},
},
IdpLinks: []*user_v2beta.IDPLink{
{
IdpId: idpID,
UserId: "userID",
UserName: "username",
},
},
},
},
},
},
},
want: &org.AddOrganizationResponse{
CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{
// a single admin is expected, because the first provided already exists
{
UserId: integration.NotEmpty,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.AddOrganization(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
// check details
assert.NotZero(t, got.GetDetails().GetSequence())
gotCD := got.GetDetails().GetChangeDate().AsTime()
now := time.Now()
assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute))
assert.NotEmpty(t, got.GetDetails().GetResourceOwner())
// organization id must be the same as the resourceOwner
assert.Equal(t, got.GetDetails().GetResourceOwner(), got.GetOrganizationId())
// check the admins
require.Len(t, got.GetCreatedAdmins(), len(tt.want.GetCreatedAdmins()))
for i, admin := range tt.want.GetCreatedAdmins() {
gotAdmin := got.GetCreatedAdmins()[i]
assertCreatedAdmin(t, admin, gotAdmin)
}
})
}
}
func assertCreatedAdmin(t *testing.T, expected, got *org.AddOrganizationResponse_CreatedAdmin) {
if expected.GetUserId() != "" {
assert.NotEmpty(t, got.GetUserId())
} else {
assert.Empty(t, got.GetUserId())
}
if expected.GetEmailCode() != "" {
assert.NotEmpty(t, got.GetEmailCode())
} else {
assert.Empty(t, got.GetEmailCode())
}
if expected.GetPhoneCode() != "" {
assert.NotEmpty(t, got.GetPhoneCode())
} else {
assert.Empty(t, got.GetPhoneCode())
}
}

View File

@@ -0,0 +1,172 @@
package org
import (
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func Test_addOrganizationRequestToCommand(t *testing.T) {
type args struct {
request *org.AddOrganizationRequest
}
tests := []struct {
name string
args args
want *command.OrgSetup
wantErr error
}{
{
name: "nil user",
args: args{
request: &org.AddOrganizationRequest{
Name: "name",
Admins: []*org.AddOrganizationRequest_Admin{
{},
},
},
},
wantErr: zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", nil),
},
{
name: "user ID",
args: args{
request: &org.AddOrganizationRequest{
Name: "name",
Admins: []*org.AddOrganizationRequest_Admin{
{
UserType: &org.AddOrganizationRequest_Admin_UserId{
UserId: "userID",
},
Roles: nil,
},
},
},
},
want: &command.OrgSetup{
Name: "name",
CustomDomain: "",
Admins: []*command.OrgSetupAdmin{
{
ID: "userID",
},
},
},
},
{
name: "human user",
args: args{
request: &org.AddOrganizationRequest{
Name: "name",
Admins: []*org.AddOrganizationRequest_Admin{
{
UserType: &org.AddOrganizationRequest_Admin_Human{
Human: &user.AddHumanUserRequest{
Profile: &user.SetHumanProfile{
GivenName: "firstname",
FamilyName: "lastname",
},
Email: &user.SetHumanEmail{
Email: "email@test.com",
},
},
},
Roles: nil,
},
},
},
},
want: &command.OrgSetup{
Name: "name",
CustomDomain: "",
Admins: []*command.OrgSetupAdmin{
{
Human: &command.AddHuman{
Username: "email@test.com",
FirstName: "firstname",
LastName: "lastname",
Email: command.Email{
Address: "email@test.com",
},
Metadata: make([]*command.AddMetadataEntry, 0),
Links: make([]*command.AddLink, 0),
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := addOrganizationRequestToCommand(tt.args.request)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}
func Test_createdOrganizationToPb(t *testing.T) {
now := time.Now()
type args struct {
createdOrg *command.CreatedOrg
}
tests := []struct {
name string
args args
want *org.AddOrganizationResponse
wantErr error
}{
{
name: "human user with phone and email code",
args: args{
createdOrg: &command.CreatedOrg{
ObjectDetails: &domain.ObjectDetails{
Sequence: 1,
EventDate: now,
ResourceOwner: "orgID",
},
CreatedAdmins: []*command.CreatedOrgAdmin{
{
ID: "id",
EmailCode: gu.Ptr("emailCode"),
PhoneCode: gu.Ptr("phoneCode"),
},
},
},
},
want: &org.AddOrganizationResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.New(now),
ResourceOwner: "orgID",
},
OrganizationId: "orgID",
CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{
{
UserId: "id",
EmailCode: gu.Ptr("emailCode"),
PhoneCode: gu.Ptr("phoneCode"),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := createdOrganizationToPb(tt.args.createdOrg)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -0,0 +1,55 @@
package org
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/domain"
"github.com/zitadel/zitadel/internal/query"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
)
var _ org.OrganizationServiceServer = (*Server)(nil)
type Server struct {
org.UnimplementedOrganizationServiceServer
command *command.Commands
query *query.Queries
checkPermission domain.PermissionCheck
}
type Config struct{}
func CreateServer(
command *command.Commands,
query *query.Queries,
checkPermission domain.PermissionCheck,
) *Server {
return &Server{
command: command,
query: query,
checkPermission: checkPermission,
}
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
org.RegisterOrganizationServiceServer(grpcServer, s)
}
func (s *Server) AppName() string {
return org.OrganizationService_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return org.OrganizationService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return org.OrganizationService_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return org.RegisterOrganizationServiceHandler
}