feat: exchange gRPC server implementation to connectRPC (#10145)

# Which Problems Are Solved

The current maintained gRPC server in combination with a REST (grpc)
gateway is getting harder and harder to maintain. Additionally, there
have been and still are issues with supporting / displaying `oneOf`s
correctly.
We therefore decided to exchange the server implementation to
connectRPC, which apart from supporting connect as protocol, also also
"standard" gRCP clients as well as HTTP/1.1 / rest like clients, e.g.
curl directly call the server without any additional gateway.

# How the Problems Are Solved

- All v2 services are moved to connectRPC implementation. (v1 services
are still served as pure grpc servers)
- All gRPC server interceptors were migrated / copied to a corresponding
connectRPC interceptor.
- API.ListGrpcServices and API. ListGrpcMethods were changed to include
the connect services and endpoints.
- gRPC server reflection was changed to a `StaticReflector` using the
`ListGrpcServices` list.
- The `grpc.Server` interfaces was split into different combinations to
be able to handle the different cases (grpc server and prefixed gateway,
connect server with grpc gateway, connect server only, ...)
- Docs of services serving connectRPC only with no additional gateway
(instance, webkey, project, app, org v2 beta) are changed to expose that
- since the plugin is not yet available on buf, we download it using
`postinstall` hook of the docs

# Additional Changes

- WebKey service is added as v2 service (in addition to the current
v2beta)

# Additional Context

closes #9483

---------

Co-authored-by: Elio Bischof <elio@zitadel.com>
This commit is contained in:
Livio Spring
2025-07-04 10:06:20 -04:00
committed by GitHub
parent 82cd1cee08
commit 9ebf2316c6
133 changed files with 5191 additions and 1187 deletions

View File

@@ -118,7 +118,7 @@ service SystemService {
// Returns a list of ZITADEL instances
//
// Deprecated: Use [ListInstances](apis/resources/instance_service_v2/instance-service-list-instances.api.mdx) instead to list instances
// Deprecated: Use [ListInstances](apis/resources/instance_service_v2/zitadel-instance-v-2-beta-instance-service-list-instances.api.mdx) instead to list instances
rpc ListInstances(ListInstancesRequest) returns (ListInstancesResponse) {
option (google.api.http) = {
post: "/instances/_search"
@@ -136,7 +136,7 @@ service SystemService {
// Returns the detail of an instance
//
// Deprecated: Use [GetInstance](apis/resources/instance_service_v2/instance-service-get-instance.api.mdx) instead to get the details of the instance in context
// Deprecated: Use [GetInstance](apis/resources/instance_service_v2/zitadel-instance-v-2-beta-instance-service-get-instance.api.mdx) instead to get the details of the instance in context
rpc GetInstance(GetInstanceRequest) returns (GetInstanceResponse) {
option (google.api.http) = {
get: "/instances/{instance_id}";
@@ -171,7 +171,7 @@ service SystemService {
// Updates name of an existing instance
//
// Deprecated: Use [UpdateInstance](apis/resources/instance_service_v2/instance-service-update-instance.api.mdx) instead to update the name of the instance in context
// Deprecated: Use [UpdateInstance](apis/resources/instance_service_v2/zitadel-instance-v-2-beta-instance-service-update-instance.api.mdx) instead to update the name of the instance in context
rpc UpdateInstance(UpdateInstanceRequest) returns (UpdateInstanceResponse) {
option (google.api.http) = {
put: "/instances/{instance_id}"
@@ -203,7 +203,7 @@ service SystemService {
// Removes an instance
// This might take some time
//
// Deprecated: Use [DeleteInstance](apis/resources/instance_service_v2/instance-service-delete-instance.api.mdx) instead to delete an instance
// Deprecated: Use [DeleteInstance](apis/resources/instance_service_v2/zitadel-instance-v-2-beta-instance-service-delete-instance.api.mdx) instead to delete an instance
rpc RemoveInstance(RemoveInstanceRequest) returns (RemoveInstanceResponse) {
option (google.api.http) = {
delete: "/instances/{instance_id}"
@@ -234,7 +234,7 @@ service SystemService {
// Checks if a domain exists
//
// Deprecated: Use [ListCustomDomains](apis/resources/instance_service_v2/instance-service-list-custom-domains.api.mdx) instead to check existence of an instance
// Deprecated: Use [ListCustomDomains](apis/resources/instance_service_v2/zitadel-instance-v-2-beta-instance-service-list-custom-domains.api.mdx) instead to check existence of an instance
rpc ExistsDomain(ExistsDomainRequest) returns (ExistsDomainResponse) {
option (google.api.http) = {
post: "/domains/{domain}/_exists";
@@ -270,7 +270,7 @@ service SystemService {
// Adds a domain to an instance
//
// Deprecated: Use [AddCustomDomain](apis/resources/instance_service_v2/instance-service-add-custom-domain.api.mdx) instead to add a custom domain to the instance in context
// Deprecated: Use [AddCustomDomain](apis/resources/instance_service_v2/zitadel-instance-v-2-beta-instance-service-add-custom-domain.api.mdx) instead to add a custom domain to the instance in context
rpc AddDomain(AddDomainRequest) returns (AddDomainResponse) {
option (google.api.http) = {
post: "/instances/{instance_id}/domains";
@@ -288,7 +288,7 @@ service SystemService {
// Removes the domain of an instance
//
// Deprecated: Use [RemoveDomain](apis/resources/instance_service_v2/instance-service-remove-custom-domain.api.mdx) instead to remove a custom domain from the instance in context
// Deprecated: Use [RemoveDomain](apis/resources/instance_service_v2/zitadel-instance-v-2-beta-instance-service-remove-custom-domain.api.mdx) instead to remove a custom domain from the instance in context
rpc RemoveDomain(RemoveDomainRequest) returns (RemoveDomainResponse) {
option (google.api.http) = {
delete: "/instances/{instance_id}/domains/{domain}";

View File

@@ -0,0 +1,109 @@
syntax = "proto3";
package zitadel.webkey.v2;
import "google/protobuf/timestamp.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/webkey/v2;webkey";
enum State {
STATE_UNSPECIFIED = 0;
// A newly created key is in the initial state and published to the public key endpoint.
STATE_INITIAL = 1;
// The active key is used to sign tokens. Only one key can be active at a time.
STATE_ACTIVE = 2;
// The inactive key is not used to sign tokens anymore, but still published to the public key endpoint.
STATE_INACTIVE = 3;
// The removed key is not used to sign tokens anymore and not published to the public key endpoint.
STATE_REMOVED = 4;
}
message WebKey {
// The unique identifier of the key.
string id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629012906488334\"";
}
];
// The timestamp of the key 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 key (e.g. creation, activation, deactivation).
google.protobuf.Timestamp change_date = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2025-01-23T10:34:18.051Z\"";
}
];
// State of the key
State state = 4;
// Configured type of the key (either RSA, ECDSA or ED25519)
oneof key {
RSA rsa = 5;
ECDSA ecdsa = 6;
ED25519 ed25519 = 7;
}
}
message RSA {
// Bit size of the RSA key. Default is 2048 bits.
RSABits bits = 1 [
(validate.rules).enum = {defined_only: true, not_in: [0]},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
default: "RSA_BITS_2048";
}
];
// Signing algrithm used. Default is SHA256.
RSAHasher hasher = 2 [
(validate.rules).enum = {defined_only: true, not_in: [0]},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
default: "RSA_HASHER_SHA256";
}
];
}
enum RSABits {
RSA_BITS_UNSPECIFIED = 0;
// 2048 bit RSA key
RSA_BITS_2048 = 1;
// 3072 bit RSA key
RSA_BITS_3072 = 2;
// 4096 bit RSA key
RSA_BITS_4096 = 3;
}
enum RSAHasher {
RSA_HASHER_UNSPECIFIED = 0;
// SHA256 hashing algorithm resulting in the RS256 algorithm header
RSA_HASHER_SHA256 = 1;
// SHA384 hashing algorithm resulting in the RS384 algorithm header
RSA_HASHER_SHA384 = 2;
// SHA512 hashing algorithm resulting in the RS512 algorithm header
RSA_HASHER_SHA512 = 3;
}
message ECDSA {
// Curve of the ECDSA key. Default is P-256.
ECDSACurve curve = 1 [
(validate.rules).enum = {defined_only: true, not_in: [0]},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
default: "ECDSA_CURVE_P256";
}
];
}
enum ECDSACurve {
ECDSA_CURVE_UNSPECIFIED = 0;
// NIST P-256 curve resulting in the ES256 algorithm header
ECDSA_CURVE_P256 = 1;
// NIST P-384 curve resulting in the ES384 algorithm header
ECDSA_CURVE_P384 = 2;
// NIST P-512 curve resulting in the ES512 algorithm header
ECDSA_CURVE_P512 = 3;
}
message ED25519 {}

View File

@@ -0,0 +1,335 @@
syntax = "proto3";
package zitadel.webkey.v2;
import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "google/protobuf/timestamp.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/protoc_gen_zitadel/v2/options.proto";
import "zitadel/webkey/v2/key.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/webkey/v2;webkey";
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
info: {
title: "Web Key Service";
version: "2.0";
description: "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens.\n\nThe public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.\n\nPlease make sure to enable the `web_key` feature flag on your instance to use this service.";
contact:{
name: "ZITADEL"
url: "https://zitadel.com"
email: "hi@zitadel.com"
}
license: {
name: "Apache 2.0",
url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md";
};
};
schemes: HTTPS;
schemes: HTTP;
consumes: "application/json";
produces: "application/json";
consumes: "application/grpc";
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 does not exist.";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
}
}
}
}
};
// Service to manage web keys for OIDC token signing and validation.
// The service provides methods to create, activate, delete and list web keys.
// The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.
//
// Please make sure to enable the `web_key` feature flag on your instance to use this service.
service WebKeyService {
// Create Web Key
//
// Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation.
// The public key can be used to validate OIDC tokens.
// The newly created key will have the state `STATE_INITIAL` and is published to the public key endpoint.
// Note that the JWKs OIDC endpoint returns a cacheable response.
//
// If no key type is provided, a RSA key pair with 2048 bits and SHA256 hashing will be created.
//
// Required permission:
// - `iam.web_key.write`
//
// Required feature flag:
// - `web_key`
rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) {
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "iam.web_key.write"
}
};
}
// Activate Web Key
//
// Switch the active signing web key. The previously active key will be deactivated.
// Note that the JWKs OIDC endpoint returns a cacheable response.
// Therefore it is not advised to activate a key that has been created within the cache duration (default is 5min),
// as the public key may not have been propagated to caches and clients yet.
//
// Required permission:
// - `iam.web_key.write`
//
// Required feature flag:
// - `web_key`
rpc ActivateWebKey(ActivateWebKeyRequest) returns (ActivateWebKeyResponse) {
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "iam.web_key.write"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200"
value: {
description: "Web key activated successfully.";
}
};
responses: {
key: "400"
value: {
description: "The feature flag `web_key` is not enabled.";
}
};
responses: {
key: "404"
value: {
description: "The web key to active does not exist.";
}
};
};
}
// Delete Web Key
//
// Delete a web key pair. Only inactive keys can be deleted. Once a key is deleted,
// any tokens signed by this key will be invalid.
// Note that the JWKs OIDC endpoint returns a cacheable response.
// In case the web key is not found, the request will return a successful response as
// the desired state is already achieved.
// You can check the change date in the response to verify if the web key was deleted during the request.
//
// Required permission:
// - `iam.web_key.delete`
//
// Required feature flag:
// - `web_key`
rpc DeleteWebKey(DeleteWebKeyRequest) returns (DeleteWebKeyResponse) {
option (google.api.http) = {
delete: "/v2/web_keys/{id}"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "iam.web_key.delete"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200"
value: {
description: "Web key deleted successfully.";
}
};
responses: {
key: "400"
value: {
description: "The feature flag `web_key` is not enabled or the web key is currently active.";
}
};
};
}
// List Web Keys
//
// List all web keys and their states.
//
// Required permission:
// - `iam.web_key.read`
//
// Required feature flag:
// - `web_key`
rpc ListWebKeys(ListWebKeysRequest) returns (ListWebKeysResponse) {
option (google.api.http) = {
get: "/v2/web_keys"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "iam.web_key.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200"
value: {
description: "List of all web keys.";
}
};
responses: {
key: "400"
value: {
description: "The feature flag `web_key` is not enabled.";
}
};
};
}
}
message CreateWebKeyRequest {
// The key type to create (RSA, ECDSA, ED25519).
// If no key type is provided, a RSA key pair with 2048 bits and SHA256 hashing will be created.
oneof key {
// Create a RSA key pair and specify the bit size and hashing algorithm.
// If no bits and hasher are provided, a RSA key pair with 2048 bits and SHA256 hashing will be created.
RSA rsa = 1;
// Create a ECDSA key pair and specify the curve.
// If no curve is provided, a ECDSA key pair with P-256 curve will be created.
ECDSA ecdsa = 2;
// Create a ED25519 key pair.
ED25519 ed25519 = 3;
}
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
example: "{\"rsa\":{\"bits\":\"RSA_BITS_2048\",\"hasher\":\"RSA_HASHER_SHA256\"}}";
};
}
message CreateWebKeyResponse {
// The unique identifier of the newly created key.
string id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629012906488334\"";
}
];
// The timestamp of the key creation.
google.protobuf.Timestamp creation_date = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2024-12-18T07:50:47.492Z\"";
}
];
}
message ActivateWebKeyRequest {
string id = 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: "\"69629026806489455\"";
}
];
}
message ActivateWebKeyResponse {
// The timestamp of the activation of the key.
google.protobuf.Timestamp change_date = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2025-01-23T10:34:18.051Z\"";
}
];
}
message DeleteWebKeyRequest {
string id = 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: "\"69629026806489455\"";
}
];
}
message DeleteWebKeyResponse {
// The timestamp of the deletion of the key.
// Note that the deletion date is only guaranteed to be set if the deletion was successful during the request.
// In case the deletion occurred in a previous request, the deletion date might be empty.
google.protobuf.Timestamp deletion_date = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2025-01-23T10:34:18.051Z\"";
}
];
}
message ListWebKeysRequest {}
message ListWebKeysResponse {
repeated WebKey web_keys = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[{\"id\":\"69629012906488334\",\"creationDate\":\"2024-12-18T07:50:47.492Z\",\"changeDate\":\"2024-12-18T08:04:47.492Z\",\"state\":\"STATE_ACTIVE\",\"rsa\":{\"bits\":\"RSA_BITS_2048\",\"hasher\":\"RSA_HASHER_SHA256\"}},{\"id\":\"69629012909346200\",\"creationDate\":\"2025-01-18T12:05:47.492Z\",\"state\":\"STATE_INITIAL\",\"ecdsa\":{\"curve\":\"ECDSA_CURVE_P256\"}}]";
}
];
}