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:
Livio Spring 2023-08-11 16:19:14 +02:00 committed by GitHub
parent 77e561af72
commit 372755bddd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1382 additions and 130 deletions

View File

@ -21,7 +21,7 @@ import (
type FirstInstance struct {
InstanceName string
DefaultLanguage language.Tag
Org command.OrgSetup
Org command.InstanceOrgSetup
MachineKeyPath string
PatPath string

View File

@ -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))

View File

@ -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
}

View 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
}

View 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())
}
}

View File

@ -0,0 +1,172 @@
package org
import (
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
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)
})
}
}

View File

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

View File

@ -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()

View File

@ -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,
},
}
}

View File

@ -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

View File

@ -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,

View File

@ -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)
})
}
}

View File

@ -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",

View File

@ -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

View File

@ -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) {

View 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;
}