From 2794e29f46f855e3ebe881372c9233bfa59413e4 Mon Sep 17 00:00:00 2001 From: Jim Ezesinachi Date: Mon, 21 Oct 2024 20:42:37 +0100 Subject: [PATCH] feat: permissions (#61) --- packages/backend/src/QueryEngine.ts | 22 ++- packages/backend/src/SynthqlError.ts | 28 +++- .../backend/src/execution/middleware.test.ts | 48 ++++-- packages/backend/src/execution/middleware.ts | 54 ++++++- .../properties/permissions.test.ts | 147 ++++++++++++++++++ packages/backend/src/tests/queryEngine.ts | 9 +- .../static/reference/assets/navigation.js | 2 +- .../docs/static/reference/assets/search.js | 2 +- packages/queries/src/query.ts | 45 ++++++ packages/queries/src/types/AnyQuery.ts | 12 ++ packages/queries/src/types/types.ts | 1 + 11 files changed, 347 insertions(+), 23 deletions(-) create mode 100644 packages/backend/src/tests/propertyBased/properties/permissions.test.ts diff --git a/packages/backend/src/QueryEngine.ts b/packages/backend/src/QueryEngine.ts index e7b0b49b..8fb0ec29 100644 --- a/packages/backend/src/QueryEngine.ts +++ b/packages/backend/src/QueryEngine.ts @@ -4,7 +4,7 @@ import { QueryPlan, collectLast } from '.'; import { QueryExecutor } from './execution/types'; import { QueryProvider } from './QueryProvider'; import { execute } from './execution/execute'; -import { Middleware } from './execution/middleware'; +import { Middleware, permissionsMiddleware } from './execution/middleware'; import { PgExecutor } from './execution/executors/PgExecutor'; import { QueryProviderExecutor } from './execution/executors/QueryProviderExecutor'; import { composeQuery } from './execution/executors/PgExecutor/composeQuery'; @@ -15,7 +15,7 @@ export interface QueryEngineProps { /** * The database connection string. * - * e.g. `postgresql://user:password@localhost:5432/db`. + * e.g. `postgresql://user:password@localhost:5432/db` * * If you use this option, SynthQL will create * a conection pool for you internally. @@ -39,6 +39,12 @@ export interface QueryEngineProps { * ``` */ prependSql?: string; + /** + * If true, the executor will execute queries that don't + * have the listed permissions in `query.permissions` + * passed via the query context permissions list. + */ + dangerouslyIgnorePermissions?: boolean; /** * A list of middlewares that you want to be used to * transform any matching queries, before execution. @@ -119,7 +125,7 @@ export class QueryEngine { private pool: Pool; private schema: string; private prependSql?: string; - private middlewares: Array; + private middlewares: Array>; private executors: Array; constructor(config: QueryEngineProps) { @@ -131,7 +137,9 @@ export class QueryEngine { connectionString: config.url, max: 10, }); - this.middlewares = config.middlewares ?? []; + this.middlewares = config.dangerouslyIgnorePermissions + ? config.middlewares ?? [] + : [...(config.middlewares ?? []), permissionsMiddleware]; const qpe = new QueryProviderExecutor(config.providers ?? []); this.executors = [ @@ -158,7 +166,7 @@ export class QueryEngine { * The name of the database schema to * execute your SynthQL query against * - * e.g `public` + * e.g. `public` */ schema?: string; /** @@ -174,12 +182,12 @@ export class QueryEngine { if ( middleware.predicate({ query, - context: opts?.context, + context: opts?.context ?? {}, }) ) { transformedQuery = middleware.transformQuery({ query: transformedQuery, - context: opts?.context, + context: opts?.context ?? {}, }); } } diff --git a/packages/backend/src/SynthqlError.ts b/packages/backend/src/SynthqlError.ts index a91b6741..306dde71 100644 --- a/packages/backend/src/SynthqlError.ts +++ b/packages/backend/src/SynthqlError.ts @@ -92,7 +92,7 @@ export class SynthqlError extends Error { '', JSON.stringify(query, null, 2), '', - 'Check your query and make sure you have `read` access to all included', + 'Check your query and make sure you have`read` access to all included', 'tables and columns, and have registered all queries via the QueryEngine', ]; @@ -135,6 +135,31 @@ export class SynthqlError extends Error { return new SynthqlError(new Error(), type, lines.join('\n'), 404); } + + static createPermissionsError({ + query, + missingPermissions, + contextPermissions, + }: { + query: AnyQuery; + missingPermissions: string[]; + contextPermissions: string[]; + }) { + const type = 'PermissionsError'; + + const lines = [ + `The query ${query?.name} is missing the following permissions: ${missingPermissions.join(', ')}`, + '', + 'in the passed context permissions list:', + `${JSON.stringify(contextPermissions, null, 2)}`, + '', + 'from its list of required permissions defined:', + `${JSON.stringify(query?.permissions, null, 2)}`, + '', + ]; + + return new SynthqlError(new Error(), type, lines.join('\n'), 403); + } } function printError(err: any): string { @@ -161,6 +186,7 @@ function composeMessage(err: any, props: SqlExecutionErrorProps): string { '', 'And which was composed from the following SynthQL query:', JSON.stringify(props.query, null, 2), + '', ]; return lines.join('\n'); diff --git a/packages/backend/src/execution/middleware.test.ts b/packages/backend/src/execution/middleware.test.ts index 7d4a408e..432a4349 100644 --- a/packages/backend/src/execution/middleware.test.ts +++ b/packages/backend/src/execution/middleware.test.ts @@ -1,16 +1,19 @@ import { test, describe, expect } from 'vitest'; import { DB, from } from '../tests/generated'; -import { Query } from '@synthql/queries'; +import { col, Query } from '@synthql/queries'; import { middleware } from './middleware'; import { createQueryEngine } from '../tests/queryEngine'; // Create type/interface for context type UserRole = 'user' | 'admin' | 'super'; +type UserPermission = 'user:read' | 'admin:read' | 'super:read'; + interface Session { id: number; email: string; - roles: UserRole[]; isActive: boolean; + roles: UserRole[]; + permissions: UserPermission[]; } // Create middleware @@ -28,9 +31,12 @@ const restrictPaymentsByCustomer = middleware, Session>({ }), }); -describe('createExpressSynthqlHandler', async () => { - test('1', async () => { - const queryEngine = createQueryEngine([restrictPaymentsByCustomer]); +describe('middleware', async () => { + test('Query middleware is correctly executed', async () => { + const queryEngine = createQueryEngine({ + middlewares: [restrictPaymentsByCustomer], + dangerouslyIgnorePermissions: false, + }); // Create context // This would usually be an object generated from a server @@ -38,14 +44,15 @@ describe('createExpressSynthqlHandler', async () => { const context: Session = { id: 1, email: 'user@example.com', - roles: ['user', 'admin', 'super'], isActive: true, + roles: ['user', 'admin', 'super'], + permissions: ['user:read', 'admin:read', 'super:read'], }; // Create base query - const q = from('payment').one(); + const q = createPaymentQuery().one(); - const queryWithContextManuallyAdded = from('payment') + const queryWithContextManuallyAdded = createPaymentQuery() .where({ customer_id: context.id, }) @@ -54,8 +61,31 @@ describe('createExpressSynthqlHandler', async () => { const result = await queryEngine.executeAndWait(q, { context }); const resultFromQueryWithContextManuallyAdded = - await queryEngine.executeAndWait(queryWithContextManuallyAdded); + await queryEngine.executeAndWait(queryWithContextManuallyAdded, { + context: { permissions: context.permissions }, + }); expect(result).toEqual(resultFromQueryWithContextManuallyAdded); }); }); + +function createPaymentQuery() { + return from('payment') + .permissions('user:read') + .include({ + customer: from('customer') + .permissions('admin:read') + .where({ + customer_id: col('payment.customer_id'), + }) + .one(), + }) + .groupBy( + 'amount', + 'customer_id', + 'payment_date', + 'payment_id', + 'rental_id', + 'staff_id', + ); +} diff --git a/packages/backend/src/execution/middleware.ts b/packages/backend/src/execution/middleware.ts index b87a94fb..4603ac32 100644 --- a/packages/backend/src/execution/middleware.ts +++ b/packages/backend/src/execution/middleware.ts @@ -1,4 +1,11 @@ -export interface Middleware { +import { AnyContext, AnyQuery } from '@synthql/queries'; +import { mapRecursive } from '../util/tree/mapRecursive'; +import { SynthqlError } from '../SynthqlError'; + +export interface Middleware< + TQuery extends AnyQuery, + TContext extends AnyContext, +> { predicate: ({ query, context, @@ -15,7 +22,10 @@ export interface Middleware { }) => TQuery; } -export function middleware({ +export function middleware< + TQuery extends AnyQuery, + TContext extends AnyContext, +>({ predicate, transformQuery, }: { @@ -39,3 +49,43 @@ export function middleware({ transformQuery, }; } + +export const permissionsMiddleware = middleware({ + predicate: () => true, + transformQuery: ({ query, context }) => { + throwIfPermissionsMissing(query, context?.permissions); + + return query; + }, +}); + +function throwIfPermissionsMissing( + query: AnyQuery, + contextPermissions: AnyContext['permissions'] = [], +) { + mapRecursive(query, (node) => { + if (isQueryWithPermissions(node)) { + const missingPermissions = node?.permissions + ? node?.permissions.filter( + (permission) => !contextPermissions.includes(permission), + ) + : []; + + if (missingPermissions.length > 0) { + throw SynthqlError.createPermissionsError({ + query: node, + missingPermissions, + contextPermissions, + }); + } + } + + return node; + }); +} + +function isQueryWithPermissions( + x: any, +): x is AnyQuery & { permissions: string[] } { + return Array.isArray(x?.permissions); +} diff --git a/packages/backend/src/tests/propertyBased/properties/permissions.test.ts b/packages/backend/src/tests/propertyBased/properties/permissions.test.ts new file mode 100644 index 00000000..6d10d203 --- /dev/null +++ b/packages/backend/src/tests/propertyBased/properties/permissions.test.ts @@ -0,0 +1,147 @@ +import { describe, expect } from 'vitest'; +import { test } from '@fast-check/vitest'; +import { AnyQuery, Query } from '@synthql/queries'; +import { ArbitraryQueryBuilder } from '../arbitraries/ArbitraryQueryBuilder'; +import { createQueryEngine } from '../../queryEngine'; +import { DB } from '../../generated'; + +const queryBuilder = ArbitraryQueryBuilder.fromPagila(); +const queryEngine = createQueryEngine({ + dangerouslyIgnorePermissions: false, +}); + +// Create type/interface for context +type UserRole = 'user' | 'admin' | 'super'; +type UserPermission = 'user:read' | 'admin:read' | 'super:read'; +interface Session { + id: number; + email: string; + isActive: boolean; + roles: UserRole[]; + permissions: UserPermission[]; +} + +// Create context +// This would usually be an object generated from a server +// request handler (e.g a parsed cookie/token) +const context: Session = { + id: 1, + email: 'user@example.com', + isActive: true, + roles: ['user', 'admin', 'super'], + permissions: ['user:read', 'admin:read', 'super:read'], +}; + +describe('Property based tests for permissions', () => { + const numRuns = 100; + const timeout = numRuns * 1000; + const endOnFailure = true; + const testConfig = { + verbose: true, + numRuns, + endOnFailure, + }; + + test.prop( + [queryBuilder.withCardinality('many').withSomeResults().build()], + testConfig, + )( + ['A query with permissions will fail when no permissions are met'].join( + '', + ), + async (query) => { + const permissionedQuery: Query = { + ...query, + permissions: context.permissions, + name: defaultName(query), + }; + + // Here we check if it errors when + // none of the permissions are met + expect( + async () => await queryEngine.executeAndWait(permissionedQuery), + ).rejects.toThrow( + `The query ${permissionedQuery?.name} is missing the following permissions: ${context.permissions.join(', ')}`, + ); + }, + timeout, + ); + + test.prop( + [queryBuilder.withCardinality('many').withSomeResults().build()], + testConfig, + )( + [ + 'A query with permissions will fail when not all permissions are met', + ].join(''), + async (query) => { + const permissionedQuery: Query = { + ...query, + permissions: context.permissions, + name: defaultName(query), + }; + + const [permission, ...rest] = context.permissions; + + // Here we check if it errors when some, + // but not all the permissions, are met + expect( + async () => + await queryEngine.executeAndWait(permissionedQuery, { + context: { permissions: [permission] }, + }), + ).rejects.toThrow( + `The query ${permissionedQuery?.name} is missing the following permissions: ${rest.join(', ')}`, + ); + }, + timeout, + ); + + test.prop( + [queryBuilder.withCardinality('many').withSomeResults().build()], + testConfig, + )( + [ + 'A query with no permissions will never fail for permission issues', + ].join(''), + async (query) => { + expect( + async () => await queryEngine.executeAndWait(query), + ).not.toThrow(); + }, + timeout, + ); + + test.prop( + [queryBuilder.withCardinality('many').withSomeResults().build()], + testConfig, + )( + [ + 'A query with all permissions will never fail for permission issues', + ].join(''), + async (query) => { + const permissionedQuery: Query = { + ...query, + permissions: context.permissions, + name: defaultName(query), + }; + + await expect( + queryEngine.executeAndWait(permissionedQuery, { + context: { permissions: context.permissions }, + }), + ).resolves.not.toThrow(); + }, + timeout, + ); +}); + +function defaultName(query: AnyQuery) { + const whereName = Object.keys(query.where ?? {}).join('-and-'); + + if (whereName === '') { + return `${query.from}-all`; + } + + return `${query.from}-by-${whereName}`; +} diff --git a/packages/backend/src/tests/queryEngine.ts b/packages/backend/src/tests/queryEngine.ts index 9bbcdd33..981cb7ba 100644 --- a/packages/backend/src/tests/queryEngine.ts +++ b/packages/backend/src/tests/queryEngine.ts @@ -11,10 +11,15 @@ export const pool = new Pool({ 'postgres://postgres:postgres@localhost:5432/postgres', }); -export function createQueryEngine(middlewares?: Array>) { +export function createQueryEngine(data?: { + middlewares?: Array>; + dangerouslyIgnorePermissions?: boolean; +}) { return new QueryEngine({ pool, schema: 'public', - middlewares, + middlewares: data?.middlewares, + dangerouslyIgnorePermissions: + data?.dangerouslyIgnorePermissions ?? true, }); } diff --git a/packages/docs/static/reference/assets/navigation.js b/packages/docs/static/reference/assets/navigation.js index a63992c5..a5b463ca 100644 --- a/packages/docs/static/reference/assets/navigation.js +++ b/packages/docs/static/reference/assets/navigation.js @@ -1 +1 @@ -window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAAA5WXbU/bMBCA/0s+lzHYYFu/lVJpbBN0Ldo0IYSMcyUWjpPaDmuF9t+nOmnjxM7ZfK0fP3658+V695po2OhknDwS+gwiPVaSJqOkJDpLxklepBUHddwMPihJ32U658koeWYiTcano4RmjKcSRDK+O8h+ViC3M/HEBLQyyolSPZkFdsUnp5//jQ6+5VbobM1nUhYSF9okZrQWnsuiVK2VCQ1yRejwTs2E3j2cnfflc05EnHVHYjpacA5U/yBKt8JVJahmhej6LLRrPP/YEeZlocAsHja2LKLMWZpy+EskhIQt6eju7T1y5s9FypknD62ZGREpB3kEm1KCUn5LAz00UHRmUwlEw6ye1STb19rlO7hvGUSB3sj+XAI2Gj/Ujog+0TVs9NBB9Lb0ad0pvZXef/l0cmbHxJ2xgHUFdkJHr9XMDC1ZXzN2OjdMh1WHJqMBYkLLQpVAB8LTjkcH5wkESKK9j6qn26PoFtcVSAYDb6IZjN7ctOBVLi5h5S1ztu1AYmXuAO3qK0jNwF+VveJ2TtQSbzCjwiXNICdBWY1holvyyCHmKvcgJpuI7eVF/2XZEgOEHtBEbHtfCK/I82XwuszGAy7DxLo6tzWsc+/KMV4wQeT2psR0eybWtXuLhVNKfcaaDHmnRKZMEM40GhALCxpNhqMyQ8R5brclGt2WivP9IryKEBosZLyEFUgJ6QJUxZ1Pji3tkiHvlaC8StFdNkjI9K1gAs+/mgh5gg826rXWXSmRJAfttgOO7kBGecNBsLCQcQEr/NoMELIsYdc0Y5qaCHmCJS6qvv3OQKIWA0RZppxUKuyqsWBlu7qeLP483Mxni8ntzWLZal+IZLuT9epbj+/6P3R6tYL7OhzbRguO/P1gaihn/bY+j6p7STZk9KRaR1TuFgtpDIRI1kP/2WyJgY5O0E5QAhnqU81QdBfYNMjTQpgffP1LK+zCaGdVkyZM38GpaY5yD4ayWA1st83hVt1l+xfayeBmF3NZvLDUn3/OlvcwmnyD9+ATOziiVm8R92BPbrXiSkGzC9zZclEyJ2K405ti5g3c/wcPd/4t9RIAAA==" \ No newline at end of file +window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAAE5WXXW/TMBRA/0ueC2ODDehb11ZigLbSTiA0TZPn3K7WXCe1ndEK8d9RnbR2Yufae5xyfGzfD+/27m+mYauzYfZI6DOI/ERJmg2ykuhVNszWRV5xUCfNxwcl6duVXvNskD0zkWfDs0FGV4znEkQ2vDvKflQgd1PxxARYGeVEqY7MAdvi07NP/wZH32In9GrDp1IWEhe6JGZ0Np7JolTWyoQGuSS0/6RmQScO5xdd+YwTkWbdk5iOFpwD1d+J0la4rATVrBBtn4O2jRcfWsJ1WSgwm8eNlkWUa5bnHP4QCTGhJT3dvXtGzsK1SDkL1KGzckVEzkG+gW0pQamwpYEeGii5sqkEomFar2qK7UvtCl08tA2iQCNyuJeArcYvtSeSb3QNW913Eb0rQ1p/SWend58/np67OfFXzGFTgVvQyXs1K2Nb1mHGbuen6bhr32I0QUxoWagSaE967Pfk5DyBAEl0sKk6ugOKHnFTgWTQ0xPNx+TDjQtercUElsFnzrUdSeyZO0L79xWkZhB+lYNiuyZpi1eYUeGCrmBNorIaw0S35JFDSigPICYbid24EOaPTnu5JkvFWmkkdpPLiGpymWDp/McJigL/aYIuE4iIyzCprlb0+3V+7D3jJRNE7m5KTHdgUl373i68pzlkrMmYd0xkzgThTKMJcbCo0XQMKjNEmud2V6LZtVSa7yfhVYLQYDHjBJYgJeRzUBVHe6xNxrxXgvIqR0/ZIDHT14IJvP5qIuaJNmxSt9ZTLpFkDdofLzzdkUzyxpPgYDHjHJZ42AwQsyxgP4RjmpqIeaJPXNL79msFErUYIMky5qRScVeNRV+2q+vR/PfDzWw6H93ezBdW+0Ik29+s8751+Lb/fWv2K3hoYnJttODIzxmm+mo2bOvyqLpTZH3GQKm1ROV+s5jGQIhk0/cb0JUY6M0pOllKIH1zr/mUPFU2A7c3wDjzkBW2YXRSq0mTpm/gvWme8gDGqlj1HNfWsFW32W5AWxXcnGImixeWh+vPO/IBRouvNw4hsYcjavUacQcO1JYVVwqaU+BOyyXJvIzhzmCJmR64/w/9nO/IRRMAAA==" \ No newline at end of file diff --git a/packages/docs/static/reference/assets/search.js b/packages/docs/static/reference/assets/search.js index 798ef0f0..c77385ce 100644 --- a/packages/docs/static/reference/assets/search.js +++ b/packages/docs/static/reference/assets/search.js @@ -1 +1 @@ -window.searchData = "data:application/octet-stream;base64,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"; \ No newline at end of file +window.searchData = "data:application/octet-stream;base64,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"; \ No newline at end of file diff --git a/packages/queries/src/query.ts b/packages/queries/src/query.ts index 8d755519..934c5d3f 100644 --- a/packages/queries/src/query.ts +++ b/packages/queries/src/query.ts @@ -32,6 +32,7 @@ export class QueryBuilder< private _cardinality: TCardinality, private _lazy: TLazy, private _groupBy: TGroupBy, + private _permissions?: string[], private _name?: string, ) { validateNestedQueriesHaveAValidRefOp({ @@ -44,15 +45,18 @@ export class QueryBuilder< cardinality: this._cardinality ?? 'many', lazy: this._lazy, groupBy: this._groupBy, + permissions: this._permissions, name: this._name, }); } private defaultName() { const whereName = Object.keys(this._where).join('-and-'); + if (whereName === '') { return `${this._from}-all`; } + return `${this._from}-by-${whereName}`; } @@ -66,6 +70,7 @@ export class QueryBuilder< cardinality: TCardinality; lazy: TLazy; groupBy: TGroupBy; + permissions: string[]; hash: string; name: string; } { @@ -81,6 +86,7 @@ export class QueryBuilder< cardinality: this._cardinality ?? 'many', lazy: this._lazy, groupBy: this._groupBy, + permissions: this._permissions ?? [], hash: '', name: this._name ?? defaultName, }; @@ -115,6 +121,7 @@ export class QueryBuilder< this._cardinality, this._lazy, this._groupBy, + this._permissions, this._name, ); } @@ -188,6 +195,7 @@ export class QueryBuilder< this._cardinality, this._lazy, this._groupBy, + this._permissions, this._name, ); } @@ -235,6 +243,7 @@ export class QueryBuilder< this._cardinality, this._lazy, this._groupBy, + this._permissions, this._name, ); } @@ -286,6 +295,7 @@ export class QueryBuilder< this._cardinality, this._lazy, this._groupBy, + this._permissions, this._name, ); } @@ -312,6 +322,7 @@ export class QueryBuilder< this._cardinality, this._lazy, this._groupBy, + this._permissions, this._name, ); } @@ -338,6 +349,7 @@ export class QueryBuilder< this._cardinality, this._lazy, this._groupBy, + this._permissions, this._name, ); } @@ -371,6 +383,7 @@ export class QueryBuilder< this._cardinality, this._lazy, this._groupBy, + this._permissions, this._name, ); } @@ -410,6 +423,7 @@ export class QueryBuilder< this._cardinality, true, this._groupBy, + this._permissions, this._name, ); } @@ -436,6 +450,7 @@ export class QueryBuilder< this._cardinality, this._lazy, id, + this._permissions, this._name, ); } @@ -464,6 +479,7 @@ export class QueryBuilder< cardinality, this._lazy, this._groupBy, + this._permissions, this._name, ); } @@ -502,9 +518,37 @@ export class QueryBuilder< this._cardinality, this._lazy, this._groupBy, + this._permissions, name, ); } + + permissions(...permissions: string[]) { + return new QueryBuilder< + DB, + TTable, + TWhere, + TSelect, + TInclude, + TLimit, + TOffset, + TCardinality, + TLazy, + TGroupBy + >( + this._from, + this._where, + this._select, + this._include, + this._limit, + this._offset, + this._cardinality, + this._lazy, + this._groupBy, + permissions, + this._name, + ); + } } export function query(schema: Schema) { @@ -536,6 +580,7 @@ export function query(schema: Schema) { undefined, primaryKeys, undefined, + undefined, ); }, }; diff --git a/packages/queries/src/types/AnyQuery.ts b/packages/queries/src/types/AnyQuery.ts index ae4de94d..e638fb3a 100644 --- a/packages/queries/src/types/AnyQuery.ts +++ b/packages/queries/src/types/AnyQuery.ts @@ -1,6 +1,17 @@ import { Table } from './Table'; import { Query } from './types'; +export type AnyContext = { + /** + * A list of permissions that you can pass as + * part of a context object with which SynthQL + * will use to transform the query using any + * middlewares registered on the QueryEngine + * that it matches + */ + permissions?: string[]; +}; + export type AnyTableDef = { columns: Record< string, @@ -14,6 +25,7 @@ export type AnyTableDef = { } >; }; + export type AnyDB = Record; export type AnyTable = Table; export type AnyQuery = Query; diff --git a/packages/queries/src/types/types.ts b/packages/queries/src/types/types.ts index 98818ef5..d365d8eb 100644 --- a/packages/queries/src/types/types.ts +++ b/packages/queries/src/types/types.ts @@ -14,6 +14,7 @@ export type Query = Table> = { cardinality?: Cardinality; lazy?: true; groupBy?: string[]; + permissions?: string[]; hash?: string; name?: string; };