@pagopa/dx-cli 0.21.1 → 0.21.3
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/dist/adapters/azure/__tests__/cloud-account-service.test.js +9 -1
- package/dist/adapters/azure/cloud-account-service.js +7 -0
- package/dist/adapters/commander/commands/__tests__/add.test.js +12 -4
- package/dist/adapters/commander/commands/__tests__/init.test.d.ts +4 -0
- package/dist/adapters/commander/commands/__tests__/init.test.js +48 -0
- package/dist/adapters/commander/commands/add.js +5 -2
- package/dist/adapters/commander/commands/init.d.ts +2 -0
- package/dist/adapters/commander/commands/init.js +30 -4
- package/dist/adapters/pagopa-technology/__tests__/authorization.test.js +349 -31
- package/dist/adapters/pagopa-technology/azure-authorization-config.d.ts +13 -0
- package/dist/adapters/pagopa-technology/azure-authorization-config.js +43 -0
- package/dist/adapters/pagopa-technology/{authorization.d.ts → azure-authorization.d.ts} +2 -2
- package/dist/adapters/pagopa-technology/azure-authorization.js +239 -0
- package/dist/adapters/plop/actions/__tests__/init-cloud-accounts.test.js +7 -0
- package/dist/adapters/plop/actions/__tests__/provision-terraform-backend.test.js +3 -0
- package/dist/adapters/plop/generators/environment/__tests__/actions.test.js +1 -0
- package/dist/adapters/plop/generators/environment/__tests__/prompts.test.js +156 -2
- package/dist/adapters/plop/generators/environment/prompts.d.ts +10 -0
- package/dist/adapters/plop/generators/environment/prompts.js +45 -1
- package/dist/domain/authorization.d.ts +6 -9
- package/dist/domain/authorization.js +27 -10
- package/dist/domain/github.d.ts +1 -0
- package/dist/domain/github.js +1 -0
- package/dist/index.js +2 -2
- package/dist/use-cases/__tests__/request-authorization.test.js +5 -3
- package/dist/use-cases/request-authorization.d.ts +2 -2
- package/dist/use-cases/request-authorization.js +2 -2
- package/package.json +1 -1
- package/dist/adapters/pagopa-technology/authorization.js +0 -124
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { describe, expect, it } from "vitest";
|
|
5
5
|
import { mock } from "vitest-mock-extended";
|
|
6
|
-
import { AuthorizationResult,
|
|
6
|
+
import { AuthorizationResult, InvalidAuthorizationFileFormatError, requestAuthorizationInputSchema, } from "../../../domain/authorization.js";
|
|
7
7
|
import { FileNotFoundError, PullRequest, } from "../../../domain/github.js";
|
|
8
|
-
import {
|
|
8
|
+
import { DEFAULT_GROUP_SPECS, makeAzureAdGroupName as makeGroupName, } from "../azure-authorization-config.js";
|
|
9
|
+
import { makeAzureAuthorizationService as makeAuthorizationService } from "../azure-authorization.js";
|
|
9
10
|
const makeEnv = () => {
|
|
10
11
|
const gitHubService = mock();
|
|
11
12
|
const authorizationService = makeAuthorizationService(gitHubService);
|
|
@@ -16,12 +17,15 @@ const makeEnv = () => {
|
|
|
16
17
|
};
|
|
17
18
|
const makeSampleInput = () => requestAuthorizationInputSchema.parse({
|
|
18
19
|
bootstrapIdentityId: "test-bootstrap-identity-id",
|
|
20
|
+
envShort: "d",
|
|
21
|
+
prefix: "test",
|
|
19
22
|
repoName: "test-repo",
|
|
20
23
|
subscriptionName: "test-subscription",
|
|
21
24
|
});
|
|
22
25
|
const FILE_PATH = "src/azure-subscriptions/subscriptions/test-subscription/terraform.tfvars.json";
|
|
23
26
|
// eslint-disable-next-line max-lines-per-function
|
|
24
27
|
describe("PagoPA AuthorizationService", () => {
|
|
28
|
+
// eslint-disable-next-line max-lines-per-function
|
|
25
29
|
describe("happy path", () => {
|
|
26
30
|
it("should create a pull request when all steps succeed", async () => {
|
|
27
31
|
const { authorizationService, gitHubService } = makeEnv();
|
|
@@ -39,21 +43,21 @@ describe("PagoPA AuthorizationService", () => {
|
|
|
39
43
|
const authResult = result._unsafeUnwrap();
|
|
40
44
|
expect(authResult).toBeInstanceOf(AuthorizationResult);
|
|
41
45
|
expect(authResult.url).toBe("https://github.com/pagopa/eng-azure-authorization/pull/42");
|
|
42
|
-
expect(gitHubService.
|
|
43
|
-
branchName: "feats/add-test-repo-test-subscription-bootstrap-identity",
|
|
44
|
-
fromRef: "main",
|
|
46
|
+
expect(gitHubService.getFileContent).toHaveBeenCalledWith({
|
|
45
47
|
owner: "pagopa",
|
|
48
|
+
path: FILE_PATH,
|
|
49
|
+
ref: "main",
|
|
46
50
|
repo: "eng-azure-authorization",
|
|
47
51
|
});
|
|
48
|
-
expect(gitHubService.
|
|
52
|
+
expect(gitHubService.createBranch).toHaveBeenCalledWith({
|
|
53
|
+
branchName: "feats/add-test-repo-test-subscription-bootstrap-identity",
|
|
54
|
+
fromRef: "main",
|
|
49
55
|
owner: "pagopa",
|
|
50
|
-
path: FILE_PATH,
|
|
51
|
-
ref: "feats/add-test-repo-test-subscription-bootstrap-identity",
|
|
52
56
|
repo: "eng-azure-authorization",
|
|
53
57
|
});
|
|
54
58
|
expect(gitHubService.updateFile).toHaveBeenCalledWith(expect.objectContaining({
|
|
55
59
|
branch: "feats/add-test-repo-test-subscription-bootstrap-identity",
|
|
56
|
-
message: "Add
|
|
60
|
+
message: "Add bootstrap identity and AD groups for test-subscription",
|
|
57
61
|
owner: "pagopa",
|
|
58
62
|
path: FILE_PATH,
|
|
59
63
|
repo: "eng-azure-authorization",
|
|
@@ -61,12 +65,128 @@ describe("PagoPA AuthorizationService", () => {
|
|
|
61
65
|
}));
|
|
62
66
|
expect(gitHubService.createPullRequest).toHaveBeenCalledWith({
|
|
63
67
|
base: "main",
|
|
64
|
-
body: "This PR adds the bootstrap identity `test-bootstrap-identity-id` to the directory readers for subscription `test-subscription`.",
|
|
68
|
+
body: "This PR adds the bootstrap identity `test-bootstrap-identity-id` to the directory readers and configures AD groups for subscription `test-subscription`.",
|
|
65
69
|
head: "feats/add-test-repo-test-subscription-bootstrap-identity",
|
|
66
70
|
owner: "pagopa",
|
|
67
71
|
repo: "eng-azure-authorization",
|
|
68
|
-
title: "Add
|
|
72
|
+
title: "Add bootstrap identity and AD groups for test-subscription",
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
it("should add all default AD groups when none exist", async () => {
|
|
76
|
+
const { authorizationService, gitHubService } = makeEnv();
|
|
77
|
+
const input = makeSampleInput();
|
|
78
|
+
const originalContent = JSON.stringify({ directory_readers: { service_principals_name: [] } }, null, 2);
|
|
79
|
+
gitHubService.createBranch.mockResolvedValue(undefined);
|
|
80
|
+
gitHubService.getFileContent.mockResolvedValue({
|
|
81
|
+
content: originalContent,
|
|
82
|
+
sha: "sha-groups-1",
|
|
69
83
|
});
|
|
84
|
+
gitHubService.updateFile.mockResolvedValue(undefined);
|
|
85
|
+
gitHubService.createPullRequest.mockResolvedValue(new PullRequest("https://github.com/pagopa/eng-azure-authorization/pull/50"));
|
|
86
|
+
const result = await authorizationService.requestAuthorization(input);
|
|
87
|
+
expect(result.isOk()).toBe(true);
|
|
88
|
+
const updateCall = gitHubService.updateFile.mock.calls[0][0];
|
|
89
|
+
const updatedParsed = JSON.parse(updateCall.content);
|
|
90
|
+
expect(updatedParsed.groups).toHaveLength(DEFAULT_GROUP_SPECS.length);
|
|
91
|
+
for (const spec of DEFAULT_GROUP_SPECS) {
|
|
92
|
+
const groupName = makeGroupName("test", "d", spec.groupName);
|
|
93
|
+
const found = updatedParsed.groups.find((g) => g.name === groupName);
|
|
94
|
+
expect(found).toBeDefined();
|
|
95
|
+
expect(found.roles).toEqual(spec.roles);
|
|
96
|
+
expect(found.members).toEqual([]);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
it("should preserve existing members and add missing groups", async () => {
|
|
100
|
+
const { authorizationService, gitHubService } = makeEnv();
|
|
101
|
+
const input = makeSampleInput();
|
|
102
|
+
const originalContent = JSON.stringify({
|
|
103
|
+
directory_readers: { service_principals_name: [] },
|
|
104
|
+
groups: [
|
|
105
|
+
{
|
|
106
|
+
members: ["alice@pagopa.it"],
|
|
107
|
+
name: "test-d-adgroup-admin",
|
|
108
|
+
roles: ["Owner"],
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
}, null, 2);
|
|
112
|
+
gitHubService.createBranch.mockResolvedValue(undefined);
|
|
113
|
+
gitHubService.getFileContent.mockResolvedValue({
|
|
114
|
+
content: originalContent,
|
|
115
|
+
sha: "sha-groups-2",
|
|
116
|
+
});
|
|
117
|
+
gitHubService.updateFile.mockResolvedValue(undefined);
|
|
118
|
+
gitHubService.createPullRequest.mockResolvedValue(new PullRequest("https://github.com/pagopa/eng-azure-authorization/pull/51"));
|
|
119
|
+
const result = await authorizationService.requestAuthorization(input);
|
|
120
|
+
expect(result.isOk()).toBe(true);
|
|
121
|
+
const updateCall = gitHubService.updateFile.mock.calls[0][0];
|
|
122
|
+
const updatedParsed = JSON.parse(updateCall.content);
|
|
123
|
+
// All default groups should be present
|
|
124
|
+
expect(updatedParsed.groups).toHaveLength(DEFAULT_GROUP_SPECS.length);
|
|
125
|
+
// Admin group should preserve existing member
|
|
126
|
+
const adminGroup = updatedParsed.groups.find((g) => g.name === "test-d-adgroup-admin");
|
|
127
|
+
expect(adminGroup.members).toContain("alice@pagopa.it");
|
|
128
|
+
});
|
|
129
|
+
it("should update roles on existing group while preserving members", async () => {
|
|
130
|
+
const { authorizationService, gitHubService } = makeEnv();
|
|
131
|
+
const input = makeSampleInput();
|
|
132
|
+
const originalContent = JSON.stringify({
|
|
133
|
+
directory_readers: { service_principals_name: [] },
|
|
134
|
+
groups: [
|
|
135
|
+
{
|
|
136
|
+
// externals normally gets "Owner" but file has "Reader"
|
|
137
|
+
members: ["bob@pagopa.it"],
|
|
138
|
+
name: "test-d-adgroup-externals",
|
|
139
|
+
roles: ["Reader"],
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
}, null, 2);
|
|
143
|
+
gitHubService.createBranch.mockResolvedValue(undefined);
|
|
144
|
+
gitHubService.getFileContent.mockResolvedValue({
|
|
145
|
+
content: originalContent,
|
|
146
|
+
sha: "sha-groups-3",
|
|
147
|
+
});
|
|
148
|
+
gitHubService.updateFile.mockResolvedValue(undefined);
|
|
149
|
+
gitHubService.createPullRequest.mockResolvedValue(new PullRequest("https://github.com/pagopa/eng-azure-authorization/pull/52"));
|
|
150
|
+
const result = await authorizationService.requestAuthorization(input);
|
|
151
|
+
expect(result.isOk()).toBe(true);
|
|
152
|
+
const updateCall = gitHubService.updateFile.mock.calls[0][0];
|
|
153
|
+
const updatedParsed = JSON.parse(updateCall.content);
|
|
154
|
+
const externalsGroup = updatedParsed.groups.find((g) => g.name === "test-d-adgroup-externals");
|
|
155
|
+
// Roles updated to default
|
|
156
|
+
expect(externalsGroup.roles).toEqual(["Owner"]);
|
|
157
|
+
// Members preserved
|
|
158
|
+
expect(externalsGroup.members).toContain("bob@pagopa.it");
|
|
159
|
+
});
|
|
160
|
+
it("should preserve custom (non-default) groups", async () => {
|
|
161
|
+
const { authorizationService, gitHubService } = makeEnv();
|
|
162
|
+
const input = makeSampleInput();
|
|
163
|
+
const originalContent = JSON.stringify({
|
|
164
|
+
directory_readers: { service_principals_name: [] },
|
|
165
|
+
groups: [
|
|
166
|
+
{
|
|
167
|
+
members: ["carol@pagopa.it"],
|
|
168
|
+
name: "test-d-adgroup-custom-team",
|
|
169
|
+
roles: ["Contributor"],
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
}, null, 2);
|
|
173
|
+
gitHubService.createBranch.mockResolvedValue(undefined);
|
|
174
|
+
gitHubService.getFileContent.mockResolvedValue({
|
|
175
|
+
content: originalContent,
|
|
176
|
+
sha: "sha-groups-4",
|
|
177
|
+
});
|
|
178
|
+
gitHubService.updateFile.mockResolvedValue(undefined);
|
|
179
|
+
gitHubService.createPullRequest.mockResolvedValue(new PullRequest("https://github.com/pagopa/eng-azure-authorization/pull/53"));
|
|
180
|
+
const result = await authorizationService.requestAuthorization(input);
|
|
181
|
+
expect(result.isOk()).toBe(true);
|
|
182
|
+
const updateCall = gitHubService.updateFile.mock.calls[0][0];
|
|
183
|
+
const updatedParsed = JSON.parse(updateCall.content);
|
|
184
|
+
// Custom group preserved
|
|
185
|
+
const customGroup = updatedParsed.groups.find((g) => g.name === "test-d-adgroup-custom-team");
|
|
186
|
+
expect(customGroup).toBeDefined();
|
|
187
|
+
expect(customGroup.members).toContain("carol@pagopa.it");
|
|
188
|
+
// All defaults also present
|
|
189
|
+
expect(updatedParsed.groups).toHaveLength(DEFAULT_GROUP_SPECS.length + 1);
|
|
70
190
|
});
|
|
71
191
|
it("should preserve existing fields in the JSON file", async () => {
|
|
72
192
|
const { authorizationService, gitHubService } = makeEnv();
|
|
@@ -92,19 +212,14 @@ describe("PagoPA AuthorizationService", () => {
|
|
|
92
212
|
expect(result.isOk()).toBe(true);
|
|
93
213
|
const updateCall = gitHubService.updateFile.mock.calls[0][0];
|
|
94
214
|
const updatedParsed = JSON.parse(updateCall.content);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
entra_groups: {
|
|
104
|
-
readers: ["reader-group"],
|
|
105
|
-
},
|
|
106
|
-
other_top_level: true,
|
|
107
|
-
});
|
|
215
|
+
// Identity added
|
|
216
|
+
expect(updatedParsed.directory_readers.service_principals_name).toContain("test-bootstrap-identity-id");
|
|
217
|
+
// Extra fields preserved
|
|
218
|
+
expect(updatedParsed.directory_readers.some_other_field).toBe("keep-me");
|
|
219
|
+
expect(updatedParsed.entra_groups).toEqual({ readers: ["reader-group"] });
|
|
220
|
+
expect(updatedParsed.other_top_level).toBe(true);
|
|
221
|
+
// All default groups added
|
|
222
|
+
expect(updatedParsed.groups).toHaveLength(DEFAULT_GROUP_SPECS.length);
|
|
108
223
|
});
|
|
109
224
|
it("should append identity to an existing non-empty list", async () => {
|
|
110
225
|
const { authorizationService, gitHubService } = makeEnv();
|
|
@@ -128,23 +243,150 @@ describe("PagoPA AuthorizationService", () => {
|
|
|
128
243
|
expect(updatedParsed.directory_readers.service_principals_name).toContain("test-bootstrap-identity-id");
|
|
129
244
|
expect(updatedParsed.directory_readers.service_principals_name).toContain("existing-identity");
|
|
130
245
|
});
|
|
246
|
+
it("should use identity-only messages when groups are already correct", async () => {
|
|
247
|
+
const { authorizationService, gitHubService } = makeEnv();
|
|
248
|
+
const input = makeSampleInput();
|
|
249
|
+
// Groups already correct, but identity is missing
|
|
250
|
+
const allGroups = DEFAULT_GROUP_SPECS.map((spec) => ({
|
|
251
|
+
members: [],
|
|
252
|
+
name: makeGroupName("test", "d", spec.groupName),
|
|
253
|
+
roles: [...spec.roles],
|
|
254
|
+
}));
|
|
255
|
+
const originalContent = JSON.stringify({
|
|
256
|
+
directory_readers: { service_principals_name: [] },
|
|
257
|
+
groups: allGroups,
|
|
258
|
+
}, null, 2);
|
|
259
|
+
gitHubService.getFileContent.mockResolvedValue({
|
|
260
|
+
content: originalContent,
|
|
261
|
+
sha: "sha-identity-only",
|
|
262
|
+
});
|
|
263
|
+
gitHubService.createBranch.mockResolvedValue(undefined);
|
|
264
|
+
gitHubService.updateFile.mockResolvedValue(undefined);
|
|
265
|
+
gitHubService.createPullRequest.mockResolvedValue(new PullRequest("https://github.com/pagopa/eng-azure-authorization/pull/60"));
|
|
266
|
+
const result = await authorizationService.requestAuthorization(input);
|
|
267
|
+
expect(result.isOk()).toBe(true);
|
|
268
|
+
expect(gitHubService.updateFile).toHaveBeenCalledWith(expect.objectContaining({
|
|
269
|
+
message: "Add bootstrap identity for test-subscription",
|
|
270
|
+
}));
|
|
271
|
+
expect(gitHubService.createPullRequest).toHaveBeenCalledWith(expect.objectContaining({
|
|
272
|
+
body: "This PR adds the bootstrap identity `test-bootstrap-identity-id` to the directory readers for subscription `test-subscription`.",
|
|
273
|
+
title: "Add bootstrap identity for test-subscription",
|
|
274
|
+
}));
|
|
275
|
+
});
|
|
276
|
+
it("should use groups-only messages when identity already exists", async () => {
|
|
277
|
+
const { authorizationService, gitHubService } = makeEnv();
|
|
278
|
+
const input = makeSampleInput();
|
|
279
|
+
// Identity present, no groups yet
|
|
280
|
+
const originalContent = JSON.stringify({
|
|
281
|
+
directory_readers: {
|
|
282
|
+
service_principals_name: ["test-bootstrap-identity-id"],
|
|
283
|
+
},
|
|
284
|
+
}, null, 2);
|
|
285
|
+
gitHubService.getFileContent.mockResolvedValue({
|
|
286
|
+
content: originalContent,
|
|
287
|
+
sha: "sha-groups-only",
|
|
288
|
+
});
|
|
289
|
+
gitHubService.createBranch.mockResolvedValue(undefined);
|
|
290
|
+
gitHubService.updateFile.mockResolvedValue(undefined);
|
|
291
|
+
gitHubService.createPullRequest.mockResolvedValue(new PullRequest("https://github.com/pagopa/eng-azure-authorization/pull/61"));
|
|
292
|
+
const result = await authorizationService.requestAuthorization(input);
|
|
293
|
+
expect(result.isOk()).toBe(true);
|
|
294
|
+
expect(gitHubService.updateFile).toHaveBeenCalledWith(expect.objectContaining({
|
|
295
|
+
message: "Configure AD groups for test-subscription",
|
|
296
|
+
}));
|
|
297
|
+
expect(gitHubService.createPullRequest).toHaveBeenCalledWith(expect.objectContaining({
|
|
298
|
+
body: "This PR configures AD groups for subscription `test-subscription`.",
|
|
299
|
+
title: "Configure AD groups for test-subscription",
|
|
300
|
+
}));
|
|
301
|
+
});
|
|
302
|
+
it("should preserve original group order and append missing defaults at end", async () => {
|
|
303
|
+
const { authorizationService, gitHubService } = makeEnv();
|
|
304
|
+
const input = makeSampleInput();
|
|
305
|
+
// Start with custom group + one default group (externals) in deliberate order
|
|
306
|
+
const originalContent = JSON.stringify({
|
|
307
|
+
directory_readers: { service_principals_name: [] },
|
|
308
|
+
groups: [
|
|
309
|
+
{
|
|
310
|
+
members: ["carol@pagopa.it"],
|
|
311
|
+
name: "test-d-adgroup-custom-team",
|
|
312
|
+
roles: ["Contributor"],
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
members: ["alice@pagopa.it"],
|
|
316
|
+
name: "test-d-adgroup-externals",
|
|
317
|
+
roles: ["Owner"],
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
}, null, 2);
|
|
321
|
+
gitHubService.getFileContent.mockResolvedValue({
|
|
322
|
+
content: originalContent,
|
|
323
|
+
sha: "sha-order",
|
|
324
|
+
});
|
|
325
|
+
gitHubService.createBranch.mockResolvedValue(undefined);
|
|
326
|
+
gitHubService.updateFile.mockResolvedValue(undefined);
|
|
327
|
+
gitHubService.createPullRequest.mockResolvedValue(new PullRequest("https://github.com/pagopa/eng-azure-authorization/pull/62"));
|
|
328
|
+
const result = await authorizationService.requestAuthorization(input);
|
|
329
|
+
expect(result.isOk()).toBe(true);
|
|
330
|
+
const updateCall = gitHubService.updateFile.mock.calls[0][0];
|
|
331
|
+
const updatedParsed = JSON.parse(updateCall.content);
|
|
332
|
+
const groupNames = updatedParsed.groups.map((g) => g.name);
|
|
333
|
+
// Original groups preserve their order
|
|
334
|
+
expect(groupNames[0]).toBe("test-d-adgroup-custom-team");
|
|
335
|
+
expect(groupNames[1]).toBe("test-d-adgroup-externals");
|
|
336
|
+
// Missing defaults appended after existing groups
|
|
337
|
+
expect(groupNames.length).toBe(DEFAULT_GROUP_SPECS.length + 1);
|
|
338
|
+
});
|
|
339
|
+
it("should preserve extra fields on group entries through round-trip", async () => {
|
|
340
|
+
const { authorizationService, gitHubService } = makeEnv();
|
|
341
|
+
const input = makeSampleInput();
|
|
342
|
+
// Group with extra metadata field
|
|
343
|
+
const originalContent = JSON.stringify({
|
|
344
|
+
directory_readers: { service_principals_name: [] },
|
|
345
|
+
groups: [
|
|
346
|
+
{
|
|
347
|
+
members: ["alice@pagopa.it"],
|
|
348
|
+
metadata: { created_by: "automation" },
|
|
349
|
+
name: "test-d-adgroup-admin",
|
|
350
|
+
roles: ["Owner"],
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
}, null, 2);
|
|
354
|
+
gitHubService.getFileContent.mockResolvedValue({
|
|
355
|
+
content: originalContent,
|
|
356
|
+
sha: "sha-extra-fields",
|
|
357
|
+
});
|
|
358
|
+
gitHubService.createBranch.mockResolvedValue(undefined);
|
|
359
|
+
gitHubService.updateFile.mockResolvedValue(undefined);
|
|
360
|
+
gitHubService.createPullRequest.mockResolvedValue(new PullRequest("https://github.com/pagopa/eng-azure-authorization/pull/63"));
|
|
361
|
+
const result = await authorizationService.requestAuthorization(input);
|
|
362
|
+
expect(result.isOk()).toBe(true);
|
|
363
|
+
const updateCall = gitHubService.updateFile.mock.calls[0][0];
|
|
364
|
+
const updatedParsed = JSON.parse(updateCall.content);
|
|
365
|
+
const adminGroup = updatedParsed.groups.find((g) => g.name === "test-d-adgroup-admin");
|
|
366
|
+
// Extra field preserved
|
|
367
|
+
expect(adminGroup.metadata).toEqual({ created_by: "automation" });
|
|
368
|
+
// Members preserved
|
|
369
|
+
expect(adminGroup.members).toContain("alice@pagopa.it");
|
|
370
|
+
});
|
|
131
371
|
});
|
|
372
|
+
// eslint-disable-next-line max-lines-per-function
|
|
132
373
|
describe("error handling", () => {
|
|
133
374
|
it("should return error when file is not found", async () => {
|
|
134
375
|
const { authorizationService, gitHubService } = makeEnv();
|
|
135
376
|
const input = makeSampleInput();
|
|
136
|
-
gitHubService.createBranch.mockResolvedValue(undefined);
|
|
137
377
|
gitHubService.getFileContent.mockRejectedValue(new FileNotFoundError(FILE_PATH));
|
|
138
378
|
const result = await authorizationService.requestAuthorization(input);
|
|
139
379
|
expect(result.isErr()).toBe(true);
|
|
140
380
|
const error = result._unsafeUnwrapErr();
|
|
141
381
|
expect(error.message).toContain("Unable to get");
|
|
142
382
|
expect(error.message).toContain("terraform.tfvars.json");
|
|
383
|
+
expect(gitHubService.createBranch).not.toHaveBeenCalled();
|
|
143
384
|
expect(gitHubService.updateFile).not.toHaveBeenCalled();
|
|
144
385
|
});
|
|
145
|
-
it("should
|
|
386
|
+
it("should upsert groups and create PR when identity already exists", async () => {
|
|
146
387
|
const { authorizationService, gitHubService } = makeEnv();
|
|
147
388
|
const input = makeSampleInput();
|
|
389
|
+
// Identity already present, no groups yet
|
|
148
390
|
const content = JSON.stringify({
|
|
149
391
|
directory_readers: {
|
|
150
392
|
service_principals_name: ["test-bootstrap-identity-id"],
|
|
@@ -155,15 +397,86 @@ describe("PagoPA AuthorizationService", () => {
|
|
|
155
397
|
content,
|
|
156
398
|
sha: "sha-123",
|
|
157
399
|
});
|
|
400
|
+
gitHubService.updateFile.mockResolvedValue(undefined);
|
|
401
|
+
gitHubService.createPullRequest.mockResolvedValue(new PullRequest("https://github.com/pagopa/eng-azure-authorization/pull/55"));
|
|
158
402
|
const result = await authorizationService.requestAuthorization(input);
|
|
159
|
-
expect(result.
|
|
160
|
-
|
|
403
|
+
expect(result.isOk()).toBe(true);
|
|
404
|
+
const authResult = result._unsafeUnwrap();
|
|
405
|
+
expect(authResult.url).toBe("https://github.com/pagopa/eng-azure-authorization/pull/55");
|
|
406
|
+
// Identity must NOT be duplicated
|
|
407
|
+
const updateCall = gitHubService.updateFile.mock.calls[0][0];
|
|
408
|
+
const updatedParsed = JSON.parse(updateCall.content);
|
|
409
|
+
expect(updatedParsed.directory_readers.service_principals_name).toHaveLength(1);
|
|
410
|
+
expect(updatedParsed.directory_readers.service_principals_name).toContain("test-bootstrap-identity-id");
|
|
411
|
+
// All default groups must be created
|
|
412
|
+
expect(updatedParsed.groups).toHaveLength(DEFAULT_GROUP_SPECS.length);
|
|
413
|
+
});
|
|
414
|
+
it("should skip update and PR when identity exists and groups are already correct", async () => {
|
|
415
|
+
const { authorizationService, gitHubService } = makeEnv();
|
|
416
|
+
const input = makeSampleInput();
|
|
417
|
+
// Build a file where the identity is present and all groups are already correct
|
|
418
|
+
const allGroups = DEFAULT_GROUP_SPECS.map((spec) => ({
|
|
419
|
+
members: [],
|
|
420
|
+
name: makeGroupName("test", "d", spec.groupName),
|
|
421
|
+
roles: [...spec.roles],
|
|
422
|
+
}));
|
|
423
|
+
const content = JSON.stringify({
|
|
424
|
+
directory_readers: {
|
|
425
|
+
service_principals_name: ["test-bootstrap-identity-id"],
|
|
426
|
+
},
|
|
427
|
+
groups: allGroups,
|
|
428
|
+
}, null, 2);
|
|
429
|
+
gitHubService.getFileContent.mockResolvedValue({
|
|
430
|
+
content,
|
|
431
|
+
sha: "sha-noop",
|
|
432
|
+
});
|
|
433
|
+
const result = await authorizationService.requestAuthorization(input);
|
|
434
|
+
// Should succeed as a no-op (no PR created)
|
|
435
|
+
expect(result.isOk()).toBe(true);
|
|
436
|
+
expect(result._unsafeUnwrap().url).toBeUndefined();
|
|
437
|
+
// Branch, file update, and PR must not be touched
|
|
438
|
+
expect(gitHubService.createBranch).not.toHaveBeenCalled();
|
|
161
439
|
expect(gitHubService.updateFile).not.toHaveBeenCalled();
|
|
440
|
+
expect(gitHubService.createPullRequest).not.toHaveBeenCalled();
|
|
162
441
|
});
|
|
163
|
-
it("should
|
|
442
|
+
it("should update group roles and create PR when identity exists with wrong roles", async () => {
|
|
164
443
|
const { authorizationService, gitHubService } = makeEnv();
|
|
165
444
|
const input = makeSampleInput();
|
|
445
|
+
// Identity present, but externals group has wrong roles
|
|
446
|
+
const content = JSON.stringify({
|
|
447
|
+
directory_readers: {
|
|
448
|
+
service_principals_name: ["test-bootstrap-identity-id"],
|
|
449
|
+
},
|
|
450
|
+
groups: [
|
|
451
|
+
{
|
|
452
|
+
members: ["bob@pagopa.it"],
|
|
453
|
+
name: "test-d-adgroup-externals",
|
|
454
|
+
roles: ["Reader"],
|
|
455
|
+
},
|
|
456
|
+
],
|
|
457
|
+
}, null, 2);
|
|
166
458
|
gitHubService.createBranch.mockResolvedValue(undefined);
|
|
459
|
+
gitHubService.getFileContent.mockResolvedValue({
|
|
460
|
+
content,
|
|
461
|
+
sha: "sha-roles",
|
|
462
|
+
});
|
|
463
|
+
gitHubService.updateFile.mockResolvedValue(undefined);
|
|
464
|
+
gitHubService.createPullRequest.mockResolvedValue(new PullRequest("https://github.com/pagopa/eng-azure-authorization/pull/56"));
|
|
465
|
+
const result = await authorizationService.requestAuthorization(input);
|
|
466
|
+
expect(result.isOk()).toBe(true);
|
|
467
|
+
expect(result._unsafeUnwrap().url).toBe("https://github.com/pagopa/eng-azure-authorization/pull/56");
|
|
468
|
+
const updateCall = gitHubService.updateFile.mock.calls[0][0];
|
|
469
|
+
const updatedParsed = JSON.parse(updateCall.content);
|
|
470
|
+
const externalsGroup = updatedParsed.groups.find((g) => g.name === "test-d-adgroup-externals");
|
|
471
|
+
expect(externalsGroup.roles).toEqual(["Owner"]);
|
|
472
|
+
// Member preserved
|
|
473
|
+
expect(externalsGroup.members).toContain("bob@pagopa.it");
|
|
474
|
+
// Identity not duplicated
|
|
475
|
+
expect(updatedParsed.directory_readers.service_principals_name).toHaveLength(1);
|
|
476
|
+
});
|
|
477
|
+
it("should return error when file content is not valid JSON", async () => {
|
|
478
|
+
const { authorizationService, gitHubService } = makeEnv();
|
|
479
|
+
const input = makeSampleInput();
|
|
167
480
|
gitHubService.getFileContent.mockResolvedValue({
|
|
168
481
|
content: "not valid json {{",
|
|
169
482
|
sha: "sha-123",
|
|
@@ -171,12 +484,12 @@ describe("PagoPA AuthorizationService", () => {
|
|
|
171
484
|
const result = await authorizationService.requestAuthorization(input);
|
|
172
485
|
expect(result.isErr()).toBe(true);
|
|
173
486
|
expect(result._unsafeUnwrapErr()).toBeInstanceOf(InvalidAuthorizationFileFormatError);
|
|
487
|
+
expect(gitHubService.createBranch).not.toHaveBeenCalled();
|
|
174
488
|
expect(gitHubService.updateFile).not.toHaveBeenCalled();
|
|
175
489
|
});
|
|
176
490
|
it("should return error when JSON is missing expected keys", async () => {
|
|
177
491
|
const { authorizationService, gitHubService } = makeEnv();
|
|
178
492
|
const input = makeSampleInput();
|
|
179
|
-
gitHubService.createBranch.mockResolvedValue(undefined);
|
|
180
493
|
gitHubService.getFileContent.mockResolvedValue({
|
|
181
494
|
content: JSON.stringify({ unexpected_key: {} }),
|
|
182
495
|
sha: "sha-123",
|
|
@@ -184,16 +497,21 @@ describe("PagoPA AuthorizationService", () => {
|
|
|
184
497
|
const result = await authorizationService.requestAuthorization(input);
|
|
185
498
|
expect(result.isErr()).toBe(true);
|
|
186
499
|
expect(result._unsafeUnwrapErr()).toBeInstanceOf(InvalidAuthorizationFileFormatError);
|
|
500
|
+
expect(gitHubService.createBranch).not.toHaveBeenCalled();
|
|
187
501
|
expect(gitHubService.updateFile).not.toHaveBeenCalled();
|
|
188
502
|
});
|
|
189
503
|
it("should return error when branch creation fails", async () => {
|
|
190
504
|
const { authorizationService, gitHubService } = makeEnv();
|
|
191
505
|
const input = makeSampleInput();
|
|
506
|
+
const content = JSON.stringify({ directory_readers: { service_principals_name: [] } }, null, 2);
|
|
507
|
+
gitHubService.getFileContent.mockResolvedValue({
|
|
508
|
+
content,
|
|
509
|
+
sha: "sha-123",
|
|
510
|
+
});
|
|
192
511
|
gitHubService.createBranch.mockRejectedValue(new Error("Failed to create branch: branch already exists"));
|
|
193
512
|
const result = await authorizationService.requestAuthorization(input);
|
|
194
513
|
expect(result.isErr()).toBe(true);
|
|
195
514
|
expect(result._unsafeUnwrapErr().message).toContain("Unable to create branch");
|
|
196
|
-
expect(gitHubService.getFileContent).not.toHaveBeenCalled();
|
|
197
515
|
expect(gitHubService.updateFile).not.toHaveBeenCalled();
|
|
198
516
|
});
|
|
199
517
|
it("should return error when file update fails", async () => {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure authorization group configuration
|
|
3
|
+
*
|
|
4
|
+
* Defines the default Azure AD groups that the PagoPA Technology adapter
|
|
5
|
+
* keeps aligned inside the subscription authorization repository.
|
|
6
|
+
*/
|
|
7
|
+
type AzureAdGroupSpec = {
|
|
8
|
+
readonly groupName: string;
|
|
9
|
+
readonly roles: readonly string[];
|
|
10
|
+
};
|
|
11
|
+
export declare const DEFAULT_GROUP_SPECS: readonly AzureAdGroupSpec[];
|
|
12
|
+
export declare const makeAzureAdGroupName: (prefix: string, envShort: string, groupName: string) => string;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure authorization group configuration
|
|
3
|
+
*
|
|
4
|
+
* Defines the default Azure AD groups that the PagoPA Technology adapter
|
|
5
|
+
* keeps aligned inside the subscription authorization repository.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_GROUP_SPECS = [
|
|
8
|
+
{ groupName: "admin", roles: ["Owner"] },
|
|
9
|
+
{ groupName: "developers", roles: ["Owner"] },
|
|
10
|
+
{
|
|
11
|
+
groupName: "operations",
|
|
12
|
+
roles: [
|
|
13
|
+
"Reader",
|
|
14
|
+
"Monitoring Contributor",
|
|
15
|
+
"Support Request Contributor",
|
|
16
|
+
"Storage Blob Data Reader",
|
|
17
|
+
"Storage Queue Data Reader",
|
|
18
|
+
"Cosmos DB Account Reader Role",
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
{ groupName: "security", roles: ["Reader", "Support Request Contributor"] },
|
|
22
|
+
{
|
|
23
|
+
groupName: "technical-project-managers",
|
|
24
|
+
roles: ["Reader", "Monitoring Contributor", "Support Request Contributor"],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
groupName: "product-owners",
|
|
28
|
+
roles: ["Reader", "Support Request Contributor"],
|
|
29
|
+
},
|
|
30
|
+
{ groupName: "externals", roles: ["Owner"] },
|
|
31
|
+
{
|
|
32
|
+
groupName: "oncall",
|
|
33
|
+
roles: [
|
|
34
|
+
"Reader",
|
|
35
|
+
"Monitoring Contributor",
|
|
36
|
+
"Support Request Contributor",
|
|
37
|
+
"Storage Blob Data Reader",
|
|
38
|
+
"Storage Queue Data Reader",
|
|
39
|
+
"Cosmos DB Account Reader Role",
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
export const makeAzureAdGroupName = (prefix, envShort, groupName) => `${prefix}-${envShort}-adgroup-${groupName}`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PagoPA Technology Authorization Adapter
|
|
2
|
+
* PagoPA Technology Azure Authorization Adapter
|
|
3
3
|
*
|
|
4
4
|
* Implements the AuthorizationService interface for the PagoPA Azure
|
|
5
5
|
* authorization workflow. Encapsulates all platform-specific details:
|
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { AuthorizationService } from "../../domain/authorization.js";
|
|
10
10
|
import { GitHubService } from "../../domain/github.js";
|
|
11
|
-
export declare const
|
|
11
|
+
export declare const makeAzureAuthorizationService: (gitHubService: GitHubService) => AuthorizationService;
|