-
Notifications
You must be signed in to change notification settings - Fork 328
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New command: entra roledefinition remove
- Loading branch information
Showing
8 changed files
with
466 additions
and
7 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
52 changes: 52 additions & 0 deletions
52
docs/docs/cmd/entra/roledefinition/roledefinition-remove.mdx
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,52 @@ | ||
import Global from '/docs/cmd/_global.mdx'; | ||
|
||
# entra roledefinition remove | ||
|
||
Removes a custom Microsoft Entra role definition | ||
|
||
## Usage | ||
|
||
```sh | ||
m365 entra roledefinition remove [options] | ||
``` | ||
|
||
## Options | ||
|
||
```md definition-list | ||
`-i, --id [id]` | ||
: The id of the role definition. Specify either `id` or `displayName`, but not both. | ||
|
||
`-n, --displayName [displayName]` | ||
: The name of the role definition. Specify either `id` or `displayName`, but not both. | ||
|
||
`-f, --force` | ||
: Don't prompt for confirmation. | ||
``` | ||
|
||
<Global /> | ||
|
||
## Remarks | ||
|
||
:::info | ||
|
||
When the role definition is removed, all the associated role assignments are deleted. | ||
|
||
::: | ||
|
||
## Examples | ||
|
||
Remove a role definition specified by id without prompting | ||
|
||
```sh | ||
m365 entra roledefinition remove --id 0bed8b86-5026-4a93-ac7d-56750cc099f1 --force | ||
``` | ||
|
||
Remove a role definition specified by name and prompt for confirmation | ||
|
||
```sh | ||
m365 entra roledefinition remove --displayName 'Custom Role' | ||
``` | ||
|
||
## Response | ||
|
||
The command won't return a response on success |
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
166 changes: 166 additions & 0 deletions
166
src/m365/entra/commands/roledefinition/roledefinition-remove.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,166 @@ | ||
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-remove.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_REMOVE, () => { | ||
const roleId = 'abcd1234-de71-4623-b4af-96380a352509'; | ||
const roleDisplayName = 'Bitlocker Keys Reader'; | ||
|
||
let log: string[]; | ||
let logger: Logger; | ||
let promptIssued: boolean; | ||
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); | ||
} | ||
}; | ||
sinon.stub(cli, 'promptForConfirmation').callsFake(() => { | ||
promptIssued = true; | ||
return Promise.resolve(false); | ||
}); | ||
|
||
promptIssued = false; | ||
}); | ||
|
||
afterEach(() => { | ||
sinonUtil.restore([ | ||
request.get, | ||
request.delete, | ||
cli.promptForConfirmation | ||
]); | ||
}); | ||
|
||
after(() => { | ||
sinon.restore(); | ||
auth.connection.active = false; | ||
}); | ||
|
||
it('has correct name', () => { | ||
assert.strictEqual(command.name, commands.ROLEDEFINITION_REMOVE); | ||
}); | ||
|
||
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('prompts before removing the role definition when confirm option not passed', async () => { | ||
await command.action(logger, { options: { id: roleId } }); | ||
|
||
assert(promptIssued); | ||
}); | ||
|
||
it('aborts removing the role definition when prompt not confirmed', async () => { | ||
const deleteSpy = sinon.stub(request, 'delete').resolves(); | ||
|
||
await command.action(logger, { options: { id: roleId } }); | ||
assert(deleteSpy.notCalled); | ||
}); | ||
|
||
it('removes the role definition specified by id without prompting for confirmation', async () => { | ||
const deleteRequestStub = sinon.stub(request, 'delete').callsFake(async (opts) => { | ||
if (opts.url === `https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions/${roleId}`) { | ||
return; | ||
} | ||
|
||
throw 'Invalid request'; | ||
}); | ||
|
||
await command.action(logger, { options: { id: roleId, force: true, verbose: true } }); | ||
assert(deleteRequestStub.called); | ||
}); | ||
|
||
it('removes the role definition specified by displayName while prompting for confirmation', async () => { | ||
sinon.stub(roleDefinition, 'getRoleDefinitionByDisplayName').resolves({ id: roleId }); | ||
|
||
const deleteRequestStub = sinon.stub(request, 'delete').callsFake(async (opts) => { | ||
if (opts.url === `https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions/${roleId}`) { | ||
return; | ||
} | ||
|
||
throw 'Invalid request'; | ||
}); | ||
|
||
sinonUtil.restore(cli.promptForConfirmation); | ||
sinon.stub(cli, 'promptForConfirmation').resolves(true); | ||
|
||
await command.action(logger, { options: { displayName: roleDisplayName } }); | ||
assert(deleteRequestStub.called); | ||
}); | ||
|
||
it('handles error when role definition specified by id was not found', async () => { | ||
sinon.stub(request, 'delete').callsFake(async (opts) => { | ||
if (opts.url === `https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions/${roleId}`) { | ||
throw { | ||
error: | ||
{ | ||
code: 'Request_ResourceNotFound', | ||
message: `Resource '${roleId}' does not exist or one of its queried reference-property objects are not present.` | ||
} | ||
}; | ||
} | ||
throw `Invalid request`; | ||
}); | ||
|
||
sinonUtil.restore(cli.promptForConfirmation); | ||
sinon.stub(cli, 'promptForConfirmation').resolves(true); | ||
|
||
await assert.rejects( | ||
command.action(logger, { options: { id: roleId } }), | ||
new CommandError(`Resource '${roleId}' does not exist or one of its queried reference-property objects are not present.`) | ||
); | ||
}); | ||
}); |
93 changes: 93 additions & 0 deletions
93
src/m365/entra/commands/roledefinition/roledefinition-remove.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,93 @@ | ||
import { Logger } from '../../../../cli/Logger.js'; | ||
import GraphCommand from '../../../base/GraphCommand.js'; | ||
import commands from '../../commands.js'; | ||
import { z } from 'zod'; | ||
import { globalOptionsZod } from '../../../../Command.js'; | ||
import { zod } from '../../../../utils/zod.js'; | ||
import { roleDefinition } from '../../../../utils/roleDefinition.js'; | ||
import { validation } from '../../../../utils/validation.js'; | ||
import request, { CliRequestOptions } from '../../../../request.js'; | ||
import { cli } from '../../../../cli/cli.js'; | ||
|
||
const options = globalOptionsZod | ||
.extend({ | ||
id: zod.alias('i', z.string().optional()), | ||
displayName: zod.alias('n', z.string().optional()), | ||
force: zod.alias('f', z.boolean().optional()) | ||
}) | ||
.strict(); | ||
|
||
declare type Options = z.infer<typeof options>; | ||
|
||
interface CommandArgs { | ||
options: Options; | ||
} | ||
|
||
class EntraRoleDefinitionRemoveCommand extends GraphCommand { | ||
public get name(): string { | ||
return commands.ROLEDEFINITION_REMOVE; | ||
} | ||
|
||
public get description(): string { | ||
return 'Removes a specific 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'] | ||
})); | ||
} | ||
|
||
public async commandAction(logger: Logger, args: CommandArgs): Promise<void> { | ||
const removeRoleDefinition = async (): 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(`Removing role definition with ID ${roleDefinitionId}...`); | ||
} | ||
|
||
const requestOptions: CliRequestOptions = { | ||
url: `${this.resource}/v1.0/roleManagement/directory/roleDefinitions/${roleDefinitionId}`, | ||
headers: { | ||
accept: 'application/json;odata.metadata=none' | ||
} | ||
}; | ||
|
||
await request.delete(requestOptions); | ||
} | ||
catch (err: any) { | ||
this.handleRejectedODataJsonPromise(err); | ||
} | ||
}; | ||
|
||
if (args.options.force) { | ||
await removeRoleDefinition(); | ||
} | ||
else { | ||
const result = await cli.promptForConfirmation({ message: `Are you sure you want to remove role definition '${args.options.id || args.options.displayName}'?` }); | ||
|
||
if (result) { | ||
await removeRoleDefinition(); | ||
} | ||
} | ||
} | ||
} | ||
|
||
export default new EntraRoleDefinitionRemoveCommand(); |
Oops, something went wrong.