diff --git a/src/engine/entity/Entity.ts b/src/engine/entity/Entity.ts index 7a757341..8c52712c 100644 --- a/src/engine/entity/Entity.ts +++ b/src/engine/entity/Entity.ts @@ -390,7 +390,6 @@ export class Entity { return this.subscriptions; } - // this.getStates(); this.subscriptions = this._processSubscriptions(); return this.subscriptions; } @@ -799,7 +798,21 @@ export class Entity { }); } - // todo subscription + if (this.permissions.subscriptions && this.subscriptions) { + this.subscriptions.map(subscription => { + const subscriptionName = subscription.name; + const permission = this.permissions.subscriptions[subscriptionName]; + + if (permission) { + const descriptionPermissions = generatePermissionDescription( + permission, + ); + if (descriptionPermissions) { + subscription.description += descriptionPermissions; + } + } + }); + } } } @@ -828,7 +841,7 @@ export class Entity { } this.getMutations(); - // this.getSubscriptions(); + this.getSubscriptions(); this.permissions = this._processPermissions(); this._generatePermissionDescriptions(); return this.permissions; diff --git a/src/engine/permission/Permission.spec.ts b/src/engine/permission/Permission.spec.ts index 5296d250..28ce8c1a 100644 --- a/src/engine/permission/Permission.spec.ts +++ b/src/engine/permission/Permission.spec.ts @@ -906,9 +906,13 @@ describe('Permission', () => { mutations: { update: new Permission().role('manager'), }, + subscriptions: { + onUpdate: new Permission().role('manager'), + }, }; - processEntityPermissions(entity, permissions); + const permissionMap = processEntityPermissions(entity, permissions); + expect(permissionMap).toMatchSnapshot(); }); it('should throw if provided with an invalid map of permissions', () => { @@ -933,6 +937,18 @@ describe('Permission', () => { expect(fn).toThrowErrorMatchingSnapshot(); }); + it('should throw if provided with an invalid map of subscription permissions', () => { + const permissions = { + subscriptions: ['bad'], + }; + + function fn() { + processEntityPermissions(entity, permissions); + } + + expect(fn).toThrowErrorMatchingSnapshot(); + }); + it('should throw if provided with an invalid permissions', () => { const permissions1 = { read: ['bad'], @@ -1001,6 +1017,18 @@ describe('Permission', () => { } expect(fn3).toThrowErrorMatchingSnapshot(); + + const permissions4 = { + subscriptions: { + onUpdate: new Permission().userAttribute('notHere'), + }, + }; + + function fn4() { + processEntityPermissions(entity, permissions4); + } + + expect(fn4).toThrowErrorMatchingSnapshot(); }); it('should throw if permissions have invalid attributes defined', () => { @@ -1029,6 +1057,20 @@ describe('Permission', () => { expect(fn).toThrowErrorMatchingSnapshot(); }); + it('should throw if permissions are assigned to unknown subscriptions', () => { + const permissions = { + subscriptions: { + noSuchSubscription: new Permission().userAttribute('someAttribute'), + }, + }; + + function fn() { + processEntityPermissions(entity, permissions); + } + + expect(fn).toThrowErrorMatchingSnapshot(); + }); + it('should throw if permission is used on a create type mutation and using data-bound permission types', () => { const permissions1 = { mutations: { diff --git a/src/engine/permission/Permission.ts b/src/engine/permission/Permission.ts index 7406f85d..85139387 100644 --- a/src/engine/permission/Permission.ts +++ b/src/engine/permission/Permission.ts @@ -10,6 +10,11 @@ import { isMutation, Mutation, } from '../mutation/Mutation'; +import { + SUBSCRIPTION_TYPE_CREATE, + // isSubscription, + Subscription, +} from '../subscription/Subscription'; import { isDataTypeState } from '../datatype/DataTypeState'; /* @@ -29,6 +34,7 @@ export type PermissionMap = { read?: Permission | Permission[]; find?: Permission | Permission[]; mutations?: {} | Permission | Permission[]; + subscriptions?: {} | Permission | Permission[]; }; export class Permission { @@ -973,6 +979,28 @@ const validatePermissionMutationTypes = ( } }; +const validatePermissionSubscriptionTypes = ( + entity: Entity, + permissions: Permission | Permission[], + subscription: Subscription, +): void => { + if (subscription.type === SUBSCRIPTION_TYPE_CREATE) { + const permissionsArray = isArray(permissions as Permission[]) + ? (permissions as Permission[]) + : ([permissions] as Permission[]); + + permissionsArray.map(permission => { + passOrThrow( + !permission.userAttributes.length && + !permission.states.length && + !permission.values.length, + () => + `Create type subscription permission '${subscription.name}' in '${entity.name}.permissions' can only be of type 'authenticated', 'everyone', 'role' or 'lookup'`, + ); + }); + } +}; + export const hasEmptyPermissions = ( _permissions: Permission | Permission[], ): boolean => { @@ -1075,6 +1103,41 @@ export const processEntityPermissions = ( } } + const entitySubscriptions = entity.getSubscriptions(); + + if (!permissions.subscriptions && defaultPermissions) { + permissions.subscriptions = {}; + } + + if (permissions.subscriptions) { + passOrThrow( + isMap(permissions.subscriptions), + () => + `Entity '${entity.name}' permissions definition for subscriptions needs to be a map of subscriptions and permissions`, + ); + + const subscriptionNames = Object.keys(permissions.subscriptions); + subscriptionNames.map((subscriptionName, idx) => { + passOrThrow( + isPermission(permissions.subscriptions[subscriptionName]) || + isPermissionsArray(permissions.subscriptions[subscriptionName]), + () => + `Invalid subscription permission definition for entity '${entity.name}' at position '${idx}'`, + ); + }); + + if (defaultPermissions) { + entitySubscriptions.map(({ name: subscriptionName }) => { + if (defaultPermissions.subscriptions) { + permissions.subscriptions[subscriptionName] = + permissions.subscriptions[subscriptionName] || + defaultPermissions.subscriptions[subscriptionName] || + defaultPermissions.subscriptions._default; + } + }); + } + } + if (permissions.find) { validatePermissionAttributesAndStates(entity, permissions.find, 'find'); } @@ -1107,6 +1170,36 @@ export const processEntityPermissions = ( }); } + if (permissions.subscriptions && entitySubscriptions) { + const permissionSubscriptionNames = Object.keys(permissions.subscriptions); + + const subscriptionNames = entitySubscriptions.map( + subscription => subscription.name, + ); + + permissionSubscriptionNames.map(permissionSubscriptionName => { + passOrThrow( + subscriptionNames.includes(permissionSubscriptionName), + () => + `Unknown subscription '${permissionSubscriptionName}' used for permissions in entity '${entity.name}'`, + ); + }); + + entitySubscriptions.map(subscription => { + const subscriptionName = subscription.name; + const permission = permissions.subscriptions[subscriptionName]; + if (permission) { + // not sure it's needed for subscription + validatePermissionSubscriptionTypes(entity, permission, subscription); + validatePermissionAttributesAndStates( + entity, + permission, + subscription.type, + ); + } + }); + } + const emptyPermissionsIn = findEmptyEntityPermissions(permissions); passOrThrow( diff --git a/src/engine/permission/__snapshots__/Permission.spec.ts.snap b/src/engine/permission/__snapshots__/Permission.spec.ts.snap index e0806bd9..37f56cb1 100644 --- a/src/engine/permission/__snapshots__/Permission.spec.ts.snap +++ b/src/engine/permission/__snapshots__/Permission.spec.ts.snap @@ -487,6 +487,63 @@ exports[`Permission processActionPermissions should throw if provided with inval exports[`Permission processActionPermissions should throw if provided with invalid permissions 2`] = `"Invalid permission definition for action 'SomeActionName'"`; +exports[`Permission processEntityPermissions should accept a correct permissions setup 1`] = ` +Object { + "mutations": Object { + "update": Permission { + "authenticatedCanAccess": false, + "everyoneCanAccess": false, + "isEmpty": false, + "lookups": Array [], + "roles": Array [ + "manager", + ], + "states": Array [], + "types": Object { + "role": true, + }, + "userAttributes": Array [], + "values": Array [], + }, + }, + "read": Permission { + "authenticatedCanAccess": false, + "everyoneCanAccess": false, + "isEmpty": false, + "lookups": Array [], + "roles": Array [], + "states": Array [], + "types": Object { + "value": true, + }, + "userAttributes": Array [], + "values": Array [ + Object { + "attributeName": "someAttribute", + "value": 123, + }, + ], + }, + "subscriptions": Object { + "onUpdate": Permission { + "authenticatedCanAccess": false, + "everyoneCanAccess": false, + "isEmpty": false, + "lookups": Array [], + "roles": Array [ + "manager", + ], + "states": Array [], + "types": Object { + "role": true, + }, + "userAttributes": Array [], + "values": Array [], + }, + }, +} +`; + exports[`Permission processEntityPermissions should throw if permission is used on a create type mutation and using data-bound permission types 1`] = `"Create type mutation permission 'create' in 'SomeEntityName.permissions' can only be of type 'authenticated', 'everyone', 'role' or 'lookup'"`; exports[`Permission processEntityPermissions should throw if permission is used on a create type mutation and using data-bound permission types 2`] = `"Create type mutation permission 'create' in 'SomeEntityName.permissions' can only be of type 'authenticated', 'everyone', 'role' or 'lookup'"`; @@ -495,6 +552,8 @@ exports[`Permission processEntityPermissions should throw if permission is used exports[`Permission processEntityPermissions should throw if permissions are assigned to unknown mutations 1`] = `"Unknown mutation 'noSuchMutation' used for permissions in entity 'SomeEntityName'"`; +exports[`Permission processEntityPermissions should throw if permissions are assigned to unknown subscriptions 1`] = `"Unknown subscription 'noSuchSubscription' used for permissions in entity 'SomeEntityName'"`; + exports[`Permission processEntityPermissions should throw if permissions have invalid attributes defined 1`] = `"Cannot use attribute 'someAttribute' in 'SomeEntityName.permissions' as 'userAttribute' as it is not a reference to the User entity"`; exports[`Permission processEntityPermissions should throw if permissions have unknown attributes defined 1`] = `"Cannot use attribute 'notHere' in 'SomeEntityName.permissions' for 'read' as it does not exist"`; @@ -503,10 +562,14 @@ exports[`Permission processEntityPermissions should throw if permissions have un exports[`Permission processEntityPermissions should throw if permissions have unknown attributes defined 3`] = `"Cannot use attribute 'notHere' in 'SomeEntityName.permissions' for 'update' as it does not exist"`; +exports[`Permission processEntityPermissions should throw if permissions have unknown attributes defined 4`] = `"Cannot use attribute 'notHere' in 'SomeEntityName.permissions' for 'onUpdate' as it does not exist"`; + exports[`Permission processEntityPermissions should throw if provided with an invalid map of mutation permissions 1`] = `"Entity 'SomeEntityName' permissions definition for mutations needs to be a map of mutations and permissions"`; exports[`Permission processEntityPermissions should throw if provided with an invalid map of permissions 1`] = `"Entity 'SomeEntityName' permissions definition needs to be an object"`; +exports[`Permission processEntityPermissions should throw if provided with an invalid map of subscription permissions 1`] = `"Entity 'SomeEntityName' permissions definition for subscriptions needs to be a map of subscriptions and permissions"`; + exports[`Permission processEntityPermissions should throw if provided with an invalid permissions 1`] = `"Invalid 'read' permission definition for entity 'SomeEntityName'"`; exports[`Permission processEntityPermissions should throw if provided with an invalid permissions 2`] = `"Invalid 'find' permission definition for entity 'SomeEntityName'"`; diff --git a/src/engine/schema/Schema.ts b/src/engine/schema/Schema.ts index 7740951a..155766fd 100644 --- a/src/engine/schema/Schema.ts +++ b/src/engine/schema/Schema.ts @@ -4,7 +4,11 @@ import { Entity, isEntity } from '../entity/Entity'; import { Action, isAction } from '../action/Action'; import { isDataTypeUser } from '../datatype/DataTypeUser'; import { StorageType, isStorageType } from '../storage/StorageType'; -import { isPermission, isPermissionsArray } from '../permission/Permission'; +import { + isPermission, + isPermissionsArray, + Permission, +} from '../permission/Permission'; import { isViewEntity, ViewEntity } from '../entity/ViewEntity'; import { isShadowEntity } from '../entity/ShadowEntity'; @@ -220,11 +224,14 @@ export class Schema { const entityDefaultPermissions = this.permissionsMap.entities[entity.name] || {}; entityDefaultPermissions.mutations = - entityDefaultPermissions.mutations || ({} as Entity); + entityDefaultPermissions.mutations || ({} as Permission); + entityDefaultPermissions.subscriptions = + entityDefaultPermissions.subscriptions || ({} as Permission); const defaultPermissions = this.permissionsMap.entities ._defaultPermissions; defaultPermissions.mutations = defaultPermissions.mutations || {}; + defaultPermissions.subscriptions = defaultPermissions.subscriptions || {}; const newDefaultPermissions = { read: entityDefaultPermissions.read || defaultPermissions.read, @@ -233,6 +240,10 @@ export class Schema { ...defaultPermissions.mutations, ...entityDefaultPermissions.mutations, }, + subscriptions: { + ...defaultPermissions.subscriptions, + ...entityDefaultPermissions.subscriptions, + }, }; if (isEntity(entity) || isViewEntity(entity)) {