mirror of
https://github.com/zitadel/zitadel.git
synced 2025-05-31 20:08:20 +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 {
|
||||
InstanceName string
|
||||
DefaultLanguage language.Tag
|
||||
Org command.OrgSetup
|
||||
Org command.InstanceOrgSetup
|
||||
MachineKeyPath string
|
||||
PatPath string
|
||||
|
||||
|
@ -36,6 +36,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/auth"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/management"
|
||||
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/settings/v2"
|
||||
"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 {
|
||||
return err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, org.CreateServer(commands, queries, permissionCheck)); err != nil {
|
||||
return err
|
||||
}
|
||||
instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader, login.IgnoreInstanceEndpoints...)
|
||||
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))
|
||||
|
@ -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
|
||||
|
||||
userID, objectDetails, err := s.command.SetUpOrg(ctx, &command.OrgSetup{
|
||||
createdOrg, err := s.command.SetUpOrg(ctx, &command.OrgSetup{
|
||||
Name: req.Org.Name,
|
||||
CustomDomain: req.Org.Domain,
|
||||
Human: human,
|
||||
Roles: req.Roles,
|
||||
}, userIDs...)
|
||||
Admins: []*command.OrgSetupAdmin{
|
||||
{
|
||||
Human: human,
|
||||
Roles: req.Roles,
|
||||
},
|
||||
},
|
||||
}, true, userIDs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var userID string
|
||||
if len(createdOrg.CreatedAdmins) == 1 {
|
||||
userID = createdOrg.CreatedAdmins[0].ID
|
||||
}
|
||||
return &admin_pb.SetUpOrgResponse{
|
||||
Details: object.DomainToAddDetailsPb(objectDetails),
|
||||
OrgId: objectDetails.ResourceOwner,
|
||||
Details: object.DomainToAddDetailsPb(createdOrg.ObjectDetails),
|
||||
OrgId: createdOrg.ObjectDetails.ResourceOwner,
|
||||
UserId: userID,
|
||||
}, 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) {
|
||||
human, err := addUserRequestToAddHuman(req)
|
||||
human, err := AddUserRequestToAddHuman(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -36,7 +36,7 @@ func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest
|
||||
}, nil
|
||||
}
|
||||
|
||||
func addUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman, error) {
|
||||
func AddUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman, error) {
|
||||
username := req.GetUsername()
|
||||
if username == "" {
|
||||
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)
|
||||
return
|
||||
}
|
||||
_, _, err = l.command.SetUpOrg(ctx, data.toCommandOrg(), userIDs...)
|
||||
_, err = l.command.SetUpOrg(ctx, data.toCommandOrg(), true, userIDs...)
|
||||
if err != nil {
|
||||
l.renderRegisterOrg(w, r, authRequest, data, err)
|
||||
return
|
||||
@ -144,15 +144,19 @@ func (d registerOrgFormData) toCommandOrg() *command.OrgSetup {
|
||||
}
|
||||
return &command.OrgSetup{
|
||||
Name: d.RegisterOrgName,
|
||||
Human: &command.AddHuman{
|
||||
Username: d.Username,
|
||||
FirstName: d.Firstname,
|
||||
LastName: d.Lastname,
|
||||
Email: command.Email{
|
||||
Address: d.Email,
|
||||
Admins: []*command.OrgSetupAdmin{
|
||||
{
|
||||
Human: &command.AddHuman{
|
||||
Username: d.Username,
|
||||
FirstName: d.Firstname,
|
||||
LastName: d.Lastname,
|
||||
Email: command.Email{
|
||||
Address: d.Email,
|
||||
},
|
||||
Password: d.Password,
|
||||
Register: true,
|
||||
},
|
||||
},
|
||||
Password: d.Password,
|
||||
Register: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ type InstanceSetup struct {
|
||||
InstanceName string
|
||||
CustomDomain string
|
||||
DefaultLanguage language.Tag
|
||||
Org OrgSetup
|
||||
Org InstanceOrgSetup
|
||||
SecretGenerators struct {
|
||||
PasswordSaltCost uint
|
||||
ClientSecret *crypto.GeneratorConfig
|
||||
|
@ -14,7 +14,10 @@ import (
|
||||
"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
|
||||
CustomDomain string
|
||||
Human *AddHuman
|
||||
@ -22,83 +25,210 @@ type OrgSetup struct {
|
||||
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) {
|
||||
userID, err = c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return "", "", nil, nil, err
|
||||
type OrgSetup struct {
|
||||
Name string
|
||||
CustomDomain string
|
||||
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)
|
||||
userAgg := user.NewAggregate(userID, orgID)
|
||||
|
||||
roles := []string{domain.RoleOrgOwner}
|
||||
if len(o.Roles) > 0 {
|
||||
roles = o.Roles
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *orgSetupCommands) setupOrgAdmin(admin *OrgSetupAdmin, allowInitialMail bool) error {
|
||||
if admin.ID != "" {
|
||||
c.validations = append(c.validations, c.commands.AddOrgMemberCommand(c.aggregate, admin.ID, orgAdminRoles(admin.Roles)...))
|
||||
return nil
|
||||
}
|
||||
userID, err := c.commands.idGenerator.Next()
|
||||
if err != nil {
|
||||
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
|
||||
if o.Human != nil {
|
||||
o.Human.ID = userID
|
||||
validations = append(validations, c.AddHumanCommand(o.Human, orgID, c.userPasswordHasher, c.userEncryption, true))
|
||||
} else if o.Machine != nil {
|
||||
validations = append(validations, AddMachineCommand(userAgg, o.Machine.Machine))
|
||||
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 {
|
||||
return "", "", nil, nil, err
|
||||
}
|
||||
pat.TokenID = tokenID
|
||||
validations = append(validations, prepareAddPersonalAccessToken(pat, c.keyAlgorithm))
|
||||
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
|
||||
}
|
||||
if o.Machine.MachineKey != nil {
|
||||
machineKey = NewMachineKey(orgID, userID, o.Machine.MachineKey.ExpirationDate, o.Machine.MachineKey.Type)
|
||||
keyID, err := c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return "", "", nil, nil, err
|
||||
}
|
||||
machineKey.KeyID = keyID
|
||||
validations = append(validations, prepareAddUserMachineKey(machineKey, c.keySize))
|
||||
pat.TokenID = tokenID
|
||||
c.pats = append(c.pats, pat)
|
||||
c.validations = append(c.validations, prepareAddPersonalAccessToken(pat, c.commands.keyAlgorithm))
|
||||
}
|
||||
if machine.MachineKey != nil {
|
||||
machineKey = NewMachineKey(orgAgg.ID, machine.Machine.AggregateID, machine.MachineKey.ExpirationDate, machine.MachineKey.Type)
|
||||
keyID, err := c.commands.idGenerator.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
machineKey.KeyID = keyID
|
||||
c.machineKeys = append(c.machineKeys, machineKey)
|
||||
c.validations = append(c.validations, prepareAddUserMachineKey(machineKey, c.commands.keySize))
|
||||
}
|
||||
validations = append(validations, c.AddOrgMemberCommand(orgAgg, userID, roles...))
|
||||
return nil
|
||||
}
|
||||
|
||||
if o.CustomDomain != "" {
|
||||
validations = append(validations, c.prepareAddOrgDomain(orgAgg, o.CustomDomain, userIDs))
|
||||
func (c *orgSetupCommands) addCustomDomain(domain string, userIDs []string) error {
|
||||
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 {
|
||||
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 {
|
||||
return "", "", nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pat != nil {
|
||||
token = pat.Token
|
||||
}
|
||||
|
||||
return userID, token, machineKey, &domain.ObjectDetails{
|
||||
Sequence: events[len(events)-1].Sequence(),
|
||||
EventDate: events[len(events)-1].CreationDate(),
|
||||
ResourceOwner: orgID,
|
||||
return &CreatedOrg{
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: events[len(events)-1].Sequence(),
|
||||
EventDate: events[len(events)-1].CreationDate(),
|
||||
ResourceOwner: c.aggregate.ID,
|
||||
},
|
||||
CreatedAdmins: c.createdAdmins(),
|
||||
}, 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()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userID, _, _, details, err := c.setUpOrgWithIDs(ctx, o, orgID, userIDs...)
|
||||
return userID, details, err
|
||||
return c.setUpOrgWithIDs(ctx, o, orgID, allowInitialMail, userIDs...)
|
||||
}
|
||||
|
||||
// AddOrgCommand defines the commands to create a new org,
|
||||
|
@ -3,11 +3,15 @@ package command
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
openid "github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"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"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/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"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/system"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||
@ -34,6 +35,7 @@ type Client struct {
|
||||
UserV2 user.UserServiceClient
|
||||
SessionV2 session.SessionServiceClient
|
||||
OIDCv2 oidc_pb.OIDCServiceClient
|
||||
OrgV2 organisation.OrganizationServiceClient
|
||||
System system.SystemServiceClient
|
||||
}
|
||||
|
||||
@ -46,6 +48,7 @@ func newClient(cc *grpc.ClientConn) Client {
|
||||
UserV2: user.NewUserServiceClient(cc),
|
||||
SessionV2: session.NewSessionServiceClient(cc),
|
||||
OIDCv2: oidc_pb.NewOIDCServiceClient(cc),
|
||||
OrgV2: organisation.NewOrganizationServiceClient(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 {
|
||||
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",
|
||||
ClientID: "clientID",
|
||||
ClientSecret: "clientSecret",
|
||||
|
@ -46,6 +46,10 @@ var (
|
||||
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
|
||||
// a short explinanation with the purpose
|
||||
// a serverice user.
|
||||
@ -153,11 +157,38 @@ func (s *Tester) pollHealth(ctx context.Context) (err error) {
|
||||
}
|
||||
|
||||
const (
|
||||
LoginUser = "loginClient"
|
||||
MachineUser = "integration"
|
||||
LoginUser = "loginClient"
|
||||
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
|
||||
|
||||
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())
|
||||
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")
|
||||
user, err := s.Queries.GetUser(ctx, true, true, usernameQuery)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
@ -175,69 +206,25 @@ func (s *Tester) createMachineUser(ctx context.Context, instanceId string) {
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
ResourceOwner: s.Organisation.ID,
|
||||
},
|
||||
Username: MachineUser,
|
||||
Name: MachineUser,
|
||||
Username: username,
|
||||
Name: username,
|
||||
Description: "who cares?",
|
||||
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)
|
||||
}
|
||||
logging.WithFields("username", SystemUser).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")
|
||||
}
|
||||
logging.WithFields("username", username).OnError(err).Fatal("get user")
|
||||
|
||||
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)
|
||||
_, err = s.Commands.AddPersonalAccessToken(ctx, pat)
|
||||
logging.WithFields("username", SystemUser).OnError(err).Fatal("add pat")
|
||||
s.Users.Set(instanceId, OrgOwner, &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{
|
||||
s.Users.Set(FirstInstanceUsersKey, userType, &User{
|
||||
User: user,
|
||||
Token: pat.Token,
|
||||
})
|
||||
return ctx, user
|
||||
}
|
||||
|
||||
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.createLoginClient(ctx)
|
||||
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())
|
||||
|
||||
return &tester
|
||||
|
@ -10,11 +10,14 @@ func _() {
|
||||
var x [1]struct{}
|
||||
_ = x[Unspecified-0]
|
||||
_ = 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 {
|
||||
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