feat: improve instance not found error (#7413)

* feat: improve instance not found error

* unit tests

* check if is templatable

* lint

* assert

* compile tests

* remove error templates

* link to instance not found page

* fmt

* cleanup

* lint
This commit is contained in:
Elio Bischof 2024-02-28 11:49:57 +01:00 committed by GitHub
parent 062d153cfe
commit f4c72cbe14
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 55 additions and 38 deletions

View File

@ -360,7 +360,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, limitingAccessInterceptor)
apis, err := api.New(ctx, config.Port, router, queries, verifier, config.InternalAuthZ, tlsConfig, config.HTTP2HostHeader, config.HTTP1HostHeader, config.ExternalDomain, limitingAccessInterceptor)
if err != nil {
return nil, fmt.Errorf("error creating api %w", err)
}
@ -410,7 +410,7 @@ func startAPIs(
if err := apis.RegisterService(ctx, execution_v3_alpha.CreateServer(commands, queries, domain.AllFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil {
return nil, err
}
instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader, login.IgnoreInstanceEndpoints...)
instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader, 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))

View File

@ -268,6 +268,8 @@ ZITADEL hosts everything under a single domain: `{instance}.zitadel.cloud` or yo
The domain is used as the OIDC issuer and as the base url for the gRPC and REST APIs, the Login and Console UI, which you'll find under `{your_domain}/ui/console/`.
Are you self-hosting and having troubles with *Instance not found* errors? [Check out this page](https://zitadel.com/docs/self-hosting/manage/custom-domain).
## API path prefixes
If you run ZITADEL on a custom domain, you may want to reuse that domain for other applications.

View File

@ -74,7 +74,8 @@ func New(
queries *query.Queries,
verifier internal_authz.APITokenVerifier,
authZ internal_authz.Config,
tlsConfig *tls.Config, http2HostName, http1HostName string,
tlsConfig *tls.Config,
http2HostName, http1HostName, externalDomain string,
accessInterceptor *http_mw.AccessInterceptor,
) (_ *API, err error) {
api := &API{
@ -87,7 +88,7 @@ func New(
accessInterceptor: accessInterceptor,
}
api.grpcServer = server.CreateServer(api.verifier, authZ, queries, http2HostName, tlsConfig, accessInterceptor.AccessService())
api.grpcServer = server.CreateServer(api.verifier, authZ, queries, http2HostName, externalDomain, tlsConfig, accessInterceptor.AccessService())
api.grpcGateway, err = server.CreateGateway(ctx, port, http1HostName, accessInterceptor, tlsConfig)
if err != nil {
return nil, err

View File

@ -14,6 +14,7 @@ import (
"google.golang.org/grpc/status"
"github.com/zitadel/zitadel/internal/api/authz"
zitadel_http "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
@ -23,15 +24,15 @@ const (
HTTP1Host = "x-zitadel-http1-host"
)
func InstanceInterceptor(verifier authz.InstanceVerifier, headerName string, explicitInstanceIdServices ...string) grpc.UnaryServerInterceptor {
func InstanceInterceptor(verifier authz.InstanceVerifier, headerName, 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, translator, explicitInstanceIdServices...)
return setInstance(ctx, req, info, handler, verifier, headerName, externalDomain, translator, explicitInstanceIdServices...)
}
}
func setInstance(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.InstanceVerifier, headerName 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, headerName, externalDomain string, translator *i18n.Translator, idFromRequestsServices ...string) (_ interface{}, err error) {
interceptorCtx, span := tracing.NewServerInterceptorSpan(ctx)
defer func() { span.EndWithError(err) }()
for _, service := range idFromRequestsServices {
@ -55,16 +56,18 @@ 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())
}
instance, err := verifier.InstanceByHost(interceptorCtx, host)
if err != nil {
notFoundErr := new(zerrors.NotFoundError)
if errors.As(err, &notFoundErr) {
notFoundErr.Message = translator.LocalizeFromCtx(ctx, notFoundErr.GetMessage(), nil)
err = fmt.Errorf("unable to set instance using origin %s (ExternalDomain is %s): %w", zitadel_http.ComposedOrigin(ctx), externalDomain, err)
zErr := new(zerrors.ZitadelError)
if errors.As(err, &zErr) {
zErr.SetMessage(translator.LocalizeFromCtx(ctx, zErr.GetMessage(), nil))
zErr.Parent = err
err = zErr
}
return nil, status.Error(codes.NotFound, err.Error())
}

View File

@ -137,7 +137,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, tt.args.headerName, "", nil)
if (err != nil) != tt.res.err {
t.Errorf("setInstance() error = %v, wantErr %v", err, tt.res.err)
return

View File

@ -39,6 +39,7 @@ func CreateServer(
authConfig authz.Config,
queries *query.Queries,
hostHeaderName string,
externalDomain string,
tlsConfig *tls.Config,
accessSvc *logstore.Service[*record.AccessLog],
) *grpc.Server {
@ -50,7 +51,7 @@ func CreateServer(
middleware.DefaultTracingServer(),
middleware.MetricsHandler(metricTypes, grpc_api.Probes...),
middleware.NoCacheInterceptor(),
middleware.InstanceInterceptor(queries, hostHeaderName, system_pb.SystemService_ServiceDesc.ServiceName, healthpb.Health_ServiceDesc.ServiceName),
middleware.InstanceInterceptor(queries, hostHeaderName, externalDomain, system_pb.SystemService_ServiceDesc.ServiceName, healthpb.Health_ServiceDesc.ServiceName),
middleware.AccessStorageInterceptor(accessSvc),
middleware.ErrorHandler(),
middleware.LimitsInterceptor(system_pb.SystemService_ServiceDesc.ServiceName),

View File

@ -19,16 +19,17 @@ import (
)
type instanceInterceptor struct {
verifier authz.InstanceVerifier
headerName string
ignoredPrefixes []string
translator *i18n.Translator
verifier authz.InstanceVerifier
headerName, externalDomain string
ignoredPrefixes []string
translator *i18n.Translator
}
func InstanceInterceptor(verifier authz.InstanceVerifier, headerName string, ignoredPrefixes ...string) *instanceInterceptor {
func InstanceInterceptor(verifier authz.InstanceVerifier, headerName, externalDomain string, ignoredPrefixes ...string) *instanceInterceptor {
return &instanceInterceptor{
verifier: verifier,
headerName: headerName,
externalDomain: externalDomain,
ignoredPrefixes: ignoredPrefixes,
translator: newZitadelTranslator(),
}
@ -55,9 +56,12 @@ func (a *instanceInterceptor) handleInstance(w http.ResponseWriter, r *http.Requ
}
ctx, err := setInstance(r, a.verifier, a.headerName)
if err != nil {
caosErr := new(zerrors.NotFoundError)
if errors.As(err, &caosErr) {
caosErr.Message = a.translator.LocalizeFromRequest(r, caosErr.GetMessage(), nil)
err = fmt.Errorf("unable to set instance using origin %s (ExternalDomain is %s): %w", zitadel_http.ComposedOrigin(r.Context()), a.externalDomain, err)
zErr := new(zerrors.ZitadelError)
if errors.As(err, &zErr) {
zErr.SetMessage(a.translator.LocalizeFromRequest(r, zErr.GetMessage(), nil))
zErr.Parent = err
err = zErr
}
http.Error(w, err.Error(), http.StatusNotFound)
return
@ -68,13 +72,13 @@ func (a *instanceInterceptor) handleInstance(w http.ResponseWriter, r *http.Requ
func setInstance(r *http.Request, verifier authz.InstanceVerifier, headerName string) (_ 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 {
return nil, zerrors.ThrowNotFound(err, "INST-zWq7X", "Errors.Instance.NotFound")
return nil, zerrors.ThrowNotFound(err, "INST-zWq7X", "Errors.IAM.NotFound")
}
instance, err := verifier.InstanceByHost(authCtx, host)

View File

@ -6,6 +6,7 @@ import (
_ "embed"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
@ -196,7 +197,12 @@ var (
func (q *Queries) InstanceByHost(ctx context.Context, host string) (_ authz.Instance, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
defer func() {
if err != nil {
err = fmt.Errorf("unable to get instance by host %s: %w", host, err)
}
span.EndWithError(err)
}()
domain := strings.Split(host, ":")[0] // remove possible port
instance, scan := scanAuthzInstance(host, domain)

View File

@ -330,7 +330,7 @@ Errors:
NotActive: Грантът по проекта не е активен
NotInactive: Грантът по проекта не е неактивен
IAM:
NotFound: Екземплярът не е намерен. Вижте https://zitadel.com/docs/self-hosting/manage/custom-domain
NotFound: IAM не е намерен. Уверете се, че сте получили правилния домейн. Вижте https://zitadel.com/docs/apis/introduction#domains
Member:
RolesNotChanged: Ролите не са сменени
MemberInvalid: Членът е невалиден

View File

@ -327,7 +327,7 @@ Errors:
NotActive: Grant projektu není aktivní
NotInactive: Grant projektu není neaktivní
IAM:
NotFound: Instance nenalezena. Podívejte se na https://zitadel.com/docs/self-hosting/manage/custom-domain
NotFound: Instance nebyla nalezena. Ujistěte se, že jste získali správnou doménu. Podívejte se na https://zitadel.com/docs/apis/introduction#domains
Member:
RolesNotChanged: Role nebyly změněny
MemberInvalid: Člen je neplatný

View File

@ -328,7 +328,7 @@ Errors:
NotActive: Projekt Grant ist nicht aktiv
NotInactive: Projekt Grant ist nicht inaktiv
IAM:
NotFound: Instanz nicht gefunden. Schau dir https://zitadel.com/docs/self-hosting/manage/custom-domain an
NotFound: Instanz nicht gefunden. Stelle sicher, dass Du die richtige Domain hast. Schau unter https://zitadel.com/docs/apis/introduction#domains
Member:
RolesNotChanged: Rollen wurden nicht verändert
MemberInvalid: Member ist ungültig

View File

@ -328,7 +328,7 @@ Errors:
NotActive: Project grant is not active
NotInactive: Project grant is not inactive
IAM:
NotFound: Instance not found. Check out https://zitadel.com/docs/self-hosting/manage/custom-domain
NotFound: Instance not found. Make sure you got the domain right. Check out https://zitadel.com/docs/apis/introduction#domains
Member:
RolesNotChanged: Roles have not been changed
MemberInvalid: Member is invalid

View File

@ -328,7 +328,7 @@ Errors:
NotActive: La concesión del proyecto no está activa
NotInactive: La concesión del proyecto no está inactiva
IAM:
NotFound: Instancia no encontrada. Consulta https://zitadel.com/docs/self-hosting/manage/custom-domain
NotFound: Instancia no encontrada. Asegúrate de que tienes el dominio correcto. Consulta https://zitadel.com/docs/apis/introduction#domains
Member:
RolesNotChanged: Los roles no han cambiado
MemberInvalid: El miembro no es válido

View File

@ -328,7 +328,7 @@ Errors:
NotActive: La subvention de projet n'est pas active
NotInactive: La subvention du projet n'est pas inactive
IAM:
NotFound: Instance non trouvée. Consultez https://zitadel.com/docs/self-hosting/manage/custom-domain
NotFound: IAM non trouvé. Assurez-vous que vous avez la bonne organisation. Vérifiez https://zitadel.com/docs/apis/introduction#organizations
Member:
RolesNotChanged: Les rôles n'ont pas été modifiés
MemberInvalid: Le membre n'est pas valide

View File

@ -329,7 +329,7 @@ Errors:
NotActive: Grant del progetto non è attivo
NotInactive: Grant del progetto non è inattivo
IAM:
NotFound: Istanza non trovata. Controlla https://zitadel.com/docs/self-hosting/manage/custom-domain
NotFound: IAM non trovato. Assicurati di avere il dominio corretto. Guarda su https://zitadel.com/docs/apis/introduction#domains
Member:
RolesNotChanged: I ruoli non sono stati cambiati
MemberInvalid: Il membro non è valido

View File

@ -317,7 +317,7 @@ Errors:
NotActive: プロジェクトグラントはアクティブではありません
NotInactive: プロジェクトグラントは非アクティブではありません
IAM:
NotFound: インスタンスが見つかりません https://zitadel.com/docs/self-hosting/manage/custom-domain
NotFound: IAMが見つかりません。正しいドメインを持っていることを確認してください。 https://zitadel.com/docs/apis/introduction#domains を参照してください
Member:
RolesNotChanged: ロールは変更されていません
MemberInvalid: 無効なメンバーです

View File

@ -327,7 +327,7 @@ Errors:
NotActive: Овластувањето за проектот не е активно
NotInactive: Овластувањето за проектот не е неактивно
IAM:
NotFound: Инстанцата не е пронајдена. Проверете https://zitadel.com/docs/self-hosting/manage/custom-domain
NotFound: IAM не е пронајден. Проверете дали имате точен домен. Погледнете на https://zitadel.com/docs/apis/introduction#domains
Member:
RolesNotChanged: Улогите не се променети
MemberInvalid: Членот е невалиден

View File

@ -328,7 +328,7 @@ Errors:
NotActive: Projecttoekenning is niet actief
NotInactive: Projecttoekenning is niet gedeactiveerd
IAM:
NotFound: Instantie niet gevonden. Bekijk https://zitadel.com/docs/self-hosting/manage/custom-domain
NotFound: IAM niet gevonden. Zorg ervoor dat u het juiste domein heeft. Kijk op https://zitadel.com/docs/apis/introduction#domains
Member:
RolesNotChanged: Rollen zijn niet veranderd
MemberInvalid: Lid is ongeldig

View File

@ -328,7 +328,7 @@ Errors:
NotActive: Grant projektu jest nieaktywny
NotInactive: Grant projektu nie jest nieaktywny
IAM:
NotFound: Instancja nie znaleziona. Sprawdź https://zitadel.com/docs/self-hosting/manage/custom-domain
NotFound: IAM nie znaleziony. Upewnij się, że masz poprawną domenę. Sprawdź https://zitadel.com/docs/apis/introduction#domains
Member:
RolesNotChanged: Role nie zmienione
MemberInvalid: Członek jest nieprawidłowy

View File

@ -326,7 +326,7 @@ Errors:
NotActive: A concessão do projeto não está ativa
NotInactive: A concessão do projeto não está inativa
IAM:
NotFound: Instância não encontrada. Confira https://zitadel.com/docs/self-hosting/manage/custom-domain
NotFound: IAM não encontrado. Verifique se você tem o domínio correto. Consulte https://zitadel.com/docs/apis/introduction#domains
Member:
RolesNotChanged: As funções não foram alteradas
MemberInvalid: O membro é inválido

View File

@ -321,7 +321,7 @@ Errors:
NotActive: Грант проекта не активен
NotInactive: Грант проекта не неактивен
IAM:
NotFound: Экземпляр не найден. Проверьте https://zitadel.com/docs/self-hosting/manage/custom-domain
NotFound: IAM не найден. Убедитесь, что у вас есть правильный домен. Смотрите на https://zitadel.com/docs/apis/introduction#domains
Member:
RolesNotChanged: Роли не изменились
MemberInvalid: Участник недействителен

View File

@ -328,7 +328,7 @@ Errors:
NotActive: 项目授权不是启用状态
NotInactive: 项目授权不是停用状态
IAM:
NotFound: 实例未找到。查看 https://zitadel.com/docs/self-hosting/manage/custom-domain
NotFound: IAM 未找到。确保您有正确的域。查看 https://zitadel.com/docs/apis/introduction#domains
Member:
RolesNotChanged: 角色没有改变
MemberInvalid: 成员无效