feat: Add CreateInstance endpoint (#9452)

This commit is contained in:
Marco Ardizzone
2025-04-28 16:13:20 +02:00
parent 45ad238ecd
commit 3b005c4e06
6 changed files with 308 additions and 1 deletions

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

View File

@@ -1,13 +1,21 @@
package instance
import (
"strings"
"github.com/zitadel/oidc/v3/pkg/oidc"
"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"
"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/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
"github.com/zitadel/zitadel/pkg/grpc/instance/v2"
"golang.org/x/text/language"
)
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")
}
}
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
}

View File

@@ -9,6 +9,28 @@ import (
"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) {
instanceID := strings.TrimSpace(request.GetInstanceId())
if err := validateParam(instanceID, "instance_id"); err != nil {

View File

@@ -20,6 +20,8 @@ type Server struct {
query *query.Queries
checkPermission domain.PermissionCheck
systemDefaults systemdefaults.SystemDefaults
defaultInstance command.InstanceSetup
externalDomain string
}
type Config struct{}
@@ -29,12 +31,16 @@ func CreateServer(
query *query.Queries,
checkPermission domain.PermissionCheck,
systemDefaults systemdefaults.SystemDefaults,
defaultInstance command.InstanceSetup,
externalDomain string,
) *Server {
return &Server{
command: command,
query: query,
checkPermission: checkPermission,
systemDefaults: systemDefaults,
defaultInstance: defaultInstance,
externalDomain: externalDomain,
}
}

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

View File

@@ -6,10 +6,12 @@ import "validate/validate.proto";
import "zitadel/object/v2/object.proto";
import "zitadel/instance/v2/instance.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 "google/protobuf/empty.proto";
import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "google/protobuf/timestamp.proto";
import "zitadel/protoc_gen_zitadel/v2/options.proto";
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 {
string instance_id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
@@ -164,6 +232,36 @@ message ListInstancesResponse {
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.
rpc DeleteInstance(DeleteInstanceRequest) returns (DeleteInstanceResponse) {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {