Rename USER to PROJECT in permissions (#576)

<!-- ELLIPSIS_HIDDEN -->


> [!IMPORTANT]
> Renamed user-related permissions to project-related permissions across
the codebase, affecting enums, schemas, APIs, models, and tests.
> 
>   - **Behavior**:
> - Renamed `USER` to `PROJECT` in `PermissionScope` enum in
`schema.prisma` and `migration.sql`.
> - Updated `isDefaultUserPermission` to `isDefaultProjectPermission` in
`schema.prisma` and `migration.sql`.
>     - Removed `jwks.json/route.ts` file.
>   - **API Changes**:
> - Renamed `user-permission-definitions` and `user-permissions`
endpoints to `project-permission-definitions` and `project-permissions`
in `route.tsx` files.
>     - Updated CRUD handlers in `crud.tsx` files to reflect new naming.
>   - **Models**:
> - Updated models in `permissions.tsx` to use `ProjectPermission` and
`AdminProjectPermission`.
>     - Updated `KnownErrors` to use `ProjectPermissionRequired`.
>   - **Tests**:
> - Renamed test files and updated test cases in
`e2e/tests/backend/endpoints/api/v1` to reflect new naming.
>   - **Misc**:
> - Updated `admin-app-impl.ts`, `client-app-impl.ts`, and
`server-app-impl.ts` to use new project permission naming.
>     - Updated `schema-fields.ts` to reflect new permission ID schema.
> 
> <sup>This description was created by </sup>[<img alt="Ellipsis"
src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup>
for 08924f5241. It will automatically
update as commits are pushed.</sup>


<!-- ELLIPSIS_HIDDEN -->

---------

Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
This commit is contained in:
CactusBlue 2025-03-27 09:39:48 -07:00 committed by GitHub
parent bba3859449
commit 793272c8c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 518 additions and 379 deletions

View File

@ -1,5 +1,20 @@
/*
Warnings:
- The values [USER] on the enum `PermissionScope` will be removed. If these variants are still used in the database, this will fail.
*/
-- AlterEnum
BEGIN;
CREATE TYPE "PermissionScope_new" AS ENUM ('PROJECT', 'TEAM');
ALTER TABLE "Permission" ALTER COLUMN "scope" TYPE "PermissionScope_new" USING ("scope"::text::"PermissionScope_new");
ALTER TYPE "PermissionScope" RENAME TO "PermissionScope_old";
ALTER TYPE "PermissionScope_new" RENAME TO "PermissionScope";
DROP TYPE "PermissionScope_old";
COMMIT;
-- AlterTable
ALTER TABLE "Permission" ADD COLUMN "isDefaultUserPermission" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "Permission" ADD COLUMN "isDefaultProjectPermission" BOOLEAN NOT NULL DEFAULT false;
-- CreateTable
CREATE TABLE "ProjectUserDirectPermission" (

View File

@ -224,14 +224,14 @@ model Permission {
isDefaultTeamCreatorPermission Boolean @default(false)
isDefaultTeamMemberPermission Boolean @default(false)
isDefaultUserPermission Boolean @default(false)
isDefaultProjectPermission Boolean @default(false)
@@unique([projectConfigId, queryableId])
@@unique([tenancyId, teamId, queryableId])
}
enum PermissionScope {
USER
PROJECT
TEAM
}

View File

@ -1,32 +0,0 @@
import { yupArray, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
import { getPublicJwkSet } from "@stackframe/stack-shared/dist/utils/jwt";
import { createSmartRouteHandler } from "../../../route-handlers/smart-route-handler";
export const GET = createSmartRouteHandler({
metadata: {
summary: "JWKS Endpoint",
description: "Returns information about the JSON Web Key Set (JWKS) used to sign and verify JWTs.",
tags: [],
hidden: true,
},
request: yupObject({}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({
keys: yupArray().defined(),
message: yupString().optional(),
}).defined(),
}),
async handler() {
return {
statusCode: 200,
bodyType: "json",
body: {
...await getPublicJwkSet(getEnvVariable("STACK_SERVER_SECRET")),
message: "This is deprecated. Please disable the legacy JWT signing in the tenancy setting page, and move to /api/v1/projects/<tenancy-id>/.well-known/jwks.json",
}
};
},
});

View File

@ -0,0 +1,4 @@
import { projectPermissionDefinitionsCrudHandlers } from "../crud";
export const PATCH = projectPermissionDefinitionsCrudHandlers.updateHandler;
export const DELETE = projectPermissionDefinitionsCrudHandlers.deleteHandler;

View File

@ -1,18 +1,18 @@
import { createPermissionDefinition, deletePermissionDefinition, listPermissionDefinitions, updatePermissionDefinitions } from "@/lib/permissions";
import { retryTransaction } from "@/prisma-client";
import { createCrudHandlers } from "@/route-handlers/crud-handler";
import { userPermissionDefinitionsCrud } from '@stackframe/stack-shared/dist/interface/crud/user-permissions';
import { teamPermissionDefinitionIdSchema, yupObject } from "@stackframe/stack-shared/dist/schema-fields";
import { projectPermissionDefinitionsCrud } from '@stackframe/stack-shared/dist/interface/crud/project-permissions';
import { permissionDefinitionIdSchema, yupObject } from "@stackframe/stack-shared/dist/schema-fields";
import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies";
export const userPermissionDefinitionsCrudHandlers = createLazyProxy(() => createCrudHandlers(userPermissionDefinitionsCrud, {
export const projectPermissionDefinitionsCrudHandlers = createLazyProxy(() => createCrudHandlers(projectPermissionDefinitionsCrud, {
paramsSchema: yupObject({
permission_id: teamPermissionDefinitionIdSchema.defined(),
permission_id: permissionDefinitionIdSchema.defined(),
}),
async onCreate({ auth, data }) {
return await retryTransaction(async (tx) => {
return await createPermissionDefinition(tx, {
scope: "USER",
scope: "PROJECT",
tenancy: auth.tenancy,
data,
});
@ -21,7 +21,7 @@ export const userPermissionDefinitionsCrudHandlers = createLazyProxy(() => creat
async onUpdate({ auth, data, params }) {
return await retryTransaction(async (tx) => {
return await updatePermissionDefinitions(tx, {
scope: "USER",
scope: "PROJECT",
tenancy: auth.tenancy,
permissionId: params.permission_id,
data,
@ -39,7 +39,7 @@ export const userPermissionDefinitionsCrudHandlers = createLazyProxy(() => creat
async onList({ auth }) {
return await retryTransaction(async (tx) => {
return {
items: await listPermissionDefinitions(tx, "USER", auth.tenancy),
items: await listPermissionDefinitions(tx, "PROJECT", auth.tenancy),
is_paginated: false,
};
});

View File

@ -0,0 +1,4 @@
import { projectPermissionDefinitionsCrudHandlers } from "./crud";
export const POST = projectPermissionDefinitionsCrudHandlers.createHandler;
export const GET = projectPermissionDefinitionsCrudHandlers.listHandler;

View File

@ -0,0 +1,4 @@
import { projectPermissionsCrudHandlers } from "../../crud";
export const POST = projectPermissionsCrudHandlers.createHandler;
export const DELETE = projectPermissionsCrudHandlers.deleteHandler;

View File

@ -1,37 +1,37 @@
import { grantUserPermission, listUserPermissions, revokeUserPermission } from "@/lib/permissions";
import { ensureUserExists, ensureUserPermissionExists } from "@/lib/request-checks";
import { sendUserPermissionCreatedWebhook, sendUserPermissionDeletedWebhook } from "@/lib/webhooks";
import { grantProjectPermission, listProjectPermissions, revokeProjectPermission } from "@/lib/permissions";
import { ensureUserExists, ensureProjectPermissionExists } from "@/lib/request-checks";
import { sendProjectPermissionCreatedWebhook, sendProjectPermissionDeletedWebhook } from "@/lib/webhooks";
import { retryTransaction } from "@/prisma-client";
import { createCrudHandlers } from "@/route-handlers/crud-handler";
import { runAsynchronouslyAndWaitUntil } from "@/utils/vercel";
import { KnownErrors } from "@stackframe/stack-shared";
import { userPermissionsCrud } from '@stackframe/stack-shared/dist/interface/crud/user-permissions';
import { teamPermissionDefinitionIdSchema, userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { projectPermissionsCrud } from '@stackframe/stack-shared/dist/interface/crud/project-permissions';
import { permissionDefinitionIdSchema, userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies";
export const userPermissionsCrudHandlers = createLazyProxy(() => createCrudHandlers(userPermissionsCrud, {
export const projectPermissionsCrudHandlers = createLazyProxy(() => createCrudHandlers(projectPermissionsCrud, {
querySchema: yupObject({
user_id: userIdOrMeSchema.optional().meta({ openapiField: { onlyShowInOperations: [ 'List' ], description: 'Filter with the user ID. If set, only the permissions this user has will be returned. Client request must set `user_id=me`', exampleValue: 'me' } }),
permission_id: teamPermissionDefinitionIdSchema.optional().meta({ openapiField: { onlyShowInOperations: [ 'List' ], description: 'Filter with the permission ID. If set, only the permissions with this specific ID will be returned', exampleValue: '16399452-c4f3-4554-8e44-c2d67bb60360' } }),
permission_id: permissionDefinitionIdSchema.optional().meta({ openapiField: { onlyShowInOperations: [ 'List' ], description: 'Filter with the permission ID. If set, only the permissions with this specific ID will be returned', exampleValue: '16399452-c4f3-4554-8e44-c2d67bb60360' } }),
recursive: yupString().oneOf(['true', 'false']).optional().meta({ openapiField: { onlyShowInOperations: [ 'List' ], description: 'Whether to list permissions recursively. If set to `false`, only the permission the users directly have will be listed. If set to `true` all the direct and indirect permissions will be listed.', exampleValue: 'true' } }),
}),
paramsSchema: yupObject({
user_id: userIdOrMeSchema.defined(),
permission_id: teamPermissionDefinitionIdSchema.defined(),
permission_id: permissionDefinitionIdSchema.defined(),
}),
async onCreate({ auth, params }) {
const result = await retryTransaction(async (tx) => {
await ensureUserExists(tx, { tenancyId: auth.tenancy.id, userId: params.user_id });
return await grantUserPermission(tx, {
return await grantProjectPermission(tx, {
tenancy: auth.tenancy,
userId: params.user_id,
permissionId: params.permission_id
});
});
runAsynchronouslyAndWaitUntil(sendUserPermissionCreatedWebhook({
runAsynchronouslyAndWaitUntil(sendProjectPermissionCreatedWebhook({
projectId: auth.project.id,
data: {
id: params.permission_id,
@ -43,7 +43,7 @@ export const userPermissionsCrudHandlers = createLazyProxy(() => createCrudHandl
},
async onDelete({ auth, params }) {
const result = await retryTransaction(async (tx) => {
await ensureUserPermissionExists(tx, {
await ensureProjectPermissionExists(tx, {
tenancy: auth.tenancy,
userId: params.user_id,
permissionId: params.permission_id,
@ -51,14 +51,14 @@ export const userPermissionsCrudHandlers = createLazyProxy(() => createCrudHandl
recursive: false,
});
return await revokeUserPermission(tx, {
return await revokeProjectPermission(tx, {
tenancy: auth.tenancy,
userId: params.user_id,
permissionId: params.permission_id
});
});
runAsynchronouslyAndWaitUntil(sendUserPermissionDeletedWebhook({
runAsynchronouslyAndWaitUntil(sendProjectPermissionDeletedWebhook({
projectId: auth.project.id,
data: {
id: params.permission_id,
@ -79,7 +79,7 @@ export const userPermissionsCrudHandlers = createLazyProxy(() => createCrudHandl
return await retryTransaction(async (tx) => {
return {
items: await listUserPermissions(tx, {
items: await listProjectPermissions(tx, {
tenancy: auth.tenancy,
permissionId: query.permission_id,
userId: query.user_id,

View File

@ -0,0 +1,3 @@
import { projectPermissionsCrudHandlers } from "./crud";
export const GET = projectPermissionsCrudHandlers.listHandler;

View File

@ -34,27 +34,27 @@ export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(pro
] as const;
const teamPermissions = await listPermissionDefinitions(tx, "TEAM", auth.tenancy);
const userPermissions = await listPermissionDefinitions(tx, "USER", auth.tenancy);
const projectPermissions = await listPermissionDefinitions(tx, "PROJECT", auth.tenancy);
// Handle user default permissions
const userDefaultPerms = data.config?.user_default_permissions?.map((p) => p.id);
if (userDefaultPerms) {
if (!userDefaultPerms.every((id) => userPermissions.some((perm) => perm.id === id))) {
if (!userDefaultPerms.every((id) => projectPermissions.some((perm) => perm.id === id))) {
throw new StatusError(StatusError.BadRequest,
`Invalid user default permission ids: ${userDefaultPerms.filter(id => !userPermissions.some(perm => perm.id === id)).join(', ')}`);
`Invalid user default permission ids: ${userDefaultPerms.filter(id => !projectPermissions.some(perm => perm.id === id)).join(', ')}`);
}
// Remove existing default user permissions
// Remove existing default project permissions
await tx.permission.updateMany({
where: {
projectConfigId: oldProject.config.id,
},
data: {
isDefaultUserPermission: false,
isDefaultProjectPermission: false,
},
});
// Add new default user permissions
// Add new default project permissions
await tx.permission.updateMany({
where: {
projectConfigId: oldProject.config.id,
@ -63,7 +63,7 @@ export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(pro
},
},
data: {
isDefaultUserPermission: true,
isDefaultProjectPermission: true,
},
});
}

View File

@ -2,12 +2,12 @@ import { createPermissionDefinition, deletePermissionDefinition, listPermissionD
import { retryTransaction } from "@/prisma-client";
import { createCrudHandlers } from "@/route-handlers/crud-handler";
import { teamPermissionDefinitionsCrud } from '@stackframe/stack-shared/dist/interface/crud/team-permissions';
import { teamPermissionDefinitionIdSchema, yupObject } from "@stackframe/stack-shared/dist/schema-fields";
import { permissionDefinitionIdSchema, yupObject } from "@stackframe/stack-shared/dist/schema-fields";
import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies";
export const teamPermissionDefinitionsCrudHandlers = createLazyProxy(() => createCrudHandlers(teamPermissionDefinitionsCrud, {
paramsSchema: yupObject({
permission_id: teamPermissionDefinitionIdSchema.defined(),
permission_id: permissionDefinitionIdSchema.defined(),
}),
async onCreate({ auth, data }) {
return await retryTransaction(async (tx) => {

View File

@ -6,7 +6,7 @@ import { createCrudHandlers } from "@/route-handlers/crud-handler";
import { runAsynchronouslyAndWaitUntil } from "@/utils/vercel";
import { KnownErrors } from "@stackframe/stack-shared";
import { teamPermissionsCrud } from '@stackframe/stack-shared/dist/interface/crud/team-permissions';
import { teamPermissionDefinitionIdSchema, userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { permissionDefinitionIdSchema, userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies";
@ -14,13 +14,13 @@ export const teamPermissionsCrudHandlers = createLazyProxy(() => createCrudHandl
querySchema: yupObject({
team_id: yupString().uuid().optional().meta({ openapiField: { onlyShowInOperations: [ 'List' ], description: 'Filter with the team ID. If set, only the permissions of the members in a specific team will be returned.', exampleValue: 'cce084a3-28b7-418e-913e-c8ee6d802ea4' } }),
user_id: userIdOrMeSchema.optional().meta({ openapiField: { onlyShowInOperations: [ 'List' ], description: 'Filter with the user ID. If set, only the permissions this user has will be returned. Client request must set `user_id=me`', exampleValue: 'me' } }),
permission_id: teamPermissionDefinitionIdSchema.optional().meta({ openapiField: { onlyShowInOperations: [ 'List' ], description: 'Filter with the permission ID. If set, only the permissions with this specific ID will be returned', exampleValue: '16399452-c4f3-4554-8e44-c2d67bb60360' } }),
permission_id: permissionDefinitionIdSchema.optional().meta({ openapiField: { onlyShowInOperations: [ 'List' ], description: 'Filter with the permission ID. If set, only the permissions with this specific ID will be returned', exampleValue: '16399452-c4f3-4554-8e44-c2d67bb60360' } }),
recursive: yupString().oneOf(['true', 'false']).optional().meta({ openapiField: { onlyShowInOperations: [ 'List' ], description: 'Whether to list permissions recursively. If set to `false`, only the permission the users directly have will be listed. If set to `true` all the direct and indirect permissions will be listed.', exampleValue: 'true' } }),
}),
paramsSchema: yupObject({
team_id: yupString().uuid().defined(),
user_id: userIdOrMeSchema.defined(),
permission_id: teamPermissionDefinitionIdSchema.defined(),
permission_id: permissionDefinitionIdSchema.defined(),
}),
async onCreate({ auth, params }) {
const result = await retryTransaction(async (tx) => {

View File

@ -1,4 +0,0 @@
import { userPermissionDefinitionsCrudHandlers } from "../crud";
export const PATCH = userPermissionDefinitionsCrudHandlers.updateHandler;
export const DELETE = userPermissionDefinitionsCrudHandlers.deleteHandler;

View File

@ -1,4 +0,0 @@
import { userPermissionDefinitionsCrudHandlers } from "./crud";
export const POST = userPermissionDefinitionsCrudHandlers.createHandler;
export const GET = userPermissionDefinitionsCrudHandlers.listHandler;

View File

@ -1,4 +0,0 @@
import { userPermissionsCrudHandlers } from "../../crud";
export const POST = userPermissionsCrudHandlers.createHandler;
export const DELETE = userPermissionsCrudHandlers.deleteHandler;

View File

@ -1,3 +0,0 @@
import { userPermissionsCrudHandlers } from "./crud";
export const GET = userPermissionsCrudHandlers.listHandler;

View File

@ -1,5 +1,5 @@
import { grantDefaultProjectPermissions } from "@/lib/permissions";
import { ensureTeamMembershipExists, ensureUserExists } from "@/lib/request-checks";
import { grantDefaultUserPermissions } from "@/lib/permissions";
import { getSoleTenancyFromProject, getTenancy } from "@/lib/tenancies";
import { PrismaTransaction } from "@/lib/types";
import { sendTeamMembershipDeletedWebhook, sendUserCreatedWebhook, sendUserDeletedWebhook, sendUserUpdatedWebhook } from "@/lib/webhooks";
@ -712,7 +712,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
}
// Grant default user permissions
await grantDefaultUserPermissions(tx, {
await grantDefaultProjectPermissions(tx, {
tenancy: auth.tenancy,
userId: newUser.projectUserId
});

View File

@ -1,7 +1,7 @@
import { TeamSystemPermission as DBTeamSystemPermission, Prisma } from "@prisma/client";
import { KnownErrors } from "@stackframe/stack-shared";
import { TeamPermissionDefinitionsCrud, TeamPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/team-permissions";
import { UserPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/user-permissions";
import { ProjectPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/project-permissions";
import { groupBy } from "@stackframe/stack-shared/dist/utils/arrays";
import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
import { stringCompare, typedToLowercase, typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings";
@ -42,7 +42,7 @@ type ExtendedTeamPermissionDefinition = TeamPermissionDefinitionsCrud["Admin"]["
__database_id: string,
__is_default_team_member_permission?: boolean,
__is_default_team_creator_permission?: boolean,
__is_default_user_permission?: boolean,
__is_default_project_permission?: boolean,
};
export function teamPermissionDefinitionJsonFromDbType(db: Prisma.PermissionGetPayload<{ include: typeof fullPermissionInclude }>): ExtendedTeamPermissionDefinition {
@ -54,13 +54,13 @@ export function teamPermissionDefinitionJsonFromDbType(db: Prisma.PermissionGetP
export function teamPermissionDefinitionJsonFromRawDbType(db: any | Prisma.PermissionGetPayload<{ include: typeof fullPermissionInclude }>): ExtendedTeamPermissionDefinition {
if (!db.projectConfigId && !db.teamId) throw new StackAssertionError(`Permission DB object should have either projectConfigId or teamId`, { db });
if (db.projectConfigId && db.teamId) throw new StackAssertionError(`Permission DB object should have either projectConfigId or teamId, not both`, { db });
if (db.scope === "USER" && db.teamId) throw new StackAssertionError(`Permission DB object should not have teamId when scope is USER`, { db });
if (db.scope === "PROJECT" && db.teamId) throw new StackAssertionError(`Permission DB object should not have teamId when scope is PROJECT`, { db });
return {
__database_id: db.dbId,
__is_default_team_member_permission: db.isDefaultTeamMemberPermission,
__is_default_team_creator_permission: db.isDefaultTeamCreatorPermission,
__is_default_user_permission: db.isDefaultUserPermission,
__is_default_project_permission: db.isDefaultProjectPermission,
id: db.queryableId,
description: db.description || undefined,
contained_permission_ids: db.parentEdges?.map((edge: any) => {
@ -78,7 +78,7 @@ export function teamPermissionDefinitionJsonFromRawDbType(db: any | Prisma.Permi
export function teamPermissionDefinitionJsonFromTeamSystemDbType(db: DBTeamSystemPermission, projectConfig: {
teamCreateDefaultSystemPermissions: string[] | null,
teamMemberDefaultSystemPermissions: string[] | null,
userDefaultPermissions?: string[] | null,
projectDefaultPermissions?: string[] | null,
}): ExtendedTeamPermissionDefinition {
if ((["teamMemberDefaultSystemPermissions", "teamCreateDefaultSystemPermissions"] as const).some(key => projectConfig[key] !== null && !Array.isArray(projectConfig[key]))) {
throw new StackAssertionError(`Project config should have (nullable) array values for teamMemberDefaultSystemPermissions and teamCreateDefaultSystemPermissions`, { projectConfig });
@ -88,7 +88,7 @@ export function teamPermissionDefinitionJsonFromTeamSystemDbType(db: DBTeamSyste
__database_id: '$' + typedToLowercase(db),
__is_default_team_member_permission: projectConfig.teamMemberDefaultSystemPermissions?.includes(db) ?? false,
__is_default_team_creator_permission: projectConfig.teamCreateDefaultSystemPermissions?.includes(db) ?? false,
__is_default_user_permission: projectConfig.userDefaultPermissions?.includes(db) ?? false,
__is_default_project_permission: projectConfig.projectDefaultPermissions?.includes(db) ?? false,
id: '$' + typedToLowercase(db),
description: descriptionMap[db],
contained_permission_ids: [] as string[],
@ -99,7 +99,7 @@ async function getParentDbIds(
tx: PrismaTransaction,
options: {
tenancy: Tenancy,
scope: "TEAM" | "USER",
scope: "TEAM" | "PROJECT",
containedPermissionIds?: string[],
}
) {
@ -324,7 +324,7 @@ export async function revokeTeamPermission(
export async function listPermissionDefinitions(
tx: PrismaTransaction,
scope: "TEAM" | "USER",
scope: "TEAM" | "PROJECT",
tenancy: Tenancy
): Promise<(TeamPermissionDefinitionsCrud["Admin"]["Read"] & { __database_id: string })[]> {
const projectConfig = await tx.projectConfig.findUnique({
@ -356,7 +356,7 @@ export async function listPermissionDefinitions(
export async function createPermissionDefinition(
tx: PrismaTransaction,
options: {
scope: "TEAM" | "USER",
scope: "TEAM" | "PROJECT",
tenancy: Tenancy,
data: {
id: string,
@ -402,7 +402,7 @@ export async function createPermissionDefinition(
export async function updatePermissionDefinitions(
tx: PrismaTransaction,
options: {
scope: "TEAM" | "USER",
scope: "TEAM" | "PROJECT",
tenancy: Tenancy,
permissionId: string,
data: {
@ -477,7 +477,7 @@ export async function deletePermissionDefinition(
// User permission functions
export async function listUserPermissions(
export async function listProjectPermissions(
tx: PrismaTransaction,
options: {
tenancy: Tenancy,
@ -485,8 +485,8 @@ export async function listUserPermissions(
permissionId?: string,
recursive: boolean,
}
): Promise<UserPermissionsCrud["Admin"]["Read"][]> {
const permissionDefs = await listPermissionDefinitions(tx, "USER", options.tenancy);
): Promise<ProjectPermissionsCrud["Admin"]["Read"][]> {
const permissionDefs = await listPermissionDefinitions(tx, "PROJECT", options.tenancy);
const permissionsMap = new Map(permissionDefs.map(p => [p.id, p]));
const results = await tx.projectUserDirectPermission.findMany({
where: {
@ -528,7 +528,7 @@ export async function listUserPermissions(
.filter(p => options.permissionId ? p.id === options.permissionId : true);
}
export async function grantUserPermission(
export async function grantProjectPermission(
tx: PrismaTransaction,
options: {
tenancy: Tenancy,
@ -579,7 +579,7 @@ export async function grantUserPermission(
};
}
export async function revokeUserPermission(
export async function revokeProjectPermission(
tx: PrismaTransaction,
options: {
tenancy: Tenancy,
@ -610,10 +610,10 @@ export async function revokeUserPermission(
}
/**
* Grants default user permissions to a user
* Grants default project permissions to a user
* This function should be called when a new user is created
*/
export async function grantDefaultUserPermissions(
export async function grantDefaultProjectPermissions(
tx: PrismaTransaction,
options: {
tenancy: Tenancy,
@ -623,7 +623,7 @@ export async function grantDefaultUserPermissions(
const defaultPermissions = await tx.permission.findMany({
where: {
projectConfigId: options.tenancy.config.id,
isDefaultUserPermission: true,
isDefaultProjectPermission: true,
}
});

View File

@ -173,7 +173,7 @@ export function projectPrismaToCrud(
.concat(prisma.config.teamMemberDefaultSystemPermissions.map(db => teamPermissionDefinitionJsonFromTeamSystemDbType(db, prisma.config)))
.sort((a, b) => stringCompare(a.id, b.id))
.map(perm => ({ id: perm.id })),
user_default_permissions: prisma.config.permissions.filter(perm => perm.isDefaultUserPermission)
user_default_permissions: prisma.config.permissions.filter(perm => perm.isDefaultProjectPermission)
.map(teamPermissionDefinitionJsonFromDbType)
.sort((a, b) => stringCompare(a.id, b.id))
.map(perm => ({ id: perm.id })),
@ -464,7 +464,7 @@ export function getProjectQuery(projectId: string): RawQuery<ProjectsCrud["Admin
.filter(perm => perm.__is_default_team_member_permission)
.map(perm => ({ id: perm.id })),
user_default_permissions: teamPermissions
.filter(perm => perm.__is_default_user_permission)
.filter(perm => perm.__is_default_project_permission)
.map(perm => ({ id: perm.id })),
},
};

View File

@ -3,7 +3,7 @@ import { KnownErrors } from "@stackframe/stack-shared";
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
import { ProviderType, sharedProviders, standardProviders } from "@stackframe/stack-shared/dist/utils/oauth";
import { typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings";
import { listUserPermissions, listUserTeamPermissions } from "./permissions";
import { listProjectPermissions, listUserTeamPermissions } from "./permissions";
import { Tenancy } from "./tenancies";
import { PrismaTransaction } from "./types";
@ -113,7 +113,7 @@ export async function ensureUserTeamPermissionExists(
}
}
export async function ensureUserPermissionExists(
export async function ensureProjectPermissionExists(
tx: PrismaTransaction,
options: {
tenancy: Tenancy,
@ -128,7 +128,7 @@ export async function ensureUserPermissionExists(
userId: options.userId,
});
const result = await listUserPermissions(tx, {
const result = await listProjectPermissions(tx, {
tenancy: options.tenancy,
userId: options.userId,
permissionId: options.permissionId,
@ -139,7 +139,7 @@ export async function ensureUserPermissionExists(
if (options.errorType === 'not-exist') {
throw new KnownErrors.PermissionNotFound(options.permissionId);
} else {
throw new KnownErrors.UserPermissionRequired(options.userId, options.permissionId);
throw new KnownErrors.ProjectPermissionRequired(options.userId, options.permissionId);
}
}
}

View File

@ -1,8 +1,8 @@
import { projectPermissionCreatedWebhookEvent, projectPermissionDeletedWebhookEvent } from "@stackframe/stack-shared/dist/interface/crud/project-permissions";
import { teamMembershipCreatedWebhookEvent, teamMembershipDeletedWebhookEvent } from "@stackframe/stack-shared/dist/interface/crud/team-memberships";
import { teamPermissionCreatedWebhookEvent, teamPermissionDeletedWebhookEvent } from "@stackframe/stack-shared/dist/interface/crud/team-permissions";
import { teamCreatedWebhookEvent, teamDeletedWebhookEvent, teamUpdatedWebhookEvent } from "@stackframe/stack-shared/dist/interface/crud/teams";
import { userCreatedWebhookEvent, userDeletedWebhookEvent, userUpdatedWebhookEvent } from "@stackframe/stack-shared/dist/interface/crud/users";
import { userPermissionCreatedWebhookEvent, userPermissionDeletedWebhookEvent } from "@stackframe/stack-shared/dist/interface/crud/user-permissions";
import { WebhookEvent } from "@stackframe/stack-shared/dist/interface/webhooks";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
import { StackAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors";
@ -74,5 +74,5 @@ export const sendTeamMembershipCreatedWebhook = createWebhookSender(teamMembersh
export const sendTeamMembershipDeletedWebhook = createWebhookSender(teamMembershipDeletedWebhookEvent);
export const sendTeamPermissionCreatedWebhook = createWebhookSender(teamPermissionCreatedWebhookEvent);
export const sendTeamPermissionDeletedWebhook = createWebhookSender(teamPermissionDeletedWebhookEvent);
export const sendUserPermissionCreatedWebhook = createWebhookSender(userPermissionCreatedWebhookEvent);
export const sendUserPermissionDeletedWebhook = createWebhookSender(userPermissionDeletedWebhookEvent);
export const sendProjectPermissionCreatedWebhook = createWebhookSender(projectPermissionCreatedWebhookEvent);
export const sendProjectPermissionDeletedWebhook = createWebhookSender(projectPermissionDeletedWebhookEvent);

View File

@ -10,12 +10,12 @@ import { useAdminApp } from "../use-admin-app";
export default function PageClient() {
const stackAdminApp = useAdminApp();
const permissions = stackAdminApp.useUserPermissionDefinitions();
const permissions = stackAdminApp.useProjectPermissionDefinitions();
const [createPermissionModalOpen, setCreatePermissionModalOpen] = React.useState(false);
return (
<PageLayout
title="User Permissions"
title="Project Permissions"
actions={
<Button onClick={() => setCreatePermissionModalOpen(true)}>
Create Permission
@ -24,7 +24,7 @@ export default function PageClient() {
<PermissionTable
permissions={permissions}
permissionType="user"
permissionType="project"
/>
<CreateDialog
@ -40,7 +40,7 @@ function CreateDialog(props: {
onOpenChange: (open: boolean) => void,
}) {
const stackAdminApp = useAdminApp();
const permissions = stackAdminApp.useUserPermissionDefinitions();
const permissions = stackAdminApp.useProjectPermissionDefinitions();
const formSchema = yup.object({
id: yup.string().defined()
@ -62,7 +62,7 @@ function CreateDialog(props: {
formSchema={formSchema}
okButton={{ label: "Create" }}
onSubmit={async (values) => {
await stackAdminApp.createUserPermissionDefinition({
await stackAdminApp.createProjectPermissionDefinition({
id: values.id,
description: values.description,
containedPermissionIds: values.containedPermissionIds,

View File

@ -112,9 +112,9 @@ const navigationItems: (Label | Item | Hidden)[] = [
type: 'item'
},
{
name: "User Permissions",
href: "/user-permissions",
regex: /^\/projects\/[^\/]+\/user-permissions$/,
name: "Project Permissions",
href: "/project-permissions",
regex: /^\/projects\/[^\/]+\/project-permissions$/,
icon: LockKeyhole,
type: 'item'
},

View File

@ -14,7 +14,7 @@ type AdminPermissionDefinition = {
containedPermissionIds: string[],
};
type PermissionType = 'user' | 'team';
type PermissionType = 'project' | 'team';
function toolbarRender<TData>(table: Table<TData>) {
return (
@ -31,8 +31,8 @@ function EditDialog(props: {
permissionType: PermissionType,
}) {
const stackAdminApp = useAdminApp();
const permissions = props.permissionType === 'user'
? stackAdminApp.useUserPermissionDefinitions()
const permissions = props.permissionType === 'project'
? stackAdminApp.useProjectPermissionDefinitions()
: stackAdminApp.useTeamPermissionDefinitions();
const currentPermission = permissions.find((p) => p.id === props.selectedPermissionId);
if (!currentPermission) {
@ -62,10 +62,6 @@ function EditDialog(props: {
})
}).default(currentPermission);
const updatePermission = props.permissionType === 'user'
? stackAdminApp.updateUserPermissionDefinition
: stackAdminApp.updateTeamPermissionDefinition;
return <SmartFormDialog
open={props.open}
onOpenChange={props.onOpenChange}
@ -74,7 +70,11 @@ function EditDialog(props: {
okButton={{ label: "Save" }}
onSubmit={(values) => {
runAsynchronously(async () => {
await updatePermission(props.selectedPermissionId, values);
if (props.permissionType === 'project') {
await stackAdminApp.updateProjectPermissionDefinition(props.selectedPermissionId, values);
} else {
await stackAdminApp.updateTeamPermissionDefinition(props.selectedPermissionId, values);
}
});
}}
cancelButton
@ -87,10 +87,7 @@ function DeleteDialog<T extends AdminPermissionDefinition>(props: {
onOpenChange: (open: boolean) => void,
permissionType: PermissionType,
}) {
const stackApp = useAdminApp();
const deletePermission = props.permissionType === 'user'
? stackApp.deleteUserPermissionDefinition
: stackApp.deleteTeamPermissionDefinition;
const stackAdminApp = useAdminApp();
return <ActionDialog
open={props.open}
@ -98,7 +95,13 @@ function DeleteDialog<T extends AdminPermissionDefinition>(props: {
title="Delete Permission"
danger
cancelButton
okButton={{ label: "Delete Permission", onClick: async () => { await deletePermission(props.permission.id); } }}
okButton={{ label: "Delete Permission", onClick: async () => {
if (props.permissionType === 'project') {
await stackAdminApp.deleteProjectPermissionDefinition(props.permission.id);
} else {
await stackAdminApp.deleteTeamPermissionDefinition(props.permission.id);
}
} }}
confirmText="I understand this will remove the permission from all users and other permissions that contain it."
>
{`Are you sure you want to delete the permission "${props.permission.id}"?`}

View File

@ -6,7 +6,7 @@ it("lists all the user permissions", async ({ expect }) => {
backendContext.set({ projectKeys: InternalProjectKeys });
const { adminAccessToken } = await Project.createAndGetAdminToken();
const response = await niceBackendFetch(`/api/v1/user-permission-definitions`, {
const response = await niceBackendFetch(`/api/v1/project-permission-definitions`, {
accessType: "admin",
method: "GET",
headers: {
@ -29,7 +29,7 @@ it("creates, updates, and deletes a new user permission", async ({ expect }) =>
backendContext.set({ projectKeys: InternalProjectKeys });
const { adminAccessToken } = await Project.createAndGetAdminToken();
const response1 = await niceBackendFetch(`/api/v1/user-permission-definitions`, {
const response1 = await niceBackendFetch(`/api/v1/project-permission-definitions`, {
accessType: "admin",
method: "POST",
body: {
@ -51,7 +51,7 @@ it("creates, updates, and deletes a new user permission", async ({ expect }) =>
`);
// create another permission with contained permissions
const response2 = await niceBackendFetch(`/api/v1/user-permission-definitions`, {
const response2 = await niceBackendFetch(`/api/v1/project-permission-definitions`, {
accessType: "admin",
method: "POST",
body: {
@ -74,7 +74,7 @@ it("creates, updates, and deletes a new user permission", async ({ expect }) =>
`);
// test recursive case
const response3 = await niceBackendFetch(`/api/v1/user-permission-definitions`, {
const response3 = await niceBackendFetch(`/api/v1/project-permission-definitions`, {
accessType: "admin",
method: "POST",
body: {
@ -98,7 +98,7 @@ it("creates, updates, and deletes a new user permission", async ({ expect }) =>
`);
// list all permissions again
const response4 = await niceBackendFetch(`/api/v1/user-permission-definitions`, {
const response4 = await niceBackendFetch(`/api/v1/project-permission-definitions`, {
accessType: "admin",
method: "GET",
headers: {
@ -130,7 +130,7 @@ it("creates, updates, and deletes a new user permission", async ({ expect }) =>
`);
// delete the permission
const response5 = await niceBackendFetch(`/api/v1/user-permission-definitions/p1`, {
const response5 = await niceBackendFetch(`/api/v1/project-permission-definitions/p1`, {
accessType: "admin",
method: "DELETE",
headers: {
@ -146,7 +146,7 @@ it("creates, updates, and deletes a new user permission", async ({ expect }) =>
`);
// list all permissions again
const response6 = await niceBackendFetch(`/api/v1/user-permission-definitions`, {
const response6 = await niceBackendFetch(`/api/v1/project-permission-definitions`, {
accessType: "admin",
method: "GET",
headers: {

View File

@ -1,11 +1,11 @@
import { wait } from "@stackframe/stack-shared/dist/utils/promises";
import { it } from "../../../../helpers";
import { ApiKey, Auth, InternalProjectKeys, Project, Team, Webhook, backendContext, niceBackendFetch } from "../../../backend-helpers";
import { ApiKey, Auth, InternalProjectKeys, Project, Webhook, backendContext, niceBackendFetch } from "../../../backend-helpers";
it("is not allowed to list permissions from the other users on the client", async ({ expect }) => {
await Auth.Otp.signIn();
const response = await niceBackendFetch(`/api/v1/user-permissions`, {
const response = await niceBackendFetch(`/api/v1/project-permissions`, {
accessType: "client",
method: "GET",
});
@ -21,7 +21,7 @@ it("is not allowed to list permissions from the other users on the client", asyn
it("is not allowed to grant non-existing permission to a user on the server", async ({ expect }) => {
const { userId } = await Auth.Otp.signIn();
const response = await niceBackendFetch(`/api/v1/user-permissions/${userId}/does_not_exist`, {
const response = await niceBackendFetch(`/api/v1/project-permissions/${userId}/does_not_exist`, {
accessType: "server",
method: "POST",
body: {},
@ -47,7 +47,7 @@ it("can create a new permission and grant it to a user on the server", async ({
const { adminAccessToken } = await Project.createAndGetAdminToken({ config: { magic_link_enabled: true } });
// create a permission child
await niceBackendFetch(`/api/v1/user-permission-definitions`, {
await niceBackendFetch(`/api/v1/project-permission-definitions`, {
accessType: "admin",
method: "POST",
body: {
@ -60,7 +60,7 @@ it("can create a new permission and grant it to a user on the server", async ({
});
// create a permission parent
await niceBackendFetch(`/api/v1/user-permission-definitions`, {
await niceBackendFetch(`/api/v1/project-permission-definitions`, {
accessType: "admin",
method: "POST",
body: {
@ -78,7 +78,7 @@ it("can create a new permission and grant it to a user on the server", async ({
const { userId } = await Auth.Password.signUpWithEmail({ password: 'test1234' });
// list current permissions
const response1 = await niceBackendFetch(`/api/v1/user-permissions?user_id=me`, {
const response1 = await niceBackendFetch(`/api/v1/project-permissions?user_id=me`, {
accessType: "client",
method: "GET",
});
@ -94,7 +94,7 @@ it("can create a new permission and grant it to a user on the server", async ({
`);
// grant new permission
const response2 = await niceBackendFetch(`/api/v1/user-permissions/${userId}/parent`, {
const response2 = await niceBackendFetch(`/api/v1/project-permissions/${userId}/parent`, {
accessType: "server",
method: "POST",
body: {},
@ -111,7 +111,7 @@ it("can create a new permission and grant it to a user on the server", async ({
`);
// list current permissions (should have the new permission)
const response3 = await niceBackendFetch(`/api/v1/user-permissions?user_id=me`, {
const response3 = await niceBackendFetch(`/api/v1/project-permissions?user_id=me`, {
accessType: "client",
method: "GET",
});
@ -136,7 +136,7 @@ it("can customize default user permissions", async ({ expect }) => {
await Auth.Otp.signIn();
const { adminAccessToken } = await Project.createAndGetAdminToken();
const response1 = await niceBackendFetch(`/api/v1/user-permission-definitions`, {
const response1 = await niceBackendFetch(`/api/v1/project-permission-definitions`, {
accessType: "admin",
method: "POST",
body: {
@ -202,7 +202,7 @@ it("can customize default user permissions", async ({ expect }) => {
// sign up a new user
const { userId } = await Auth.Password.signUpWithEmail({ password: 'test1234' });
// list permissions for the new user
const response3 = await niceBackendFetch(`/api/v1/user-permissions?user_id=${userId}`, {
const response3 = await niceBackendFetch(`/api/v1/project-permissions?user_id=${userId}`, {
accessType: "client",
method: "GET",
});
@ -223,18 +223,18 @@ it("can customize default user permissions", async ({ expect }) => {
`);
});
it("should trigger user permission webhook when a permission is granted to a user", async ({ expect }) => {
it("should trigger project permission webhook when a permission is granted to a user", async ({ expect }) => {
const { projectId, svixToken, endpointId } = await Webhook.createProjectWithEndpoint();
const { userId } = await Auth.Otp.signIn();
await niceBackendFetch(`/api/v1/user-permission-definitions`, {
await niceBackendFetch(`/api/v1/project-permission-definitions`, {
accessType: "admin",
method: "POST",
body: { id: 'test_permission' },
});
const grantPermissionResponse = await niceBackendFetch(`/api/v1/user-permissions/${userId}/test_permission`, {
const grantPermissionResponse = await niceBackendFetch(`/api/v1/project-permissions/${userId}/test_permission`, {
accessType: "server",
method: "POST",
body: {},
@ -245,39 +245,39 @@ it("should trigger user permission webhook when a permission is granted to a use
await wait(3000);
const attemptResponse = await Webhook.listWebhookAttempts(projectId, endpointId, svixToken);
const userPermissionCreatedEvent = attemptResponse.find(event => event.eventType === "user_permission.created");
const projectPermissionCreatedEvent = attemptResponse.find(event => event.eventType === "project_permission.created");
expect(userPermissionCreatedEvent).toMatchInlineSnapshot(`
expect(projectPermissionCreatedEvent).toMatchInlineSnapshot(`
{
"channels": null,
"eventId": null,
"eventType": "user_permission.created",
"eventType": "project_permission.created",
"id": "<stripped svix message id>",
"payload": {
"data": {
"id": "test_permission",
"user_id": "<stripped UUID>",
},
"type": "user_permission.created",
"type": "project_permission.created",
},
"timestamp": <stripped field 'timestamp'>,
}
`);
});
it("should trigger user permission webhook when a permission is revoked from a user", async ({ expect }) => {
it("should trigger project permission webhook when a permission is revoked from a user", async ({ expect }) => {
const { projectId, svixToken, endpointId } = await Webhook.createProjectWithEndpoint();
const { userId } = await Auth.Otp.signIn();
await niceBackendFetch(`/api/v1/user-permission-definitions`, {
await niceBackendFetch(`/api/v1/project-permission-definitions`, {
accessType: "admin",
method: "POST",
body: { id: 'test_permission' },
});
// First grant the permission
const grantPermissionResponse = await niceBackendFetch(`/api/v1/user-permissions/${userId}/test_permission`, {
const grantPermissionResponse = await niceBackendFetch(`/api/v1/project-permissions/${userId}/test_permission`, {
accessType: "server",
method: "POST",
body: {},
@ -286,7 +286,7 @@ it("should trigger user permission webhook when a permission is revoked from a u
expect(grantPermissionResponse.status).toBe(201);
// Then revoke the permission
const revokePermissionResponse = await niceBackendFetch(`/api/v1/user-permissions/${userId}/test_permission`, {
const revokePermissionResponse = await niceBackendFetch(`/api/v1/project-permissions/${userId}/test_permission`, {
accessType: "server",
method: "DELETE",
});
@ -296,20 +296,20 @@ it("should trigger user permission webhook when a permission is revoked from a u
await wait(3000);
const attemptResponse = await Webhook.listWebhookAttempts(projectId, endpointId, svixToken);
const userPermissionDeletedEvent = attemptResponse.find(event => event.eventType === "user_permission.deleted");
const projectPermissionDeletedEvent = attemptResponse.find(event => event.eventType === "project_permission.deleted");
expect(userPermissionDeletedEvent).toMatchInlineSnapshot(`
expect(projectPermissionDeletedEvent).toMatchInlineSnapshot(`
{
"channels": null,
"eventId": null,
"eventType": "user_permission.deleted",
"eventType": "project_permission.deleted",
"id": "<stripped svix message id>",
"payload": {
"data": {
"id": "test_permission",
"user_id": "<stripped UUID>",
},
"type": "user_permission.deleted",
"type": "project_permission.deleted",
},
"timestamp": <stripped field 'timestamp'>,
}

View File

@ -2,10 +2,10 @@ import { InternalSession } from "../sessions";
import { ApiKeysCrud } from "./crud/api-keys";
import { EmailTemplateCrud, EmailTemplateType } from "./crud/email-templates";
import { InternalEmailsCrud } from "./crud/emails";
import { ProjectPermissionDefinitionsCrud } from "./crud/project-permissions";
import { ProjectsCrud } from "./crud/projects";
import { SvixTokenCrud } from "./crud/svix-token";
import { TeamPermissionDefinitionsCrud } from "./crud/team-permissions";
import { UserPermissionDefinitionsCrud } from "./crud/user-permissions";
import { ServerAuthApplicationOptions, StackServerInterface } from "./serverInterface";
export type AdminAuthApplicationOptions = ServerAuthApplicationOptions &(
@ -194,15 +194,15 @@ export class StackAdminInterface extends StackServerInterface {
);
}
async listUserPermissionDefinitions(): Promise<UserPermissionDefinitionsCrud['Admin']['Read'][]> {
const response = await this.sendAdminRequest(`/user-permission-definitions`, {}, null);
const result = await response.json() as UserPermissionDefinitionsCrud['Admin']['List'];
async listProjectPermissionDefinitions(): Promise<ProjectPermissionDefinitionsCrud['Admin']['Read'][]> {
const response = await this.sendAdminRequest(`/project-permission-definitions`, {}, null);
const result = await response.json() as ProjectPermissionDefinitionsCrud['Admin']['List'];
return result.items;
}
async createUserPermissionDefinition(data: UserPermissionDefinitionsCrud['Admin']['Create']): Promise<UserPermissionDefinitionsCrud['Admin']['Read']> {
async createProjectPermissionDefinition(data: ProjectPermissionDefinitionsCrud['Admin']['Create']): Promise<ProjectPermissionDefinitionsCrud['Admin']['Read']> {
const response = await this.sendAdminRequest(
"/user-permission-definitions",
"/project-permission-definitions",
{
method: "POST",
headers: {
@ -215,9 +215,9 @@ export class StackAdminInterface extends StackServerInterface {
return await response.json();
}
async updateUserPermissionDefinition(permissionId: string, data: UserPermissionDefinitionsCrud['Admin']['Update']): Promise<UserPermissionDefinitionsCrud['Admin']['Read']> {
async updateProjectPermissionDefinition(permissionId: string, data: ProjectPermissionDefinitionsCrud['Admin']['Update']): Promise<ProjectPermissionDefinitionsCrud['Admin']['Read']> {
const response = await this.sendAdminRequest(
`/user-permission-definitions/${permissionId}`,
`/project-permission-definitions/${permissionId}`,
{
method: "PATCH",
headers: {
@ -230,9 +230,9 @@ export class StackAdminInterface extends StackServerInterface {
return await response.json();
}
async deleteUserPermissionDefinition(permissionId: string): Promise<void> {
async deleteProjectPermissionDefinition(permissionId: string): Promise<void> {
await this.sendAdminRequest(
`/user-permission-definitions/${permissionId}`,
`/project-permission-definitions/${permissionId}`,
{ method: "DELETE" },
null,
);

View File

@ -19,6 +19,7 @@ import { InternalProjectsCrud, ProjectsCrud } from './crud/projects';
import { SessionsCrud } from './crud/sessions';
import { TeamInvitationCrud } from './crud/team-invitation';
import { TeamMemberProfilesCrud } from './crud/team-member-profiles';
import { ProjectPermissionsCrud } from './crud/project-permissions';
import { TeamPermissionsCrud } from './crud/team-permissions';
import { TeamsCrud } from './crud/teams';
@ -1183,6 +1184,21 @@ export class StackClientInterface {
return result.items;
}
async listCurrentUserProjectPermissions(
options: {
recursive: boolean,
},
session: InternalSession
): Promise<ProjectPermissionsCrud['Client']['Read'][]> {
const response = await this.sendClientRequest(
`/project-permissions?user_id=me&recursive=${options.recursive}`,
{},
session,
);
const result = await response.json() as ProjectPermissionsCrud['Client']['List'];
return result.items;
}
async listCurrentUserTeams(session: InternalSession): Promise<TeamsCrud["Client"]["Read"][]> {
const response = await this.sendClientRequest(
"/teams?user_id=me",

View File

@ -0,0 +1,118 @@
import { CrudTypeOf, createCrud } from "../../crud";
import * as schemaFields from "../../schema-fields";
import { yupMixed, yupObject } from "../../schema-fields";
import { WebhookEvent } from "../webhooks";
// =============== Project permissions =================
export const projectPermissionsCrudClientReadSchema = yupObject({
id: schemaFields.permissionDefinitionIdSchema.defined(),
user_id: schemaFields.userIdSchema.defined(),
}).defined();
export const projectPermissionsCrudServerCreateSchema = yupObject({
}).defined();
export const projectPermissionsCrudServerDeleteSchema = yupMixed();
export const projectPermissionsCrud = createCrud({
clientReadSchema: projectPermissionsCrudClientReadSchema,
serverCreateSchema: projectPermissionsCrudServerCreateSchema,
serverDeleteSchema: projectPermissionsCrudServerDeleteSchema,
docs: {
clientList: {
summary: "List project permissions",
description: "List global permissions of the current user. `user_id=me` must be set for client requests. `(user_id, permission_id)` together uniquely identify a permission.",
tags: ["Permissions"],
},
serverList: {
summary: "List project permissions",
description: "Query and filter the permission with `user_id` and `permission_id`. `(user_id, permission_id)` together uniquely identify a permission.",
tags: ["Permissions"],
},
serverCreate: {
summary: "Grant a global permission to a user",
description: "Grant a global permission to a user (the permission must be created first on the Stack dashboard)",
tags: ["Permissions"],
},
serverDelete: {
summary: "Revoke a global permission from a user",
description: "Revoke a global permission from a user",
tags: ["Permissions"],
},
},
});
export type ProjectPermissionsCrud = CrudTypeOf<typeof projectPermissionsCrud>;
export const projectPermissionCreatedWebhookEvent = {
type: "project_permission.created",
schema: projectPermissionsCrud.server.readSchema,
metadata: {
summary: "Project Permission Created",
description: "This event is triggered when a project permission is created.",
tags: ["Users"],
},
} satisfies WebhookEvent<typeof projectPermissionsCrud.server.readSchema>;
export const projectPermissionDeletedWebhookEvent = {
type: "project_permission.deleted",
schema: projectPermissionsCrud.server.readSchema,
metadata: {
summary: "Project Permission Deleted",
description: "This event is triggered when a project permission is deleted.",
tags: ["Users"],
},
} satisfies WebhookEvent<typeof projectPermissionsCrud.server.readSchema>;
// =============== Project permission definitions =================
export const projectPermissionDefinitionsCrudAdminReadSchema = yupObject({
id: schemaFields.permissionDefinitionIdSchema.defined(),
description: schemaFields.teamPermissionDescriptionSchema.optional(),
contained_permission_ids: schemaFields.containedPermissionIdsSchema.defined(),
}).defined();
export const projectPermissionDefinitionsCrudAdminCreateSchema = yupObject({
id: schemaFields.customPermissionDefinitionIdSchema.defined(),
description: schemaFields.teamPermissionDescriptionSchema.optional(),
contained_permission_ids: schemaFields.containedPermissionIdsSchema.optional(),
}).defined();
export const projectPermissionDefinitionsCrudAdminUpdateSchema = yupObject({
id: schemaFields.customPermissionDefinitionIdSchema.optional(),
description: schemaFields.teamPermissionDescriptionSchema.optional(),
contained_permission_ids: schemaFields.containedPermissionIdsSchema.optional(),
}).defined();
export const projectPermissionDefinitionsCrudAdminDeleteSchema = yupMixed();
export const projectPermissionDefinitionsCrud = createCrud({
adminReadSchema: projectPermissionDefinitionsCrudAdminReadSchema,
adminCreateSchema: projectPermissionDefinitionsCrudAdminCreateSchema,
adminUpdateSchema: projectPermissionDefinitionsCrudAdminUpdateSchema,
adminDeleteSchema: projectPermissionDefinitionsCrudAdminDeleteSchema,
docs: {
adminList: {
summary: "List project permission definitions",
description: "Query and filter project permission definitions (the equivalent of listing permissions on the Stack dashboard)",
tags: ["Permissions"],
},
adminCreate: {
summary: "Create a new project permission definition",
description: "Create a new project permission definition (the equivalent of creating a new permission on the Stack dashboard)",
tags: ["Permissions"],
},
adminUpdate: {
summary: "Update a project permission definition",
description: "Update a project permission definition (the equivalent of updating a permission on the Stack dashboard)",
tags: ["Permissions"],
},
adminDelete: {
summary: "Delete a project permission definition",
description: "Delete a project permission definition (the equivalent of deleting a permission on the Stack dashboard)",
tags: ["Permissions"],
},
},
});
export type ProjectPermissionDefinitionsCrud = CrudTypeOf<typeof projectPermissionDefinitionsCrud>;

View File

@ -6,7 +6,7 @@ import { WebhookEvent } from "../webhooks";
// =============== Team permissions =================
export const teamPermissionsCrudClientReadSchema = yupObject({
id: schemaFields.teamPermissionDefinitionIdSchema.defined(),
id: schemaFields.permissionDefinitionIdSchema.defined(),
user_id: schemaFields.userIdSchema.defined(),
team_id: schemaFields.teamIdSchema.defined(),
}).defined();
@ -68,19 +68,19 @@ export const teamPermissionDeletedWebhookEvent = {
// =============== Team permission definitions =================
export const teamPermissionDefinitionsCrudAdminReadSchema = yupObject({
id: schemaFields.teamPermissionDefinitionIdSchema.defined(),
id: schemaFields.permissionDefinitionIdSchema.defined(),
description: schemaFields.teamPermissionDescriptionSchema.optional(),
contained_permission_ids: schemaFields.containedPermissionIdsSchema.defined(),
}).defined();
export const teamPermissionDefinitionsCrudAdminCreateSchema = yupObject({
id: schemaFields.customTeamPermissionDefinitionIdSchema.defined(),
id: schemaFields.customPermissionDefinitionIdSchema.defined(),
description: schemaFields.teamPermissionDescriptionSchema.optional(),
contained_permission_ids: schemaFields.containedPermissionIdsSchema.optional(),
}).defined();
export const teamPermissionDefinitionsCrudAdminUpdateSchema = yupObject({
id: schemaFields.customTeamPermissionDefinitionIdSchema.optional(),
id: schemaFields.customPermissionDefinitionIdSchema.optional(),
description: schemaFields.teamPermissionDescriptionSchema.optional(),
contained_permission_ids: schemaFields.containedPermissionIdsSchema.optional(),
}).defined();

View File

@ -1,118 +0,0 @@
import { CrudTypeOf, createCrud } from "../../crud";
import * as schemaFields from "../../schema-fields";
import { yupMixed, yupObject } from "../../schema-fields";
import { WebhookEvent } from "../webhooks";
// =============== User permissions =================
export const userPermissionsCrudClientReadSchema = yupObject({
id: schemaFields.teamPermissionDefinitionIdSchema.defined(),
user_id: schemaFields.userIdSchema.defined(),
}).defined();
export const userPermissionsCrudServerCreateSchema = yupObject({
}).defined();
export const userPermissionsCrudServerDeleteSchema = yupMixed();
export const userPermissionsCrud = createCrud({
clientReadSchema: userPermissionsCrudClientReadSchema,
serverCreateSchema: userPermissionsCrudServerCreateSchema,
serverDeleteSchema: userPermissionsCrudServerDeleteSchema,
docs: {
clientList: {
summary: "List user permissions",
description: "List global permissions of the current user. `user_id=me` must be set for client requests. `(user_id, permission_id)` together uniquely identify a permission.",
tags: ["Permissions"],
},
serverList: {
summary: "List user permissions",
description: "Query and filter the permission with `user_id` and `permission_id`. `(user_id, permission_id)` together uniquely identify a permission.",
tags: ["Permissions"],
},
serverCreate: {
summary: "Grant a global permission to a user",
description: "Grant a global permission to a user (the permission must be created first on the Stack dashboard)",
tags: ["Permissions"],
},
serverDelete: {
summary: "Revoke a global permission from a user",
description: "Revoke a global permission from a user",
tags: ["Permissions"],
},
},
});
export type UserPermissionsCrud = CrudTypeOf<typeof userPermissionsCrud>;
export const userPermissionCreatedWebhookEvent = {
type: "user_permission.created",
schema: userPermissionsCrud.server.readSchema,
metadata: {
summary: "User Permission Created",
description: "This event is triggered when a user permission is created.",
tags: ["Users"],
},
} satisfies WebhookEvent<typeof userPermissionsCrud.server.readSchema>;
export const userPermissionDeletedWebhookEvent = {
type: "user_permission.deleted",
schema: userPermissionsCrud.server.readSchema,
metadata: {
summary: "User Permission Deleted",
description: "This event is triggered when a user permission is deleted.",
tags: ["Users"],
},
} satisfies WebhookEvent<typeof userPermissionsCrud.server.readSchema>;
// =============== User permission definitions =================
export const userPermissionDefinitionsCrudAdminReadSchema = yupObject({
id: schemaFields.teamPermissionDefinitionIdSchema.defined(),
description: schemaFields.teamPermissionDescriptionSchema.optional(),
contained_permission_ids: schemaFields.containedPermissionIdsSchema.defined(),
}).defined();
export const userPermissionDefinitionsCrudAdminCreateSchema = yupObject({
id: schemaFields.customTeamPermissionDefinitionIdSchema.defined(),
description: schemaFields.teamPermissionDescriptionSchema.optional(),
contained_permission_ids: schemaFields.containedPermissionIdsSchema.optional(),
}).defined();
export const userPermissionDefinitionsCrudAdminUpdateSchema = yupObject({
id: schemaFields.customTeamPermissionDefinitionIdSchema.optional(),
description: schemaFields.teamPermissionDescriptionSchema.optional(),
contained_permission_ids: schemaFields.containedPermissionIdsSchema.optional(),
}).defined();
export const userPermissionDefinitionsCrudAdminDeleteSchema = yupMixed();
export const userPermissionDefinitionsCrud = createCrud({
adminReadSchema: userPermissionDefinitionsCrudAdminReadSchema,
adminCreateSchema: userPermissionDefinitionsCrudAdminCreateSchema,
adminUpdateSchema: userPermissionDefinitionsCrudAdminUpdateSchema,
adminDeleteSchema: userPermissionDefinitionsCrudAdminDeleteSchema,
docs: {
adminList: {
summary: "List user permission definitions",
description: "Query and filter user permission definitions (the equivalent of listing permissions on the Stack dashboard)",
tags: ["Permissions"],
},
adminCreate: {
summary: "Create a new user permission definition",
description: "Create a new user permission definition (the equivalent of creating a new permission on the Stack dashboard)",
tags: ["Permissions"],
},
adminUpdate: {
summary: "Update a user permission definition",
description: "Update a user permission definition (the equivalent of updating a permission on the Stack dashboard)",
tags: ["Permissions"],
},
adminDelete: {
summary: "Delete a user permission definition",
description: "Delete a user permission definition (the equivalent of deleting a permission on the Stack dashboard)",
tags: ["Permissions"],
},
},
});
export type UserPermissionDefinitionsCrud = CrudTypeOf<typeof userPermissionDefinitionsCrud>;

View File

@ -15,6 +15,7 @@ import { SessionsCrud } from "./crud/sessions";
import { TeamInvitationCrud } from "./crud/team-invitation";
import { TeamMemberProfilesCrud } from "./crud/team-member-profiles";
import { TeamMembershipsCrud } from "./crud/team-memberships";
import { ProjectPermissionsCrud } from "./crud/project-permissions";
import { TeamPermissionsCrud } from "./crud/team-permissions";
import { TeamsCrud } from "./crud/teams";
import { UsersCrud } from "./crud/users";
@ -195,6 +196,25 @@ export class StackServerInterface extends StackClientInterface {
return result.items;
}
async listServerProjectPermissions(
options: {
userId?: string,
recursive: boolean,
},
session: InternalSession | null,
): Promise<ProjectPermissionsCrud['Server']['Read'][]> {
const response = await this.sendServerRequest(
`/project-permissions?${new URLSearchParams(filterUndefined({
user_id: options.userId,
recursive: options.recursive.toString(),
}))}`,
{},
session,
);
const result = await response.json() as ProjectPermissionsCrud['Server']['List'];
return result.items;
}
async listServerUsers(options: {
cursor?: string,
limit?: number,

View File

@ -1104,9 +1104,9 @@ const TeamMembershipAlreadyExists = createKnownErrorConstructor(
() => [] as const,
);
const UserPermissionRequired = createKnownErrorConstructor(
const ProjectPermissionRequired = createKnownErrorConstructor(
KnownError,
"USER_PERMISSION_REQUIRED",
"PROJECT_PERMISSION_REQUIRED",
(userId, permissionId) => [
401,
`User ${userId} does not have permission ${permissionId}.`,
@ -1308,7 +1308,7 @@ export const KnownErrors = {
InvalidTotpCode,
UserAuthenticationRequired,
TeamMembershipAlreadyExists,
UserPermissionRequired,
ProjectPermissionRequired,
TeamPermissionRequired,
InvalidSharedOAuthProviderId,
InvalidStandardOAuthProviderId,

View File

@ -359,7 +359,7 @@ export const teamSystemPermissions = [
'$remove_members',
'$invite_members',
] as const;
export const teamPermissionDefinitionIdSchema = yupString()
export const permissionDefinitionIdSchema = yupString()
.matches(/^\$?[a-z0-9_:]+$/, 'Only lowercase letters, numbers, ":", "_" and optional "$" at the beginning are allowed')
.test('is-system-permission', 'System permissions must start with a dollar sign', (value, ctx) => {
if (!value) return true;
@ -369,11 +369,11 @@ export const teamPermissionDefinitionIdSchema = yupString()
return true;
})
.meta({ openapiField: { description: `The permission ID used to uniquely identify a permission. Can either be a custom permission with lowercase letters, numbers, \`:\`, and \`_\` characters, or one of the system permissions: ${teamSystemPermissions.map(x => `\`${x}\``).join(', ')}`, exampleValue: 'read_secret_info' } });
export const customTeamPermissionDefinitionIdSchema = yupString()
export const customPermissionDefinitionIdSchema = yupString()
.matches(/^[a-z0-9_:]+$/, 'Only lowercase letters, numbers, ":", "_" are allowed')
.meta({ openapiField: { description: 'The permission ID used to uniquely identify a permission. Can only contain lowercase letters, numbers, ":", and "_" characters', exampleValue: 'read_secret_info' } });
export const teamPermissionDescriptionSchema = yupString().meta({ openapiField: { description: 'A human-readable description of the permission', exampleValue: 'Read secret information' } });
export const containedPermissionIdsSchema = yupArray(teamPermissionDefinitionIdSchema.defined()).meta({ openapiField: { description: 'The IDs of the permissions that are contained in this permission', exampleValue: ['read_public_info'] } });
export const containedPermissionIdsSchema = yupArray(permissionDefinitionIdSchema.defined()).meta({ openapiField: { description: 'The IDs of the permissions that are contained in this permission', exampleValue: ['read_public_info'] } });
// Teams
export const teamIdSchema = yupString().uuid().meta({ openapiField: { description: _idDescription('team'), exampleValue: 'ad962777-8244-496a-b6a2-e0c6a449c79e' } });

View File

@ -12,7 +12,7 @@ import { AdminSentEmail } from "../..";
import { ApiKey, ApiKeyBase, ApiKeyBaseCrudRead, ApiKeyCreateOptions, ApiKeyFirstView, apiKeyCreateOptionsToCrud } from "../../api-keys";
import { EmailConfig, stackAppInternalsSymbol } from "../../common";
import { AdminEmailTemplate, AdminEmailTemplateUpdateOptions, adminEmailTemplateUpdateOptionsToCrud } from "../../email-templates";
import { AdminTeamPermission, AdminTeamPermissionDefinition, AdminTeamPermissionDefinitionCreateOptions, AdminTeamPermissionDefinitionUpdateOptions, AdminUserPermission, AdminUserPermissionDefinition, AdminUserPermissionDefinitionCreateOptions, AdminUserPermissionDefinitionUpdateOptions, adminTeamPermissionDefinitionCreateOptionsToCrud, adminTeamPermissionDefinitionUpdateOptionsToCrud, adminUserPermissionDefinitionCreateOptionsToCrud, adminUserPermissionDefinitionUpdateOptionsToCrud } from "../../permissions";
import { AdminTeamPermission, AdminTeamPermissionDefinition, AdminTeamPermissionDefinitionCreateOptions, AdminTeamPermissionDefinitionUpdateOptions, AdminProjectPermission, AdminProjectPermissionDefinition, AdminProjectPermissionDefinitionCreateOptions, AdminProjectPermissionDefinitionUpdateOptions, adminTeamPermissionDefinitionCreateOptionsToCrud, adminTeamPermissionDefinitionUpdateOptionsToCrud, adminProjectPermissionDefinitionCreateOptionsToCrud, adminProjectPermissionDefinitionUpdateOptionsToCrud } from "../../permissions";
import { AdminOwnedProject, AdminProject, AdminProjectUpdateOptions, adminProjectUpdateOptionsToCrud } from "../../projects";
import { StackAdminApp, StackAdminAppConstructorOptions } from "../interfaces/admin-app";
import { clientVersion, createCache, getBaseUrl, getDefaultProjectId, getDefaultPublishableClientKey, getDefaultSecretServerKey, getDefaultSuperSecretAdminKey } from "./common";
@ -37,8 +37,8 @@ export class _StackAdminAppImplIncomplete<HasTokenStore extends boolean, Project
private readonly _adminTeamPermissionDefinitionsCache = createCache(async () => {
return await this._interface.listTeamPermissionDefinitions();
});
private readonly _adminUserPermissionDefinitionsCache = createCache(async () => {
return await this._interface.listUserPermissionDefinitions();
private readonly _adminProjectPermissionDefinitionsCache = createCache(async () => {
return await this._interface.listProjectPermissionDefinitions();
});
private readonly _svixTokenCache = createCache(async () => {
return await this._interface.getSvixToken();
@ -296,32 +296,32 @@ export class _StackAdminAppImplIncomplete<HasTokenStore extends boolean, Project
}
// END_PLATFORM
async createUserPermissionDefinition(data: AdminUserPermissionDefinitionCreateOptions): Promise<AdminUserPermission> {
const crud = await this._interface.createUserPermissionDefinition(adminUserPermissionDefinitionCreateOptionsToCrud(data));
await this._adminUserPermissionDefinitionsCache.refresh([]);
return this._serverUserPermissionDefinitionFromCrud(crud);
async createProjectPermissionDefinition(data: AdminProjectPermissionDefinitionCreateOptions): Promise<AdminProjectPermission> {
const crud = await this._interface.createProjectPermissionDefinition(adminProjectPermissionDefinitionCreateOptionsToCrud(data));
await this._adminProjectPermissionDefinitionsCache.refresh([]);
return this._serverProjectPermissionDefinitionFromCrud(crud);
}
async updateUserPermissionDefinition(permissionId: string, data: AdminUserPermissionDefinitionUpdateOptions) {
await this._interface.updateUserPermissionDefinition(permissionId, adminUserPermissionDefinitionUpdateOptionsToCrud(data));
await this._adminUserPermissionDefinitionsCache.refresh([]);
async updateProjectPermissionDefinition(permissionId: string, data: AdminProjectPermissionDefinitionUpdateOptions) {
await this._interface.updateProjectPermissionDefinition(permissionId, adminProjectPermissionDefinitionUpdateOptionsToCrud(data));
await this._adminProjectPermissionDefinitionsCache.refresh([]);
}
async deleteUserPermissionDefinition(permissionId: string): Promise<void> {
await this._interface.deleteUserPermissionDefinition(permissionId);
await this._adminUserPermissionDefinitionsCache.refresh([]);
async deleteProjectPermissionDefinition(permissionId: string): Promise<void> {
await this._interface.deleteProjectPermissionDefinition(permissionId);
await this._adminProjectPermissionDefinitionsCache.refresh([]);
}
async listUserPermissionDefinitions(): Promise<AdminUserPermissionDefinition[]> {
const crud = Result.orThrow(await this._adminUserPermissionDefinitionsCache.getOrWait([], "write-only"));
return crud.map((p) => this._serverUserPermissionDefinitionFromCrud(p));
async listProjectPermissionDefinitions(): Promise<AdminProjectPermissionDefinition[]> {
const crud = Result.orThrow(await this._adminProjectPermissionDefinitionsCache.getOrWait([], "write-only"));
return crud.map((p) => this._serverProjectPermissionDefinitionFromCrud(p));
}
// IF_PLATFORM react-like
useUserPermissionDefinitions(): AdminUserPermissionDefinition[] {
const crud = useAsyncCache(this._adminUserPermissionDefinitionsCache, [], "useUserPermissions()");
useProjectPermissionDefinitions(): AdminProjectPermissionDefinition[] {
const crud = useAsyncCache(this._adminProjectPermissionDefinitionsCache, [], "useProjectPermissions()");
return useMemo(() => {
return crud.map((p) => this._serverUserPermissionDefinitionFromCrud(p));
return crud.map((p) => this._serverProjectPermissionDefinitionFromCrud(p));
}, [crud]);
}
// END_PLATFORM

View File

@ -2,6 +2,7 @@ import { WebAuthnError, startAuthentication, startRegistration } from "@simplewe
import { KnownErrors, StackClientInterface } from "@stackframe/stack-shared";
import { ContactChannelsCrud } from "@stackframe/stack-shared/dist/interface/crud/contact-channels";
import { CurrentUserCrud } from "@stackframe/stack-shared/dist/interface/crud/current-user";
import { ProjectPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/project-permissions";
import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects";
import { SessionsCrud } from "@stackframe/stack-shared/dist/interface/crud/sessions";
import { TeamInvitationCrud } from "@stackframe/stack-shared/dist/interface/crud/team-invitation";
@ -24,8 +25,6 @@ import { deindent, mergeScopeStrings } from "@stackframe/stack-shared/dist/utils
import { getRelativePart, isRelative } from "@stackframe/stack-shared/dist/utils/urls";
import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids";
import * as cookie from "cookie";
import * as NextNavigationUnscrambled from "next/navigation"; // import the entire module to get around some static compiler warnings emitted by Next.js in some cases | THIS_LINE_PLATFORM next
import React, { useCallback, useMemo } from "react"; // THIS_LINE_PLATFORM react-like
import { constructRedirectUrl } from "../../../../utils/url";
import { addNewOAuthProviderOrScope, callOAuthCallback, signInWithOAuth } from "../../../auth";
import { CookieHelper, createBrowserCookieHelper, createCookieHelper, createPlaceholderCookieHelper, deleteCookieClient, getCookieClient, setOrDeleteCookie, setOrDeleteCookieClient } from "../../../cookie";
@ -38,8 +37,10 @@ import { EditableTeamMemberProfile, Team, TeamCreateOptions, TeamInvitation, Tea
import { ActiveSession, Auth, BaseUser, CurrentUser, InternalUserExtra, ProjectCurrentUser, UserExtra, UserUpdateOptions, userUpdateOptionsToCrud } from "../../users";
import { StackClientApp, StackClientAppConstructorOptions, StackClientAppJson } from "../interfaces/client-app";
import { _StackAdminAppImplIncomplete } from "./admin-app-impl";
import { TokenObject, clientVersion, createCache, createCacheBySession, createEmptyTokenStore, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getUrls } from "./common";
import { TokenObject, clientVersion, createCache, createCacheBySession, createEmptyTokenStore, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getUrls, } from "./common";
import * as NextNavigationUnscrambled from "next/navigation"; // import the entire module to get around some static compiler warnings emitted by Next.js in some cases | THIS_LINE_PLATFORM next
import React, { useCallback, useMemo } from "react"; // THIS_LINE_PLATFORM react-like
import { useAsyncCache } from "./common"; // THIS_LINE_PLATFORM react-like
let isReactServer = false;
@ -115,6 +116,12 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
>(async (session, [teamId, recursive]) => {
return await this._interface.listCurrentUserTeamPermissions({ teamId, recursive }, session);
});
private readonly _currentUserProjectPermissionsCache = createCacheBySession<
[boolean],
ProjectPermissionsCrud['Client']['Read'][]
>(async (session, [recursive]) => {
return await this._interface.listCurrentUserProjectPermissions({ recursive }, session);
});
private readonly _currentUserTeamsCache = createCacheBySession(async (session) => {
return await this._interface.listCurrentUserTeams(session);
});
@ -584,7 +591,7 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
};
}
protected _clientTeamPermissionFromCrud(crud: TeamPermissionsCrud['Client']['Read']): TeamPermission {
protected _clientPermissionFromCrud(crud: TeamPermissionsCrud['Client']['Read'] | ProjectPermissionsCrud['Client']['Read']): TeamPermission {
return {
id: crud.id,
};
@ -863,30 +870,66 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
await app._interface.leaveTeam(team.id, session);
// TODO: refresh cache
},
async listPermissions(scope: Team, options?: { recursive?: boolean }): Promise<TeamPermission[]> {
const recursive = options?.recursive ?? true;
const permissions = Result.orThrow(await app._currentUserPermissionsCache.getOrWait([session, scope.id, recursive], "write-only"));
return permissions.map((crud) => app._clientTeamPermissionFromCrud(crud));
async listPermissions(scopeOrOptions?: Team | { recursive?: boolean }, options?: { recursive?: boolean }): Promise<TeamPermission[]> {
if (scopeOrOptions && 'id' in scopeOrOptions) {
const scope = scopeOrOptions;
const recursive = options?.recursive ?? true;
const permissions = Result.orThrow(await app._currentUserPermissionsCache.getOrWait([session, scope.id, recursive], "write-only"));
return permissions.map((crud) => app._clientPermissionFromCrud(crud));
} else {
const opts = scopeOrOptions;
const recursive = opts?.recursive ?? true;
const permissions = Result.orThrow(await app._currentUserProjectPermissionsCache.getOrWait([session, recursive], "write-only"));
return permissions.map((crud) => app._clientPermissionFromCrud(crud));
}
},
// IF_PLATFORM react-like
usePermissions(scope: Team, options?: { recursive?: boolean }): TeamPermission[] {
const recursive = options?.recursive ?? true;
const permissions = useAsyncCache(app._currentUserPermissionsCache, [session, scope.id, recursive] as const, "user.usePermissions()");
return useMemo(() => permissions.map((crud) => app._clientTeamPermissionFromCrud(crud)), [permissions]);
usePermissions(scopeOrOptions?: Team | { recursive?: boolean }, options?: { recursive?: boolean }): TeamPermission[] {
if (scopeOrOptions && 'id' in scopeOrOptions) {
const scope = scopeOrOptions;
const recursive = options?.recursive ?? true;
const permissions = useAsyncCache(app._currentUserPermissionsCache, [session, scope.id, recursive] as const, "user.usePermissions()");
return useMemo(() => permissions.map((crud) => app._clientPermissionFromCrud(crud)), [permissions]);
} else {
const opts = scopeOrOptions;
const recursive = opts?.recursive ?? true;
const permissions = useAsyncCache(app._currentUserProjectPermissionsCache, [session, recursive] as const, "user.usePermissions()");
return useMemo(() => permissions.map((crud) => app._clientPermissionFromCrud(crud)), [permissions]);
}
},
// END_PLATFORM
// IF_PLATFORM react-like
usePermission(scope: Team, permissionId: string): TeamPermission | null {
const permissions = this.usePermissions(scope);
return useMemo(() => permissions.find((p) => p.id === permissionId) ?? null, [permissions, permissionId]);
usePermission(scopeOrPermissionId: Team | string, permissionId?: string): TeamPermission | null {
if (scopeOrPermissionId && typeof scopeOrPermissionId !== 'string') {
const scope = scopeOrPermissionId;
const permissions = this.usePermissions(scope);
return useMemo(() => permissions.find((p) => p.id === permissionId) ?? null, [permissions, permissionId]);
} else {
const pid = scopeOrPermissionId;
const permissions = this.usePermissions();
return useMemo(() => permissions.find((p) => p.id === pid) ?? null, [permissions, pid]);
}
},
// END_PLATFORM
async getPermission(scope: Team, permissionId: string): Promise<TeamPermission | null> {
const permissions = await this.listPermissions(scope);
return permissions.find((p) => p.id === permissionId) ?? null;
async getPermission(scopeOrPermissionId: Team | string, permissionId?: string): Promise<TeamPermission | null> {
if (scopeOrPermissionId && typeof scopeOrPermissionId !== 'string') {
const scope = scopeOrPermissionId;
const permissions = await this.listPermissions(scope);
return permissions.find((p) => p.id === permissionId) ?? null;
} else {
const pid = scopeOrPermissionId;
const permissions = await this.listPermissions();
return permissions.find((p) => p.id === pid) ?? null;
}
},
async hasPermission(scope: Team, permissionId: string): Promise<boolean> {
return (await this.getPermission(scope, permissionId)) !== null;
async hasPermission(scopeOrPermissionId: Team | string, permissionId?: string): Promise<boolean> {
if (scopeOrPermissionId && typeof scopeOrPermissionId !== 'string') {
const scope = scopeOrPermissionId;
return (await this.getPermission(scope, permissionId as string)) !== null;
} else {
const pid = scopeOrPermissionId;
return (await this.getPermission(pid)) !== null;
}
},
async update(update) {
return await app._updateClientUser(update, session);

View File

@ -3,7 +3,7 @@ import { ContactChannelsCrud } from "@stackframe/stack-shared/dist/interface/cru
import { TeamInvitationCrud } from "@stackframe/stack-shared/dist/interface/crud/team-invitation";
import { TeamMemberProfilesCrud } from "@stackframe/stack-shared/dist/interface/crud/team-member-profiles";
import { TeamPermissionDefinitionsCrud, TeamPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/team-permissions";
import { UserPermissionDefinitionsCrud } from "@stackframe/stack-shared/dist/interface/crud/user-permissions";
import { ProjectPermissionDefinitionsCrud, ProjectPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/project-permissions";
import { TeamsCrud } from "@stackframe/stack-shared/dist/interface/crud/teams";
import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users";
import { InternalSession } from "@stackframe/stack-shared/dist/sessions";
@ -17,7 +17,7 @@ import { constructRedirectUrl } from "../../../../utils/url";
import { GetUserOptions, HandlerUrls, OAuthScopesOnSignIn, TokenStoreInit } from "../../common";
import { OAuthConnection } from "../../connected-accounts";
import { ServerContactChannel, ServerContactChannelCreateOptions, ServerContactChannelUpdateOptions, serverContactChannelCreateOptionsToCrud, serverContactChannelUpdateOptionsToCrud } from "../../contact-channels";
import { AdminTeamPermission, AdminTeamPermissionDefinition, AdminUserPermissionDefinition } from "../../permissions";
import { AdminTeamPermission, AdminTeamPermissionDefinition, AdminProjectPermissionDefinition } from "../../permissions";
import { EditableTeamMemberProfile, ServerListUsersOptions, ServerTeam, ServerTeamCreateOptions, ServerTeamUpdateOptions, ServerTeamUser, Team, TeamInvitation, serverTeamCreateOptionsToCrud, serverTeamUpdateOptionsToCrud } from "../../teams";
import { ProjectCurrentServerUser, ServerUser, ServerUserCreateOptions, ServerUserUpdateOptions, serverUserCreateOptionsToCrud, serverUserUpdateOptionsToCrud } from "../../users";
import { StackServerAppConstructorOptions } from "../interfaces/server-app";
@ -61,6 +61,12 @@ export class _StackServerAppImplIncomplete<HasTokenStore extends boolean, Projec
>(async ([teamId, userId, recursive]) => {
return await this._interface.listServerTeamPermissions({ teamId, userId, recursive }, null);
});
private readonly _serverUserProjectPermissionsCache = createCache<
[string, boolean],
ProjectPermissionsCrud['Server']['Read'][]
>(async ([userId, recursive]) => {
return await this._interface.listServerProjectPermissions({ userId, recursive }, null);
});
private readonly _serverUserOAuthConnectionAccessTokensCache = createCache<[string, string, string], { accessToken: string } | null>(
async ([userId, providerId, scope]) => {
try {
@ -316,30 +322,66 @@ export class _StackServerAppImplIncomplete<HasTokenStore extends boolean, Projec
await app._interface.leaveServerTeam({ teamId: team.id, userId: crud.id });
// TODO: refresh cache
},
async listPermissions(scope: Team, options?: { recursive?: boolean }): Promise<AdminTeamPermission[]> {
const recursive = options?.recursive ?? true;
const permissions = Result.orThrow(await app._serverTeamUserPermissionsCache.getOrWait([scope.id, crud.id, recursive], "write-only"));
return permissions.map((crud) => app._serverPermissionFromCrud(crud));
async listPermissions(scopeOrOptions?: Team | { recursive?: boolean }, options?: { recursive?: boolean }): Promise<AdminTeamPermission[]> {
if (scopeOrOptions && 'id' in scopeOrOptions) {
const scope = scopeOrOptions;
const recursive = options?.recursive ?? true;
const permissions = Result.orThrow(await app._serverTeamUserPermissionsCache.getOrWait([scope.id, crud.id, recursive], "write-only"));
return permissions.map((crud) => app._serverPermissionFromCrud(crud));
} else {
const opts = scopeOrOptions;
const recursive = opts?.recursive ?? true;
const permissions = Result.orThrow(await app._serverUserProjectPermissionsCache.getOrWait([crud.id, recursive], "write-only"));
return permissions.map((crud) => app._serverPermissionFromCrud(crud));
}
},
// IF_PLATFORM react-like
usePermissions(scope: Team, options?: { recursive?: boolean }): AdminTeamPermission[] {
const recursive = options?.recursive ?? true;
const permissions = useAsyncCache(app._serverTeamUserPermissionsCache, [scope.id, crud.id, recursive] as const, "user.usePermissions()");
return useMemo(() => permissions.map((crud) => app._serverPermissionFromCrud(crud)), [permissions]);
usePermissions(scopeOrOptions?: Team | { recursive?: boolean }, options?: { recursive?: boolean }): AdminTeamPermission[] {
if (scopeOrOptions && 'id' in scopeOrOptions) {
const scope = scopeOrOptions;
const recursive = options?.recursive ?? true;
const permissions = useAsyncCache(app._serverTeamUserPermissionsCache, [scope.id, crud.id, recursive] as const, "user.usePermissions()");
return useMemo(() => permissions.map((crud) => app._serverPermissionFromCrud(crud)), [permissions]);
} else {
const opts = scopeOrOptions;
const recursive = opts?.recursive ?? true;
const permissions = useAsyncCache(app._serverUserProjectPermissionsCache, [crud.id, recursive] as const, "user.usePermissions()");
return useMemo(() => permissions.map((crud) => app._serverPermissionFromCrud(crud)), [permissions]);
}
},
// END_PLATFORM
async getPermission(scope: Team, permissionId: string): Promise<AdminTeamPermission | null> {
const permissions = await this.listPermissions(scope);
return permissions.find((p) => p.id === permissionId) ?? null;
async getPermission(scopeOrPermissionId: Team | string, permissionId?: string): Promise<AdminTeamPermission | null> {
if (scopeOrPermissionId && typeof scopeOrPermissionId !== 'string') {
const scope = scopeOrPermissionId;
const permissions = await this.listPermissions(scope);
return permissions.find((p) => p.id === permissionId) ?? null;
} else {
const pid = scopeOrPermissionId;
const permissions = await this.listPermissions();
return permissions.find((p) => p.id === pid) ?? null;
}
},
// IF_PLATFORM react-like
usePermission(scope: Team, permissionId: string): AdminTeamPermission | null {
const permissions = this.usePermissions(scope);
return useMemo(() => permissions.find((p) => p.id === permissionId) ?? null, [permissions, permissionId]);
usePermission(scopeOrPermissionId: Team | string, permissionId?: string): AdminTeamPermission | null {
if (scopeOrPermissionId && typeof scopeOrPermissionId !== 'string') {
const scope = scopeOrPermissionId;
const permissions = this.usePermissions(scope);
return useMemo(() => permissions.find((p) => p.id === permissionId) ?? null, [permissions, permissionId]);
} else {
const pid = scopeOrPermissionId;
const permissions = this.usePermissions();
return useMemo(() => permissions.find((p) => p.id === pid) ?? null, [permissions, pid]);
}
},
// END_PLATFORM
async hasPermission(scope: Team, permissionId: string): Promise<boolean> {
return await this.getPermission(scope, permissionId) !== null;
async hasPermission(scopeOrPermissionId: Team | string, permissionId?: string): Promise<boolean> {
if (scopeOrPermissionId && typeof scopeOrPermissionId !== 'string') {
const scope = scopeOrPermissionId;
return (await this.getPermission(scope, permissionId as string)) !== null;
} else {
const pid = scopeOrPermissionId;
return (await this.getPermission(pid)) !== null;
}
},
async update(update: ServerUserUpdateOptions) {
await app._updateServerUser(crud.id, update);
@ -626,7 +668,7 @@ export class _StackServerAppImplIncomplete<HasTokenStore extends boolean, Projec
}
// END_PLATFORM
_serverPermissionFromCrud(crud: TeamPermissionsCrud['Server']['Read']): AdminTeamPermission {
_serverPermissionFromCrud(crud: TeamPermissionsCrud['Server']['Read'] | ProjectPermissionsCrud['Server']['Read']): AdminTeamPermission {
return {
id: crud.id,
};
@ -640,7 +682,7 @@ export class _StackServerAppImplIncomplete<HasTokenStore extends boolean, Projec
};
}
_serverUserPermissionDefinitionFromCrud(crud: UserPermissionDefinitionsCrud['Admin']['Read']): AdminUserPermissionDefinition {
_serverProjectPermissionDefinitionFromCrud(crud: ProjectPermissionDefinitionsCrud['Admin']['Read']): AdminProjectPermissionDefinition {
return {
id: crud.id,
description: crud.description,

View File

@ -5,7 +5,7 @@ import { ApiKey, ApiKeyCreateOptions, ApiKeyFirstView } from "../../api-keys";
import { AsyncStoreProperty, EmailConfig } from "../../common";
import { AdminSentEmail } from "../../email";
import { AdminEmailTemplate, AdminEmailTemplateUpdateOptions } from "../../email-templates";
import { AdminTeamPermission, AdminTeamPermissionDefinition, AdminTeamPermissionDefinitionCreateOptions, AdminTeamPermissionDefinitionUpdateOptions, AdminUserPermission, AdminUserPermissionDefinition, AdminUserPermissionDefinitionCreateOptions, AdminUserPermissionDefinitionUpdateOptions } from "../../permissions";
import { AdminTeamPermission, AdminTeamPermissionDefinition, AdminTeamPermissionDefinitionCreateOptions, AdminTeamPermissionDefinitionUpdateOptions, AdminProjectPermission, AdminProjectPermissionDefinition, AdminProjectPermissionDefinitionCreateOptions, AdminProjectPermissionDefinitionUpdateOptions } from "../../permissions";
import { AdminProject } from "../../projects";
import { _StackAdminAppImpl } from "../implementations";
import { StackServerApp, StackServerAppConstructorOptions } from "./server-app";
@ -31,7 +31,7 @@ export type StackAdminApp<HasTokenStore extends boolean = boolean, ProjectId ext
& AsyncStoreProperty<"project", [], AdminProject, false>
& AsyncStoreProperty<"apiKeys", [], ApiKey[], true>
& AsyncStoreProperty<"teamPermissionDefinitions", [], AdminTeamPermissionDefinition[], true>
& AsyncStoreProperty<"userPermissionDefinitions", [], AdminUserPermissionDefinition[], true>
& AsyncStoreProperty<"projectPermissionDefinitions", [], AdminProjectPermissionDefinition[], true>
& {
useEmailTemplates(): AdminEmailTemplate[], // THIS_LINE_PLATFORM react-like
listEmailTemplates(): Promise<AdminEmailTemplate[]>,
@ -44,9 +44,9 @@ export type StackAdminApp<HasTokenStore extends boolean = boolean, ProjectId ext
updateTeamPermissionDefinition(permissionId: string, data: AdminTeamPermissionDefinitionUpdateOptions): Promise<void>,
deleteTeamPermissionDefinition(permissionId: string): Promise<void>,
createUserPermissionDefinition(data: AdminUserPermissionDefinitionCreateOptions): Promise<AdminUserPermission>,
updateUserPermissionDefinition(permissionId: string, data: AdminUserPermissionDefinitionUpdateOptions): Promise<void>,
deleteUserPermissionDefinition(permissionId: string): Promise<void>,
createProjectPermissionDefinition(data: AdminProjectPermissionDefinitionCreateOptions): Promise<AdminProjectPermission>,
updateProjectPermissionDefinition(permissionId: string, data: AdminProjectPermissionDefinitionUpdateOptions): Promise<void>,
deleteProjectPermissionDefinition(permissionId: string): Promise<void>,
useSvixToken(): string, // THIS_LINE_PLATFORM react-like

View File

@ -52,10 +52,10 @@ export type {
AdminTeamPermissionDefinition,
AdminTeamPermissionDefinitionCreateOptions,
AdminTeamPermissionDefinitionUpdateOptions,
AdminUserPermission,
AdminUserPermissionDefinition,
AdminUserPermissionDefinitionCreateOptions,
AdminUserPermissionDefinitionUpdateOptions,
AdminProjectPermission,
AdminProjectPermissionDefinition,
AdminProjectPermissionDefinitionCreateOptions,
AdminProjectPermissionDefinitionUpdateOptions,
} from "./permissions";
export type {

View File

@ -1,5 +1,5 @@
import { TeamPermissionDefinitionsCrud } from "@stackframe/stack-shared/dist/interface/crud/team-permissions";
import { UserPermissionDefinitionsCrud } from "@stackframe/stack-shared/dist/interface/crud/user-permissions";
import { ProjectPermissionDefinitionsCrud } from "@stackframe/stack-shared/dist/interface/crud/project-permissions";
export type TeamPermission = {
@ -38,24 +38,24 @@ export function adminTeamPermissionDefinitionUpdateOptionsToCrud(options: AdminT
};
}
export type UserPermission = {
export type ProjectPermission = {
id: string,
};
export type AdminUserPermission = UserPermission;
export type AdminProjectPermission = ProjectPermission;
export type AdminUserPermissionDefinition = {
export type AdminProjectPermissionDefinition = {
id: string,
description?: string,
containedPermissionIds: string[],
};
export type AdminUserPermissionDefinitionCreateOptions = {
export type AdminProjectPermissionDefinitionCreateOptions = {
id: string,
description?: string,
containedPermissionIds: string[],
};
export function adminUserPermissionDefinitionCreateOptionsToCrud(options: AdminUserPermissionDefinitionCreateOptions): UserPermissionDefinitionsCrud["Admin"]["Create"] {
export function adminProjectPermissionDefinitionCreateOptionsToCrud(options: AdminProjectPermissionDefinitionCreateOptions): ProjectPermissionDefinitionsCrud["Admin"]["Create"] {
return {
id: options.id,
description: options.description,
@ -63,8 +63,8 @@ export function adminUserPermissionDefinitionCreateOptionsToCrud(options: AdminU
};
}
export type AdminUserPermissionDefinitionUpdateOptions = Partial<AdminUserPermissionDefinitionCreateOptions>;
export function adminUserPermissionDefinitionUpdateOptionsToCrud(options: AdminUserPermissionDefinitionUpdateOptions): UserPermissionDefinitionsCrud["Admin"]["Update"] {
export type AdminProjectPermissionDefinitionUpdateOptions = Partial<AdminProjectPermissionDefinitionCreateOptions>;
export function adminProjectPermissionDefinitionUpdateOptionsToCrud(options: AdminProjectPermissionDefinitionUpdateOptions): ProjectPermissionDefinitionsCrud["Admin"]["Update"] {
return {
id: options.id,
description: options.description,

View File

@ -187,6 +187,21 @@ export type UserExtra = {
// END_PLATFORM
hasPermission(scope: Team, permissionId: string): Promise<boolean>,
hasPermission(permissionId: string): Promise<boolean>,
getPermission(scope: Team, permissionId: string): Promise<TeamPermission | null>,
getPermission(permissionId: string): Promise<TeamPermission | null>,
listPermissions(scope: Team, options?: { recursive?: boolean }): Promise<TeamPermission[]>,
listPermissions(options?: { recursive?: boolean }): Promise<TeamPermission[]>,
// IF_PLATFORM react-like
usePermissions(scope: Team, options?: { recursive?: boolean }): TeamPermission[],
usePermissions(options?: { recursive?: boolean }): TeamPermission[],
usePermission(scope: Team, permissionId: string): TeamPermission | null,
usePermission(permissionId: string): TeamPermission | null,
// END_PLATFORM
readonly selectedTeam: Team | null,
setSelectedTeam(team: Team | null): Promise<void>,
@ -270,6 +285,23 @@ export type ServerBaseUser = {
grantPermission(scope: Team, permissionId: string): Promise<void>,
revokePermission(scope: Team, permissionId: string): Promise<void>,
getPermission(scope: Team, permissionId: string): Promise<TeamPermission | null>,
getPermission(permissionId: string): Promise<TeamPermission | null>,
hasPermission(scope: Team, permissionId: string): Promise<boolean>,
hasPermission(permissionId: string): Promise<boolean>,
listPermissions(scope: Team, options?: { recursive?: boolean }): Promise<TeamPermission[]>,
listPermissions(options?: { recursive?: boolean }): Promise<TeamPermission[]>,
// IF_PLATFORM react-like
usePermissions(scope: Team, options?: { recursive?: boolean }): TeamPermission[],
usePermissions(options?: { recursive?: boolean }): TeamPermission[],
usePermission(scope: Team, permissionId: string): TeamPermission | null,
usePermission(permissionId: string): TeamPermission | null,
// END_PLATFORM
/**
* Creates a new session object with a refresh token for this user. Can be used to impersonate them.
*/