Skip to content

Commit

Permalink
New command: entra roledefinition add
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinM85 committed Dec 15, 2024
1 parent 9e186e5 commit a4d9145
Show file tree
Hide file tree
Showing 5 changed files with 384 additions and 0 deletions.
127 changes: 127 additions & 0 deletions docs/docs/cmd/entra/roledefinition/roledefinition-add.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import Global from '/docs/cmd/_global.mdx';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# entra roledefinition add

Creates a custom Microsoft Entra ID role definition

## Usage

```sh
m365 entra roledefinition add [options]
```

## Options

```md definition-list
`-n, --displayName <displayName>`
: The display name for the role definition.

`--allowedResourceActions <allowedResourceActions>`
: Comma-separated list of resource actions allowed for the role.

`--description [description]`
: The description for the role definition.

`--enabled [enabled]`
: Indicates if the role is enabled for the assignment. If not specified, the role is enabled by default.

`--version [version]`
: The version of the role definition.
```

<Global />

## Examples

Create a custom Microsoft Entra ID role

```sh
m365 entra roledefinition add --displayName 'Application Remover' --description 'Allows to remove any Entra ID application' --allowedResourceActions 'microsoft.directory/applications/delete'
```

Create a custom Microsoft Entra ID role, but disable it for the assignment

```sh
m365 entra roledefinition add --displayName 'Application Remover' --version '1.0' --enabled false --allowedResourceActions 'microsoft.directory/applications/delete,microsoft.directory/applications/owners/update'
```

## Response

<Tabs>
<TabItem value="JSON">

```json
{
"id": "3844129d-f748-4c03-8165-4412ee9b4ceb",
"description": null,
"displayName": "Custom Role",
"isBuiltIn": false,
"isEnabled": true,
"resourceScopes": [
"/"
],
"templateId": "3844129d-f748-4c03-8165-4412ee9b4ceb",
"version": "1",
"rolePermissions": [
{
"allowedResourceActions": [
"microsoft.directory/groups.unified/create",
"microsoft.directory/groups.unified/delete"
],
"condition": null
}
]
}
```

</TabItem>
<TabItem value="Text">

```text
description : null
displayName : Custom Role
id : 3844129d-f748-4c03-8165-4412ee9b4ceb
isBuiltIn : false
isEnabled : true
resourceScopes : ["/"]
rolePermissions: [{"allowedResourceActions":["microsoft.directory/groups.unified/create","microsoft.directory/groups.unified/delete"],"condition":null}]
templateId : 3844129d-f748-4c03-8165-4412ee9b4ceb
version : 1
```

</TabItem>
<TabItem value="CSV">

```csv
id,description,displayName,isBuiltIn,isEnabled,templateId,version
3844129d-f748-4c03-8165-4412ee9b4ceb,,Custom Role,0,1,3844129d-f748-4c03-8165-4412ee9b4ceb,1
```

</TabItem>
<TabItem value="Markdown">

```md
# entra roledefinition add --displayName "Custom Role" --allowedResourceActions "microsoft.directory/groups.unified/create,microsoft.directory/groups.unified/delete" --version 1

Date: 12/15/2024

## Custom Role (3844129d-f748-4c03-8165-4412ee9b4ceb)

Property | Value
---------|-------
id | 3844129d-f748-4c03-8165-4412ee9b4ceb
displayName | Custom Role
isBuiltIn | false
isEnabled | true
templateId | 3844129d-f748-4c03-8165-4412ee9b4ceb
version | 1
```

</TabItem>
</Tabs>

## More information

- https://learn.microsoft.com/graph/api/rbacapplication-post-roledefinitions
5 changes: 5 additions & 0 deletions docs/src/config/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,11 @@ const sidebars: SidebarsConfig = {
},
{
roledefinition: [
{
type: 'doc',
label: 'roledefinition add',
id: 'cmd/entra/roledefinition/roledefinition-add'
},
{
type: 'doc',
label: 'roledefinition get',
Expand Down
1 change: 1 addition & 0 deletions src/m365/entra/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export default {
PIM_ROLE_ASSIGNMENT_ELIGIBILITY_LIST: `${prefix} pim role assignment eligibility list`,
PIM_ROLE_REQUEST_LIST: `${prefix} pim role request list`,
POLICY_LIST: `${prefix} policy list`,
ROLEDEFINITION_ADD: `${prefix} roledefinition add`,
ROLEDEFINITION_LIST: `${prefix} roledefinition list`,
ROLEDEFINITION_GET: `${prefix} roledefinition get`,
ROLEDEFINITION_REMOVE: `${prefix} roledefinition remove`,
Expand Down
178 changes: 178 additions & 0 deletions src/m365/entra/commands/roledefinition/roledefinition-add.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
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-add.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';

describe(commands.ROLEDEFINITION_ADD, () => {
const roleDefinitionResponse = {
"id": "e1ede50a-487c-49b3-a43e-cda270d3341f",
"description": null,
"displayName": "Custom Role",
"isBuiltIn": false,
"isEnabled": true,
"resourceScopes": [
"/"
],
"templateId": "e1ede50a-487c-49b3-a43e-cda270d3341f",
"version": null,
"rolePermissions": [
{
"allowedResourceActions": [
"microsoft.directory/groups.unified/create",
"microsoft.directory/groups.unified/delete"
],
"condition": null
}
]
};

const roleDefinitionWithDeatilsResponse = {
"id": "abcde50a-487c-49b3-a43e-cda270d3341f",
"description": "Allows creating and deleting unified groups",
"displayName": "Custom Role",
"isBuiltIn": false,
"isEnabled": false,
"resourceScopes": [
"/"
],
"templateId": "abcnpm instade50a-487c-49b3-a43e-cda270d3341f",
"version": "1",
"rolePermissions": [
{
"allowedResourceActions": [
"microsoft.directory/groups.unified/create",
"microsoft.directory/groups.unified/delete"
],
"condition": null
}
]
};

let log: string[];
let logger: Logger;
let loggerLogSpy: sinon.SinonSpy;
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);
}
};
loggerLogSpy = sinon.spy(logger, 'log');
});

afterEach(() => {
sinonUtil.restore([
request.post,
cli.promptForConfirmation
]);
});

after(() => {
sinon.restore();
auth.connection.active = false;
});

it('has correct name', () => {
assert.strictEqual(command.name, commands.ROLEDEFINITION_ADD);
});

it('has a description', () => {
assert.notStrictEqual(command.description, null);
});

it('fails validation if displayName is not provided', () => {
const actual = commandOptionsSchema.safeParse({ allowedResourceActions: "microsoft.directory/groups.unified/create"});
assert.notStrictEqual(actual.success, true);
});

it('fails validation if allowedResourceActions is not provided', () => {
const actual = commandOptionsSchema.safeParse({ displayName: "Custom Role" });
assert.notStrictEqual(actual.success, true);
});

it('creates a custom role definition with a specific display name and resource actions', async () => {
sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === 'https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions') {
return roleDefinitionResponse;
}

throw 'Invalid request';
});

await command.action(logger, { options: { displayName: 'Custom Role', allowedResourceActions: "microsoft.directory/groups.unified/create,microsoft.directory/groups.unified/delete" } });
assert(loggerLogSpy.calledOnceWithExactly(roleDefinitionResponse));
});

it('creates a custom role definition with a specific display name, description, version and resource actions', async () => {
sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === 'https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions') {
return roleDefinitionWithDeatilsResponse;
}

throw 'Invalid request';
});

await command.action(logger, {
options: {
displayName: 'Custom Role',
description: 'Allows creating and deleting unified groups',
allowedResourceActions: "microsoft.directory/groups.unified/create,microsoft.directory/groups.unified/delete",
enabled: false,
version: "1",
verbose: true
}
});
assert(loggerLogSpy.calledOnceWithExactly(roleDefinitionWithDeatilsResponse));
});

it('correctly handles API OData error', async () => {
sinon.stub(request, 'post').rejects({
error: {
'odata.error': {
code: '-1, InvalidOperationException',
message: {
value: 'Invalid request'
}
}
}
});

await assert.rejects(command.action(logger, {
options: {
displayName: 'Custom Role',
allowedResourceActions: "microsoft.directory/groups.unified/create"
}
}), new CommandError('Invalid request'));
});
});
Loading

0 comments on commit a4d9145

Please sign in to comment.