In this PR - Determine object record permissions on workflows objects (workflow, workflowVersion, workflowRun) base on settings permissions @Weiko - Add Workflow permission guards on workflow resolvers @thomtrp . **Any method within a resolver that has the SettingsPermission Guard is only callable by a apiKey or a user that has the permission** (so not by external parties). - Add checks bypass in workflow services since 1) for actions gated by settings permissions, the gate should be done at resolver level, so it will have been done before the call to the service 2) some service methods may be called by workflowTriggerController which is callable by external parties without permissions (ex: workflowCommonWorkspaceService.getWorkflowVersionOrFail). This is something we may want to change in the future (still to discuss), by removing the guard at resolver-level and relying on shouldBypassPermissionChecks at getRepository and made in a way that we only bypass for external parties. - Add checks bypass for actions performed by workflows since they should not be restricted in our current vision - Add tests
524 lines
17 KiB
TypeScript
524 lines
17 KiB
TypeScript
import { print } from 'graphql';
|
|
import request from 'supertest';
|
|
import { deleteOneRoleOperationFactory } from 'test/integration/graphql/utils/delete-one-role-operation-factory.util';
|
|
import { destroyOneOperationFactory } from 'test/integration/graphql/utils/destroy-one-operation-factory.util';
|
|
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
|
import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util';
|
|
import { updateWorkspaceMemberRole } from 'test/integration/graphql/utils/update-workspace-member-role.util';
|
|
import { createOneObjectMetadataQueryFactory } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata-query-factory.util';
|
|
import { deleteOneObjectMetadataQueryFactory } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata-query-factory.util';
|
|
|
|
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
|
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
|
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
|
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
|
import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
|
|
|
|
const client = request(`http://localhost:${APP_PORT}`);
|
|
|
|
describe('Granular settings permissions', () => {
|
|
let customRoleId: string;
|
|
let originalMemberRoleId: string;
|
|
|
|
beforeAll(async () => {
|
|
// Enable Permissions V2
|
|
const enablePermissionsV2Query = updateFeatureFlagFactory(
|
|
SEED_APPLE_WORKSPACE_ID,
|
|
'IS_PERMISSIONS_V2_ENABLED',
|
|
true,
|
|
);
|
|
|
|
await makeGraphqlAPIRequest(enablePermissionsV2Query);
|
|
|
|
// Get the original Member role ID for restoration later
|
|
const getRolesQuery = {
|
|
query: `
|
|
query GetRoles {
|
|
getRoles {
|
|
id
|
|
label
|
|
}
|
|
}
|
|
`,
|
|
};
|
|
|
|
const rolesResponse = await client
|
|
.post('/graphql')
|
|
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
|
|
.send(getRolesQuery);
|
|
|
|
originalMemberRoleId = rolesResponse.body.data.getRoles.find(
|
|
(role: any) => role.label === 'Member',
|
|
).id;
|
|
|
|
// Create a custom role with canUpdateAllSettings = false
|
|
const createRoleQuery = {
|
|
query: `
|
|
mutation CreateOneRole {
|
|
createOneRole(createRoleInput: {
|
|
label: "Custom Test Role"
|
|
description: "Role for testing specific setting permissions"
|
|
canUpdateAllSettings: false
|
|
canReadAllObjectRecords: true
|
|
canUpdateAllObjectRecords: false
|
|
canSoftDeleteAllObjectRecords: false
|
|
canDestroyAllObjectRecords: false
|
|
}) {
|
|
id
|
|
label
|
|
canUpdateAllSettings
|
|
}
|
|
}
|
|
`,
|
|
};
|
|
|
|
const createRoleResponse = await client
|
|
.post('/graphql')
|
|
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
|
|
.send(createRoleQuery);
|
|
|
|
customRoleId = createRoleResponse.body.data.createOneRole.id;
|
|
|
|
// Assign specific setting permissions to the custom role
|
|
const upsertSettingPermissionsQuery = {
|
|
query: `
|
|
mutation UpsertSettingPermissions {
|
|
upsertSettingPermissions(upsertSettingPermissionsInput: {
|
|
roleId: "${customRoleId}"
|
|
settingPermissionKeys: [${SettingPermissionType.DATA_MODEL}, ${SettingPermissionType.WORKSPACE}, ${SettingPermissionType.WORKFLOWS}]
|
|
}) {
|
|
id
|
|
setting
|
|
roleId
|
|
}
|
|
}
|
|
`,
|
|
};
|
|
|
|
await client
|
|
.post('/graphql')
|
|
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
|
|
.send(upsertSettingPermissionsQuery);
|
|
|
|
// Assign the custom role to JONY (who uses MEMBER_ACCESS_TOKEN)
|
|
await updateWorkspaceMemberRole({
|
|
client,
|
|
roleId: customRoleId,
|
|
workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY,
|
|
});
|
|
});
|
|
|
|
afterAll(async () => {
|
|
// Restore JONY's original Member role
|
|
const restoreMemberRoleQuery = {
|
|
query: `
|
|
mutation UpdateWorkspaceMemberRole {
|
|
updateWorkspaceMemberRole(
|
|
workspaceMemberId: "${WORKSPACE_MEMBER_DATA_SEED_IDS.JONY}"
|
|
roleId: "${originalMemberRoleId}"
|
|
) {
|
|
id
|
|
}
|
|
}
|
|
`,
|
|
};
|
|
|
|
await client
|
|
.post('/graphql')
|
|
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
|
|
.send(restoreMemberRoleQuery);
|
|
|
|
// Delete the custom role
|
|
const deleteRoleQuery = deleteOneRoleOperationFactory(customRoleId);
|
|
|
|
await client
|
|
.post('/graphql')
|
|
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
|
|
.send(deleteRoleQuery);
|
|
|
|
// Disable Permissions V2
|
|
const disablePermissionsV2Query = updateFeatureFlagFactory(
|
|
SEED_APPLE_WORKSPACE_ID,
|
|
'IS_PERMISSIONS_V2_ENABLED',
|
|
false,
|
|
);
|
|
|
|
await makeGraphqlAPIRequest(disablePermissionsV2Query);
|
|
});
|
|
|
|
describe('Data Model Permissions', () => {
|
|
it('should allow access to data model operations when user has DATA_MODEL setting permission', async () => {
|
|
// Test creating an object metadata (requires DATA_MODEL permission)
|
|
const { query: createObjectQuery, variables } =
|
|
createOneObjectMetadataQueryFactory({
|
|
input: {
|
|
labelSingular: 'House',
|
|
labelPlural: 'Houses',
|
|
nameSingular: 'house',
|
|
namePlural: 'houses',
|
|
description: 'a house',
|
|
icon: 'IconHome',
|
|
},
|
|
gqlFields: `
|
|
id
|
|
labelSingular
|
|
labelPlural
|
|
`,
|
|
});
|
|
|
|
const response = await client
|
|
.post('/metadata')
|
|
.set('Authorization', `Bearer ${MEMBER_ACCESS_TOKEN}`)
|
|
.send({ query: print(createObjectQuery), variables });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.errors).toBeUndefined();
|
|
expect(response.body.data.createOneObject).toBeDefined();
|
|
expect(response.body.data.createOneObject.labelSingular).toBe('House');
|
|
|
|
// Clean up - delete the created object
|
|
const { query: deleteObjectQuery, variables: deleteObjectVariables } =
|
|
deleteOneObjectMetadataQueryFactory({
|
|
input: {
|
|
idToDelete: response.body.data.createOneObject.id,
|
|
},
|
|
gqlFields: 'id',
|
|
});
|
|
|
|
await client
|
|
.post('/graphql')
|
|
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
|
|
.send({
|
|
query: print(deleteObjectQuery),
|
|
variables: deleteObjectVariables,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Workspace Permissions', () => {
|
|
it('should allow access to workspace operations when user has WORKSPACE setting permission', async () => {
|
|
// Test updating workspace settings (requires WORKSPACE permission)
|
|
const updateWorkspaceQuery = {
|
|
query: `
|
|
mutation UpdateWorkspace {
|
|
updateWorkspace(data: {
|
|
displayName: "Updated Test Workspace"
|
|
}) {
|
|
id
|
|
displayName
|
|
}
|
|
}
|
|
`,
|
|
};
|
|
|
|
const response = await client
|
|
.post('/graphql')
|
|
.set('Authorization', `Bearer ${MEMBER_ACCESS_TOKEN}`)
|
|
.send(updateWorkspaceQuery);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.errors).toBeUndefined();
|
|
expect(response.body.data.updateWorkspace).toBeDefined();
|
|
expect(response.body.data.updateWorkspace.displayName).toBe(
|
|
'Updated Test Workspace',
|
|
);
|
|
|
|
// Restore original workspace name
|
|
const restoreWorkspaceQuery = {
|
|
query: `
|
|
mutation UpdateWorkspace {
|
|
updateWorkspace(data: {
|
|
displayName: "Apple"
|
|
}) {
|
|
id
|
|
displayName
|
|
}
|
|
}
|
|
`,
|
|
};
|
|
|
|
await client
|
|
.post('/graphql')
|
|
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
|
|
.send(restoreWorkspaceQuery);
|
|
});
|
|
});
|
|
|
|
describe('Workflows Permissions', () => {
|
|
it('should allow access to workflows operations when user has WORKFLOWS setting permission', async () => {
|
|
// Test creating a workflow (requires WORKFLOWS permission)
|
|
const createWorkflowQuery = {
|
|
query: `
|
|
mutation CreateWorkflow {
|
|
createWorkflow(data: {
|
|
name: "Test Workflow"
|
|
}) {
|
|
id
|
|
name
|
|
}
|
|
}
|
|
`,
|
|
};
|
|
|
|
const response = await client
|
|
.post('/graphql')
|
|
.set('Authorization', `Bearer ${MEMBER_ACCESS_TOKEN}`)
|
|
.send(createWorkflowQuery);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.errors).toBeUndefined();
|
|
expect(response.body.data.createWorkflow).toBeDefined();
|
|
expect(response.body.data.createWorkflow.name).toBe('Test Workflow');
|
|
|
|
// Clean up - delete the created workflow
|
|
const graphqlOperation = destroyOneOperationFactory({
|
|
objectMetadataSingularName: 'workflow',
|
|
gqlFields: `
|
|
id
|
|
`,
|
|
recordId: response.body.data.createWorkflow.id,
|
|
});
|
|
|
|
await client
|
|
.post('/graphql')
|
|
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
|
|
.send(graphqlOperation);
|
|
});
|
|
});
|
|
|
|
describe('Denied Permissions', () => {
|
|
it('should deny access to roles operations when user does not have ROLES setting permission', async () => {
|
|
// Test creating a role (requires ROLES permission, which our custom role doesn't have)
|
|
const createRoleQuery = {
|
|
query: `
|
|
mutation CreateOneRole {
|
|
createOneRole(createRoleInput: {
|
|
label: "Unauthorized Role"
|
|
}) {
|
|
id
|
|
}
|
|
}
|
|
`,
|
|
};
|
|
|
|
const response = await client
|
|
.post('/graphql')
|
|
.set('Authorization', `Bearer ${MEMBER_ACCESS_TOKEN}`)
|
|
.send(createRoleQuery);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data).toBeNull();
|
|
expect(response.body.errors).toBeDefined();
|
|
expect(response.body.errors[0].message).toBe(
|
|
PermissionsExceptionMessage.PERMISSION_DENIED,
|
|
);
|
|
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
|
|
});
|
|
|
|
it('should deny access to workspace members operations when user does not have WORKSPACE_MEMBERS setting permission', async () => {
|
|
// Test inviting a workspace member (requires WORKSPACE_MEMBERS permission)
|
|
const inviteWorkspaceMemberQuery = {
|
|
query: `
|
|
mutation SendWorkspaceInvitation {
|
|
sendInvitations(emails: ["test@example.com"]) {
|
|
success
|
|
}
|
|
}
|
|
`,
|
|
};
|
|
|
|
const response = await client
|
|
.post('/graphql')
|
|
.set('Authorization', `Bearer ${MEMBER_ACCESS_TOKEN}`)
|
|
.send(inviteWorkspaceMemberQuery);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data).toBeNull();
|
|
expect(response.body.errors).toBeDefined();
|
|
expect(response.body.errors[0].message).toBe(
|
|
PermissionsExceptionMessage.PERMISSION_DENIED,
|
|
);
|
|
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
|
|
});
|
|
|
|
it('should deny access to API keys operations when user does not have API_KEYS_AND_WEBHOOKS setting permission', async () => {
|
|
// Test creating an API key (requires API_KEYS_AND_WEBHOOKS permission)
|
|
const createApiKeyQuery = {
|
|
query: `
|
|
mutation GenerateApiKeyToken {
|
|
generateApiKeyToken(apiKeyId: "setting-permissions-test-api-key-id", expiresAt: "2025-12-31T23:59:59.000Z") {
|
|
token
|
|
}
|
|
}
|
|
`,
|
|
};
|
|
|
|
const response = await client
|
|
.post('/graphql')
|
|
.set('Authorization', `Bearer ${MEMBER_ACCESS_TOKEN}`)
|
|
.send(createApiKeyQuery);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data).toBeNull();
|
|
expect(response.body.errors).toBeDefined();
|
|
expect(response.body.errors[0].message).toBe(
|
|
PermissionsExceptionMessage.PERMISSION_DENIED,
|
|
);
|
|
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
|
|
});
|
|
});
|
|
|
|
describe('Permission Inheritance', () => {
|
|
it('should verify that canUpdateAllSettings=false is properly overridden by specific setting permissions', async () => {
|
|
// Verify the role configuration
|
|
const getRoleQuery = {
|
|
query: `
|
|
query GetRole {
|
|
getRoles {
|
|
id
|
|
label
|
|
canUpdateAllSettings
|
|
settingPermissions {
|
|
setting
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
};
|
|
|
|
const response = await client
|
|
.post('/graphql')
|
|
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
|
|
.send(getRoleQuery);
|
|
|
|
const customRole = response.body.data.getRoles.find(
|
|
(role: any) => role.id === customRoleId,
|
|
);
|
|
|
|
expect(customRole).toBeDefined();
|
|
expect(customRole.canUpdateAllSettings).toBe(false);
|
|
expect(customRole.settingPermissions).toHaveLength(3);
|
|
expect(
|
|
customRole.settingPermissions.map((p: any) => p.setting),
|
|
).toContain(SettingPermissionType.DATA_MODEL);
|
|
expect(
|
|
customRole.settingPermissions.map((p: any) => p.setting),
|
|
).toContain(SettingPermissionType.WORKSPACE);
|
|
});
|
|
});
|
|
|
|
describe('Dynamic Permission Updates', () => {
|
|
it('should allow adding new setting permissions to existing role', async () => {
|
|
// Add SECURITY permission to the custom role
|
|
const upsertSecurityPermissionQuery = {
|
|
query: `
|
|
mutation UpsertSettingPermissions {
|
|
upsertSettingPermissions(upsertSettingPermissionsInput: {
|
|
roleId: "${customRoleId}"
|
|
settingPermissionKeys: [${SettingPermissionType.DATA_MODEL}, ${SettingPermissionType.WORKSPACE}, ${SettingPermissionType.SECURITY}]
|
|
}) {
|
|
id
|
|
setting
|
|
roleId
|
|
}
|
|
}
|
|
`,
|
|
};
|
|
|
|
const response = await client
|
|
.post('/graphql')
|
|
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
|
|
.send(upsertSecurityPermissionQuery);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.errors).toBeUndefined();
|
|
expect(response.body.data.upsertSettingPermissions).toHaveLength(3);
|
|
|
|
// Verify the user now has access to security operations
|
|
// Note: This would require a specific security operation to test
|
|
// For now, we just verify the permission was added
|
|
const getRoleQuery = {
|
|
query: `
|
|
query GetRole {
|
|
getRoles {
|
|
id
|
|
settingPermissions {
|
|
setting
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
};
|
|
|
|
const roleResponse = await client
|
|
.post('/graphql')
|
|
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
|
|
.send(getRoleQuery);
|
|
|
|
const updatedRole = roleResponse.body.data.getRoles.find(
|
|
(role: any) => role.id === customRoleId,
|
|
);
|
|
|
|
expect(updatedRole.settingPermissions).toHaveLength(3);
|
|
expect(
|
|
updatedRole.settingPermissions.map((p: any) => p.setting),
|
|
).toContain(SettingPermissionType.SECURITY);
|
|
});
|
|
|
|
it('should allow removing setting permissions from existing role', async () => {
|
|
// Remove SECURITY permission, keep only DATA_MODEL and WORKSPACE
|
|
const upsertReducedPermissionsQuery = {
|
|
query: `
|
|
mutation UpsertSettingPermissions {
|
|
upsertSettingPermissions(upsertSettingPermissionsInput: {
|
|
roleId: "${customRoleId}"
|
|
settingPermissionKeys: [${SettingPermissionType.DATA_MODEL}, ${SettingPermissionType.WORKSPACE}]
|
|
}) {
|
|
id
|
|
setting
|
|
roleId
|
|
}
|
|
}
|
|
`,
|
|
};
|
|
|
|
const response = await client
|
|
.post('/graphql')
|
|
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
|
|
.send(upsertReducedPermissionsQuery);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.errors).toBeUndefined();
|
|
expect(response.body.data.upsertSettingPermissions).toHaveLength(2);
|
|
|
|
// Verify SECURITY permission was removed
|
|
const getRoleQuery = {
|
|
query: `
|
|
query GetRole {
|
|
getRoles {
|
|
id
|
|
settingPermissions {
|
|
setting
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
};
|
|
|
|
const roleResponse = await client
|
|
.post('/graphql')
|
|
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
|
|
.send(getRoleQuery);
|
|
|
|
const updatedRole = roleResponse.body.data.getRoles.find(
|
|
(role: any) => role.id === customRoleId,
|
|
);
|
|
|
|
expect(updatedRole.settingPermissions).toHaveLength(2);
|
|
expect(
|
|
updatedRole.settingPermissions.map((p: any) => p.setting),
|
|
).not.toContain(SettingPermissionType.SECURITY);
|
|
});
|
|
});
|
|
});
|