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>
2025-07-04 10:06:20 -04:00
package connect_middleware
import (
"context"
"errors"
"fmt"
"strings"
"connectrpc.com/connect"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
zitadel_http "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
object_v3 "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
)
2025-09-09 08:34:59 +02:00
func InstanceInterceptor ( verifier authz . InstanceVerifier , externalDomain string , translator * i18n . Translator , explicitInstanceIdServices ... string ) connect . UnaryInterceptorFunc {
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>
2025-07-04 10:06:20 -04:00
return func ( handler connect . UnaryFunc ) connect . UnaryFunc {
return func ( ctx context . Context , req connect . AnyRequest ) ( connect . AnyResponse , error ) {
return setInstance ( ctx , req , handler , verifier , externalDomain , translator , explicitInstanceIdServices ... )
}
}
}
func setInstance ( ctx context . Context , req connect . AnyRequest , handler connect . UnaryFunc , verifier authz . InstanceVerifier , externalDomain string , translator * i18n . Translator , idFromRequestsServices ... string ) ( _ connect . AnyResponse , err error ) {
interceptorCtx , span := tracing . NewServerInterceptorSpan ( ctx )
defer func ( ) { span . EndWithError ( err ) } ( )
for _ , service := range idFromRequestsServices {
if ! strings . HasPrefix ( service , "/" ) {
service = "/" + service
}
if strings . HasPrefix ( req . Spec ( ) . Procedure , service ) {
withInstanceIDProperty , ok := req . Any ( ) . ( interface {
GetInstanceId ( ) string
} )
if ! ok {
return handler ( ctx , req )
}
2025-10-28 14:01:14 +01:00
instanceID := withInstanceIDProperty . GetInstanceId ( )
if instanceID != "" {
return addInstanceByID ( interceptorCtx , req , handler , verifier , translator , instanceID )
}
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>
2025-07-04 10:06:20 -04:00
}
}
explicitInstanceRequest , ok := req . Any ( ) . ( interface {
GetInstance ( ) * object_v3 . Instance
} )
if ok {
instance := explicitInstanceRequest . GetInstance ( )
if id := instance . GetId ( ) ; id != "" {
return addInstanceByID ( interceptorCtx , req , handler , verifier , translator , id )
}
if domain := instance . GetDomain ( ) ; domain != "" {
return addInstanceByDomain ( interceptorCtx , req , handler , verifier , translator , domain )
}
}
return addInstanceByRequestedHost ( interceptorCtx , req , handler , verifier , translator , externalDomain )
}
func addInstanceByID ( ctx context . Context , req connect . AnyRequest , handler connect . UnaryFunc , verifier authz . InstanceVerifier , translator * i18n . Translator , id string ) ( connect . AnyResponse , error ) {
instance , err := verifier . InstanceByID ( ctx , id )
if err != nil {
2025-10-28 14:01:14 +01:00
// We do not want to expose whether the instance id was invalid or not
// to prevent leaking information about existing instances.
// In case the user has permission to access the instance, but the instance does not exist,
// the error will be returned by the business logic later on.
logging . WithFields ( "instanceID" , id ) . WithError ( err ) . Error ( "unable to set instance by id" )
return handler ( ctx , req )
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>
2025-07-04 10:06:20 -04:00
}
return handler ( authz . WithInstance ( ctx , instance ) , req )
}
func addInstanceByDomain ( ctx context . Context , req connect . AnyRequest , handler connect . UnaryFunc , verifier authz . InstanceVerifier , translator * i18n . Translator , domain string ) ( connect . AnyResponse , error ) {
instance , err := verifier . InstanceByHost ( ctx , domain , "" )
if err != nil {
notFoundErr := new ( zerrors . NotFoundError )
if errors . As ( err , & notFoundErr ) {
notFoundErr . Message = translator . LocalizeFromCtx ( ctx , notFoundErr . GetMessage ( ) , nil )
}
return nil , connect . NewError ( connect . CodeNotFound , fmt . Errorf ( "unable to set instance using domain %s: %w" , domain , notFoundErr ) )
}
return handler ( authz . WithInstance ( ctx , instance ) , req )
}
func addInstanceByRequestedHost ( ctx context . Context , req connect . AnyRequest , handler connect . UnaryFunc , verifier authz . InstanceVerifier , translator * i18n . Translator , externalDomain string ) ( connect . AnyResponse , error ) {
requestContext := zitadel_http . DomainContext ( ctx )
if requestContext . InstanceHost == "" {
logging . WithFields ( "origin" , requestContext . Origin ( ) , "externalDomain" , externalDomain ) . Error ( "unable to set instance" )
return nil , connect . NewError ( connect . CodeNotFound , errors . New ( "no instanceHost specified" ) )
}
instance , err := verifier . InstanceByHost ( ctx , requestContext . InstanceHost , requestContext . PublicHost )
if err != nil {
origin := zitadel_http . DomainContext ( ctx )
logging . WithFields ( "origin" , requestContext . Origin ( ) , "externalDomain" , externalDomain ) . WithError ( err ) . Error ( "unable to set instance" )
zErr := new ( zerrors . ZitadelError )
if errors . As ( err , & zErr ) {
zErr . SetMessage ( translator . LocalizeFromCtx ( ctx , zErr . GetMessage ( ) , nil ) )
zErr . Parent = err
return nil , connect . NewError ( connect . CodeNotFound , fmt . Errorf ( "unable to set instance using origin %s (ExternalDomain is %s): %s" , origin , externalDomain , zErr . Error ( ) ) )
}
return nil , connect . NewError ( connect . CodeNotFound , fmt . Errorf ( "unable to set instance using origin %s (ExternalDomain is %s)" , origin , externalDomain ) )
}
return handler ( authz . WithInstance ( ctx , instance ) , req )
}