diff --git a/src/commands/solid-perms.ts b/src/commands/solid-perms.ts index 914285d..91a847b 100644 --- a/src/commands/solid-perms.ts +++ b/src/commands/solid-perms.ts @@ -1,33 +1,7 @@ +import { getResourceInfo, universalAccess } from "@inrupt/solid-client"; + import { - getAgentAccessAll, - AgentAccess, - Access, - getPublicAccess, - getResourceAcl, - getGroupAccessAll, - getAgentDefaultAccessAll, - getPublicDefaultAccess, - getGroupDefaultAccessAll, - getAgentResourceAccessAll, - getGroupResourceAccessAll, - getPublicResourceAccess, - getResourceInfoWithAcl, - setAgentDefaultAccess, - setGroupDefaultAccess, - setPublicDefaultAccess, - setAgentResourceAccess, - setGroupResourceAccess, - setPublicResourceAccess, - hasAccessibleAcl, - getFallbackAcl, - AclDataset, - saveAclFor, - WithAccessibleAcl, - deleteAclFor, - hasResourceAcl, - createAclFromFallbackAcl, - hasFallbackAcl, - createAcl, + AccessModes, } from '@inrupt/solid-client'; import { writeErrorString } from '../utils/util'; import type { Logger } from '../logger'; @@ -39,49 +13,38 @@ export type Record = { [P in K]: T; }; +export type UniversalAccess = { + read: boolean; + append: boolean; + write: boolean; + controlWrite: boolean; + controlRead: boolean; +}; + export interface IPermissionListing { access: { - agent?: null | AgentAccess, - group?: null | Record, - public?: null | Access + agent?: null | Record, + public?: null | AccessModes }, default?: { - agent?: null | AgentAccess, - group?: null | Record, - public?: null | Access + agent?: null | Record, + public?: null | AccessModes } resource?: { - agent?: null | AgentAccess, - group?: null | Record, - public?: null | Access + agent?: null | Record, + public?: null | AccessModes } } + export async function listPermissions(resourceUrl: string, options?: ICommandOptionsPermissions) { let commandOptions = setOptionDefaults(options || {}); let permissions : IPermissionListing = { access: {} } try { - const resourceInfo = await getResourceInfoWithAcl(resourceUrl, { fetch: commandOptions.fetch }) - permissions.access.agent = await getAgentAccessAll(resourceInfo) - permissions.access.group = await getGroupAccessAll(resourceInfo) - permissions.access.public = await getPublicAccess(resourceInfo) - - let aclDataset = getResourceAcl(resourceInfo); - - if (aclDataset) { - permissions.default = {}; - permissions.default.agent = await getAgentDefaultAccessAll(aclDataset) - permissions.default.group = await getGroupDefaultAccessAll(aclDataset) - permissions.default.public = await getPublicDefaultAccess(aclDataset) - - permissions.resource = {}; - permissions.resource.agent = await getAgentResourceAccessAll(aclDataset) - permissions.resource.group = await getGroupResourceAccessAll(aclDataset) - permissions.resource.public = await getPublicResourceAccess(aclDataset) - - } + permissions.access.agent = await universalAccess.getAgentAccessAll(resourceUrl, {fetch: commandOptions.fetch}) + permissions.access.public = await universalAccess.getPublicAccess(resourceUrl, {fetch: commandOptions.fetch}) return permissions } catch (e) { if (commandOptions.verbose) writeErrorString(`Could not retrieve permissions for ${resourceUrl}`, e, commandOptions) @@ -89,93 +52,55 @@ export async function listPermissions(resourceUrl: string, options?: ICommandOpt } export interface IPermissionOperation { - type: 'agent' | 'group' | 'public', + type: 'agent' | 'public', id?: string, read?: boolean, write?: boolean, append?: boolean, control?: boolean, - default?: boolean, + acl?: boolean, } -export async function changePermissions(resourceUrl: string, operations: IPermissionOperation[], options?: ICommandOptionsPermissions) { - let commandOptions = setOptionDefaults(options || {}); - const resourceInfo = await getResourceInfoWithAcl(resourceUrl, { fetch: commandOptions.fetch }) - let aclDataset : AclDataset | null; - if (await hasResourceAcl(resourceInfo)) { - aclDataset = await getResourceAcl(resourceInfo); - } else { +// todo: getWebID here +export async function setPermission(resourceUrl: string, operations: IPermissionOperation[], options?: ICommandOptionsPermissions) { + let commandOptions = setOptionDefaults(options || {}); + + for (let operation of operations) { try { - if (hasFallbackAcl(resourceInfo) && hasAccessibleAcl(resourceInfo)) { - aclDataset = await createAclFromFallbackAcl(resourceInfo) - } else if (hasAccessibleAcl(resourceInfo)) { - aclDataset = await createAcl(resourceInfo) - } else { - throw new Error('No acl found in path to root. This tool requires at least a root acl to be set.'); + if (operation.type === 'agent') { + // Update access rights + if (!operation.id) { throw new Error('Please specify agent id in the passed operation.')} + let access: UniversalAccess = { read: false, write: false, append: false, controlRead: false, controlWrite: false } + access = updateAccess(access, operation) + // Update local acl for agent with new rights + await universalAccess.setAgentAccess(resourceUrl, operation.id, access, { fetch: commandOptions.fetch }) + } else if (operation.type === 'public') { + // Update access rights + let access: UniversalAccess = { read: false, write: false, append: false, controlRead: false, controlWrite: false } + access = updateAccess(access, operation) + // Update local acl for agent with new rights + await universalAccess.setPublicAccess(resourceUrl, access, { fetch: commandOptions.fetch }) + } else { + if (commandOptions.verbose) writeErrorString("Incorrect operation type", 'Please provide an operation type of agent or public.', commandOptions) } + commandOptions.logger.log(`Updated permissions for: ${resourceUrl}`) } catch (e) { - throw new Error(`Could not find fallback ACL file to initialize permissions for ${resourceUrl}: ${(e).message}`) + if (commandOptions.verbose) writeErrorString( + `Problem setting permissions for resource ${resourceUrl} operation type`, (e as Error).message, commandOptions + ) } } - if (!aclDataset) { - throw new Error(`You do not have the permissions to edit the ACL file for ${resourceUrl}`) - } - for (let operation of operations) { - if (operation.type === 'agent') { - // Update access rights - if (!operation.id) { throw new Error('Please specify agent id in the passed operation.')} - let access = { read: false, write: false, append: false, control: false } - access = updateAccess(access, operation) - // Update local acl for agent with new rights - if (operation.default) aclDataset = await setAgentDefaultAccess(aclDataset, operation.id, access) - aclDataset = await setAgentResourceAccess(aclDataset, operation.id, access) - - } else if (operation.type === 'group') { - // Update access rights - if (!operation.id) { throw new Error('Please specify group id in the passed operation.')} - let access = { read: false, write: false, append: false, control: false } - access = updateAccess(access, operation) - // Update local acl for group with new rights - if (operation.default) aclDataset = await setGroupDefaultAccess(aclDataset, operation.id, access) - aclDataset = await setGroupResourceAccess(aclDataset, operation.id, access) - - } else if (operation.type === 'public') { - // Update access rights - if (!operation.id) { throw new Error('Please specify agent id in the passed operation.')} - let access = { read: false, write: false, append: false, control: false } - access = updateAccess(access, operation) - // Update local acl for agent with new rights - if (operation.default) aclDataset = await setPublicDefaultAccess(aclDataset, access) - aclDataset = await setPublicResourceAccess(aclDataset, access) - } else { - if (commandOptions.verbose) writeErrorString("Incorrect operation type", 'Please provide an operation type of agent, group or public.', commandOptions) - } - } - // Post updated acl to pod - if (aclDataset && await hasAccessibleAcl(resourceInfo)) { - await saveAclFor(resourceInfo as WithAccessibleAcl, aclDataset, {fetch: commandOptions.fetch}) - if (commandOptions.verbose) commandOptions.logger.log(`Updated permissions for: ${resourceUrl}`) - } -} - -export async function deletePermissions(resourceUrl: string, options?: ICommandOptionsPermissions) { - let commandOptions = setOptionDefaults(options || {}); - - let resourceInfo = await getResourceInfoWithAcl(resourceUrl, {fetch: commandOptions.fetch}) - if (hasAccessibleAcl(resourceInfo)) { - await deleteAclFor(resourceInfo, {fetch: commandOptions.fetch}) - if (commandOptions.verbose) commandOptions.logger.log(`Deleted resource at ${resourceUrl}`) - } else { - throw Error(`Resource at ${resourceUrl} does not have an accessible ACL resource`) - } } -function updateAccess(access: Access, operation: IPermissionOperation) { +function updateAccess(access: UniversalAccess, operation: IPermissionOperation) { if (operation.read !== undefined) access.read = operation.read if (operation.write !== undefined) access.write = operation.write if (operation.append !== undefined) access.append = operation.append - if (operation.control !== undefined) access.control = operation.control + if (operation.control !== undefined) { + access.controlRead = operation.control + access.controlWrite = operation.control + } return access -} \ No newline at end of file +} diff --git a/src/commands/solid-perms_acl.ts b/src/commands/solid-perms_acl.ts new file mode 100644 index 0000000..914285d --- /dev/null +++ b/src/commands/solid-perms_acl.ts @@ -0,0 +1,181 @@ +import { + getAgentAccessAll, + AgentAccess, + Access, + getPublicAccess, + getResourceAcl, + getGroupAccessAll, + getAgentDefaultAccessAll, + getPublicDefaultAccess, + getGroupDefaultAccessAll, + getAgentResourceAccessAll, + getGroupResourceAccessAll, + getPublicResourceAccess, + getResourceInfoWithAcl, + setAgentDefaultAccess, + setGroupDefaultAccess, + setPublicDefaultAccess, + setAgentResourceAccess, + setGroupResourceAccess, + setPublicResourceAccess, + hasAccessibleAcl, + getFallbackAcl, + AclDataset, + saveAclFor, + WithAccessibleAcl, + deleteAclFor, + hasResourceAcl, + createAclFromFallbackAcl, + hasFallbackAcl, + createAcl, +} from '@inrupt/solid-client'; +import { writeErrorString } from '../utils/util'; +import type { Logger } from '../logger'; +import { ICommandOptions, setOptionDefaults } from './solid-command'; + +export interface ICommandOptionsPermissions extends ICommandOptions { } + +export type Record = { + [P in K]: T; +}; + + +export interface IPermissionListing { + access: { + agent?: null | AgentAccess, + group?: null | Record, + public?: null | Access + }, + default?: { + agent?: null | AgentAccess, + group?: null | Record, + public?: null | Access + } + resource?: { + agent?: null | AgentAccess, + group?: null | Record, + public?: null | Access + } +} + +export async function listPermissions(resourceUrl: string, options?: ICommandOptionsPermissions) { + let commandOptions = setOptionDefaults(options || {}); + + let permissions : IPermissionListing = { access: {} } + try { + const resourceInfo = await getResourceInfoWithAcl(resourceUrl, { fetch: commandOptions.fetch }) + permissions.access.agent = await getAgentAccessAll(resourceInfo) + permissions.access.group = await getGroupAccessAll(resourceInfo) + permissions.access.public = await getPublicAccess(resourceInfo) + + let aclDataset = getResourceAcl(resourceInfo); + + if (aclDataset) { + permissions.default = {}; + permissions.default.agent = await getAgentDefaultAccessAll(aclDataset) + permissions.default.group = await getGroupDefaultAccessAll(aclDataset) + permissions.default.public = await getPublicDefaultAccess(aclDataset) + + permissions.resource = {}; + permissions.resource.agent = await getAgentResourceAccessAll(aclDataset) + permissions.resource.group = await getGroupResourceAccessAll(aclDataset) + permissions.resource.public = await getPublicResourceAccess(aclDataset) + + } + return permissions + } catch (e) { + if (commandOptions.verbose) writeErrorString(`Could not retrieve permissions for ${resourceUrl}`, e, commandOptions) + } +} + +export interface IPermissionOperation { + type: 'agent' | 'group' | 'public', + id?: string, + read?: boolean, + write?: boolean, + append?: boolean, + control?: boolean, + default?: boolean, +} + +export async function changePermissions(resourceUrl: string, operations: IPermissionOperation[], options?: ICommandOptionsPermissions) { + let commandOptions = setOptionDefaults(options || {}); + + const resourceInfo = await getResourceInfoWithAcl(resourceUrl, { fetch: commandOptions.fetch }) + let aclDataset : AclDataset | null; + if (await hasResourceAcl(resourceInfo)) { + aclDataset = await getResourceAcl(resourceInfo); + } else { + try { + if (hasFallbackAcl(resourceInfo) && hasAccessibleAcl(resourceInfo)) { + aclDataset = await createAclFromFallbackAcl(resourceInfo) + } else if (hasAccessibleAcl(resourceInfo)) { + aclDataset = await createAcl(resourceInfo) + } else { + throw new Error('No acl found in path to root. This tool requires at least a root acl to be set.'); + } + } catch (e) { + throw new Error(`Could not find fallback ACL file to initialize permissions for ${resourceUrl}: ${(e).message}`) + } + } + if (!aclDataset) { + throw new Error(`You do not have the permissions to edit the ACL file for ${resourceUrl}`) + } + + for (let operation of operations) { + if (operation.type === 'agent') { + // Update access rights + if (!operation.id) { throw new Error('Please specify agent id in the passed operation.')} + let access = { read: false, write: false, append: false, control: false } + access = updateAccess(access, operation) + // Update local acl for agent with new rights + if (operation.default) aclDataset = await setAgentDefaultAccess(aclDataset, operation.id, access) + aclDataset = await setAgentResourceAccess(aclDataset, operation.id, access) + + } else if (operation.type === 'group') { + // Update access rights + if (!operation.id) { throw new Error('Please specify group id in the passed operation.')} + let access = { read: false, write: false, append: false, control: false } + access = updateAccess(access, operation) + // Update local acl for group with new rights + if (operation.default) aclDataset = await setGroupDefaultAccess(aclDataset, operation.id, access) + aclDataset = await setGroupResourceAccess(aclDataset, operation.id, access) + + } else if (operation.type === 'public') { + // Update access rights + if (!operation.id) { throw new Error('Please specify agent id in the passed operation.')} + let access = { read: false, write: false, append: false, control: false } + access = updateAccess(access, operation) + // Update local acl for agent with new rights + if (operation.default) aclDataset = await setPublicDefaultAccess(aclDataset, access) + aclDataset = await setPublicResourceAccess(aclDataset, access) + } else { + if (commandOptions.verbose) writeErrorString("Incorrect operation type", 'Please provide an operation type of agent, group or public.', commandOptions) + } + } + // Post updated acl to pod + if (aclDataset && await hasAccessibleAcl(resourceInfo)) { + await saveAclFor(resourceInfo as WithAccessibleAcl, aclDataset, {fetch: commandOptions.fetch}) + if (commandOptions.verbose) commandOptions.logger.log(`Updated permissions for: ${resourceUrl}`) + } +} + +export async function deletePermissions(resourceUrl: string, options?: ICommandOptionsPermissions) { + let commandOptions = setOptionDefaults(options || {}); + + let resourceInfo = await getResourceInfoWithAcl(resourceUrl, {fetch: commandOptions.fetch}) + if (hasAccessibleAcl(resourceInfo)) { + await deleteAclFor(resourceInfo, {fetch: commandOptions.fetch}) + if (commandOptions.verbose) commandOptions.logger.log(`Deleted resource at ${resourceUrl}`) + } else { + throw Error(`Resource at ${resourceUrl} does not have an accessible ACL resource`) + } +} + +function updateAccess(access: Access, operation: IPermissionOperation) { + if (operation.read !== undefined) access.read = operation.read + if (operation.write !== undefined) access.write = operation.write + if (operation.append !== undefined) access.append = operation.append + if (operation.control !== undefined) access.control = operation.control + return access +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 1bd2e0b..ff557af 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,13 +9,14 @@ import makeDirectory, { ICommandOptionsMakeDirectory } from "./commands/solid-mk import touch, {ICommandOptionsTouch} from "./commands/solid-touch" // import shell from "./commands/solid-shell" import createSolidPods, {IAccountData} from "./commands/solid-pod-create" -import { listPermissions, changePermissions, deletePermissions, ICommandOptionsPermissions, IPermissionOperation, IPermissionListing, Record } from './commands/solid-perms' +import { listPermissions, setPermission, ICommandOptionsPermissions, IPermissionOperation, IPermissionListing, Record } from './commands/solid-perms' +import { changePermissions as setPermissionsAcl, deletePermissions as deletePermissionsAcl } from './commands/solid-perms_acl' import { authenticateWithTokenFromJavascript } from "./authentication/AuthenticationToken" import { generateCSSToken, ICSSClientCredentialsTokenGenerationOptions, CSSToken } from "./authentication/TokenCreationCSS" import { FileInfo, ResourceInfo } from './utils/util'; // General Solid functionality -export { copy, list, remove, move, find, query, listPermissions, changePermissions, deletePermissions, makeDirectory, touch } +export { copy, list, remove, move, find, query, listPermissions, setPermission, setPermissionsAcl, deletePermissionsAcl, makeDirectory, touch } // Authentication Functionality export { authenticateWithTokenFromJavascript as authenticateToken, generateCSSToken } diff --git a/src/shell/commands/perms.ts b/src/shell/commands/perms.ts index a413471..94818cc 100644 --- a/src/shell/commands/perms.ts +++ b/src/shell/commands/perms.ts @@ -1,5 +1,6 @@ import { Command } from 'commander'; -import { changePermissions, deletePermissions, listPermissions, IPermissionOperation } from '../../commands/solid-perms'; +import * as acl_perms from '../../commands/solid-perms_acl'; +import { setPermission, listPermissions, IPermissionOperation } from '../../commands/solid-perms'; import authenticate from '../../authentication/authenticate'; import { addEnvOptions, changeUrlPrefixes, getAndNormalizeURL } from '../../utils/shellutils'; import { writeErrorString } from '../../utils/util'; @@ -12,101 +13,120 @@ export default class PermsCommand extends SolidCommand { public addCommand(program: Command) { this.programopts = program.opts(); - program - .command('chmod') - .description('Utility to list and edit resource permissions on a data pod. Only supports operations on ACL and not ACP.') - .argument('', 'list, edit, delete') + + const access = program + .command('access') + .description('Control and edit resource permissions.'); + + + + access + .command('list') .argument('', 'Resource URL') - .argument('[permissions...]', `Permission operations to edit resource permissions. - Formatted according to id=[d][g][a][c][r][w]. - For public permissions please set id to "p". - For the current authenticated user please set id to "u". - To set updated permissions as default, please add the [d] option as follows: id=d[g][a][c][r][w] - To indicate the id as a group id, please add the [g] option as follows: id=g[d][a][c][r][w] - `) .option('-p, --pretty', 'Pretty format') - .option('-v, --verbose', 'Log all operations') // Should this be default? - .action(this.executeCommand) - - program - .command('perms') - .description('Utility to list and edit resource permissions on a data pod. Only supports operations on ACL and not ACP.') - .argument('', 'list, edit, delete') + .option('-v, --verbose', 'Log all operations') + .action(async (url: string, options: any) => { + let listings = await listPermissions(url, options) + if (listings) formatPermissionListing(url, listings, options) + if (this.mayExit) process.exit(0) + }) + + + access + .command('set') .argument('', 'Resource URL') - .argument('[permissions...]', `Permission operations to edit resource permissions. - Formatted according to id=[d][g][a][c][r][w]. + .argument('[permissions...]', + `Permission format when setting permissions. + Format according to id=[a][c][r][w]. For public permissions please set id to "p". For the current authenticated user please set id to "u". - To set updated permissions as default, please add the [d] option as follows: id=d[g][a][c][r][w] - To indicate the id as a group id, please add the [g] option as follows: id=g[d][a][c][r][w] + For specific agents, set id to be the agent webid. `) - .option('-p, --pretty', 'Pretty format') + .option('--acl', 'Enables ACL specific operations --default and --group') + .option('--default', 'Set the defined permissions as default (only in --acl mode)') + .option('--group', 'Process identifier as a group identifier (only in --acl mode)') .option('-v, --verbose', 'Log all operations') // Should this be default? - .action(this.executeCommand) + .action( async (url: string, permissions: string[], options: any) => { - return program - } - - async executeCommand (operation: string, url: string, permissions: string[], options: any) { - let programOpts = addEnvOptions(this.programopts || {}); - const authenticationInfo = await authenticate(programOpts) - options.fetch = authenticationInfo.fetch - try { - if (this.shell) url = getAndNormalizeURL(url, this.shell); - url = await changeUrlPrefixes(authenticationInfo, url) + let programOpts = addEnvOptions(this.programopts || {}); + const authenticationInfo = await authenticate(programOpts) + options.fetch = authenticationInfo.fetch + url = await changeUrlPrefixes(authenticationInfo, url) - if (operation === 'list') { - let listings = await listPermissions(url, options) - if (listings) formatPermissionListing(url, listings, options) - } else if (operation === 'edit') { - let parsedPermissions = permissions.map(permission => { - const splitPerm = permission.split('=') - if (! (splitPerm.length === 2)) { - writeErrorString('Incorrect permission format.', 'Please format your permissions as id=[d][a][c][r][w].', options) - process.exit(0) - } - let id = splitPerm[0] - const permissionOptions = splitPerm[1].split('') - let type; - if (id === 'p') { - type = 'public' - } else if (id === 'u') { - if (!authenticationInfo.webId) { - writeErrorString('Could not autmatically fill in webId of authenticated user.', 'Please make sure you have an authenticated session to auto-fill your webId', options); + try { + // if (this.shell) url = getAndNormalizeURL(url, this.shell); + let parsedPermissions = permissions.map(permission => { + const splitPerm = permission.split('=') + if (! (splitPerm.length === 2)) { + writeErrorString('Incorrect permission format.', 'Please format your permissions as id=[a][c][r][w].', options) + process.exit(0) + } + let id = splitPerm[0] + const permissionOptions = splitPerm[1].split('') + let type; + const acl = options.acl + + if (options.group) { + if (!acl) { + writeErrorString('Cannot set group permissions outside of --acl mode.', options); + process.exit(0) + } + type = 'group' + } else if (id === 'p') { + type = 'public' + } else if (id === 'u') { + if (!authenticationInfo.webId) { + writeErrorString('Could not autmatically fill in webId of authenticated user.', 'Please make sure you have an authenticated session to auto-fill your webId', options); + process.exit(0) + } + type = 'agent' + id = authenticationInfo.webId + } + const read = permissionOptions.indexOf('r') !== -1 + const write = permissionOptions.indexOf('w') !== -1 + const append = permissionOptions.indexOf('a') !== -1 + const control = permissionOptions.indexOf('c') !== -1 + const def = options.default + if (options.default && !options.acl) { + writeErrorString('Cannot set default permissions outside of --acl mode.', options); process.exit(0) } - type = 'agent' - id = authenticationInfo.webId - } else { - type = permissionOptions.indexOf('g') === -1 ? 'agent' : 'group' + return ({ type, id, read, write, append, control, default: def, acl } as IPermissionOperation) + }) + try { + for (let permission of parsedPermissions) { + if (permission.acl) { + await acl_perms.changePermissions(url, parsedPermissions, options) + } else { + await setPermission(url, parsedPermissions, options) + } + + } + } catch (e) { + if (options.verbose) writeErrorString(`Could not update permissions for resource at ${url}`, e, options) } - const read = permissionOptions.indexOf('r') !== -1 - const write = permissionOptions.indexOf('w') !== -1 - const append = permissionOptions.indexOf('a') !== -1 - const control = permissionOptions.indexOf('c') !== -1 - const def = permissionOptions.indexOf('d') !== -1 - return ({ type, id, read, write, append, control, default: def } as IPermissionOperation) - }) - try { - await changePermissions(url, parsedPermissions, options) - } catch (e) { - if (options.verbose) writeErrorString(`Could not update permissions for resource at ${url}`, e, options) } - } else if (operation === 'delete') { + catch (e) { + writeErrorString(`Could not evaluate permissions for ${url}`, e, options) + if (this.mayExit) process.exit(1) + } + }) + + access + .command('delete') + .argument('', 'Resource URL') + .description('Delete ACL resource attached to resource with given URI. Does not work for ACP based pods!') + .option('-v, --verbose', 'Log all operations') // Should this be default? + .action(async (url: string, options: any) => { try { - await deletePermissions(url, options) + await acl_perms.deletePermissions(url, options) } catch (e) { if (options.verbose) writeErrorString(`Could not delete permissions for resource at ${url}`, e, options) } - } else { - console.error('Invalid operation.') - } - } - catch (e) { - writeErrorString(`Could not evaluate permissions for ${url}`, e, options) - if (this.mayExit) process.exit(1) - } - if (this.mayExit) process.exit(0) + if (this.mayExit) process.exit(0) + }) + + return program } } diff --git a/src/shell/commands/perms_acl.ts b/src/shell/commands/perms_acl.ts new file mode 100644 index 0000000..e9b0ea9 --- /dev/null +++ b/src/shell/commands/perms_acl.ts @@ -0,0 +1,273 @@ +import { Command } from 'commander'; +import { changePermissions, deletePermissions, listPermissions, IPermissionOperation } from '../../commands/solid-perms_acl'; +import authenticate from '../../authentication/authenticate'; +import { addEnvOptions, changeUrlPrefixes, getAndNormalizeURL } from '../../utils/shellutils'; +import { writeErrorString } from '../../utils/util'; +import chalk from 'chalk'; +import SolidCommand from './SolidCommand'; +const Table = require('cli-table'); + +export default class PermsCommand extends SolidCommand { + + public addCommand(program: Command) { + this.programopts = program.opts(); + + program + .command('chmod') + .description('Utility to list and edit resource permissions on a data pod. Only supports operations on ACL and not ACP.') + .argument('', 'list, edit, delete') + .argument('', 'Resource URL') + .argument('[permissions...]', `Permission operations to edit resource permissions. + Formatted according to id=[d][g][a][c][r][w]. + For public permissions please set id to "p". + For the current authenticated user please set id to "u". + To set updated permissions as default, please add the [d] option as follows: id=d[g][a][c][r][w] + To indicate the id as a group id, please add the [g] option as follows: id=g[d][a][c][r][w] + `) + .option('-p, --pretty', 'Pretty format') + .option('-v, --verbose', 'Log all operations') // Should this be default? + .action(this.executeCommand) + + program + .command('perms') + .description('Utility to list and edit resource permissions on a data pod. Only supports operations on ACL and not ACP.') + .argument('', 'list, edit, delete') + .argument('', 'Resource URL') + .argument('[permissions...]', `Permission operations to edit resource permissions. + Formatted according to id=[d][g][a][c][r][w]. + For public permissions please set id to "p". + For the current authenticated user please set id to "u". + To set updated permissions as default, please add the [d] option as follows: id=d[g][a][c][r][w] + To indicate the id as a group id, please add the [g] option as follows: id=g[d][a][c][r][w] + `) + .option('-p, --pretty', 'Pretty format') + .option('-v, --verbose', 'Log all operations') // Should this be default? + .action(this.executeCommand) + + return program + } + + async executeCommand (operation: string, url: string, permissions: string[], options: any) { + let programOpts = addEnvOptions(this.programopts || {}); + const authenticationInfo = await authenticate(programOpts) + options.fetch = authenticationInfo.fetch + try { + if (this.shell) url = getAndNormalizeURL(url, this.shell); + url = await changeUrlPrefixes(authenticationInfo, url) + + if (operation === 'list') { + let listings = await listPermissions(url, options) + if (listings) formatPermissionListing(url, listings, options) + } else if (operation === 'edit') { + let parsedPermissions = permissions.map(permission => { + const splitPerm = permission.split('=') + if (! (splitPerm.length === 2)) { + writeErrorString('Incorrect permission format.', 'Please format your permissions as id=[d][a][c][r][w].', options) + process.exit(0) + } + let id = splitPerm[0] + const permissionOptions = splitPerm[1].split('') + let type; + if (id === 'p') { + type = 'public' + } else if (id === 'u') { + if (!authenticationInfo.webId) { + writeErrorString('Could not autmatically fill in webId of authenticated user.', 'Please make sure you have an authenticated session to auto-fill your webId', options); + process.exit(0) + } + type = 'agent' + id = authenticationInfo.webId + } else { + type = permissionOptions.indexOf('g') === -1 ? 'agent' : 'group' + } + const read = permissionOptions.indexOf('r') !== -1 + const write = permissionOptions.indexOf('w') !== -1 + const append = permissionOptions.indexOf('a') !== -1 + const control = permissionOptions.indexOf('c') !== -1 + const def = permissionOptions.indexOf('d') !== -1 + return ({ type, id, read, write, append, control, default: def } as IPermissionOperation) + }) + try { + await changePermissions(url, parsedPermissions, options) + } catch (e) { + if (options.verbose) writeErrorString(`Could not update permissions for resource at ${url}`, e, options) + } + } else if (operation === 'delete') { + try { + await deletePermissions(url, options) + } catch (e) { + if (options.verbose) writeErrorString(`Could not delete permissions for resource at ${url}`, e, options) + } + } else { + console.error('Invalid operation.') + } + } + catch (e) { + writeErrorString(`Could not evaluate permissions for ${url}`, e, options) + if (this.mayExit) process.exit(1) + } + if (this.mayExit) process.exit(0) + } + +} + + + + +function formatPermissionListing(url: string, permissions: any, options: any) { + let formattedString = `` + let formattedPerms = permissions.access + if (permissions.resource) { + if (permissions.resource.agent) { + for (let agentId of Object.keys(permissions.resource.agent)) { + formattedPerms.agent[agentId]['resource'] = true + } + } + if (permissions.resource.group) { + for (let groupId of Object.keys(permissions.resource.group)) { + formattedPerms.group[groupId]['resource'] = true + } + } + if (permissions.resource.public) { + formattedPerms.public['resource'] = true + } + } + + if (permissions.default) { + if (permissions.default.agent) { + for (let agentId of Object.keys(permissions.default.agent)) { + formattedPerms.agent[agentId]['default'] = true; + } + } + if (permissions.default.group) { + for (let groupId of Object.keys(permissions.default.group)) { + formattedPerms.group[groupId]['default'] = true; + } + } + if (permissions.default.public) { + let isDefault = true; + for (let value of ["read", "append", "write", "control"]) { + if (permissions.resource.public[value] !== permissions.default.public[value]) { + isDefault = false; + } + } + if (isDefault) formattedPerms.public['default'] = true; + } + } + + + if (options.pretty) { + let head = [ + chalk.cyan.bold("ID"), + chalk.bold("read"), + chalk.bold("append"), + chalk.bold("write"), + chalk.bold("control"), + "inherited", + "is default", + ] + let table = new Table({ head }); + if (!isEmpty(formattedPerms.agent)) { + table.push([chalk.bold('Agent'), '', '', '', '', '', '']) + for (let id of Object.keys(formattedPerms.agent)) { + table.push([ + id, + formattedPerms.agent[id].read || 'false', + formattedPerms.agent[id].append || 'false', + formattedPerms.agent[id].write || 'false', + formattedPerms.agent[id].control || 'false', + formattedPerms.agent[id].resource ? !formattedPerms.agent[id].resource : 'true', // inherited + formattedPerms.agent[id].default || 'false', + ]) + } + } + if (!isEmpty(formattedPerms.group)) { + table.push([chalk.bold('Group'), '', '', '', '', '', '']) + for (let id of Object.keys(formattedPerms.group)) { + table.push([ + chalk.green(id), + formattedPerms.group[id].read || 'false', + formattedPerms.group[id].append || 'false', + formattedPerms.group[id].write || 'false', + formattedPerms.group[id].control || 'false', + formattedPerms.group[id].resource ? !formattedPerms.group[id].resource : 'true', // inherited + formattedPerms.group[id].default || 'false', + ]) + } + } + if (!isEmpty(formattedPerms.public)) { + table.push([chalk.bold('Public'), '', '', '', '', '', '']) + table.push([ + chalk.blue('#public'), + chalk.bold(formattedPerms.public.read || 'false'), + chalk.bold(formattedPerms.public.append || 'false'), + chalk.bold(formattedPerms.public.write || 'false'), + chalk.bold(formattedPerms.public.control || 'false'), + chalk.bold(formattedPerms.public.resource ? !formattedPerms.public.resource : 'true'), // inherited + chalk.bold(formattedPerms.public.default || 'false'), + ]) + + } + formattedString += `> ${chalk.bold(url)}\n` + formattedString += `${table.toString()}` + } else { + formattedString += `> ${chalk.bold(url)}\n` + if (!isEmpty(formattedPerms.agent)) { + formattedString += `${chalk.bold('Agent')}\n` + for (let id of Object.keys(formattedPerms.agent)) { + formattedString += `${id} - ` + let inherited = true; + for (let permission of Object.entries(formattedPerms.agent[id])) { + if (permission[0] === 'resource') { + inherited = false + } else if (permission[1]) { + formattedString += `${permission[0]} ` + } + } + if (inherited) { + formattedString += `${chalk.cyan('inherited')} ` + } + formattedString += `\n` + } + } + if (!isEmpty(formattedPerms.group)) { + formattedString += `${chalk.bold('Group')}\n` + for (let id of Object.keys(formattedPerms.group)) { + formattedString += `${id} - ` + let inherited = true; + for (let permission of Object.entries(formattedPerms.group[id])) { + if (permission[0] === 'resource') { + inherited = false + } else if (permission[1]) { + formattedString += `${permission[0]} ` + } + } + if (inherited) { + formattedString += `${chalk.cyan('inherited')} ` + } + formattedString += `\n` + } + } + if (!isEmpty(formattedPerms.public)) { + formattedString += `${chalk.bold('Public')}\n` + formattedString += `${'#public'} - ` + let inherited = true; + for (let permission of Object.entries(formattedPerms.public)) { + if (permission[0] === 'resource') { + inherited = false + } else if (permission[1]) { + formattedString += `${permission[0]} ` + } + } + if (inherited) { + formattedString += `${chalk.cyan('inherited')} ` + } + formattedString += `\n` + } + } + console.log(formattedString) +} + +function isEmpty (obj: any) { + return Object.keys(obj).length === 0 +}