diff --git a/docs/docs/cmd/spo/file/file-roleassignment-list.mdx b/docs/docs/cmd/spo/file/file-roleassignment-list.mdx new file mode 100644 index 00000000000..0278370f824 --- /dev/null +++ b/docs/docs/cmd/spo/file/file-roleassignment-list.mdx @@ -0,0 +1,71 @@ +import Global from '../../_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# spo file roleassignment list + +Lists all role assignments from a specific file + +## Usage + +```sh +m365 spo file roleassignment list [options] +``` + +## Options + +```md definition-list +`-u, --webUrl ` +: URL of the site where the file is located. + +`--fileUrl [fileUrl]` +: The server- or site-relative decoded URL of the file. Specify either `fileUrl` or `fileId` but not both. + +`-i, --fileId [fileId]` +: The UniqueId (GUID) of the file. Specify either `fileUrl` or `fileId` but not both. +``` + + + +## Examples + +List all role assignments of a file specified by URL. + +```sh +m365 spo file roleassignment list --webUrl "https://contoso.sharepoint.com/sites/Marketing" --fileUrl "/Branding/Logos/Contoso.jpg" +``` + +List all role assignments of a file specified by ID. + +```sh +m365 spo file roleassignment list --webUrl "https://contoso.sharepoint.com/sites/Marketing" --fileId "04796e10-cb5f-4aa3-a438-bc06028a9073" +``` + +## Response + + + + + ```json + ``` + + + + + ```text + ``` + + + + + ```csv + ``` + + + + + ```md + ``` + + + diff --git a/docs/src/config/sidebars.ts b/docs/src/config/sidebars.ts index e6e537e6841..930f992fbe7 100644 --- a/docs/src/config/sidebars.ts +++ b/docs/src/config/sidebars.ts @@ -2771,6 +2771,11 @@ const sidebars: SidebarsConfig = { label: 'file roleassignment add', id: 'cmd/spo/file/file-roleassignment-add' }, + { + type: 'doc', + label: 'file roleassignment list', + id: 'cmd/spo/file/file-roleassignment-list' + }, { type: 'doc', label: 'file roleassignment remove', diff --git a/src/m365/spo/commands.ts b/src/m365/spo/commands.ts index f5c71f8667a..ae7e24a7be9 100644 --- a/src/m365/spo/commands.ts +++ b/src/m365/spo/commands.ts @@ -76,6 +76,7 @@ export default { FILE_RETENTIONLABEL_ENSURE: `${prefix} file retentionlabel ensure`, FILE_RETENTIONLABEL_REMOVE: `${prefix} file retentionlabel remove`, FILE_ROLEASSIGNMENT_ADD: `${prefix} file roleassignment add`, + FILE_ROLEASSIGNMENT_LIST: `${prefix} file roleassignment list`, FILE_ROLEASSIGNMENT_REMOVE: `${prefix} file roleassignment remove`, FILE_ROLEINHERITANCE_BREAK: `${prefix} file roleinheritance break`, FILE_ROLEINHERITANCE_RESET: `${prefix} file roleinheritance reset`, diff --git a/src/m365/spo/commands/file/file-get.spec.ts b/src/m365/spo/commands/file/file-get.spec.ts index 9004e28018b..d218388cf0a 100644 --- a/src/m365/spo/commands/file/file-get.spec.ts +++ b/src/m365/spo/commands/file/file-get.spec.ts @@ -15,8 +15,81 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './file-get.js'; import { settingsNames } from '../../../../settingsNames.js'; +import { spo } from '../../../../utils/spo.js'; describe(commands.FILE_GET, () => { + const fileRoleAssignmentsResponse = { + value: [{ + "Member": { + "Id": 3, + "IsHiddenInUI": false, + "LoginName": "Communication site Owners", + "Title": "Communication site Owners", + "PrincipalType": 8, + "AllowMembersEditMembership": false, + "AllowRequestToJoinLeave": false, + "AutoAcceptRequestToJoinLeave": false, + "Description": null, + "OnlyAllowMembersViewMembership": false, + "OwnerTitle": "Communication site Owners", + "RequestToJoinLeaveEmailSetting": "" + }, + "RoleDefinitionBindings": [ + { + "BasePermissions": { + "High": "2147483647", + "Low": "4294967295" + }, + "Description": "Has full control.", + "Hidden": false, + "Id": 1073741829, + "Name": "Full Control", + "Order": 1, + "RoleTypeKind": 5, + "BasePermissionsValue": [ + "ViewListItems", + "AddListItems", + "EditListItems", + "DeleteListItems", + "ApproveItems", + "OpenItems", + "ViewVersions", + "DeleteVersions", + "CancelCheckout", + "ManagePersonalViews", + "ManageLists", + "ViewFormPages", + "AnonymousSearchAccessList", + "Open", + "ViewPages", + "AddAndCustomizePages", + "ApplyThemeAndBorder", + "ApplyStyleSheets", + "ViewUsageData", + "CreateSSCSite", + "ManageSubwebs", + "CreateGroups", + "ManagePermissions", + "BrowseDirectories", + "BrowseUserInfo", + "AddDelPrivateWebParts", + "UpdatePersonalWebParts", + "ManageWeb", + "AnonymousSearchAccessWebLists", + "UseClientIntegration", + "UseRemoteAPIs", + "ManageAlerts", + "CreateAlerts", + "EditMyUserInfo", + "EnumeratePermissions" + ], + "RoleTypeKindValue": "Administrator" + } + ], + "PrincipalId": 3 + }] + }; + let log: any[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; @@ -51,6 +124,7 @@ describe(commands.FILE_GET, () => { sinonUtil.restore([ request.get, fs.createWriteStream, + spo.getFileRoleAssignments, cli.getSettingWithDefaultValue ]); }); @@ -275,8 +349,10 @@ describe(commands.FILE_GET, () => { }); it('retrieves and prints all details of file as ListItem object with permissions', async () => { + sinon.stub(spo, 'getFileRoleAssignments').resolves(fileRoleAssignmentsResponse.value); + sinon.stub(request, 'get').callsFake(async (opts) => { - if ((opts.url as string).indexOf('?$expand=ListItemAllFields') > -1) { + if (opts.url === "https://contoso.sharepoint.com/sites/project-x/_api/web/GetFileById('b2307a39-e878-458b-bc90-03bc578531d6')?$expand=ListItemAllFields") { return { "ListItemAllFields": { "FileSystemObjectType": 0, @@ -320,43 +396,6 @@ describe(commands.FILE_GET, () => { }; } - if (opts.url === `https://contoso.sharepoint.com/sites/project-x/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/project-x/Documents/Test1.docx')/ListItemAllFields/RoleAssignments?$expand=Member,RoleDefinitionBindings`) { - return { - value: [ - { - "Member": { - "Id": 3, - "IsHiddenInUI": false, - "LoginName": "Communication site Owners", - "Title": "Communication site Owners", - "PrincipalType": 8, - "AllowMembersEditMembership": false, - "AllowRequestToJoinLeave": false, - "AutoAcceptRequestToJoinLeave": false, - "Description": null, - "OnlyAllowMembersViewMembership": false, - "OwnerTitle": "Communication site Owners", - "RequestToJoinLeaveEmailSetting": "" - }, - "RoleDefinitionBindings": [ - { - "BasePermissions": { - "High": "2147483647", - "Low": "4294967295" - }, - "Description": "Has full control.", - "Hidden": false, - "Id": 1073741829, - "Name": "Full Control", - "Order": 1, - "RoleTypeKind": 5 - } - ], - "PrincipalId": 3 - }] - }; - } - throw 'Invalid request'; }); diff --git a/src/m365/spo/commands/file/file-get.ts b/src/m365/spo/commands/file/file-get.ts index 34aab34d22b..c36058235d9 100644 --- a/src/m365/spo/commands/file/file-get.ts +++ b/src/m365/spo/commands/file/file-get.ts @@ -9,6 +9,7 @@ import { validation } from '../../../../utils/validation.js'; import SpoCommand from '../../../base/SpoCommand.js'; import commands from '../../commands.js'; import { FileProperties } from './FileProperties.js'; +import { spo } from '../../../../utils/spo.js'; interface CommandArgs { options: Options; @@ -212,14 +213,10 @@ class SpoFileGetCommand extends SpoCommand { const fileProperties: FileProperties = JSON.parse(JSON.stringify(file)); if (args.options.withPermissions) { - requestOptions.url = `${args.options.webUrl}/_api/web/GetFileByServerRelativePath(DecodedUrl='${file.ServerRelativeUrl}')/ListItemAllFields/RoleAssignments?$expand=Member,RoleDefinitionBindings`; - const response = await request.get<{ value: any[] }>(requestOptions); - response.value.forEach((r: any) => { - r.RoleDefinitionBindings = formatting.setFriendlyPermissions(r.RoleDefinitionBindings); - }); - fileProperties.RoleAssignments = response.value; + const fileRoleAssignments = await spo.getFileRoleAssignments(args.options.webUrl, file.ServerRelativeUrl); + fileProperties.RoleAssignments = fileRoleAssignments; if (args.options.asListItem) { - fileProperties.ListItemAllFields.RoleAssignments = response.value; + fileProperties.ListItemAllFields.RoleAssignments = fileRoleAssignments; } } diff --git a/src/m365/spo/commands/file/file-roleassignment-list.spec.ts b/src/m365/spo/commands/file/file-roleassignment-list.spec.ts new file mode 100644 index 00000000000..c821b2ff569 --- /dev/null +++ b/src/m365/spo/commands/file/file-roleassignment-list.spec.ts @@ -0,0 +1,174 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import auth from '../../../../Auth.js'; +import { CommandError } from '../../../../Command.js'; +import { cli } from '../../../../cli/cli.js'; +import { CommandInfo } from '../../../../cli/CommandInfo.js'; +import { Logger } from '../../../../cli/Logger.js'; +import { telemetry } from '../../../../telemetry.js'; +import { pid } from '../../../../utils/pid.js'; +import { session } from '../../../../utils/session.js'; +import { sinonUtil } from '../../../../utils/sinonUtil.js'; +import commands from '../../commands.js'; +import { spo } from '../../../../utils/spo.js'; +import command from './file-roleassignment-list.js'; + +describe(commands.FILE_ROLEASSIGNMENT_LIST, () => { + const webUrl = 'https://contoso.sharepoint.com/sites/project-x'; + const fileId = 'b2307a39-e878-4586-8901-08ff728c5496'; + const fileUrl = '/sites/project-x/Documents/Test1.docx'; + const fileResponse = { + CheckInComment: '', + CheckOutType: 2, + ContentTag: '{F09C4EFE-B8C0-4E89-A166-03418661B89B},9,12', + CustomizedPageStatus: 0, + ETag: '"{F09C4EFE-B8C0-4E89-A166-03418661B89B},9"', + Exists: true, + IrmEnabled: false, + Length: '331673', + Level: 1, + LinkingUri: 'https://contoso.sharepoint.com/sites/project-x/documents/Test1.docx?d=wc39926a80d2c4067afa6cff9902eb866', + LinkingUrl: 'https://contoso.sharepoint.com/sites/project-x/documents/Test1.docx?d=wc39926a80d2c4067afa6cff9902eb866', + ListItemAllFields: { + Id: 1, + ID: 1 + }, + MajorVersion: 3, + MinorVersion: 0, + Name: 'Test1.docx', + ServerRelativeUrl: '/sites/project-x/documents/Test1.docx', + TimeCreated: '2018-02-05T08:42:36Z', + TimeLastModified: '2018-02-05T08:44:03Z', + Title: '', + UIVersion: 1536, + UIVersionLabel: '3.0', + UniqueId: 'b2307a39-e878-458b-bc90-03bc578531d6' + }; + const fileRoleAssignmentsResponse = { + value: [{ + Member: { + Id: 3, + IsHiddenInUI: false, + LoginName: "Communication site Owners", + Title: "Communication site Owners", + PrincipalType: 8, + AllowMembersEditMembership: false, + AllowRequestToJoinLeave: false, + AutoAcceptRequestToJoinLeave: false, + Description: null, + OnlyAllowMembersViewMembership: false, + OwnerTitle: "Communication site Owners", + RequestToJoinLeaveEmailSetting: "" + }, + RoleDefinitionBindings: [ + { + BasePermissions: { + High: "2147483647", + Low: "4294967295" + }, + Description: "Has full control.", + Hidden: false, + Id: 1073741829, + Name: "Full Control", + Order: 1, + RoleTypeKind: 5 + } + ], + PrincipalId: 32 + }] + }; + + let log: any[]; + let logger: Logger; + let loggerLogSpy: sinon.SinonSpy; + let commandInfo: CommandInfo; + + before(() => { + sinon.stub(auth, 'restoreAuth').resolves(); + sinon.stub(telemetry, 'trackEvent').resolves(); + sinon.stub(pid, 'getProcessName').returns(''); + sinon.stub(session, 'getId').returns(''); + auth.connection.active = true; + commandInfo = cli.getCommandInfo(command); + }); + + beforeEach(() => { + log = []; + logger = { + log: async (msg: string) => { + log.push(msg); + }, + logRaw: async (msg: string) => { + log.push(msg); + }, + logToStderr: async (msg: string) => { + log.push(msg); + } + }; + loggerLogSpy = sinon.spy(logger, 'log'); + }); + + afterEach(() => { + sinonUtil.restore([ + spo.getFileById, + spo.getFileByUrl, + spo.getFileRoleAssignments + ]); + }); + + after(() => { + sinon.restore(); + auth.connection.active = false; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.FILE_ROLEASSIGNMENT_LIST); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('fails validation if the webUrl option is not a valid SharePoint site URL', async () => { + const actual = await command.validate({ options: { webUrl: 'foo', fileId: fileId } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation if the fileId option is not a valid GUID', async () => { + const actual = await command.validate({ options: { webUrl: webUrl, fileId: 'foo' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('passes validation if webUrl and fileId are valid', async () => { + const actual = await command.validate({ options: { webUrl: webUrl, fileId: fileId } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('passes validation if webUrl and fileUrl are valid', async () => { + const actual = await command.validate({ options: { webUrl: webUrl, fileUrl: fileUrl } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('retrieves file role assignments by file id', async () => { + sinon.stub(spo, 'getFileById').resolves(fileResponse); + sinon.stub(spo, 'getFileRoleAssignments').resolves(fileRoleAssignmentsResponse.value); + + await command.action(logger, { options: { debug: true, webUrl: webUrl, fileId: fileId } }); + assert(loggerLogSpy.calledWith(fileRoleAssignmentsResponse.value)); + }); + + it('retrieves file role assignments by file url', async () => { + sinon.stub(spo, 'getFileByUrl').resolves(fileResponse); + sinon.stub(spo, 'getFileRoleAssignments').resolves(fileRoleAssignmentsResponse.value); + + await command.action(logger, { options: { debug: true, webUrl: webUrl, fileUrl: fileUrl } }); + assert(loggerLogSpy.calledWith(fileRoleAssignmentsResponse.value)); + }); + + it('correctly handles error when retrieving file role assignments', async () => { + sinon.stub(spo, 'getFileById').resolves(fileResponse); + sinon.stub(spo, 'getFileRoleAssignments').rejects(new Error('An error has occurred')); + + await assert.rejects(command.action(logger, { options: { debug: true, webUrl: webUrl, fileId: fileId } }), new CommandError('An error has occurred')); + }); +}); \ No newline at end of file diff --git a/src/m365/spo/commands/file/file-roleassignment-list.ts b/src/m365/spo/commands/file/file-roleassignment-list.ts new file mode 100644 index 00000000000..b8719ffa902 --- /dev/null +++ b/src/m365/spo/commands/file/file-roleassignment-list.ts @@ -0,0 +1,108 @@ +import { Logger } from '../../../../cli/Logger.js'; +import GlobalOptions from '../../../../GlobalOptions.js'; +import { spo } from '../../../../utils/spo.js'; +import { validation } from '../../../../utils/validation.js'; +import SpoCommand from '../../../base/SpoCommand.js'; +import commands from '../../commands.js'; + +interface CommandArgs { + options: Options; +} + +interface Options extends GlobalOptions { + webUrl: string; + fileUrl?: string; + fileId?: string; +} + +class SpoFileRoleAssignmentListCommand extends SpoCommand { + public get name(): string { + return commands.FILE_ROLEASSIGNMENT_LIST; + } + + public get description(): string { + return 'Lists all role assignments from a specific file.'; + } + + constructor() { + super(); + + this.#initTelemetry(); + this.#initOptions(); + this.#initValidators(); + this.#initOptionSets(); + this.#initTypes(); + } + + #initTelemetry(): void { + this.telemetry.push((args: CommandArgs) => { + Object.assign(this.telemetryProperties, { + fileUrl: typeof args.options.fileUrl !== 'undefined', + fileId: typeof args.options.fileId !== 'undefined' + }); + }); + } + + #initOptions(): void { + this.options.unshift( + { + option: '-u, --webUrl ' + }, + { + option: '--fileUrl [fileUrl]' + }, + { + option: '--fileId [fileId]' + } + ); + } + + #initValidators(): void { + this.validators.push( + async (args: CommandArgs) => { + const isValidSharePointUrl: boolean | string = validation.isValidSharePointUrl(args.options.webUrl); + if (isValidSharePointUrl !== true) { + return isValidSharePointUrl; + } + + if (args.options.fileId && !validation.isValidGuid(args.options.fileId)) { + return `${args.options.fileId} is not a valid GUID`; + } + + return true; + } + ); + } + + #initOptionSets(): void { + this.optionSets.push({ options: ['fileId', 'fileUrl'] }); + } + + #initTypes(): void { + this.types.string.push('webUrl', 'fileUrl', 'fileId'); + } + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + if (this.verbose) { + await logger.logToStderr(`Retrieving role assignments for file with ${args.options.fileId || args.options.fileUrl} in site at ${args.options.webUrl}...`); + } + + try { + let file; + if (args.options.fileId) { + file = await spo.getFileById(args.options.webUrl, args.options.fileId, logger, this.verbose); + } + else { + file = await spo.getFileByUrl(args.options.webUrl, args.options.fileUrl!, logger, this.verbose); + } + + const fileRoleAssignments = await spo.getFileRoleAssignments(args.options.webUrl, file.ServerRelativeUrl, logger, this.verbose); + await logger.log(fileRoleAssignments); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } +} + +export default new SpoFileRoleAssignmentListCommand(); \ No newline at end of file diff --git a/src/utils/spo.spec.ts b/src/utils/spo.spec.ts index d58850dd8eb..e89f438d184 100644 --- a/src/utils/spo.spec.ts +++ b/src/utils/spo.spec.ts @@ -34,7 +34,7 @@ const stubGetResponses: any = ( getFolderByServerRelativeUrlResp: any = null ) => { return sinon.stub(request, 'get').callsFake(async (opts) => { - if ((opts.url as string).indexOf('/_api/web/GetFolderByServerRelativePath(DecodedUrl=') > -1) { + if ((opts.url as string).indexOf('/_api/web/GetFolderByServerRelativePath(decodedUrl=') > -1) { if (getFolderByServerRelativeUrlResp) { throw getFolderByServerRelativeUrlResp; } @@ -618,7 +618,7 @@ describe('utils/spo', () => { stubGetResponses(JSON.stringify({ "odata.error": { "code": "-2130575338, Microsoft.SharePoint.SPException", "message": { "lang": "en-US", "value": "Error: Not found." } } })); await spo.ensureFolder("https://contoso.sharepoint.com", "/folder2/folder3", logger, true); - assert.strictEqual(postStubs.lastCall.args[0].url, 'https://contoso.sharepoint.com/_api/web/GetFolderByServerRelativePath(DecodedUrl=@a1)/AddSubFolderUsingPath(DecodedUrl=@a2)?@a1=%27%2Ffolder2%27&@a2=%27folder3%27'); + assert.strictEqual(postStubs.lastCall.args[0].url, 'https://contoso.sharepoint.com/_api/web/GetFolderByServerRelativePath(decodedUrl=@a1)/AddSubFolderUsingPath(decodedUrl=@a2)?@a1=%27%2Ffolder2%27&@a2=%27folder3%27'); }); it('should have the correct url including uppercase letters when calling AddSubFolderUsingPath', async () => { @@ -627,7 +627,7 @@ describe('utils/spo', () => { stubGetResponses(JSON.stringify({ "odata.error": { "code": "-2130575338, Microsoft.SharePoint.SPException", "message": { "lang": "en-US", "value": "Error: Not found." } } })); await spo.ensureFolder("https://contoso.sharepoint.com/sites/Site1", "/folder2/folder3", logger, true); - assert.strictEqual(postStubs.lastCall.args[0].url, 'https://contoso.sharepoint.com/sites/Site1/_api/web/GetFolderByServerRelativePath(DecodedUrl=@a1)/AddSubFolderUsingPath(DecodedUrl=@a2)?@a1=%27%2Fsites%2FSite1%2Ffolder2%27&@a2=%27folder3%27'); + assert.strictEqual(postStubs.lastCall.args[0].url, 'https://contoso.sharepoint.com/sites/Site1/_api/web/GetFolderByServerRelativePath(decodedUrl=@a1)/AddSubFolderUsingPath(decodedUrl=@a2)?@a1=%27%2Fsites%2FSite1%2Ffolder2%27&@a2=%27folder3%27'); }); it('should call two times AddSubFolderUsingPath when folderUrl is folder2/folder3', async () => { @@ -635,8 +635,8 @@ describe('utils/spo', () => { stubGetResponses(JSON.stringify({ "odata.error": { "code": "-2130575338, Microsoft.SharePoint.SPException", "message": { "lang": "en-US", "value": "Error: Not found." } } })); await spo.ensureFolder("https://contoso.sharepoint.com/sites/Site1", "/folder2/folder3", logger, true); - assert.strictEqual(postStubs.getCall(0).args[0].url, 'https://contoso.sharepoint.com/sites/Site1/_api/web/GetFolderByServerRelativePath(DecodedUrl=@a1)/AddSubFolderUsingPath(DecodedUrl=@a2)?@a1=%27%2Fsites%2FSite1%27&@a2=%27folder2%27'); - assert.strictEqual(postStubs.getCall(1).args[0].url, 'https://contoso.sharepoint.com/sites/Site1/_api/web/GetFolderByServerRelativePath(DecodedUrl=@a1)/AddSubFolderUsingPath(DecodedUrl=@a2)?@a1=%27%2Fsites%2FSite1%2Ffolder2%27&@a2=%27folder3%27'); + assert.strictEqual(postStubs.getCall(0).args[0].url, 'https://contoso.sharepoint.com/sites/Site1/_api/web/GetFolderByServerRelativePath(decodedUrl=@a1)/AddSubFolderUsingPath(decodedUrl=@a2)?@a1=%27%2Fsites%2FSite1%27&@a2=%27folder2%27'); + assert.strictEqual(postStubs.getCall(1).args[0].url, 'https://contoso.sharepoint.com/sites/Site1/_api/web/GetFolderByServerRelativePath(decodedUrl=@a1)/AddSubFolderUsingPath(decodedUrl=@a2)?@a1=%27%2Fsites%2FSite1%2Ffolder2%27&@a2=%27folder3%27'); }); it('should handle end slashes in the command options for webUrl and for folder', async () => { @@ -644,8 +644,8 @@ describe('utils/spo', () => { stubGetResponses(JSON.stringify({ "odata.error": { "code": "-2130575338, Microsoft.SharePoint.SPException", "message": { "lang": "en-US", "value": "Error: Not found." } } })); await spo.ensureFolder("https://contoso.sharepoint.com/sites/Site1/", "/folder2/folder3/", logger, true); - assert.strictEqual(postStubs.getCall(0).args[0].url, 'https://contoso.sharepoint.com/sites/Site1/_api/web/GetFolderByServerRelativePath(DecodedUrl=@a1)/AddSubFolderUsingPath(DecodedUrl=@a2)?@a1=%27%2Fsites%2FSite1%27&@a2=%27folder2%27'); - assert.strictEqual(postStubs.getCall(1).args[0].url, 'https://contoso.sharepoint.com/sites/Site1/_api/web/GetFolderByServerRelativePath(DecodedUrl=@a1)/AddSubFolderUsingPath(DecodedUrl=@a2)?@a1=%27%2Fsites%2FSite1%2Ffolder2%27&@a2=%27folder3%27'); + assert.strictEqual(postStubs.getCall(0).args[0].url, 'https://contoso.sharepoint.com/sites/Site1/_api/web/GetFolderByServerRelativePath(decodedUrl=@a1)/AddSubFolderUsingPath(decodedUrl=@a2)?@a1=%27%2Fsites%2FSite1%27&@a2=%27folder2%27'); + assert.strictEqual(postStubs.getCall(1).args[0].url, 'https://contoso.sharepoint.com/sites/Site1/_api/web/GetFolderByServerRelativePath(decodedUrl=@a1)/AddSubFolderUsingPath(decodedUrl=@a2)?@a1=%27%2Fsites%2FSite1%2Ffolder2%27&@a2=%27folder3%27'); }); it('should have the correct url when folder option has uppercase letters when calling AddSubFolderUsingPath', async () => { @@ -653,8 +653,8 @@ describe('utils/spo', () => { stubGetResponses(JSON.stringify({ "odata.error": { "code": "-2130575338, Microsoft.SharePoint.SPException", "message": { "lang": "en-US", "value": "Error: Not found." } } })); await spo.ensureFolder("https://contoso.sharepoint.com/sites/site1/", "PnP1/Folder2/", logger, true); - assert.strictEqual(postStubs.getCall(0).args[0].url, 'https://contoso.sharepoint.com/sites/site1/_api/web/GetFolderByServerRelativePath(DecodedUrl=@a1)/AddSubFolderUsingPath(DecodedUrl=@a2)?@a1=%27%2Fsites%2Fsite1%27&@a2=%27PnP1%27'); - assert.strictEqual(postStubs.getCall(1).args[0].url, 'https://contoso.sharepoint.com/sites/site1/_api/web/GetFolderByServerRelativePath(DecodedUrl=@a1)/AddSubFolderUsingPath(DecodedUrl=@a2)?@a1=%27%2Fsites%2Fsite1%2FPnP1%27&@a2=%27Folder2%27'); + assert.strictEqual(postStubs.getCall(0).args[0].url, 'https://contoso.sharepoint.com/sites/site1/_api/web/GetFolderByServerRelativePath(decodedUrl=@a1)/AddSubFolderUsingPath(decodedUrl=@a2)?@a1=%27%2Fsites%2Fsite1%27&@a2=%27PnP1%27'); + assert.strictEqual(postStubs.getCall(1).args[0].url, 'https://contoso.sharepoint.com/sites/site1/_api/web/GetFolderByServerRelativePath(decodedUrl=@a1)/AddSubFolderUsingPath(decodedUrl=@a2)?@a1=%27%2Fsites%2Fsite1%2FPnP1%27&@a2=%27Folder2%27'); }); it('should call GetFolderByServerRelativeUrl with the correct url OData values', async () => { @@ -662,8 +662,8 @@ describe('utils/spo', () => { const getStubs: sinon.SinonStub = stubGetResponses(); await spo.ensureFolder("https://contoso.sharepoint.com/sites/Site1", "/folder2/folder3", logger, true); - assert.strictEqual(getStubs.getCall(0).args[0].url, 'https://contoso.sharepoint.com/sites/Site1/_api/web/GetFolderByServerRelativePath(DecodedUrl=\'%2Fsites%2FSite1%2Ffolder2\')'); - assert.strictEqual(getStubs.getCall(1).args[0].url, 'https://contoso.sharepoint.com/sites/Site1/_api/web/GetFolderByServerRelativePath(DecodedUrl=\'%2Fsites%2FSite1%2Ffolder2%2Ffolder3\')'); + assert.strictEqual(getStubs.getCall(0).args[0].url, 'https://contoso.sharepoint.com/sites/Site1/_api/web/GetFolderByServerRelativePath(decodedUrl=\'%2Fsites%2FSite1%2Ffolder2\')'); + assert.strictEqual(getStubs.getCall(1).args[0].url, 'https://contoso.sharepoint.com/sites/Site1/_api/web/GetFolderByServerRelativePath(decodedUrl=\'%2Fsites%2FSite1%2Ffolder2%2Ffolder3\')'); }); //#region Custom Action Mock Responses @@ -961,7 +961,7 @@ describe('utils/spo', () => { assert.deepStrictEqual(postStub.lastCall.args[0].data, { menuState: topNavigation }); }); - it(`retrieves spo group by name sucessfully`, async () => { + it(`retrieves spo group by name successfully`, async () => { const groupResponse = { Id: 11, IsHiddenInUI: false, @@ -989,7 +989,7 @@ describe('utils/spo', () => { assert.deepEqual(group, groupResponse); }); - it(`retrieves roledefinition by name sucessfully`, async () => { + it(`retrieves roledefinition by name successfully`, async () => { const roledefinitionResponse: RoleDefinition = { BasePermissions: { High: 176, @@ -1927,7 +1927,7 @@ describe('utils/spo', () => { }); - it(`retrieves spo group by name sucessfully`, async () => { + it(`retrieves spo group by name successfully`, async () => { const groupResponse = { Id: 11, IsHiddenInUI: false, @@ -1955,7 +1955,7 @@ describe('utils/spo', () => { assert.deepEqual(group, groupResponse); }); - it(`retrieves spo user by email sucessfully`, async () => { + it(`retrieves spo user by email successfully`, async () => { const userResponse = { Id: 11, IsHiddenInUI: false, @@ -2131,7 +2131,7 @@ describe('utils/spo', () => { }; sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `${webUrl}/_api/web/GetFileByServerRelativePath(DecodedUrl=@f)?$expand=ListItemAllFields&@f='%2Fsites%2Fsales%2FDocuments%2FTest1.docx'`) { + if (opts.url === `${webUrl}/_api/web/GetFileByServerRelativePath(decodedUrl=@f)?$expand=ListItemAllFields&@f='%2Fsites%2Fsales%2FDocuments%2FTest1.docx'`) { return fileResponse; } @@ -2714,7 +2714,7 @@ describe('utils/spo', () => { assert.strictEqual(result, primaryAdminLoginName); }); - it(`retrieves a file with its properties sucessfully`, async () => { + it(`retrieves a file by id with its properties successfully`, async () => { const id = 'b2307a39-e878-458b-bc90-03bc578531d6'; const fileResponse = { ListItemAllFields: { @@ -2766,8 +2766,103 @@ describe('utils/spo', () => { throw 'Invalid request'; }); - const group = await spo.getFileById(webUrl, id, logger, true); - assert.deepEqual(group, fileResponse); + const file = await spo.getFileById(webUrl, id, logger, true); + assert.deepEqual(file, fileResponse); + }); + + it(`retrieves a file by url with its properties successfully`, async () => { + const serverRelativeUrl = '/sites/sales/Documents/Test1.docx'; + const fileResponse = { + ListItemAllFields: { + FileSystemObjectType: 0, + Id: 4, + ServerRedirectedEmbedUri: 'https://contoso.sharepoint.com/sites/sales/_layouts/15/WopiFrame.aspx?sourcedoc={b2307a39-e878-458b-bc90-03bc578531d6}&action=interactivepreview', + ServerRedirectedEmbedUrl: 'https://contoso.sharepoint.com/sites/sales/_layouts/15/WopiFrame.aspx?sourcedoc={b2307a39-e878-458b-bc90-03bc578531d6}&action=interactivepreview', + ContentTypeId: '0x0101008E462E3ACE8DB844B3BEBF9473311889', + ComplianceAssetId: null, + Title: null, + ID: 4, + Created: '2018-02-05T09:42:36', + AuthorId: 1, + Modified: '2018-02-05T09:44:03', + EditorId: 1, + 'OData__CopySource': null, + CheckoutUserId: null, + 'OData__UIVersionString': '3.0', + GUID: '2054f49e-0f76-46d4-ac55-50e1c057941c' + }, + CheckInComment: '', + CheckOutType: 2, + ContentTag: '{F09C4EFE-B8C0-4E89-A166-03418661B89B},9,12', + CustomizedPageStatus: 0, + ETag: '\'{F09C4EFE-B8C0-4E89-A166-03418661B89B},9\'', + Exists: true, + IrmEnabled: false, + Length: '331673', + Level: 1, + LinkingUri: 'https://contoso.sharepoint.com/sites/sales/Documents/Test1.docx?d=wf09c4efeb8c04e89a16603418661b89b', + LinkingUrl: 'https://contoso.sharepoint.com/sites/sales/Documents/Test1.docx?d=wf09c4efeb8c04e89a16603418661b89b', + MajorVersion: 3, + MinorVersion: 0, + Name: 'Opendag maart 2018.docx', + ServerRelativeUrl: '/sites/sales/Documents/Test1.docx', + TimeCreated: '2018-02-05T08:42:36Z', + TimeLastModified: '2018-02-05T08:44:03Z', + Title: '', + UIVersion: 1536, + UIVersionLabel: '3.0', + UniqueId: 'b2307a39-e878-458b-bc90-03bc578531d6' + }; + + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/sites/sales/_api/web/GetFileByServerRelativePath(decodedUrl='${formatting.encodeQueryParameter(serverRelativeUrl)}')`) { + return fileResponse; + } + + throw 'Invalid request'; + }); + + const file = await spo.getFileByUrl(webUrl, serverRelativeUrl, logger, true); + assert.deepEqual(file, fileResponse); + }); + + it(`retrieves file role assignments`, async () => { + const serverRelativeUrl = '/sites/sales/Documents/Test1.docx'; + const roleAssignmentsResponse = { + value: [ + { + Member: { + Id: 3, + LoginName: 'i:0#.f|membership|user@contoso.com' + }, + RoleDefinitionBindings: [ + { + BasePermissions: { + High: "2147483647", + Low: "4294967295" + }, + Description: "Has full control.", + Hidden: false, + Id: 1073741829, + Name: "Full Control", + Order: 1, + RoleTypeKind: 5 + } + ] + } + ] + }; + + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/sites/sales/_api/web/GetFileByServerRelativePath(decodedUrl='${formatting.encodeQueryParameter(serverRelativeUrl)}')/ListItemAllFields/RoleAssignments?$expand=Member,RoleDefinitionBindings`) { + return roleAssignmentsResponse; + } + + throw 'Invalid request'; + }); + + const roleAssignments = await spo.getFileRoleAssignments(webUrl, serverRelativeUrl, logger, true); + assert.deepEqual(roleAssignments, roleAssignmentsResponse.value); }); it('correctly outputs result when calling createFileCopyJob', async () => { diff --git a/src/utils/spo.ts b/src/utils/spo.ts index 98dd27bd9fe..0bb0d0700fc 100644 --- a/src/utils/spo.ts +++ b/src/utils/spo.ts @@ -514,7 +514,7 @@ export const spo = { const folderServerRelativeUrl = urlUtil.getServerRelativePath(webFullUrl, nextFolder); const requestOptions: CliRequestOptions = { - url: `${webFullUrl}/_api/web/GetFolderByServerRelativePath(DecodedUrl='${formatting.encodeQueryParameter(folderServerRelativeUrl)}')`, + url: `${webFullUrl}/_api/web/GetFolderByServerRelativePath(decodedUrl='${formatting.encodeQueryParameter(folderServerRelativeUrl)}')`, headers: { 'accept': 'application/json;odata=nometadata' } @@ -528,7 +528,7 @@ export const spo = { catch { const prevFolderServerRelativeUrl: string = urlUtil.getServerRelativePath(webFullUrl, prevFolder); const requestOptions: CliRequestOptions = { - url: `${webFullUrl}/_api/web/GetFolderByServerRelativePath(DecodedUrl=@a1)/AddSubFolderUsingPath(DecodedUrl=@a2)?@a1=%27${formatting.encodeQueryParameter(prevFolderServerRelativeUrl)}%27&@a2=%27${formatting.encodeQueryParameter(folders[folderIndex])}%27`, + url: `${webFullUrl}/_api/web/GetFolderByServerRelativePath(decodedUrl=@a1)/AddSubFolderUsingPath(decodedUrl=@a2)?@a1=%27${formatting.encodeQueryParameter(prevFolderServerRelativeUrl)}%27&@a2=%27${formatting.encodeQueryParameter(folders[folderIndex])}%27`, headers: { 'accept': 'application/json;odata=nometadata' }, @@ -1686,7 +1686,7 @@ export const spo = { } const serverRelativePath = urlUtil.getServerRelativePath(absoluteListUrl, url); - const requestUrl = `${absoluteListUrl}/_api/web/GetFileByServerRelativePath(DecodedUrl=@f)?$expand=ListItemAllFields&@f='${formatting.encodeQueryParameter(serverRelativePath)}'`; + const requestUrl = `${absoluteListUrl}/_api/web/GetFileByServerRelativePath(decodedUrl=@f)?$expand=ListItemAllFields&@f='${formatting.encodeQueryParameter(serverRelativePath)}'`; const requestOptions: CliRequestOptions = { url: requestUrl, @@ -2178,6 +2178,35 @@ export const spo = { return file; }, + /** + * Retrieves the file by url. + * Returns a FileProperties object + * @param webUrl Web url + * @param url the url of the file + * @param logger the Logger object + * @param verbose set for verbose logging + */ + async getFileByUrl(webUrl: string, url: string, logger?: Logger, verbose?: boolean): Promise { + if (verbose && logger) { + await logger.logToStderr(`Retrieving the file with url ${url}`); + } + + const fileServerRelativeUrl: string = urlUtil.getServerRelativePath(webUrl, url); + const requestUrl = `${webUrl}/_api/web/GetFileByServerRelativePath(decodedUrl='${formatting.encodeQueryParameter(fileServerRelativeUrl)}')`; + + const requestOptions: CliRequestOptions = { + url: requestUrl, + headers: { + accept: 'application/json;odata=nometadata' + }, + responseType: 'json' + }; + + const file: FileProperties = await request.get(requestOptions); + + return file; + }, + /** * Create a SharePoint copy job to copy a file to another location. * @param webUrl Absolute web URL where the source file is located. @@ -2440,5 +2469,34 @@ export const spo = { } } return results; + }, + + /** + * Retrieves the role assignments for a file. + * Returns an array of role assignments + * @param webUrl The web url + * @param url the url of the file + * @param logger The logger object + * @param verbose If in verbose mode + * @returns An array of role assignments for the file + */ + async getFileRoleAssignments(webUrl: string, url: string, logger?: Logger, verbose?: boolean): Promise { + if (verbose && logger) { + await logger.logToStderr(`Retrieving the role assignments for the file ${url}`); + } + + const fileServerRelativeUrl: string = urlUtil.getServerRelativePath(webUrl, url); + const requestOptions: CliRequestOptions = { + url: `${webUrl}/_api/web/GetFileByServerRelativePath(decodedUrl='${formatting.encodeQueryParameter(fileServerRelativeUrl)}')/ListItemAllFields/RoleAssignments?$expand=Member,RoleDefinitionBindings`, + headers: { accept: 'application/json;odata=nometadata' }, + responseType: 'json' + }; + + const response = await request.get<{ value: any[] }>(requestOptions); + response.value.forEach(r => { + r.RoleDefinitionBindings = formatting.setFriendlyPermissions(r.RoleDefinitionBindings); + }); + + return response.value; } }; \ No newline at end of file