Skip to content

Commit

Permalink
feat: adding permissions checks for SF (#293)
Browse files Browse the repository at this point in the history
  • Loading branch information
manas-vessel authored Aug 15, 2023
1 parent aba55ea commit 5ef575c
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 1 deletion.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vesselapi/integrations",
"version": "1.0.68",
"version": "1.0.69",
"description": "Vessel integrations",
"main": "dist/index.js",
"module": "dist/index.mjs",
Expand Down
12 changes: 12 additions & 0 deletions src/platforms/salesforce/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import {
salesforceTaskRelationalSelect,
SalesforceTaskUpdate,
salesforceUser,
salesforceUserPermissions,
} from './schemas';

const request = makeRequestFactory(async (auth, options) => {
Expand Down Expand Up @@ -246,6 +247,17 @@ export const client = {
schema: salesforceConnectOrganizationResponse,
})),
},
userPermissions: request(({}) => ({
url: `/query/`,
method: 'GET',
query: {
q: formatQuery(`SELECT FIELDS(ALL) FROM UserPermissionAccess LIMIT 1`),
},
schema: z.object({
records: z.array(salesforceUserPermissions),
totalSize: z.number(),
}),
})),
query: request(({ query }: { query: string }) => ({
url: `/query/`,
method: 'GET',
Expand Down
2 changes: 2 additions & 0 deletions src/platforms/salesforce/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { client } from '@/platforms/salesforce/client';
import * as constants from '@/platforms/salesforce/constants';
import boxIcon from '@/platforms/salesforce/logos/box';
import fullIcon from '@/platforms/salesforce/logos/full';
import { permissions } from '@/platforms/salesforce/permissions';
import { auth, platform } from '@/sdk';
import {
SalesforceAccountType,
Expand Down Expand Up @@ -67,6 +68,7 @@ export default platform('salesforce', {
categories: ['crm'],
},
client,
permissions,
constants,
actions: {
query,
Expand Down
93 changes: 93 additions & 0 deletions src/platforms/salesforce/permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { first, isArray, parallel, sift, tryit } from 'radash';
import { PlatformPermissions } from '../../sdk';
import { client } from './client';
import {
isSalesforceSupportedObjectType,
SalesforceSupportedObjectType,
} from './schemas';

// NOTE: These will be used in the UI, so they should be user-friendly.
const USER_PERMISSIONS_API_DISABLED_ERROR_MESSAGE =
'API access is disabled. Please check with your system administrator.';
const MISSING_OBJECTS_ERROR_MESSAGE =
'Missing permissions. Please ensure you have access to the following objects: ';

export const makePermissions = (): PlatformPermissions => {
return {
validate: async ({ resources, auth }) => {
const [err, result] = await tryit(client.userPermissions)(auth, {});
if (err) {
console.warn({
message: 'Failed to validate permissions',
metadata: {
context: err,
},
});
return {
errorMessage: USER_PERMISSIONS_API_DISABLED_ERROR_MESSAGE,
valid: false,
};
}
const {
data: { records },
} = result;
if (!first(records)?.permissionsApiEnabled) {
return {
errorMessage: USER_PERMISSIONS_API_DISABLED_ERROR_MESSAGE,
valid: false,
};
}

const describe = async (objectType: SalesforceSupportedObjectType) => {
try {
return await client.sobjects.describe(auth, { objectType });
} catch (err) {
const errBody = (err as any).body;
const { error } = isArray(errBody) ? errBody[0] : errBody;
if (
!['INVALID_TYPE', 'OBJECT_NOT_FOUND', 'NOT_FOUND'].includes(error)
) {
console.warn({
message: `Unknown permissions error for ${objectType}`,
error,
metadata: {
context: err,
},
});
}
return {
data: { createable: false, updateable: false, retrieveable: false },
};
}
};

const check = async (objectType: string) => {
// Content Notes aren't available for all accounts.
if (objectType === 'ContentNote') return null;
if (!isSalesforceSupportedObjectType(objectType)) return null;

const {
data: { createable, updateable, retrieveable },
} = await describe(objectType);

// Users and ListViews only need to be retrievable.
if (objectType === 'User' && retrieveable) return null;
if (objectType === 'ListView' && retrieveable) return null;

if (!retrieveable || !createable || !updateable) return objectType;
};

const missingObjects = sift(await parallel(5, resources, check));
if (missingObjects.length === 0) {
return { errorMessage: null, valid: true };
}

return {
errorMessage: MISSING_OBJECTS_ERROR_MESSAGE + missingObjects.join(', '),
valid: false,
};
},
};
};

export const permissions = makePermissions();
20 changes: 20 additions & 0 deletions src/platforms/salesforce/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ const requiredFields = {
// -
// SObjects
// -
export const isSalesforceSupportedObjectType = (
value: unknown,
): value is SalesforceSupportedObjectType =>
SALESFORCE_SUPPORTED_OBJECT_TYPE.includes(value as any);

export const salesforceSupportedObjectType = z.enum(
SALESFORCE_SUPPORTED_OBJECT_TYPE,
);
Expand Down Expand Up @@ -68,6 +73,9 @@ export const salesforceField = z.object({

export const salesforceDescribeResponse = z.object({
fields: z.array(salesforceField),
createable: z.boolean(),
updateable: z.boolean(),
retrieveable: z.boolean(),
});

// -
Expand All @@ -85,6 +93,15 @@ export const salesforceQueryResponse = z.object({
totalSize: z.number(),
});

// -
// Permissions
// -
export const salesforceUserPermissions = z
.object({
permissionsApiEnabled: z.boolean().nullable(),
})
.partial();

// -
// Jobs
// -
Expand Down Expand Up @@ -988,6 +1005,9 @@ export const salesforceOAuthUrlsByAccountType: Record<
export type SalesforceSupportedObjectType =
(typeof SALESFORCE_SUPPORTED_OBJECT_TYPE)[number];
export type SalesforceSObject = z.infer<typeof salesforceSObject>;
export type SalesforceUserPermissions = z.infer<
typeof salesforceUserPermissions
>;
export type SalesforceField = z.infer<typeof salesforceField>;
export type SalesforceQueryRecord = z.infer<typeof salesforceQueryRecord>;
export type SalesforceUser = z.infer<typeof salesforceUser>;
Expand Down
3 changes: 3 additions & 0 deletions src/sdk/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
PlatformClient,
PlatformConstants,
PlatformDisplayConfig,
PlatformPermissions,
} from './types';

export type PlatformOptions<
Expand Down Expand Up @@ -40,6 +41,7 @@ export type PlatformOptions<
actions: TActions;
display: PlatformDisplayConfig;
client: TClient;
permissions?: PlatformPermissions;
};

export const platform = <
Expand Down Expand Up @@ -110,6 +112,7 @@ export const platform = <
id,
client: options.client,
auth: authConfigs,
permissions: options.permissions,
display: options.display,
rawActions: Object.values(options.actions),
constants: options.constants,
Expand Down
14 changes: 14 additions & 0 deletions src/sdk/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,19 @@ export type PlatformDisplayConfig = {
categories: Category[];
};

export type PlatformPermissions = {
validate: ({
resources,
auth,
}: {
resources: string[];
auth: Auth;
}) => Promise<{
errorMessage: string | null;
valid: boolean;
}>;
};

export type PlatformConstants = Record<string, any>;
export type Platform<
TActions extends Record<string, Action<string, any, any>>,
Expand All @@ -270,6 +283,7 @@ export type Platform<
constants: TConstants;
actions: TActions;
display: PlatformDisplayConfig;
permissions?: PlatformPermissions;
};

export type ActionFunction<
Expand Down

0 comments on commit 5ef575c

Please sign in to comment.