Catch assumption needed errors

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-02-09 16:08:08 +04:00
parent 25262ee6fb
commit ce69899909
8 changed files with 50 additions and 56 deletions

View File

@@ -1,5 +1,4 @@
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { AuthenticationRequiredError } from "@probo/relay";
import { IconPageCross } from "@probo/ui"; import { IconPageCross } from "@probo/ui";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { useLocation, useRouteError } from "react-router"; import { useLocation, useRouteError } from "react-router";
@@ -33,27 +32,6 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
} }
}, [location, resetErrorBoundary]); }, [location, resetErrorBoundary]);
useEffect(() => {
if (error instanceof AuthenticationRequiredError) {
window.location.href = error.redirectUrl;
}
}, [error]);
if (error instanceof AuthenticationRequiredError) {
return (
<div className={classNames.wrapper}>
<h1 className={classNames.title}>
{__("Additional authentication required")}
</h1>
<p className={classNames.description}>
{error.requiresSaml
? __("Redirecting to SAML authentication...")
: __("Redirecting to login...")}
</p>
</div>
);
}
if (!error || (error instanceof Error && error.message.includes("PAGE_NOT_FOUND"))) { if (!error || (error instanceof Error && error.message.includes("PAGE_NOT_FOUND"))) {
return ( return (
<div className={classNames.wrapper}> <div className={classNames.wrapper}>

View File

@@ -1,6 +1,7 @@
import { Role } from "@probo/helpers"; import { Role } from "@probo/helpers";
import { lazy } from "@probo/react-lazy"; import { lazy } from "@probo/react-lazy";
import { import {
NotAssumingError,
UnAuthenticatedError, UnAuthenticatedError,
} from "@probo/relay"; } from "@probo/relay";
import { type AppRoute, routeFromAppRoute } from "@probo/routes"; import { type AppRoute, routeFromAppRoute } from "@probo/routes";
@@ -47,6 +48,11 @@ function ErrorBoundary() {
return <Navigate to="/auth/login" />; return <Navigate to="/auth/login" />;
} }
if (error instanceof NotAssumingError) {
// TODO redirect to right URL
return <Navigate to="/" />;
}
return <PageError error={error instanceof Error ? error : new Error("unknown error")} />; return <PageError error={error instanceof Error ? error : new Error("unknown error")} />;
} }

View File

@@ -14,25 +14,11 @@ export class InternalServerError extends Error {
} }
} }
export class AuthenticationRequiredError extends Error { export class NotAssumingError extends Error {
public redirectUrl: string; constructor(message?: string) {
public requiresSaml: boolean; super(message ?? "NOT_ASSUMING");
public organizationId: string; this.name = "NotAssumingError";
public samlConfigId?: string; Object.setPrototypeOf(this, NotAssumingError.prototype)
constructor(extensions: {
redirectUrl: string;
requiresSaml: boolean;
organizationId: string;
samlConfigId?: string;
}) {
super("AUTHENTICATION_REQUIRED");
this.name = "AuthenticationRequiredError";
Object.setPrototypeOf(this, AuthenticationRequiredError.prototype);
this.redirectUrl = extensions.redirectUrl;
this.requiresSaml = extensions.requiresSaml;
this.organizationId = extensions.organizationId;
this.samlConfigId = extensions.samlConfigId;
} }
} }

View File

@@ -2,17 +2,17 @@ import { type FetchFunction } from "relay-runtime";
import { import {
InternalServerError, InternalServerError,
UnAuthenticatedError, UnAuthenticatedError,
AuthenticationRequiredError,
UnauthorizedError, UnauthorizedError,
ForbiddenError, ForbiddenError,
NotAssumingError,
} from "./errors"; } from "./errors";
import { GraphQLError } from "graphql"; import { GraphQLError } from "graphql";
const hasUnauthenticatedError = (error: GraphQLError) => const hasUnauthenticatedError = (error: GraphQLError) =>
error.extensions?.code == "UNAUTHENTICATED"; error.extensions?.code == "UNAUTHENTICATED";
const hasAuthenticationRequiredError = (error: GraphQLError) => const hasNotAssumingError = (error: GraphQLError) =>
error.extensions?.code == "AUTHENTICATION_REQUIRED"; error.extensions?.code == "NOT_ASSUMING";
const hasUnauthorizedError = (error: GraphQLError) => const hasUnauthorizedError = (error: GraphQLError) =>
error.extensions?.code == "UNAUTHORIZED"; error.extensions?.code == "UNAUTHORIZED";
@@ -83,17 +83,9 @@ export const makeFetchQuery = (endpoint: string): FetchFunction => {
throw new UnAuthenticatedError(unauthenticatedError.message); throw new UnAuthenticatedError(unauthenticatedError.message);
} }
const authRequiredError = errors.find(hasAuthenticationRequiredError); const notAssumingError = errors.find(hasNotAssumingError);
if (authRequiredError?.extensions) { if (notAssumingError) {
const { redirectUrl, requiresSaml, organizationId, samlConfigId } = throw new NotAssumingError(notAssumingError.message)
authRequiredError.extensions;
throw new AuthenticationRequiredError({
redirectUrl: redirectUrl as string,
requiresSaml: requiresSaml as boolean,
organizationId: organizationId as string,
samlConfigId: samlConfigId as string | undefined,
});
} }
const unauthorizedError = errors.find(hasUnauthorizedError); const unauthorizedError = errors.find(hasUnauthorizedError);

View File

@@ -99,7 +99,7 @@ func (a *Authorizer) authorize(ctx context.Context, conn pg.Conn, params Authori
var errSessionExpired *ErrSessionExpired var errSessionExpired *ErrSessionExpired
if errors.As(err, &errSessionNotFound) || errors.As(err, &errSessionExpired) { if errors.As(err, &errSessionNotFound) || errors.As(err, &errSessionExpired) {
return NewInsufficientPermissionsError(params.Principal, params.Resource, params.Action) return NewAssumptionNeededError(params.Principal, membership.ID)
} }
return fmt.Errorf("cannot get active child session for membership: %w", err) return fmt.Errorf("cannot get active child session for membership: %w", err)

View File

@@ -183,6 +183,19 @@ func (e ErrInsufficientPermissions) Error() string {
return fmt.Sprintf("identity %q does not have sufficient permissions to perform action %s on entity %q", e.IdentityID, e.Action, e.EntityID) return fmt.Sprintf("identity %q does not have sufficient permissions to perform action %s on entity %q", e.IdentityID, e.Action, e.EntityID)
} }
type ErrAssumptionNeeded struct {
IdentityID gid.GID
MembershipID gid.GID
}
func NewAssumptionNeededError(identityID gid.GID, membershipID gid.GID) error {
return &ErrAssumptionNeeded{IdentityID: identityID, MembershipID: membershipID}
}
func (e ErrAssumptionNeeded) Error() string {
return fmt.Sprintf("assumption for identity %q needed for membership %q", e.IdentityID, e.MembershipID)
}
type ErrSessionNotFound struct{ SessionID gid.GID } type ErrSessionNotFound struct{ SessionID gid.GID }
func NewSessionNotFoundError(sessionID gid.GID) error { func NewSessionNotFoundError(sessionID gid.GID) error {

View File

@@ -72,6 +72,11 @@ func NewAuthorizeFunc(
} }
if err := svc.Authorizer.Authorize(ctx, params); err != nil { if err := svc.Authorizer.Authorize(ctx, params); err != nil {
var errAssumptionNeeded *iam.ErrAssumptionNeeded
if errors.As(err, &errAssumptionNeeded) {
return gqlutils.NotAssuming(ctx, err)
}
var errInsufficientPermissions *iam.ErrInsufficientPermissions var errInsufficientPermissions *iam.ErrInsufficientPermissions
if errors.As(err, &errInsufficientPermissions) { if errors.As(err, &errInsufficientPermissions) {
return gqlutils.Forbidden(ctx, err) return gqlutils.Forbidden(ctx, err)

View File

@@ -52,6 +52,20 @@ func Unauthenticatedf(ctx context.Context, format string, a ...any) *gqlerror.Er
return Unauthenticated(ctx, fmt.Errorf(format, a...)) return Unauthenticated(ctx, fmt.Errorf(format, a...))
} }
func NotAssuming(ctx context.Context, err error) *gqlerror.Error {
return &gqlerror.Error{
Message: err.Error(),
Path: graphql.GetPath(ctx),
Extensions: map[string]any{
"code": "NOT_ASSUMING",
},
}
}
func NotAssumingf(ctx context.Context, format string, a ...any) *gqlerror.Error {
return NotAssuming(ctx, fmt.Errorf(format, a...))
}
func Forbidden(ctx context.Context, err error) *gqlerror.Error { func Forbidden(ctx context.Context, err error) *gqlerror.Error {
return &gqlerror.Error{ return &gqlerror.Error{
Message: err.Error(), Message: err.Error(),