fix: project settings options not checking permissions (#4472)
This commit is contained in:
committed by
GitHub
parent
bb68ef6a95
commit
2de4fef006
@@ -76,7 +76,7 @@
|
||||
<template #actions="{ item }">
|
||||
<LayoutMenu
|
||||
v-model:open="showActionsMenu[item.id]"
|
||||
:items="actionItems"
|
||||
:items="actionItems[item.id]"
|
||||
mount-menu-on-body
|
||||
:menu-position="HorizontalDirection.Left"
|
||||
@chosen="({ item: actionItem }) => onActionChosen(actionItem, item)"
|
||||
@@ -140,10 +140,21 @@ graphql(`
|
||||
avatar
|
||||
}
|
||||
}
|
||||
permissions {
|
||||
canDelete {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
canReadSettings {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
canRead {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
projects?: SettingsSharedProjects_ProjectFragment[]
|
||||
workspaceId?: string
|
||||
disableCreate?: boolean
|
||||
@@ -174,13 +185,37 @@ enum ActionTypes {
|
||||
|
||||
const showActionsMenu = ref<Record<string, boolean>>({})
|
||||
|
||||
const actionItems: LayoutMenuItem[][] = [
|
||||
[
|
||||
{ title: 'View project', id: ActionTypes.ViewProject },
|
||||
{ title: 'Edit members', id: ActionTypes.EditMembers },
|
||||
{ title: 'Delete project...', id: ActionTypes.DeleteProject }
|
||||
]
|
||||
]
|
||||
const actionItems = computed((): { [projectId: string]: LayoutMenuItem[][] } =>
|
||||
(props.projects || []).reduce((ret, project) => {
|
||||
const canRead = project.permissions.canRead
|
||||
const canDelete = project.permissions.canDelete
|
||||
const canReadSettings = project.permissions.canReadSettings
|
||||
|
||||
ret[project.id] = [
|
||||
[
|
||||
{
|
||||
title: 'View project',
|
||||
id: ActionTypes.ViewProject,
|
||||
disabled: !canRead?.authorized,
|
||||
disabledTooltip: canRead?.message
|
||||
},
|
||||
{
|
||||
title: 'Edit members',
|
||||
id: ActionTypes.EditMembers,
|
||||
disabled: !canReadSettings?.authorized,
|
||||
disabledTooltip: canReadSettings?.message
|
||||
},
|
||||
{
|
||||
title: 'Delete project...',
|
||||
id: ActionTypes.DeleteProject,
|
||||
disabled: !canDelete?.authorized,
|
||||
disabledTooltip: canDelete?.message
|
||||
}
|
||||
]
|
||||
]
|
||||
return ret
|
||||
}, {} as { [projectId: string]: LayoutMenuItem[][] })
|
||||
)
|
||||
|
||||
const onActionChosen = (
|
||||
actionItem: LayoutMenuItem,
|
||||
|
||||
@@ -107,7 +107,7 @@ type Documents = {
|
||||
"\n fragment SettingsSidebar_User on User {\n id\n workspaces {\n items {\n ...SettingsSidebar_Workspace\n }\n }\n }\n": typeof types.SettingsSidebar_UserFragmentDoc,
|
||||
"\n fragment SettingsServerRegionsAddEditDialog_ServerRegionItem on ServerRegionItem {\n id\n name\n description\n key\n }\n": typeof types.SettingsServerRegionsAddEditDialog_ServerRegionItemFragmentDoc,
|
||||
"\n fragment SettingsServerRegionsTable_ServerRegionItem on ServerRegionItem {\n id\n name\n key\n description\n }\n": typeof types.SettingsServerRegionsTable_ServerRegionItemFragmentDoc,
|
||||
"\n fragment SettingsSharedProjects_Project on Project {\n ...ProjectsDeleteDialog_Project\n id\n name\n visibility\n createdAt\n updatedAt\n models(limit: 0) {\n totalCount\n }\n versions(limit: 0) {\n totalCount\n }\n team {\n id\n user {\n name\n id\n avatar\n }\n }\n }\n": typeof types.SettingsSharedProjects_ProjectFragmentDoc,
|
||||
"\n fragment SettingsSharedProjects_Project on Project {\n ...ProjectsDeleteDialog_Project\n id\n name\n visibility\n createdAt\n updatedAt\n models(limit: 0) {\n totalCount\n }\n versions(limit: 0) {\n totalCount\n }\n team {\n id\n user {\n name\n id\n avatar\n }\n }\n permissions {\n canDelete {\n ...FullPermissionCheckResult\n }\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canRead {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.SettingsSharedProjects_ProjectFragmentDoc,
|
||||
"\n fragment SettingsUserProfileChangePassword_User on User {\n id\n email\n }\n": typeof types.SettingsUserProfileChangePassword_UserFragmentDoc,
|
||||
"\n fragment SettingsUserProfileDeleteAccount_User on User {\n id\n email\n }\n": typeof types.SettingsUserProfileDeleteAccount_UserFragmentDoc,
|
||||
"\n fragment SettingsUserProfileDetails_User on User {\n id\n name\n company\n ...UserProfileEditDialogAvatar_User\n }\n": typeof types.SettingsUserProfileDetails_UserFragmentDoc,
|
||||
@@ -529,7 +529,7 @@ const documents: Documents = {
|
||||
"\n fragment SettingsSidebar_User on User {\n id\n workspaces {\n items {\n ...SettingsSidebar_Workspace\n }\n }\n }\n": types.SettingsSidebar_UserFragmentDoc,
|
||||
"\n fragment SettingsServerRegionsAddEditDialog_ServerRegionItem on ServerRegionItem {\n id\n name\n description\n key\n }\n": types.SettingsServerRegionsAddEditDialog_ServerRegionItemFragmentDoc,
|
||||
"\n fragment SettingsServerRegionsTable_ServerRegionItem on ServerRegionItem {\n id\n name\n key\n description\n }\n": types.SettingsServerRegionsTable_ServerRegionItemFragmentDoc,
|
||||
"\n fragment SettingsSharedProjects_Project on Project {\n ...ProjectsDeleteDialog_Project\n id\n name\n visibility\n createdAt\n updatedAt\n models(limit: 0) {\n totalCount\n }\n versions(limit: 0) {\n totalCount\n }\n team {\n id\n user {\n name\n id\n avatar\n }\n }\n }\n": types.SettingsSharedProjects_ProjectFragmentDoc,
|
||||
"\n fragment SettingsSharedProjects_Project on Project {\n ...ProjectsDeleteDialog_Project\n id\n name\n visibility\n createdAt\n updatedAt\n models(limit: 0) {\n totalCount\n }\n versions(limit: 0) {\n totalCount\n }\n team {\n id\n user {\n name\n id\n avatar\n }\n }\n permissions {\n canDelete {\n ...FullPermissionCheckResult\n }\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canRead {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.SettingsSharedProjects_ProjectFragmentDoc,
|
||||
"\n fragment SettingsUserProfileChangePassword_User on User {\n id\n email\n }\n": types.SettingsUserProfileChangePassword_UserFragmentDoc,
|
||||
"\n fragment SettingsUserProfileDeleteAccount_User on User {\n id\n email\n }\n": types.SettingsUserProfileDeleteAccount_UserFragmentDoc,
|
||||
"\n fragment SettingsUserProfileDetails_User on User {\n id\n name\n company\n ...UserProfileEditDialogAvatar_User\n }\n": types.SettingsUserProfileDetails_UserFragmentDoc,
|
||||
@@ -1247,7 +1247,7 @@ export function graphql(source: "\n fragment SettingsServerRegionsTable_ServerR
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment SettingsSharedProjects_Project on Project {\n ...ProjectsDeleteDialog_Project\n id\n name\n visibility\n createdAt\n updatedAt\n models(limit: 0) {\n totalCount\n }\n versions(limit: 0) {\n totalCount\n }\n team {\n id\n user {\n name\n id\n avatar\n }\n }\n }\n"): (typeof documents)["\n fragment SettingsSharedProjects_Project on Project {\n ...ProjectsDeleteDialog_Project\n id\n name\n visibility\n createdAt\n updatedAt\n models(limit: 0) {\n totalCount\n }\n versions(limit: 0) {\n totalCount\n }\n team {\n id\n user {\n name\n id\n avatar\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n fragment SettingsSharedProjects_Project on Project {\n ...ProjectsDeleteDialog_Project\n id\n name\n visibility\n createdAt\n updatedAt\n models(limit: 0) {\n totalCount\n }\n versions(limit: 0) {\n totalCount\n }\n team {\n id\n user {\n name\n id\n avatar\n }\n }\n permissions {\n canDelete {\n ...FullPermissionCheckResult\n }\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canRead {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment SettingsSharedProjects_Project on Project {\n ...ProjectsDeleteDialog_Project\n id\n name\n visibility\n createdAt\n updatedAt\n models(limit: 0) {\n totalCount\n }\n versions(limit: 0) {\n totalCount\n }\n team {\n id\n user {\n name\n id\n avatar\n }\n }\n permissions {\n canDelete {\n ...FullPermissionCheckResult\n }\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canRead {\n ...FullPermissionCheckResult\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -7,6 +7,7 @@ type ProjectPermissionChecks {
|
||||
canMoveToWorkspace(workspaceId: String): PermissionCheckResult!
|
||||
canRead: PermissionCheckResult!
|
||||
canUpdate: PermissionCheckResult!
|
||||
canDelete: PermissionCheckResult!
|
||||
canUpdateAllowPublicComments: PermissionCheckResult!
|
||||
canReadSettings: PermissionCheckResult!
|
||||
canReadWebhooks: PermissionCheckResult!
|
||||
|
||||
@@ -2591,6 +2591,7 @@ export type ProjectPermissionChecks = {
|
||||
canBroadcastActivity: PermissionCheckResult;
|
||||
canCreateComment: PermissionCheckResult;
|
||||
canCreateModel: PermissionCheckResult;
|
||||
canDelete: PermissionCheckResult;
|
||||
canLeave: PermissionCheckResult;
|
||||
canMoveToWorkspace: PermissionCheckResult;
|
||||
canRead: PermissionCheckResult;
|
||||
@@ -6769,6 +6770,7 @@ export type ProjectPermissionChecksResolvers<ContextType = GraphQLContext, Paren
|
||||
canBroadcastActivity?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canCreateComment?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canCreateModel?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canDelete?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canLeave?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canMoveToWorkspace?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType, Partial<ProjectPermissionChecksCanMoveToWorkspaceArgs>>;
|
||||
canRead?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
|
||||
@@ -50,6 +50,13 @@ export default {
|
||||
})
|
||||
return Authz.toGraphqlResult(canUpdate)
|
||||
},
|
||||
canDelete: async (parent, _args, ctx) => {
|
||||
const canDelete = await ctx.authPolicies.project.canDelete({
|
||||
projectId: parent.projectId,
|
||||
userId: ctx.userId
|
||||
})
|
||||
return Authz.toGraphqlResult(canDelete)
|
||||
},
|
||||
canUpdateAllowPublicComments: async (parent, _args, ctx) => {
|
||||
const canUpdateAllowPublicComments =
|
||||
await ctx.authPolicies.project.canUpdateAllowPublicComments({
|
||||
|
||||
@@ -203,13 +203,21 @@ export = {
|
||||
},
|
||||
ProjectMutations: {
|
||||
async batchDelete(_parent, args, ctx) {
|
||||
args.ids.forEach((id) => {
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: id,
|
||||
resourceType: TokenResourceIdentifierType.Project,
|
||||
resourceAccessRules: ctx.resourceAccessRules
|
||||
await Promise.all(
|
||||
args.ids.map(async (id) => {
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: id,
|
||||
resourceType: TokenResourceIdentifierType.Project,
|
||||
resourceAccessRules: ctx.resourceAccessRules
|
||||
})
|
||||
|
||||
const canDelete = await ctx.authPolicies.project.canDelete({
|
||||
projectId: id,
|
||||
userId: ctx.userId
|
||||
})
|
||||
throwIfAuthNotOk(canDelete)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const results = await withOperationLogging(
|
||||
async () =>
|
||||
@@ -251,11 +259,11 @@ export = {
|
||||
streamId: projectId //legacy
|
||||
})
|
||||
|
||||
const canUpdate = await authPolicies.project.canUpdate({
|
||||
const canDelete = await authPolicies.project.canDelete({
|
||||
projectId,
|
||||
userId
|
||||
})
|
||||
throwIfAuthNotOk(canUpdate)
|
||||
throwIfAuthNotOk(canDelete)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
const deleteStreamAndNotify = deleteStreamAndNotifyFactory({
|
||||
|
||||
@@ -2571,6 +2571,7 @@ export type ProjectPermissionChecks = {
|
||||
canBroadcastActivity: PermissionCheckResult;
|
||||
canCreateComment: PermissionCheckResult;
|
||||
canCreateModel: PermissionCheckResult;
|
||||
canDelete: PermissionCheckResult;
|
||||
canLeave: PermissionCheckResult;
|
||||
canMoveToWorkspace: PermissionCheckResult;
|
||||
canRead: PermissionCheckResult;
|
||||
|
||||
@@ -2572,6 +2572,7 @@ export type ProjectPermissionChecks = {
|
||||
canBroadcastActivity: PermissionCheckResult;
|
||||
canCreateComment: PermissionCheckResult;
|
||||
canCreateModel: PermissionCheckResult;
|
||||
canDelete: PermissionCheckResult;
|
||||
canLeave: PermissionCheckResult;
|
||||
canMoveToWorkspace: PermissionCheckResult;
|
||||
canRead: PermissionCheckResult;
|
||||
|
||||
@@ -22,6 +22,7 @@ import { canUpdateProjectVersionPolicy } from './project/version/canUpdate.js'
|
||||
import { canReceiveProjectVersionPolicy } from './project/version/canReceive.js'
|
||||
import { canRequestProjectVersionRenderPolicy } from './project/version/canRequestRender.js'
|
||||
import { canReceiveWorkspaceProjectsUpdatedMessagePolicy } from './workspace/canReceiveProjectsUpdatedMessage.js'
|
||||
import { canDeleteProjectPolicy } from './project/canDelete.js'
|
||||
|
||||
export const authPoliciesFactory = (loaders: AllAuthCheckContextLoaders) => ({
|
||||
project: {
|
||||
@@ -46,6 +47,7 @@ export const authPoliciesFactory = (loaders: AllAuthCheckContextLoaders) => ({
|
||||
canMoveToWorkspace: canMoveToWorkspacePolicy(loaders),
|
||||
canCreatePersonal: canCreatePersonalProjectPolicy(loaders),
|
||||
canUpdate: canUpdateProjectPolicy(loaders),
|
||||
canDelete: canDeleteProjectPolicy(loaders),
|
||||
canUpdateAllowPublicComments: canUpdateProjectAllowPublicCommentsPolicy(loaders),
|
||||
canReadSettings: canReadProjectSettingsPolicy(loaders),
|
||||
canReadWebhooks: canReadProjectWebhooksPolicy(loaders),
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { canDeleteProjectPolicy } from './canDelete.js'
|
||||
import { parseFeatureFlags } from '../../../environment/index.js'
|
||||
import { getProjectFake } from '../../../tests/fakes.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import { TIME_MS } from '../../../core/index.js'
|
||||
|
||||
describe('canDeleteProjectPolicy', () => {
|
||||
const buildSUT = (
|
||||
overrides?: Partial<Parameters<typeof canDeleteProjectPolicy>[0]>
|
||||
) =>
|
||||
canDeleteProjectPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Owner,
|
||||
getServerRole: async () => Roles.Server.User,
|
||||
getWorkspace: async () => null,
|
||||
getWorkspaceRole: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
...overrides
|
||||
})
|
||||
|
||||
const buildWorkspaceSUT = (
|
||||
overrides?: Partial<Parameters<typeof canDeleteProjectPolicy>[0]>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
}),
|
||||
getWorkspace: async () => ({
|
||||
id: 'workspace-id',
|
||||
slug: 'workspace-slug'
|
||||
}),
|
||||
getProjectRole: async () => null,
|
||||
getWorkspaceRole: async () => Roles.Workspace.Admin,
|
||||
getWorkspaceSsoProvider: async () => ({
|
||||
providerId: 'provider-id'
|
||||
}),
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date(Date.now() + TIME_MS.day)
|
||||
}),
|
||||
...overrides
|
||||
})
|
||||
|
||||
it('works for project owner', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('it works for server admin', async () => {
|
||||
const sut = buildSUT({
|
||||
getServerRole: async () => Roles.Server.Admin,
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
describe('with workspace project', () => {
|
||||
it('works for workspace admin', async () => {
|
||||
const sut = buildWorkspaceSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('it works for server admin', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getServerRole: async () => Roles.Server.Admin,
|
||||
getProjectRole: async () => null,
|
||||
getWorkspaceRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
import { ok } from 'true-myth/result'
|
||||
import { MaybeUserContext, ProjectContext } from '../../domain/context.js'
|
||||
import { AuthPolicy } from '../../domain/policies.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import { Loaders } from '../../domain/loaders.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotEnoughPermissionsError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
ServerNotEnoughPermissionsError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceNotEnoughPermissionsError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { ensureMinimumServerRoleFragment } from '../../fragments/server.js'
|
||||
import { canUpdateProjectPolicy } from './canUpdate.js'
|
||||
|
||||
export const canDeleteProjectPolicy: AuthPolicy<
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole,
|
||||
ProjectContext & MaybeUserContext,
|
||||
InstanceType<
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof ServerNotEnoughPermissionsError
|
||||
| typeof WorkspaceSsoSessionNoAccessError
|
||||
| typeof WorkspaceNotEnoughPermissionsError
|
||||
| typeof ProjectNotEnoughPermissionsError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId }) => {
|
||||
// Server admins can also delete projects
|
||||
const ensuredAdminAccess = await ensureMinimumServerRoleFragment(loaders)({
|
||||
userId,
|
||||
role: Roles.Server.Admin
|
||||
})
|
||||
if (ensuredAdminAccess.isOk) {
|
||||
return ok()
|
||||
}
|
||||
|
||||
// Otherwise follow standard canUpdate check
|
||||
const ensuredCanUpdate = await canUpdateProjectPolicy(loaders)({
|
||||
userId,
|
||||
projectId
|
||||
})
|
||||
|
||||
return ensuredCanUpdate
|
||||
}
|
||||
Reference in New Issue
Block a user