-
Notifications
You must be signed in to change notification settings - Fork 330
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds command 'entra roledefinition set'. Closes #6467
- Loading branch information
1 parent
81cfb8c
commit 60298b1
Showing
5 changed files
with
326 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import Global from '/docs/cmd/_global.mdx'; | ||
|
||
# entra roledefinition set | ||
|
||
Updates a custom Microsoft Entra ID role definition | ||
|
||
## Usage | ||
|
||
```sh | ||
m365 entra roledefinition set [options] | ||
``` | ||
|
||
## Options | ||
|
||
```md definition-list | ||
`-i, --id [id]` | ||
: The id of the role definition to be updated. Specify either `id` or `displayName`, but not both. | ||
|
||
`-n, --displayName [displayName]` | ||
: The display name of the role definition to be updated. Specify either `id` or `displayName`, but not both. | ||
|
||
`--newDisplayName [newDisplayName]` | ||
: Updated display name for the role definition. | ||
|
||
`-d, --description [description]` | ||
: Updated description for the role definition. | ||
|
||
`-e, --enabled [enabled]` | ||
: Indicates if the role is enabled for the assignment. | ||
|
||
`a-, --allowedResourceActions [allowedResourceActions]` | ||
: Updated comma-separated list of resource actions allowed for the role. | ||
|
||
`-v, --version [version]` | ||
: Updated version of the role definition. | ||
``` | ||
|
||
<Global /> | ||
|
||
## Examples | ||
|
||
Update a custom Microsoft Entra ID role specified by the id | ||
|
||
```sh | ||
m365 entra roledefinition set --id fadbc488-151d-4431-9143-6abbffae759f --newDisplayName 'Application Remover' --description 'Allows to remove any Entra ID application' --allowedResourceActions 'microsoft.directory/applications/delete' | ||
``` | ||
|
||
Update a custom Microsoft Entra ID role specified by the display name | ||
|
||
```sh | ||
m365 entra roledefinition set --displayName 'Application Remover' --version '1.0' --enabled true --allowedResourceActions 'microsoft.directory/applications/delete,microsoft.directory/applications/owners/update' | ||
``` | ||
|
||
## Response | ||
|
||
The command won't return a response on success | ||
|
||
## More information | ||
|
||
- https://learn.microsoft.com/graph/api/rbacapplication-post-roledefinitions |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
156 changes: 156 additions & 0 deletions
156
src/m365/entra/commands/roledefinition/roledefinition-set.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import assert from 'assert'; | ||
import sinon from 'sinon'; | ||
import auth from '../../../../Auth.js'; | ||
import commands from '../../commands.js'; | ||
import request from '../../../../request.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 command from './roledefinition-set.js'; | ||
import { sinonUtil } from '../../../../utils/sinonUtil.js'; | ||
import { CommandError } from '../../../../Command.js'; | ||
import { z } from 'zod'; | ||
import { CommandInfo } from '../../../../cli/CommandInfo.js'; | ||
import { cli } from '../../../../cli/cli.js'; | ||
import { roleDefinition } from '../../../../utils/roleDefinition.js'; | ||
|
||
describe(commands.ROLEDEFINITION_SET, () => { | ||
const roleId = 'abcd1234-de71-4623-b4af-96380a352509'; | ||
const roleDisplayName = 'Custom Role'; | ||
|
||
let log: string[]; | ||
let logger: Logger; | ||
let commandInfo: CommandInfo; | ||
let commandOptionsSchema: z.ZodTypeAny; | ||
|
||
before(() => { | ||
sinon.stub(auth, 'restoreAuth').resolves(); | ||
sinon.stub(telemetry, 'trackEvent').returns(); | ||
sinon.stub(pid, 'getProcessName').returns(''); | ||
sinon.stub(session, 'getId').returns(''); | ||
auth.connection.active = true; | ||
commandInfo = cli.getCommandInfo(command); | ||
commandOptionsSchema = commandInfo.command.getSchemaToParse()!; | ||
}); | ||
|
||
beforeEach(() => { | ||
log = []; | ||
logger = { | ||
log: async (msg: string) => { | ||
log.push(msg); | ||
}, | ||
logRaw: async (msg: string) => { | ||
log.push(msg); | ||
}, | ||
logToStderr: async (msg: string) => { | ||
log.push(msg); | ||
} | ||
}; | ||
}); | ||
|
||
afterEach(() => { | ||
sinonUtil.restore([ | ||
request.patch | ||
]); | ||
}); | ||
|
||
after(() => { | ||
sinon.restore(); | ||
auth.connection.active = false; | ||
}); | ||
|
||
it('has correct name', () => { | ||
assert.strictEqual(command.name, commands.ROLEDEFINITION_SET); | ||
}); | ||
|
||
it('has a description', () => { | ||
assert.notStrictEqual(command.description, null); | ||
}); | ||
|
||
it('fails validation if id is not a valid GUID', () => { | ||
const actual = commandOptionsSchema.safeParse({ | ||
id: 'foo' | ||
}); | ||
assert.notStrictEqual(actual.success, true); | ||
}); | ||
|
||
it('fails validation if both id and displayName are provided', () => { | ||
const actual = commandOptionsSchema.safeParse({ | ||
id: roleId, | ||
displayName: roleDisplayName | ||
}); | ||
assert.notStrictEqual(actual.success, true); | ||
}); | ||
|
||
it('fails validation if neither id nor displayName is provided', () => { | ||
const actual = commandOptionsSchema.safeParse({}); | ||
assert.notStrictEqual(actual.success, true); | ||
}); | ||
|
||
it('fails validation if neither newDisplayName, description, allowedResourceActions, enabled nor version is provided', () => { | ||
const actual = commandOptionsSchema.safeParse({ id: roleId }); | ||
assert.notStrictEqual(actual.success, true); | ||
}); | ||
|
||
it('updates a custom role definition specified by id', async () => { | ||
const patchRequestStub = sinon.stub(request, 'patch').callsFake(async (opts) => { | ||
if (opts.url === `https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions/${roleId}`) { | ||
return; | ||
} | ||
|
||
throw 'Invalid request'; | ||
}); | ||
|
||
const parsedSchema = commandOptionsSchema.safeParse({ id: roleId, allowedResourceActions: "microsoft.directory/groups.unified/create,microsoft.directory/groups.unified/delete" }); | ||
await command.action(logger, { options: parsedSchema.data }); | ||
assert(patchRequestStub.called); | ||
}); | ||
|
||
it('updates a custom role definition specified by displayName', async () => { | ||
sinon.stub(roleDefinition, 'getRoleDefinitionByDisplayName').resolves({ id: roleId }); | ||
|
||
const patchRequestStub = sinon.stub(request, 'patch').callsFake(async (opts) => { | ||
if (opts.url === `https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions/${roleId}`) { | ||
return; | ||
} | ||
|
||
throw 'Invalid request'; | ||
}); | ||
|
||
const parsedSchema = commandOptionsSchema.safeParse({ | ||
displayName: roleDisplayName, | ||
newDisplayName: 'Custom Role Test', | ||
description: 'Allows creating and deleting unified groups', | ||
allowedResourceActions: "microsoft.directory/groups.unified/create,microsoft.directory/groups.unified/delete", | ||
enabled: false, | ||
version: "2", | ||
verbose: true | ||
}); | ||
await command.action(logger, { | ||
options: parsedSchema.data | ||
}); | ||
assert(patchRequestStub.called); | ||
}); | ||
|
||
it('correctly handles API OData error', async () => { | ||
sinon.stub(request, 'patch').rejects({ | ||
error: { | ||
'odata.error': { | ||
code: '-1, InvalidOperationException', | ||
message: { | ||
value: 'Invalid request' | ||
} | ||
} | ||
} | ||
}); | ||
|
||
const parsedSchema = commandOptionsSchema.safeParse({ | ||
displayName: 'Custom Role', | ||
allowedResourceActions: "microsoft.directory/groups.unified/create" | ||
}); | ||
await assert.rejects(command.action(logger, { | ||
options: parsedSchema.data | ||
}), new CommandError('Invalid request')); | ||
}); | ||
}); |
104 changes: 104 additions & 0 deletions
104
src/m365/entra/commands/roledefinition/roledefinition-set.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { z } from 'zod'; | ||
import { globalOptionsZod } from '../../../../Command.js'; | ||
import { zod } from '../../../../utils/zod.js'; | ||
import GraphCommand from '../../../base/GraphCommand.js'; | ||
import commands from '../../commands.js'; | ||
import { Logger } from '../../../../cli/Logger.js'; | ||
import request, { CliRequestOptions } from '../../../../request.js'; | ||
import { roleDefinition } from '../../../../utils/roleDefinition.js'; | ||
import { validation } from '../../../../utils/validation.js'; | ||
import { UnifiedRoleDefinition } from '@microsoft/microsoft-graph-types'; | ||
|
||
const options = globalOptionsZod | ||
.extend({ | ||
id: zod.alias('i', z.string().optional()), | ||
displayName: zod.alias('n', z.string().optional()), | ||
newDisplayName: z.string().optional(), | ||
allowedResourceActions: zod.alias('a', z.string().optional()), | ||
description: zod.alias('d', z.string().optional()), | ||
enabled: zod.alias('e', z.boolean().optional()), | ||
version: zod.alias('v', z.string().optional()) | ||
}) | ||
.strict(); | ||
|
||
declare type Options = z.infer<typeof options>; | ||
|
||
interface CommandArgs { | ||
options: Options; | ||
} | ||
|
||
class EntraRoleDefinitionSetCommand extends GraphCommand { | ||
public get name(): string { | ||
return commands.ROLEDEFINITION_SET; | ||
} | ||
|
||
public get description(): string { | ||
return 'Updates a custom Microsoft Entra ID role definition'; | ||
} | ||
|
||
public get schema(): z.ZodTypeAny | undefined { | ||
return options; | ||
} | ||
|
||
public getRefinedSchema(schema: typeof options): z.ZodEffects<any> | undefined { | ||
return schema | ||
.refine(options => !options.id !== !options.displayName, { | ||
message: 'Specify either id or displayName, but not both' | ||
}) | ||
.refine(options => options.id || options.displayName, { | ||
message: 'Specify either id or displayName' | ||
}) | ||
.refine(options => (!options.id && !options.displayName) || options.displayName || (options.id && validation.isValidGuid(options.id)), options => ({ | ||
message: `The '${options.id}' must be a valid GUID`, | ||
path: ['id'] | ||
})) | ||
.refine(options => Object.values([options.newDisplayName, options.description, options.allowedResourceActions, options.enabled, options.version]).filter(v => typeof v !== 'undefined').length > 0, { | ||
message: 'Provide value for at least one of the following parameters: newDisplayName, description, allowedResourceActions, enabled or version' | ||
}); | ||
} | ||
|
||
public async commandAction(logger: Logger, args: CommandArgs): Promise<void> { | ||
try { | ||
let roleDefinitionId = args.options.id; | ||
|
||
if (args.options.displayName) { | ||
roleDefinitionId = (await roleDefinition.getRoleDefinitionByDisplayName(args.options.displayName, 'id')).id; | ||
} | ||
|
||
if (args.options.verbose) { | ||
await logger.logToStderr(`Updating custom role definition with ID ${roleDefinitionId}...`); | ||
} | ||
|
||
const data: UnifiedRoleDefinition = { | ||
displayName: args.options.newDisplayName, | ||
description: args.options.description, | ||
isEnabled: args.options.enabled, | ||
version: args.options.version | ||
}; | ||
|
||
if (args.options.allowedResourceActions) { | ||
data['rolePermissions'] = [ | ||
{ | ||
allowedResourceActions: args.options.allowedResourceActions.split(',') | ||
} | ||
]; | ||
} | ||
|
||
const requestOptions: CliRequestOptions = { | ||
url: `${this.resource}/v1.0/roleManagement/directory/roleDefinitions/${roleDefinitionId}`, | ||
headers: { | ||
accept: 'application/json;odata.metadata=none' | ||
}, | ||
data: data, | ||
responseType: 'json' | ||
}; | ||
|
||
await request.patch(requestOptions); | ||
} | ||
catch (err: any) { | ||
this.handleRejectedODataJsonPromise(err); | ||
} | ||
} | ||
} | ||
|
||
export default new EntraRoleDefinitionSetCommand(); |