UNPKG

pubnub

Version:

Publish & Subscribe Real-time Messaging with PubNub

269 lines (232 loc) 8.71 kB
/** * PAM Grant Token REST API module. * * @internal */ import { TransportResponse } from '../../types/transport-response'; import { TransportMethod } from '../../types/transport-request'; import { AbstractRequest } from '../../components/request'; import RequestOperation from '../../constants/operations'; import * as PAM from '../../types/api/access-manager'; import { KeySet } from '../../types/api'; // -------------------------------------------------------- // ------------------------ Types ------------------------- // -------------------------------------------------------- // region Types /** * Request configuration parameters. */ type RequestParameters = (PAM.GrantTokenParameters | PAM.ObjectsGrantTokenParameters) & { /** * PubNub REST API access key set. */ keySet: KeySet; }; /** * Permissions group payload. * * User can configure permissions per-resource or per-resource which match RegExp. */ type PermissionPayload = { /** * Object containing `uuid` metadata permissions. */ uuids?: Record<string, number>; /** * Object containing `channel` permissions. */ channels?: Record<string, number>; /** * Object containing `channel group` permissions. */ groups?: Record<string, number>; /** * Extra metadata to be published with the request. * * **Important:** Values must be scalar only; `arrays` or `objects` aren't supported. */ meta?: PAM.Metadata; }; /** * Service success response. */ type ServiceResponse = { /** * Request result status code. */ status: number; /** * Request processing result data. */ data: { /** * Permissions token grant human-readable result. */ message: string; /** * Generate token with requested permissions. */ token: string; }; /** * Name of the service which provided response. */ service: string; }; // endregion /** * Grant token permissions request. * * @internal */ export class GrantTokenRequest extends AbstractRequest<PAM.GrantTokenResponse, ServiceResponse> { constructor(private readonly parameters: RequestParameters) { super({ method: TransportMethod.POST }); // Apply defaults. this.parameters.resources ??= {}; this.parameters.patterns ??= {}; } operation(): RequestOperation { return RequestOperation.PNAccessManagerGrantToken; } validate(): string | undefined { const { keySet: { subscribeKey, publishKey, secretKey }, resources, patterns, } = this.parameters; if (!subscribeKey) return 'Missing Subscribe Key'; if (!publishKey) return 'Missing Publish Key'; if (!secretKey) return 'Missing Secret Key'; if (!resources && !patterns) return 'Missing either Resources or Patterns'; if ( this.isVspPermissions(this.parameters) && ('channels' in (this.parameters.resources ?? {}) || 'uuids' in (this.parameters.resources ?? {}) || 'groups' in (this.parameters.resources ?? {}) || 'channels' in (this.parameters.patterns ?? {}) || 'uuids' in (this.parameters.patterns ?? {}) || 'groups' in (this.parameters.patterns ?? {})) ) return ( 'Cannot mix `users`, `spaces` and `authorizedUserId` with `uuids`, `channels`,' + ' `groups` and `authorized_uuid`' ); let permissionsEmpty = true; [this.parameters.resources, this.parameters.patterns].forEach((refPerm) => { Object.keys(refPerm ?? {}).forEach((scope) => { // @ts-expect-error Permissions with backward compatibility. if (refPerm && permissionsEmpty && Object.keys(refPerm[scope] ?? {}).length > 0) { permissionsEmpty = false; } }); }); if (permissionsEmpty) return 'Missing values for either Resources or Patterns'; } async parse(response: TransportResponse): Promise<PAM.GrantTokenResponse> { return this.deserializeResponse(response).data.token; } protected get path(): string { return `/v3/pam/${this.parameters.keySet.subscribeKey}/grant`; } protected get headers(): Record<string, string> | undefined { return { ...(super.headers ?? {}), 'Content-Type': 'application/json' }; } protected get body(): string { const { ttl, meta } = this.parameters; const body: Record<string, unknown> = { ...(ttl || ttl === 0 ? { ttl } : {}) }; const uuid = this.isVspPermissions(this.parameters) ? this.parameters.authorizedUserId : this.parameters.authorized_uuid; const permissions: Record<string, PAM.Metadata | string | Record<string, PermissionPayload>> = {}; const resourcePermissions: PermissionPayload = {}; const patternPermissions: PermissionPayload = {}; const mapPermissions = ( name: string, permissionBit: number, type: keyof PermissionPayload, permissions: PermissionPayload, ) => { if (!permissions[type]) permissions[type] = {}; permissions[type]![name] = permissionBit; }; const { resources, patterns } = this.parameters; [resources, patterns].forEach((refPerm, idx) => { const target = idx === 0 ? resourcePermissions : patternPermissions; let channelsPermissions: Record<string, PAM.ChannelTokenPermissions> = {}; let channelGroupsPermissions: Record<string, PAM.ChannelGroupTokenPermissions> = {}; let uuidsPermissions: Record<string, PAM.UuidTokenPermissions> = {}; if (!target.channels) target.channels = {}; if (!target.groups) target.groups = {}; if (!target.uuids) target.uuids = {}; // @ts-expect-error Not used, needed for api backward compatibility if (!target.users) target.users = {}; // @ts-expect-error Not used, needed for api backward compatibility if (!target.spaces) target.spaces = {}; if (refPerm) { // Check whether working with legacy Objects permissions. if ('spaces' in refPerm || 'users' in refPerm) { channelsPermissions = refPerm.spaces ?? {}; uuidsPermissions = refPerm.users ?? {}; } else if ('channels' in refPerm || 'uuids' in refPerm || 'groups' in refPerm) { channelsPermissions = refPerm.channels ?? {}; channelGroupsPermissions = refPerm.groups ?? {}; uuidsPermissions = refPerm.uuids ?? {}; } } Object.keys(channelsPermissions).forEach((channel) => mapPermissions(channel, this.extractPermissions(channelsPermissions[channel]), 'channels', target), ); Object.keys(channelGroupsPermissions).forEach((groups) => mapPermissions(groups, this.extractPermissions(channelGroupsPermissions[groups]), 'groups', target), ); Object.keys(uuidsPermissions).forEach((uuids) => mapPermissions(uuids, this.extractPermissions(uuidsPermissions[uuids]), 'uuids', target), ); }); if (uuid) permissions.uuid = `${uuid}`; permissions.resources = resourcePermissions; permissions.patterns = patternPermissions; permissions.meta = meta ?? {}; body.permissions = permissions; return JSON.stringify(body); } /** * Extract permissions bit from permission configuration object. * * @param permissions - User provided scope-based permissions. * * @returns Permissions bit. */ private extractPermissions( permissions: PAM.UuidTokenPermissions | PAM.ChannelTokenPermissions | PAM.ChannelGroupTokenPermissions, ): number { let permissionsResult = 0; if ('join' in permissions && permissions.join) permissionsResult |= 128; if ('update' in permissions && permissions.update) permissionsResult |= 64; if ('get' in permissions && permissions.get) permissionsResult |= 32; if ('delete' in permissions && permissions.delete) permissionsResult |= 8; if ('manage' in permissions && permissions.manage) permissionsResult |= 4; if ('write' in permissions && permissions.write) permissionsResult |= 2; if ('read' in permissions && permissions.read) permissionsResult |= 1; return permissionsResult; } /** * Check whether provided parameters is part of legacy VSP access token configuration. * * @param parameters - Parameters which should be checked. * * @returns VSP request parameters if it is legacy configuration. */ private isVspPermissions( parameters: PAM.GrantTokenParameters | PAM.ObjectsGrantTokenParameters, ): parameters is PAM.ObjectsGrantTokenParameters { return ( 'authorizedUserId' in parameters || 'spaces' in (parameters.resources ?? {}) || 'users' in (parameters.resources ?? {}) || 'spaces' in (parameters.patterns ?? {}) || 'users' in (parameters.patterns ?? {}) ); } }