feat(authz): automate policies (#4491)
This commit is contained in:
@@ -60,7 +60,7 @@ import {
|
||||
} from '~/lib/projects/graphql/queries'
|
||||
import type { CreateAutomationSelectableFunction } from '~/lib/automate/helpers/automations'
|
||||
import { usePaginatedQuery } from '~/lib/common/composables/graphql'
|
||||
import { Roles, type Nullable } from '@speckle/shared'
|
||||
import type { Nullable } from '@speckle/shared'
|
||||
import type { AutomateOnboardingAction } from '~/components/project/page/automations/EmptyState.vue'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -88,12 +88,27 @@ const workspace = computed(() => result.value?.project?.workspace ?? undefined)
|
||||
const workspaceFunctionCount = computed(
|
||||
() => result.value?.project.workspace?.automateFunctions.totalCount ?? 0
|
||||
)
|
||||
|
||||
const canCreateAutomation = computed(
|
||||
() => result.value?.project?.permissions.canCreateAutomation
|
||||
)
|
||||
|
||||
const hiddenActions = computed<AutomateOnboardingAction[]>(() => {
|
||||
return workspaceFunctionCount.value > 0 ? [] : ['view-functions']
|
||||
})
|
||||
const disabledActions = computed<
|
||||
{ action: AutomateOnboardingAction; reason: string }[]
|
||||
>(() => {
|
||||
if (!canCreateAutomation.value?.authorized) {
|
||||
return [
|
||||
{
|
||||
action: 'create-automation',
|
||||
reason:
|
||||
canCreateAutomation.value?.message ??
|
||||
'You are not authorized to create an automation.'
|
||||
}
|
||||
]
|
||||
}
|
||||
if (workspaceFunctionCount.value === 0) {
|
||||
return [
|
||||
{
|
||||
@@ -103,14 +118,6 @@ const disabledActions = computed<
|
||||
}
|
||||
]
|
||||
}
|
||||
if (result.value?.project?.role !== Roles.Stream.Owner) {
|
||||
return [
|
||||
{
|
||||
action: 'create-automation',
|
||||
reason: 'Only project owners can create new automations.'
|
||||
}
|
||||
]
|
||||
}
|
||||
if ((result.value?.project?.models?.items.length || 0) === 0) {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -266,7 +266,7 @@ type Documents = {
|
||||
"\n query ProjectModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n ...ProjectModelPageVersionsPagination\n }\n }\n": typeof types.ProjectModelVersionsDocument,
|
||||
"\n query ProjectModelsPage($projectId: String!) {\n project(id: $projectId) {\n id\n ...ProjectModelsPageHeader_Project\n ...ProjectModelsPageResults_Project\n }\n }\n": typeof types.ProjectModelsPageDocument,
|
||||
"\n query ProjectDiscussionsPage($projectId: String!) {\n project(id: $projectId) {\n id\n ...ProjectDiscussionsPageHeader_Project\n ...ProjectDiscussionsPageResults_Project\n }\n }\n": typeof types.ProjectDiscussionsPageDocument,
|
||||
"\n query ProjectAutomationsTab($projectId: String!) {\n project(id: $projectId) {\n id\n role\n models(limit: 1) {\n items {\n id\n }\n }\n automations(filter: null, cursor: null, limit: 5) {\n totalCount\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n cursor\n }\n workspace {\n id\n automateFunctions(limit: 0) {\n totalCount\n }\n ...AutomateFunctionCreateDialog_Workspace\n }\n ...FormSelectProjects_Project\n }\n ...AutomateFunctionsPageHeader_Query\n }\n": typeof types.ProjectAutomationsTabDocument,
|
||||
"\n query ProjectAutomationsTab($projectId: String!) {\n project(id: $projectId) {\n id\n role\n models(limit: 1) {\n items {\n id\n }\n }\n automations(filter: null, cursor: null, limit: 5) {\n totalCount\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n cursor\n }\n workspace {\n id\n automateFunctions(limit: 0) {\n totalCount\n }\n ...AutomateFunctionCreateDialog_Workspace\n }\n permissions {\n canCreateAutomation {\n ...FullPermissionCheckResult\n }\n }\n ...FormSelectProjects_Project\n }\n ...AutomateFunctionsPageHeader_Query\n }\n": typeof types.ProjectAutomationsTabDocument,
|
||||
"\n query ProjectAutomationsTabAutomationsPagination(\n $projectId: String!\n $search: String = null\n $cursor: String = null\n ) {\n project(id: $projectId) {\n id\n automations(filter: $search, cursor: $cursor, limit: 5) {\n totalCount\n cursor\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n }\n }\n }\n": typeof types.ProjectAutomationsTabAutomationsPaginationDocument,
|
||||
"\n query ProjectAutomationPage($projectId: String!, $automationId: String!) {\n project(id: $projectId) {\n id\n ...ProjectPageAutomationPage_Project\n automation(id: $automationId) {\n id\n ...ProjectPageAutomationPage_Automation\n }\n }\n }\n": typeof types.ProjectAutomationPageDocument,
|
||||
"\n query ProjectAutomationPagePaginatedRuns(\n $projectId: String!\n $automationId: String!\n $cursor: String = null\n ) {\n project(id: $projectId) {\n id\n automation(id: $automationId) {\n id\n runs(cursor: $cursor, limit: 10) {\n totalCount\n cursor\n items {\n id\n ...AutomationRunDetails\n }\n }\n }\n }\n }\n": typeof types.ProjectAutomationPagePaginatedRunsDocument,
|
||||
@@ -420,7 +420,7 @@ type Documents = {
|
||||
"\n query AutomateFunctionPage($functionId: ID!) {\n automateFunction(id: $functionId) {\n ...AutomateFunctionPage_AutomateFunction\n }\n activeUser {\n workspaces {\n items {\n ...AutomateFunctionCreateDialog_Workspace\n ...AutomateFunctionEditDialog_Workspace\n }\n }\n }\n }\n": typeof types.AutomateFunctionPageDocument,
|
||||
"\n query AutomateFunctionPageWorkspace($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n ...AutomateFunctionPageHeader_Workspace\n }\n }\n": typeof types.AutomateFunctionPageWorkspaceDocument,
|
||||
"\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMoveToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...WorkspaceMoveProjectManager_ProjectBase\n ...ProjectPageSettingsTab_Project\n }\n": typeof types.ProjectPageProjectFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationPage_Automation on Automation {\n id\n ...ProjectPageAutomationHeader_Automation\n ...ProjectPageAutomationFunctions_Automation\n ...ProjectPageAutomationRuns_Automation\n }\n": typeof types.ProjectPageAutomationPage_AutomationFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationPage_Automation on Automation {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageAutomationHeader_Automation\n ...ProjectPageAutomationFunctions_Automation\n ...ProjectPageAutomationRuns_Automation\n }\n": typeof types.ProjectPageAutomationPage_AutomationFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationPage_Project on Project {\n id\n workspaceId\n ...ProjectPageAutomationHeader_Project\n }\n": typeof types.ProjectPageAutomationPage_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectPageSettingsTab_ProjectFragmentDoc,
|
||||
"\n fragment SettingsServerProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": typeof types.SettingsServerProjects_ProjectCollectionFragmentDoc,
|
||||
@@ -687,7 +687,7 @@ const documents: Documents = {
|
||||
"\n query ProjectModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n ...ProjectModelPageVersionsPagination\n }\n }\n": types.ProjectModelVersionsDocument,
|
||||
"\n query ProjectModelsPage($projectId: String!) {\n project(id: $projectId) {\n id\n ...ProjectModelsPageHeader_Project\n ...ProjectModelsPageResults_Project\n }\n }\n": types.ProjectModelsPageDocument,
|
||||
"\n query ProjectDiscussionsPage($projectId: String!) {\n project(id: $projectId) {\n id\n ...ProjectDiscussionsPageHeader_Project\n ...ProjectDiscussionsPageResults_Project\n }\n }\n": types.ProjectDiscussionsPageDocument,
|
||||
"\n query ProjectAutomationsTab($projectId: String!) {\n project(id: $projectId) {\n id\n role\n models(limit: 1) {\n items {\n id\n }\n }\n automations(filter: null, cursor: null, limit: 5) {\n totalCount\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n cursor\n }\n workspace {\n id\n automateFunctions(limit: 0) {\n totalCount\n }\n ...AutomateFunctionCreateDialog_Workspace\n }\n ...FormSelectProjects_Project\n }\n ...AutomateFunctionsPageHeader_Query\n }\n": types.ProjectAutomationsTabDocument,
|
||||
"\n query ProjectAutomationsTab($projectId: String!) {\n project(id: $projectId) {\n id\n role\n models(limit: 1) {\n items {\n id\n }\n }\n automations(filter: null, cursor: null, limit: 5) {\n totalCount\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n cursor\n }\n workspace {\n id\n automateFunctions(limit: 0) {\n totalCount\n }\n ...AutomateFunctionCreateDialog_Workspace\n }\n permissions {\n canCreateAutomation {\n ...FullPermissionCheckResult\n }\n }\n ...FormSelectProjects_Project\n }\n ...AutomateFunctionsPageHeader_Query\n }\n": types.ProjectAutomationsTabDocument,
|
||||
"\n query ProjectAutomationsTabAutomationsPagination(\n $projectId: String!\n $search: String = null\n $cursor: String = null\n ) {\n project(id: $projectId) {\n id\n automations(filter: $search, cursor: $cursor, limit: 5) {\n totalCount\n cursor\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n }\n }\n }\n": types.ProjectAutomationsTabAutomationsPaginationDocument,
|
||||
"\n query ProjectAutomationPage($projectId: String!, $automationId: String!) {\n project(id: $projectId) {\n id\n ...ProjectPageAutomationPage_Project\n automation(id: $automationId) {\n id\n ...ProjectPageAutomationPage_Automation\n }\n }\n }\n": types.ProjectAutomationPageDocument,
|
||||
"\n query ProjectAutomationPagePaginatedRuns(\n $projectId: String!\n $automationId: String!\n $cursor: String = null\n ) {\n project(id: $projectId) {\n id\n automation(id: $automationId) {\n id\n runs(cursor: $cursor, limit: 10) {\n totalCount\n cursor\n items {\n id\n ...AutomationRunDetails\n }\n }\n }\n }\n }\n": types.ProjectAutomationPagePaginatedRunsDocument,
|
||||
@@ -841,7 +841,7 @@ const documents: Documents = {
|
||||
"\n query AutomateFunctionPage($functionId: ID!) {\n automateFunction(id: $functionId) {\n ...AutomateFunctionPage_AutomateFunction\n }\n activeUser {\n workspaces {\n items {\n ...AutomateFunctionCreateDialog_Workspace\n ...AutomateFunctionEditDialog_Workspace\n }\n }\n }\n }\n": types.AutomateFunctionPageDocument,
|
||||
"\n query AutomateFunctionPageWorkspace($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n ...AutomateFunctionPageHeader_Workspace\n }\n }\n": types.AutomateFunctionPageWorkspaceDocument,
|
||||
"\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMoveToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...WorkspaceMoveProjectManager_ProjectBase\n ...ProjectPageSettingsTab_Project\n }\n": types.ProjectPageProjectFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationPage_Automation on Automation {\n id\n ...ProjectPageAutomationHeader_Automation\n ...ProjectPageAutomationFunctions_Automation\n ...ProjectPageAutomationRuns_Automation\n }\n": types.ProjectPageAutomationPage_AutomationFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationPage_Automation on Automation {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageAutomationHeader_Automation\n ...ProjectPageAutomationFunctions_Automation\n ...ProjectPageAutomationRuns_Automation\n }\n": types.ProjectPageAutomationPage_AutomationFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationPage_Project on Project {\n id\n workspaceId\n ...ProjectPageAutomationHeader_Project\n }\n": types.ProjectPageAutomationPage_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectPageSettingsTab_ProjectFragmentDoc,
|
||||
"\n fragment SettingsServerProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": types.SettingsServerProjects_ProjectCollectionFragmentDoc,
|
||||
@@ -1881,7 +1881,7 @@ export function graphql(source: "\n query ProjectDiscussionsPage($projectId: St
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query ProjectAutomationsTab($projectId: String!) {\n project(id: $projectId) {\n id\n role\n models(limit: 1) {\n items {\n id\n }\n }\n automations(filter: null, cursor: null, limit: 5) {\n totalCount\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n cursor\n }\n workspace {\n id\n automateFunctions(limit: 0) {\n totalCount\n }\n ...AutomateFunctionCreateDialog_Workspace\n }\n ...FormSelectProjects_Project\n }\n ...AutomateFunctionsPageHeader_Query\n }\n"): (typeof documents)["\n query ProjectAutomationsTab($projectId: String!) {\n project(id: $projectId) {\n id\n role\n models(limit: 1) {\n items {\n id\n }\n }\n automations(filter: null, cursor: null, limit: 5) {\n totalCount\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n cursor\n }\n workspace {\n id\n automateFunctions(limit: 0) {\n totalCount\n }\n ...AutomateFunctionCreateDialog_Workspace\n }\n ...FormSelectProjects_Project\n }\n ...AutomateFunctionsPageHeader_Query\n }\n"];
|
||||
export function graphql(source: "\n query ProjectAutomationsTab($projectId: String!) {\n project(id: $projectId) {\n id\n role\n models(limit: 1) {\n items {\n id\n }\n }\n automations(filter: null, cursor: null, limit: 5) {\n totalCount\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n cursor\n }\n workspace {\n id\n automateFunctions(limit: 0) {\n totalCount\n }\n ...AutomateFunctionCreateDialog_Workspace\n }\n permissions {\n canCreateAutomation {\n ...FullPermissionCheckResult\n }\n }\n ...FormSelectProjects_Project\n }\n ...AutomateFunctionsPageHeader_Query\n }\n"): (typeof documents)["\n query ProjectAutomationsTab($projectId: String!) {\n project(id: $projectId) {\n id\n role\n models(limit: 1) {\n items {\n id\n }\n }\n automations(filter: null, cursor: null, limit: 5) {\n totalCount\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n cursor\n }\n workspace {\n id\n automateFunctions(limit: 0) {\n totalCount\n }\n ...AutomateFunctionCreateDialog_Workspace\n }\n permissions {\n canCreateAutomation {\n ...FullPermissionCheckResult\n }\n }\n ...FormSelectProjects_Project\n }\n ...AutomateFunctionsPageHeader_Query\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -2497,7 +2497,7 @@ export function graphql(source: "\n fragment ProjectPageProject on Project {\n
|
||||
/**
|
||||
* 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 ProjectPageAutomationPage_Automation on Automation {\n id\n ...ProjectPageAutomationHeader_Automation\n ...ProjectPageAutomationFunctions_Automation\n ...ProjectPageAutomationRuns_Automation\n }\n"): (typeof documents)["\n fragment ProjectPageAutomationPage_Automation on Automation {\n id\n ...ProjectPageAutomationHeader_Automation\n ...ProjectPageAutomationFunctions_Automation\n ...ProjectPageAutomationRuns_Automation\n }\n"];
|
||||
export function graphql(source: "\n fragment ProjectPageAutomationPage_Automation on Automation {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageAutomationHeader_Automation\n ...ProjectPageAutomationFunctions_Automation\n ...ProjectPageAutomationRuns_Automation\n }\n"): (typeof documents)["\n fragment ProjectPageAutomationPage_Automation on Automation {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageAutomationHeader_Automation\n ...ProjectPageAutomationFunctions_Automation\n ...ProjectPageAutomationRuns_Automation\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
@@ -240,6 +240,11 @@ export const projectAutomationsTabQuery = graphql(`
|
||||
}
|
||||
...AutomateFunctionCreateDialog_Workspace
|
||||
}
|
||||
permissions {
|
||||
canCreateAutomation {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
...FormSelectProjects_Project
|
||||
}
|
||||
...AutomateFunctionsPageHeader_Query
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
<div v-else />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import { projectAutomationPageQuery } from '~/lib/projects/graphql/queries'
|
||||
@@ -38,6 +37,11 @@ import { projectAutomationPageQuery } from '~/lib/projects/graphql/queries'
|
||||
graphql(`
|
||||
fragment ProjectPageAutomationPage_Automation on Automation {
|
||||
id
|
||||
permissions {
|
||||
canUpdate {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
...ProjectPageAutomationHeader_Automation
|
||||
...ProjectPageAutomationFunctions_Automation
|
||||
...ProjectPageAutomationRuns_Automation
|
||||
@@ -71,7 +75,6 @@ const automation = computed(() => result.value?.project.automation || null)
|
||||
const project = computed(() => result.value?.project)
|
||||
const workspaceId = computed(() => project.value?.workspaceId ?? undefined)
|
||||
const isEditable = computed(() => {
|
||||
const allowedRoles: string[] = [Roles.Stream.Owner]
|
||||
return allowedRoles.includes(result.value?.project.role ?? '')
|
||||
return result?.value?.project?.automation?.permissions?.canUpdate.authorized ?? false
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -338,11 +338,10 @@ type AutomateMutations {
|
||||
|
||||
extend type Project {
|
||||
automations(filter: String, cursor: String, limit: Int): AutomationCollection!
|
||||
@hasStreamRole(role: STREAM_REVIEWER)
|
||||
"""
|
||||
Get a single automation by id. Error will be thrown if automation is not found or inaccessible.
|
||||
"""
|
||||
automation(id: String!): Automation! @hasStreamRole(role: STREAM_REVIEWER)
|
||||
automation(id: String!): Automation!
|
||||
}
|
||||
|
||||
input AutomateAuthCodePayloadTest {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
extend type Automation {
|
||||
permissions: AutomationPermissionChecks!
|
||||
}
|
||||
|
||||
type AutomationPermissionChecks {
|
||||
canRead: PermissionCheckResult!
|
||||
canUpdate: PermissionCheckResult!
|
||||
}
|
||||
|
||||
extend type ProjectPermissionChecks {
|
||||
canCreateAutomation: PermissionCheckResult!
|
||||
}
|
||||
@@ -13,7 +13,7 @@ enum ProjectVisibility {
|
||||
}
|
||||
|
||||
"""
|
||||
Visbility without the "discoverable" option
|
||||
Visibility without the "discoverable" option
|
||||
"""
|
||||
enum SimpleProjectVisibility {
|
||||
PRIVATE
|
||||
|
||||
@@ -47,6 +47,7 @@ generates:
|
||||
AutomateFunction: '@/modules/automate/helpers/graphTypes#AutomateFunctionGraphQLReturn'
|
||||
AutomateFunctionRelease: '@/modules/automate/helpers/graphTypes#AutomateFunctionReleaseGraphQLReturn'
|
||||
Automation: '@/modules/automate/helpers/graphTypes#AutomationGraphQLReturn'
|
||||
AutomationPermissionChecks: '@/modules/automate/helpers/graphTypes#AutomationPermissionChecksGraphQLReturn'
|
||||
AutomationRevision: '@/modules/automate/helpers/graphTypes#AutomationRevisionGraphQLReturn'
|
||||
AutomationRevisionFunction: '@/modules/automate/helpers/graphTypes#AutomationRevisionFunctionGraphQLReturn'
|
||||
AutomateRun: '@/modules/automate/helpers/graphTypes#AutomateRunGraphQLReturn'
|
||||
|
||||
@@ -122,6 +122,7 @@ import {
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import { BranchNotFoundError } from '@/modules/core/errors/branch'
|
||||
import { mapAuthToServerError } from '@/modules/shared/helpers/errorHelper'
|
||||
import { withOperationLogging } from '@/observability/domain/businessLogging'
|
||||
|
||||
const { FF_AUTOMATE_MODULE_ENABLED } = getFeatureFlags()
|
||||
@@ -209,6 +210,14 @@ export = (FF_AUTOMATE_MODULE_ENABLED
|
||||
},
|
||||
Project: {
|
||||
async automation(parent, args, ctx) {
|
||||
const canReadAutomation = await ctx.authPolicies.project.automation.canRead({
|
||||
userId: ctx.userId,
|
||||
projectId: parent.id
|
||||
})
|
||||
if (!canReadAutomation.isOk) {
|
||||
throw mapAuthToServerError(canReadAutomation.error)
|
||||
}
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: parent.id })
|
||||
|
||||
const res = ctx.loaders
|
||||
@@ -224,7 +233,15 @@ export = (FF_AUTOMATE_MODULE_ENABLED
|
||||
|
||||
return res
|
||||
},
|
||||
async automations(parent, args) {
|
||||
async automations(parent, args, ctx) {
|
||||
const canReadAutomation = await ctx.authPolicies.project.automation.canRead({
|
||||
userId: ctx.userId,
|
||||
projectId: parent.id
|
||||
})
|
||||
if (!canReadAutomation.isOk) {
|
||||
throw mapAuthToServerError(canReadAutomation.error)
|
||||
}
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: parent.id })
|
||||
|
||||
const retrievalArgs: GetProjectAutomationsParams = {
|
||||
@@ -607,6 +624,14 @@ export = (FF_AUTOMATE_MODULE_ENABLED
|
||||
},
|
||||
ProjectAutomationMutations: {
|
||||
async create(parent, { input }, ctx) {
|
||||
const canCreate = await ctx.authPolicies.project.automation.canCreate({
|
||||
userId: ctx.userId,
|
||||
projectId: parent.projectId
|
||||
})
|
||||
if (!canCreate.isOk) {
|
||||
throw mapAuthToServerError(canCreate.error)
|
||||
}
|
||||
|
||||
const projectId = parent.projectId
|
||||
|
||||
const logger = ctx.log.child({
|
||||
@@ -621,7 +646,6 @@ export = (FF_AUTOMATE_MODULE_ENABLED
|
||||
automateCreateAutomation: clientCreateAutomation,
|
||||
storeAutomation: storeAutomationFactory({ db: projectDb }),
|
||||
storeAutomationToken: storeAutomationTokenFactory({ db: projectDb }),
|
||||
validateStreamAccess,
|
||||
eventEmit: getEventBus().emit
|
||||
})
|
||||
|
||||
@@ -643,6 +667,14 @@ export = (FF_AUTOMATE_MODULE_ENABLED
|
||||
return automation
|
||||
},
|
||||
async update(parent, { input }, ctx) {
|
||||
const canUpdate = await ctx.authPolicies.project.automation.canUpdate({
|
||||
userId: ctx.userId,
|
||||
projectId: parent.projectId
|
||||
})
|
||||
if (!canUpdate.isOk) {
|
||||
throw mapAuthToServerError(canUpdate.error)
|
||||
}
|
||||
|
||||
const projectId = parent.projectId
|
||||
const automationId = input.id
|
||||
|
||||
@@ -657,7 +689,6 @@ export = (FF_AUTOMATE_MODULE_ENABLED
|
||||
const update = validateAndUpdateAutomationFactory({
|
||||
getAutomation: getAutomationFactory({ db: projectDb }),
|
||||
updateAutomation: updateAutomationFactory({ db: projectDb }),
|
||||
validateStreamAccess,
|
||||
eventEmit: getEventBus().emit
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Resolvers } from '@/modules/core/graph/generated/graphql'
|
||||
import { Authz } from '@speckle/shared'
|
||||
|
||||
export default {
|
||||
Automation: {
|
||||
permissions: (parent) => ({ projectId: parent.projectId })
|
||||
},
|
||||
AutomationPermissionChecks: {
|
||||
canRead: async (parent, _args, context) => {
|
||||
const canReadAutomation = await context.authPolicies.project.automation.canRead({
|
||||
userId: context.userId,
|
||||
projectId: parent.projectId
|
||||
})
|
||||
return Authz.toGraphqlResult(canReadAutomation)
|
||||
},
|
||||
canUpdate: async (parent, _args, context) => {
|
||||
const canUpdateAutomation =
|
||||
await context.authPolicies.project.automation.canUpdate({
|
||||
userId: context.userId,
|
||||
projectId: parent.projectId
|
||||
})
|
||||
return Authz.toGraphqlResult(canUpdateAutomation)
|
||||
}
|
||||
},
|
||||
ProjectPermissionChecks: {
|
||||
canCreateAutomation: async (parent, _args, context) => {
|
||||
const canCreateAutomation =
|
||||
await context.authPolicies.project.automation.canCreate({
|
||||
userId: context.userId,
|
||||
projectId: parent.projectId
|
||||
})
|
||||
return Authz.toGraphqlResult(canCreateAutomation)
|
||||
}
|
||||
}
|
||||
} as Resolvers
|
||||
@@ -95,3 +95,5 @@ export type ProjectAutomationsUpdatedMessageGraphQLReturn = Merge<
|
||||
>
|
||||
|
||||
export type UserAutomateInfoGraphQLReturn = { userId: string }
|
||||
|
||||
export type AutomationPermissionChecksGraphQLReturn = { projectId: string }
|
||||
|
||||
@@ -60,7 +60,6 @@ export type CreateAutomationDeps = {
|
||||
automateCreateAutomation: typeof clientCreateAutomation
|
||||
storeAutomation: StoreAutomation
|
||||
storeAutomationToken: StoreAutomationToken
|
||||
validateStreamAccess: ValidateStreamAccess
|
||||
eventEmit: EventBusEmit
|
||||
}
|
||||
|
||||
@@ -75,27 +74,18 @@ export const createAutomationFactory =
|
||||
const {
|
||||
input: { name, enabled },
|
||||
projectId,
|
||||
userId,
|
||||
userResourceAccessRules
|
||||
userId
|
||||
} = params
|
||||
const {
|
||||
createAuthCode,
|
||||
automateCreateAutomation,
|
||||
storeAutomation,
|
||||
storeAutomationToken,
|
||||
validateStreamAccess,
|
||||
eventEmit
|
||||
} = deps
|
||||
|
||||
validateAutomationName(name)
|
||||
|
||||
await validateStreamAccess(
|
||||
userId,
|
||||
projectId,
|
||||
Roles.Stream.Owner,
|
||||
userResourceAccessRules
|
||||
)
|
||||
|
||||
const authCode = await createAuthCode({
|
||||
userId,
|
||||
action: AuthCodePayloadAction.CreateAutomation
|
||||
@@ -253,7 +243,6 @@ export const createTestAutomationFactory =
|
||||
export type ValidateAndUpdateAutomationDeps = {
|
||||
getAutomation: GetAutomation
|
||||
updateAutomation: UpdateAutomation
|
||||
validateStreamAccess: ValidateStreamAccess
|
||||
eventEmit: EventBusEmit
|
||||
}
|
||||
|
||||
@@ -268,8 +257,8 @@ export const validateAndUpdateAutomationFactory =
|
||||
*/
|
||||
projectId?: string
|
||||
}) => {
|
||||
const { getAutomation, updateAutomation, validateStreamAccess, eventEmit } = deps
|
||||
const { input, userId, userResourceAccessRules, projectId } = params
|
||||
const { getAutomation, updateAutomation, eventEmit } = deps
|
||||
const { input, projectId } = params
|
||||
|
||||
const existingAutomation = await getAutomation({
|
||||
automationId: input.id,
|
||||
@@ -279,13 +268,6 @@ export const validateAndUpdateAutomationFactory =
|
||||
throw new AutomationUpdateError('Automation not found')
|
||||
}
|
||||
|
||||
await validateStreamAccess(
|
||||
userId,
|
||||
existingAutomation.projectId,
|
||||
Roles.Stream.Owner,
|
||||
userResourceAccessRules
|
||||
)
|
||||
|
||||
// Filter out empty (null) values from input
|
||||
const updates = removeNullOrUndefinedKeys(input)
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@ const buildAutomationUpdate = () => {
|
||||
const update = validateAndUpdateAutomationFactory({
|
||||
getAutomation,
|
||||
updateAutomation: updateDbAutomation,
|
||||
validateStreamAccess,
|
||||
eventEmit: getEventBus().emit
|
||||
})
|
||||
|
||||
@@ -136,38 +135,6 @@ const buildAutomationUpdate = () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if refering to a project that doesnt exist', async () => {
|
||||
const create = buildAutomationCreate()
|
||||
|
||||
const e = await expectToThrow(
|
||||
async () =>
|
||||
await create({
|
||||
input: { name: 'Automation', enabled: true },
|
||||
projectId: 'non-existent',
|
||||
userId: me.id
|
||||
})
|
||||
)
|
||||
expect(e)
|
||||
.to.have.property('message')
|
||||
.match(/^User does not have required access to stream/)
|
||||
})
|
||||
|
||||
it('fails if user does not have access to the project', async () => {
|
||||
const create = buildAutomationCreate()
|
||||
|
||||
const e = await expectToThrow(
|
||||
async () =>
|
||||
await create({
|
||||
input: { name: 'Automation', enabled: true },
|
||||
projectId: myStream.id,
|
||||
userId: otherGuy.id
|
||||
})
|
||||
)
|
||||
expect(e)
|
||||
.to.have.property('message')
|
||||
.match(/^User does not have required access to stream/)
|
||||
})
|
||||
|
||||
it('creates an automation', async () => {
|
||||
let eventFired = false
|
||||
const name = 'My Super Automation #1'
|
||||
@@ -222,22 +189,6 @@ const buildAutomationUpdate = () => {
|
||||
expect(e).to.have.property('message', 'Automation not found')
|
||||
})
|
||||
|
||||
it('fails if refering to an automation in a project owned by someone else', async () => {
|
||||
const update = buildAutomationUpdate()
|
||||
|
||||
const e = await expectToThrow(
|
||||
async () =>
|
||||
await update({
|
||||
input: { id: createdAutomation.automation.id, enabled: false },
|
||||
userId: otherGuy.id,
|
||||
projectId: myStream.id
|
||||
})
|
||||
)
|
||||
expect(e)
|
||||
.to.have.property('message')
|
||||
.match(/^User does not have required access to stream/)
|
||||
})
|
||||
|
||||
it('fails if automation is mismatched with specified project id', async () => {
|
||||
const update = buildAutomationUpdate()
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { StreamAccessRequestGraphQLReturn, ProjectAccessRequestGraphQLReturn } f
|
||||
import { CommentReplyAuthorCollectionGraphQLReturn, CommentGraphQLReturn, CommentPermissionChecksGraphQLReturn } from '@/modules/comments/helpers/graphTypes';
|
||||
import { PendingStreamCollaboratorGraphQLReturn } from '@/modules/serverinvites/helpers/graphTypes';
|
||||
import { FileUploadGraphQLReturn } from '@/modules/fileuploads/helpers/types';
|
||||
import { AutomateFunctionGraphQLReturn, AutomateFunctionReleaseGraphQLReturn, AutomationGraphQLReturn, AutomationRevisionGraphQLReturn, AutomationRevisionFunctionGraphQLReturn, AutomateRunGraphQLReturn, AutomationRunTriggerGraphQLReturn, AutomationRevisionTriggerDefinitionGraphQLReturn, AutomateFunctionRunGraphQLReturn, TriggeredAutomationsStatusGraphQLReturn, ProjectAutomationMutationsGraphQLReturn, ProjectTriggeredAutomationsStatusUpdatedMessageGraphQLReturn, ProjectAutomationsUpdatedMessageGraphQLReturn, UserAutomateInfoGraphQLReturn } from '@/modules/automate/helpers/graphTypes';
|
||||
import { AutomateFunctionGraphQLReturn, AutomateFunctionReleaseGraphQLReturn, AutomationGraphQLReturn, AutomationPermissionChecksGraphQLReturn, AutomationRevisionGraphQLReturn, AutomationRevisionFunctionGraphQLReturn, AutomateRunGraphQLReturn, AutomationRunTriggerGraphQLReturn, AutomationRevisionTriggerDefinitionGraphQLReturn, AutomateFunctionRunGraphQLReturn, TriggeredAutomationsStatusGraphQLReturn, ProjectAutomationMutationsGraphQLReturn, ProjectTriggeredAutomationsStatusUpdatedMessageGraphQLReturn, ProjectAutomationsUpdatedMessageGraphQLReturn, UserAutomateInfoGraphQLReturn } from '@/modules/automate/helpers/graphTypes';
|
||||
import { WorkspaceGraphQLReturn, WorkspaceSsoGraphQLReturn, WorkspaceMutationsGraphQLReturn, WorkspaceJoinRequestMutationsGraphQLReturn, WorkspaceInviteMutationsGraphQLReturn, WorkspaceProjectMutationsGraphQLReturn, PendingWorkspaceCollaboratorGraphQLReturn, WorkspaceCollaboratorGraphQLReturn, WorkspaceJoinRequestGraphQLReturn, LimitedWorkspaceJoinRequestGraphQLReturn, ProjectMoveToWorkspaceDryRunGraphQLReturn, ProjectRoleGraphQLReturn, WorkspacePermissionChecksGraphQLReturn } from '@/modules/workspacesCore/helpers/graphTypes';
|
||||
import { WorkspacePlanGraphQLReturn, WorkspacePlanUsageGraphQLReturn, PriceGraphQLReturn } from '@/modules/gatekeeperCore/helpers/graphTypes';
|
||||
import { WorkspaceBillingMutationsGraphQLReturn, WorkspaceSubscriptionSeatsGraphQLReturn, WorkspaceSubscriptionGraphQLReturn } from '@/modules/gatekeeper/helpers/graphTypes';
|
||||
@@ -454,6 +454,7 @@ export type Automation = {
|
||||
id: Scalars['ID']['output'];
|
||||
isTestAutomation: Scalars['Boolean']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
permissions: AutomationPermissionChecks;
|
||||
runs: AutomateRunCollection;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
@@ -471,6 +472,12 @@ export type AutomationCollection = {
|
||||
totalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type AutomationPermissionChecks = {
|
||||
__typename?: 'AutomationPermissionChecks';
|
||||
canRead: PermissionCheckResult;
|
||||
canUpdate: PermissionCheckResult;
|
||||
};
|
||||
|
||||
export type AutomationRevision = {
|
||||
__typename?: 'AutomationRevision';
|
||||
functions: Array<AutomationRevisionFunction>;
|
||||
@@ -2589,6 +2596,7 @@ export type ProjectPendingVersionsUpdatedMessageType = typeof ProjectPendingVers
|
||||
export type ProjectPermissionChecks = {
|
||||
__typename?: 'ProjectPermissionChecks';
|
||||
canBroadcastActivity: PermissionCheckResult;
|
||||
canCreateAutomation: PermissionCheckResult;
|
||||
canCreateComment: PermissionCheckResult;
|
||||
canCreateModel: PermissionCheckResult;
|
||||
canDelete: PermissionCheckResult;
|
||||
@@ -3244,7 +3252,7 @@ export type SetPrimaryUserEmailInput = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
/** Visbility without the "discoverable" option */
|
||||
/** Visibility without the "discoverable" option */
|
||||
export const SimpleProjectVisibility = {
|
||||
Private: 'PRIVATE',
|
||||
Unlisted: 'UNLISTED'
|
||||
@@ -5232,6 +5240,7 @@ export type ResolversTypes = {
|
||||
AutomateRunTriggerType: AutomateRunTriggerType;
|
||||
Automation: ResolverTypeWrapper<AutomationGraphQLReturn>;
|
||||
AutomationCollection: ResolverTypeWrapper<Omit<AutomationCollection, 'items'> & { items: Array<ResolversTypes['Automation']> }>;
|
||||
AutomationPermissionChecks: ResolverTypeWrapper<AutomationPermissionChecksGraphQLReturn>;
|
||||
AutomationRevision: ResolverTypeWrapper<AutomationRevisionGraphQLReturn>;
|
||||
AutomationRevisionCreateFunctionInput: AutomationRevisionCreateFunctionInput;
|
||||
AutomationRevisionFunction: ResolverTypeWrapper<AutomationRevisionFunctionGraphQLReturn>;
|
||||
@@ -5568,6 +5577,7 @@ export type ResolversParentTypes = {
|
||||
AutomateRunCollection: Omit<AutomateRunCollection, 'items'> & { items: Array<ResolversParentTypes['AutomateRun']> };
|
||||
Automation: AutomationGraphQLReturn;
|
||||
AutomationCollection: Omit<AutomationCollection, 'items'> & { items: Array<ResolversParentTypes['Automation']> };
|
||||
AutomationPermissionChecks: AutomationPermissionChecksGraphQLReturn;
|
||||
AutomationRevision: AutomationRevisionGraphQLReturn;
|
||||
AutomationRevisionCreateFunctionInput: AutomationRevisionCreateFunctionInput;
|
||||
AutomationRevisionFunction: AutomationRevisionFunctionGraphQLReturn;
|
||||
@@ -6077,6 +6087,7 @@ export type AutomationResolvers<ContextType = GraphQLContext, ParentType extends
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
isTestAutomation?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
permissions?: Resolver<ResolversTypes['AutomationPermissionChecks'], ParentType, ContextType>;
|
||||
runs?: Resolver<ResolversTypes['AutomateRunCollection'], ParentType, ContextType, Partial<AutomationRunsArgs>>;
|
||||
updatedAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
@@ -6089,6 +6100,12 @@ export type AutomationCollectionResolvers<ContextType = GraphQLContext, ParentTy
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type AutomationPermissionChecksResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AutomationPermissionChecks'] = ResolversParentTypes['AutomationPermissionChecks']> = {
|
||||
canRead?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canUpdate?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type AutomationRevisionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AutomationRevision'] = ResolversParentTypes['AutomationRevision']> = {
|
||||
functions?: Resolver<Array<ResolversTypes['AutomationRevisionFunction']>, ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
@@ -6768,6 +6785,7 @@ export type ProjectPendingVersionsUpdatedMessageResolvers<ContextType = GraphQLC
|
||||
|
||||
export type ProjectPermissionChecksResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['ProjectPermissionChecks'] = ResolversParentTypes['ProjectPermissionChecks']> = {
|
||||
canBroadcastActivity?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canCreateAutomation?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canCreateComment?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canCreateModel?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canDelete?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
@@ -7654,6 +7672,7 @@ export type Resolvers<ContextType = GraphQLContext> = {
|
||||
AutomateRunCollection?: AutomateRunCollectionResolvers<ContextType>;
|
||||
Automation?: AutomationResolvers<ContextType>;
|
||||
AutomationCollection?: AutomationCollectionResolvers<ContextType>;
|
||||
AutomationPermissionChecks?: AutomationPermissionChecksResolvers<ContextType>;
|
||||
AutomationRevision?: AutomationRevisionResolvers<ContextType>;
|
||||
AutomationRevisionFunction?: AutomationRevisionFunctionResolvers<ContextType>;
|
||||
AutomationRevisionTriggerDefinition?: AutomationRevisionTriggerDefinitionResolvers<ContextType>;
|
||||
|
||||
@@ -434,6 +434,7 @@ export type Automation = {
|
||||
id: Scalars['ID']['output'];
|
||||
isTestAutomation: Scalars['Boolean']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
permissions: AutomationPermissionChecks;
|
||||
runs: AutomateRunCollection;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
@@ -451,6 +452,12 @@ export type AutomationCollection = {
|
||||
totalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type AutomationPermissionChecks = {
|
||||
__typename?: 'AutomationPermissionChecks';
|
||||
canRead: PermissionCheckResult;
|
||||
canUpdate: PermissionCheckResult;
|
||||
};
|
||||
|
||||
export type AutomationRevision = {
|
||||
__typename?: 'AutomationRevision';
|
||||
functions: Array<AutomationRevisionFunction>;
|
||||
@@ -2569,6 +2576,7 @@ export type ProjectPendingVersionsUpdatedMessageType = typeof ProjectPendingVers
|
||||
export type ProjectPermissionChecks = {
|
||||
__typename?: 'ProjectPermissionChecks';
|
||||
canBroadcastActivity: PermissionCheckResult;
|
||||
canCreateAutomation: PermissionCheckResult;
|
||||
canCreateComment: PermissionCheckResult;
|
||||
canCreateModel: PermissionCheckResult;
|
||||
canDelete: PermissionCheckResult;
|
||||
@@ -3224,7 +3232,7 @@ export type SetPrimaryUserEmailInput = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
/** Visbility without the "discoverable" option */
|
||||
/** Visibility without the "discoverable" option */
|
||||
export const SimpleProjectVisibility = {
|
||||
Private: 'PRIVATE',
|
||||
Unlisted: 'UNLISTED'
|
||||
|
||||
@@ -435,6 +435,7 @@ export type Automation = {
|
||||
id: Scalars['ID']['output'];
|
||||
isTestAutomation: Scalars['Boolean']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
permissions: AutomationPermissionChecks;
|
||||
runs: AutomateRunCollection;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
@@ -452,6 +453,12 @@ export type AutomationCollection = {
|
||||
totalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type AutomationPermissionChecks = {
|
||||
__typename?: 'AutomationPermissionChecks';
|
||||
canRead: PermissionCheckResult;
|
||||
canUpdate: PermissionCheckResult;
|
||||
};
|
||||
|
||||
export type AutomationRevision = {
|
||||
__typename?: 'AutomationRevision';
|
||||
functions: Array<AutomationRevisionFunction>;
|
||||
@@ -2570,6 +2577,7 @@ export type ProjectPendingVersionsUpdatedMessageType = typeof ProjectPendingVers
|
||||
export type ProjectPermissionChecks = {
|
||||
__typename?: 'ProjectPermissionChecks';
|
||||
canBroadcastActivity: PermissionCheckResult;
|
||||
canCreateAutomation: PermissionCheckResult;
|
||||
canCreateComment: PermissionCheckResult;
|
||||
canCreateModel: PermissionCheckResult;
|
||||
canDelete: PermissionCheckResult;
|
||||
@@ -3225,7 +3233,7 @@ export type SetPrimaryUserEmailInput = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
/** Visbility without the "discoverable" option */
|
||||
/** Visibility without the "discoverable" option */
|
||||
export const SimpleProjectVisibility = {
|
||||
Private: 'PRIVATE',
|
||||
Unlisted: 'UNLISTED'
|
||||
|
||||
@@ -80,7 +80,6 @@ export const buildAutomationCreate = (
|
||||
})),
|
||||
storeAutomation: storeAutomationFactory({ db: dbClient }),
|
||||
storeAutomationToken: storeAutomationTokenFactory({ db: dbClient }),
|
||||
validateStreamAccess,
|
||||
eventEmit: getEventBus().emit
|
||||
})
|
||||
|
||||
|
||||
@@ -21,11 +21,19 @@ import { canCreateProjectVersionPolicy } from './project/version/canCreate.js'
|
||||
import { canUpdateProjectVersionPolicy } from './project/version/canUpdate.js'
|
||||
import { canReceiveProjectVersionPolicy } from './project/version/canReceive.js'
|
||||
import { canRequestProjectVersionRenderPolicy } from './project/version/canRequestRender.js'
|
||||
import { canCreateAutomationPolicy } from './project/automation/canCreate.js'
|
||||
import { canUpdateAutomationPolicy } from './project/automation/canUpdate.js'
|
||||
import { canReadAutomationPolicy } from './project/automation/canRead.js'
|
||||
import { canReceiveWorkspaceProjectsUpdatedMessagePolicy } from './workspace/canReceiveProjectsUpdatedMessage.js'
|
||||
import { canDeleteProjectPolicy } from './project/canDelete.js'
|
||||
|
||||
export const authPoliciesFactory = (loaders: AllAuthCheckContextLoaders) => ({
|
||||
project: {
|
||||
automation: {
|
||||
canCreate: canCreateAutomationPolicy(loaders),
|
||||
canRead: canReadAutomationPolicy(loaders),
|
||||
canUpdate: canUpdateAutomationPolicy(loaders)
|
||||
},
|
||||
model: {
|
||||
canCreate: canCreateModelPolicy(loaders),
|
||||
canUpdate: canUpdateModelPolicy(loaders),
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
import { parseFeatureFlags } from '../../../../environment/index.js'
|
||||
import { canCreateAutomationPolicy } from './canCreate.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotEnoughPermissionsError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
ServerNotEnoughPermissionsError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
|
||||
const buildCanCreatePolicy = (
|
||||
overrides?: Partial<Parameters<typeof canCreateAutomationPolicy>[0]>
|
||||
) =>
|
||||
canCreateAutomationPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: async () => ({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
allowPublicComments: false
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Owner,
|
||||
getServerRole: async () => Roles.Server.User,
|
||||
getWorkspace: async () => null,
|
||||
getWorkspaceRole: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
...overrides
|
||||
})
|
||||
|
||||
describe('canCreateAutomation', () => {
|
||||
it('returns error if user is not logged in', async () => {
|
||||
const canCreateAutomation = buildCanCreatePolicy()
|
||||
|
||||
const result = await canCreateAutomation({
|
||||
userId: undefined,
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoSessionError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if user is not found', async () => {
|
||||
const canCreateAutomation = buildCanCreatePolicy({
|
||||
getServerRole: async () => null
|
||||
})
|
||||
|
||||
const result = await canCreateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if user is a server guest', async () => {
|
||||
const canCreateAutomation = buildCanCreatePolicy({
|
||||
getServerRole: async () => Roles.Server.Guest
|
||||
})
|
||||
|
||||
const result = await canCreateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNotEnoughPermissionsError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error project not found', async () => {
|
||||
const canCreateAutomation = buildCanCreatePolicy({
|
||||
getProject: async () => null
|
||||
})
|
||||
|
||||
const result = await canCreateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNotFoundError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if no role at all', async () => {
|
||||
const canCreateAutomation = buildCanCreatePolicy({
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
const result = await canCreateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if not owner', async () => {
|
||||
const canCreateAutomation = buildCanCreatePolicy({
|
||||
getProjectRole: async () => Roles.Stream.Reviewer
|
||||
})
|
||||
const result = await canCreateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNotEnoughPermissionsError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns ok if permissible', async () => {
|
||||
const canCreateAutomation = buildCanCreatePolicy()
|
||||
const result = await canCreateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
describe('with workspace project', () => {
|
||||
const overrides = {
|
||||
getProject: async () => ({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
allowPublicComments: false
|
||||
}),
|
||||
getWorkspace: async () => ({
|
||||
id: 'workspace-id',
|
||||
slug: 'workspace-slug'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getWorkspaceSsoProvider: async () => ({
|
||||
providerId: 'provider-id'
|
||||
}),
|
||||
getWorkspaceSsoSession: async () => {
|
||||
const validUntil = new Date()
|
||||
validUntil.setDate(validUntil.getDate() + 7)
|
||||
return {
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('returns ok if permissible', async () => {
|
||||
const canCreateAutomation = buildCanCreatePolicy(overrides)
|
||||
const result = await canCreateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('returns ok with implicit owner role', async () => {
|
||||
const canCreateAutomation = buildCanCreatePolicy({
|
||||
...overrides,
|
||||
getWorkspaceRole: async () => Roles.Workspace.Admin,
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
const result = await canCreateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('returns error if no workspace role, even w/ valid project role', async () => {
|
||||
const canCreateAutomation = buildCanCreatePolicy({
|
||||
...overrides,
|
||||
getWorkspaceRole: async () => null,
|
||||
getProjectRole: async () => Roles.Stream.Owner
|
||||
})
|
||||
|
||||
const result = await canCreateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if invalid workspace and project role', async () => {
|
||||
const canCreateAutomation = buildCanCreatePolicy({
|
||||
...overrides,
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getProjectRole: async () => Roles.Stream.Contributor
|
||||
})
|
||||
const result = await canCreateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNotEnoughPermissionsError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns ok if no sso configured', async () => {
|
||||
const canCreateAutomation = buildCanCreatePolicy({
|
||||
...overrides,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
const result = await canCreateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('returns error if no sso session', async () => {
|
||||
const canCreateAutomation = buildCanCreatePolicy({
|
||||
...overrides,
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
const result = await canCreateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if sso expired', async () => {
|
||||
const canCreateAutomation = buildCanCreatePolicy({
|
||||
...overrides,
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date(new Date().getTime() - 1000)
|
||||
})
|
||||
})
|
||||
const result = await canCreateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
import { err, 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 { ensureImplicitProjectMemberWithWriteAccessFragment } from '../../../fragments/projects.js'
|
||||
import { Loaders } from '../../../domain/loaders.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotEnoughPermissionsError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
ServerNotEnoughPermissionsError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceNotEnoughPermissionsError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
|
||||
type PolicyLoaderKeys =
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole
|
||||
|
||||
type PolicyArgs = ProjectContext & MaybeUserContext
|
||||
|
||||
type PolicyErrors = InstanceType<
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof ServerNotEnoughPermissionsError
|
||||
| typeof WorkspaceSsoSessionNoAccessError
|
||||
| typeof WorkspaceNotEnoughPermissionsError
|
||||
| typeof ProjectNotEnoughPermissionsError
|
||||
>
|
||||
|
||||
export const canCreateAutomationPolicy: AuthPolicy<
|
||||
PolicyLoaderKeys,
|
||||
PolicyArgs,
|
||||
PolicyErrors
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId }) => {
|
||||
// Project owners may create automations
|
||||
const ensuredWriteAccess = await ensureImplicitProjectMemberWithWriteAccessFragment(
|
||||
loaders
|
||||
)({
|
||||
userId,
|
||||
projectId,
|
||||
role: Roles.Stream.Owner
|
||||
})
|
||||
if (ensuredWriteAccess.isErr) {
|
||||
return err(ensuredWriteAccess.error)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { OverridesOf } from '../../../../tests/helpers/types.js'
|
||||
import { canReadAutomationPolicy } from './canRead.js'
|
||||
import { parseFeatureFlags } from '../../../../environment/index.js'
|
||||
import { getProjectFake, getWorkspaceFake } from '../../../../tests/fakes.js'
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
|
||||
const buildCanReadAutomationPolicy = (
|
||||
overrides?: OverridesOf<typeof canReadAutomationPolicy>
|
||||
) =>
|
||||
canReadAutomationPolicy({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isPublic: false,
|
||||
isDiscoverable: false
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Reviewer,
|
||||
getAdminOverrideEnabled: async () => false,
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getServerRole: async () => Roles.Server.Guest,
|
||||
getWorkspaceRole: async () => null,
|
||||
getWorkspace: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
...overrides
|
||||
})
|
||||
|
||||
describe('canReadAutomationPolicy', () => {
|
||||
it('should allow for reviewers+', async () => {
|
||||
const canReadAutomation = buildCanReadAutomationPolicy()
|
||||
|
||||
const result = await canReadAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('should allow for admin w/ override', async () => {
|
||||
const canReadAutomation = buildCanReadAutomationPolicy({
|
||||
getAdminOverrideEnabled: async () => true,
|
||||
getServerRole: async () => Roles.Server.Admin,
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await canReadAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails without user', async () => {
|
||||
const canReadAutomation = buildCanReadAutomationPolicy()
|
||||
|
||||
const result = await canReadAutomation({
|
||||
userId: undefined,
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoSessionError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user not found', async () => {
|
||||
const canReadAutomation = buildCanReadAutomationPolicy({
|
||||
getServerRole: async () => null
|
||||
})
|
||||
|
||||
const result = await canReadAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user has no project role', async () => {
|
||||
const canReadAutomation = buildCanReadAutomationPolicy({
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await canReadAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if project not found', async () => {
|
||||
const canReadAutomation = buildCanReadAutomationPolicy({
|
||||
getProject: async () => null
|
||||
})
|
||||
|
||||
const result = await canReadAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNotFoundError.code
|
||||
})
|
||||
})
|
||||
|
||||
describe('with workspace project', () => {
|
||||
const overrides = {
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isPublic: false,
|
||||
isDiscoverable: false
|
||||
}),
|
||||
getWorkspace: getWorkspaceFake({
|
||||
id: 'workspace-id'
|
||||
}),
|
||||
getProjectRole: async () => null,
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date(Date.now() + 1000 * 60 * 60)
|
||||
}),
|
||||
getWorkspaceSsoProvider: async () => ({
|
||||
providerId: 'provider-id'
|
||||
})
|
||||
}
|
||||
|
||||
it('succeeds w/ implicit project role', async () => {
|
||||
const canReadAutomation = buildCanReadAutomationPolicy(overrides)
|
||||
|
||||
const result = await canReadAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails w/o workspace role, even w/ project role', async () => {
|
||||
const canReadAutomation = buildCanReadAutomationPolicy({
|
||||
...overrides,
|
||||
getProjectRole: async () => Roles.Stream.Reviewer,
|
||||
getWorkspaceRole: async () => null
|
||||
})
|
||||
|
||||
const result = await canReadAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails w/o workspace role', async () => {
|
||||
const canReadAutomation = buildCanReadAutomationPolicy({
|
||||
...overrides,
|
||||
getWorkspaceRole: async () => null
|
||||
})
|
||||
|
||||
const result = await canReadAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('succeeds w/o sso, if not needed', async () => {
|
||||
const canReadAutomation = buildCanReadAutomationPolicy({
|
||||
...overrides,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null
|
||||
})
|
||||
|
||||
const result = await canReadAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails w/o sso', async () => {
|
||||
const canReadAutomation = buildCanReadAutomationPolicy({
|
||||
...overrides,
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
|
||||
const result = await canReadAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if sso session expired', async () => {
|
||||
const canReadAutomation = buildCanReadAutomationPolicy({
|
||||
...overrides,
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date(Date.now() - 1000 * 60 * 60)
|
||||
})
|
||||
})
|
||||
|
||||
const result = await canReadAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { AuthPolicy } from '../../../domain/policies.js'
|
||||
import { MaybeUserContext, ProjectContext } from '../../../domain/context.js'
|
||||
import { ensureImplicitProjectMemberWithReadAccessFragment } from '../../../fragments/projects.js'
|
||||
import {} from '../../../fragments/server.js'
|
||||
import { Loaders } from '../../../domain/loaders.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotEnoughPermissionsError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
ServerNotEnoughPermissionsError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceNotEnoughPermissionsError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
|
||||
export const canReadAutomationPolicy: AuthPolicy<
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole
|
||||
| typeof Loaders.getAdminOverrideEnabled,
|
||||
MaybeUserContext & ProjectContext,
|
||||
InstanceType<
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof ServerNotEnoughPermissionsError
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof WorkspaceSsoSessionNoAccessError
|
||||
| typeof WorkspaceNotEnoughPermissionsError
|
||||
| typeof ProjectNotEnoughPermissionsError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId }) => {
|
||||
// Project reviewers may read automations
|
||||
const hasReadAccess = await ensureImplicitProjectMemberWithReadAccessFragment(
|
||||
loaders
|
||||
)({
|
||||
userId,
|
||||
projectId,
|
||||
role: Roles.Stream.Reviewer
|
||||
})
|
||||
if (hasReadAccess.isErr) {
|
||||
return err(hasReadAccess.error)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
import { parseFeatureFlags } from '../../../../environment/index.js'
|
||||
import { canUpdateAutomationPolicy } from './canUpdate.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotEnoughPermissionsError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
ServerNotEnoughPermissionsError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
|
||||
const buildCanUpdatePolicy = (
|
||||
overrides?: Partial<Parameters<typeof canUpdateAutomationPolicy>[0]>
|
||||
) =>
|
||||
canUpdateAutomationPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: async () => ({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
allowPublicComments: false
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Owner,
|
||||
getServerRole: async () => Roles.Server.User,
|
||||
getWorkspace: async () => null,
|
||||
getWorkspaceRole: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
...overrides
|
||||
})
|
||||
|
||||
describe('canUpdateAutomation', () => {
|
||||
it('returns error if user is not logged in', async () => {
|
||||
const canUpdateAutomation = buildCanUpdatePolicy()
|
||||
|
||||
const result = await canUpdateAutomation({
|
||||
userId: undefined,
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoSessionError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if user is not found', async () => {
|
||||
const canUpdateAutomation = buildCanUpdatePolicy({
|
||||
getServerRole: async () => null
|
||||
})
|
||||
|
||||
const result = await canUpdateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if user is a server guest', async () => {
|
||||
const canUpdateAutomation = buildCanUpdatePolicy({
|
||||
getServerRole: async () => Roles.Server.Guest
|
||||
})
|
||||
|
||||
const result = await canUpdateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNotEnoughPermissionsError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error project not found', async () => {
|
||||
const canUpdateAutomation = buildCanUpdatePolicy({
|
||||
getProject: async () => null
|
||||
})
|
||||
|
||||
const result = await canUpdateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNotFoundError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if no role at all', async () => {
|
||||
const canUpdateAutomation = buildCanUpdatePolicy({
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
const result = await canUpdateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if not owner', async () => {
|
||||
const canUpdateAutomation = buildCanUpdatePolicy({
|
||||
getProjectRole: async () => Roles.Stream.Reviewer
|
||||
})
|
||||
const result = await canUpdateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNotEnoughPermissionsError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns ok if permissible', async () => {
|
||||
const canUpdateAutomation = buildCanUpdatePolicy()
|
||||
const result = await canUpdateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
describe('with workspace project', () => {
|
||||
const overrides = {
|
||||
getProject: async () => ({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
allowPublicComments: false
|
||||
}),
|
||||
getWorkspace: async () => ({
|
||||
id: 'workspace-id',
|
||||
slug: 'workspace-slug'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getWorkspaceSsoProvider: async () => ({
|
||||
providerId: 'provider-id'
|
||||
}),
|
||||
getWorkspaceSsoSession: async () => {
|
||||
const validUntil = new Date()
|
||||
validUntil.setDate(validUntil.getDate() + 7)
|
||||
return {
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('returns ok if permissible', async () => {
|
||||
const canUpdateAutomation = buildCanUpdatePolicy(overrides)
|
||||
const result = await canUpdateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('returns ok with implicit owner role', async () => {
|
||||
const canUpdateAutomation = buildCanUpdatePolicy({
|
||||
...overrides,
|
||||
getWorkspaceRole: async () => Roles.Workspace.Admin,
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
const result = await canUpdateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('returns error if no workspace role, even w/ valid project role', async () => {
|
||||
const canUpdateAutomation = buildCanUpdatePolicy({
|
||||
...overrides,
|
||||
getWorkspaceRole: async () => null,
|
||||
getProjectRole: async () => Roles.Stream.Owner
|
||||
})
|
||||
|
||||
const result = await canUpdateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if invalid workspace and project role', async () => {
|
||||
const canUpdateAutomation = buildCanUpdatePolicy({
|
||||
...overrides,
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getProjectRole: async () => Roles.Stream.Contributor
|
||||
})
|
||||
const result = await canUpdateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNotEnoughPermissionsError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns ok if no sso configured', async () => {
|
||||
const canUpdateAutomation = buildCanUpdatePolicy({
|
||||
...overrides,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
const result = await canUpdateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('returns error if no sso session', async () => {
|
||||
const canUpdateAutomation = buildCanUpdatePolicy({
|
||||
...overrides,
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
const result = await canUpdateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if sso expired', async () => {
|
||||
const canUpdateAutomation = buildCanUpdatePolicy({
|
||||
...overrides,
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date(new Date().getTime() - 1000)
|
||||
})
|
||||
})
|
||||
const result = await canUpdateAutomation({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
import { err, 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 { ensureImplicitProjectMemberWithWriteAccessFragment } from '../../../fragments/projects.js'
|
||||
import { Loaders } from '../../../domain/loaders.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotEnoughPermissionsError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
ServerNotEnoughPermissionsError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceNotEnoughPermissionsError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
|
||||
type PolicyLoaderKeys =
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole
|
||||
|
||||
type PolicyArgs = ProjectContext & MaybeUserContext
|
||||
|
||||
type PolicyErrors = InstanceType<
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof ServerNotEnoughPermissionsError
|
||||
| typeof WorkspaceSsoSessionNoAccessError
|
||||
| typeof WorkspaceNotEnoughPermissionsError
|
||||
| typeof ProjectNotEnoughPermissionsError
|
||||
>
|
||||
|
||||
export const canUpdateAutomationPolicy: AuthPolicy<
|
||||
PolicyLoaderKeys,
|
||||
PolicyArgs,
|
||||
PolicyErrors
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId }) => {
|
||||
// Project owners may update automations
|
||||
const ensuredWriteAccess = await ensureImplicitProjectMemberWithWriteAccessFragment(
|
||||
loaders
|
||||
)({
|
||||
userId,
|
||||
projectId,
|
||||
role: Roles.Stream.Owner
|
||||
})
|
||||
if (ensuredWriteAccess.isErr) {
|
||||
return err(ensuredWriteAccess.error)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
Reference in New Issue
Block a user