Skip to content

Commit

Permalink
Merge pull request #1850 from dubinc/oauth-flow-disable-button
Browse files Browse the repository at this point in the history
Enhance OAuth flow role validation
  • Loading branch information
steven-tey authored Jan 2, 2025
2 parents db8f80a + 4e08574 commit ecccee4
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 18 deletions.
15 changes: 15 additions & 0 deletions apps/web/app/api/oauth/authorize/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DubApiError } from "@/lib/api/errors";
import { OAUTH_CONFIG } from "@/lib/api/oauth/constants";
import { createToken } from "@/lib/api/oauth/utils";
import { consolidateScopes, getScopesForRole } from "@/lib/api/tokens/scopes";
import { parseRequestBody } from "@/lib/api/utils";
import { withWorkspace } from "@/lib/auth";
import { authorizeRequestSchema } from "@/lib/zod/schemas/oauth";
Expand All @@ -18,6 +19,20 @@ export const POST = withWorkspace(async ({ session, req, workspace }) => {
code_challenge_method: codeChallengeMethod,
} = authorizeRequestSchema.parse(await parseRequestBody(req));

// Check if the user has the required scopes for the workspace selected
const userRole = workspace.users[0].role;
const scopesForRole = getScopesForRole(userRole);
const scopesMissing = consolidateScopes(scope).filter(
(scope) => !scopesForRole.includes(scope) && scope !== "user.read",
);

if (scopesMissing.length > 0) {
throw new DubApiError({
code: "bad_request",
message: "You don't have the permission to install this integration.",
});
}

const app = await prisma.oAuthApp.findUniqueOrThrow({
where: {
clientId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { consolidateScopes, getScopesForRole } from "@/lib/api/tokens/scopes";
import useWorkspaces from "@/lib/swr/use-workspaces";
import z from "@/lib/zod";
import { authorizeRequestSchema } from "@/lib/zod/schemas/oauth";
Expand Down Expand Up @@ -32,6 +33,9 @@ export const AuthorizeForm = ({
const [selectedWorkspace, setSelectedWorkspace] =
useState<InputSelectItemProps | null>(null);

// missing scopes for the user's role on the workspace selected
const [missingScopes, setMissingScopes] = useState<string[]>([]);

const selectOptions = useMemo(() => {
return workspaces
? workspaces.map((workspace) => ({
Expand All @@ -50,6 +54,29 @@ export const AuthorizeForm = ({
);
}, [selectOptions, session]);

// Check if the user has the required scopes for the workspace selected
useEffect(() => {
if (!selectedWorkspace) {
return;
}

const workspace = workspaces?.find(
(workspace) => workspace.slug === selectedWorkspace.id,
);

if (!workspace) {
return;
}

const userRole = workspace.users[0].role;
const scopesForRole = getScopesForRole(userRole);
const scopesMissing = consolidateScopes(scope).filter(
(scope) => !scopesForRole.includes(scope) && scope !== "user.read",
);

setMissingScopes(scopesMissing);
}, [selectedWorkspace]);

// Decline the request
const onDecline = () => {
const searchParams = new URLSearchParams({
Expand Down Expand Up @@ -148,7 +175,11 @@ export const AuthorizeForm = ({
loading={submitting}
disabled={!selectedWorkspace}
disabledTooltip={
!selectedWorkspace ? "Please select a workspace to continue" : ""
!selectedWorkspace
? "Please select a workspace to continue"
: missingScopes.length > 0
? "You don't have the permission to install this integration"
: undefined
}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { OAUTH_SCOPE_DESCRIPTIONS } from "@/lib/api/oauth/constants";
import { consolidateScopes } from "@/lib/api/tokens/scopes";
import { Check } from "lucide-react";

interface ScopesProps {
Expand Down Expand Up @@ -48,20 +49,3 @@ export const ScopesRequested = ({ scopes }: ScopesProps) => {
</>
);
};

// Consolidate scopes to avoid duplication and show only the most permissive scope
const consolidateScopes = (scopes: string[]) => {
const consolidated = new Set();

scopes.forEach((scope) => {
const [resource, action] = scope.split(".");

if (action === "write") {
consolidated.add(`${resource}.write`);
} else if (action === "read" && !consolidated.has(`${resource}.write`)) {
consolidated.add(`${resource}.read`);
}
});

return Array.from(consolidated) as string[];
};
22 changes: 22 additions & 0 deletions apps/web/lib/api/tokens/scopes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,25 @@ export const validateScopesForRole = (scopes: Scope[], role: Role) => {

return !(invalidScopes.length > 0);
};

// Get the scopes for a role
export const getScopesForRole = (role: Role) => {
return ROLE_SCOPES_MAP[role];
};

// Consolidate scopes to avoid duplication and show only the most permissive scope
export const consolidateScopes = (scopes: string[]) => {
const consolidated = new Set();

scopes.forEach((scope) => {
const [resource, action] = scope.split(".");

if (action === "write") {
consolidated.add(`${resource}.write`);
} else if (action === "read" && !consolidated.has(`${resource}.write`)) {
consolidated.add(`${resource}.read`);
}
});

return Array.from(consolidated) as string[];
};

0 comments on commit ecccee4

Please sign in to comment.