mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-07 16:32:24 +00:00
feat: Add CreateInstance endpoint (#9452)
This commit is contained in:
15
internal/api/grpc/authn/v2beta/converter.go
Normal file
15
internal/api/grpc/authn/v2beta/converter.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package authn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
authn "github.com/zitadel/zitadel/pkg/grpc/authn/v2beta"
|
||||||
|
)
|
||||||
|
|
||||||
|
func KeyTypeToDomain(t authn.KeyType) domain.AuthNKeyType {
|
||||||
|
switch t {
|
||||||
|
case authn.KeyType_KEY_TYPE_JSON:
|
||||||
|
return domain.AuthNKeyTypeJSON
|
||||||
|
default:
|
||||||
|
return domain.AuthNKeyTypeNONE
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,21 @@
|
|||||||
package instance
|
package instance
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||||
"github.com/zitadel/zitadel/cmd/build"
|
"github.com/zitadel/zitadel/cmd/build"
|
||||||
|
authn "github.com/zitadel/zitadel/internal/api/grpc/authn/v2beta"
|
||||||
filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta"
|
filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta"
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||||
|
z_oidc "github.com/zitadel/zitadel/internal/api/oidc"
|
||||||
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
"github.com/zitadel/zitadel/internal/config/systemdefaults"
|
"github.com/zitadel/zitadel/internal/config/systemdefaults"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
"github.com/zitadel/zitadel/pkg/grpc/instance/v2"
|
"github.com/zitadel/zitadel/pkg/grpc/instance/v2"
|
||||||
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
func InstancesToPb(instances []*query.Instance) []*instance.Instance {
|
func InstancesToPb(instances []*query.Instance) []*instance.Instance {
|
||||||
@@ -109,3 +117,126 @@ func instanceQueryToModel(searchQuery *instance.Query) (query.SearchQuery, error
|
|||||||
return nil, zerrors.ThrowInvalidArgument(nil, "INST-3m0se", "List.Query.Invalid")
|
return nil, zerrors.ThrowInvalidArgument(nil, "INST-3m0se", "List.Query.Invalid")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CreateInstancePbToSetupInstance(req *instance.CreateInstanceRequest, defaultInstance command.InstanceSetup, externalDomain string) *command.InstanceSetup {
|
||||||
|
instance := defaultInstance
|
||||||
|
if req.InstanceName != "" {
|
||||||
|
instance.InstanceName = req.InstanceName
|
||||||
|
instance.Org.Name = req.InstanceName
|
||||||
|
}
|
||||||
|
if req.CustomDomain != "" {
|
||||||
|
instance.CustomDomain = req.CustomDomain
|
||||||
|
}
|
||||||
|
if req.FirstOrgName != "" {
|
||||||
|
instance.Org.Name = req.FirstOrgName
|
||||||
|
}
|
||||||
|
|
||||||
|
if user := req.GetMachine(); user != nil {
|
||||||
|
defaultMachine := instance.Org.Machine
|
||||||
|
if defaultMachine == nil {
|
||||||
|
defaultMachine = new(command.AddMachine)
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.Org.Machine = createInstancePbToAddMachine(user, *defaultMachine)
|
||||||
|
instance.Org.Human = nil
|
||||||
|
} else if user := req.GetHuman(); user != nil {
|
||||||
|
defaultHuman := instance.Org.Human
|
||||||
|
if instance.Org.Human != nil {
|
||||||
|
defaultHuman = new(command.AddHuman)
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.Org.Human = createInstancePbToAddHuman(user, *defaultHuman, instance.DomainPolicy.UserLoginMustBeDomain, instance.Org.Name, externalDomain)
|
||||||
|
instance.Org.Machine = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if lang := language.Make(req.DefaultLanguage); !lang.IsRoot() {
|
||||||
|
instance.DefaultLanguage = lang
|
||||||
|
}
|
||||||
|
|
||||||
|
return &instance
|
||||||
|
}
|
||||||
|
|
||||||
|
func createInstancePbToAddHuman(req *instance.CreateInstanceRequest_Human, defaultHuman command.AddHuman, userLoginMustBeDomain bool, org, externalDomain string) *command.AddHuman {
|
||||||
|
user := defaultHuman
|
||||||
|
if req.Email != nil {
|
||||||
|
user.Email.Address = domain.EmailAddress(req.Email.Email)
|
||||||
|
user.Email.Verified = req.Email.IsEmailVerified
|
||||||
|
}
|
||||||
|
if req.Profile != nil {
|
||||||
|
if req.Profile.FirstName != "" {
|
||||||
|
user.FirstName = req.Profile.FirstName
|
||||||
|
}
|
||||||
|
if req.Profile.LastName != "" {
|
||||||
|
user.LastName = req.Profile.LastName
|
||||||
|
}
|
||||||
|
if req.Profile.PreferredLanguage != "" {
|
||||||
|
lang, err := language.Parse(req.Profile.PreferredLanguage)
|
||||||
|
if err == nil {
|
||||||
|
user.PreferredLanguage = lang
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check if default username is email style or else append @<orgname>.<custom-domain>
|
||||||
|
// this way we have the same value as before changing `UserLoginMustBeDomain` to false
|
||||||
|
if !userLoginMustBeDomain && !strings.Contains(user.Username, "@") {
|
||||||
|
orgDomain, _ := domain.NewIAMDomainName(org, externalDomain)
|
||||||
|
user.Username = user.Username + "@" + orgDomain
|
||||||
|
}
|
||||||
|
if req.UserName != "" {
|
||||||
|
user.Username = req.UserName
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Password != nil {
|
||||||
|
user.Password = req.Password.Password
|
||||||
|
user.PasswordChangeRequired = req.Password.PasswordChangeRequired
|
||||||
|
}
|
||||||
|
return &user
|
||||||
|
}
|
||||||
|
|
||||||
|
func createInstancePbToAddMachine(req *instance.CreateInstanceRequest_Machine, defaultMachine command.AddMachine) (machine *command.AddMachine) {
|
||||||
|
machine = new(command.AddMachine)
|
||||||
|
if defaultMachine.Machine != nil {
|
||||||
|
machineCopy := *defaultMachine.Machine
|
||||||
|
machine.Machine = &machineCopy
|
||||||
|
} else {
|
||||||
|
machine.Machine = new(command.Machine)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.UserName != "" {
|
||||||
|
machine.Machine.Username = req.UserName
|
||||||
|
}
|
||||||
|
if req.Name != "" {
|
||||||
|
machine.Machine.Name = req.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if defaultMachine.Pat != nil || req.PersonalAccessToken != nil {
|
||||||
|
pat := command.AddPat{
|
||||||
|
// Scopes are currently static and can not be overwritten
|
||||||
|
Scopes: []string{oidc.ScopeOpenID, oidc.ScopeProfile, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner},
|
||||||
|
}
|
||||||
|
if req.GetPersonalAccessToken().GetExpirationDate().IsValid() {
|
||||||
|
pat.ExpirationDate = req.PersonalAccessToken.ExpirationDate.AsTime()
|
||||||
|
} else if defaultMachine.Pat != nil && !defaultMachine.Pat.ExpirationDate.IsZero() {
|
||||||
|
pat.ExpirationDate = defaultMachine.Pat.ExpirationDate
|
||||||
|
}
|
||||||
|
machine.Pat = &pat
|
||||||
|
}
|
||||||
|
|
||||||
|
if defaultMachine.MachineKey != nil || req.MachineKey != nil {
|
||||||
|
machineKey := command.AddMachineKey{}
|
||||||
|
if defaultMachine.MachineKey != nil {
|
||||||
|
machineKey = *defaultMachine.MachineKey
|
||||||
|
}
|
||||||
|
if req.MachineKey != nil {
|
||||||
|
if req.MachineKey.Type != 0 {
|
||||||
|
machineKey.Type = authn.KeyTypeToDomain(req.MachineKey.Type)
|
||||||
|
}
|
||||||
|
if req.MachineKey.ExpirationDate.IsValid() {
|
||||||
|
machineKey.ExpirationDate = req.MachineKey.ExpirationDate.AsTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
machine.MachineKey = &machineKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return machine
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,28 @@ import (
|
|||||||
"github.com/zitadel/zitadel/pkg/grpc/instance/v2"
|
"github.com/zitadel/zitadel/pkg/grpc/instance/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (s *Server) CreateInstance(ctx context.Context, req *instance.CreateInstanceRequest) (*instance.CreateInstanceResponse, error) {
|
||||||
|
id, pat, key, details, err := s.command.SetUpInstance(ctx, CreateInstancePbToSetupInstance(req, s.defaultInstance, s.externalDomain))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var machineKey []byte
|
||||||
|
if key != nil {
|
||||||
|
machineKey, err = key.Detail()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &instance.CreateInstanceResponse{
|
||||||
|
Pat: pat,
|
||||||
|
MachineKey: machineKey,
|
||||||
|
InstanceId: id,
|
||||||
|
Details: object.DomainToDetailsPb(details),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) DeleteInstance(ctx context.Context, request *instance.DeleteInstanceRequest) (*instance.DeleteInstanceResponse, error) {
|
func (s *Server) DeleteInstance(ctx context.Context, request *instance.DeleteInstanceRequest) (*instance.DeleteInstanceResponse, error) {
|
||||||
instanceID := strings.TrimSpace(request.GetInstanceId())
|
instanceID := strings.TrimSpace(request.GetInstanceId())
|
||||||
if err := validateParam(instanceID, "instance_id"); err != nil {
|
if err := validateParam(instanceID, "instance_id"); err != nil {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ type Server struct {
|
|||||||
query *query.Queries
|
query *query.Queries
|
||||||
checkPermission domain.PermissionCheck
|
checkPermission domain.PermissionCheck
|
||||||
systemDefaults systemdefaults.SystemDefaults
|
systemDefaults systemdefaults.SystemDefaults
|
||||||
|
defaultInstance command.InstanceSetup
|
||||||
|
externalDomain string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct{}
|
type Config struct{}
|
||||||
@@ -29,12 +31,16 @@ func CreateServer(
|
|||||||
query *query.Queries,
|
query *query.Queries,
|
||||||
checkPermission domain.PermissionCheck,
|
checkPermission domain.PermissionCheck,
|
||||||
systemDefaults systemdefaults.SystemDefaults,
|
systemDefaults systemdefaults.SystemDefaults,
|
||||||
|
defaultInstance command.InstanceSetup,
|
||||||
|
externalDomain string,
|
||||||
) *Server {
|
) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
command: command,
|
command: command,
|
||||||
query: query,
|
query: query,
|
||||||
checkPermission: checkPermission,
|
checkPermission: checkPermission,
|
||||||
systemDefaults: systemDefaults,
|
systemDefaults: systemDefaults,
|
||||||
|
defaultInstance: defaultInstance,
|
||||||
|
externalDomain: externalDomain,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
35
proto/zitadel/authn/v2beta/authn.proto
Normal file
35
proto/zitadel/authn/v2beta/authn.proto
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
import "zitadel/object/v2/object.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
|
|
||||||
|
package zitadel.authn.v2beta;
|
||||||
|
|
||||||
|
option go_package ="github.com/zitadel/zitadel/pkg/grpc/authn/v2beta;authn";
|
||||||
|
|
||||||
|
message Key {
|
||||||
|
string id = 1 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"69629023906488334\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
zitadel.object.v2.Details details = 2;
|
||||||
|
KeyType type = 3 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"KEY_TYPE_JSON\"";
|
||||||
|
description: "the file type of the key";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
google.protobuf.Timestamp expiration_date = 4 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "the date a key will expire";
|
||||||
|
example: "\"3019-04-01T08:45:00.000000Z\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
enum KeyType {
|
||||||
|
KEY_TYPE_UNSPECIFIED = 0;
|
||||||
|
KEY_TYPE_JSON = 1;
|
||||||
|
}
|
||||||
@@ -6,10 +6,12 @@ import "validate/validate.proto";
|
|||||||
import "zitadel/object/v2/object.proto";
|
import "zitadel/object/v2/object.proto";
|
||||||
import "zitadel/instance/v2/instance.proto";
|
import "zitadel/instance/v2/instance.proto";
|
||||||
import "zitadel/filter/v2beta/filter.proto";
|
import "zitadel/filter/v2beta/filter.proto";
|
||||||
import "google/protobuf/empty.proto";
|
import "zitadel/authn/v2beta/authn.proto";
|
||||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
import "google/api/annotations.proto";
|
import "google/api/annotations.proto";
|
||||||
import "google/api/field_behavior.proto";
|
import "google/api/field_behavior.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
import "zitadel/protoc_gen_zitadel/v2/options.proto";
|
import "zitadel/protoc_gen_zitadel/v2/options.proto";
|
||||||
|
|
||||||
option go_package = "github.com/zitadel/zitadel/pkg/grpc/instance/v2;instance";
|
option go_package = "github.com/zitadel/zitadel/pkg/grpc/instance/v2;instance";
|
||||||
@@ -102,6 +104,72 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
message CreateInstanceRequest {
|
||||||
|
message Profile {
|
||||||
|
string first_name = 1 [(validate.rules).string = {max_len: 200}];
|
||||||
|
string last_name = 2 [(validate.rules).string = {max_len: 200}];
|
||||||
|
string preferred_language = 3 [(validate.rules).string = {max_len: 10}];
|
||||||
|
}
|
||||||
|
message Email {
|
||||||
|
string email = 1[(validate.rules).string = {min_len: 1, max_len: 200, email: true}];
|
||||||
|
bool is_email_verified = 2;
|
||||||
|
}
|
||||||
|
message Password {
|
||||||
|
string password = 1 [(validate.rules).string = {max_len: 200}];
|
||||||
|
bool password_change_required = 2;
|
||||||
|
}
|
||||||
|
message Human {
|
||||||
|
string user_name = 1 [(validate.rules).string = {max_len: 200}];
|
||||||
|
Email email = 2 [(validate.rules).message.required = true];
|
||||||
|
Profile profile = 3 [(validate.rules).message.required = false];
|
||||||
|
Password password = 4 [(validate.rules).message.required = false];
|
||||||
|
}
|
||||||
|
message PersonalAccessToken {
|
||||||
|
google.protobuf.Timestamp expiration_date = 1 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"2519-04-01T08:45:00.000000Z\"";
|
||||||
|
description: "The date the token will expire and no logins will be possible";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
message MachineKey {
|
||||||
|
zitadel.authn.v2beta.KeyType type = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}];
|
||||||
|
google.protobuf.Timestamp expiration_date = 2 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"2519-04-01T08:45:00.000000Z\"";
|
||||||
|
description: "The date the key will expire and no logins will be possible";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
message Machine {
|
||||||
|
string user_name = 1 [(validate.rules).string = {max_len: 200}];
|
||||||
|
string name = 2 [(validate.rules).string = {max_len: 200}];
|
||||||
|
PersonalAccessToken personal_access_token = 3;
|
||||||
|
MachineKey machine_key = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
string instance_name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||||
|
string first_org_name = 2 [(validate.rules).string = {max_len: 200}];
|
||||||
|
string custom_domain = 3 [(validate.rules).string = {max_len: 200}];
|
||||||
|
|
||||||
|
oneof owner {
|
||||||
|
option (validate.required) = true;
|
||||||
|
|
||||||
|
// oneof field for the user managing the instance
|
||||||
|
Human human = 4;
|
||||||
|
Machine machine = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
string default_language = 6 [(validate.rules).string = {max_len: 10}];
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateInstanceResponse {
|
||||||
|
string instance_id = 1;
|
||||||
|
zitadel.object.v2.Details details = 2;
|
||||||
|
string pat = 3;
|
||||||
|
bytes machine_key = 4;
|
||||||
|
}
|
||||||
|
|
||||||
message DeleteInstanceRequest {
|
message DeleteInstanceRequest {
|
||||||
string instance_id = 1 [
|
string instance_id = 1 [
|
||||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
@@ -164,6 +232,36 @@ message ListInstancesResponse {
|
|||||||
|
|
||||||
service InstanceService {
|
service InstanceService {
|
||||||
|
|
||||||
|
// CreateInstance creates a new instance
|
||||||
|
rpc CreateInstance(CreateInstanceRequest) returns (CreateInstanceResponse) {
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
description: "Creates a new instance";
|
||||||
|
tags: "Instance";
|
||||||
|
responses: {
|
||||||
|
key: "200";
|
||||||
|
value: {
|
||||||
|
description: "The created instance.";
|
||||||
|
schema: {
|
||||||
|
json_schema: {
|
||||||
|
ref: "#/definitions/CreateInstanceResponse";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
option (google.api.http) = {
|
||||||
|
post: "/v2/instances"
|
||||||
|
body: "*"
|
||||||
|
};
|
||||||
|
|
||||||
|
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||||
|
auth_option: {
|
||||||
|
permission: "system.instance.write"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteInstance deletes an instance with the given ID.
|
// DeleteInstance deletes an instance with the given ID.
|
||||||
rpc DeleteInstance(DeleteInstanceRequest) returns (DeleteInstanceResponse) {
|
rpc DeleteInstance(DeleteInstanceRequest) returns (DeleteInstanceResponse) {
|
||||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
|||||||
Reference in New Issue
Block a user