diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 58847e233..7ee106840 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -83,9 +83,17 @@ TLS: Cert: # ZITADEL_TLS_CERT # Header name of HTTP2 (incl. gRPC) calls from which the instance will be matched +# Deprecated: Use the InstanceHostHeaders instead HTTP2HostHeader: ":authority" # ZITADEL_HTTP2HOSTHEADER # Header name of HTTP1 calls from which the instance will be matched +# Deprecated: Use the InstanceHostHeaders instead HTTP1HostHeader: "host" # ZITADEL_HTTP1HOSTHEADER +# Ordered header name list, which will be used to match the instance +InstanceHostHeaders: # ZITADEL_INSTANCEHOSTHEADERS + - "x-zitadel-instance-host" +# Ordered header name list, which will be used as the public host +PublicHostHeaders: # ZITADEL_PUBLICHOSTHEADERS + - "x-zitadel-public-host" WebAuthNName: ZITADEL # ZITADEL_WEBAUTHNNAME diff --git a/cmd/start/config.go b/cmd/start/config.go index c96386521..4ac5da13a 100644 --- a/cmd/start/config.go +++ b/cmd/start/config.go @@ -35,40 +35,42 @@ import ( ) type Config struct { - Log *logging.Config - Port uint16 - ExternalPort uint16 - ExternalDomain string - ExternalSecure bool - TLS network.TLS - HTTP2HostHeader string - HTTP1HostHeader string - WebAuthNName string - Database database.Config - Tracing tracing.Config - Metrics metrics.Config - Projections projection.Config - Auth auth_es.Config - Admin admin_es.Config - UserAgentCookie *middleware.UserAgentCookieConfig - OIDC oidc.Config - SAML saml.Config - Login login.Config - Console console.Config - AssetStorage static_config.AssetStorageConfig - InternalAuthZ internal_authz.Config - SystemDefaults systemdefaults.SystemDefaults - EncryptionKeys *encryption.EncryptionKeyConfig - DefaultInstance command.InstanceSetup - AuditLogRetention time.Duration - SystemAPIUsers map[string]*internal_authz.SystemAPIUser - CustomerPortal string - Machine *id.Config - Actions *actions.Config - Eventstore *eventstore.Config - LogStore *logstore.Configs - Quotas *QuotasConfig - Telemetry *handlers.TelemetryPusherConfig + Log *logging.Config + Port uint16 + ExternalPort uint16 + ExternalDomain string + ExternalSecure bool + TLS network.TLS + InstanceHostHeaders []string + PublicHostHeaders []string + HTTP2HostHeader string + HTTP1HostHeader string + WebAuthNName string + Database database.Config + Tracing tracing.Config + Metrics metrics.Config + Projections projection.Config + Auth auth_es.Config + Admin admin_es.Config + UserAgentCookie *middleware.UserAgentCookieConfig + OIDC oidc.Config + SAML saml.Config + Login login.Config + Console console.Config + AssetStorage static_config.AssetStorageConfig + InternalAuthZ internal_authz.Config + SystemDefaults systemdefaults.SystemDefaults + EncryptionKeys *encryption.EncryptionKeyConfig + DefaultInstance command.InstanceSetup + AuditLogRetention time.Duration + SystemAPIUsers map[string]*internal_authz.SystemAPIUser + CustomerPortal string + Machine *id.Config + Actions *actions.Config + Eventstore *eventstore.Config + LogStore *logstore.Configs + Quotas *QuotasConfig + Telemetry *handlers.TelemetryPusherConfig } type QuotasConfig struct { diff --git a/cmd/start/start.go b/cmd/start/start.go index 6bed1168d..1a65ea3f2 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -59,6 +59,7 @@ import ( "github.com/zitadel/zitadel/internal/api/robots_txt" "github.com/zitadel/zitadel/internal/api/saml" "github.com/zitadel/zitadel/internal/api/ui/console" + "github.com/zitadel/zitadel/internal/api/ui/console/path" "github.com/zitadel/zitadel/internal/api/ui/login" auth_es "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing" "github.com/zitadel/zitadel/internal/authz" @@ -346,7 +347,7 @@ func startAPIs( } oidcPrefixes := []string{"/.well-known/openid-configuration", "/oidc/v1", "/oauth/v2"} // always set the origin in the context if available in the http headers, no matter for what protocol - router.Use(middleware.WithOrigin(config.ExternalSecure)) + router.Use(middleware.WithOrigin(config.ExternalSecure, config.HTTP1HostHeader, config.HTTP2HostHeader, config.InstanceHostHeaders, config.PublicHostHeaders)) systemTokenVerifier, err := internal_authz.StartSystemTokenVerifierFromConfig(http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), config.SystemAPIUsers) if err != nil { return nil, err @@ -374,7 +375,7 @@ func startAPIs( http_util.WithMaxAge(int(math.Floor(config.Quotas.Access.ExhaustedCookieMaxAge.Seconds()))), ) limitingAccessInterceptor := middleware.NewAccessInterceptor(accessSvc, exhaustedCookieHandler, &config.Quotas.Access.AccessConfig) - apis, err := api.New(ctx, config.Port, router, queries, verifier, config.InternalAuthZ, tlsConfig, config.HTTP2HostHeader, config.HTTP1HostHeader, config.ExternalDomain, limitingAccessInterceptor) + apis, err := api.New(ctx, config.Port, router, queries, verifier, config.InternalAuthZ, tlsConfig, config.ExternalDomain, append(config.InstanceHostHeaders, config.PublicHostHeaders...), limitingAccessInterceptor) if err != nil { return nil, fmt.Errorf("error creating api %w", err) } @@ -396,25 +397,25 @@ func startAPIs( if err := apis.RegisterServer(ctx, system.CreateServer(commands, queries, config.Database.DatabaseName(), config.DefaultInstance, config.ExternalDomain), tlsConfig); err != nil { return nil, err } - if err := apis.RegisterServer(ctx, admin.CreateServer(config.Database.DatabaseName(), commands, queries, config.SystemDefaults, config.ExternalSecure, keys.User, config.AuditLogRetention), tlsConfig); err != nil { + if err := apis.RegisterServer(ctx, admin.CreateServer(config.Database.DatabaseName(), commands, queries, keys.User, config.AuditLogRetention), tlsConfig); err != nil { return nil, err } - if err := apis.RegisterServer(ctx, management.CreateServer(commands, queries, config.SystemDefaults, keys.User, config.ExternalSecure), tlsConfig); err != nil { + if err := apis.RegisterServer(ctx, management.CreateServer(commands, queries, config.SystemDefaults, keys.User), tlsConfig); err != nil { return nil, err } - if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, keys.User, config.ExternalSecure), tlsConfig); err != nil { + if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, keys.User), tlsConfig); err != nil { return nil, err } - if err := apis.RegisterService(ctx, user_v2beta.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(config.ExternalSecure), idp.SAMLRootURL(config.ExternalSecure), assets.AssetAPI(config.ExternalSecure), permissionCheck)); err != nil { + if err := apis.RegisterService(ctx, user_v2beta.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, user_v2.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(config.ExternalSecure), idp.SAMLRootURL(config.ExternalSecure), assets.AssetAPI(config.ExternalSecure), permissionCheck)); err != nil { + if err := apis.RegisterService(ctx, user_v2.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil { return nil, err } if err := apis.RegisterService(ctx, session_v2beta.CreateServer(commands, queries)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, settings_v2beta.CreateServer(commands, queries, config.ExternalSecure)); err != nil { + if err := apis.RegisterService(ctx, settings_v2beta.CreateServer(commands, queries)); err != nil { return nil, err } if err := apis.RegisterService(ctx, org_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil { @@ -426,7 +427,7 @@ func startAPIs( if err := apis.RegisterService(ctx, session_v2.CreateServer(commands, queries)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, settings_v2.CreateServer(commands, queries, config.ExternalSecure)); err != nil { + if err := apis.RegisterService(ctx, settings_v2.CreateServer(commands, queries)); err != nil { return nil, err } if err := apis.RegisterService(ctx, org_v2.CreateServer(commands, queries, permissionCheck)); err != nil { @@ -441,11 +442,11 @@ func startAPIs( if err := apis.RegisterService(ctx, user_schema_v3_alpha.CreateServer(commands, queries)); err != nil { return nil, err } - instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader, config.ExternalDomain, login.IgnoreInstanceEndpoints...) + instanceInterceptor := middleware.InstanceInterceptor(queries, config.ExternalDomain, login.IgnoreInstanceEndpoints...) assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge) apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle)) - apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, config.ExternalSecure, instanceInterceptor.Handler)) + apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, instanceInterceptor.Handler)) userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources, login.EndpointExternalLoginCallbackFormPost, login.EndpointSAMLACS) if err != nil { @@ -482,8 +483,8 @@ func startAPIs( if err != nil { return nil, fmt.Errorf("unable to start console: %w", err) } - apis.RegisterHandlerOnPrefix(console.HandlerPrefix, c) - consolePath := console.HandlerPrefix + "/" + apis.RegisterHandlerOnPrefix(path.HandlerPrefix, c) + consolePath := path.HandlerPrefix + "/" l, err := login.CreateLogin( config.Login, commands, diff --git a/internal/activity/activity.go b/internal/activity/activity.go index 85095c759..34fc411d6 100644 --- a/internal/activity/activity.go +++ b/internal/activity/activity.go @@ -61,7 +61,7 @@ func Trigger(ctx context.Context, orgID, userID string, trigger TriggerMethod, r authz.GetInstance(ctx).InstanceID(), orgID, userID, - http_utils.ComposedOrigin(ctx), + http_utils.DomainContext(ctx).Origin(), // TODO: origin? trigger, ai.Method, ai.Path, @@ -78,7 +78,7 @@ func TriggerGRPCWithContext(ctx context.Context, trigger TriggerMethod) { authz.GetInstance(ctx).InstanceID(), authz.GetCtxData(ctx).OrgID, authz.GetCtxData(ctx).UserID, - http_utils.ComposedOrigin(ctx), + http_utils.DomainContext(ctx).Origin(), // TODO: origin? trigger, ai.Method, ai.Path, diff --git a/internal/api/api.go b/internal/api/api.go index f3ed27c2b..15d6c5b99 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -32,7 +32,7 @@ type API struct { verifier internal_authz.APITokenVerifier health healthCheck router *mux.Router - http1HostName string + hostHeaders []string grpcGateway *server.Gateway healthServer *health.Server accessInterceptor *http_mw.AccessInterceptor @@ -75,7 +75,8 @@ func New( verifier internal_authz.APITokenVerifier, authZ internal_authz.Config, tlsConfig *tls.Config, - http2HostName, http1HostName, externalDomain string, + externalDomain string, + hostHeaders []string, accessInterceptor *http_mw.AccessInterceptor, ) (_ *API, err error) { api := &API{ @@ -83,13 +84,13 @@ func New( verifier: verifier, health: queries, router: router, - http1HostName: http1HostName, queries: queries, accessInterceptor: accessInterceptor, + hostHeaders: hostHeaders, } - api.grpcServer = server.CreateServer(api.verifier, authZ, queries, http2HostName, externalDomain, tlsConfig, accessInterceptor.AccessService()) - api.grpcGateway, err = server.CreateGateway(ctx, port, http1HostName, accessInterceptor, tlsConfig) + api.grpcServer = server.CreateServer(api.verifier, authZ, queries, externalDomain, tlsConfig, accessInterceptor.AccessService()) + api.grpcGateway, err = server.CreateGateway(ctx, port, hostHeaders, accessInterceptor, tlsConfig) if err != nil { return nil, err } @@ -112,9 +113,8 @@ func (a *API) RegisterServer(ctx context.Context, grpcServer server.WithGatewayP ctx, grpcServer, a.port, - a.http1HostName, + a.hostHeaders, a.accessInterceptor, - a.queries, tlsConfig, ) if err != nil { diff --git a/internal/api/assets/asset.go b/internal/api/assets/asset.go index 5f30c94a2..57ad3710b 100644 --- a/internal/api/assets/asset.go +++ b/internal/api/assets/asset.go @@ -55,9 +55,9 @@ func (h *Handler) Storage() static.Storage { return h.storage } -func AssetAPI(externalSecure bool) func(context.Context) string { +func AssetAPI() func(context.Context) string { return func(ctx context.Context) string { - return http_util.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), externalSecure) + HandlerPrefix + return http_util.DomainContext(ctx).Origin() + HandlerPrefix } } diff --git a/internal/api/authz/instance.go b/internal/api/authz/instance.go index 18767a315..9610283a5 100644 --- a/internal/api/authz/instance.go +++ b/internal/api/authz/instance.go @@ -18,8 +18,6 @@ type Instance interface { ProjectID() string ConsoleClientID() string ConsoleApplicationID() string - RequestedDomain() string - RequestedHost() string DefaultLanguage() language.Tag DefaultOrganisationID() string SecurityPolicyAllowedOrigins() []string @@ -30,7 +28,7 @@ type Instance interface { } type InstanceVerifier interface { - InstanceByHost(ctx context.Context, host string) (Instance, error) + InstanceByHost(ctx context.Context, host, publicDomain string) (Instance, error) InstanceByID(ctx context.Context) (Instance, error) } @@ -68,14 +66,6 @@ func (i *instance) ConsoleApplicationID() string { return i.appID } -func (i *instance) RequestedDomain() string { - return i.domain -} - -func (i *instance) RequestedHost() string { - return i.domain -} - func (i *instance) DefaultLanguage() language.Tag { return language.Und } @@ -116,16 +106,6 @@ func WithInstanceID(ctx context.Context, id string) context.Context { return context.WithValue(ctx, instanceKey, &instance{id: id}) } -func WithRequestedDomain(ctx context.Context, domain string) context.Context { - i, ok := ctx.Value(instanceKey).(*instance) - if !ok { - i = new(instance) - } - - i.domain = domain - return context.WithValue(ctx, instanceKey, i) -} - func WithConsole(ctx context.Context, projectID, appID string) context.Context { i, ok := ctx.Value(instanceKey).(*instance) if !ok { diff --git a/internal/api/authz/instance_test.go b/internal/api/authz/instance_test.go index c6cac09d4..f71cbac5d 100644 --- a/internal/api/authz/instance_test.go +++ b/internal/api/authz/instance_test.go @@ -118,14 +118,6 @@ func (m *mockInstance) DefaultOrganisationID() string { return "orgID" } -func (m *mockInstance) RequestedDomain() string { - return "zitadel.cloud" -} - -func (m *mockInstance) RequestedHost() string { - return "zitadel.cloud:443" -} - func (m *mockInstance) SecurityPolicyAllowedOrigins() []string { return nil } diff --git a/internal/api/grpc/admin/instance.go b/internal/api/grpc/admin/instance.go index f973c8a32..74f1576ae 100644 --- a/internal/api/grpc/admin/instance.go +++ b/internal/api/grpc/admin/instance.go @@ -37,3 +37,42 @@ func (s *Server) ListInstanceDomains(ctx context.Context, req *admin_pb.ListInst ), }, nil } + +func (s *Server) ListInstanceTrustedDomains(ctx context.Context, req *admin_pb.ListInstanceTrustedDomainsRequest) (*admin_pb.ListInstanceTrustedDomainsResponse, error) { + queries, err := ListInstanceTrustedDomainsRequestToModel(req) + if err != nil { + return nil, err + } + domains, err := s.query.SearchInstanceTrustedDomains(ctx, queries) + if err != nil { + return nil, err + } + return &admin_pb.ListInstanceTrustedDomainsResponse{ + Result: instance_grpc.TrustedDomainsToPb(domains.Domains), + Details: object.ToListDetails( + domains.Count, + domains.Sequence, + domains.LastRun, + ), + }, nil +} + +func (s *Server) AddInstanceTrustedDomain(ctx context.Context, req *admin_pb.AddInstanceTrustedDomainRequest) (*admin_pb.AddInstanceTrustedDomainResponse, error) { + details, err := s.command.AddTrustedDomain(ctx, req.Domain) + if err != nil { + return nil, err + } + return &admin_pb.AddInstanceTrustedDomainResponse{ + Details: object.DomainToAddDetailsPb(details), + }, nil +} + +func (s *Server) RemoveInstanceTrustedDomain(ctx context.Context, req *admin_pb.RemoveInstanceTrustedDomainRequest) (*admin_pb.RemoveInstanceTrustedDomainResponse, error) { + details, err := s.command.RemoveTrustedDomain(ctx, req.Domain) + if err != nil { + return nil, err + } + return &admin_pb.RemoveInstanceTrustedDomainResponse{ + Details: object.DomainToChangeDetailsPb(details), + }, nil +} diff --git a/internal/api/grpc/admin/instance_converter.go b/internal/api/grpc/admin/instance_converter.go index 0443d8bb8..397845c9e 100644 --- a/internal/api/grpc/admin/instance_converter.go +++ b/internal/api/grpc/admin/instance_converter.go @@ -39,3 +39,20 @@ func fieldNameToInstanceDomainColumn(fieldName instance.DomainFieldName) query.C return query.Column{} } } + +func ListInstanceTrustedDomainsRequestToModel(req *admin_pb.ListInstanceTrustedDomainsRequest) (*query.InstanceTrustedDomainSearchQueries, error) { + offset, limit, asc := object.ListQueryToModel(req.Query) + queries, err := instance_grpc.TrustedDomainQueriesToModel(req.Queries) + if err != nil { + return nil, err + } + return &query.InstanceTrustedDomainSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: fieldNameToInstanceDomainColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} diff --git a/internal/api/grpc/admin/org.go b/internal/api/grpc/admin/org.go index 7d3203970..81064cebf 100644 --- a/internal/api/grpc/admin/org.go +++ b/internal/api/grpc/admin/org.go @@ -6,6 +6,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/object" org_grpc "github.com/zitadel/zitadel/internal/api/grpc/org" + http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" @@ -69,7 +70,7 @@ func (s *Server) ListOrgs(ctx context.Context, req *admin_pb.ListOrgsRequest) (* } func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (*admin_pb.SetUpOrgResponse, error) { - orgDomain, err := domain.NewIAMDomainName(req.Org.Name, authz.GetInstance(ctx).RequestedDomain()) + orgDomain, err := domain.NewIAMDomainName(req.Org.Name, http_utils.DomainContext(ctx).RequestedDomain()) if err != nil { return nil, err } diff --git a/internal/api/grpc/admin/server.go b/internal/api/grpc/admin/server.go index 96bf20834..639d1a9df 100644 --- a/internal/api/grpc/admin/server.go +++ b/internal/api/grpc/admin/server.go @@ -11,7 +11,6 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" - "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/pkg/grpc/admin" @@ -41,8 +40,6 @@ func CreateServer( database string, command *command.Commands, query *query.Queries, - sd systemdefaults.SystemDefaults, - externalSecure bool, userCodeAlg crypto.EncryptionAlgorithm, auditLogRetention time.Duration, ) *Server { @@ -50,7 +47,7 @@ func CreateServer( database: database, command: command, query: query, - assetsAPIDomain: assets.AssetAPI(externalSecure), + assetsAPIDomain: assets.AssetAPI(), userCodeAlg: userCodeAlg, auditLogRetention: auditLogRetention, } diff --git a/internal/api/grpc/auth/passwordless.go b/internal/api/grpc/auth/passwordless.go index b7b914d2c..ddc0a331c 100644 --- a/internal/api/grpc/auth/passwordless.go +++ b/internal/api/grpc/auth/passwordless.go @@ -67,10 +67,9 @@ func (s *Server) AddMyPasswordlessLink(ctx context.Context, _ *auth_pb.AddMyPass if err != nil { return nil, err } - origin := http.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), s.externalSecure) return &auth_pb.AddMyPasswordlessLinkResponse{ Details: object.AddToDetailsPb(initCode.Sequence, initCode.ChangeDate, initCode.ResourceOwner), - Link: initCode.Link(origin + login.HandlerPrefix + login.EndpointPasswordlessRegistration), + Link: initCode.Link(http.DomainContext(ctx).Origin() + login.HandlerPrefix + login.EndpointPasswordlessRegistration), Expiration: durationpb.New(initCode.Expiration), }, nil } diff --git a/internal/api/grpc/auth/server.go b/internal/api/grpc/auth/server.go index 44a617276..3200acffc 100644 --- a/internal/api/grpc/auth/server.go +++ b/internal/api/grpc/auth/server.go @@ -31,7 +31,6 @@ type Server struct { defaults systemdefaults.SystemDefaults assetsAPIDomain func(context.Context) string userCodeAlg crypto.EncryptionAlgorithm - externalSecure bool } type Config struct { @@ -43,16 +42,14 @@ func CreateServer(command *command.Commands, authRepo repository.Repository, defaults systemdefaults.SystemDefaults, userCodeAlg crypto.EncryptionAlgorithm, - externalSecure bool, ) *Server { return &Server{ command: command, query: query, repo: authRepo, defaults: defaults, - assetsAPIDomain: assets.AssetAPI(externalSecure), + assetsAPIDomain: assets.AssetAPI(), userCodeAlg: userCodeAlg, - externalSecure: externalSecure, } } diff --git a/internal/api/grpc/instance/converter.go b/internal/api/grpc/instance/converter.go index 9bad4a3ef..4094da4a7 100644 --- a/internal/api/grpc/instance/converter.go +++ b/internal/api/grpc/instance/converter.go @@ -115,3 +115,43 @@ func DomainToPb(d *query.InstanceDomain) *instance_pb.Domain { ), } } + +func TrustedDomainQueriesToModel(queries []*instance_pb.TrustedDomainSearchQuery) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = TrustedDomainQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func TrustedDomainQueryToModel(searchQuery *instance_pb.TrustedDomainSearchQuery) (query.SearchQuery, error) { + switch q := searchQuery.Query.(type) { + case *instance_pb.TrustedDomainSearchQuery_DomainQuery: + return query.NewInstanceTrustedDomainDomainSearchQuery(object.TextMethodToQuery(q.DomainQuery.Method), q.DomainQuery.Domain) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "INST-Ags42", "List.Query.Invalid") + } +} + +func TrustedDomainsToPb(domains []*query.InstanceTrustedDomain) []*instance_pb.TrustedDomain { + d := make([]*instance_pb.TrustedDomain, len(domains)) + for i, domain := range domains { + d[i] = TrustedDomainToPb(domain) + } + return d +} + +func TrustedDomainToPb(d *query.InstanceTrustedDomain) *instance_pb.TrustedDomain { + return &instance_pb.TrustedDomain{ + Domain: d.Domain, + Details: object.ToViewDetailsPb( + d.Sequence, + d.CreationDate, + d.ChangeDate, + d.InstanceID, + ), + } +} diff --git a/internal/api/grpc/management/information.go b/internal/api/grpc/management/information.go index d18e115bf..8b769f682 100644 --- a/internal/api/grpc/management/information.go +++ b/internal/api/grpc/management/information.go @@ -5,7 +5,6 @@ import ( "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/http" mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management" ) @@ -15,7 +14,7 @@ func (s *Server) Healthz(context.Context, *mgmt_pb.HealthzRequest) (*mgmt_pb.Hea } func (s *Server) GetOIDCInformation(ctx context.Context, _ *mgmt_pb.GetOIDCInformationRequest) (*mgmt_pb.GetOIDCInformationResponse, error) { - issuer := http.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), s.externalSecure) + issuer := http.DomainContext(ctx).Origin() return &mgmt_pb.GetOIDCInformationResponse{ Issuer: issuer, DiscoveryEndpoint: issuer + oidc.DiscoveryEndpoint, diff --git a/internal/api/grpc/management/org.go b/internal/api/grpc/management/org.go index aed1394d6..91ef8e3b8 100644 --- a/internal/api/grpc/management/org.go +++ b/internal/api/grpc/management/org.go @@ -11,6 +11,7 @@ import ( obj_grpc "github.com/zitadel/zitadel/internal/api/grpc/object" org_grpc "github.com/zitadel/zitadel/internal/api/grpc/org" policy_grpc "github.com/zitadel/zitadel/internal/api/grpc/policy" + http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/v1/models" @@ -73,7 +74,7 @@ func (s *Server) ListOrgChanges(ctx context.Context, req *mgmt_pb.ListOrgChanges } func (s *Server) AddOrg(ctx context.Context, req *mgmt_pb.AddOrgRequest) (*mgmt_pb.AddOrgResponse, error) { - orgDomain, err := domain.NewIAMDomainName(req.Name, authz.GetInstance(ctx).RequestedDomain()) + orgDomain, err := domain.NewIAMDomainName(req.Name, http_utils.DomainContext(ctx).RequestedDomain()) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/server.go b/internal/api/grpc/management/server.go index cdefabeab..06a3381fa 100644 --- a/internal/api/grpc/management/server.go +++ b/internal/api/grpc/management/server.go @@ -28,7 +28,6 @@ type Server struct { systemDefaults systemdefaults.SystemDefaults assetAPIPrefix func(context.Context) string userCodeAlg crypto.EncryptionAlgorithm - externalSecure bool } func CreateServer( @@ -36,15 +35,13 @@ func CreateServer( query *query.Queries, sd systemdefaults.SystemDefaults, userCodeAlg crypto.EncryptionAlgorithm, - externalSecure bool, ) *Server { return &Server{ command: command, query: query, systemDefaults: sd, - assetAPIPrefix: assets.AssetAPI(externalSecure), + assetAPIPrefix: assets.AssetAPI(), userCodeAlg: userCodeAlg, - externalSecure: externalSecure, } } diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index d3cab6296..64d99ea78 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -286,9 +286,8 @@ func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUs ), } if code != nil { - origin := http.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), s.externalSecure) resp.PasswordlessRegistration = &mgmt_pb.ImportHumanUserResponse_PasswordlessRegistration{ - Link: code.Link(origin + login.HandlerPrefix + login.EndpointPasswordlessRegistration), + Link: code.Link(http.DomainContext(ctx).Origin() + login.HandlerPrefix + login.EndpointPasswordlessRegistration), Lifetime: durationpb.New(code.Expiration), Expiration: durationpb.New(code.Expiration), } @@ -691,10 +690,9 @@ func (s *Server) AddPasswordlessRegistration(ctx context.Context, req *mgmt_pb.A if err != nil { return nil, err } - origin := http.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), s.externalSecure) return &mgmt_pb.AddPasswordlessRegistrationResponse{ Details: obj_grpc.AddToDetailsPb(initCode.Sequence, initCode.ChangeDate, initCode.ResourceOwner), - Link: initCode.Link(origin + login.HandlerPrefix + login.EndpointPasswordlessRegistration), + Link: initCode.Link(http.DomainContext(ctx).Origin() + login.HandlerPrefix + login.EndpointPasswordlessRegistration), Expiration: durationpb.New(initCode.Expiration), }, nil } diff --git a/internal/api/grpc/oidc/v2/oidc.go b/internal/api/grpc/oidc/v2/oidc.go index d84edd1c2..826c198fa 100644 --- a/internal/api/grpc/oidc/v2/oidc.go +++ b/internal/api/grpc/oidc/v2/oidc.go @@ -8,7 +8,6 @@ import ( "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/oidc" @@ -107,7 +106,7 @@ func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID str return nil, err } authReq := &oidc.AuthRequestV2{CurrentAuthRequest: aar} - ctx = op.ContextWithIssuer(ctx, http.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), s.externalSecure)) + ctx = op.ContextWithIssuer(ctx, http.DomainContext(ctx).Origin()) var callback string if aar.ResponseType == domain.OIDCResponseTypeCode { callback, err = oidc.CreateCodeCallbackURL(ctx, authReq, s.op.Provider()) diff --git a/internal/api/grpc/oidc/v2beta/oidc.go b/internal/api/grpc/oidc/v2beta/oidc.go index d504e411f..04ffdbb34 100644 --- a/internal/api/grpc/oidc/v2beta/oidc.go +++ b/internal/api/grpc/oidc/v2beta/oidc.go @@ -8,7 +8,6 @@ import ( "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/zitadel/zitadel/internal/api/authz" object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/oidc" @@ -107,7 +106,7 @@ func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID str return nil, err } authReq := &oidc.AuthRequestV2{CurrentAuthRequest: aar} - ctx = op.ContextWithIssuer(ctx, http.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), s.externalSecure)) + ctx = op.ContextWithIssuer(ctx, http.DomainContext(ctx).Origin()) var callback string if aar.ResponseType == domain.OIDCResponseTypeCode { callback, err = oidc.CreateCodeCallbackURL(ctx, authReq, s.op.Provider()) diff --git a/internal/api/grpc/server/gateway.go b/internal/api/grpc/server/gateway.go index 25b4c6996..3e41be94a 100644 --- a/internal/api/grpc/server/gateway.go +++ b/internal/api/grpc/server/gateway.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "fmt" "net/http" + "slices" "strings" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" @@ -19,10 +20,8 @@ import ( "google.golang.org/protobuf/proto" client_middleware "github.com/zitadel/zitadel/internal/api/grpc/client/middleware" - "github.com/zitadel/zitadel/internal/api/grpc/server/middleware" http_utils "github.com/zitadel/zitadel/internal/api/http" http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" - "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/telemetry/metrics" ) @@ -57,26 +56,29 @@ var ( }, ) - serveMuxOptions = []runtime.ServeMuxOption{ - runtime.WithMarshalerOption(jsonMarshaler.ContentType(nil), jsonMarshaler), - runtime.WithMarshalerOption(mimeWildcard, jsonMarshaler), - runtime.WithMarshalerOption(runtime.MIMEWildcard, jsonMarshaler), - runtime.WithIncomingHeaderMatcher(headerMatcher), - runtime.WithOutgoingHeaderMatcher(runtime.DefaultHeaderMatcher), - runtime.WithForwardResponseOption(responseForwarder), - runtime.WithRoutingErrorHandler(httpErrorHandler), + serveMuxOptions = func(hostHeaders []string) []runtime.ServeMuxOption { + return []runtime.ServeMuxOption{ + runtime.WithMarshalerOption(jsonMarshaler.ContentType(nil), jsonMarshaler), + runtime.WithMarshalerOption(mimeWildcard, jsonMarshaler), + runtime.WithMarshalerOption(runtime.MIMEWildcard, jsonMarshaler), + runtime.WithIncomingHeaderMatcher(headerMatcher(hostHeaders)), + runtime.WithOutgoingHeaderMatcher(runtime.DefaultHeaderMatcher), + runtime.WithForwardResponseOption(responseForwarder), + runtime.WithRoutingErrorHandler(httpErrorHandler), + } } - headerMatcher = runtime.HeaderMatcherFunc( - func(header string) (string, bool) { + headerMatcher = func(hostHeaders []string) runtime.HeaderMatcherFunc { + customHeaders = slices.Compact(append(customHeaders, hostHeaders...)) + return func(header string) (string, bool) { for _, customHeader := range customHeaders { if strings.HasPrefix(strings.ToLower(header), customHeader) { return header, true } } return runtime.DefaultHeaderMatcher(header) - }, - ) + } + } responseForwarder = func(ctx context.Context, w http.ResponseWriter, resp proto.Message) error { t, ok := resp.(CustomHTTPResponse) @@ -90,14 +92,12 @@ var ( type Gateway struct { mux *runtime.ServeMux - http1HostName string connection *grpc.ClientConn accessInterceptor *http_mw.AccessInterceptor - queries *query.Queries } func (g *Gateway) Handler() http.Handler { - return addInterceptors(g.mux, g.http1HostName, g.accessInterceptor, g.queries) + return addInterceptors(g.mux, g.accessInterceptor) } type CustomHTTPResponse interface { @@ -110,12 +110,11 @@ func CreateGatewayWithPrefix( ctx context.Context, g WithGatewayPrefix, port uint16, - http1HostName string, + hostHeaders []string, accessInterceptor *http_mw.AccessInterceptor, - queries *query.Queries, tlsConfig *tls.Config, ) (http.Handler, string, error) { - runtimeMux := runtime.NewServeMux(serveMuxOptions...) + runtimeMux := runtime.NewServeMux(serveMuxOptions(hostHeaders)...) opts := []grpc.DialOption{ grpc.WithTransportCredentials(grpcCredentials(tlsConfig)), grpc.WithChainUnaryInterceptor( @@ -131,13 +130,13 @@ func CreateGatewayWithPrefix( if err != nil { return nil, "", fmt.Errorf("failed to register grpc gateway: %w", err) } - return addInterceptors(runtimeMux, http1HostName, accessInterceptor, queries), g.GatewayPathPrefix(), nil + return addInterceptors(runtimeMux, accessInterceptor), g.GatewayPathPrefix(), nil } func CreateGateway( ctx context.Context, port uint16, - http1HostName string, + hostHeaders []string, accessInterceptor *http_mw.AccessInterceptor, tlsConfig *tls.Config, ) (*Gateway, error) { @@ -153,10 +152,9 @@ func CreateGateway( if err != nil { return nil, err } - runtimeMux := runtime.NewServeMux(append(serveMuxOptions, runtime.WithHealthzEndpoint(healthpb.NewHealthClient(connection)))...) + runtimeMux := runtime.NewServeMux(append(serveMuxOptions(hostHeaders), runtime.WithHealthzEndpoint(healthpb.NewHealthClient(connection)))...) return &Gateway{ mux: runtimeMux, - http1HostName: http1HostName, connection: connection, accessInterceptor: accessInterceptor, }, nil @@ -195,12 +193,9 @@ func dial(ctx context.Context, port uint16, opts []grpc.DialOption) (*grpc.Clien func addInterceptors( handler http.Handler, - http1HostName string, accessInterceptor *http_mw.AccessInterceptor, - queries *query.Queries, ) http.Handler { handler = http_mw.CallDurationHandler(handler) - handler = http1Host(handler, http1HostName) handler = http_mw.CORSInterceptor(handler) handler = http_mw.RobotsTagHandler(handler) handler = http_mw.DefaultTelemetryHandler(handler) @@ -215,18 +210,6 @@ func addInterceptors( return handler } -func http1Host(next http.Handler, http1HostName string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - host, err := http_mw.HostFromRequest(r, http1HostName) - if err != nil { - http.Error(w, err.Error(), http.StatusForbidden) - return - } - r.Header.Set(middleware.HTTP1Host, host) - next.ServeHTTP(w, r) - }) -} - func exhaustedCookieInterceptor( next http.Handler, accessInterceptor *http_mw.AccessInterceptor, diff --git a/internal/api/grpc/server/middleware/access_interceptor.go b/internal/api/grpc/server/middleware/access_interceptor.go index 719e03da7..100264c3f 100644 --- a/internal/api/grpc/server/middleware/access_interceptor.go +++ b/internal/api/grpc/server/middleware/access_interceptor.go @@ -9,6 +9,7 @@ import ( "google.golang.org/grpc/status" "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/logstore" "github.com/zitadel/zitadel/internal/logstore/record" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -35,6 +36,7 @@ func AccessStorageInterceptor(svc *logstore.Service[*record.AccessLog]) grpc.Una resMd, _ := metadata.FromOutgoingContext(ctx) instance := authz.GetInstance(ctx) + domainCtx := http_util.DomainContext(ctx) r := &record.AccessLog{ LogDate: time.Now(), @@ -45,8 +47,8 @@ func AccessStorageInterceptor(svc *logstore.Service[*record.AccessLog]) grpc.Una ResponseHeaders: resMd, InstanceID: instance.InstanceID(), ProjectID: instance.ProjectID(), - RequestedDomain: instance.RequestedDomain(), - RequestedHost: instance.RequestedHost(), + RequestedDomain: domainCtx.RequestedDomain(), + RequestedHost: domainCtx.RequestedHost(), } svc.Handle(interceptorCtx, r) diff --git a/internal/api/grpc/server/middleware/instance_interceptor.go b/internal/api/grpc/server/middleware/instance_interceptor.go index ba637d59f..07ae1ba27 100644 --- a/internal/api/grpc/server/middleware/instance_interceptor.go +++ b/internal/api/grpc/server/middleware/instance_interceptor.go @@ -10,7 +10,6 @@ import ( "golang.org/x/text/language" "google.golang.org/grpc" "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "github.com/zitadel/zitadel/internal/api/authz" @@ -24,15 +23,15 @@ const ( HTTP1Host = "x-zitadel-http1-host" ) -func InstanceInterceptor(verifier authz.InstanceVerifier, headerName, externalDomain string, explicitInstanceIdServices ...string) grpc.UnaryServerInterceptor { +func InstanceInterceptor(verifier authz.InstanceVerifier, externalDomain string, explicitInstanceIdServices ...string) grpc.UnaryServerInterceptor { translator, err := i18n.NewZitadelTranslator(language.English) logging.OnError(err).Panic("unable to get translator") return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - return setInstance(ctx, req, info, handler, verifier, headerName, externalDomain, translator, explicitInstanceIdServices...) + return setInstance(ctx, req, info, handler, verifier, externalDomain, translator, explicitInstanceIdServices...) } } -func setInstance(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.InstanceVerifier, headerName, externalDomain string, translator *i18n.Translator, idFromRequestsServices ...string) (_ interface{}, err error) { +func setInstance(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.InstanceVerifier, externalDomain string, translator *i18n.Translator, idFromRequestsServices ...string) (_ interface{}, err error) { interceptorCtx, span := tracing.NewServerInterceptorSpan(ctx) defer func() { span.EndWithError(err) }() for _, service := range idFromRequestsServices { @@ -56,14 +55,15 @@ func setInstance(ctx context.Context, req interface{}, info *grpc.UnaryServerInf return handler(authz.WithInstance(ctx, instance), req) } } - host, err := hostFromContext(interceptorCtx, headerName) - if err != nil { - return nil, status.Error(codes.NotFound, err.Error()) + requestContext := zitadel_http.DomainContext(ctx) + if requestContext.InstanceHost == "" { + logging.WithFields("origin", requestContext.Origin(), "externalDomain", externalDomain).WithError(err).Error("unable to set instance") + return nil, status.Error(codes.NotFound, "no instanceHost specified") } - instance, err := verifier.InstanceByHost(interceptorCtx, host) + instance, err := verifier.InstanceByHost(interceptorCtx, requestContext.InstanceHost, requestContext.PublicHost) if err != nil { - origin := zitadel_http.ComposedOrigin(ctx) - logging.WithFields("origin", origin, "externalDomain", externalDomain).WithError(err).Error("unable to set instance") + 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)) @@ -75,33 +75,3 @@ func setInstance(ctx context.Context, req interface{}, info *grpc.UnaryServerInf span.End() return handler(authz.WithInstance(ctx, instance), req) } - -func hostFromContext(ctx context.Context, headerName string) (string, error) { - md, ok := metadata.FromIncomingContext(ctx) - if !ok { - return "", fmt.Errorf("cannot read metadata") - } - host, ok := md[HTTP1Host] - if ok && len(host) == 1 { - if !isAllowedToSendHTTP1Header(md) { - return "", fmt.Errorf("no valid host header") - } - return host[0], nil - } - host, ok = md[headerName] - if !ok { - return "", fmt.Errorf("cannot find header: %v", headerName) - } - if len(host) != 1 { - return "", fmt.Errorf("invalid host header: %v", host) - } - return host[0], nil -} - -// isAllowedToSendHTTP1Header check if the gRPC call was sent to `localhost` -// this is only possible when calling the server directly running on localhost -// or through the gRPC gateway -func isAllowedToSendHTTP1Header(md metadata.MD) bool { - authority, ok := md[":authority"] - return ok && len(authority) == 1 && strings.Split(authority[0], ":")[0] == "localhost" -} diff --git a/internal/api/grpc/server/middleware/instance_interceptor_test.go b/internal/api/grpc/server/middleware/instance_interceptor_test.go index 132ced4e6..211444a70 100644 --- a/internal/api/grpc/server/middleware/instance_interceptor_test.go +++ b/internal/api/grpc/server/middleware/instance_interceptor_test.go @@ -9,82 +9,19 @@ import ( "golang.org/x/text/language" "google.golang.org/grpc" - "google.golang.org/grpc/metadata" "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/feature" ) -func Test_hostNameFromContext(t *testing.T) { - type args struct { - ctx context.Context - headerName string - } - type res struct { - want string - err bool - } - tests := []struct { - name string - args args - res res - }{ - { - "empty context, error", - args{ - ctx: context.Background(), - headerName: "header", - }, - res{ - want: "", - err: true, - }, - }, - { - "header not found", - args{ - ctx: metadata.NewIncomingContext(context.Background(), nil), - headerName: "header", - }, - res{ - want: "", - err: true, - }, - }, - { - "header not found", - args{ - ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("header", "value")), - headerName: "header", - }, - res{ - want: "value", - err: false, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := hostFromContext(tt.args.ctx, tt.args.headerName) - if (err != nil) != tt.res.err { - t.Errorf("hostFromContext() error = %v, wantErr %v", err, tt.res.err) - return - } - if got != tt.res.want { - t.Errorf("hostFromContext() got = %v, want %v", got, tt.res.want) - } - }) - } -} - func Test_setInstance(t *testing.T) { type args struct { - ctx context.Context - req interface{} - info *grpc.UnaryServerInfo - handler grpc.UnaryHandler - verifier authz.InstanceVerifier - headerName string + ctx context.Context + req interface{} + info *grpc.UnaryServerInfo + handler grpc.UnaryHandler + verifier authz.InstanceVerifier } type res struct { want interface{} @@ -108,10 +45,9 @@ func Test_setInstance(t *testing.T) { { "invalid host, error", args{ - ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("header", "host2")), - req: &mockRequest{}, - verifier: &mockInstanceVerifier{"host"}, - headerName: "header", + ctx: http_util.WithDomainContext(context.Background(), &http_util.DomainCtx{InstanceHost: "host2"}), + req: &mockRequest{}, + verifier: &mockInstanceVerifier{instanceHost: "host"}, }, res{ want: nil, @@ -121,10 +57,9 @@ func Test_setInstance(t *testing.T) { { "valid host", args{ - ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("header", "host")), - req: &mockRequest{}, - verifier: &mockInstanceVerifier{"host"}, - headerName: "header", + ctx: http_util.WithDomainContext(context.Background(), &http_util.DomainCtx{InstanceHost: "host"}), + req: &mockRequest{}, + verifier: &mockInstanceVerifier{instanceHost: "host"}, handler: func(ctx context.Context, req interface{}) (interface{}, error) { return req, nil }, @@ -137,7 +72,7 @@ func Test_setInstance(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := setInstance(tt.args.ctx, tt.args.req, tt.args.info, tt.args.handler, tt.args.verifier, tt.args.headerName, "", nil) + got, err := setInstance(tt.args.ctx, tt.args.req, tt.args.info, tt.args.handler, tt.args.verifier, "", nil) if (err != nil) != tt.res.err { t.Errorf("setInstance() error = %v, wantErr %v", err, tt.res.err) return @@ -152,11 +87,18 @@ func Test_setInstance(t *testing.T) { type mockRequest struct{} type mockInstanceVerifier struct { - host string + instanceHost string + publicHost string } -func (m *mockInstanceVerifier) InstanceByHost(_ context.Context, host string) (authz.Instance, error) { - if host != m.host { +func (m *mockInstanceVerifier) InstanceByHost(_ context.Context, instanceHost, publicHost string) (authz.Instance, error) { + if instanceHost != m.instanceHost { + return nil, fmt.Errorf("invalid host") + } + if publicHost == "" { + return &mockInstance{}, nil + } + if publicHost != instanceHost && publicHost != m.publicHost { return nil, fmt.Errorf("invalid host") } return &mockInstance{}, nil @@ -198,14 +140,6 @@ func (m *mockInstance) DefaultOrganisationID() string { return "orgID" } -func (m *mockInstance) RequestedDomain() string { - return "localhost" -} - -func (m *mockInstance) RequestedHost() string { - return "localhost:8080" -} - func (m *mockInstance) SecurityPolicyAllowedOrigins() []string { return nil } diff --git a/internal/api/grpc/server/server.go b/internal/api/grpc/server/server.go index ef4c271bf..5408ae257 100644 --- a/internal/api/grpc/server/server.go +++ b/internal/api/grpc/server/server.go @@ -38,7 +38,6 @@ func CreateServer( verifier authz.APITokenVerifier, authConfig authz.Config, queries *query.Queries, - hostHeaderName string, externalDomain string, tlsConfig *tls.Config, accessSvc *logstore.Service[*record.AccessLog], @@ -51,7 +50,7 @@ func CreateServer( middleware.DefaultTracingServer(), middleware.MetricsHandler(metricTypes, grpc_api.Probes...), middleware.NoCacheInterceptor(), - middleware.InstanceInterceptor(queries, hostHeaderName, externalDomain, system_pb.SystemService_ServiceDesc.ServiceName, healthpb.Health_ServiceDesc.ServiceName), + middleware.InstanceInterceptor(queries, externalDomain, system_pb.SystemService_ServiceDesc.ServiceName, healthpb.Health_ServiceDesc.ServiceName), middleware.AccessStorageInterceptor(accessSvc), middleware.ErrorHandler(), middleware.LimitsInterceptor(system_pb.SystemService_ServiceDesc.ServiceName), diff --git a/internal/api/grpc/settings/v2/server.go b/internal/api/grpc/settings/v2/server.go index 0391d0118..9cae50824 100644 --- a/internal/api/grpc/settings/v2/server.go +++ b/internal/api/grpc/settings/v2/server.go @@ -27,12 +27,11 @@ type Config struct{} func CreateServer( command *command.Commands, query *query.Queries, - externalSecure bool, ) *Server { return &Server{ command: command, query: query, - assetsAPIDomain: assets.AssetAPI(externalSecure), + assetsAPIDomain: assets.AssetAPI(), } } diff --git a/internal/api/grpc/settings/v2beta/server.go b/internal/api/grpc/settings/v2beta/server.go index f00154959..24c8f7774 100644 --- a/internal/api/grpc/settings/v2beta/server.go +++ b/internal/api/grpc/settings/v2beta/server.go @@ -27,12 +27,11 @@ type Config struct{} func CreateServer( command *command.Commands, query *query.Queries, - externalSecure bool, ) *Server { return &Server{ command: command, query: query, - assetsAPIDomain: assets.AssetAPI(externalSecure), + assetsAPIDomain: assets.AssetAPI(), } } diff --git a/internal/api/http/header.go b/internal/api/http/header.go index 14ae3dfb6..91726c433 100644 --- a/internal/api/http/header.go +++ b/internal/api/http/header.go @@ -20,6 +20,9 @@ const ( Pragma = "pragma" UserAgentHeader = "user-agent" ForwardedFor = "x-forwarded-for" + ForwardedHost = "x-forwarded-host" + ForwardedProto = "x-forwarded-proto" + Forwarded = "forwarded" XUserAgent = "x-user-agent" XGrpcWeb = "x-grpc-web" XRequestedWith = "x-requested-with" @@ -45,7 +48,7 @@ type key int const ( httpHeaders key = iota remoteAddr - origin + domainCtx ) func CopyHeadersToContext(h http.Handler) http.Handler { @@ -70,18 +73,6 @@ func OriginHeader(ctx context.Context) string { return headers.Get(Origin) } -func ComposedOrigin(ctx context.Context) string { - o, ok := ctx.Value(origin).(string) - if !ok { - return "" - } - return o -} - -func WithComposedOrigin(ctx context.Context, composed string) context.Context { - return context.WithValue(ctx, origin, composed) -} - func RemoteIPFromCtx(ctx context.Context) string { ctxHeaders, ok := HeadersFromCtx(ctx) if !ok { diff --git a/internal/api/http/middleware/access_interceptor.go b/internal/api/http/middleware/access_interceptor.go index f1dd0182f..f1a935d95 100644 --- a/internal/api/http/middleware/access_interceptor.go +++ b/internal/api/http/middleware/access_interceptor.go @@ -163,6 +163,7 @@ func (a *AccessInterceptor) writeLog(ctx context.Context, wrappedWriter *statusR logging.WithError(err).WithField("url", requestURL).Warning("failed to unescape request url") } instance := authz.GetInstance(ctx) + domainCtx := http_utils.DomainContext(ctx) a.logstoreSvc.Handle(ctx, &record.AccessLog{ LogDate: time.Now(), Protocol: record.HTTP, @@ -172,8 +173,8 @@ func (a *AccessInterceptor) writeLog(ctx context.Context, wrappedWriter *statusR ResponseHeaders: writer.Header(), InstanceID: instance.InstanceID(), ProjectID: instance.ProjectID(), - RequestedDomain: instance.RequestedDomain(), - RequestedHost: instance.RequestedHost(), + RequestedDomain: domainCtx.RequestedDomain(), + RequestedHost: domainCtx.RequestedHost(), NotCountable: notCountable, }) } diff --git a/internal/api/http/middleware/instance_interceptor.go b/internal/api/http/middleware/instance_interceptor.go index 2117b98d3..facb2ceec 100644 --- a/internal/api/http/middleware/instance_interceptor.go +++ b/internal/api/http/middleware/instance_interceptor.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "net/http" - "net/url" "strings" "github.com/zitadel/logging" @@ -19,16 +18,15 @@ import ( ) type instanceInterceptor struct { - verifier authz.InstanceVerifier - headerName, externalDomain string - ignoredPrefixes []string - translator *i18n.Translator + verifier authz.InstanceVerifier + externalDomain string + ignoredPrefixes []string + translator *i18n.Translator } -func InstanceInterceptor(verifier authz.InstanceVerifier, headerName, externalDomain string, ignoredPrefixes ...string) *instanceInterceptor { +func InstanceInterceptor(verifier authz.InstanceVerifier, externalDomain string, ignoredPrefixes ...string) *instanceInterceptor { return &instanceInterceptor{ verifier: verifier, - headerName: headerName, externalDomain: externalDomain, ignoredPrefixes: ignoredPrefixes, translator: newZitadelTranslator(), @@ -54,10 +52,10 @@ func (a *instanceInterceptor) handleInstance(w http.ResponseWriter, r *http.Requ return } } - ctx, err := setInstance(r, a.verifier, a.headerName) + ctx, err := setInstance(r, a.verifier) if err != nil { - origin := zitadel_http.ComposedOrigin(r.Context()) - logging.WithFields("origin", origin, "externalDomain", a.externalDomain).WithError(err).Error("unable to set instance") + origin := zitadel_http.DomainContext(r.Context()) + logging.WithFields("origin", origin.Origin(), "externalDomain", a.externalDomain).WithError(err).Error("unable to set instance") zErr := new(zerrors.ZitadelError) if errors.As(err, &zErr) { zErr.SetMessage(a.translator.LocalizeFromRequest(r, zErr.GetMessage(), nil)) @@ -71,18 +69,16 @@ func (a *instanceInterceptor) handleInstance(w http.ResponseWriter, r *http.Requ next.ServeHTTP(w, r) } -func setInstance(r *http.Request, verifier authz.InstanceVerifier, headerName string) (_ context.Context, err error) { +func setInstance(r *http.Request, verifier authz.InstanceVerifier) (_ context.Context, err error) { ctx := r.Context() authCtx, span := tracing.NewServerInterceptorSpan(ctx) defer func() { span.EndWithError(err) }() - host, err := HostFromRequest(r, headerName) - - if err != nil { + requestContext := zitadel_http.DomainContext(ctx) + if requestContext.InstanceHost == "" { return nil, zerrors.ThrowNotFound(err, "INST-zWq7X", "Errors.IAM.NotFound") } - - instance, err := verifier.InstanceByHost(authCtx, host) + instance, err := verifier.InstanceByHost(authCtx, requestContext.InstanceHost, requestContext.PublicHost) if err != nil { return nil, err } @@ -90,39 +86,6 @@ func setInstance(r *http.Request, verifier authz.InstanceVerifier, headerName st return authz.WithInstance(ctx, instance), nil } -func HostFromRequest(r *http.Request, headerName string) (host string, err error) { - if headerName != "host" { - return hostFromSpecialHeader(r, headerName) - } - return hostFromOrigin(r.Context()) -} - -func hostFromSpecialHeader(r *http.Request, headerName string) (host string, err error) { - host = r.Header.Get(headerName) - if host == "" { - return "", fmt.Errorf("host header `%s` not found", headerName) - } - return host, nil -} - -func hostFromOrigin(ctx context.Context) (host string, err error) { - defer func() { - if err != nil { - err = fmt.Errorf("invalid origin: %w", err) - } - }() - origin := zitadel_http.ComposedOrigin(ctx) - u, err := url.Parse(origin) - if err != nil { - return "", err - } - host = u.Host - if host == "" { - err = errors.New("empty host") - } - return host, err -} - func newZitadelTranslator() *i18n.Translator { translator, err := i18n.NewZitadelTranslator(language.English) logging.OnError(err).Panic("unable to get translator") diff --git a/internal/api/http/middleware/instance_interceptor_test.go b/internal/api/http/middleware/instance_interceptor_test.go index 4bb027691..7a2d7aeb2 100644 --- a/internal/api/http/middleware/instance_interceptor_test.go +++ b/internal/api/http/middleware/instance_interceptor_test.go @@ -19,8 +19,7 @@ import ( func Test_instanceInterceptor_Handler(t *testing.T) { type fields struct { - verifier authz.InstanceVerifier - headerName string + verifier authz.InstanceVerifier } type args struct { request *http.Request @@ -38,8 +37,7 @@ func Test_instanceInterceptor_Handler(t *testing.T) { { "setInstance error", fields{ - verifier: &mockInstanceVerifier{}, - headerName: "header", + verifier: &mockInstanceVerifier{}, }, args{ request: httptest.NewRequest("", "/url", nil), @@ -52,19 +50,18 @@ func Test_instanceInterceptor_Handler(t *testing.T) { { "setInstance ok", fields{ - verifier: &mockInstanceVerifier{"host"}, - headerName: "header", + verifier: &mockInstanceVerifier{instanceHost: "host"}, }, args{ request: func() *http.Request { r := httptest.NewRequest("", "/url", nil) - r.Header.Set("header", "host") + r = r.WithContext(zitadel_http.WithDomainContext(r.Context(), &zitadel_http.DomainCtx{InstanceHost: "host"})) return r }(), }, res{ statusCode: 200, - context: authz.WithInstance(context.Background(), &mockInstance{}), + context: authz.WithInstance(zitadel_http.WithDomainContext(context.Background(), &zitadel_http.DomainCtx{InstanceHost: "host"}), &mockInstance{}), }, }, } @@ -72,7 +69,6 @@ func Test_instanceInterceptor_Handler(t *testing.T) { t.Run(tt.name, func(t *testing.T) { a := &instanceInterceptor{ verifier: tt.fields.verifier, - headerName: tt.fields.headerName, translator: newZitadelTranslator(), } next := &testHandler{} @@ -87,8 +83,7 @@ func Test_instanceInterceptor_Handler(t *testing.T) { func Test_instanceInterceptor_HandlerFunc(t *testing.T) { type fields struct { - verifier authz.InstanceVerifier - headerName string + verifier authz.InstanceVerifier } type args struct { request *http.Request @@ -106,8 +101,7 @@ func Test_instanceInterceptor_HandlerFunc(t *testing.T) { { "setInstance error", fields{ - verifier: &mockInstanceVerifier{}, - headerName: "header", + verifier: &mockInstanceVerifier{}, }, args{ request: httptest.NewRequest("", "/url", nil), @@ -120,19 +114,18 @@ func Test_instanceInterceptor_HandlerFunc(t *testing.T) { { "setInstance ok", fields{ - verifier: &mockInstanceVerifier{"host"}, - headerName: "header", + verifier: &mockInstanceVerifier{instanceHost: "host"}, }, args{ request: func() *http.Request { r := httptest.NewRequest("", "/url", nil) - r.Header.Set("header", "host") + r = r.WithContext(zitadel_http.WithDomainContext(r.Context(), &zitadel_http.DomainCtx{InstanceHost: "host"})) return r }(), }, res{ statusCode: 200, - context: authz.WithInstance(context.Background(), &mockInstance{}), + context: authz.WithInstance(zitadel_http.WithDomainContext(context.Background(), &zitadel_http.DomainCtx{InstanceHost: "host"}), &mockInstance{}), }, }, } @@ -140,7 +133,6 @@ func Test_instanceInterceptor_HandlerFunc(t *testing.T) { t.Run(tt.name, func(t *testing.T) { a := &instanceInterceptor{ verifier: tt.fields.verifier, - headerName: tt.fields.headerName, translator: newZitadelTranslator(), } next := &testHandler{} @@ -155,9 +147,8 @@ func Test_instanceInterceptor_HandlerFunc(t *testing.T) { func Test_setInstance(t *testing.T) { type args struct { - r *http.Request - verifier authz.InstanceVerifier - headerName string + r *http.Request + verifier authz.InstanceVerifier } type res struct { want context.Context @@ -169,14 +160,13 @@ func Test_setInstance(t *testing.T) { res res }{ { - "special host header not found, error", + "no domain context, not found error", args{ r: func() *http.Request { r := httptest.NewRequest("", "/url", nil) return r }(), - verifier: &mockInstanceVerifier{}, - headerName: "", + verifier: &mockInstanceVerifier{}, }, res{ want: nil, @@ -184,77 +174,27 @@ func Test_setInstance(t *testing.T) { }, }, { - "special host header invalid, error", + "instanceHost found, ok", args{ r: func() *http.Request { r := httptest.NewRequest("", "/url", nil) - r.Header.Set("header", "host2") - return r + return r.WithContext(zitadel_http.WithDomainContext(r.Context(), &zitadel_http.DomainCtx{InstanceHost: "host", Protocol: "https"})) }(), - verifier: &mockInstanceVerifier{"host"}, - headerName: "header", + verifier: &mockInstanceVerifier{instanceHost: "host"}, }, res{ - want: nil, - err: true, - }, - }, - { - "special host header valid, ok", - args{ - r: func() *http.Request { - r := httptest.NewRequest("", "/url", nil) - r.Header.Set("header", "host") - return r - }(), - verifier: &mockInstanceVerifier{"host"}, - headerName: "header", - }, - res{ - want: authz.WithInstance(context.Background(), &mockInstance{}), + want: authz.WithInstance(zitadel_http.WithDomainContext(context.Background(), &zitadel_http.DomainCtx{InstanceHost: "host", Protocol: "https"}), &mockInstance{}), err: false, }, }, { - "host from origin if header is not special, ok", + "instanceHost not found, error", args{ r: func() *http.Request { r := httptest.NewRequest("", "/url", nil) - r.Header.Set("host", "fromrequest") - return r.WithContext(zitadel_http.WithComposedOrigin(r.Context(), "https://fromorigin:9999")) + return r.WithContext(zitadel_http.WithDomainContext(r.Context(), &zitadel_http.DomainCtx{InstanceHost: "fromorigin:9999", Protocol: "https"})) }(), - verifier: &mockInstanceVerifier{"fromorigin:9999"}, - headerName: "host", - }, - res{ - want: authz.WithInstance(zitadel_http.WithComposedOrigin(context.Background(), "https://fromorigin:9999"), &mockInstance{}), - err: false, - }, - }, - { - "host from origin, instance not found", - args{ - r: func() *http.Request { - r := httptest.NewRequest("", "/url", nil) - return r.WithContext(zitadel_http.WithComposedOrigin(r.Context(), "https://fromorigin:9999")) - }(), - verifier: &mockInstanceVerifier{"unknowndomain"}, - headerName: "host", - }, - res{ - want: nil, - err: true, - }, - }, - { - "host from origin invalid, err", - args{ - r: func() *http.Request { - r := httptest.NewRequest("", "/url", nil) - return r.WithContext(zitadel_http.WithComposedOrigin(r.Context(), "https://from origin:9999")) - }(), - verifier: &mockInstanceVerifier{"from origin"}, - headerName: "host", + verifier: &mockInstanceVerifier{instanceHost: "unknowndomain"}, }, res{ want: nil, @@ -264,7 +204,7 @@ func Test_setInstance(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := setInstance(tt.args.r, tt.args.verifier, tt.args.headerName) + got, err := setInstance(tt.args.r, tt.args.verifier) if (err != nil) != tt.res.err { t.Errorf("setInstance() error = %v, wantErr %v", err, tt.res.err) return @@ -285,11 +225,18 @@ func (t *testHandler) ServeHTTP(_ http.ResponseWriter, r *http.Request) { } type mockInstanceVerifier struct { - host string + instanceHost string + publicHost string } -func (m *mockInstanceVerifier) InstanceByHost(_ context.Context, host string) (authz.Instance, error) { - if host != m.host { +func (m *mockInstanceVerifier) InstanceByHost(_ context.Context, instanceHost, publicHost string) (authz.Instance, error) { + if instanceHost != m.instanceHost { + return nil, fmt.Errorf("invalid host") + } + if publicHost == "" { + return &mockInstance{}, nil + } + if publicHost != instanceHost && publicHost != m.publicHost { return nil, fmt.Errorf("invalid host") } return &mockInstance{}, nil @@ -333,14 +280,6 @@ func (m *mockInstance) DefaultOrganisationID() string { return "orgID" } -func (m *mockInstance) RequestedDomain() string { - return "zitadel.cloud" -} - -func (m *mockInstance) RequestedHost() string { - return "zitadel.cloud:443" -} - func (m *mockInstance) SecurityPolicyAllowedOrigins() []string { return nil } diff --git a/internal/api/http/middleware/origin_interceptor.go b/internal/api/http/middleware/origin_interceptor.go index 02a67ad05..bbec9dc14 100644 --- a/internal/api/http/middleware/origin_interceptor.go +++ b/internal/api/http/middleware/origin_interceptor.go @@ -1,53 +1,82 @@ package middleware import ( - "fmt" "net/http" + "slices" "github.com/gorilla/mux" "github.com/muhlemmer/httpforwarded" - "github.com/zitadel/logging" http_util "github.com/zitadel/zitadel/internal/api/http" ) -func WithOrigin(fallBackToHttps bool) mux.MiddlewareFunc { +func WithOrigin(fallBackToHttps bool, http1Header, http2Header string, instanceHostHeaders, publicDomainHeaders []string) mux.MiddlewareFunc { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - origin := composeOrigin(r, fallBackToHttps) - if !http_util.IsOrigin(origin) { - logging.Debugf("extracted origin is not valid: %s", origin) - next.ServeHTTP(w, r) - return - } - next.ServeHTTP(w, r.WithContext(http_util.WithComposedOrigin(r.Context(), origin))) + origin := composeDomainContext( + r, + fallBackToHttps, + // to make sure we don't break existing configurations we append the existing checked headers as well + slices.Compact(append(instanceHostHeaders, http1Header, http2Header, http_util.Forwarded, http_util.ForwardedFor, http_util.ForwardedHost, http_util.ForwardedProto)), + publicDomainHeaders, + ) + next.ServeHTTP(w, r.WithContext(http_util.WithDomainContext(r.Context(), origin))) }) } } -func composeOrigin(r *http.Request, fallBackToHttps bool) string { - var proto, host string - fwd, fwdErr := httpforwarded.ParseFromRequest(r) - if fwdErr == nil { - proto = oldestForwardedValue(fwd, "proto") - host = oldestForwardedValue(fwd, "host") +func composeDomainContext(r *http.Request, fallBackToHttps bool, instanceDomainHeaders, publicDomainHeaders []string) *http_util.DomainCtx { + instanceHost, instanceProto := hostFromRequest(r, instanceDomainHeaders) + publicHost, publicProto := hostFromRequest(r, publicDomainHeaders) + if publicProto == "" { + publicProto = instanceProto } - if proto == "" { - proto = r.Header.Get("X-Forwarded-Proto") - } - if host == "" { - host = r.Header.Get("X-Forwarded-Host") - } - if proto == "" { - proto = "http" + if publicProto == "" { + publicProto = "http" if fallBackToHttps { - proto = "https" + publicProto = "https" } } - if host == "" { - host = r.Host + if instanceHost == "" { + instanceHost = r.Host } - return fmt.Sprintf("%s://%s", proto, host) + return &http_util.DomainCtx{ + InstanceHost: instanceHost, + Protocol: publicProto, + PublicHost: publicHost, + } +} + +func hostFromRequest(r *http.Request, headers []string) (host, proto string) { + var hostFromHeader, protoFromHeader string + for _, header := range headers { + switch http.CanonicalHeaderKey(header) { + case http.CanonicalHeaderKey(http_util.Forwarded), + http.CanonicalHeaderKey(http_util.ForwardedFor): + hostFromHeader, protoFromHeader = hostFromForwarded(r.Header.Values(header)) + case http.CanonicalHeaderKey(http_util.ForwardedHost): + hostFromHeader = r.Header.Get(header) + case http.CanonicalHeaderKey(http_util.ForwardedProto): + protoFromHeader = r.Header.Get(header) + default: + hostFromHeader = r.Header.Get(header) + } + if host == "" { + host = hostFromHeader + } + if proto == "" { + proto = protoFromHeader + } + } + return host, proto +} + +func hostFromForwarded(values []string) (string, string) { + fwd, fwdErr := httpforwarded.Parse(values) + if fwdErr == nil { + return oldestForwardedValue(fwd, "host"), oldestForwardedValue(fwd, "proto") + } + return "", "" } func oldestForwardedValue(forwarded map[string][]string, key string) string { diff --git a/internal/api/http/middleware/origin_interceptor_test.go b/internal/api/http/middleware/origin_interceptor_test.go index 31b2136b5..989e4d48b 100644 --- a/internal/api/http/middleware/origin_interceptor_test.go +++ b/internal/api/http/middleware/origin_interceptor_test.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + + http_util "github.com/zitadel/zitadel/internal/api/http" ) func Test_composeOrigin(t *testing.T) { @@ -15,10 +17,13 @@ func Test_composeOrigin(t *testing.T) { tests := []struct { name string args args - want string + want *http_util.DomainCtx }{{ name: "no proxy headers", - want: "http://host.header", + want: &http_util.DomainCtx{ + InstanceHost: "host.header", + Protocol: "http", + }, }, { name: "forwarded proto", args: args{ @@ -27,7 +32,10 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: false, }, - want: "https://host.header", + want: &http_util.DomainCtx{ + InstanceHost: "host.header", + Protocol: "https", + }, }, { name: "forwarded host", args: args{ @@ -36,7 +44,10 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: false, }, - want: "http://forwarded.host", + want: &http_util.DomainCtx{ + InstanceHost: "forwarded.host", + Protocol: "http", + }, }, { name: "forwarded proto and host", args: args{ @@ -45,7 +56,10 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: false, }, - want: "https://forwarded.host", + want: &http_util.DomainCtx{ + InstanceHost: "forwarded.host", + Protocol: "https", + }, }, { name: "forwarded proto and host with multiple complete entries", args: args{ @@ -54,7 +68,10 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: false, }, - want: "https://forwarded.host", + want: &http_util.DomainCtx{ + InstanceHost: "forwarded.host", + Protocol: "https", + }, }, { name: "forwarded proto and host with multiple incomplete entries", args: args{ @@ -63,7 +80,10 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: false, }, - want: "https://forwarded.host", + want: &http_util.DomainCtx{ + InstanceHost: "forwarded.host", + Protocol: "https", + }, }, { name: "forwarded proto and host with incomplete entries in different values", args: args{ @@ -72,7 +92,10 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: true, }, - want: "http://forwarded.host", + want: &http_util.DomainCtx{ + InstanceHost: "forwarded.host", + Protocol: "http", + }, }, { name: "x-forwarded-proto https", args: args{ @@ -81,7 +104,10 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: false, }, - want: "https://host.header", + want: &http_util.DomainCtx{ + InstanceHost: "host.header", + Protocol: "https", + }, }, { name: "x-forwarded-proto http", args: args{ @@ -90,19 +116,28 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: true, }, - want: "http://host.header", + want: &http_util.DomainCtx{ + InstanceHost: "host.header", + Protocol: "http", + }, }, { name: "fallback to http", args: args{ fallBackToHttps: false, }, - want: "http://host.header", + want: &http_util.DomainCtx{ + InstanceHost: "host.header", + Protocol: "http", + }, }, { name: "fallback to https", args: args{ fallBackToHttps: true, }, - want: "https://host.header", + want: &http_util.DomainCtx{ + InstanceHost: "host.header", + Protocol: "https", + }, }, { name: "x-forwarded-host", args: args{ @@ -111,7 +146,10 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: false, }, - want: "http://x-forwarded.host", + want: &http_util.DomainCtx{ + InstanceHost: "x-forwarded.host", + Protocol: "http", + }, }, { name: "x-forwarded-proto and x-forwarded-host", args: args{ @@ -121,7 +159,10 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: false, }, - want: "https://x-forwarded.host", + want: &http_util.DomainCtx{ + InstanceHost: "x-forwarded.host", + Protocol: "https", + }, }, { name: "forwarded host and x-forwarded-host", args: args{ @@ -131,7 +172,10 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: false, }, - want: "http://forwarded.host", + want: &http_util.DomainCtx{ + InstanceHost: "forwarded.host", + Protocol: "http", + }, }, { name: "forwarded host and x-forwarded-proto", args: args{ @@ -141,17 +185,22 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: false, }, - want: "https://forwarded.host", + want: &http_util.DomainCtx{ + InstanceHost: "forwarded.host", + Protocol: "https", + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, composeOrigin( + assert.Equalf(t, tt.want, composeDomainContext( &http.Request{ Host: "host.header", Header: tt.args.h, }, tt.args.fallBackToHttps, + []string{http_util.Forwarded, http_util.ForwardedFor, http_util.ForwardedHost, http_util.ForwardedProto}, + []string{"x-zitadel-public-host"}, ), "headers: %+v, fallBackToHttps: %t", tt.args.h, tt.args.fallBackToHttps) }) } diff --git a/internal/api/http/request_context.go b/internal/api/http/request_context.go new file mode 100644 index 000000000..9ed345ed8 --- /dev/null +++ b/internal/api/http/request_context.go @@ -0,0 +1,60 @@ +package http + +import ( + "context" + "fmt" + "strings" +) + +type DomainCtx struct { + InstanceHost string + PublicHost string + Protocol string +} + +// RequestedHost returns the host (hostname[:port]) for which the request was handled. +// The instance host is returned if not public host was set. +func (r *DomainCtx) RequestedHost() string { + if r.PublicHost != "" { + return r.PublicHost + } + return r.InstanceHost +} + +// RequestedDomain returns the domain (hostname) for which the request was handled. +// The instance domain is returned if not public host / domain was set. +func (r *DomainCtx) RequestedDomain() string { + return strings.Split(r.RequestedHost(), ":")[0] +} + +// Origin returns the origin (protocol://hostname[:port]) for which the request was handled. +// The instance host is used if not public host was set. +func (r *DomainCtx) Origin() string { + host := r.PublicHost + if host == "" { + host = r.InstanceHost + } + return fmt.Sprintf("%s://%s", r.Protocol, host) +} + +func DomainContext(ctx context.Context) *DomainCtx { + o, ok := ctx.Value(domainCtx).(*DomainCtx) + if !ok { + return &DomainCtx{} + } + return o +} + +func WithDomainContext(ctx context.Context, domainContext *DomainCtx) context.Context { + return context.WithValue(ctx, domainCtx, domainContext) +} + +func WithRequestedHost(ctx context.Context, host string) context.Context { + i, ok := ctx.Value(domainCtx).(*DomainCtx) + if !ok { + i = new(DomainCtx) + } + + i.PublicHost = host + return context.WithValue(ctx, domainCtx, i) +} diff --git a/internal/api/idp/idp.go b/internal/api/idp/idp.go index 0209e0337..b058ba5c2 100644 --- a/internal/api/idp/idp.go +++ b/internal/api/idp/idp.go @@ -13,7 +13,6 @@ import ( "github.com/gorilla/mux" "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/ui/login" "github.com/zitadel/zitadel/internal/command" @@ -79,21 +78,21 @@ type externalSAMLIDPCallbackData struct { } // CallbackURL generates the instance specific URL to the IDP callback handler -func CallbackURL(externalSecure bool) func(ctx context.Context) string { +func CallbackURL() func(ctx context.Context) string { return func(ctx context.Context) string { - return http_utils.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), externalSecure) + HandlerPrefix + callbackPath + return http_utils.DomainContext(ctx).Origin() + HandlerPrefix + callbackPath } } -func SAMLRootURL(externalSecure bool) func(ctx context.Context, idpID string) string { +func SAMLRootURL() func(ctx context.Context, idpID string) string { return func(ctx context.Context, idpID string) string { - return http_utils.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), externalSecure) + HandlerPrefix + "/" + idpID + "/" + return http_utils.DomainContext(ctx).Origin() + HandlerPrefix + "/" + idpID + "/" } } -func LoginSAMLRootURL(externalSecure bool) func(ctx context.Context) string { +func LoginSAMLRootURL() func(ctx context.Context) string { return func(ctx context.Context) string { - return http_utils.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), externalSecure) + login.HandlerPrefix + login.EndpointSAMLACS + return http_utils.DomainContext(ctx).Origin() + login.HandlerPrefix + login.EndpointSAMLACS } } @@ -101,7 +100,6 @@ func NewHandler( commands *command.Commands, queries *query.Queries, encryptionAlgorithm crypto.EncryptionAlgorithm, - externalSecure bool, instanceInterceptor func(next http.Handler) http.Handler, ) http.Handler { h := &Handler{ @@ -109,9 +107,9 @@ func NewHandler( queries: queries, parser: form.NewParser(), encryptionAlgorithm: encryptionAlgorithm, - callbackURL: CallbackURL(externalSecure), - samlRootURL: SAMLRootURL(externalSecure), - loginSAMLRootURL: LoginSAMLRootURL(externalSecure), + callbackURL: CallbackURL(), + samlRootURL: SAMLRootURL(), + loginSAMLRootURL: LoginSAMLRootURL(), } router := mux.NewRouter() diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index 24e201e62..67bc1765a 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -100,7 +100,7 @@ func NewServer( if err != nil { return nil, zerrors.ThrowInternal(err, "OIDC-EGrqd", "cannot create op config: %w") } - storage := newStorage(config, command, query, repo, encryptionAlg, es, projections, externalSecure) + storage := newStorage(config, command, query, repo, encryptionAlg, es, projections) keyCache := newPublicKeyCache(ctx, config.PublicKeyCacheMaxAge, query.GetPublicKeyByID) accessTokenKeySet := newOidcKeySet(keyCache, withKeyExpiryCheck(true)) idTokenHintKeySet := newOidcKeySet(keyCache) @@ -115,7 +115,7 @@ func NewServer( provider, err := op.NewProvider( opConfig, storage, - op.IssuerFromForwardedOrHost("", op.WithIssuerFromCustomHeaders("forwarded", "x-zitadel-forwarded")), + IssuerFromContext, options..., ) if err != nil { @@ -142,7 +142,7 @@ func NewServer( signingKeyAlgorithm: config.SigningKeyAlgorithm, encAlg: encryptionAlg, opCrypto: op.NewAESCrypto(opConfig.CryptoKey), - assetAPIPrefix: assets.AssetAPI(externalSecure), + assetAPIPrefix: assets.AssetAPI(), } metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount} server.Handler = op.RegisterLegacyServer(server, @@ -162,6 +162,12 @@ func NewServer( return server, nil } +func IssuerFromContext(_ bool) (op.IssuerFromRequest, error) { + return func(r *http.Request) string { + return http_utils.DomainContext(r.Context()).Origin() + }, nil +} + func publicAuthPathPrefixes(endpoints *EndpointConfig) []string { authURL := op.DefaultEndpoints.Authorization.Relative() keysURL := op.DefaultEndpoints.JwksURI.Relative() @@ -194,7 +200,7 @@ func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey [] return opConfig, nil } -func newStorage(config Config, command *command.Commands, query *query.Queries, repo repository.Repository, encAlg crypto.EncryptionAlgorithm, es *eventstore.Eventstore, db *database.DB, externalSecure bool) *OPStorage { +func newStorage(config Config, command *command.Commands, query *query.Queries, repo repository.Repository, encAlg crypto.EncryptionAlgorithm, es *eventstore.Eventstore, db *database.DB) *OPStorage { return &OPStorage{ repo: repo, command: command, @@ -210,7 +216,7 @@ func newStorage(config Config, command *command.Commands, query *query.Queries, defaultRefreshTokenExpiration: config.DefaultRefreshTokenExpiration, encAlg: encAlg, locker: crdb.NewLocker(db.DB, locksTable, signingKey), - assetAPIPrefix: assets.AssetAPI(externalSecure), + assetAPIPrefix: assets.AssetAPI(), } } diff --git a/internal/api/ui/console/console.go b/internal/api/ui/console/console.go index 9e28dae70..515f26db9 100644 --- a/internal/api/ui/console/console.go +++ b/internal/api/ui/console/console.go @@ -21,6 +21,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/http/middleware" + console_path "github.com/zitadel/zitadel/internal/api/ui/console/path" ) type Config struct { @@ -40,7 +41,6 @@ var ( const ( envRequestPath = "/assets/environment.json" - HandlerPrefix = "/ui/console" ) var ( @@ -56,7 +56,7 @@ var ( ) func LoginHintLink(origin, username string) string { - return origin + HandlerPrefix + "?login_hint=" + username + return origin + console_path.HandlerPrefix + "?login_hint=" + username } func (i *spaHandler) Open(name string) (http.File, error) { diff --git a/internal/api/ui/console/path/paths.go b/internal/api/ui/console/path/paths.go new file mode 100644 index 000000000..8890c516c --- /dev/null +++ b/internal/api/ui/console/path/paths.go @@ -0,0 +1,7 @@ +package path + +const ( + HandlerPrefix = "/ui/console" + RedirectPath = HandlerPrefix + "/auth/callback" + PostLogoutPath = HandlerPrefix + "/signedout" +) diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index ab0d98b99..b8c6a70ab 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -1042,7 +1042,7 @@ func (l *Login) samlProvider(ctx context.Context, identityProvider *query.IDPTem opts = append(opts, saml.WithTransientMappingAttributeName(identityProvider.SAMLIDPTemplate.TransientMappingAttributeName)) } opts = append(opts, - saml.WithEntityID(http_utils.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), l.externalSecure)+"/idps/"+identityProvider.ID+"/saml/metadata"), + saml.WithEntityID(http_utils.DomainContext(ctx).Origin()+"/idps/"+identityProvider.ID+"/saml/metadata"), saml.WithCustomRequestTracker( requesttracker.New( func(ctx context.Context, authRequestID, samlRequestID string) error { diff --git a/internal/api/ui/login/login.go b/internal/api/ui/login/login.go index 5c431f1f2..bda1ecac5 100644 --- a/internal/api/ui/login/login.go +++ b/internal/api/ui/login/login.go @@ -129,7 +129,7 @@ func createCSRFInterceptor(cookieName string, csrfCookieKey []byte, externalSecu sameSiteMode = csrf.SameSiteNoneMode // ... and since SameSite none requires the secure flag, we'll set it for TLS and for localhost // (regardless of the TLS / externalSecure settings) - secureOnly = externalSecure || instance.RequestedDomain() == "localhost" + secureOnly = externalSecure || http_utils.DomainContext(r.Context()).RequestedDomain() == "localhost" } csrf.Protect(csrfCookieKey, csrf.Secure(secureOnly), @@ -163,7 +163,7 @@ func (l *Login) Handler() http.Handler { } func (l *Login) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgName string) ([]string, error) { - orgDomain, err := domain.NewIAMDomainName(orgName, authz.GetInstance(ctx).RequestedDomain()) + orgDomain, err := domain.NewIAMDomainName(orgName, http_utils.DomainContext(ctx).RequestedDomain()) if err != nil { return nil, err } @@ -199,5 +199,5 @@ func setUserContext(ctx context.Context, userID, resourceOwner string) context.C } func (l *Login) baseURL(ctx context.Context) string { - return http_utils.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), l.externalSecure) + HandlerPrefix + return http_utils.DomainContext(ctx).Origin() + HandlerPrefix } diff --git a/internal/api/ui/login/register_org_handler.go b/internal/api/ui/login/register_org_handler.go index 0243a3756..acb032d8f 100644 --- a/internal/api/ui/login/register_org_handler.go +++ b/internal/api/ui/login/register_org_handler.go @@ -3,7 +3,7 @@ package login import ( "net/http" - "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" @@ -128,7 +128,7 @@ func (l *Login) renderRegisterOrg(w http.ResponseWriter, r *http.Request, authRe orgPolicy, _ := l.getDefaultDomainPolicy(r) if orgPolicy != nil { data.UserLoginMustBeDomain = orgPolicy.UserLoginMustBeDomain - data.IamDomain = authz.GetInstance(r.Context()).RequestedDomain() + data.IamDomain = http_util.DomainContext(r.Context()).RequestedDomain() } if authRequest == nil { diff --git a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go index 4d8823913..c9286d61d 100644 --- a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go +++ b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go @@ -297,8 +297,7 @@ func (repo *TokenVerifierRepo) getTokenIDAndSubject(ctx context.Context, accessT func (repo *TokenVerifierRepo) jwtTokenVerifier(ctx context.Context) *op.AccessTokenVerifier { keySet := &openIDKeySet{repo.Query} - issuer := http_util.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), repo.ExternalSecure) - return op.NewAccessTokenVerifier(issuer, keySet) + return op.NewAccessTokenVerifier(http_util.DomainContext(ctx).Origin(), keySet) } func (repo *TokenVerifierRepo) decryptAccessToken(token string) (string, error) { diff --git a/internal/command/instance.go b/internal/command/instance.go index b5fb70045..4d3e1d252 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -7,7 +7,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/ui/console" + "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -26,13 +26,11 @@ import ( ) const ( - zitadelProjectName = "ZITADEL" - mgmtAppName = "Management-API" - adminAppName = "Admin-API" - authAppName = "Auth-API" - consoleAppName = "Console" - consoleRedirectPath = console.HandlerPrefix + "/auth/callback" - consolePostLogoutPath = console.HandlerPrefix + "/signedout" + zitadelProjectName = "ZITADEL" + mgmtAppName = "Management-API" + adminAppName = "Admin-API" + authAppName = "Auth-API" + consoleAppName = "Console" ) type InstanceSetup struct { @@ -233,7 +231,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str func contextWithInstanceSetupInfo(ctx context.Context, instanceID, projectID, consoleAppID, externalDomain string) context.Context { return authz.WithConsole( authz.SetCtxData( - authz.WithRequestedDomain( + http.WithRequestedHost( authz.WithInstanceID( ctx, instanceID), diff --git a/internal/command/instance_domain.go b/internal/command/instance_domain.go index 43e1aebbf..8eacee359 100644 --- a/internal/command/instance_domain.go +++ b/internal/command/instance_domain.go @@ -7,6 +7,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/api/ui/console/path" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -74,7 +75,7 @@ func (c *Commands) RemoveInstanceDomain(ctx context.Context, instanceDomain stri } func (c *Commands) addGeneratedInstanceDomain(ctx context.Context, a *instance.Aggregate, instanceName string) ([]preparation.Validation, error) { - domain, err := c.GenerateDomain(instanceName, authz.GetInstance(ctx).RequestedDomain()) + domain, err := c.GenerateDomain(instanceName, http.DomainContext(ctx).RequestedDomain()) if err != nil { return nil, err } @@ -143,12 +144,12 @@ func (c *Commands) updateConsoleRedirectURIs(ctx context.Context, filter prepara if !appWriteModel.State.Exists() { return nil, nil } - redirectURI := http.BuildHTTP(instanceDomain, c.externalPort, c.externalSecure) + consoleRedirectPath + redirectURI := http.BuildHTTP(instanceDomain, c.externalPort, c.externalSecure) + path.RedirectPath changes := make([]project.OIDCConfigChanges, 0, 2) if !containsURI(appWriteModel.RedirectUris, redirectURI) { changes = append(changes, project.ChangeRedirectURIs(append(appWriteModel.RedirectUris, redirectURI))) } - postLogoutRedirectURI := http.BuildHTTP(instanceDomain, c.externalPort, c.externalSecure) + consolePostLogoutPath + postLogoutRedirectURI := http.BuildHTTP(instanceDomain, c.externalPort, c.externalSecure) + path.PostLogoutPath if !containsURI(appWriteModel.PostLogoutRedirectUris, postLogoutRedirectURI) { changes = append(changes, project.ChangePostLogoutRedirectURIs(append(appWriteModel.PostLogoutRedirectUris, postLogoutRedirectURI))) } diff --git a/internal/command/instance_trusted_domain.go b/internal/command/instance_trusted_domain.go new file mode 100644 index 000000000..f404e6665 --- /dev/null +++ b/internal/command/instance_trusted_domain.go @@ -0,0 +1,50 @@ +package command + +import ( + "context" + "slices" + "strings" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func (c *Commands) AddTrustedDomain(ctx context.Context, trustedDomain string) (*domain.ObjectDetails, error) { + trustedDomain = strings.TrimSpace(trustedDomain) + if trustedDomain == "" || len(trustedDomain) > 253 { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-Stk21", "Errors.Invalid.Argument") + } + if !allowDomainRunes.MatchString(trustedDomain) { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-S3v3w", "Errors.Instance.Domain.InvalidCharacter") + } + model := NewInstanceTrustedDomainsWriteModel(ctx) + err := c.eventstore.FilterToQueryReducer(ctx, model) + if err != nil { + return nil, err + } + if slices.Contains(model.Domains, trustedDomain) { + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-hg42a", "Errors.Instance.Domain.AlreadyExists") + } + err = c.pushAppendAndReduce(ctx, model, instance.NewTrustedDomainAddedEvent(ctx, InstanceAggregateFromWriteModel(&model.WriteModel), trustedDomain)) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&model.WriteModel), nil +} + +func (c *Commands) RemoveTrustedDomain(ctx context.Context, trustedDomain string) (*domain.ObjectDetails, error) { + model := NewInstanceTrustedDomainsWriteModel(ctx) + err := c.eventstore.FilterToQueryReducer(ctx, model) + if err != nil { + return nil, err + } + if !slices.Contains(model.Domains, trustedDomain) { + return nil, zerrors.ThrowNotFound(nil, "COMMA-de3z9", "Errors.Instance.Domain.NotFound") + } + err = c.pushAppendAndReduce(ctx, model, instance.NewTrustedDomainRemovedEvent(ctx, InstanceAggregateFromWriteModel(&model.WriteModel), trustedDomain)) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&model.WriteModel), nil +} diff --git a/internal/command/instance_trusted_domain_test.go b/internal/command/instance_trusted_domain_test.go new file mode 100644 index 000000000..f4f0001c7 --- /dev/null +++ b/internal/command/instance_trusted_domain_test.go @@ -0,0 +1,197 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestCommands_AddTrustedDomain(t *testing.T) { + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + } + type args struct { + ctx context.Context + trustedDomain string + } + type want struct { + details *domain.ObjectDetails + err error + } + tests := []struct { + name string + fields fields + args args + want want + }{ + { + name: "empty domain, error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: "", + }, + want: want{ + err: zerrors.ThrowInvalidArgument(nil, "COMMA-Stk21", "Errors.Invalid.Argument"), + }, + }, + { + name: "invalid domain (length), error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: "my-very-endleeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeess-looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo0ooooooooooooooooooooooong.domain.com", + }, + want: want{ + err: zerrors.ThrowInvalidArgument(nil, "COMMA-Stk21", "Errors.Invalid.Argument"), + }, + }, + { + name: "invalid domain (chars), error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: "&.com", + }, + want: want{ + err: zerrors.ThrowInvalidArgument(nil, "COMMA-S3v3w", "Errors.Instance.Domain.InvalidCharacter"), + }, + }, + { + name: "domain already exists, error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewTrustedDomainAddedEvent(context.Background(), + &instance.NewAggregate("instanceID").Aggregate, "domain.com"), + ), + ), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: "domain.com", + }, + want: want{ + err: zerrors.ThrowPreconditionFailed(nil, "COMMA-hg42a", "Errors.Instance.Domain.AlreadyExists"), + }, + }, + { + name: "domain add ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectPush( + instance.NewTrustedDomainAddedEvent(context.Background(), + &instance.NewAggregate("instanceID").Aggregate, "domain.com"), + ), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: "domain.com", + }, + want: want{ + details: &domain.ObjectDetails{ + ResourceOwner: "instanceID", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + } + got, err := c.AddTrustedDomain(tt.args.ctx, tt.args.trustedDomain) + assert.ErrorIs(t, err, tt.want.err) + assert.Equal(t, tt.want.details, got) + }) + } +} + +func TestCommands_RemoveTrustedDomain(t *testing.T) { + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + } + type args struct { + ctx context.Context + trustedDomain string + } + type want struct { + details *domain.ObjectDetails + err error + } + tests := []struct { + name string + fields fields + args args + want want + }{ + { + name: "domain does not exists, error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: "domain.com", + }, + want: want{ + err: zerrors.ThrowNotFound(nil, "COMMA-de3z9", "Errors.Instance.Domain.NotFound"), + }, + }, + { + name: "domain remove ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewTrustedDomainAddedEvent(context.Background(), + &instance.NewAggregate("instanceID").Aggregate, "domain.com"), + ), + ), + expectPush( + instance.NewTrustedDomainRemovedEvent(context.Background(), + &instance.NewAggregate("instanceID").Aggregate, "domain.com"), + ), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: "domain.com", + }, + want: want{ + details: &domain.ObjectDetails{ + ResourceOwner: "instanceID", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + } + got, err := c.RemoveTrustedDomain(tt.args.ctx, tt.args.trustedDomain) + assert.ErrorIs(t, err, tt.want.err) + assert.Equal(t, tt.want.details, got) + }) + } +} diff --git a/internal/command/instance_trusted_domains_model.go b/internal/command/instance_trusted_domains_model.go new file mode 100644 index 000000000..ddfc0a110 --- /dev/null +++ b/internal/command/instance_trusted_domains_model.go @@ -0,0 +1,54 @@ +package command + +import ( + "context" + "slices" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/instance" +) + +type InstanceTrustedDomainsWriteModel struct { + eventstore.WriteModel + + Domains []string +} + +func NewInstanceTrustedDomainsWriteModel(ctx context.Context) *InstanceTrustedDomainsWriteModel { + instanceID := authz.GetInstance(ctx).InstanceID() + return &InstanceTrustedDomainsWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: instanceID, + ResourceOwner: instanceID, + InstanceID: instanceID, + }, + } +} + +func (wm *InstanceTrustedDomainsWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *instance.TrustedDomainAddedEvent: + wm.Domains = append(wm.Domains, e.Domain) + case *instance.TrustedDomainRemovedEvent: + wm.Domains = slices.DeleteFunc(wm.Domains, func(domain string) bool { + return domain == e.Domain + }) + } + } + return wm.WriteModel.Reduce() +} + +func (wm *InstanceTrustedDomainsWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(instance.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + instance.TrustedDomainAddedEventType, + instance.TrustedDomainRemovedEventType, + ). + Builder() +} diff --git a/internal/command/main_test.go b/internal/command/main_test.go index a5991dd16..e75392309 100644 --- a/internal/command/main_test.go +++ b/internal/command/main_test.go @@ -208,14 +208,6 @@ func (m *mockInstance) DefaultOrganisationID() string { return "defaultOrgID" } -func (m *mockInstance) RequestedDomain() string { - return "zitadel.cloud" -} - -func (m *mockInstance) RequestedHost() string { - return "zitadel.cloud:443" -} - func (m *mockInstance) SecurityPolicyAllowedOrigins() []string { return nil } diff --git a/internal/command/org.go b/internal/command/org.go index e603531b3..261a571cb 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -239,7 +240,7 @@ func AddOrgCommand(ctx context.Context, a *org.Aggregate, name string) preparati if name = strings.TrimSpace(name); name == "" { return nil, zerrors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument") } - defaultDomain, err := domain.NewIAMDomainName(name, authz.GetInstance(ctx).RequestedDomain()) + defaultDomain, err := domain.NewIAMDomainName(name, http_util.DomainContext(ctx).RequestedDomain()) if err != nil { return nil, err } @@ -708,7 +709,7 @@ func (c *Commands) addOrgWithID(ctx context.Context, organisation *domain.Org, o } organisation.AggregateID = orgID - organisation.AddIAMDomain(authz.GetInstance(ctx).RequestedDomain()) + organisation.AddIAMDomain(http_util.DomainContext(ctx).RequestedDomain()) addedOrg := NewOrgWriteModel(organisation.AggregateID) orgAgg := OrgAggregateFromWriteModel(&addedOrg.WriteModel) diff --git a/internal/command/org_domain.go b/internal/command/org_domain.go index 20091aeb8..929e0ab75 100644 --- a/internal/command/org_domain.go +++ b/internal/command/org_domain.go @@ -323,7 +323,7 @@ func (c *Commands) changeDefaultDomain(ctx context.Context, orgID, newName strin if err != nil { return nil, err } - iamDomain := authz.GetInstance(ctx).RequestedDomain() + iamDomain := http_utils.DomainContext(ctx).RequestedDomain() defaultDomain, _ := domain.NewIAMDomainName(orgDomains.OrgName, iamDomain) isPrimary := defaultDomain == orgDomains.PrimaryDomain orgAgg := OrgAggregateFromWriteModel(&orgDomains.WriteModel) @@ -356,7 +356,7 @@ func (c *Commands) removeCustomDomains(ctx context.Context, orgID string) ([]eve return nil, err } hasDefault := false - defaultDomain, _ := domain.NewIAMDomainName(orgDomains.OrgName, authz.GetInstance(ctx).RequestedDomain()) + defaultDomain, _ := domain.NewIAMDomainName(orgDomains.OrgName, http_utils.DomainContext(ctx).RequestedDomain()) isPrimary := defaultDomain == orgDomains.PrimaryDomain orgAgg := OrgAggregateFromWriteModel(&orgDomains.WriteModel) events := make([]eventstore.Command, 0, len(orgDomains.Domains)) diff --git a/internal/command/org_domain_test.go b/internal/command/org_domain_test.go index 8af38098c..80ec08284 100644 --- a/internal/command/org_domain_test.go +++ b/internal/command/org_domain_test.go @@ -8,7 +8,6 @@ import ( "go.uber.org/mock/gomock" "golang.org/x/text/language" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" @@ -105,7 +104,7 @@ func TestAddDomain(t *testing.T) { Commands: []eventstore.Command{ org.NewDomainAddedEvent(context.Background(), &agg.Aggregate, "domain"), org.NewDomainVerifiedEvent(context.Background(), &agg.Aggregate, "domain"), - user.NewDomainClaimedEvent(context.Background(), &user.NewAggregate("userID1", "org2").Aggregate, "newID@temporary.domain", "username", false), + user.NewDomainClaimedEvent(http.WithRequestedHost(context.Background(), "domain"), &user.NewAggregate("userID1", "org2").Aggregate, "newID@temporary.domain", "username", false), }, }, }, @@ -132,7 +131,7 @@ func TestAddDomain(t *testing.T) { t.Run(tt.name, func(t *testing.T) { AssertValidation( t, - authz.WithRequestedDomain(context.Background(), "domain"), + http.WithRequestedHost(context.Background(), "domain"), (&Commands{idGenerator: tt.args.idGenerator}).prepareAddOrgDomain(tt.args.a, tt.args.domain, tt.args.claimedUserIDs), tt.args.filter, tt.want, @@ -673,7 +672,7 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) { func TestCommandSide_ValidateOrgDomain(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore idGenerator id.Generator secretGenerator crypto.Generator alg crypto.EncryptionAlgorithm @@ -697,9 +696,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { { name: "invalid domain, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -716,9 +713,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { { name: "missing aggregateid, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -733,8 +728,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { { name: "domain not exists, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -762,8 +756,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { { name: "domain already verified, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -803,8 +796,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { { name: "no code existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -838,8 +830,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { { name: "invalid domain verification, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -894,8 +885,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { { name: "domain verification, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -952,8 +942,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { { name: "domain verification, claimed users not found, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -1012,8 +1001,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { { name: "domain verification, claimed users, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -1067,7 +1055,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { &org.NewAggregate("org1").Aggregate, "domain.ch", ), - user.NewDomainClaimedEvent(context.Background(), + user.NewDomainClaimedEvent(http.WithRequestedHost(context.Background(), "zitadel.ch"), &user.NewAggregate("user1", "org2").Aggregate, "tempid@temporary.zitadel.ch", "username@domain.ch", @@ -1100,13 +1088,13 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), domainVerificationGenerator: tt.fields.secretGenerator, domainVerificationAlg: tt.fields.alg, domainVerificationValidator: tt.fields.domainValidationFunc, idGenerator: tt.fields.idGenerator, } - got, err := r.ValidateOrgDomain(authz.WithRequestedDomain(tt.args.ctx, "zitadel.ch"), tt.args.domain, tt.args.claimedUserIDs) + got, err := r.ValidateOrgDomain(http.WithRequestedHost(tt.args.ctx, "zitadel.ch"), tt.args.domain, tt.args.claimedUserIDs) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/org_test.go b/internal/command/org_test.go index 5c69034b5..cc6a384f2 100644 --- a/internal/command/org_test.go +++ b/internal/command/org_test.go @@ -11,6 +11,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -65,7 +66,7 @@ func TestAddOrg(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - AssertValidation(t, context.Background(), AddOrgCommand(authz.WithRequestedDomain(context.Background(), "localhost"), tt.args.a, tt.args.name), nil, tt.want) + AssertValidation(t, context.Background(), AddOrgCommand(http_util.WithRequestedHost(context.Background(), "localhost"), tt.args.a, tt.args.name), nil, tt.want) }) } } @@ -229,7 +230,7 @@ func TestCommandSide_AddOrg(t *testing.T) { }, }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"), + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), name: "Org", userID: "user1", resourceOwner: "org1", @@ -297,7 +298,7 @@ func TestCommandSide_AddOrg(t *testing.T) { }, }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"), + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), name: "Org", userID: "user1", resourceOwner: "org1", @@ -360,7 +361,7 @@ func TestCommandSide_AddOrg(t *testing.T) { }, }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"), + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), name: "Org", userID: "user1", resourceOwner: "org1", @@ -431,7 +432,7 @@ func TestCommandSide_AddOrg(t *testing.T) { }, }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"), + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), name: " Org ", userID: "user1", resourceOwner: "org1", @@ -551,7 +552,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { ), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "zitadel.ch"), + ctx: http_util.WithRequestedHost(context.Background(), "zitadel.ch"), orgID: "org1", name: " org ", }, @@ -581,7 +582,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { ), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "zitadel.ch"), + ctx: http_util.WithRequestedHost(context.Background(), "zitadel.ch"), orgID: "org1", name: "neworg", }, @@ -635,7 +636,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { ), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "zitadel.ch"), + ctx: http_util.WithRequestedHost(context.Background(), "zitadel.ch"), orgID: "org1", name: "neworg", }, @@ -695,7 +696,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { ), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "zitadel.ch"), + ctx: http_util.WithRequestedHost(context.Background(), "zitadel.ch"), orgID: "org1", name: "neworg", }, @@ -1286,7 +1287,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID"), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"), + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), setupOrg: &OrgSetup{ Name: "", }, @@ -1304,7 +1305,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID"), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"), + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), setupOrg: &OrgSetup{ Name: "Org", Admins: []*OrgSetupAdmin{ @@ -1325,7 +1326,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID", "userID"), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"), + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), setupOrg: &OrgSetup{ Name: "Org", Admins: []*OrgSetupAdmin{ @@ -1418,7 +1419,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { ), eventFromEventPusher( user.NewHumanInitialCodeAddedEvent( - context.Background(), + http_util.WithRequestedHost(context.Background(), "iam-domain"), &user.NewAggregate("userID", "orgID").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -1441,7 +1442,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { newCode: mockEncryptedCode("userinit", time.Hour), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"), + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), setupOrg: &OrgSetup{ Name: "Org", Admins: []*OrgSetupAdmin{ @@ -1521,7 +1522,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID"), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"), + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), setupOrg: &OrgSetup{ Name: "Org", Admins: []*OrgSetupAdmin{ @@ -1621,7 +1622,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"), + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), setupOrg: &OrgSetup{ Name: "Org", Admins: []*OrgSetupAdmin{ diff --git a/internal/command/user.go b/internal/command/user.go index 6d8d10bca..bb3c75775 100644 --- a/internal/command/user.go +++ b/internal/command/user.go @@ -7,7 +7,7 @@ import ( "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -277,7 +277,7 @@ func (c *Commands) userDomainClaimed(ctx context.Context, userID string) (events user.NewDomainClaimedEvent( ctx, userAgg, - fmt.Sprintf("%s@temporary.%s", id, authz.GetInstance(ctx).RequestedDomain()), + fmt.Sprintf("%s@temporary.%s", id, http_util.DomainContext(ctx).RequestedDomain()), existingUser.UserName, domainPolicy.UserLoginMustBeDomain), }, changedUserGrant, nil @@ -305,7 +305,7 @@ func (c *Commands) prepareUserDomainClaimed(ctx context.Context, filter preparat return user.NewDomainClaimedEvent( ctx, userAgg, - fmt.Sprintf("%s@temporary.%s", id, authz.GetInstance(ctx).RequestedDomain()), + fmt.Sprintf("%s@temporary.%s", id, http_util.DomainContext(ctx).RequestedDomain()), userWriteModel.UserName, domainPolicy.UserLoginMustBeDomain), nil } diff --git a/internal/command/user_human_otp.go b/internal/command/user_human_otp.go index 8ecd51751..7f587cf9e 100644 --- a/internal/command/user_human_otp.go +++ b/internal/command/user_human_otp.go @@ -8,6 +8,7 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -107,7 +108,7 @@ func (c *Commands) createHumanTOTP(ctx context.Context, userID, resourceOwner st } issuer := c.multifactors.OTP.Issuer if issuer == "" { - issuer = authz.GetInstance(ctx).RequestedDomain() + issuer = http_util.DomainContext(ctx).RequestedDomain() } key, err := domain.NewTOTPKey(issuer, accountName) if err != nil { diff --git a/internal/command/user_human_otp_test.go b/internal/command/user_human_otp_test.go index 806d24dd4..94a47f6da 100644 --- a/internal/command/user_human_otp_test.go +++ b/internal/command/user_human_otp_test.go @@ -14,6 +14,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -534,7 +535,7 @@ func TestCommands_createHumanTOTP(t *testing.T) { ), }, args: args{ - ctx: authz.WithRequestedDomain(authz.NewMockContext("instanceID", "org1", "user1"), "zitadel.com"), + ctx: http_util.WithRequestedHost(authz.NewMockContext("instanceID", "org1", "user1"), "zitadel.com"), resourceOwner: "org1", userID: "user1", }, @@ -583,7 +584,7 @@ func TestCommands_createHumanTOTP(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: authz.WithRequestedDomain(authz.NewMockContext("instanceID", "org1", "user1"), "zitadel.com"), + ctx: http_util.WithRequestedHost(authz.NewMockContext("instanceID", "org1", "user1"), "zitadel.com"), resourceOwner: "org1", userID: "user2", }, diff --git a/internal/command/user_v2_passkey_test.go b/internal/command/user_v2_passkey_test.go index 7b1343b86..5a12cb9dc 100644 --- a/internal/command/user_v2_passkey_test.go +++ b/internal/command/user_v2_passkey_test.go @@ -12,6 +12,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -25,7 +26,7 @@ import ( func TestCommands_RegisterUserPasskey(t *testing.T) { ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil) - ctx = authz.WithRequestedDomain(ctx, "example.com") + ctx = http_util.WithRequestedHost(ctx, "example.com") webauthnConfig := &webauthn_helper.Config{ DisplayName: "test", @@ -129,7 +130,7 @@ func TestCommands_RegisterUserPasskey(t *testing.T) { } func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) { - ctx := authz.WithRequestedDomain(context.Background(), "example.com") + ctx := http_util.WithRequestedHost(context.Background(), "example.com") webauthnConfig := &webauthn_helper.Config{ DisplayName: "test", ExternalSecure: true, @@ -231,7 +232,7 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) { } func TestCommands_verifyUserPasskeyCode(t *testing.T) { - ctx := authz.WithRequestedDomain(context.Background(), "example.com") + ctx := http_util.WithRequestedHost(context.Background(), "example.com") alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t)) es := eventstoreExpect(t, expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))), @@ -339,7 +340,7 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) { } func TestCommands_pushUserPasskey(t *testing.T) { - ctx := authz.WithRequestedDomain(authz.NewMockContext("instance1", "org1", "user1"), "example.com") + ctx := http_util.WithRequestedHost(authz.NewMockContext("instance1", "org1", "user1"), "example.com") webauthnConfig := &webauthn_helper.Config{ DisplayName: "test", ExternalSecure: true, diff --git a/internal/command/user_v2_u2f_test.go b/internal/command/user_v2_u2f_test.go index b2a5788d0..4d23ac80b 100644 --- a/internal/command/user_v2_u2f_test.go +++ b/internal/command/user_v2_u2f_test.go @@ -9,6 +9,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/id" @@ -21,7 +22,7 @@ import ( func TestCommands_RegisterUserU2F(t *testing.T) { ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil) - ctx = authz.WithRequestedDomain(ctx, "example.com") + ctx = http_util.WithRequestedHost(ctx, "example.com") webauthnConfig := &webauthn_helper.Config{ DisplayName: "test", @@ -143,7 +144,7 @@ func TestCommands_RegisterUserU2F(t *testing.T) { } func TestCommands_pushUserU2F(t *testing.T) { - ctx := authz.WithRequestedDomain(authz.NewMockContext("instance1", "org1", "user1"), "example.com") + ctx := http_util.WithRequestedHost(authz.NewMockContext("instance1", "org1", "user1"), "example.com") webauthnConfig := &webauthn_helper.Config{ DisplayName: "test", ExternalSecure: true, diff --git a/internal/integration/integration.go b/internal/integration/integration.go index ebca10d3e..807f976a3 100644 --- a/internal/integration/integration.go +++ b/internal/integration/integration.go @@ -205,7 +205,7 @@ func (s *Tester) createLoginClient(ctx context.Context) { func (s *Tester) createMachineUser(ctx context.Context, username string, userType UserType) (context.Context, *query.User) { var err error - s.Instance, err = s.Queries.InstanceByHost(ctx, s.Host()) + s.Instance, err = s.Queries.InstanceByHost(ctx, s.Host(), "") logging.OnError(err).Fatal("query instance") ctx = authz.WithInstance(ctx, s.Instance) diff --git a/internal/notification/handlers/origin.go b/internal/notification/handlers/origin.go index 0d12ac303..8846f5e2d 100644 --- a/internal/notification/handlers/origin.go +++ b/internal/notification/handlers/origin.go @@ -6,7 +6,6 @@ import ( "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/query" @@ -27,11 +26,7 @@ func (n *NotificationQueries) Origin(ctx context.Context, e eventstore.Event) (c origin = originEvent.TriggerOrigin() } if origin != "" { - originURL, err := url.Parse(origin) - if err != nil { - return ctx, err - } - return enrichCtx(ctx, originURL.Hostname(), origin), nil + return enrichCtx(ctx, origin) } primary, err := query.NewInstanceDomainPrimarySearchQuery(true) if err != nil { @@ -48,13 +43,19 @@ func (n *NotificationQueries) Origin(ctx context.Context, e eventstore.Event) (c } return enrichCtx( ctx, - domains.Domains[0].Domain, http_utils.BuildHTTP(domains.Domains[0].Domain, n.externalPort, n.externalSecure), - ), nil + ) } -func enrichCtx(ctx context.Context, host, origin string) context.Context { - ctx = authz.WithRequestedDomain(ctx, host) - ctx = http_utils.WithComposedOrigin(ctx, origin) - return ctx +func enrichCtx(ctx context.Context, origin string) (context.Context, error) { + u, err := url.Parse(origin) + if err != nil { + return nil, err + } + ctx = http_utils.WithDomainContext(ctx, &http_utils.DomainCtx{ + InstanceHost: u.Host, + PublicHost: u.Host, + Protocol: u.Scheme, + }) + return ctx, nil } diff --git a/internal/notification/handlers/user_notifier.go b/internal/notification/handlers/user_notifier.go index fd3acc84a..066796ae3 100644 --- a/internal/notification/handlers/user_notifier.go +++ b/internal/notification/handlers/user_notifier.go @@ -491,7 +491,7 @@ func (u *userNotifier) reduceOTPEmail( if err != nil { return nil, err } - url, err := urlTmpl(plainCode, http_util.ComposedOrigin(ctx), notifyUser) + url, err := urlTmpl(plainCode, http_util.DomainContext(ctx).Origin(), notifyUser) if err != nil { return nil, err } diff --git a/internal/notification/types/domain_claimed.go b/internal/notification/types/domain_claimed.go index 975cc74db..433728392 100644 --- a/internal/notification/types/domain_claimed.go +++ b/internal/notification/types/domain_claimed.go @@ -11,7 +11,7 @@ import ( ) func (notify Notify) SendDomainClaimed(ctx context.Context, user *query.NotifyUser, username string) error { - url := login.LoginLink(http_utils.ComposedOrigin(ctx), user.ResourceOwner) + url := login.LoginLink(http_utils.DomainContext(ctx).Origin(), user.ResourceOwner) index := strings.LastIndex(user.LastEmail, "@") args := make(map[string]interface{}) args["TempUsername"] = username diff --git a/internal/notification/types/email_verification_code.go b/internal/notification/types/email_verification_code.go index 912e846a9..4ff59137b 100644 --- a/internal/notification/types/email_verification_code.go +++ b/internal/notification/types/email_verification_code.go @@ -13,7 +13,7 @@ import ( func (notify Notify) SendEmailVerificationCode(ctx context.Context, user *query.NotifyUser, code string, urlTmpl, authRequestID string) error { var url string if urlTmpl == "" { - url = login.MailVerificationLink(http_utils.ComposedOrigin(ctx), user.ID, code, user.ResourceOwner, authRequestID) + url = login.MailVerificationLink(http_utils.DomainContext(ctx).Origin(), user.ID, code, user.ResourceOwner, authRequestID) } else { var buf strings.Builder if err := domain.RenderConfirmURLTemplate(&buf, urlTmpl, user.ID, code, user.ResourceOwner); err != nil { diff --git a/internal/notification/types/email_verification_code_test.go b/internal/notification/types/email_verification_code_test.go index 3f538b3fb..2196e25b0 100644 --- a/internal/notification/types/email_verification_code_test.go +++ b/internal/notification/types/email_verification_code_test.go @@ -16,7 +16,7 @@ import ( func TestNotify_SendEmailVerificationCode(t *testing.T) { type args struct { user *query.NotifyUser - origin string + origin *http_utils.DomainCtx code string urlTmpl string authRequestID string @@ -34,7 +34,7 @@ func TestNotify_SendEmailVerificationCode(t *testing.T) { ID: "user1", ResourceOwner: "org1", }, - origin: "https://example.com", + origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"}, code: "123", urlTmpl: "", authRequestID: "authRequestID", @@ -53,7 +53,7 @@ func TestNotify_SendEmailVerificationCode(t *testing.T) { ID: "user1", ResourceOwner: "org1", }, - origin: "https://example.com", + origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"}, code: "123", urlTmpl: "{{", authRequestID: "authRequestID", @@ -68,7 +68,7 @@ func TestNotify_SendEmailVerificationCode(t *testing.T) { ID: "user1", ResourceOwner: "org1", }, - origin: "https://example.com", + origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"}, code: "123", urlTmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", authRequestID: "authRequestID", @@ -84,7 +84,7 @@ func TestNotify_SendEmailVerificationCode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, notify := mockNotify() - err := notify.SendEmailVerificationCode(http_utils.WithComposedOrigin(context.Background(), tt.args.origin), tt.args.user, tt.args.code, tt.args.urlTmpl, tt.args.authRequestID) + err := notify.SendEmailVerificationCode(http_utils.WithDomainContext(context.Background(), tt.args.origin), tt.args.user, tt.args.code, tt.args.urlTmpl, tt.args.authRequestID) require.ErrorIs(t, err, tt.wantErr) assert.Equal(t, tt.want, got) }) diff --git a/internal/notification/types/init_code.go b/internal/notification/types/init_code.go index 11ea75ab2..3e38cc284 100644 --- a/internal/notification/types/init_code.go +++ b/internal/notification/types/init_code.go @@ -10,7 +10,7 @@ import ( ) func (notify Notify) SendUserInitCode(ctx context.Context, user *query.NotifyUser, code, authRequestID string) error { - url := login.InitUserLink(http_utils.ComposedOrigin(ctx), user.ID, user.PreferredLoginName, code, user.ResourceOwner, user.PasswordSet, authRequestID) + url := login.InitUserLink(http_utils.DomainContext(ctx).Origin(), user.ID, user.PreferredLoginName, code, user.ResourceOwner, user.PasswordSet, authRequestID) args := make(map[string]interface{}) args["Code"] = code return notify(url, args, domain.InitCodeMessageType, true) diff --git a/internal/notification/types/otp.go b/internal/notification/types/otp.go index 91befa23f..3242b2da3 100644 --- a/internal/notification/types/otp.go +++ b/internal/notification/types/otp.go @@ -4,7 +4,6 @@ import ( "context" "time" - "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/domain" ) @@ -20,10 +19,11 @@ func (notify Notify) SendOTPEmailCode(ctx context.Context, url, code string, exp } func otpArgs(ctx context.Context, code string, expiry time.Duration) map[string]interface{} { + domainCtx := http_utils.DomainContext(ctx) args := make(map[string]interface{}) args["OTP"] = code - args["Origin"] = http_utils.ComposedOrigin(ctx) - args["Domain"] = authz.GetInstance(ctx).RequestedDomain() + args["Origin"] = domainCtx.Origin() + args["Domain"] = domainCtx.RequestedDomain() args["Expiry"] = expiry return args } diff --git a/internal/notification/types/password_change.go b/internal/notification/types/password_change.go index 61483f047..8536ac4c0 100644 --- a/internal/notification/types/password_change.go +++ b/internal/notification/types/password_change.go @@ -10,7 +10,7 @@ import ( ) func (notify Notify) SendPasswordChange(ctx context.Context, user *query.NotifyUser) error { - url := console.LoginHintLink(http_utils.ComposedOrigin(ctx), user.PreferredLoginName) + url := console.LoginHintLink(http_utils.DomainContext(ctx).Origin(), user.PreferredLoginName) args := make(map[string]interface{}) return notify(url, args, domain.PasswordChangeMessageType, true) } diff --git a/internal/notification/types/password_code.go b/internal/notification/types/password_code.go index c5616e6e2..40ffee3e6 100644 --- a/internal/notification/types/password_code.go +++ b/internal/notification/types/password_code.go @@ -13,7 +13,7 @@ import ( func (notify Notify) SendPasswordCode(ctx context.Context, user *query.NotifyUser, code, urlTmpl, authRequestID string) error { var url string if urlTmpl == "" { - url = login.InitPasswordLink(http_utils.ComposedOrigin(ctx), user.ID, code, user.ResourceOwner, authRequestID) + url = login.InitPasswordLink(http_utils.DomainContext(ctx).Origin(), user.ID, code, user.ResourceOwner, authRequestID) } else { var buf strings.Builder if err := domain.RenderConfirmURLTemplate(&buf, urlTmpl, user.ID, code, user.ResourceOwner); err != nil { diff --git a/internal/notification/types/passwordless_registration_link.go b/internal/notification/types/passwordless_registration_link.go index 940aeaec0..64af1a979 100644 --- a/internal/notification/types/passwordless_registration_link.go +++ b/internal/notification/types/passwordless_registration_link.go @@ -13,7 +13,7 @@ import ( func (notify Notify) SendPasswordlessRegistrationLink(ctx context.Context, user *query.NotifyUser, code, codeID, urlTmpl string) error { var url string if urlTmpl == "" { - url = domain.PasswordlessInitCodeLink(http_utils.ComposedOrigin(ctx)+login.HandlerPrefix+login.EndpointPasswordlessRegistration, user.ID, user.ResourceOwner, codeID, code) + url = domain.PasswordlessInitCodeLink(http_utils.DomainContext(ctx).Origin()+login.HandlerPrefix+login.EndpointPasswordlessRegistration, user.ID, user.ResourceOwner, codeID, code) } else { var buf strings.Builder if err := domain.RenderPasskeyURLTemplate(&buf, urlTmpl, user.ID, user.ResourceOwner, codeID, code); err != nil { diff --git a/internal/notification/types/passwordless_registration_link_test.go b/internal/notification/types/passwordless_registration_link_test.go index 95fb75603..0a04b7a0f 100644 --- a/internal/notification/types/passwordless_registration_link_test.go +++ b/internal/notification/types/passwordless_registration_link_test.go @@ -16,7 +16,7 @@ import ( func TestNotify_SendPasswordlessRegistrationLink(t *testing.T) { type args struct { user *query.NotifyUser - origin string + origin *http_utils.DomainCtx code string codeID string urlTmpl string @@ -34,7 +34,7 @@ func TestNotify_SendPasswordlessRegistrationLink(t *testing.T) { ID: "user1", ResourceOwner: "org1", }, - origin: "https://example.com", + origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"}, code: "123", codeID: "456", urlTmpl: "", @@ -52,7 +52,7 @@ func TestNotify_SendPasswordlessRegistrationLink(t *testing.T) { ID: "user1", ResourceOwner: "org1", }, - origin: "https://example.com", + origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"}, code: "123", codeID: "456", urlTmpl: "{{", @@ -67,7 +67,7 @@ func TestNotify_SendPasswordlessRegistrationLink(t *testing.T) { ID: "user1", ResourceOwner: "org1", }, - origin: "https://example.com", + origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"}, code: "123", codeID: "456", urlTmpl: "https://example.com/passkey/register?userID={{.UserID}}&orgID={{.OrgID}}&codeID={{.CodeID}}&code={{.Code}}", @@ -82,7 +82,7 @@ func TestNotify_SendPasswordlessRegistrationLink(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, notify := mockNotify() - err := notify.SendPasswordlessRegistrationLink(http_utils.WithComposedOrigin(context.Background(), tt.args.origin), tt.args.user, tt.args.code, tt.args.codeID, tt.args.urlTmpl) + err := notify.SendPasswordlessRegistrationLink(http_utils.WithDomainContext(context.Background(), tt.args.origin), tt.args.user, tt.args.code, tt.args.codeID, tt.args.urlTmpl) require.ErrorIs(t, err, tt.wantErr) assert.Equal(t, tt.want, got) }) diff --git a/internal/notification/types/phone_verification_code.go b/internal/notification/types/phone_verification_code.go index 4b583c1e4..461b85749 100644 --- a/internal/notification/types/phone_verification_code.go +++ b/internal/notification/types/phone_verification_code.go @@ -3,13 +3,13 @@ package types import ( "context" - "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/domain" ) func (notify Notify) SendPhoneVerificationCode(ctx context.Context, code string) error { args := make(map[string]interface{}) args["Code"] = code - args["Domain"] = authz.GetInstance(ctx).RequestedDomain() + args["Domain"] = http_util.DomainContext(ctx).RequestedDomain() return notify("", args, domain.VerifyPhoneMessageType, true) } diff --git a/internal/notification/types/templateData.go b/internal/notification/types/templateData.go index 23b7deee7..4a410c027 100644 --- a/internal/notification/types/templateData.go +++ b/internal/notification/types/templateData.go @@ -13,7 +13,7 @@ import ( ) func GetTemplateData(ctx context.Context, translator *i18n.Translator, translateArgs map[string]interface{}, href, msgType, lang string, policy *query.LabelPolicy) templates.TemplateData { - assetsPrefix := http_util.ComposedOrigin(ctx) + assets.HandlerPrefix + assetsPrefix := http_util.DomainContext(ctx).Origin() + assets.HandlerPrefix templateData := templates.TemplateData{ URL: href, PrimaryColor: templates.DefaultPrimaryColor, diff --git a/internal/query/instance.go b/internal/query/instance.go index 8bc83b2d4..d547ae538 100644 --- a/internal/query/instance.go +++ b/internal/query/instance.go @@ -195,19 +195,24 @@ var ( instanceByIDQuery string ) -func (q *Queries) InstanceByHost(ctx context.Context, host string) (_ authz.Instance, err error) { +func (q *Queries) InstanceByHost(ctx context.Context, instanceHost, publicHost string) (_ authz.Instance, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { if err != nil { - err = fmt.Errorf("unable to get instance by host %s: %w", host, err) + err = fmt.Errorf("unable to get instance by host: instanceHost %s, publicHost %s: %w", instanceHost, publicHost, err) } span.EndWithError(err) }() - domain := strings.Split(host, ":")[0] // remove possible port - instance, scan := scanAuthzInstance(host, domain) - err = q.client.QueryRowContext(ctx, scan, instanceByDomainQuery, domain) - logging.OnError(err).WithField("host", host).WithField("domain", domain).Warn("instance by host") + instanceDomain := strings.Split(instanceHost, ":")[0] // remove possible port + publicDomain := strings.Split(publicHost, ":")[0] // remove possible port + instance, scan := scanAuthzInstance() + // in case public domain is the same as the instance domain, we do not need to check it + // and can empty it for the check + if instanceDomain == publicDomain { + publicDomain = "" + } + err = q.client.QueryRowContext(ctx, scan, instanceByDomainQuery, instanceDomain, publicDomain) return instance, err } @@ -216,7 +221,7 @@ func (q *Queries) InstanceByID(ctx context.Context) (_ authz.Instance, err error defer func() { span.EndWithError(err) }() instanceID := authz.GetInstance(ctx).InstanceID() - instance, scan := scanAuthzInstance("", "") + instance, scan := scanAuthzInstance() err = q.client.QueryRowContext(ctx, scan, instanceByIDQuery, instanceID) logging.OnError(err).WithField("instance_id", instanceID).Warn("instance by ID") return instance, err @@ -421,8 +426,6 @@ type authzInstance struct { iamProjectID string consoleID string consoleAppID string - host string - domain string defaultLang language.Tag defaultOrgID string csp csp @@ -453,14 +456,6 @@ func (i *authzInstance) ConsoleApplicationID() string { return i.consoleAppID } -func (i *authzInstance) RequestedDomain() string { - return strings.Split(i.host, ":")[0] -} - -func (i *authzInstance) RequestedHost() string { - return i.host -} - func (i *authzInstance) DefaultLanguage() language.Tag { return i.defaultLang } @@ -492,11 +487,8 @@ func (i *authzInstance) Features() feature.Features { return i.features } -func scanAuthzInstance(host, domain string) (*authzInstance, func(row *sql.Row) error) { - instance := &authzInstance{ - host: host, - domain: domain, - } +func scanAuthzInstance() (*authzInstance, func(row *sql.Row) error) { + instance := &authzInstance{} return instance, func(row *sql.Row) error { var ( lang string diff --git a/internal/query/instance_by_domain.sql b/internal/query/instance_by_domain.sql index 0c914df77..0d0aeeb4f 100644 --- a/internal/query/instance_by_domain.sql +++ b/internal/query/instance_by_domain.sql @@ -30,6 +30,8 @@ select f.features from domain d join projections.instances i on i.id = d.instance_id +left join projections.instance_trusted_domains td on i.id = td.instance_id left join projections.security_policies2 s on i.id = s.instance_id left join projections.limits l on i.id = l.instance_id -left join features f on i.id = f.instance_id; +left join features f on i.id = f.instance_id +where case when $2 = '' then true else td.domain = $2 end; diff --git a/internal/query/instance_trusted_domain.go b/internal/query/instance_trusted_domain.go new file mode 100644 index 000000000..2847c3969 --- /dev/null +++ b/internal/query/instance_trusted_domain.go @@ -0,0 +1,142 @@ +package query + +import ( + "context" + "database/sql" + "time" + + sq "github.com/Masterminds/squirrel" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/call" + "github.com/zitadel/zitadel/internal/query/projection" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type InstanceTrustedDomain struct { + CreationDate time.Time + ChangeDate time.Time + Sequence uint64 + Domain string + InstanceID string +} + +type InstanceTrustedDomains struct { + SearchResponse + Domains []*InstanceTrustedDomain +} + +type InstanceTrustedDomainSearchQueries struct { + SearchRequest + Queries []SearchQuery +} + +func (q *InstanceTrustedDomainSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + query = q.SearchRequest.toQuery(query) + for _, q := range q.Queries { + query = q.toQuery(query) + } + return query +} + +func NewInstanceTrustedDomainDomainSearchQuery(method TextComparison, value string) (SearchQuery, error) { + return NewTextQuery(InstanceTrustedDomainDomainCol, value, method) +} + +func (q *Queries) SearchInstanceTrustedDomains(ctx context.Context, queries *InstanceTrustedDomainSearchQueries) (domains *InstanceTrustedDomains, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + query, scan := prepareInstanceTrustedDomainsQuery(ctx, q.client) + stmt, args, err := queries.toQuery(query). + Where(sq.Eq{ + InstanceTrustedDomainInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), + }).ToSql() + if err != nil { + return nil, zerrors.ThrowInvalidArgument(err, "QUERY-SGrt4", "Errors.Query.SQLStatement") + } + + return q.queryInstanceTrustedDomains(ctx, stmt, scan, args...) +} + +func (q *Queries) queryInstanceTrustedDomains(ctx context.Context, stmt string, scan func(*sql.Rows) (*InstanceTrustedDomains, error), args ...interface{}) (domains *InstanceTrustedDomains, err error) { + err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { + domains, err = scan(rows) + return err + }, stmt, args...) + if err != nil { + return nil, err + } + domains.State, err = q.latestState(ctx, instanceDomainsTable) + return domains, err +} + +func prepareInstanceTrustedDomainsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*InstanceTrustedDomains, error)) { + return sq.Select( + InstanceTrustedDomainCreationDateCol.identifier(), + InstanceTrustedDomainChangeDateCol.identifier(), + InstanceTrustedDomainSequenceCol.identifier(), + InstanceTrustedDomainDomainCol.identifier(), + InstanceTrustedDomainInstanceIDCol.identifier(), + countColumn.identifier(), + ).From(instanceTrustedDomainsTable.identifier() + db.Timetravel(call.Took(ctx))). + PlaceholderFormat(sq.Dollar), + func(rows *sql.Rows) (*InstanceTrustedDomains, error) { + domains := make([]*InstanceTrustedDomain, 0) + var count uint64 + for rows.Next() { + domain := new(InstanceTrustedDomain) + err := rows.Scan( + &domain.CreationDate, + &domain.ChangeDate, + &domain.Sequence, + &domain.Domain, + &domain.InstanceID, + &count, + ) + if err != nil { + return nil, err + } + domains = append(domains, domain) + } + + if err := rows.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-SDg4h", "Errors.Query.CloseRows") + } + + return &InstanceTrustedDomains{ + Domains: domains, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil + } +} + +var ( + instanceTrustedDomainsTable = table{ + name: projection.InstanceTrustedDomainTable, + instanceIDCol: projection.InstanceTrustedDomainInstanceIDCol, + } + InstanceTrustedDomainCreationDateCol = Column{ + name: projection.InstanceTrustedDomainCreationDateCol, + table: instanceTrustedDomainsTable, + } + InstanceTrustedDomainChangeDateCol = Column{ + name: projection.InstanceTrustedDomainChangeDateCol, + table: instanceTrustedDomainsTable, + } + InstanceTrustedDomainSequenceCol = Column{ + name: projection.InstanceTrustedDomainSequenceCol, + table: instanceTrustedDomainsTable, + } + InstanceTrustedDomainDomainCol = Column{ + name: projection.InstanceTrustedDomainDomainCol, + table: instanceTrustedDomainsTable, + } + InstanceTrustedDomainInstanceIDCol = Column{ + name: projection.InstanceTrustedDomainInstanceIDCol, + table: instanceTrustedDomainsTable, + } +) diff --git a/internal/query/instance_trusted_domain_test.go b/internal/query/instance_trusted_domain_test.go new file mode 100644 index 000000000..6e3eea027 --- /dev/null +++ b/internal/query/instance_trusted_domain_test.go @@ -0,0 +1,157 @@ +package query + +import ( + "database/sql" + "database/sql/driver" + "errors" + "fmt" + "regexp" + "testing" +) + +var ( + prepareInstanceTrustedDomainsStmt = `SELECT projections.instance_trusted_domains.creation_date,` + + ` projections.instance_trusted_domains.change_date,` + + ` projections.instance_trusted_domains.sequence,` + + ` projections.instance_trusted_domains.domain,` + + ` projections.instance_trusted_domains.instance_id,` + + ` COUNT(*) OVER ()` + + ` FROM projections.instance_trusted_domains` + + ` AS OF SYSTEM TIME '-1 ms'` + prepareInstanceTrustedDomainsCols = []string{ + "creation_date", + "change_date", + "sequence", + "domain", + "instance_id", + "count", + } +) + +func Test_InstanceTrustedDomainPrepares(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + prepare interface{} + want want + object interface{} + }{ + { + name: "prepareInstanceTrustedDomainsQuery no result", + prepare: prepareInstanceTrustedDomainsQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareInstanceTrustedDomainsStmt), + nil, + nil, + ), + }, + object: &InstanceTrustedDomains{Domains: []*InstanceTrustedDomain{}}, + }, + { + name: "prepareInstanceTrustedDomainsQuery one result", + prepare: prepareInstanceTrustedDomainsQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareInstanceTrustedDomainsStmt), + prepareInstanceTrustedDomainsCols, + [][]driver.Value{ + { + testNow, + testNow, + uint64(20211109), + "zitadel.ch", + "inst-id", + }, + }, + ), + }, + object: &InstanceTrustedDomains{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Domains: []*InstanceTrustedDomain{ + { + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211109, + Domain: "zitadel.ch", + InstanceID: "inst-id", + }, + }, + }, + }, + { + name: "prepareInstanceTrustedDomainsQuery multiple result", + prepare: prepareInstanceTrustedDomainsQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareInstanceTrustedDomainsStmt), + prepareInstanceTrustedDomainsCols, + [][]driver.Value{ + { + testNow, + testNow, + uint64(20211109), + "zitadel.ch", + "inst-id", + }, + { + testNow, + testNow, + uint64(20211109), + "zitadel.com", + "inst-id", + }, + }, + ), + }, + object: &InstanceTrustedDomains{ + SearchResponse: SearchResponse{ + Count: 2, + }, + Domains: []*InstanceTrustedDomain{ + { + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211109, + Domain: "zitadel.ch", + InstanceID: "inst-id", + }, + { + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211109, + Domain: "zitadel.com", + InstanceID: "inst-id", + }, + }, + }, + }, + { + name: "prepareInstanceTrustedDomainsQuery sql err", + prepare: prepareInstanceTrustedDomainsQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(prepareInstanceTrustedDomainsStmt), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: (*Domains)(nil), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + }) + } +} diff --git a/internal/query/oidc_client.go b/internal/query/oidc_client.go index 6669b398b..d4364248b 100644 --- a/internal/query/oidc_client.go +++ b/internal/query/oidc_client.go @@ -8,6 +8,8 @@ import ( "time" "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/api/ui/console/path" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -56,5 +58,9 @@ func (q *Queries) GetOIDCClientByID(ctx context.Context, clientID string, getKey if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-ieR7R", "Errors.Internal") } + if authz.GetInstance(ctx).ConsoleClientID() == clientID { + client.RedirectURIs = append(client.RedirectURIs, http_util.DomainContext(ctx).Origin()+path.RedirectPath) + client.PostLogoutRedirectURIs = append(client.PostLogoutRedirectURIs, http_util.DomainContext(ctx).Origin()+path.PostLogoutPath) + } return client, err } diff --git a/internal/query/projection/instance_trusted_domain.go b/internal/query/projection/instance_trusted_domain.go new file mode 100644 index 000000000..b8c881c17 --- /dev/null +++ b/internal/query/projection/instance_trusted_domain.go @@ -0,0 +1,102 @@ +package projection + +import ( + "context" + + "github.com/zitadel/zitadel/internal/eventstore" + old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/repository/instance" +) + +const ( + InstanceTrustedDomainTable = "projections.instance_trusted_domains" + + InstanceTrustedDomainInstanceIDCol = "instance_id" + InstanceTrustedDomainCreationDateCol = "creation_date" + InstanceTrustedDomainChangeDateCol = "change_date" + InstanceTrustedDomainSequenceCol = "sequence" + InstanceTrustedDomainDomainCol = "domain" +) + +type instanceTrustedDomainProjection struct{} + +func newInstanceTrustedDomainProjection(ctx context.Context, config handler.Config) *handler.Handler { + return handler.NewHandler(ctx, &config, new(instanceTrustedDomainProjection)) +} + +func (*instanceTrustedDomainProjection) Name() string { + return InstanceTrustedDomainTable +} + +func (*instanceTrustedDomainProjection) Init() *old_handler.Check { + return handler.NewTableCheck( + handler.NewTable([]*handler.InitColumn{ + handler.NewColumn(InstanceTrustedDomainInstanceIDCol, handler.ColumnTypeText), + handler.NewColumn(InstanceTrustedDomainCreationDateCol, handler.ColumnTypeTimestamp), + handler.NewColumn(InstanceTrustedDomainChangeDateCol, handler.ColumnTypeTimestamp), + handler.NewColumn(InstanceTrustedDomainSequenceCol, handler.ColumnTypeInt64), + handler.NewColumn(InstanceTrustedDomainDomainCol, handler.ColumnTypeText), + }, + handler.NewPrimaryKey(InstanceTrustedDomainInstanceIDCol, InstanceTrustedDomainDomainCol), + handler.WithIndex( + handler.NewIndex("instance_trusted_domain", []string{InstanceTrustedDomainDomainCol}, + handler.WithInclude(InstanceTrustedDomainCreationDateCol, InstanceTrustedDomainChangeDateCol, InstanceTrustedDomainSequenceCol), + ), + ), + ), + ) +} + +func (p *instanceTrustedDomainProjection) Reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: instance.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: instance.TrustedDomainAddedEventType, + Reduce: p.reduceDomainAdded, + }, + { + Event: instance.TrustedDomainRemovedEventType, + Reduce: p.reduceDomainRemoved, + }, + { + Event: instance.InstanceRemovedEventType, + Reduce: reduceInstanceRemovedHelper(InstanceTrustedDomainInstanceIDCol), + }, + }, + }, + } +} + +func (p *instanceTrustedDomainProjection) reduceDomainAdded(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.TrustedDomainAddedEvent](event) + if err != nil { + return nil, err + } + return handler.NewCreateStatement( + e, + []handler.Column{ + handler.NewCol(InstanceTrustedDomainCreationDateCol, e.CreatedAt()), + handler.NewCol(InstanceTrustedDomainChangeDateCol, e.CreatedAt()), + handler.NewCol(InstanceTrustedDomainSequenceCol, e.Sequence()), + handler.NewCol(InstanceTrustedDomainDomainCol, e.Domain), + handler.NewCol(InstanceTrustedDomainInstanceIDCol, e.Aggregate().ID), + }, + ), nil +} + +func (p *instanceTrustedDomainProjection) reduceDomainRemoved(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.TrustedDomainRemovedEvent](event) + if err != nil { + return nil, err + } + return handler.NewDeleteStatement( + e, + []handler.Condition{ + handler.NewCond(InstanceTrustedDomainDomainCol, e.Domain), + handler.NewCond(InstanceTrustedDomainInstanceIDCol, e.Aggregate().ID), + }, + ), nil +} diff --git a/internal/query/projection/instance_trusted_domain_test.go b/internal/query/projection/instance_trusted_domain_test.go new file mode 100644 index 000000000..d06bb2e2e --- /dev/null +++ b/internal/query/projection/instance_trusted_domain_test.go @@ -0,0 +1,119 @@ +package projection + +import ( + "testing" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestInstanceTrustedDomainProjection_reduces(t *testing.T) { + type args struct { + event func(t *testing.T) eventstore.Event + } + tests := []struct { + name string + args args + reduce func(event eventstore.Event) (*handler.Statement, error) + want wantReduce + }{ + { + name: "reduceDomainAdded", + args: args{ + event: getEvent( + testEvent( + instance.TrustedDomainAddedEventType, + instance.AggregateType, + []byte(`{"domain": "domain.new"}`), + ), eventstore.GenericEventMapper[instance.TrustedDomainAddedEvent]), + }, + reduce: (&instanceTrustedDomainProjection{}).reduceDomainAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO projections.instance_trusted_domains (creation_date, change_date, sequence, domain, instance_id) VALUES ($1, $2, $3, $4, $5)", + expectedArgs: []interface{}{ + anyArg{}, + anyArg{}, + uint64(15), + "domain.new", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceDomainRemoved", + args: args{ + event: getEvent( + testEvent( + instance.TrustedDomainRemovedEventType, + instance.AggregateType, + []byte(`{"domain": "domain.new"}`), + ), eventstore.GenericEventMapper[instance.TrustedDomainRemovedEvent]), + }, + reduce: (&instanceTrustedDomainProjection{}).reduceDomainRemoved, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.instance_trusted_domains WHERE (domain = $1) AND (instance_id = $2)", + expectedArgs: []interface{}{ + "domain.new", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceInstanceRemoved", + args: args{ + event: getEvent( + testEvent( + instance.InstanceRemovedEventType, + instance.AggregateType, + nil, + ), instance.InstanceRemovedEventMapper), + }, + reduce: reduceInstanceRemovedHelper(InstanceTrustedDomainInstanceIDCol), + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.instance_trusted_domains WHERE (instance_id = $1)", + expectedArgs: []interface{}{ + "agg-id", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := baseEvent(t) + got, err := tt.reduce(event) + if ok := zerrors.IsErrorInvalidArgument(err); !ok { + t.Errorf("no wrong event mapping: %v, got: %v", err, got) + } + + event = tt.args.event(t) + got, err = tt.reduce(event) + assertReduce(t, got, err, InstanceTrustedDomainTable, tt.want) + }) + } +} diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index 0cc3abeb0..a7776d24a 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -45,6 +45,7 @@ var ( LoginNameProjection *handler.Handler OrgMemberProjection *handler.Handler InstanceDomainProjection *handler.Handler + InstanceTrustedDomainProjection *handler.Handler InstanceMemberProjection *handler.Handler ProjectMemberProjection *handler.Handler ProjectGrantMemberProjection *handler.Handler @@ -132,6 +133,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore, LoginNameProjection = newLoginNameProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["login_names"])) OrgMemberProjection = newOrgMemberProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["org_members"])) InstanceDomainProjection = newInstanceDomainProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["instance_domains"])) + InstanceTrustedDomainProjection = newInstanceTrustedDomainProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["instance_trusted_domains"])) InstanceMemberProjection = newInstanceMemberProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["iam_members"])) ProjectMemberProjection = newProjectMemberProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_members"])) ProjectGrantMemberProjection = newProjectGrantMemberProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_grant_members"])) @@ -260,6 +262,7 @@ func newProjectionsList() { LoginNameProjection, OrgMemberProjection, InstanceDomainProjection, + InstanceTrustedDomainProjection, InstanceMemberProjection, ProjectMemberProjection, ProjectGrantMemberProjection, diff --git a/internal/repository/instance/eventstore.go b/internal/repository/instance/eventstore.go index d17dd2aa2..16b7e3967 100644 --- a/internal/repository/instance/eventstore.go +++ b/internal/repository/instance/eventstore.go @@ -121,4 +121,6 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, InstanceRemovedEventType, InstanceRemovedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, NotificationPolicyAddedEventType, NotificationPolicyAddedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, NotificationPolicyChangedEventType, NotificationPolicyChangedEventMapper) + eventstore.RegisterFilterEventMapper(AggregateType, TrustedDomainAddedEventType, eventstore.GenericEventMapper[TrustedDomainAddedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, TrustedDomainRemovedEventType, eventstore.GenericEventMapper[TrustedDomainRemovedEvent]) } diff --git a/internal/repository/instance/trusted_domain.go b/internal/repository/instance/trusted_domain.go new file mode 100644 index 000000000..29d356d9e --- /dev/null +++ b/internal/repository/instance/trusted_domain.go @@ -0,0 +1,95 @@ +package instance + +import ( + "context" + + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ( + trustedDomainPrefix = "trusted_domains." + UniqueTrustedDomain = "trusted_domain" + TrustedDomainAddedEventType = instanceEventTypePrefix + trustedDomainPrefix + "added" + TrustedDomainRemovedEventType = instanceEventTypePrefix + trustedDomainPrefix + "removed" +) + +func NewAddTrustedDomainUniqueConstraint(trustedDomain string) *eventstore.UniqueConstraint { + return eventstore.NewAddEventUniqueConstraint( + UniqueTrustedDomain, + trustedDomain, + "Errors.Instance.Domain.AlreadyExists") +} + +func NewRemoveTrustedDomainUniqueConstraint(trustedDomain string) *eventstore.UniqueConstraint { + return eventstore.NewRemoveUniqueConstraint( + UniqueTrustedDomain, + trustedDomain) +} + +type TrustedDomainAddedEvent struct { + eventstore.BaseEvent `json:"-"` + + Domain string `json:"domain"` +} + +func (e *TrustedDomainAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = *event +} + +func NewTrustedDomainAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + trustedDomain string, +) *TrustedDomainAddedEvent { + event := &TrustedDomainAddedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + TrustedDomainAddedEventType, + ), + Domain: trustedDomain, + } + return event +} + +func (e *TrustedDomainAddedEvent) Payload() interface{} { + return e +} + +func (e *TrustedDomainAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return []*eventstore.UniqueConstraint{NewAddTrustedDomainUniqueConstraint(e.Domain)} +} + +type TrustedDomainRemovedEvent struct { + eventstore.BaseEvent `json:"-"` + + Domain string `json:"domain"` +} + +func (e *TrustedDomainRemovedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = *event +} + +func NewTrustedDomainRemovedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + trustedDomain string, +) *TrustedDomainRemovedEvent { + event := &TrustedDomainRemovedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + TrustedDomainRemovedEventType, + ), + Domain: trustedDomain, + } + return event +} + +func (e *TrustedDomainRemovedEvent) Payload() interface{} { + return e +} + +func (e *TrustedDomainRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return []*eventstore.UniqueConstraint{NewRemoveTrustedDomainUniqueConstraint(e.Domain)} +} diff --git a/internal/repository/session/session.go b/internal/repository/session/session.go index b8768c639..3e9b727f5 100644 --- a/internal/repository/session/session.go +++ b/internal/repository/session/session.go @@ -358,7 +358,7 @@ func NewOTPSMSChallengedEvent( Code: code, Expiry: expiry, CodeReturned: codeReturned, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } @@ -468,7 +468,7 @@ func NewOTPEmailChallengedEvent( Expiry: expiry, ReturnCode: returnCode, URLTmpl: urlTmpl, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } diff --git a/internal/repository/user/human.go b/internal/repository/user/human.go index ab5b76d8a..e9fd49a35 100644 --- a/internal/repository/user/human.go +++ b/internal/repository/user/human.go @@ -278,7 +278,7 @@ func NewHumanInitialCodeAddedEvent( ), Code: code, Expiry: expiry, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), AuthRequestID: authRequestID, } } diff --git a/internal/repository/user/human_email.go b/internal/repository/user/human_email.go index 942f10191..34e490323 100644 --- a/internal/repository/user/human_email.go +++ b/internal/repository/user/human_email.go @@ -171,7 +171,7 @@ func NewHumanEmailCodeAddedEventV2( Expiry: expiry, URLTemplate: urlTemplate, CodeReturned: codeReturned, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), AuthRequestID: authRequestID, } } diff --git a/internal/repository/user/human_mfa_otp.go b/internal/repository/user/human_mfa_otp.go index 1eb74f340..f0f3762c8 100644 --- a/internal/repository/user/human_mfa_otp.go +++ b/internal/repository/user/human_mfa_otp.go @@ -314,7 +314,7 @@ func NewHumanOTPSMSCodeAddedEvent( ), Code: code, Expiry: expiry, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), AuthRequestInfo: info, } } @@ -515,7 +515,7 @@ func NewHumanOTPEmailCodeAddedEvent( Code: code, Expiry: expiry, AuthRequestInfo: info, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } diff --git a/internal/repository/user/human_mfa_passwordless.go b/internal/repository/user/human_mfa_passwordless.go index 603c65749..52264f8d6 100644 --- a/internal/repository/user/human_mfa_passwordless.go +++ b/internal/repository/user/human_mfa_passwordless.go @@ -356,7 +356,7 @@ func NewHumanPasswordlessInitCodeRequestedEvent( Expiry: expiry, URLTemplate: urlTmpl, CodeReturned: codeReturned, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } diff --git a/internal/repository/user/human_password.go b/internal/repository/user/human_password.go index bf7ba278c..c425c144b 100644 --- a/internal/repository/user/human_password.go +++ b/internal/repository/user/human_password.go @@ -62,7 +62,7 @@ func NewHumanPasswordChangedEvent( EncodedHash: encodeHash, ChangeRequired: changeRequired, UserAgentID: userAgentID, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } @@ -120,7 +120,7 @@ func NewHumanPasswordCodeAddedEvent( Code: code, Expiry: expiry, NotificationType: notificationType, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), AuthRequestID: authRequestID, } } @@ -145,7 +145,7 @@ func NewHumanPasswordCodeAddedEventV2( NotificationType: notificationType, URLTemplate: urlTemplate, CodeReturned: codeReturned, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } diff --git a/internal/repository/user/human_phone.go b/internal/repository/user/human_phone.go index 2b3a1f24d..5655b1b3d 100644 --- a/internal/repository/user/human_phone.go +++ b/internal/repository/user/human_phone.go @@ -190,7 +190,7 @@ func NewHumanPhoneCodeAddedEventV2( Code: code, Expiry: expiry, CodeReturned: codeReturned, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } diff --git a/internal/repository/user/user.go b/internal/repository/user/user.go index a736cb4d2..e8faddb64 100644 --- a/internal/repository/user/user.go +++ b/internal/repository/user/user.go @@ -430,7 +430,7 @@ func NewDomainClaimedEvent( UserName: userName, oldUserName: oldUserName, userLoginMustBeDomain: userLoginMustBeDomain, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } diff --git a/internal/webauthn/webauthn.go b/internal/webauthn/webauthn.go index bb13d28bd..5938f885b 100644 --- a/internal/webauthn/webauthn.go +++ b/internal/webauthn/webauthn.go @@ -10,7 +10,6 @@ import ( "github.com/go-webauthn/webauthn/webauthn" "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" @@ -195,11 +194,11 @@ func (w *Config) serverFromContext(ctx context.Context, id, origin string) (*web } func (w *Config) configFromContext(ctx context.Context) *webauthn.Config { - instance := authz.GetInstance(ctx) + domainCtx := http.DomainContext(ctx) return &webauthn.Config{ RPDisplayName: w.DisplayName, - RPID: instance.RequestedDomain(), - RPOrigins: []string{http.BuildOrigin(instance.RequestedHost(), w.ExternalSecure)}, + RPID: domainCtx.RequestedDomain(), + RPOrigins: []string{domainCtx.Origin()}, } } diff --git a/internal/webauthn/webauthn_test.go b/internal/webauthn/webauthn_test.go index b7c4276ae..b020f802c 100644 --- a/internal/webauthn/webauthn_test.go +++ b/internal/webauthn/webauthn_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -31,7 +31,11 @@ func TestConfig_serverFromContext(t *testing.T) { }, { name: "success from ctx", - args: args{authz.WithRequestedDomain(context.Background(), "example.com"), "", ""}, + args: args{ + ctx: http_util.WithDomainContext(context.Background(), &http_util.DomainCtx{InstanceHost: "example.com", Protocol: "https"}), + id: "", + origin: "", + }, want: &webauthn.WebAuthn{ Config: &webauthn.Config{ RPDisplayName: "DisplayName", @@ -42,7 +46,11 @@ func TestConfig_serverFromContext(t *testing.T) { }, { name: "success from id", - args: args{authz.WithRequestedDomain(context.Background(), "example.com"), "external.com", "https://external.com"}, + args: args{ + ctx: http_util.WithDomainContext(context.Background(), &http_util.DomainCtx{InstanceHost: "example.com", Protocol: "https"}), + id: "external.com", + origin: "https://external.com", + }, want: &webauthn.WebAuthn{ Config: &webauthn.Config{ RPDisplayName: "DisplayName", diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 8fa9b0322..9fe6ff643 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -323,6 +323,54 @@ service AdminService { }; } + rpc ListInstanceTrustedDomains(ListInstanceTrustedDomainsRequest) returns (ListInstanceTrustedDomainsResponse) { + option (google.api.http) = { + post: "/trusted_domains/_search"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.read"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Instance"; + summary: "List Instance Trusted Domains"; + description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts." + }; + } + + rpc AddInstanceTrustedDomain(AddInstanceTrustedDomainRequest) returns (AddInstanceTrustedDomainResponse) { + option (google.api.http) = { + post: "/trusted_domains"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Instance"; + summary: "Add an Instance Trusted Domain"; + description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts." + }; + } + + rpc RemoveInstanceTrustedDomain(RemoveInstanceTrustedDomainRequest) returns (RemoveInstanceTrustedDomainResponse) { + option (google.api.http) = { + delete: "/trusted_domains/{domain}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Instance"; + summary: "Remove an Instance Trusted Domain"; + description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts." + }; + } + rpc ListSecretGenerators(ListSecretGeneratorsRequest) returns (ListSecretGeneratorsResponse) { option (google.api.http) = { post: "/secretgenerators/_search" @@ -4062,6 +4110,52 @@ message ListInstanceDomainsResponse { repeated zitadel.instance.v1.Domain result = 3; } +message ListInstanceTrustedDomainsRequest { + zitadel.v1.ListQuery query = 1; + // the field the result is sorted + zitadel.instance.v1.DomainFieldName sorting_column = 2; + //criteria the client is looking for + repeated zitadel.instance.v1.TrustedDomainSearchQuery queries = 3; +} + +message ListInstanceTrustedDomainsResponse { + zitadel.v1.ListDetails details = 1; + zitadel.instance.v1.DomainFieldName sorting_column = 2; + repeated zitadel.instance.v1.TrustedDomain result = 3; +} + +message AddInstanceTrustedDomainRequest { + string domain = 1 [ + (validate.rules).string = {min_len: 1, max_len: 253}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"login.example.com\""; + min_length: 1; + max_length: 253; + } + ]; +} + +message AddInstanceTrustedDomainResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message RemoveInstanceTrustedDomainRequest { + string domain = 1 [ + (validate.rules).string = {min_len: 1, max_len: 253}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"login.example.com\""; + min_length: 1; + max_length: 253; + } + ]; +} + +message RemoveInstanceTrustedDomainResponse { + zitadel.v1.ObjectDetails details = 1; +} + message ListSecretGeneratorsRequest { //list limitations and ordering zitadel.v1.ListQuery query = 1; diff --git a/proto/zitadel/instance.proto b/proto/zitadel/instance.proto index e6fdbd341..c5ef206ee 100644 --- a/proto/zitadel/instance.proto +++ b/proto/zitadel/instance.proto @@ -163,3 +163,20 @@ enum DomainFieldName { DOMAIN_FIELD_NAME_GENERATED = 3; DOMAIN_FIELD_NAME_CREATION_DATE = 4; } + +message TrustedDomain { + zitadel.v1.ObjectDetails details = 1; + string domain = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.com\"" + } + ]; +} + +message TrustedDomainSearchQuery { + oneof query { + option (validate.required) = true; + + DomainQuery domain_query = 1; + } +}