azp-cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/extensions.json +3 -0
- package/.vscode/launch.json +25 -0
- package/.vscode/settings.json +25 -0
- package/.vscode/tasks.json +28 -0
- package/README.md +203 -0
- package/dist/auth.d.ts +11 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +52 -0
- package/dist/auth.js.map +1 -0
- package/dist/azure-pim.d.ts +44 -0
- package/dist/azure-pim.d.ts.map +1 -0
- package/dist/azure-pim.js +183 -0
- package/dist/azure-pim.js.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +339 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/ui/App.d.ts +9 -0
- package/dist/ui/App.d.ts.map +1 -0
- package/dist/ui/App.js +85 -0
- package/dist/ui/App.js.map +1 -0
- package/dist/ui/components.d.ts +18 -0
- package/dist/ui/components.d.ts.map +1 -0
- package/dist/ui/components.js +13 -0
- package/dist/ui/components.js.map +1 -0
- package/dist/ui/router.d.ts +22 -0
- package/dist/ui/router.d.ts.map +1 -0
- package/dist/ui/router.js +19 -0
- package/dist/ui/router.js.map +1 -0
- package/dist/ui/screens/ActivateFlow.d.ts +25 -0
- package/dist/ui/screens/ActivateFlow.d.ts.map +1 -0
- package/dist/ui/screens/ActivateFlow.js +195 -0
- package/dist/ui/screens/ActivateFlow.js.map +1 -0
- package/dist/ui/screens/DeactivateFlow.d.ts +22 -0
- package/dist/ui/screens/DeactivateFlow.d.ts.map +1 -0
- package/dist/ui/screens/DeactivateFlow.js +115 -0
- package/dist/ui/screens/DeactivateFlow.js.map +1 -0
- package/dist/ui/useExitConfirmation.d.ts +7 -0
- package/dist/ui/useExitConfirmation.d.ts.map +1 -0
- package/dist/ui/useExitConfirmation.js +17 -0
- package/dist/ui/useExitConfirmation.js.map +1 -0
- package/dist/ui/widgets/CheckboxList.d.ts +14 -0
- package/dist/ui/widgets/CheckboxList.d.ts.map +1 -0
- package/dist/ui/widgets/CheckboxList.js +59 -0
- package/dist/ui/widgets/CheckboxList.js.map +1 -0
- package/dist/ui/widgets/ConfirmPrompt.d.ts +8 -0
- package/dist/ui/widgets/ConfirmPrompt.d.ts.map +1 -0
- package/dist/ui/widgets/ConfirmPrompt.js +19 -0
- package/dist/ui/widgets/ConfirmPrompt.js.map +1 -0
- package/dist/ui/widgets/NumberPrompt.d.ts +10 -0
- package/dist/ui/widgets/NumberPrompt.d.ts.map +1 -0
- package/dist/ui/widgets/NumberPrompt.js +38 -0
- package/dist/ui/widgets/NumberPrompt.js.map +1 -0
- package/dist/ui/widgets/SelectList.d.ts +14 -0
- package/dist/ui/widgets/SelectList.d.ts.map +1 -0
- package/dist/ui/widgets/SelectList.js +34 -0
- package/dist/ui/widgets/SelectList.js.map +1 -0
- package/dist/ui/widgets/TextPrompt.d.ts +10 -0
- package/dist/ui/widgets/TextPrompt.d.ts.map +1 -0
- package/dist/ui/widgets/TextPrompt.js +30 -0
- package/dist/ui/widgets/TextPrompt.js.map +1 -0
- package/dist/ui.d.ts +26 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +183 -0
- package/dist/ui.js.map +1 -0
- package/package.json +47 -0
- package/pnpm-workspace.yaml +2 -0
- package/src/auth.ts +66 -0
- package/src/azure-pim.ts +262 -0
- package/src/cli.ts +401 -0
- package/src/index.ts +65 -0
- package/src/ui.ts +253 -0
- package/tsconfig.json +45 -0
package/src/azure-pim.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { AuthorizationManagementClient } from "@azure/arm-authorization";
|
|
2
|
+
import { SubscriptionClient } from "@azure/arm-resources-subscriptions";
|
|
3
|
+
import { AzureCliCredential } from "@azure/identity";
|
|
4
|
+
import { v4 as uuidv4 } from "uuid";
|
|
5
|
+
import { failSpinner, formatStatus, logBlank, logError, logSuccess, logWarning, startSpinner, succeedSpinner, warnSpinner } from "./ui";
|
|
6
|
+
|
|
7
|
+
export interface AzureSubscription {
|
|
8
|
+
subscriptionId: string;
|
|
9
|
+
displayName: string;
|
|
10
|
+
tenantId: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface EligibleAzureRole {
|
|
14
|
+
id: string;
|
|
15
|
+
roleEligibilityScheduleId: string;
|
|
16
|
+
roleDefinitionId: string;
|
|
17
|
+
roleName: string;
|
|
18
|
+
roleDescription: string;
|
|
19
|
+
scope: string;
|
|
20
|
+
scopeDisplayName: string;
|
|
21
|
+
principalId: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ActiveAzureRole {
|
|
25
|
+
id: string;
|
|
26
|
+
roleDefinitionId: string;
|
|
27
|
+
roleName: string;
|
|
28
|
+
scope: string;
|
|
29
|
+
scopeDisplayName: string;
|
|
30
|
+
principalId: string;
|
|
31
|
+
linkedRoleEligibilityScheduleId: string;
|
|
32
|
+
startDateTime: string;
|
|
33
|
+
endDateTime: string;
|
|
34
|
+
subscriptionId: string;
|
|
35
|
+
subscriptionName: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AzureActivationRequest {
|
|
39
|
+
roleEligibilityScheduleId: string;
|
|
40
|
+
roleDefinitionId: string;
|
|
41
|
+
roleName: string;
|
|
42
|
+
scope: string;
|
|
43
|
+
principalId: string;
|
|
44
|
+
justification: string;
|
|
45
|
+
durationHours: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const fetchSubscriptions = async (credential: AzureCliCredential): Promise<AzureSubscription[]> => {
|
|
49
|
+
startSpinner("Fetching Azure subscriptions...");
|
|
50
|
+
|
|
51
|
+
const subscriptionClient = new SubscriptionClient(credential);
|
|
52
|
+
const subscriptions: AzureSubscription[] = [];
|
|
53
|
+
|
|
54
|
+
for await (const sub of subscriptionClient.subscriptions.list()) {
|
|
55
|
+
subscriptions.push({
|
|
56
|
+
subscriptionId: sub.subscriptionId || "",
|
|
57
|
+
displayName: sub.displayName || "N/A",
|
|
58
|
+
tenantId: sub.tenantId || "",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
succeedSpinner(`Found ${subscriptions.length} subscription(s)`);
|
|
63
|
+
return subscriptions;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const fetchEligibleRolesForSubscription = async (
|
|
67
|
+
credential: AzureCliCredential,
|
|
68
|
+
subscriptionId: string,
|
|
69
|
+
subscriptionName: string,
|
|
70
|
+
principalId: string
|
|
71
|
+
): Promise<EligibleAzureRole[]> => {
|
|
72
|
+
startSpinner(`Fetching eligible roles for "${subscriptionName}"...`);
|
|
73
|
+
|
|
74
|
+
const client = new AuthorizationManagementClient(credential, subscriptionId);
|
|
75
|
+
const scope = `/subscriptions/${subscriptionId}`;
|
|
76
|
+
const eligibleRoles: EligibleAzureRole[] = [];
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const schedules = client.roleEligibilitySchedules.listForScope(scope, {
|
|
80
|
+
filter: `asTarget()`,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
for await (const schedule of schedules) {
|
|
84
|
+
if (schedule.id && schedule.roleDefinitionId) {
|
|
85
|
+
eligibleRoles.push({
|
|
86
|
+
id: schedule.id,
|
|
87
|
+
roleEligibilityScheduleId: schedule.id,
|
|
88
|
+
roleDefinitionId: schedule.roleDefinitionId,
|
|
89
|
+
roleName: schedule.expandedProperties?.roleDefinition?.displayName || "Unknown Role",
|
|
90
|
+
roleDescription: "No description available",
|
|
91
|
+
scope: schedule.scope || scope,
|
|
92
|
+
scopeDisplayName: getScopeDisplayName(schedule.scope || scope),
|
|
93
|
+
principalId: schedule.principalId || principalId,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
succeedSpinner(`Found ${eligibleRoles.length} eligible role(s) for "${subscriptionName}"`);
|
|
99
|
+
return eligibleRoles;
|
|
100
|
+
} catch (error: any) {
|
|
101
|
+
if (error.statusCode === 403 || error.code === "AuthorizationFailed") {
|
|
102
|
+
warnSpinner(`Insufficient permissions for subscription "${subscriptionName}"`);
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
failSpinner(`Failed to fetch eligible roles for "${subscriptionName}"`);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const listActiveAzureRoles = async (
|
|
111
|
+
credential: AzureCliCredential,
|
|
112
|
+
subscriptionId: string,
|
|
113
|
+
subscriptionName: string,
|
|
114
|
+
principalId: string
|
|
115
|
+
): Promise<ActiveAzureRole[]> => {
|
|
116
|
+
startSpinner(`Fetching active roles for "${subscriptionName}"...`);
|
|
117
|
+
|
|
118
|
+
const client = new AuthorizationManagementClient(credential, subscriptionId);
|
|
119
|
+
const scope = `/subscriptions/${subscriptionId}`;
|
|
120
|
+
const activeRoles: ActiveAzureRole[] = [];
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const schedules = client.roleAssignmentSchedules.listForScope(scope, {
|
|
124
|
+
filter: `asTarget()`,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
for await (const schedule of schedules) {
|
|
128
|
+
if (schedule.id && schedule.roleDefinitionId && schedule.assignmentType === "Activated") {
|
|
129
|
+
activeRoles.push({
|
|
130
|
+
id: schedule.id,
|
|
131
|
+
roleDefinitionId: schedule.roleDefinitionId,
|
|
132
|
+
roleName: schedule.expandedProperties?.roleDefinition?.displayName || "Unknown Role",
|
|
133
|
+
scope: schedule.scope || scope,
|
|
134
|
+
scopeDisplayName: getScopeDisplayName(schedule.scope || scope),
|
|
135
|
+
principalId: schedule.principalId || principalId,
|
|
136
|
+
linkedRoleEligibilityScheduleId: schedule.linkedRoleEligibilityScheduleId || "",
|
|
137
|
+
startDateTime: schedule.startDateTime?.toISOString() || "",
|
|
138
|
+
endDateTime: schedule.endDateTime?.toISOString() || "",
|
|
139
|
+
subscriptionId,
|
|
140
|
+
subscriptionName,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
succeedSpinner(`Found ${activeRoles.length} active role(s) for "${subscriptionName}"`);
|
|
146
|
+
return activeRoles;
|
|
147
|
+
} catch (error: any) {
|
|
148
|
+
if (error.statusCode === 403 || error.code === "AuthorizationFailed") {
|
|
149
|
+
warnSpinner(`Insufficient permissions for subscription "${subscriptionName}"`);
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
failSpinner(`Failed to fetch active roles for "${subscriptionName}"`);
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const activateAzureRole = async (credential: AzureCliCredential, request: AzureActivationRequest, subscriptionId: string): Promise<void> => {
|
|
158
|
+
const client = new AuthorizationManagementClient(credential, subscriptionId);
|
|
159
|
+
const requestName = uuidv4();
|
|
160
|
+
const now = new Date();
|
|
161
|
+
const durationISO = `PT${request.durationHours}H`;
|
|
162
|
+
|
|
163
|
+
const linkedScheduleId = request.roleEligibilityScheduleId.includes("/")
|
|
164
|
+
? request.roleEligibilityScheduleId
|
|
165
|
+
: `${request.scope}/providers/Microsoft.Authorization/roleEligibilitySchedules/${request.roleEligibilityScheduleId}`;
|
|
166
|
+
|
|
167
|
+
const requestBody = {
|
|
168
|
+
principalId: request.principalId,
|
|
169
|
+
roleDefinitionId: request.roleDefinitionId,
|
|
170
|
+
requestType: "SelfActivate",
|
|
171
|
+
linkedRoleEligibilityScheduleId: linkedScheduleId,
|
|
172
|
+
scheduleInfo: {
|
|
173
|
+
startDateTime: now,
|
|
174
|
+
expiration: {
|
|
175
|
+
type: "AfterDuration",
|
|
176
|
+
duration: durationISO,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
justification: request.justification,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
startSpinner(`Activating role "${request.roleName}"...`);
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const response = await client.roleAssignmentScheduleRequests.create(request.scope, requestName, requestBody);
|
|
186
|
+
|
|
187
|
+
succeedSpinner(`Activation request submitted for "${request.roleName}"`);
|
|
188
|
+
logBlank();
|
|
189
|
+
|
|
190
|
+
if (response.status) {
|
|
191
|
+
console.log(` Status: ${formatStatus(response.status)}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (response.status === "Approved" || response.status === "Provisioned") {
|
|
195
|
+
logSuccess(`Role "${request.roleName}" has been activated successfully`);
|
|
196
|
+
} else if (response.status === "Denied") {
|
|
197
|
+
logError(`Role activation for "${request.roleName}" has been denied`);
|
|
198
|
+
} else if (response.status === "PendingApproval") {
|
|
199
|
+
logWarning(`Role activation for "${request.roleName}" is pending approval`);
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
failSpinner(`Failed to activate role "${request.roleName}"`);
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const deactivateAzureRole = async (
|
|
208
|
+
credential: AzureCliCredential,
|
|
209
|
+
scope: string,
|
|
210
|
+
roleEligibilityScheduleId: string,
|
|
211
|
+
subscriptionId: string,
|
|
212
|
+
principalId: string,
|
|
213
|
+
roleDefinitionId: string,
|
|
214
|
+
roleName?: string
|
|
215
|
+
): Promise<void> => {
|
|
216
|
+
const client = new AuthorizationManagementClient(credential, subscriptionId);
|
|
217
|
+
const requestName = uuidv4();
|
|
218
|
+
const displayName = roleName || "role";
|
|
219
|
+
|
|
220
|
+
startSpinner(`Deactivating "${displayName}"...`);
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
await client.roleAssignmentScheduleRequests.create(scope, requestName, {
|
|
224
|
+
principalId,
|
|
225
|
+
roleDefinitionId,
|
|
226
|
+
requestType: "SelfDeactivate",
|
|
227
|
+
linkedRoleEligibilityScheduleId: roleEligibilityScheduleId,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
succeedSpinner(`Successfully deactivated "${displayName}"`);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
failSpinner(`Failed to deactivate "${displayName}"`);
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const getScopeDisplayName = (scope: string): string => {
|
|
238
|
+
if (!scope) return "Unknown Scope";
|
|
239
|
+
|
|
240
|
+
const parts = scope.split("/");
|
|
241
|
+
|
|
242
|
+
// Management Group
|
|
243
|
+
if (scope.includes("/managementGroups/")) {
|
|
244
|
+
const mgIndex = parts.indexOf("managementGroups");
|
|
245
|
+
return `Management Group: ${parts[mgIndex + 1]}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Resource Group
|
|
249
|
+
if (scope.includes("/resourceGroups/")) {
|
|
250
|
+
const rgIndex = parts.indexOf("resourceGroups");
|
|
251
|
+
const subIndex = parts.indexOf("subscriptions");
|
|
252
|
+
return `Resource Group: ${parts[rgIndex + 1]} (Subscription: ${parts[subIndex + 1]})`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Subscription Level
|
|
256
|
+
if (scope.includes("/subscriptions/")) {
|
|
257
|
+
const subIndex = parts.indexOf("subscriptions");
|
|
258
|
+
return `Subscription: ${parts[subIndex + 1]}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return scope;
|
|
262
|
+
};
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import inquirer from "inquirer";
|
|
3
|
+
import { AuthContext } from "./auth";
|
|
4
|
+
import {
|
|
5
|
+
activateAzureRole,
|
|
6
|
+
ActiveAzureRole,
|
|
7
|
+
deactivateAzureRole,
|
|
8
|
+
fetchEligibleRolesForSubscription,
|
|
9
|
+
fetchSubscriptions,
|
|
10
|
+
listActiveAzureRoles,
|
|
11
|
+
} from "./azure-pim";
|
|
12
|
+
import {
|
|
13
|
+
formatActiveRole,
|
|
14
|
+
formatRole,
|
|
15
|
+
formatSubscription,
|
|
16
|
+
logBlank,
|
|
17
|
+
logDim,
|
|
18
|
+
logError,
|
|
19
|
+
logInfo,
|
|
20
|
+
logSuccess,
|
|
21
|
+
logWarning,
|
|
22
|
+
showDivider,
|
|
23
|
+
showSummary,
|
|
24
|
+
} from "./ui";
|
|
25
|
+
|
|
26
|
+
const promptBackToMainMenuOrExit = async (message: string): Promise<void> => {
|
|
27
|
+
logBlank();
|
|
28
|
+
const { next } = await inquirer.prompt<{ next: "back" | "exit" }>([
|
|
29
|
+
{
|
|
30
|
+
type: "select",
|
|
31
|
+
name: "next",
|
|
32
|
+
message: chalk.yellow(message),
|
|
33
|
+
choices: [
|
|
34
|
+
{ name: chalk.cyan("↩ Back to Main Menu"), value: "back" },
|
|
35
|
+
{ name: chalk.red("✕ Exit"), value: "exit" },
|
|
36
|
+
],
|
|
37
|
+
default: "back",
|
|
38
|
+
},
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
if (next === "exit") {
|
|
42
|
+
logBlank();
|
|
43
|
+
logDim("Goodbye! 👋");
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const showMainMenu = async (authContext: AuthContext): Promise<void> => {
|
|
49
|
+
while (true) {
|
|
50
|
+
showDivider();
|
|
51
|
+
logBlank();
|
|
52
|
+
const { action } = await inquirer.prompt<{ action: "activate" | "deactivate" | "exit" }>([
|
|
53
|
+
{
|
|
54
|
+
type: "select",
|
|
55
|
+
name: "action",
|
|
56
|
+
message: chalk.cyan.bold("What would you like to do?"),
|
|
57
|
+
choices: [
|
|
58
|
+
{ name: chalk.green("▶ Activate Role(s)"), value: "activate" },
|
|
59
|
+
{ name: chalk.yellow("◼ Deactivate Role(s)"), value: "deactivate" },
|
|
60
|
+
{ name: chalk.red("✕ Exit"), value: "exit" },
|
|
61
|
+
],
|
|
62
|
+
default: "activate",
|
|
63
|
+
},
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
switch (action) {
|
|
67
|
+
case "activate":
|
|
68
|
+
await handleActivation(authContext);
|
|
69
|
+
break;
|
|
70
|
+
case "deactivate":
|
|
71
|
+
await handleDeactivation(authContext);
|
|
72
|
+
break;
|
|
73
|
+
case "exit":
|
|
74
|
+
logBlank();
|
|
75
|
+
logDim("Goodbye! 👋");
|
|
76
|
+
logBlank();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const handleActivation = async (authContext: AuthContext): Promise<void> => {
|
|
83
|
+
try {
|
|
84
|
+
logBlank();
|
|
85
|
+
logInfo("Starting role activation flow...");
|
|
86
|
+
logBlank();
|
|
87
|
+
|
|
88
|
+
const subscriptions = await fetchSubscriptions(authContext.credential);
|
|
89
|
+
|
|
90
|
+
if (subscriptions.length === 0) {
|
|
91
|
+
logWarning("No subscriptions found.");
|
|
92
|
+
await promptBackToMainMenuOrExit("What would you like to do?");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const BACK_VALUE = "__BACK__";
|
|
97
|
+
const subscriptionChoices = subscriptions
|
|
98
|
+
.map((sub) => ({
|
|
99
|
+
name: formatSubscription(sub.displayName, sub.subscriptionId),
|
|
100
|
+
value: sub.subscriptionId,
|
|
101
|
+
}))
|
|
102
|
+
.concat([{ name: chalk.dim("↩ Back to Main Menu"), value: BACK_VALUE }]);
|
|
103
|
+
|
|
104
|
+
logBlank();
|
|
105
|
+
const { selectedSubscriptionId } = await inquirer.prompt<{
|
|
106
|
+
selectedSubscriptionId: string;
|
|
107
|
+
}>([
|
|
108
|
+
{
|
|
109
|
+
type: "select",
|
|
110
|
+
name: "selectedSubscriptionId",
|
|
111
|
+
message: chalk.cyan("Select a subscription:"),
|
|
112
|
+
choices: subscriptionChoices,
|
|
113
|
+
pageSize: 15,
|
|
114
|
+
default: subscriptionChoices[0]?.value,
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
if (selectedSubscriptionId === BACK_VALUE) {
|
|
119
|
+
logDim("Returning to main menu...");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const selectedSubscription = subscriptions.find((sub) => sub.subscriptionId === selectedSubscriptionId);
|
|
124
|
+
if (!selectedSubscription) {
|
|
125
|
+
logError("Selected subscription not found.");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const eligibleRoles = await fetchEligibleRolesForSubscription(
|
|
130
|
+
authContext.credential,
|
|
131
|
+
selectedSubscription.subscriptionId,
|
|
132
|
+
selectedSubscription.displayName,
|
|
133
|
+
authContext.userId
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (eligibleRoles.length === 0) {
|
|
137
|
+
logWarning("No eligible roles found for the selected subscription.");
|
|
138
|
+
await promptBackToMainMenuOrExit("What would you like to do?");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
logBlank();
|
|
143
|
+
const { rolesToActivate } = await inquirer.prompt([
|
|
144
|
+
{
|
|
145
|
+
type: "checkbox",
|
|
146
|
+
name: "rolesToActivate",
|
|
147
|
+
message: chalk.cyan("Select role(s) to activate:"),
|
|
148
|
+
choices: eligibleRoles.map((role) => ({
|
|
149
|
+
name: formatRole(role.roleName, role.scopeDisplayName),
|
|
150
|
+
value: role.id,
|
|
151
|
+
checked: false,
|
|
152
|
+
})),
|
|
153
|
+
validate: (answer) => {
|
|
154
|
+
if (answer.length < 1) {
|
|
155
|
+
return chalk.red("You must choose at least one role.");
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
},
|
|
159
|
+
pageSize: 15,
|
|
160
|
+
},
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
logBlank();
|
|
164
|
+
const activationDetails = await inquirer.prompt([
|
|
165
|
+
{
|
|
166
|
+
type: "number",
|
|
167
|
+
name: "durationHours",
|
|
168
|
+
message: chalk.cyan("Duration (hours, max 8):"),
|
|
169
|
+
default: 8,
|
|
170
|
+
validate: (value) => {
|
|
171
|
+
if (!value) return chalk.red("Please enter a valid number.");
|
|
172
|
+
if (value >= 1 && value <= 8) return true;
|
|
173
|
+
return chalk.red("Please enter a value between 1 and 8.");
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
type: "input",
|
|
178
|
+
name: "justification",
|
|
179
|
+
message: chalk.cyan("Justification for activation:"),
|
|
180
|
+
default: "Activated via azp-cli",
|
|
181
|
+
validate: (value) => {
|
|
182
|
+
if (value.trim().length >= 5) return true;
|
|
183
|
+
return chalk.red("Justification should be at least 5 characters long.");
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
// Show summary before confirmation
|
|
189
|
+
const selectedRoleNames = rolesToActivate
|
|
190
|
+
.map((roleId: string) => {
|
|
191
|
+
const role = eligibleRoles.find((r) => r.id === roleId);
|
|
192
|
+
return role ? `${role.roleName} @ ${role.scopeDisplayName}` : roleId;
|
|
193
|
+
})
|
|
194
|
+
.join(", ");
|
|
195
|
+
|
|
196
|
+
showSummary("Activation Summary", [
|
|
197
|
+
{ label: "Subscription", value: selectedSubscription.displayName },
|
|
198
|
+
{ label: "Role(s)", value: selectedRoleNames },
|
|
199
|
+
{ label: "Duration", value: `${activationDetails.durationHours} hour(s)` },
|
|
200
|
+
{ label: "Justification", value: activationDetails.justification },
|
|
201
|
+
]);
|
|
202
|
+
|
|
203
|
+
const { confirmActivation } = await inquirer.prompt([
|
|
204
|
+
{
|
|
205
|
+
type: "confirm",
|
|
206
|
+
name: "confirmActivation",
|
|
207
|
+
message: chalk.yellow(`Confirm activation of ${rolesToActivate.length} role(s)?`),
|
|
208
|
+
default: true,
|
|
209
|
+
},
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
if (!confirmActivation) {
|
|
213
|
+
logWarning("Activation cancelled by user.");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
logBlank();
|
|
218
|
+
logInfo(`Activating ${rolesToActivate.length} role(s)...`);
|
|
219
|
+
logBlank();
|
|
220
|
+
|
|
221
|
+
let successCount = 0;
|
|
222
|
+
let failCount = 0;
|
|
223
|
+
|
|
224
|
+
for (const roleId of rolesToActivate as string[]) {
|
|
225
|
+
const role = eligibleRoles.find((r) => r.id === roleId);
|
|
226
|
+
if (!role) {
|
|
227
|
+
logError(`Role with ID ${roleId} not found among eligible roles.`);
|
|
228
|
+
failCount++;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
await activateAzureRole(
|
|
234
|
+
authContext.credential,
|
|
235
|
+
{
|
|
236
|
+
principalId: authContext.userId,
|
|
237
|
+
roleDefinitionId: role.roleDefinitionId,
|
|
238
|
+
roleName: `${role.roleName} @ ${role.scopeDisplayName}`,
|
|
239
|
+
roleEligibilityScheduleId: role.roleEligibilityScheduleId,
|
|
240
|
+
scope: role.scope,
|
|
241
|
+
durationHours: activationDetails.durationHours,
|
|
242
|
+
justification: activationDetails.justification,
|
|
243
|
+
},
|
|
244
|
+
selectedSubscription.subscriptionId
|
|
245
|
+
);
|
|
246
|
+
successCount++;
|
|
247
|
+
} catch (error: any) {
|
|
248
|
+
logError(`Failed to activate role "${role.roleName}": ${error.message || error}`);
|
|
249
|
+
failCount++;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
logBlank();
|
|
254
|
+
showDivider();
|
|
255
|
+
if (successCount > 0 && failCount === 0) {
|
|
256
|
+
logSuccess(`All ${successCount} role(s) activated successfully!`);
|
|
257
|
+
} else if (successCount > 0 && failCount > 0) {
|
|
258
|
+
logWarning(`${successCount} role(s) activated, ${failCount} failed.`);
|
|
259
|
+
} else {
|
|
260
|
+
logError(`All ${failCount} role activation(s) failed.`);
|
|
261
|
+
}
|
|
262
|
+
} catch (error: any) {
|
|
263
|
+
logError(`Error during activation: ${error.message || error}`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
export const handleDeactivation = async (authContext: AuthContext): Promise<void> => {
|
|
269
|
+
try {
|
|
270
|
+
logBlank();
|
|
271
|
+
logInfo("Starting role deactivation flow...");
|
|
272
|
+
logBlank();
|
|
273
|
+
|
|
274
|
+
const subscriptions = await fetchSubscriptions(authContext.credential);
|
|
275
|
+
let activeAzureRoles: ActiveAzureRole[] = [];
|
|
276
|
+
|
|
277
|
+
if (subscriptions.length === 0) {
|
|
278
|
+
logWarning("No subscriptions found.");
|
|
279
|
+
await promptBackToMainMenuOrExit("What would you like to do?");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
for (const sub of subscriptions) {
|
|
284
|
+
const roles = await listActiveAzureRoles(authContext.credential, sub.subscriptionId, sub.displayName, authContext.userId);
|
|
285
|
+
activeAzureRoles = activeAzureRoles.concat(roles);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (activeAzureRoles.length === 0) {
|
|
289
|
+
logWarning("No active roles found for deactivation.");
|
|
290
|
+
await promptBackToMainMenuOrExit("What would you like to do?");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const BACK_VALUE = "__BACK__";
|
|
295
|
+
|
|
296
|
+
logBlank();
|
|
297
|
+
const { rolesToDeactivate } = await inquirer.prompt([
|
|
298
|
+
{
|
|
299
|
+
type: "checkbox",
|
|
300
|
+
name: "rolesToDeactivate",
|
|
301
|
+
message: chalk.cyan("Select role(s) to deactivate:"),
|
|
302
|
+
choices: [
|
|
303
|
+
{ name: chalk.dim("↩ Back to Main Menu"), value: BACK_VALUE },
|
|
304
|
+
...activeAzureRoles.map((role) => ({
|
|
305
|
+
name: formatActiveRole(role.roleName, role.scopeDisplayName, role.subscriptionName, role.startDateTime),
|
|
306
|
+
value: role.id,
|
|
307
|
+
checked: false,
|
|
308
|
+
})),
|
|
309
|
+
],
|
|
310
|
+
validate: (answer) => {
|
|
311
|
+
if (Array.isArray(answer) && answer.includes(BACK_VALUE)) {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
if (answer.length < 1) {
|
|
315
|
+
return chalk.red("You must choose at least one role.");
|
|
316
|
+
}
|
|
317
|
+
return true;
|
|
318
|
+
},
|
|
319
|
+
pageSize: 15,
|
|
320
|
+
},
|
|
321
|
+
]);
|
|
322
|
+
|
|
323
|
+
const selectedRoleIds = (rolesToDeactivate as string[]).includes(BACK_VALUE)
|
|
324
|
+
? (rolesToDeactivate as string[]).filter((id) => id !== BACK_VALUE)
|
|
325
|
+
: (rolesToDeactivate as string[]);
|
|
326
|
+
|
|
327
|
+
if ((rolesToDeactivate as string[]).includes(BACK_VALUE) && selectedRoleIds.length === 0) {
|
|
328
|
+
logDim("Returning to main menu...");
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Show summary before confirmation
|
|
333
|
+
const selectedRoleNames = selectedRoleIds
|
|
334
|
+
.map((roleId) => {
|
|
335
|
+
const role = activeAzureRoles.find((r) => r.id === roleId);
|
|
336
|
+
return role ? `${role.roleName} @ ${role.scopeDisplayName}` : roleId;
|
|
337
|
+
})
|
|
338
|
+
.join(", ");
|
|
339
|
+
|
|
340
|
+
showSummary("Deactivation Summary", [{ label: "Role(s) to deactivate", value: selectedRoleNames }]);
|
|
341
|
+
|
|
342
|
+
const { confirmDeactivation } = await inquirer.prompt([
|
|
343
|
+
{
|
|
344
|
+
type: "confirm",
|
|
345
|
+
name: "confirmDeactivation",
|
|
346
|
+
message: chalk.yellow(`Confirm deactivation of ${selectedRoleIds.length} role(s)?`),
|
|
347
|
+
default: true,
|
|
348
|
+
},
|
|
349
|
+
]);
|
|
350
|
+
|
|
351
|
+
if (!confirmDeactivation) {
|
|
352
|
+
logWarning("Deactivation cancelled by user.");
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
logBlank();
|
|
357
|
+
logInfo(`Deactivating ${selectedRoleIds.length} role(s)...`);
|
|
358
|
+
logBlank();
|
|
359
|
+
|
|
360
|
+
let successCount = 0;
|
|
361
|
+
let failCount = 0;
|
|
362
|
+
|
|
363
|
+
for (const roleId of selectedRoleIds) {
|
|
364
|
+
const role = activeAzureRoles.find((r) => r.id === roleId);
|
|
365
|
+
if (!role) {
|
|
366
|
+
logError(`Role with ID ${roleId} not found among active roles.`);
|
|
367
|
+
failCount++;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
await deactivateAzureRole(
|
|
373
|
+
authContext.credential,
|
|
374
|
+
role.scope,
|
|
375
|
+
role.linkedRoleEligibilityScheduleId,
|
|
376
|
+
role.subscriptionId,
|
|
377
|
+
authContext.userId,
|
|
378
|
+
role.roleDefinitionId,
|
|
379
|
+
`${role.roleName} @ ${role.scopeDisplayName}`
|
|
380
|
+
);
|
|
381
|
+
successCount++;
|
|
382
|
+
} catch (error: any) {
|
|
383
|
+
logError(`Failed to deactivate role "${role.roleName}": ${error.message || error}`);
|
|
384
|
+
failCount++;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
logBlank();
|
|
389
|
+
showDivider();
|
|
390
|
+
if (successCount > 0 && failCount === 0) {
|
|
391
|
+
logSuccess(`All ${successCount} role(s) deactivated successfully!`);
|
|
392
|
+
} else if (successCount > 0 && failCount > 0) {
|
|
393
|
+
logWarning(`${successCount} role(s) deactivated, ${failCount} failed.`);
|
|
394
|
+
} else {
|
|
395
|
+
logError(`All ${failCount} role deactivation(s) failed.`);
|
|
396
|
+
}
|
|
397
|
+
} catch (error: any) {
|
|
398
|
+
logError(`Error during deactivation: ${error.message || error}`);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
};
|