feat: organization settings for user uniqueness (#10246)

# Which Problems Are Solved

Currently the username uniqueness is on instance level, we want to
achieve a way to set it at organization level.

# How the Problems Are Solved

Addition of endpoints and a resource on organization level, where this
setting can be managed. If nothing it set, the uniqueness is expected to
be at instance level, where only users with instance permissions should
be able to change this setting.

# Additional Changes

None

# Additional Context

Includes #10086
Closes #9964 

---------

Co-authored-by: Marco A. <marco@zitadel.com>
This commit is contained in:
Stefan Benz
2025-07-29 15:56:21 +02:00
committed by GitHub
parent b206a8ed87
commit 6d98b33c56
55 changed files with 4785 additions and 352 deletions

View File

@@ -94,3 +94,23 @@ message TimestampFilter {
(validate.rules).enum.defined_only = true
];
}
message InIDsFilter {
// Defines the ids to query for.
repeated string ids = 1 [
(validate.rules).repeated = {
unique: true
items: {
string: {
min_len: 1
max_len: 200
}
}
},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
example: "[\"69629023906488334\",\"69622366012355662\"]";
}
];
}

View File

@@ -638,7 +638,7 @@ service ProjectService {
// Returns a list of project grants. A project grant is when the organization grants its project to another organization.
//
// Required permission:
// - `project.grant.write`
// - `project.grant.read`
rpc ListProjectGrants(ListProjectGrantsRequest) returns (ListProjectGrantsResponse) {
option (google.api.http) = {
post: "/v2beta/projects/grants/search"

View File

@@ -17,6 +17,8 @@ import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "google/protobuf/struct.proto";
import "zitadel/settings/v2/settings.proto";
import "google/protobuf/timestamp.proto";
import "zitadel/filter/v2/filter.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2;settings";
@@ -415,7 +417,7 @@ service SettingsService {
}
};
};
option (google.api.http) = {
put: "/v2/settings/hosted_login_translation";
body: "*"
@@ -579,8 +581,8 @@ message GetHostedLoginTranslationResponse {
google.protobuf.Struct translations = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "{\"common\":{\"back\":\"Indietro\"},\"accounts\":{\"title\":\"Account\",\"description\":\"Seleziona l'account che desideri utilizzare.\",\"addAnother\":\"Aggiungi un altro account\",\"noResults\":\"Nessun account trovato\"}}";
description: "Translations contains the translations in the request language.";
example: "{\"common\":{\"back\":\"Indietro\"},\"accounts\":{\"title\":\"Account\",\"description\":\"Seleziona l'account che desideri utilizzare.\",\"addAnother\":\"Aggiungi un altro account\",\"noResults\":\"Nessun account trovato\"}}";
description: "Translations contains the translations in the request language.";
}
];
}
@@ -590,7 +592,7 @@ message SetHostedLoginTranslationRequest {
bool instance = 1 [(validate.rules).bool = {const: true}];
string organization_id = 2;
}
string locale = 3 [
(validate.rules).string = {min_len: 2},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
@@ -601,8 +603,8 @@ message SetHostedLoginTranslationRequest {
google.protobuf.Struct translations = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "{\"common\":{\"back\":\"Indietro\"},\"accounts\":{\"title\":\"Account\",\"description\":\"Seleziona l'account che desideri utilizzare.\",\"addAnother\":\"Aggiungi un altro account\",\"noResults\":\"Nessun account trovato\"}}";
description: "Translations should contain the translations in the specified locale.";
example: "{\"common\":{\"back\":\"Indietro\"},\"accounts\":{\"title\":\"Account\",\"description\":\"Seleziona l'account che desideri utilizzare.\",\"addAnother\":\"Aggiungi un altro account\",\"noResults\":\"Nessun account trovato\"}}";
description: "Translations should contain the translations in the specified locale.";
}
];
}
@@ -617,4 +619,4 @@ message SetHostedLoginTranslationResponse {
example: "\"42a1ba123e6ea6f0c93e286ed97c7018\"";
}
];
}
}

View File

@@ -0,0 +1,55 @@
syntax = "proto3";
package zitadel.settings.v2beta;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta;settings";
import "google/api/field_behavior.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "google/protobuf/timestamp.proto";
import "zitadel/filter/v2beta/filter.proto";
message OrganizationSettings {
// The unique identifier of the organization the settings belong to.
string organization_id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629012906488334\"";
}
];
// The timestamp of the organization settings creation.
google.protobuf.Timestamp creation_date = 2[
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2024-12-18T07:50:47.492Z\"";
}
];
// The timestamp of the last change to the organization settings.
google.protobuf.Timestamp change_date = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2025-01-23T10:34:18.051Z\"";
}
];
// Defines if the usernames have to be unique in the organization context.
bool organization_scoped_usernames = 4;
}
enum OrganizationSettingsFieldName {
ORGANIZATION_SETTINGS_FIELD_NAME_UNSPECIFIED = 0;
ORGANIZATION_SETTINGS_FIELD_NAME_ORGANIZATION_ID = 1;
ORGANIZATION_SETTINGS_FIELD_NAME_CREATION_DATE = 2;
ORGANIZATION_SETTINGS_FIELD_NAME_CHANGE_DATE = 3;
}
message OrganizationSettingsSearchFilter {
oneof filter {
option (validate.required) = true;
zitadel.filter.v2beta.InIDsFilter in_organization_ids_filter = 1;
OrganizationScopedUsernamesFilter organization_scoped_usernames_filter = 2;
}
}
// Query for organization settings with specific scopes usernames.
message OrganizationScopedUsernamesFilter {
bool organization_scoped_usernames = 1;
}

View File

@@ -15,6 +15,9 @@ import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "google/protobuf/timestamp.proto";
import "zitadel/filter/v2beta/filter.proto";
import "zitadel/settings/v2beta/organization_settings.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta;settings";
@@ -360,6 +363,98 @@ service SettingsService {
description: "Set the security settings of the ZITADEL instance."
};
}
// Set Organization Settings
//
// Sets the settings specific to an organization.
// Organization scopes usernames defines that the usernames have to be unique in the organization scope, can only be changed if the usernames of the users are unique in the scope.
//
// Required permissions:
// - `iam.policy.write`
rpc SetOrganizationSettings(SetOrganizationSettingsRequest) returns (SetOrganizationSettingsResponse) {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "The translations was successfully set.";
}
};
};
option (google.api.http) = {
post: "/v2/settings/organization";
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "authenticated"
}
};
}
// Delete Organization Settings
//
// Delete the settings specific to an organization.
//
// Required permissions:
// - `iam.policy.delete`
rpc DeleteOrganizationSettings(DeleteOrganizationSettingsRequest) returns (DeleteOrganizationSettingsResponse) {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "The translations was successfully set.";
}
};
};
option (google.api.http) = {
delete: "/v2/settings/organization";
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "authenticated"
}
};
}
// List Organization Settings
//
// Returns a list of organization settings.
//
// Required permission:
// - `iam.policy.read`
// - `org.policy.read`
rpc ListOrganizationSettings(ListOrganizationSettingsRequest) returns (ListOrganizationSettingsResponse) {
option (google.api.http) = {
post: "/v2/settings/organization/search"
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "authenticated"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "A list of all project grants matching the query";
};
};
responses: {
key: "400";
value: {
description: "invalid list query";
};
};
};
}
}
message GetLoginSettingsRequest {
@@ -474,4 +569,55 @@ message SetSecuritySettingsRequest{
message SetSecuritySettingsResponse{
zitadel.object.v2beta.Details details = 1;
}
}
message SetOrganizationSettingsRequest {
// Organization ID in which this settings are set.
string organization_id = 1;
// Force the usernames in the organization to be unique, only possible to set if the existing users already have unique usernames in the organization context.
optional bool organization_scoped_usernames = 2;
}
message SetOrganizationSettingsResponse {
// The timestamp of the set of the organization settings.
google.protobuf.Timestamp set_date = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2025-01-23T10:34:18.051Z\"";
}
];
}
message DeleteOrganizationSettingsRequest {
// Organization ID in which this settings are set.
string organization_id = 1;
}
message DeleteOrganizationSettingsResponse {
// The timestamp of the deletion of the organization settings.
google.protobuf.Timestamp deletion_date = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2025-01-23T10:34:18.051Z\"";
}
];
}
message ListOrganizationSettingsRequest{
// List limitations and ordering.
optional zitadel.filter.v2beta.PaginationRequest pagination = 1;
// The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent.
optional OrganizationSettingsFieldName sorting_column = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
default: "\"ORGANIZATION_SETTINGS_FIELD_NAME_CREATION_DATE\""
}
];
repeated OrganizationSettingsSearchFilter filters = 4;
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"sortingColumn\":\"ORGANIZATION_SETTINGS_FIELD_NAME_CREATION_DATE\",\"filters\":[{\"inOrganizationIdsFilter\":{\"ids\":[\"28746028909593987\"]}}]}";
};
}
message ListOrganizationSettingsResponse {
zitadel.filter.v2beta.PaginationResponse pagination = 1;
repeated OrganizationSettings organization_settings = 2;
}