mirror of
https://github.com/zitadel/zitadel.git
synced 2025-07-28 15:53:49 +00:00
feat(api): add organisation service (#6340)
* setup org with multiple admins * tests * add missing proto * remove machine users (for now) * update tests with idp case * fix package * organisation -> organization * fix test
This commit is contained in:
parent
77e561af72
commit
372755bddd
@ -21,7 +21,7 @@ import (
|
|||||||
type FirstInstance struct {
|
type FirstInstance struct {
|
||||||
InstanceName string
|
InstanceName string
|
||||||
DefaultLanguage language.Tag
|
DefaultLanguage language.Tag
|
||||||
Org command.OrgSetup
|
Org command.InstanceOrgSetup
|
||||||
MachineKeyPath string
|
MachineKeyPath string
|
||||||
PatPath string
|
PatPath string
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/api/grpc/auth"
|
"github.com/zitadel/zitadel/internal/api/grpc/auth"
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/management"
|
"github.com/zitadel/zitadel/internal/api/grpc/management"
|
||||||
oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2"
|
oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2"
|
||||||
|
"github.com/zitadel/zitadel/internal/api/grpc/org/v2"
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/session/v2"
|
"github.com/zitadel/zitadel/internal/api/grpc/session/v2"
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/settings/v2"
|
"github.com/zitadel/zitadel/internal/api/grpc/settings/v2"
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/system"
|
"github.com/zitadel/zitadel/internal/api/grpc/system"
|
||||||
@ -351,6 +352,9 @@ func startAPIs(
|
|||||||
if err := apis.RegisterService(ctx, settings.CreateServer(commands, queries, config.ExternalSecure)); err != nil {
|
if err := apis.RegisterService(ctx, settings.CreateServer(commands, queries, config.ExternalSecure)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := apis.RegisterService(ctx, org.CreateServer(commands, queries, permissionCheck)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader, login.IgnoreInstanceEndpoints...)
|
instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader, login.IgnoreInstanceEndpoints...)
|
||||||
assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge)
|
assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge)
|
||||||
apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle))
|
apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle))
|
||||||
|
@ -72,18 +72,26 @@ func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (*
|
|||||||
}
|
}
|
||||||
human := setUpOrgHumanToCommand(req.User.(*admin_pb.SetUpOrgRequest_Human_).Human) //TODO: handle machine
|
human := setUpOrgHumanToCommand(req.User.(*admin_pb.SetUpOrgRequest_Human_).Human) //TODO: handle machine
|
||||||
|
|
||||||
userID, objectDetails, err := s.command.SetUpOrg(ctx, &command.OrgSetup{
|
createdOrg, err := s.command.SetUpOrg(ctx, &command.OrgSetup{
|
||||||
Name: req.Org.Name,
|
Name: req.Org.Name,
|
||||||
CustomDomain: req.Org.Domain,
|
CustomDomain: req.Org.Domain,
|
||||||
|
Admins: []*command.OrgSetupAdmin{
|
||||||
|
{
|
||||||
Human: human,
|
Human: human,
|
||||||
Roles: req.Roles,
|
Roles: req.Roles,
|
||||||
}, userIDs...)
|
},
|
||||||
|
},
|
||||||
|
}, true, userIDs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
var userID string
|
||||||
|
if len(createdOrg.CreatedAdmins) == 1 {
|
||||||
|
userID = createdOrg.CreatedAdmins[0].ID
|
||||||
|
}
|
||||||
return &admin_pb.SetUpOrgResponse{
|
return &admin_pb.SetUpOrgResponse{
|
||||||
Details: object.DomainToAddDetailsPb(objectDetails),
|
Details: object.DomainToAddDetailsPb(createdOrg.ObjectDetails),
|
||||||
OrgId: objectDetails.ResourceOwner,
|
OrgId: createdOrg.ObjectDetails.ResourceOwner,
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
83
internal/api/grpc/org/v2/org.go
Normal file
83
internal/api/grpc/org/v2/org.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package org
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||||
|
"github.com/zitadel/zitadel/internal/api/grpc/user/v2"
|
||||||
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
|
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, caos_errs.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
|
||||||
|
}
|
206
internal/api/grpc/org/v2/org_integration_test.go
Normal file
206
internal/api/grpc/org/v2/org_integration_test.go
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
//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"
|
||||||
|
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
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.OrgV2
|
||||||
|
|
||||||
|
CTX, _ = Tester.WithAuthorization(ctx, integration.IAMOwner), errCtx
|
||||||
|
User = Tester.CreateHumanUser(CTX)
|
||||||
|
return m.Run()
|
||||||
|
}())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_AddOrganization(t *testing.T) {
|
||||||
|
idpID := Tester.AddGenericOAuthProvider(t)
|
||||||
|
|
||||||
|
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.AddHumanUserRequest{
|
||||||
|
Profile: &user.SetHumanProfile{
|
||||||
|
FirstName: "firstname",
|
||||||
|
LastName: "lastname",
|
||||||
|
},
|
||||||
|
Email: &user.SetHumanEmail{
|
||||||
|
Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()),
|
||||||
|
Verification: &user.SetHumanEmail_ReturnCode{
|
||||||
|
ReturnCode: &user.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.AddHumanUserRequest{
|
||||||
|
Profile: &user.SetHumanProfile{
|
||||||
|
FirstName: "firstname",
|
||||||
|
LastName: "lastname",
|
||||||
|
},
|
||||||
|
Email: &user.SetHumanEmail{
|
||||||
|
Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()),
|
||||||
|
Verification: &user.SetHumanEmail_IsVerified{
|
||||||
|
IsVerified: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
IdpLinks: []*user.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())
|
||||||
|
}
|
||||||
|
}
|
172
internal/api/grpc/org/v2/org_test.go
Normal file
172
internal/api/grpc/org/v2/org_test.go
Normal 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"
|
||||||
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
|
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||||
|
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
|
||||||
|
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
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: caos_errs.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{
|
||||||
|
FirstName: "firstname",
|
||||||
|
LastName: "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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
55
internal/api/grpc/org/v2/server.go
Normal file
55
internal/api/grpc/org/v2/server.go
Normal 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
|
||||||
|
}
|
@ -19,7 +19,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) {
|
func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) {
|
||||||
human, err := addUserRequestToAddHuman(req)
|
human, err := AddUserRequestToAddHuman(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -36,7 +36,7 @@ func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func addUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman, error) {
|
func AddUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman, error) {
|
||||||
username := req.GetUsername()
|
username := req.GetUsername()
|
||||||
if username == "" {
|
if username == "" {
|
||||||
username = req.GetEmail().GetEmail()
|
username = req.GetEmail().GetEmail()
|
||||||
|
@ -66,7 +66,7 @@ func (l *Login) handleRegisterOrgCheck(w http.ResponseWriter, r *http.Request) {
|
|||||||
l.renderRegisterOrg(w, r, authRequest, data, err)
|
l.renderRegisterOrg(w, r, authRequest, data, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, _, err = l.command.SetUpOrg(ctx, data.toCommandOrg(), userIDs...)
|
_, err = l.command.SetUpOrg(ctx, data.toCommandOrg(), true, userIDs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.renderRegisterOrg(w, r, authRequest, data, err)
|
l.renderRegisterOrg(w, r, authRequest, data, err)
|
||||||
return
|
return
|
||||||
@ -144,6 +144,8 @@ func (d registerOrgFormData) toCommandOrg() *command.OrgSetup {
|
|||||||
}
|
}
|
||||||
return &command.OrgSetup{
|
return &command.OrgSetup{
|
||||||
Name: d.RegisterOrgName,
|
Name: d.RegisterOrgName,
|
||||||
|
Admins: []*command.OrgSetupAdmin{
|
||||||
|
{
|
||||||
Human: &command.AddHuman{
|
Human: &command.AddHuman{
|
||||||
Username: d.Username,
|
Username: d.Username,
|
||||||
FirstName: d.Firstname,
|
FirstName: d.Firstname,
|
||||||
@ -154,5 +156,7 @@ func (d registerOrgFormData) toCommandOrg() *command.OrgSetup {
|
|||||||
Password: d.Password,
|
Password: d.Password,
|
||||||
Register: true,
|
Register: true,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ type InstanceSetup struct {
|
|||||||
InstanceName string
|
InstanceName string
|
||||||
CustomDomain string
|
CustomDomain string
|
||||||
DefaultLanguage language.Tag
|
DefaultLanguage language.Tag
|
||||||
Org OrgSetup
|
Org InstanceOrgSetup
|
||||||
SecretGenerators struct {
|
SecretGenerators struct {
|
||||||
PasswordSaltCost uint
|
PasswordSaltCost uint
|
||||||
ClientSecret *crypto.GeneratorConfig
|
ClientSecret *crypto.GeneratorConfig
|
||||||
|
@ -14,7 +14,10 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/repository/user"
|
"github.com/zitadel/zitadel/internal/repository/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OrgSetup struct {
|
// InstanceOrgSetup is used for the first organisation in the instance setup.
|
||||||
|
// It used to be called OrgSetup, which now allows multiple Users, but it's used in the config.yaml and therefore
|
||||||
|
// a breaking change was not possible.
|
||||||
|
type InstanceOrgSetup struct {
|
||||||
Name string
|
Name string
|
||||||
CustomDomain string
|
CustomDomain string
|
||||||
Human *AddHuman
|
Human *AddHuman
|
||||||
@ -22,83 +25,210 @@ type OrgSetup struct {
|
|||||||
Roles []string
|
Roles []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID string, userIDs ...string) (userID string, token string, machineKey *MachineKey, details *domain.ObjectDetails, err error) {
|
type OrgSetup struct {
|
||||||
userID, err = c.idGenerator.Next()
|
Name string
|
||||||
if err != nil {
|
CustomDomain string
|
||||||
return "", "", nil, nil, err
|
Admins []*OrgSetupAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrgSetupAdmin describes a user to be created (Human / Machine) or an existing (ID) to be used for an org setup.
|
||||||
|
type OrgSetupAdmin struct {
|
||||||
|
ID string
|
||||||
|
Human *AddHuman
|
||||||
|
Machine *AddMachine
|
||||||
|
Roles []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type orgSetupCommands struct {
|
||||||
|
validations []preparation.Validation
|
||||||
|
aggregate *org.Aggregate
|
||||||
|
commands *Commands
|
||||||
|
|
||||||
|
admins []*OrgSetupAdmin
|
||||||
|
pats []*PersonalAccessToken
|
||||||
|
machineKeys []*MachineKey
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatedOrg struct {
|
||||||
|
ObjectDetails *domain.ObjectDetails
|
||||||
|
CreatedAdmins []*CreatedOrgAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatedOrgAdmin struct {
|
||||||
|
ID string
|
||||||
|
EmailCode *string
|
||||||
|
PhoneCode *string
|
||||||
|
PAT *PersonalAccessToken
|
||||||
|
MachineKey *MachineKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID string, allowInitialMail bool, userIDs ...string) (_ *CreatedOrg, err error) {
|
||||||
|
cmds := c.newOrgSetupCommands(ctx, orgID, o, userIDs)
|
||||||
|
for _, admin := range o.Admins {
|
||||||
|
if err = cmds.setupOrgAdmin(admin, allowInitialMail); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if err = cmds.addCustomDomain(o.CustomDomain, userIDs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmds.push(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) newOrgSetupCommands(ctx context.Context, orgID string, orgSetup *OrgSetup, userIDs []string) *orgSetupCommands {
|
||||||
orgAgg := org.NewAggregate(orgID)
|
orgAgg := org.NewAggregate(orgID)
|
||||||
userAgg := user.NewAggregate(userID, orgID)
|
|
||||||
|
|
||||||
roles := []string{domain.RoleOrgOwner}
|
|
||||||
if len(o.Roles) > 0 {
|
|
||||||
roles = o.Roles
|
|
||||||
}
|
|
||||||
|
|
||||||
validations := []preparation.Validation{
|
validations := []preparation.Validation{
|
||||||
AddOrgCommand(ctx, orgAgg, o.Name, userIDs...),
|
AddOrgCommand(ctx, orgAgg, orgSetup.Name, userIDs...),
|
||||||
}
|
}
|
||||||
|
return &orgSetupCommands{
|
||||||
|
validations: validations,
|
||||||
|
aggregate: orgAgg,
|
||||||
|
commands: c,
|
||||||
|
admins: orgSetup.Admins,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var pat *PersonalAccessToken
|
func (c *orgSetupCommands) setupOrgAdmin(admin *OrgSetupAdmin, allowInitialMail bool) error {
|
||||||
if o.Human != nil {
|
if admin.ID != "" {
|
||||||
o.Human.ID = userID
|
c.validations = append(c.validations, c.commands.AddOrgMemberCommand(c.aggregate, admin.ID, orgAdminRoles(admin.Roles)...))
|
||||||
validations = append(validations, c.AddHumanCommand(o.Human, orgID, c.userPasswordHasher, c.userEncryption, true))
|
return nil
|
||||||
} else if o.Machine != nil {
|
}
|
||||||
validations = append(validations, AddMachineCommand(userAgg, o.Machine.Machine))
|
userID, err := c.commands.idGenerator.Next()
|
||||||
if o.Machine.Pat != nil {
|
|
||||||
pat = NewPersonalAccessToken(orgID, userID, o.Machine.Pat.ExpirationDate, o.Machine.Pat.Scopes, domain.UserTypeMachine)
|
|
||||||
tokenID, err := c.idGenerator.Next()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", nil, nil, err
|
return err
|
||||||
|
}
|
||||||
|
if admin.Human != nil {
|
||||||
|
admin.Human.ID = userID
|
||||||
|
c.validations = append(c.validations, c.commands.AddHumanCommand(admin.Human, c.aggregate.ID, c.commands.userPasswordHasher, c.commands.userEncryption, allowInitialMail))
|
||||||
|
} else if admin.Machine != nil {
|
||||||
|
admin.Machine.Machine.AggregateID = userID
|
||||||
|
if err = c.setupOrgAdminMachine(c.aggregate, admin.Machine); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.validations = append(c.validations, c.commands.AddOrgMemberCommand(c.aggregate, userID, orgAdminRoles(admin.Roles)...))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *orgSetupCommands) setupOrgAdminMachine(orgAgg *org.Aggregate, machine *AddMachine) error {
|
||||||
|
userAgg := user.NewAggregate(machine.Machine.AggregateID, orgAgg.ID)
|
||||||
|
c.validations = append(c.validations, AddMachineCommand(userAgg, machine.Machine))
|
||||||
|
var pat *PersonalAccessToken
|
||||||
|
var machineKey *MachineKey
|
||||||
|
if machine.Pat != nil {
|
||||||
|
pat = NewPersonalAccessToken(orgAgg.ID, machine.Machine.AggregateID, machine.Pat.ExpirationDate, machine.Pat.Scopes, domain.UserTypeMachine)
|
||||||
|
tokenID, err := c.commands.idGenerator.Next()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
pat.TokenID = tokenID
|
pat.TokenID = tokenID
|
||||||
validations = append(validations, prepareAddPersonalAccessToken(pat, c.keyAlgorithm))
|
c.pats = append(c.pats, pat)
|
||||||
|
c.validations = append(c.validations, prepareAddPersonalAccessToken(pat, c.commands.keyAlgorithm))
|
||||||
}
|
}
|
||||||
if o.Machine.MachineKey != nil {
|
if machine.MachineKey != nil {
|
||||||
machineKey = NewMachineKey(orgID, userID, o.Machine.MachineKey.ExpirationDate, o.Machine.MachineKey.Type)
|
machineKey = NewMachineKey(orgAgg.ID, machine.Machine.AggregateID, machine.MachineKey.ExpirationDate, machine.MachineKey.Type)
|
||||||
keyID, err := c.idGenerator.Next()
|
keyID, err := c.commands.idGenerator.Next()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", nil, nil, err
|
return err
|
||||||
}
|
}
|
||||||
machineKey.KeyID = keyID
|
machineKey.KeyID = keyID
|
||||||
validations = append(validations, prepareAddUserMachineKey(machineKey, c.keySize))
|
c.machineKeys = append(c.machineKeys, machineKey)
|
||||||
|
c.validations = append(c.validations, prepareAddUserMachineKey(machineKey, c.commands.keySize))
|
||||||
}
|
}
|
||||||
}
|
return nil
|
||||||
validations = append(validations, c.AddOrgMemberCommand(orgAgg, userID, roles...))
|
}
|
||||||
|
|
||||||
if o.CustomDomain != "" {
|
func (c *orgSetupCommands) addCustomDomain(domain string, userIDs []string) error {
|
||||||
validations = append(validations, c.prepareAddOrgDomain(orgAgg, o.CustomDomain, userIDs))
|
if domain != "" {
|
||||||
|
c.validations = append(c.validations, c.commands.prepareAddOrgDomain(c.aggregate, domain, userIDs))
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...)
|
func orgAdminRoles(roles []string) []string {
|
||||||
|
if len(roles) > 0 {
|
||||||
|
return roles
|
||||||
|
}
|
||||||
|
return []string{domain.RoleOrgOwner}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *orgSetupCommands) push(ctx context.Context) (_ *CreatedOrg, err error) {
|
||||||
|
cmds, err := preparation.PrepareCommands(ctx, c.commands.eventstore.Filter, c.validations...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
events, err := c.eventstore.Push(ctx, cmds...)
|
events, err := c.commands.eventstore.Push(ctx, cmds...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if pat != nil {
|
return &CreatedOrg{
|
||||||
token = pat.Token
|
ObjectDetails: &domain.ObjectDetails{
|
||||||
}
|
|
||||||
|
|
||||||
return userID, token, machineKey, &domain.ObjectDetails{
|
|
||||||
Sequence: events[len(events)-1].Sequence(),
|
Sequence: events[len(events)-1].Sequence(),
|
||||||
EventDate: events[len(events)-1].CreationDate(),
|
EventDate: events[len(events)-1].CreationDate(),
|
||||||
ResourceOwner: orgID,
|
ResourceOwner: c.aggregate.ID,
|
||||||
|
},
|
||||||
|
CreatedAdmins: c.createdAdmins(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) SetUpOrg(ctx context.Context, o *OrgSetup, userIDs ...string) (string, *domain.ObjectDetails, error) {
|
func (c *orgSetupCommands) createdAdmins() []*CreatedOrgAdmin {
|
||||||
|
users := make([]*CreatedOrgAdmin, 0, len(c.admins))
|
||||||
|
for _, admin := range c.admins {
|
||||||
|
if admin.ID != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if admin.Human != nil {
|
||||||
|
users = append(users, c.createdHumanAdmin(admin))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if admin.Machine != nil {
|
||||||
|
users = append(users, c.createdMachineAdmin(admin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *orgSetupCommands) createdHumanAdmin(admin *OrgSetupAdmin) *CreatedOrgAdmin {
|
||||||
|
createdAdmin := &CreatedOrgAdmin{
|
||||||
|
ID: admin.Human.ID,
|
||||||
|
}
|
||||||
|
if admin.Human.EmailCode != nil {
|
||||||
|
createdAdmin.EmailCode = admin.Human.EmailCode
|
||||||
|
}
|
||||||
|
return createdAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *orgSetupCommands) createdMachineAdmin(admin *OrgSetupAdmin) *CreatedOrgAdmin {
|
||||||
|
createdAdmin := &CreatedOrgAdmin{
|
||||||
|
ID: admin.Machine.Machine.AggregateID,
|
||||||
|
}
|
||||||
|
if admin.Machine.Pat != nil {
|
||||||
|
for _, pat := range c.pats {
|
||||||
|
if pat.AggregateID == createdAdmin.ID {
|
||||||
|
createdAdmin.PAT = pat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if admin.Machine.MachineKey != nil {
|
||||||
|
for _, key := range c.machineKeys {
|
||||||
|
if key.AggregateID == createdAdmin.ID {
|
||||||
|
createdAdmin.MachineKey = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return createdAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) SetUpOrg(ctx context.Context, o *OrgSetup, allowInitialMail bool, userIDs ...string) (*CreatedOrg, error) {
|
||||||
orgID, err := c.idGenerator.Next()
|
orgID, err := c.idGenerator.Next()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, _, _, details, err := c.setUpOrgWithIDs(ctx, o, orgID, userIDs...)
|
return c.setUpOrgWithIDs(ctx, o, orgID, allowInitialMail, userIDs...)
|
||||||
return userID, details, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddOrgCommand defines the commands to create a new org,
|
// AddOrgCommand defines the commands to create a new org,
|
||||||
|
@ -3,11 +3,15 @@ package command
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
openid "github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/errors"
|
"github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
@ -1325,3 +1329,450 @@ func TestCommandSide_RemoveOrg(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCommandSide_SetUpOrg(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||||
|
idGenerator id.Generator
|
||||||
|
newCode cryptoCodeFunc
|
||||||
|
keyAlgorithm crypto.EncryptionAlgorithm
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
setupOrg *OrgSetup
|
||||||
|
allowInitialMail bool
|
||||||
|
userIDs []string
|
||||||
|
}
|
||||||
|
type res struct {
|
||||||
|
createdOrg *CreatedOrg
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
res res
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "org name empty, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID"),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"),
|
||||||
|
setupOrg: &OrgSetup{
|
||||||
|
Name: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: errors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "userID not existing, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID"),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"),
|
||||||
|
setupOrg: &OrgSetup{
|
||||||
|
Name: "Org",
|
||||||
|
Admins: []*OrgSetupAdmin{
|
||||||
|
{
|
||||||
|
ID: "userID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: errors.ThrowPreconditionFailed(nil, "ORG-GoXOn", "Errors.User.NotFound"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "human invalid, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID", "userID"),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"),
|
||||||
|
setupOrg: &OrgSetup{
|
||||||
|
Name: "Org",
|
||||||
|
Admins: []*OrgSetupAdmin{
|
||||||
|
{
|
||||||
|
Human: &AddHuman{
|
||||||
|
Username: "",
|
||||||
|
FirstName: "firstname",
|
||||||
|
LastName: "lastname",
|
||||||
|
Email: Email{
|
||||||
|
Address: "email@test.ch",
|
||||||
|
Verified: true,
|
||||||
|
},
|
||||||
|
PreferredLanguage: language.English,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allowInitialMail: true,
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: errors.ThrowInvalidArgument(nil, "V2-zzad3", "Errors.Invalid.Argument"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "human added with initial mail",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(), // add human exists check
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("userID", "orgID").Aggregate,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(), // org member check
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "orgID").Aggregate,
|
||||||
|
"username",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"",
|
||||||
|
"firstname lastname",
|
||||||
|
language.English,
|
||||||
|
domain.GenderUnspecified,
|
||||||
|
"email@test.ch",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(org.NewOrgAddedEvent(context.Background(),
|
||||||
|
&org.NewAggregate("orgID").Aggregate,
|
||||||
|
"Org",
|
||||||
|
)),
|
||||||
|
eventFromEventPusher(org.NewDomainAddedEvent(context.Background(),
|
||||||
|
&org.NewAggregate("orgID").Aggregate, "org.iam-domain",
|
||||||
|
)),
|
||||||
|
eventFromEventPusher(org.NewDomainVerifiedEvent(context.Background(),
|
||||||
|
&org.NewAggregate("orgID").Aggregate,
|
||||||
|
"org.iam-domain",
|
||||||
|
)),
|
||||||
|
eventFromEventPusher(org.NewDomainPrimarySetEvent(context.Background(),
|
||||||
|
&org.NewAggregate("orgID").Aggregate,
|
||||||
|
"org.iam-domain",
|
||||||
|
)),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("userID", "orgID").Aggregate,
|
||||||
|
"username",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"",
|
||||||
|
"firstname lastname",
|
||||||
|
language.English,
|
||||||
|
domain.GenderUnspecified,
|
||||||
|
"email@test.ch",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanEmailVerifiedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("userID", "orgID").Aggregate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanInitialCodeAddedEvent(
|
||||||
|
context.Background(),
|
||||||
|
&user.NewAggregate("userID", "orgID").Aggregate,
|
||||||
|
&crypto.CryptoValue{
|
||||||
|
CryptoType: crypto.TypeEncryption,
|
||||||
|
Algorithm: "enc",
|
||||||
|
KeyID: "id",
|
||||||
|
Crypted: []byte("userinit"),
|
||||||
|
},
|
||||||
|
1*time.Hour,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(org.NewMemberAddedEvent(context.Background(),
|
||||||
|
&org.NewAggregate("orgID").Aggregate,
|
||||||
|
"userID",
|
||||||
|
domain.RoleOrgOwner,
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
uniqueConstraintsFromEventConstraint(org.NewAddOrgNameUniqueConstraint("Org")),
|
||||||
|
uniqueConstraintsFromEventConstraint(org.NewAddOrgDomainUniqueConstraint("org.iam-domain")),
|
||||||
|
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "orgID", true)),
|
||||||
|
uniqueConstraintsFromEventConstraint(member.NewAddMemberUniqueConstraint("orgID", "userID")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID", "userID"),
|
||||||
|
newCode: mockCode("userinit", time.Hour),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"),
|
||||||
|
setupOrg: &OrgSetup{
|
||||||
|
Name: "Org",
|
||||||
|
Admins: []*OrgSetupAdmin{
|
||||||
|
{
|
||||||
|
Human: &AddHuman{
|
||||||
|
Username: "username",
|
||||||
|
FirstName: "firstname",
|
||||||
|
LastName: "lastname",
|
||||||
|
Email: Email{
|
||||||
|
Address: "email@test.ch",
|
||||||
|
Verified: true,
|
||||||
|
},
|
||||||
|
PreferredLanguage: language.English,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allowInitialMail: true,
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
createdOrg: &CreatedOrg{
|
||||||
|
ObjectDetails: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "orgID",
|
||||||
|
},
|
||||||
|
CreatedAdmins: []*CreatedOrgAdmin{
|
||||||
|
{
|
||||||
|
ID: "userID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "existing human added",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "orgID").Aggregate,
|
||||||
|
"username",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"",
|
||||||
|
"firstname lastname",
|
||||||
|
language.English,
|
||||||
|
domain.GenderUnspecified,
|
||||||
|
"email@test.ch",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(), // org member check
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(org.NewOrgAddedEvent(context.Background(),
|
||||||
|
&org.NewAggregate("orgID").Aggregate,
|
||||||
|
"Org",
|
||||||
|
)),
|
||||||
|
eventFromEventPusher(org.NewDomainAddedEvent(context.Background(),
|
||||||
|
&org.NewAggregate("orgID").Aggregate, "org.iam-domain",
|
||||||
|
)),
|
||||||
|
eventFromEventPusher(org.NewDomainVerifiedEvent(context.Background(),
|
||||||
|
&org.NewAggregate("orgID").Aggregate,
|
||||||
|
"org.iam-domain",
|
||||||
|
)),
|
||||||
|
eventFromEventPusher(org.NewDomainPrimarySetEvent(context.Background(),
|
||||||
|
&org.NewAggregate("orgID").Aggregate,
|
||||||
|
"org.iam-domain",
|
||||||
|
)),
|
||||||
|
eventFromEventPusher(org.NewMemberAddedEvent(context.Background(),
|
||||||
|
&org.NewAggregate("orgID").Aggregate,
|
||||||
|
"userID",
|
||||||
|
domain.RoleOrgOwner,
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
uniqueConstraintsFromEventConstraint(org.NewAddOrgNameUniqueConstraint("Org")),
|
||||||
|
uniqueConstraintsFromEventConstraint(org.NewAddOrgDomainUniqueConstraint("org.iam-domain")),
|
||||||
|
uniqueConstraintsFromEventConstraint(member.NewAddMemberUniqueConstraint("orgID", "userID")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID"),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"),
|
||||||
|
setupOrg: &OrgSetup{
|
||||||
|
Name: "Org",
|
||||||
|
Admins: []*OrgSetupAdmin{
|
||||||
|
{
|
||||||
|
ID: "userID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allowInitialMail: true,
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
createdOrg: &CreatedOrg{
|
||||||
|
ObjectDetails: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "orgID",
|
||||||
|
},
|
||||||
|
CreatedAdmins: []*CreatedOrgAdmin{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "machine added with pat",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(), // add machine exists check
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("userID", "orgID").Aggregate,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(),
|
||||||
|
expectFilter(),
|
||||||
|
expectFilter(), // org member check
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "orgID").Aggregate,
|
||||||
|
"username",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"",
|
||||||
|
"firstname lastname",
|
||||||
|
language.English,
|
||||||
|
domain.GenderUnspecified,
|
||||||
|
"email@test.ch",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(org.NewOrgAddedEvent(context.Background(),
|
||||||
|
&org.NewAggregate("orgID").Aggregate,
|
||||||
|
"Org",
|
||||||
|
)),
|
||||||
|
eventFromEventPusher(org.NewDomainAddedEvent(context.Background(),
|
||||||
|
&org.NewAggregate("orgID").Aggregate, "org.iam-domain",
|
||||||
|
)),
|
||||||
|
eventFromEventPusher(org.NewDomainVerifiedEvent(context.Background(),
|
||||||
|
&org.NewAggregate("orgID").Aggregate,
|
||||||
|
"org.iam-domain",
|
||||||
|
)),
|
||||||
|
eventFromEventPusher(org.NewDomainPrimarySetEvent(context.Background(),
|
||||||
|
&org.NewAggregate("orgID").Aggregate,
|
||||||
|
"org.iam-domain",
|
||||||
|
)),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewMachineAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("userID", "orgID").Aggregate,
|
||||||
|
"username",
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
true,
|
||||||
|
domain.OIDCTokenTypeBearer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewPersonalAccessTokenAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("userID", "orgID").Aggregate,
|
||||||
|
"tokenID",
|
||||||
|
testNow.Add(time.Hour),
|
||||||
|
[]string{openid.ScopeOpenID},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(org.NewMemberAddedEvent(context.Background(),
|
||||||
|
&org.NewAggregate("orgID").Aggregate,
|
||||||
|
"userID",
|
||||||
|
domain.RoleOrgOwner,
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
uniqueConstraintsFromEventConstraint(org.NewAddOrgNameUniqueConstraint("Org")),
|
||||||
|
uniqueConstraintsFromEventConstraint(org.NewAddOrgDomainUniqueConstraint("org.iam-domain")),
|
||||||
|
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "orgID", true)),
|
||||||
|
uniqueConstraintsFromEventConstraint(member.NewAddMemberUniqueConstraint("orgID", "userID")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID", "userID", "tokenID"),
|
||||||
|
newCode: mockCode("userinit", time.Hour),
|
||||||
|
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"),
|
||||||
|
setupOrg: &OrgSetup{
|
||||||
|
Name: "Org",
|
||||||
|
Admins: []*OrgSetupAdmin{
|
||||||
|
{
|
||||||
|
Machine: &AddMachine{
|
||||||
|
Machine: &Machine{
|
||||||
|
Username: "username",
|
||||||
|
Name: "name",
|
||||||
|
Description: "description",
|
||||||
|
AccessTokenType: domain.OIDCTokenTypeBearer,
|
||||||
|
},
|
||||||
|
Pat: &AddPat{
|
||||||
|
ExpirationDate: testNow.Add(time.Hour),
|
||||||
|
Scopes: []string{openid.ScopeOpenID},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
createdOrg: &CreatedOrg{
|
||||||
|
ObjectDetails: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "orgID",
|
||||||
|
},
|
||||||
|
CreatedAdmins: []*CreatedOrgAdmin{
|
||||||
|
{
|
||||||
|
ID: "userID",
|
||||||
|
PAT: &PersonalAccessToken{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "userID",
|
||||||
|
ResourceOwner: "orgID",
|
||||||
|
},
|
||||||
|
ExpirationDate: testNow.Add(time.Hour),
|
||||||
|
Scopes: []string{openid.ScopeOpenID},
|
||||||
|
AllowedUserType: domain.UserTypeMachine,
|
||||||
|
TokenID: "tokenID",
|
||||||
|
Token: "dG9rZW5JRDp1c2VySUQ", // token
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := &Commands{
|
||||||
|
eventstore: tt.fields.eventstore(t),
|
||||||
|
idGenerator: tt.fields.idGenerator,
|
||||||
|
newCode: tt.fields.newCode,
|
||||||
|
keyAlgorithm: tt.fields.keyAlgorithm,
|
||||||
|
zitadelRoles: []authz.RoleMapping{
|
||||||
|
{
|
||||||
|
Role: domain.RoleOrgOwner,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
got, err := r.SetUpOrg(tt.args.ctx, tt.args.setupOrg, tt.args.allowInitialMail, tt.args.userIDs...)
|
||||||
|
assert.ErrorIs(t, err, tt.res.err)
|
||||||
|
assert.Equal(t, tt.res.createdOrg, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
|
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
|
||||||
|
organisation "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
|
||||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
|
session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
|
||||||
"github.com/zitadel/zitadel/pkg/grpc/system"
|
"github.com/zitadel/zitadel/pkg/grpc/system"
|
||||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||||
@ -34,6 +35,7 @@ type Client struct {
|
|||||||
UserV2 user.UserServiceClient
|
UserV2 user.UserServiceClient
|
||||||
SessionV2 session.SessionServiceClient
|
SessionV2 session.SessionServiceClient
|
||||||
OIDCv2 oidc_pb.OIDCServiceClient
|
OIDCv2 oidc_pb.OIDCServiceClient
|
||||||
|
OrgV2 organisation.OrganizationServiceClient
|
||||||
System system.SystemServiceClient
|
System system.SystemServiceClient
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,6 +48,7 @@ func newClient(cc *grpc.ClientConn) Client {
|
|||||||
UserV2: user.NewUserServiceClient(cc),
|
UserV2: user.NewUserServiceClient(cc),
|
||||||
SessionV2: session.NewSessionServiceClient(cc),
|
SessionV2: session.NewSessionServiceClient(cc),
|
||||||
OIDCv2: oidc_pb.NewOIDCServiceClient(cc),
|
OIDCv2: oidc_pb.NewOIDCServiceClient(cc),
|
||||||
|
OrgV2: organisation.NewOrganizationServiceClient(cc),
|
||||||
System: system.NewSystemServiceClient(cc),
|
System: system.NewSystemServiceClient(cc),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,7 +156,7 @@ func (s *Tester) SetUserPassword(ctx context.Context, userID, password string) {
|
|||||||
|
|
||||||
func (s *Tester) AddGenericOAuthProvider(t *testing.T) string {
|
func (s *Tester) AddGenericOAuthProvider(t *testing.T) string {
|
||||||
ctx := authz.WithInstance(context.Background(), s.Instance)
|
ctx := authz.WithInstance(context.Background(), s.Instance)
|
||||||
id, _, err := s.Commands.AddOrgGenericOAuthProvider(ctx, s.Organisation.ID, command.GenericOAuthProvider{
|
id, _, err := s.Commands.AddInstanceGenericOAuthProvider(ctx, command.GenericOAuthProvider{
|
||||||
Name: "idp",
|
Name: "idp",
|
||||||
ClientID: "clientID",
|
ClientID: "clientID",
|
||||||
ClientSecret: "clientSecret",
|
ClientSecret: "clientSecret",
|
||||||
|
@ -46,6 +46,10 @@ var (
|
|||||||
systemUserKey []byte
|
systemUserKey []byte
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// NotEmpty can be used as placeholder, when the returned values is unknown.
|
||||||
|
// It can be used in tests to assert whether a value should be empty or not.
|
||||||
|
const NotEmpty = "not empty"
|
||||||
|
|
||||||
// UserType provides constants that give
|
// UserType provides constants that give
|
||||||
// a short explinanation with the purpose
|
// a short explinanation with the purpose
|
||||||
// a serverice user.
|
// a serverice user.
|
||||||
@ -154,10 +158,37 @@ func (s *Tester) pollHealth(ctx context.Context) (err error) {
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
LoginUser = "loginClient"
|
LoginUser = "loginClient"
|
||||||
MachineUser = "integration"
|
MachineUserOrgOwner = "integrationOrgOwner"
|
||||||
|
MachineUserInstanceOwner = "integrationInstanceOwner"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Tester) createMachineUser(ctx context.Context, instanceId string) {
|
func (s *Tester) createMachineUserOrgOwner(ctx context.Context) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ctx, user := s.createMachineUser(ctx, MachineUserOrgOwner, OrgOwner)
|
||||||
|
_, err = s.Commands.AddOrgMember(ctx, user.ResourceOwner, user.ID, "ORG_OWNER")
|
||||||
|
target := new(caos_errs.AlreadyExistsError)
|
||||||
|
if !errors.As(err, &target) {
|
||||||
|
logging.OnError(err).Fatal("add org member")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Tester) createMachineUserInstanceOwner(ctx context.Context) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ctx, user := s.createMachineUser(ctx, MachineUserInstanceOwner, IAMOwner)
|
||||||
|
_, err = s.Commands.AddInstanceMember(ctx, user.ID, "IAM_OWNER")
|
||||||
|
target := new(caos_errs.AlreadyExistsError)
|
||||||
|
if !errors.As(err, &target) {
|
||||||
|
logging.OnError(err).Fatal("add instance member")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Tester) createLoginClient(ctx context.Context) {
|
||||||
|
s.createMachineUser(ctx, LoginUser, Login)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Tester) createMachineUser(ctx context.Context, username string, userType UserType) (context.Context, *query.User) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
s.Instance, err = s.Queries.InstanceByHost(ctx, s.Host())
|
s.Instance, err = s.Queries.InstanceByHost(ctx, s.Host())
|
||||||
@ -167,7 +198,7 @@ func (s *Tester) createMachineUser(ctx context.Context, instanceId string) {
|
|||||||
s.Organisation, err = s.Queries.OrgByID(ctx, true, s.Instance.DefaultOrganisationID())
|
s.Organisation, err = s.Queries.OrgByID(ctx, true, s.Instance.DefaultOrganisationID())
|
||||||
logging.OnError(err).Fatal("query organisation")
|
logging.OnError(err).Fatal("query organisation")
|
||||||
|
|
||||||
usernameQuery, err := query.NewUserUsernameSearchQuery(MachineUser, query.TextEquals)
|
usernameQuery, err := query.NewUserUsernameSearchQuery(username, query.TextEquals)
|
||||||
logging.OnError(err).Fatal("user query")
|
logging.OnError(err).Fatal("user query")
|
||||||
user, err := s.Queries.GetUser(ctx, true, true, usernameQuery)
|
user, err := s.Queries.GetUser(ctx, true, true, usernameQuery)
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
@ -175,69 +206,25 @@ func (s *Tester) createMachineUser(ctx context.Context, instanceId string) {
|
|||||||
ObjectRoot: models.ObjectRoot{
|
ObjectRoot: models.ObjectRoot{
|
||||||
ResourceOwner: s.Organisation.ID,
|
ResourceOwner: s.Organisation.ID,
|
||||||
},
|
},
|
||||||
Username: MachineUser,
|
Username: username,
|
||||||
Name: MachineUser,
|
Name: username,
|
||||||
Description: "who cares?",
|
Description: "who cares?",
|
||||||
AccessTokenType: domain.OIDCTokenTypeJWT,
|
AccessTokenType: domain.OIDCTokenTypeJWT,
|
||||||
})
|
})
|
||||||
logging.WithFields("username", SystemUser).OnError(err).Fatal("add machine user")
|
logging.WithFields("username", username).OnError(err).Fatal("add machine user")
|
||||||
user, err = s.Queries.GetUser(ctx, true, true, usernameQuery)
|
user, err = s.Queries.GetUser(ctx, true, true, usernameQuery)
|
||||||
}
|
}
|
||||||
logging.WithFields("username", SystemUser).OnError(err).Fatal("get user")
|
logging.WithFields("username", username).OnError(err).Fatal("get user")
|
||||||
|
|
||||||
_, err = s.Commands.AddOrgMember(ctx, s.Organisation.ID, user.ID, "ORG_OWNER")
|
|
||||||
target := new(caos_errs.AlreadyExistsError)
|
|
||||||
if !errors.As(err, &target) {
|
|
||||||
logging.OnError(err).Fatal("add org member")
|
|
||||||
}
|
|
||||||
|
|
||||||
scopes := []string{oidc.ScopeOpenID, oidc.ScopeProfile, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner}
|
scopes := []string{oidc.ScopeOpenID, oidc.ScopeProfile, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner}
|
||||||
pat := command.NewPersonalAccessToken(user.ResourceOwner, user.ID, time.Now().Add(time.Hour), scopes, domain.UserTypeMachine)
|
pat := command.NewPersonalAccessToken(user.ResourceOwner, user.ID, time.Now().Add(time.Hour), scopes, domain.UserTypeMachine)
|
||||||
_, err = s.Commands.AddPersonalAccessToken(ctx, pat)
|
_, err = s.Commands.AddPersonalAccessToken(ctx, pat)
|
||||||
logging.WithFields("username", SystemUser).OnError(err).Fatal("add pat")
|
logging.WithFields("username", SystemUser).OnError(err).Fatal("add pat")
|
||||||
s.Users.Set(instanceId, OrgOwner, &User{
|
s.Users.Set(FirstInstanceUsersKey, userType, &User{
|
||||||
User: user,
|
|
||||||
Token: pat.Token,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Tester) createLoginClient(ctx context.Context) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
s.Instance, err = s.Queries.InstanceByHost(ctx, s.Host())
|
|
||||||
logging.OnError(err).Fatal("query instance")
|
|
||||||
ctx = authz.WithInstance(ctx, s.Instance)
|
|
||||||
|
|
||||||
s.Organisation, err = s.Queries.OrgByID(ctx, true, s.Instance.DefaultOrganisationID())
|
|
||||||
logging.OnError(err).Fatal("query organisation")
|
|
||||||
|
|
||||||
usernameQuery, err := query.NewUserUsernameSearchQuery(LoginUser, query.TextEquals)
|
|
||||||
logging.WithFields("username", LoginUser).OnError(err).Fatal("user query")
|
|
||||||
user, err := s.Queries.GetUser(ctx, true, true, usernameQuery)
|
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
|
||||||
_, err = s.Commands.AddMachine(ctx, &command.Machine{
|
|
||||||
ObjectRoot: models.ObjectRoot{
|
|
||||||
ResourceOwner: s.Organisation.ID,
|
|
||||||
},
|
|
||||||
Username: LoginUser,
|
|
||||||
Name: LoginUser,
|
|
||||||
Description: "who cares?",
|
|
||||||
AccessTokenType: domain.OIDCTokenTypeJWT,
|
|
||||||
})
|
|
||||||
logging.WithFields("username", LoginUser).OnError(err).Fatal("add machine user")
|
|
||||||
user, err = s.Queries.GetUser(ctx, true, true, usernameQuery)
|
|
||||||
}
|
|
||||||
logging.WithFields("username", LoginUser).OnError(err).Fatal("get user")
|
|
||||||
|
|
||||||
scopes := []string{oidc.ScopeOpenID, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner}
|
|
||||||
pat := command.NewPersonalAccessToken(user.ResourceOwner, user.ID, time.Now().Add(time.Hour), scopes, domain.UserTypeMachine)
|
|
||||||
_, err = s.Commands.AddPersonalAccessToken(ctx, pat)
|
|
||||||
logging.OnError(err).Fatal("add pat")
|
|
||||||
|
|
||||||
s.Users.Set(FirstInstanceUsersKey, Login, &User{
|
|
||||||
User: user,
|
User: user,
|
||||||
Token: pat.Token,
|
Token: pat.Token,
|
||||||
})
|
})
|
||||||
|
return ctx, user
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Tester) WithAuthorization(ctx context.Context, u UserType) context.Context {
|
func (s *Tester) WithAuthorization(ctx context.Context, u UserType) context.Context {
|
||||||
@ -333,7 +320,8 @@ func NewTester(ctx context.Context) *Tester {
|
|||||||
tester.createClientConn(ctx)
|
tester.createClientConn(ctx)
|
||||||
tester.createLoginClient(ctx)
|
tester.createLoginClient(ctx)
|
||||||
tester.WebAuthN = webauthn.NewClient(tester.Config.WebAuthNName, tester.Config.ExternalDomain, http_util.BuildOrigin(tester.Host(), tester.Config.ExternalSecure))
|
tester.WebAuthN = webauthn.NewClient(tester.Config.WebAuthNName, tester.Config.ExternalDomain, http_util.BuildOrigin(tester.Host(), tester.Config.ExternalSecure))
|
||||||
tester.createMachineUser(ctx, FirstInstanceUsersKey)
|
tester.createMachineUserOrgOwner(ctx)
|
||||||
|
tester.createMachineUserInstanceOwner(ctx)
|
||||||
tester.WebAuthN = webauthn.NewClient(tester.Config.WebAuthNName, tester.Config.ExternalDomain, "https://"+tester.Host())
|
tester.WebAuthN = webauthn.NewClient(tester.Config.WebAuthNName, tester.Config.ExternalDomain, "https://"+tester.Host())
|
||||||
|
|
||||||
return &tester
|
return &tester
|
||||||
|
@ -10,11 +10,14 @@ func _() {
|
|||||||
var x [1]struct{}
|
var x [1]struct{}
|
||||||
_ = x[Unspecified-0]
|
_ = x[Unspecified-0]
|
||||||
_ = x[OrgOwner-1]
|
_ = x[OrgOwner-1]
|
||||||
|
_ = x[Login-2]
|
||||||
|
_ = x[IAMOwner-3]
|
||||||
|
_ = x[SystemUser-4]
|
||||||
}
|
}
|
||||||
|
|
||||||
const _UserType_name = "UnspecifiedOrgOwner"
|
const _UserType_name = "UnspecifiedOrgOwnerLoginIAMOwnerSystemUser"
|
||||||
|
|
||||||
var _UserType_index = [...]uint8{0, 11, 19}
|
var _UserType_index = [...]uint8{0, 11, 19, 24, 32, 42}
|
||||||
|
|
||||||
func (i UserType) String() string {
|
func (i UserType) String() string {
|
||||||
if i < 0 || i >= UserType(len(_UserType_index)-1) {
|
if i < 0 || i >= UserType(len(_UserType_index)-1) {
|
||||||
|
145
proto/zitadel/org/v2beta/org_service.proto
Normal file
145
proto/zitadel/org/v2beta/org_service.proto
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
|
||||||
|
package zitadel.org.v2beta;
|
||||||
|
|
||||||
|
import "zitadel/object/v2alpha/object.proto";
|
||||||
|
import "zitadel/protoc_gen_zitadel/v2/options.proto";
|
||||||
|
import "zitadel/user/v2alpha/auth.proto";
|
||||||
|
import "zitadel/user/v2alpha/email.proto";
|
||||||
|
import "zitadel/user/v2alpha/phone.proto";
|
||||||
|
import "zitadel/user/v2alpha/idp.proto";
|
||||||
|
import "zitadel/user/v2alpha/password.proto";
|
||||||
|
import "zitadel/user/v2alpha/user.proto";
|
||||||
|
import "zitadel/user/v2alpha/user_service.proto";
|
||||||
|
import "google/api/annotations.proto";
|
||||||
|
import "google/api/field_behavior.proto";
|
||||||
|
import "google/protobuf/duration.proto";
|
||||||
|
import "google/protobuf/struct.proto";
|
||||||
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
|
import "validate/validate.proto";
|
||||||
|
|
||||||
|
option go_package = "github.com/zitadel/zitadel/pkg/grpc/org/v2beta;org";
|
||||||
|
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
|
||||||
|
info: {
|
||||||
|
title: "User Service";
|
||||||
|
version: "2.0-alpha";
|
||||||
|
description: "This API is intended to manage organizations in a ZITADEL instance. This project is in beta state. It can AND will continue breaking until the services provide the same functionality as the current login.";
|
||||||
|
contact:{
|
||||||
|
name: "ZITADEL"
|
||||||
|
url: "https://zitadel.com"
|
||||||
|
email: "hi@zitadel.com"
|
||||||
|
}
|
||||||
|
license: {
|
||||||
|
name: "Apache 2.0",
|
||||||
|
url: "https://github.com/zitadel/zitadel/blob/main/LICENSE";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
schemes: HTTPS;
|
||||||
|
schemes: HTTP;
|
||||||
|
|
||||||
|
consumes: "application/json";
|
||||||
|
consumes: "application/grpc";
|
||||||
|
|
||||||
|
produces: "application/json";
|
||||||
|
produces: "application/grpc";
|
||||||
|
|
||||||
|
consumes: "application/grpc-web+proto";
|
||||||
|
produces: "application/grpc-web+proto";
|
||||||
|
|
||||||
|
host: "$ZITADEL_DOMAIN";
|
||||||
|
base_path: "/";
|
||||||
|
|
||||||
|
external_docs: {
|
||||||
|
description: "Detailed information about ZITADEL",
|
||||||
|
url: "https://zitadel.com/docs"
|
||||||
|
}
|
||||||
|
|
||||||
|
responses: {
|
||||||
|
key: "403";
|
||||||
|
value: {
|
||||||
|
description: "Returned when the user does not have permission to access the resource.";
|
||||||
|
schema: {
|
||||||
|
json_schema: {
|
||||||
|
ref: "#/definitions/rpcStatus";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
responses: {
|
||||||
|
key: "404";
|
||||||
|
value: {
|
||||||
|
description: "Returned when the resource does not exist.";
|
||||||
|
schema: {
|
||||||
|
json_schema: {
|
||||||
|
ref: "#/definitions/rpcStatus";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
service OrganizationService {
|
||||||
|
|
||||||
|
// Create a new organization and grant the user(s) permission to manage it
|
||||||
|
rpc AddOrganization(AddOrganizationRequest) returns (AddOrganizationResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
post: "/v2beta/organizations"
|
||||||
|
body: "*"
|
||||||
|
};
|
||||||
|
|
||||||
|
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||||
|
auth_option: {
|
||||||
|
permission: "org.create"
|
||||||
|
}
|
||||||
|
http_response: {
|
||||||
|
success_code: 201
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
summary: "Create an Organization";
|
||||||
|
description: "Create a new organization with an administrative user. If no specific roles are sent for the users, they will be granted the role ORG_OWNER."
|
||||||
|
responses: {
|
||||||
|
key: "200"
|
||||||
|
value: {
|
||||||
|
description: "OK";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddOrganizationRequest{
|
||||||
|
message Admin {
|
||||||
|
oneof user_type{
|
||||||
|
string user_id = 1;
|
||||||
|
zitadel.user.v2alpha.AddHumanUserRequest human = 2;
|
||||||
|
}
|
||||||
|
// specify Org Member Roles for the provided user (default is ORG_OWNER if roles are empty)
|
||||||
|
repeated string roles = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
string name = 1 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(google.api.field_behavior) = REQUIRED,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
min_length: 1;
|
||||||
|
max_length: 200;
|
||||||
|
example: "\"ZITADEL\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
repeated Admin admins = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddOrganizationResponse{
|
||||||
|
message CreatedAdmin {
|
||||||
|
string user_id = 1;
|
||||||
|
optional string email_code = 2;
|
||||||
|
optional string phone_code = 3;
|
||||||
|
}
|
||||||
|
zitadel.object.v2alpha.Details details = 1;
|
||||||
|
string organization_id = 2;
|
||||||
|
repeated CreatedAdmin created_admins = 3;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user