mirror of
https://github.com/zitadel/zitadel.git
synced 2025-07-12 22:38:34 +00:00

# 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>
135 lines
5.2 KiB
Go
135 lines
5.2 KiB
Go
package saml
|
|
|
|
import (
|
|
"context"
|
|
|
|
"connectrpc.com/connect"
|
|
"github.com/zitadel/logging"
|
|
"github.com/zitadel/saml/pkg/provider"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
|
|
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
|
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
|
"github.com/zitadel/zitadel/internal/api/saml"
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
"github.com/zitadel/zitadel/internal/query"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2"
|
|
)
|
|
|
|
func (s *Server) GetSAMLRequest(ctx context.Context, req *connect.Request[saml_pb.GetSAMLRequestRequest]) (*connect.Response[saml_pb.GetSAMLRequestResponse], error) {
|
|
authRequest, err := s.query.SamlRequestByID(ctx, true, req.Msg.GetSamlRequestId(), true)
|
|
if err != nil {
|
|
logging.WithError(err).Error("query samlRequest by ID")
|
|
return nil, err
|
|
}
|
|
return connect.NewResponse(&saml_pb.GetSAMLRequestResponse{
|
|
SamlRequest: samlRequestToPb(authRequest),
|
|
}), nil
|
|
}
|
|
|
|
func samlRequestToPb(a *query.SamlRequest) *saml_pb.SAMLRequest {
|
|
return &saml_pb.SAMLRequest{
|
|
Id: a.ID,
|
|
CreationDate: timestamppb.New(a.CreationDate),
|
|
}
|
|
}
|
|
|
|
func (s *Server) CreateResponse(ctx context.Context, req *connect.Request[saml_pb.CreateResponseRequest]) (*connect.Response[saml_pb.CreateResponseResponse], error) {
|
|
switch v := req.Msg.GetResponseKind().(type) {
|
|
case *saml_pb.CreateResponseRequest_Error:
|
|
return s.failSAMLRequest(ctx, req.Msg.GetSamlRequestId(), v.Error)
|
|
case *saml_pb.CreateResponseRequest_Session:
|
|
return s.linkSessionToSAMLRequest(ctx, req.Msg.GetSamlRequestId(), v.Session)
|
|
default:
|
|
return nil, zerrors.ThrowUnimplementedf(nil, "SAMLv2-0Tfak3fBS0", "verification oneOf %T in method CreateResponse not implemented", v)
|
|
}
|
|
}
|
|
|
|
func (s *Server) failSAMLRequest(ctx context.Context, samlRequestID string, ae *saml_pb.AuthorizationError) (*connect.Response[saml_pb.CreateResponseResponse], error) {
|
|
details, aar, err := s.command.FailSAMLRequest(ctx, samlRequestID, errorReasonToDomain(ae.GetError()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
authReq := &saml.AuthRequestV2{CurrentSAMLRequest: aar}
|
|
url, body, err := s.idp.CreateErrorResponse(authReq, errorReasonToDomain(ae.GetError()), ae.GetErrorDescription())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return connect.NewResponse(createCallbackResponseFromBinding(details, url, body, authReq.RelayState)), nil
|
|
}
|
|
|
|
func (s *Server) checkPermission(ctx context.Context, issuer string, userID string) error {
|
|
permission, err := s.query.CheckProjectPermissionByEntityID(ctx, issuer, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !permission.HasProjectChecked {
|
|
return zerrors.ThrowPermissionDenied(nil, "SAML-foSyH49RvL", "Errors.User.ProjectRequired")
|
|
}
|
|
if !permission.ProjectRoleChecked {
|
|
return zerrors.ThrowPermissionDenied(nil, "SAML-foSyH49RvL", "Errors.User.GrantRequired")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) linkSessionToSAMLRequest(ctx context.Context, samlRequestID string, session *saml_pb.Session) (*connect.Response[saml_pb.CreateResponseResponse], error) {
|
|
details, aar, err := s.command.LinkSessionToSAMLRequest(ctx, samlRequestID, session.GetSessionId(), session.GetSessionToken(), true, s.checkPermission)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
authReq := &saml.AuthRequestV2{CurrentSAMLRequest: aar}
|
|
responseIssuer := authReq.ResponseIssuer
|
|
if responseIssuer == "" {
|
|
responseIssuer = http_utils.DomainContext(ctx).Origin()
|
|
}
|
|
ctx = provider.ContextWithIssuer(ctx, responseIssuer)
|
|
url, body, err := s.idp.CreateResponse(ctx, authReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return connect.NewResponse(createCallbackResponseFromBinding(details, url, body, authReq.RelayState)), nil
|
|
}
|
|
|
|
func createCallbackResponseFromBinding(details *domain.ObjectDetails, url string, body string, relayState string) *saml_pb.CreateResponseResponse {
|
|
resp := &saml_pb.CreateResponseResponse{
|
|
Details: object.DomainToDetailsPb(details),
|
|
Url: url,
|
|
}
|
|
|
|
if body != "" {
|
|
resp.Binding = &saml_pb.CreateResponseResponse_Post{
|
|
Post: &saml_pb.PostResponse{
|
|
RelayState: relayState,
|
|
SamlResponse: body,
|
|
},
|
|
}
|
|
} else {
|
|
resp.Binding = &saml_pb.CreateResponseResponse_Redirect{Redirect: &saml_pb.RedirectResponse{}}
|
|
}
|
|
return resp
|
|
}
|
|
|
|
func errorReasonToDomain(errorReason saml_pb.ErrorReason) domain.SAMLErrorReason {
|
|
switch errorReason {
|
|
case saml_pb.ErrorReason_ERROR_REASON_UNSPECIFIED:
|
|
return domain.SAMLErrorReasonUnspecified
|
|
case saml_pb.ErrorReason_ERROR_REASON_VERSION_MISSMATCH:
|
|
return domain.SAMLErrorReasonVersionMissmatch
|
|
case saml_pb.ErrorReason_ERROR_REASON_AUTH_N_FAILED:
|
|
return domain.SAMLErrorReasonAuthNFailed
|
|
case saml_pb.ErrorReason_ERROR_REASON_INVALID_ATTR_NAME_OR_VALUE:
|
|
return domain.SAMLErrorReasonInvalidAttrNameOrValue
|
|
case saml_pb.ErrorReason_ERROR_REASON_INVALID_NAMEID_POLICY:
|
|
return domain.SAMLErrorReasonInvalidNameIDPolicy
|
|
case saml_pb.ErrorReason_ERROR_REASON_REQUEST_DENIED:
|
|
return domain.SAMLErrorReasonRequestDenied
|
|
case saml_pb.ErrorReason_ERROR_REASON_REQUEST_UNSUPPORTED:
|
|
return domain.SAMLErrorReasonRequestUnsupported
|
|
case saml_pb.ErrorReason_ERROR_REASON_UNSUPPORTED_BINDING:
|
|
return domain.SAMLErrorReasonUnsupportedBinding
|
|
default:
|
|
return domain.SAMLErrorReasonUnspecified
|
|
}
|
|
}
|