feat(api): feature flags (#7356)

* feat(api): feature API proto definitions

* update proto based on discussion with @livio-a

* cleanup old feature flag stuff

* authz instance queries

* align defaults

* projection definitions

* define commands and event reducers

* implement system and instance setter APIs

* api getter implementation

* unit test repository package

* command unit tests

* unit test Get queries

* grpc converter unit tests

* migrate the V1 features

* migrate oidc to dynamic features

* projection unit test

* fix instance by host

* fix instance by id data type in sql

* fix linting errors

* add system projection test

* fix behavior inversion

* resolve proto file comments

* rename SystemDefaultLoginInstanceEventType to SystemLoginDefaultOrgEventType so it's consistent with the instance level event

* use write models and conditional set events

* system features integration tests

* instance features integration tests

* error on empty request

* documentation entry

* typo in feature.proto

* fix start unit tests

* solve linting error on key case switch

* remove system defaults after discussion with @eliobischof

* fix system feature projection

* resolve comments in defaults.yaml

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Tim Möhlmann
2024-02-28 10:55:54 +02:00
committed by GitHub
parent 2801167668
commit 26d1563643
79 changed files with 4580 additions and 868 deletions

View File

@@ -0,0 +1,35 @@
syntax = "proto3";
package zitadel.feature.v2beta;
import "protoc-gen-openapiv2/options/annotations.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature";
enum Source {
SOURCE_UNSPECIFIED = 0;
reserved 1; // in case we want to implement a "DEFAULT" level
SOURCE_SYSTEM = 2;
SOURCE_INSTANCE = 3;
SOURCE_ORGANIZATION = 4;
SOURCE_PROJECT = 5; // reserved for future use
SOURCE_APP = 6; // reserved for future use
SOURCE_USER = 7;
}
// FeatureFlag is a simple boolean Feature setting, without further payload.
message FeatureFlag {
bool enabled = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "false";
description: "Whether a feature is enabled.";
}
];
Source source = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "The source where the setting of the feature was defined. The source may be the resource itself or a resource owner through inheritance.";
}
];
}

View File

@@ -0,0 +1,395 @@
syntax = "proto3";
package zitadel.feature.v2beta;
import "google/api/annotations.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "zitadel/feature/v2beta/system.proto";
import "zitadel/feature/v2beta/instance.proto";
import "zitadel/feature/v2beta/organization.proto";
import "zitadel/feature/v2beta/user.proto";
import "zitadel/protoc_gen_zitadel/v2/options.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature";
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
info: {
title: "Feature Service";
version: "2.0-beta";
description: "This API is intended to manage features for ZITADEL. Feature settings that are available on multiple \"levels\", such as instance and organization. The higher level instance acts as a default for the lower level. When a feature is set on multiple levels, the lower level takes precedence. Features can be experimental where ZITADEL will assume a sane default, such as disabled. When over time confidence in such a feature grows, ZITADEL can default to enabling the feature. As a final step we might choose to always enable a feature and remove the setting from this API, reserving the proto field number. Such removal is not considered a breaking change. Setting a removed field will effectively result in a no-op. This project is in beta state. It can AND will continue breaking until a stable version is released.";
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: "$CUSTOM-DOMAIN";
base_path: "/";
external_docs: {
description: "Detailed information about ZITADEL",
url: "https://zitadel.com/docs"
}
security_definitions: {
security: {
key: "OAuth2";
value: {
type: TYPE_OAUTH2;
flow: FLOW_ACCESS_CODE;
authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize";
token_url: "$CUSTOM-DOMAIN/oauth/v2/token";
scopes: {
scope: {
key: "openid";
value: "openid";
}
scope: {
key: "urn:zitadel:iam:org:project:id:zitadel:aud";
value: "urn:zitadel:iam:org:project:id:zitadel:aud";
}
}
}
}
}
security: {
security_requirement: {
key: "OAuth2";
value: {
scope: "openid";
scope: "urn:zitadel:iam:org:project:id:zitadel:aud";
}
}
}
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 has no feature flag settings and inheritance from the parent is disabled.";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
}
}
}
}
};
// FeatureService is intended to manage features for ZITADEL.
//
// Feature settings that are available on multiple "levels", such as instance and organization.
// The higher level (instance) acts as a default for the lower level (organization).
// When a feature is set on multiple levels, the lower level takes precedence.
//
// Features can be experimental where ZITADEL will assume a sane default, such as disabled.
// When over time confidence in such a feature grows, ZITADEL can default to enabling the feature.
// As a final step we might choose to always enable a feature and remove the setting from this API,
// reserving the proto field number. Such removal is not considered a breaking change.
// Setting a removed field will effectively result in a no-op.
service FeatureService {
rpc SetSystemFeatures (SetSystemFeaturesRequest) returns (SetSystemFeaturesResponse) {
option (google.api.http) = {
put: "/v2beta/features/system"
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "system.feature.write"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Set system level features";
description: "Configure and set features that apply to the complete system. Only fields present in the request are set or unset."
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
rpc ResetSystemFeatures (ResetSystemFeaturesRequest) returns (ResetSystemFeaturesResponse) {
option (google.api.http) = {
delete: "/v2beta/features/system"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "system.feature.delete"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Reset system level features";
description: "Deletes ALL configured features for the system, reverting the behaviors to system defaults."
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
rpc GetSystemFeatures (GetSystemFeaturesRequest) returns (GetSystemFeaturesResponse) {
option (google.api.http) = {
get: "/v2beta/features/system"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "system.feature.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Get system level features";
description: "Returns all configured features for the system. Unset fields mean the feature is the current system default."
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
rpc SetInstanceFeatures (SetInstanceFeaturesRequest) returns (SetInstanceFeaturesResponse) {
option (google.api.http) = {
put: "/v2beta/features/instance"
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "iam.feature.write"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Set instance level features";
description: "Configure and set features that apply to a complete instance. Only fields present in the request are set or unset."
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
rpc ResetInstanceFeatures (ResetInstanceFeaturesRequest) returns (ResetInstanceFeaturesResponse) {
option (google.api.http) = {
delete: "/v2beta/features/instance"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "iam.feature.delete"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Reset instance level features";
description: "Deletes ALL configured features for an instance, reverting the behaviors to system defaults."
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
rpc GetInstanceFeatures (GetInstanceFeaturesRequest) returns (GetInstanceFeaturesResponse) {
option (google.api.http) = {
get: "/v2beta/features/instance"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "iam.feature.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Get instance level features";
description: "Returns all configured features for an instance. Unset fields mean the feature is the current system default."
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
rpc SetOrganizationFeatures (SetOrganizationFeaturesRequest) returns (SetOrganizationFeaturesResponse) {
option (google.api.http) = {
put: "/v2beta/features/organization/{organization_id}"
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "org.feature.write"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Set organization level features";
description: "Configure and set features that apply to a complete instance. Only fields present in the request are set or unset."
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
rpc ResetOrganizationFeatures (ResetOrganizationFeaturesRequest) returns (ResetOrganizationFeaturesResponse) {
option (google.api.http) = {
delete: "/v2beta/features/organization/{organization_id}"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "org.feature.write"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Reset organization level features";
description: "Deletes ALL configured features for an organization, reverting the behaviors to instance defaults."
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
rpc GetOrganizationFeatures(GetOrganizationFeaturesRequest) returns (GetOrganizationFeaturesResponse) {
option (google.api.http) = {
get: "/v2beta/features/organization/{organization_id}"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "org.feature.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Get organization level features";
description: "Returns all configured features for an organization. Unset fields mean the feature is the current instance default."
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
rpc SetUserFeatures(SetUserFeatureRequest) returns (SetUserFeaturesResponse) {
option (google.api.http) = {
put: "/v2beta/features/user/{user_id}"
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "user.feature.write"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Set user level features";
description: "Configure and set features that apply to an user. Only fields present in the request are set or unset."
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
rpc ResetUserFeatures(ResetUserFeaturesRequest) returns (ResetUserFeaturesResponse) {
option (google.api.http) = {
delete: "/v2beta/features/user/{user_id}"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "user.feature.write"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Reset user level features";
description: "Deletes ALL configured features for a user, reverting the behaviors to organization defaults."
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
rpc GetUserFeatures(GetUserFeaturesRequest) returns (GetUserFeaturesResponse) {
option (google.api.http) = {
get: "/v2beta/features/user/{user_id}"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "user.feature.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Get organization level features";
description: "Returns all configured features for an organization. Unset fields mean the feature is the current instance default."
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
}

View File

@@ -0,0 +1,76 @@
syntax = "proto3";
package zitadel.feature.v2beta;
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/object/v2beta/object.proto";
import "zitadel/feature/v2beta/feature.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature";
message SetInstanceFeaturesRequest{
optional bool login_default_org = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "The login UI will use the settings of the default org (and not from the instance) if no organization context is set";
}
];
optional bool oidc_trigger_introspection_projections = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "Enable projection triggers during an introspection request. This can act as workaround if there are noticeable consistency issues in the introspection response but can have an impact on performance. We are planning to remove triggers for introspection requests in the future. Please raise an issue if you needed to enable this feature.";
}
];
optional bool oidc_legacy_introspection = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature.";
}
];
}
message SetInstanceFeaturesResponse {
zitadel.object.v2beta.Details details = 1;
}
message ResetInstanceFeaturesRequest {}
message ResetInstanceFeaturesResponse {
zitadel.object.v2beta.Details details = 1;
}
message GetInstanceFeaturesRequest {
bool inheritance = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "Inherit unset features from the resource owners. This option is recursive: if the flag is set, the resource's ancestors are consulted up to system defaults. If this option is disabled and the feature is not set on the instance, it will be omitted from the response or Not Found is returned when the instance has no features flags at all.";
}
];
}
message GetInstanceFeaturesResponse {
zitadel.object.v2beta.Details details = 1;
FeatureFlag login_default_org = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "The login UI will use the settings of the default org (and not from the instance) if no organization context is set";
}
];
FeatureFlag oidc_trigger_introspection_projections = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "Enable projection triggers during an introspection request. This can act as workaround if there are noticeable consistency issues in the introspection response but can have an impact on performance. We are planning to remove triggers for introspection requests in the future. Please raise an issue if you needed to enable this feature.";
}
];
FeatureFlag oidc_legacy_introspection = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature.";
}
];
}

View File

@@ -0,0 +1,62 @@
syntax = "proto3";
package zitadel.feature.v2beta;
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/object/v2beta/object.proto";
import "zitadel/feature/v2beta/feature.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature";
message SetOrganizationFeaturesRequest {
string organization_id = 1[
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
example: "\"69629023906488334\"";
}
];
}
message SetOrganizationFeaturesResponse {
zitadel.object.v2beta.Details details = 1;
}
message ResetOrganizationFeaturesRequest {
string organization_id = 1[
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
example: "\"69629023906488334\"";
}
];
}
message ResetOrganizationFeaturesResponse {
zitadel.object.v2beta.Details details = 1;
}
message GetOrganizationFeaturesRequest {
string organization_id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
example: "\"69629023906488334\"";
}
];
bool inheritance = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "Inherit unset features from the resource owners. This option is recursive: if the flag is set, the resource's ancestors are consulted up to system defaults. If this option is disabled and the feature is not set on the organization, it will be omitted from the response or Not Found is returned when the organization has no features flags at all.";
}
];
}
message GetOrganizationFeaturesResponse {
zitadel.object.v2beta.Details details = 1;
}

View File

@@ -0,0 +1,70 @@
syntax = "proto3";
package zitadel.feature.v2beta;
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/object/v2beta/object.proto";
import "zitadel/feature/v2beta/feature.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature";
message SetSystemFeaturesRequest{
optional bool login_default_org = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "The login UI will use the settings of the default org (and not from the instance) if no organization context is set";
}
];
optional bool oidc_trigger_introspection_projections = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "Enable projection triggers during an introspection request. This can act as workaround if there are noticeable consistency issues in the introspection response but can have an impact on performance. We are planning to remove triggers for introspection requests in the future. Please raise an issue if you needed to enable this feature.";
}
];
optional bool oidc_legacy_introspection = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature.";
}
];
}
message SetSystemFeaturesResponse {
zitadel.object.v2beta.Details details = 1;
}
message ResetSystemFeaturesRequest {}
message ResetSystemFeaturesResponse {
zitadel.object.v2beta.Details details = 1;
}
message GetSystemFeaturesRequest {}
message GetSystemFeaturesResponse {
zitadel.object.v2beta.Details details = 1;
FeatureFlag login_default_org = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "The login UI will use the settings of the default org (and not from the instance) if no organization context is set";
}
];
FeatureFlag oidc_trigger_introspection_projections = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "Enable projection triggers during an introspection request. This can act as workaround if there are noticeable consistency issues in the introspection response but can have an impact on performance. We are planning to remove triggers for introspection requests in the future. Please raise an issue if you needed to enable this feature.";
}
];
FeatureFlag oidc_legacy_introspection = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature.";
}
];
}

View File

@@ -0,0 +1,62 @@
syntax = "proto3";
package zitadel.feature.v2beta;
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/object/v2beta/object.proto";
import "zitadel/feature/v2beta/feature.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature";
message SetUserFeatureRequest {
string user_id = 1[
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
example: "\"69629023906488334\"";
}
];
}
message SetUserFeaturesResponse {
zitadel.object.v2beta.Details details = 1;
}
message ResetUserFeaturesRequest {
string user_id = 1[
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
example: "\"69629023906488334\"";
}
];
}
message ResetUserFeaturesResponse {
zitadel.object.v2beta.Details details = 1;
}
message GetUserFeaturesRequest {
string user_id = 1[
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
example: "\"69629023906488334\"";
}
];
bool inheritance = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "Inherit unset features from the resource owners. This option is recursive: if the flag is set, the resource's ancestors are consulted up to system defaults. If this option is disabled and the feature is not set on the user, it will be ommitted from the response or Not Found is returned when the user has no features flags at all.";
}
];
}
message GetUserFeaturesResponse {
zitadel.object.v2beta.Details details = 1;
}