@backstage/plugin-catalog-backend-module-msgraph 0.0.0-nightly-202181722143

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/CHANGELOG.md ADDED
@@ -0,0 +1,77 @@
1
+ # @backstage/plugin-catalog-backend-module-msgraph
2
+
3
+ ## 0.0.0-nightly-202181722143
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+ - @backstage/plugin-catalog-backend@0.0.0-nightly-202181722143
9
+
10
+ ## 0.2.3
11
+
12
+ ### Patch Changes
13
+
14
+ - 77cdc5a84: Pass along a `UserTransformer` to the read step
15
+ - be498d22f: Pass along a `OrganizationTransformer` to the read step
16
+ - Updated dependencies
17
+ - @backstage/plugin-catalog-backend@0.13.3
18
+ - @backstage/config@0.1.7
19
+
20
+ ## 0.2.2
21
+
22
+ ### Patch Changes
23
+
24
+ - Updated dependencies
25
+ - @backstage/plugin-catalog-backend@0.13.0
26
+
27
+ ## 0.2.1
28
+
29
+ ### Patch Changes
30
+
31
+ - Updated dependencies
32
+ - @backstage/catalog-model@0.9.0
33
+ - @backstage/plugin-catalog-backend@0.12.0
34
+
35
+ ## 0.2.0
36
+
37
+ ### Minor Changes
38
+
39
+ - 115473c08: Handle error gracefully if failure occurs while loading photos using Microsoft Graph API.
40
+
41
+ This includes a breaking change: you now have to pass the `options` object to `readMicrosoftGraphUsers` and `readMicrosoftGraphOrg`.
42
+
43
+ ### Patch Changes
44
+
45
+ - Updated dependencies
46
+ - @backstage/plugin-catalog-backend@0.11.0
47
+
48
+ ## 0.1.1
49
+
50
+ ### Patch Changes
51
+
52
+ - 127048f92: Move `MicrosoftGraphOrgReaderProcessor` from `@backstage/plugin-catalog-backend`
53
+ to `@backstage/plugin-catalog-backend-module-msgraph`.
54
+
55
+ The `MicrosoftGraphOrgReaderProcessor` isn't registered by default anymore, if
56
+ you want to continue using it you have to register it manually at the catalog
57
+ builder:
58
+
59
+ 1. Add dependency to `@backstage/plugin-catalog-backend-module-msgraph` to the `package.json` of your backend.
60
+ 2. Add the processor to the catalog builder:
61
+
62
+ ```typescript
63
+ // packages/backend/src/plugins/catalog.ts
64
+ builder.addProcessor(
65
+ MicrosoftGraphOrgReaderProcessor.fromConfig(config, {
66
+ logger,
67
+ }),
68
+ );
69
+ ```
70
+
71
+ For more configuration details, see the [README of the `@backstage/plugin-catalog-backend-module-msgraph` package](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-msgraph/README.md).
72
+
73
+ - 127048f92: Allow customizations of `MicrosoftGraphOrgReaderProcessor` by passing an
74
+ optional `groupTransformer`, `userTransformer`, and `organizationTransformer`.
75
+ - Updated dependencies
76
+ - @backstage/plugin-catalog-backend@0.10.4
77
+ - @backstage/catalog-model@0.8.4
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # Catalog Backend Module for Microsoft Graph
2
+
3
+ This is an extension module to the `plugin-catalog-backend` plugin, providing a
4
+ `MicrosoftGraphOrgReaderProcessor` that can be used to ingest organization data
5
+ from the Microsoft Graph API. This processor is useful if you want to import
6
+ users and groups from Azure Active Directory or Office 365.
7
+
8
+ ## Getting Started
9
+
10
+ 1. The processor is not installed by default, therefore you have to add a
11
+ dependency to `@backstage/plugin-catalog-backend-module-msgraph` to your
12
+ backend package.
13
+
14
+ ```bash
15
+ # From your Backstage root directory
16
+ cd packages/backend
17
+ yarn add @backstage/plugin-catalog-backend-module-msgraph
18
+ ```
19
+
20
+ 2. The `MicrosoftGraphOrgReaderProcessor` is not registered by default, so you
21
+ have to register it in the catalog plugin:
22
+
23
+ ```typescript
24
+ // packages/backend/src/plugins/catalog.ts
25
+ builder.addProcessor(
26
+ MicrosoftGraphOrgReaderProcessor.fromConfig(config, {
27
+ logger,
28
+ }),
29
+ );
30
+ ```
31
+
32
+ 3. Create or use an existing App registration in the [Microsoft Azure Portal](https://portal.azure.com/).
33
+ The App registration requires at least the API permissions `Group.Read.All`,
34
+ `GroupMember.Read.All`, `User.Read` and `User.Read.All` for Microsoft Graph
35
+ (if you still run into errors about insufficient privileges, add
36
+ `Team.ReadBasic.All` and `TeamMember.Read.All` too).
37
+
38
+ 4. Configure the processor:
39
+
40
+ ```yaml
41
+ # app-config.yaml
42
+ catalog:
43
+ processors:
44
+ microsoftGraphOrg:
45
+ providers:
46
+ - target: https://graph.microsoft.com/v1.0
47
+ authority: https://login.microsoftonline.com
48
+ # If you don't know you tenantId, you can use Microsoft Graph Explorer
49
+ # to query it
50
+ tenantId: ${MICROSOFT_GRAPH_TENANT_ID}
51
+ # Client Id and Secret can be created under Certificates & secrets in
52
+ # the App registration in the Microsoft Azure Portal.
53
+ clientId: ${MICROSOFT_GRAPH_CLIENT_ID}
54
+ clientSecret: ${MICROSOFT_GRAPH_CLIENT_SECRET_TOKEN}
55
+ # Optional filter for user, see Microsoft Graph API for the syntax
56
+ # See https://docs.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0#properties
57
+ # and for the syntax https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter
58
+ userFilter: accountEnabled eq true and userType eq 'member'
59
+ # Optional filter for group, see Microsoft Graph API for the syntax
60
+ # See https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0#properties
61
+ groupFilter: securityEnabled eq false and mailEnabled eq true and groupTypes/any(c:c+eq+'Unified')
62
+ ```
63
+
64
+ 5. Add a location that ingests from Microsoft Graph:
65
+
66
+ ```yaml
67
+ # app-config.yaml
68
+ catalog:
69
+ locations:
70
+ - type: microsoft-graph-org
71
+ target: https://graph.microsoft.com/v1.0
72
+ # If you catalog doesn't allow to import Group and User entities by
73
+ # default, allow them here
74
+ rules:
75
+ - allow: [Group, User]
76
+
77
+ ```
78
+
79
+ ## Customize the Processor
80
+
81
+ In case you want to customize the ingested entities, the `MicrosoftGraphOrgReaderProcessor`
82
+ allows to pass transformers for users, groups and the organization.
83
+
84
+ 1. Create a transformer:
85
+
86
+ ```ts
87
+ export async function myGroupTransformer(
88
+ group: MicrosoftGraph.Group,
89
+ groupPhoto?: string,
90
+ ): Promise<GroupEntity | undefined> {
91
+ if (
92
+ (
93
+ group as unknown as {
94
+ creationOptions: string[];
95
+ }
96
+ ).creationOptions.includes('ProvisionGroupHomepage')
97
+ ) {
98
+ return undefined;
99
+ }
100
+
101
+ // Transformations may change namespace, change entity naming pattern, fill
102
+ // profile with more or other details...
103
+
104
+ // Create the group entity on your own, or wrap the default transformer
105
+ return await defaultGroupTransformer(group, groupPhoto);
106
+ }
107
+ ```
108
+
109
+ 2. Configure the processor with the transformer:
110
+
111
+ ```ts
112
+ builder.addProcessor(
113
+ MicrosoftGraphOrgReaderProcessor.fromConfig(config, {
114
+ logger,
115
+ groupTransformer: myGroupTransformer,
116
+ }),
117
+ );
118
+ ```
package/config.d.ts ADDED
@@ -0,0 +1,80 @@
1
+ /*
2
+ * Copyright 2020 The Backstage Authors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ export interface Config {
18
+ /**
19
+ * Configuration options for the catalog plugin.
20
+ */
21
+ catalog?: {
22
+ /**
23
+ * List of processor-specific options and attributes
24
+ */
25
+ processors?: {
26
+ /**
27
+ * MicrosoftGraphOrgReaderProcessor configuration
28
+ */
29
+ microsoftGraphOrg?: {
30
+ /**
31
+ * The configuration parameters for each single Microsoft Graph provider.
32
+ */
33
+ providers: Array<{
34
+ /**
35
+ * The prefix of the target that this matches on, e.g.
36
+ * "https://graph.microsoft.com/v1.0", with no trailing slash.
37
+ */
38
+ target: string;
39
+ /**
40
+ * The auth authority used.
41
+ *
42
+ * Default value "https://login.microsoftonline.com"
43
+ */
44
+ authority?: string;
45
+ /**
46
+ * The tenant whose org data we are interested in.
47
+ */
48
+ tenantId: string;
49
+ /**
50
+ * The OAuth client ID to use for authenticating requests.
51
+ */
52
+ clientId: string;
53
+ /**
54
+ * The OAuth client secret to use for authenticating requests.
55
+ *
56
+ * @visibility secret
57
+ */
58
+ clientSecret: string;
59
+
60
+ // TODO: Consider not making these config options and pass them in the
61
+ // constructor instead. They are probably not environment specifc, so
62
+ // they could also be configured "in code".
63
+
64
+ /**
65
+ * The filter to apply to extract users.
66
+ *
67
+ * E.g. "accountEnabled eq true and userType eq 'member'"
68
+ */
69
+ userFilter?: string;
70
+ /**
71
+ * The filter to apply to extract groups.
72
+ *
73
+ * E.g. "securityEnabled eq false and mailEnabled eq true"
74
+ */
75
+ groupFilter?: string;
76
+ }>;
77
+ };
78
+ };
79
+ };
80
+ }
@@ -0,0 +1,552 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var pluginCatalogBackend = require('@backstage/plugin-catalog-backend');
6
+ var msal = require('@azure/msal-node');
7
+ var fetch = require('cross-fetch');
8
+ var qs = require('qs');
9
+ var catalogModel = require('@backstage/catalog-model');
10
+ var limiterFactory = require('p-limit');
11
+
12
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
13
+
14
+ function _interopNamespace(e) {
15
+ if (e && e.__esModule) return e;
16
+ var n = Object.create(null);
17
+ if (e) {
18
+ Object.keys(e).forEach(function (k) {
19
+ if (k !== 'default') {
20
+ var d = Object.getOwnPropertyDescriptor(e, k);
21
+ Object.defineProperty(n, k, d.get ? d : {
22
+ enumerable: true,
23
+ get: function () {
24
+ return e[k];
25
+ }
26
+ });
27
+ }
28
+ });
29
+ }
30
+ n['default'] = e;
31
+ return Object.freeze(n);
32
+ }
33
+
34
+ var msal__namespace = /*#__PURE__*/_interopNamespace(msal);
35
+ var fetch__default = /*#__PURE__*/_interopDefaultLegacy(fetch);
36
+ var qs__default = /*#__PURE__*/_interopDefaultLegacy(qs);
37
+ var limiterFactory__default = /*#__PURE__*/_interopDefaultLegacy(limiterFactory);
38
+
39
+ class MicrosoftGraphClient {
40
+ constructor(baseUrl, pca) {
41
+ this.baseUrl = baseUrl;
42
+ this.pca = pca;
43
+ }
44
+ static create(config) {
45
+ const clientConfig = {
46
+ auth: {
47
+ clientId: config.clientId,
48
+ clientSecret: config.clientSecret,
49
+ authority: `${config.authority}/${config.tenantId}`
50
+ }
51
+ };
52
+ const pca = new msal__namespace.ConfidentialClientApplication(clientConfig);
53
+ return new MicrosoftGraphClient(config.target, pca);
54
+ }
55
+ async *requestCollection(path, query) {
56
+ let response = await this.requestApi(path, query);
57
+ for (; ; ) {
58
+ if (response.status !== 200) {
59
+ await this.handleError(path, response);
60
+ }
61
+ const result = await response.json();
62
+ const elements = result.value;
63
+ yield* elements;
64
+ if (!result["@odata.nextLink"]) {
65
+ return;
66
+ }
67
+ response = await this.requestRaw(result["@odata.nextLink"]);
68
+ }
69
+ }
70
+ async requestApi(path, query) {
71
+ var _a, _b;
72
+ const queryString = qs__default['default'].stringify({
73
+ $filter: query == null ? void 0 : query.filter,
74
+ $select: (_a = query == null ? void 0 : query.select) == null ? void 0 : _a.join(","),
75
+ $expand: (_b = query == null ? void 0 : query.expand) == null ? void 0 : _b.join(",")
76
+ }, {
77
+ addQueryPrefix: true,
78
+ encode: false
79
+ });
80
+ return await this.requestRaw(`${this.baseUrl}/${path}${queryString}`);
81
+ }
82
+ async requestRaw(url) {
83
+ const token = await this.pca.acquireTokenByClientCredential({
84
+ scopes: ["https://graph.microsoft.com/.default"]
85
+ });
86
+ if (!token) {
87
+ throw new Error("Error while requesting token for Microsoft Graph");
88
+ }
89
+ return await fetch__default['default'](url, {
90
+ headers: {
91
+ Authorization: `Bearer ${token.accessToken}`
92
+ }
93
+ });
94
+ }
95
+ async getUserProfile(userId) {
96
+ const response = await this.requestApi(`users/${userId}`);
97
+ if (response.status !== 200) {
98
+ await this.handleError("user profile", response);
99
+ }
100
+ return await response.json();
101
+ }
102
+ async getUserPhotoWithSizeLimit(userId, maxSize) {
103
+ return await this.getPhotoWithSizeLimit("users", userId, maxSize);
104
+ }
105
+ async getUserPhoto(userId, sizeId) {
106
+ return await this.getPhoto("users", userId, sizeId);
107
+ }
108
+ async *getUsers(query) {
109
+ yield* this.requestCollection(`users`, query);
110
+ }
111
+ async getGroupPhotoWithSizeLimit(groupId, maxSize) {
112
+ return await this.getPhotoWithSizeLimit("groups", groupId, maxSize);
113
+ }
114
+ async getGroupPhoto(groupId, sizeId) {
115
+ return await this.getPhoto("groups", groupId, sizeId);
116
+ }
117
+ async *getGroups(query) {
118
+ yield* this.requestCollection(`groups`, query);
119
+ }
120
+ async *getGroupMembers(groupId) {
121
+ yield* this.requestCollection(`groups/${groupId}/members`);
122
+ }
123
+ async getOrganization(tenantId) {
124
+ const response = await this.requestApi(`organization/${tenantId}`);
125
+ if (response.status !== 200) {
126
+ await this.handleError(`organization/${tenantId}`, response);
127
+ }
128
+ return await response.json();
129
+ }
130
+ async getPhotoWithSizeLimit(entityName, id, maxSize) {
131
+ const response = await this.requestApi(`${entityName}/${id}/photos`);
132
+ if (response.status === 404) {
133
+ return void 0;
134
+ } else if (response.status !== 200) {
135
+ await this.handleError(`${entityName} photos`, response);
136
+ }
137
+ const result = await response.json();
138
+ const photos = result.value;
139
+ let selectedPhoto = void 0;
140
+ for (const p of photos) {
141
+ if (!selectedPhoto || p.height >= selectedPhoto.height && p.height <= maxSize) {
142
+ selectedPhoto = p;
143
+ }
144
+ }
145
+ if (!selectedPhoto) {
146
+ return void 0;
147
+ }
148
+ return await this.getPhoto(entityName, id, selectedPhoto.id);
149
+ }
150
+ async getPhoto(entityName, id, sizeId) {
151
+ const path = sizeId ? `${entityName}/${id}/photos/${sizeId}/$value` : `${entityName}/${id}/photo/$value`;
152
+ const response = await this.requestApi(path);
153
+ if (response.status === 404) {
154
+ return void 0;
155
+ } else if (response.status !== 200) {
156
+ await this.handleError("photo", response);
157
+ }
158
+ return `data:image/jpeg;base64,${Buffer.from(await response.arrayBuffer()).toString("base64")}`;
159
+ }
160
+ async handleError(path, response) {
161
+ const result = await response.json();
162
+ const error = result.error;
163
+ throw new Error(`Error while reading ${path} from Microsoft Graph: ${error.code} - ${error.message}`);
164
+ }
165
+ }
166
+
167
+ function readMicrosoftGraphConfig(config) {
168
+ var _a, _b;
169
+ const providers = [];
170
+ const providerConfigs = (_a = config.getOptionalConfigArray("providers")) != null ? _a : [];
171
+ for (const providerConfig of providerConfigs) {
172
+ const target = providerConfig.getString("target").replace(/\/+$/, "");
173
+ const authority = ((_b = providerConfig.getOptionalString("authority")) == null ? void 0 : _b.replace(/\/+$/, "")) || "https://login.microsoftonline.com";
174
+ const tenantId = providerConfig.getString("tenantId");
175
+ const clientId = providerConfig.getString("clientId");
176
+ const clientSecret = providerConfig.getString("clientSecret");
177
+ const userFilter = providerConfig.getOptionalString("userFilter");
178
+ const groupFilter = providerConfig.getOptionalString("groupFilter");
179
+ providers.push({
180
+ target,
181
+ authority,
182
+ tenantId,
183
+ clientId,
184
+ clientSecret,
185
+ userFilter,
186
+ groupFilter
187
+ });
188
+ }
189
+ return providers;
190
+ }
191
+
192
+ const MICROSOFT_GRAPH_TENANT_ID_ANNOTATION = "graph.microsoft.com/tenant-id";
193
+ const MICROSOFT_GRAPH_GROUP_ID_ANNOTATION = "graph.microsoft.com/group-id";
194
+ const MICROSOFT_GRAPH_USER_ID_ANNOTATION = "graph.microsoft.com/user-id";
195
+
196
+ function normalizeEntityName(name) {
197
+ return name.trim().toLocaleLowerCase().replace(/[^a-zA-Z0-9_\-\.]/g, "_");
198
+ }
199
+
200
+ function buildOrgHierarchy(groups) {
201
+ const groupsByName = new Map(groups.map((g) => [g.metadata.name, g]));
202
+ for (const group of groups) {
203
+ const selfName = group.metadata.name;
204
+ const parentName = group.spec.parent;
205
+ if (parentName) {
206
+ const parent = groupsByName.get(parentName);
207
+ if (parent && !parent.spec.children.includes(selfName)) {
208
+ parent.spec.children.push(selfName);
209
+ }
210
+ }
211
+ }
212
+ for (const group of groups) {
213
+ const selfName = group.metadata.name;
214
+ for (const childName of group.spec.children) {
215
+ const child = groupsByName.get(childName);
216
+ if (child && !child.spec.parent) {
217
+ child.spec.parent = selfName;
218
+ }
219
+ }
220
+ }
221
+ }
222
+ function buildMemberOf(groups, users) {
223
+ const groupsByName = new Map(groups.map((g) => [g.metadata.name, g]));
224
+ users.forEach((user) => {
225
+ const transitiveMemberOf = new Set();
226
+ const todo = [
227
+ ...user.spec.memberOf,
228
+ ...groups.filter((g) => {
229
+ var _a;
230
+ return (_a = g.spec.members) == null ? void 0 : _a.includes(user.metadata.name);
231
+ }).map((g) => g.metadata.name)
232
+ ];
233
+ for (; ; ) {
234
+ const current = todo.pop();
235
+ if (!current) {
236
+ break;
237
+ }
238
+ if (!transitiveMemberOf.has(current)) {
239
+ transitiveMemberOf.add(current);
240
+ const group = groupsByName.get(current);
241
+ if (group == null ? void 0 : group.spec.parent) {
242
+ todo.push(group.spec.parent);
243
+ }
244
+ }
245
+ }
246
+ user.spec.memberOf = [...transitiveMemberOf];
247
+ });
248
+ }
249
+
250
+ async function defaultUserTransformer(user, userPhoto) {
251
+ if (!user.id || !user.displayName || !user.mail) {
252
+ return void 0;
253
+ }
254
+ const name = normalizeEntityName(user.mail);
255
+ const entity = {
256
+ apiVersion: "backstage.io/v1alpha1",
257
+ kind: "User",
258
+ metadata: {
259
+ name,
260
+ annotations: {
261
+ [MICROSOFT_GRAPH_USER_ID_ANNOTATION]: user.id
262
+ }
263
+ },
264
+ spec: {
265
+ profile: {
266
+ displayName: user.displayName,
267
+ email: user.mail
268
+ },
269
+ memberOf: []
270
+ }
271
+ };
272
+ if (userPhoto) {
273
+ entity.spec.profile.picture = userPhoto;
274
+ }
275
+ return entity;
276
+ }
277
+ async function readMicrosoftGraphUsers(client, options) {
278
+ var _a;
279
+ const users = [];
280
+ const limiter = limiterFactory__default['default'](10);
281
+ const transformer = (_a = options == null ? void 0 : options.transformer) != null ? _a : defaultUserTransformer;
282
+ const promises = [];
283
+ for await (const user of client.getUsers({
284
+ filter: options.userFilter
285
+ })) {
286
+ promises.push(limiter(async () => {
287
+ let userPhoto;
288
+ try {
289
+ userPhoto = await client.getUserPhotoWithSizeLimit(user.id, 120);
290
+ } catch (e) {
291
+ options.logger.warn(`Unable to load photo for ${user.id}`);
292
+ }
293
+ const entity = await transformer(user, userPhoto);
294
+ if (!entity) {
295
+ return;
296
+ }
297
+ users.push(entity);
298
+ }));
299
+ }
300
+ await Promise.all(promises);
301
+ return {users};
302
+ }
303
+ async function defaultOrganizationTransformer(organization) {
304
+ if (!organization.id || !organization.displayName) {
305
+ return void 0;
306
+ }
307
+ const name = normalizeEntityName(organization.displayName);
308
+ return {
309
+ apiVersion: "backstage.io/v1alpha1",
310
+ kind: "Group",
311
+ metadata: {
312
+ name,
313
+ description: organization.displayName,
314
+ annotations: {
315
+ [MICROSOFT_GRAPH_TENANT_ID_ANNOTATION]: organization.id
316
+ }
317
+ },
318
+ spec: {
319
+ type: "root",
320
+ profile: {
321
+ displayName: organization.displayName
322
+ },
323
+ children: []
324
+ }
325
+ };
326
+ }
327
+ async function readMicrosoftGraphOrganization(client, tenantId, options) {
328
+ var _a;
329
+ const organization = await client.getOrganization(tenantId);
330
+ const transformer = (_a = options == null ? void 0 : options.transformer) != null ? _a : defaultOrganizationTransformer;
331
+ const rootGroup = await transformer(organization);
332
+ return {rootGroup};
333
+ }
334
+ async function defaultGroupTransformer(group, groupPhoto) {
335
+ if (!group.id || !group.displayName) {
336
+ return void 0;
337
+ }
338
+ const name = normalizeEntityName(group.mailNickname || group.displayName);
339
+ const entity = {
340
+ apiVersion: "backstage.io/v1alpha1",
341
+ kind: "Group",
342
+ metadata: {
343
+ name,
344
+ annotations: {
345
+ [MICROSOFT_GRAPH_GROUP_ID_ANNOTATION]: group.id
346
+ }
347
+ },
348
+ spec: {
349
+ type: "team",
350
+ profile: {},
351
+ children: []
352
+ }
353
+ };
354
+ if (group.description) {
355
+ entity.metadata.description = group.description;
356
+ }
357
+ if (group.displayName) {
358
+ entity.spec.profile.displayName = group.displayName;
359
+ }
360
+ if (group.mail) {
361
+ entity.spec.profile.email = group.mail;
362
+ }
363
+ if (groupPhoto) {
364
+ entity.spec.profile.picture = groupPhoto;
365
+ }
366
+ return entity;
367
+ }
368
+ async function readMicrosoftGraphGroups(client, tenantId, options) {
369
+ var _a;
370
+ const groups = [];
371
+ const groupMember = new Map();
372
+ const groupMemberOf = new Map();
373
+ const limiter = limiterFactory__default['default'](10);
374
+ const {rootGroup} = await readMicrosoftGraphOrganization(client, tenantId, {
375
+ transformer: options == null ? void 0 : options.organizationTransformer
376
+ });
377
+ if (rootGroup) {
378
+ groupMember.set(rootGroup.metadata.name, new Set());
379
+ groups.push(rootGroup);
380
+ }
381
+ const transformer = (_a = options == null ? void 0 : options.groupTransformer) != null ? _a : defaultGroupTransformer;
382
+ const promises = [];
383
+ for await (const group of client.getGroups({
384
+ filter: options == null ? void 0 : options.groupFilter
385
+ })) {
386
+ promises.push(limiter(async () => {
387
+ const entity = await transformer(group);
388
+ if (!entity) {
389
+ return;
390
+ }
391
+ for await (const member of client.getGroupMembers(group.id)) {
392
+ if (!member.id) {
393
+ continue;
394
+ }
395
+ if (member["@odata.type"] === "#microsoft.graph.user") {
396
+ ensureItem(groupMemberOf, member.id, group.id);
397
+ }
398
+ if (member["@odata.type"] === "#microsoft.graph.group") {
399
+ ensureItem(groupMember, group.id, member.id);
400
+ }
401
+ }
402
+ groups.push(entity);
403
+ }));
404
+ }
405
+ await Promise.all(promises);
406
+ return {
407
+ groups,
408
+ rootGroup,
409
+ groupMember,
410
+ groupMemberOf
411
+ };
412
+ }
413
+ function resolveRelations(rootGroup, groups, users, groupMember, groupMemberOf) {
414
+ const groupMap = new Map();
415
+ for (const group of groups) {
416
+ if (group.metadata.annotations[MICROSOFT_GRAPH_GROUP_ID_ANNOTATION]) {
417
+ groupMap.set(group.metadata.annotations[MICROSOFT_GRAPH_GROUP_ID_ANNOTATION], group);
418
+ }
419
+ if (group.metadata.annotations[MICROSOFT_GRAPH_TENANT_ID_ANNOTATION]) {
420
+ groupMap.set(group.metadata.annotations[MICROSOFT_GRAPH_TENANT_ID_ANNOTATION], group);
421
+ }
422
+ }
423
+ const parentGroups = new Map();
424
+ groupMember.forEach((members, groupId) => members.forEach((m) => ensureItem(parentGroups, m, groupId)));
425
+ if (rootGroup) {
426
+ const tenantId = rootGroup.metadata.annotations[MICROSOFT_GRAPH_TENANT_ID_ANNOTATION];
427
+ groups.forEach((group) => {
428
+ const groupId = group.metadata.annotations[MICROSOFT_GRAPH_GROUP_ID_ANNOTATION];
429
+ if (!groupId) {
430
+ return;
431
+ }
432
+ if (retrieveItems(parentGroups, groupId).size === 0) {
433
+ ensureItem(parentGroups, groupId, tenantId);
434
+ ensureItem(groupMember, tenantId, groupId);
435
+ }
436
+ });
437
+ }
438
+ groups.forEach((group) => {
439
+ var _a;
440
+ const id = (_a = group.metadata.annotations[MICROSOFT_GRAPH_GROUP_ID_ANNOTATION]) != null ? _a : group.metadata.annotations[MICROSOFT_GRAPH_TENANT_ID_ANNOTATION];
441
+ retrieveItems(groupMember, id).forEach((m) => {
442
+ const childGroup = groupMap.get(m);
443
+ if (childGroup) {
444
+ group.spec.children.push(catalogModel.stringifyEntityRef(childGroup));
445
+ }
446
+ });
447
+ retrieveItems(parentGroups, id).forEach((p) => {
448
+ const parentGroup = groupMap.get(p);
449
+ if (parentGroup) {
450
+ group.spec.parent = catalogModel.stringifyEntityRef(parentGroup);
451
+ }
452
+ });
453
+ });
454
+ buildOrgHierarchy(groups);
455
+ users.forEach((user) => {
456
+ const id = user.metadata.annotations[MICROSOFT_GRAPH_USER_ID_ANNOTATION];
457
+ retrieveItems(groupMemberOf, id).forEach((p) => {
458
+ const parentGroup = groupMap.get(p);
459
+ if (parentGroup) {
460
+ user.spec.memberOf.push(catalogModel.stringifyEntityRef(parentGroup));
461
+ }
462
+ });
463
+ });
464
+ buildMemberOf(groups, users);
465
+ }
466
+ async function readMicrosoftGraphOrg(client, tenantId, options) {
467
+ const {users} = await readMicrosoftGraphUsers(client, {
468
+ userFilter: options.userFilter,
469
+ transformer: options.userTransformer,
470
+ logger: options.logger
471
+ });
472
+ const {groups, rootGroup, groupMember, groupMemberOf} = await readMicrosoftGraphGroups(client, tenantId, {
473
+ groupFilter: options == null ? void 0 : options.groupFilter,
474
+ groupTransformer: options == null ? void 0 : options.groupTransformer,
475
+ organizationTransformer: options == null ? void 0 : options.organizationTransformer
476
+ });
477
+ resolveRelations(rootGroup, groups, users, groupMember, groupMemberOf);
478
+ users.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
479
+ groups.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
480
+ return {users, groups};
481
+ }
482
+ function ensureItem(target, key, value) {
483
+ let set = target.get(key);
484
+ if (!set) {
485
+ set = new Set();
486
+ target.set(key, set);
487
+ }
488
+ set.add(value);
489
+ }
490
+ function retrieveItems(target, key) {
491
+ var _a;
492
+ return (_a = target.get(key)) != null ? _a : new Set();
493
+ }
494
+
495
+ class MicrosoftGraphOrgReaderProcessor {
496
+ static fromConfig(config, options) {
497
+ const c = config.getOptionalConfig("catalog.processors.microsoftGraphOrg");
498
+ return new MicrosoftGraphOrgReaderProcessor({
499
+ ...options,
500
+ providers: c ? readMicrosoftGraphConfig(c) : []
501
+ });
502
+ }
503
+ constructor(options) {
504
+ this.providers = options.providers;
505
+ this.logger = options.logger;
506
+ this.userTransformer = options.userTransformer;
507
+ this.groupTransformer = options.groupTransformer;
508
+ this.organizationTransformer = options.organizationTransformer;
509
+ }
510
+ async readLocation(location, _optional, emit) {
511
+ if (location.type !== "microsoft-graph-org") {
512
+ return false;
513
+ }
514
+ const provider = this.providers.find((p) => location.target.startsWith(p.target));
515
+ if (!provider) {
516
+ throw new Error(`There is no Microsoft Graph Org provider that matches ${location.target}. Please add a configuration entry for it under catalog.processors.microsoftGraphOrg.providers.`);
517
+ }
518
+ const startTimestamp = Date.now();
519
+ this.logger.info("Reading Microsoft Graph users and groups");
520
+ const client = MicrosoftGraphClient.create(provider);
521
+ const {users, groups} = await readMicrosoftGraphOrg(client, provider.tenantId, {
522
+ userFilter: provider.userFilter,
523
+ groupFilter: provider.groupFilter,
524
+ userTransformer: this.userTransformer,
525
+ groupTransformer: this.groupTransformer,
526
+ organizationTransformer: this.organizationTransformer,
527
+ logger: this.logger
528
+ });
529
+ const duration = ((Date.now() - startTimestamp) / 1e3).toFixed(1);
530
+ this.logger.debug(`Read ${users.length} users and ${groups.length} groups from Microsoft Graph in ${duration} seconds`);
531
+ for (const group of groups) {
532
+ emit(pluginCatalogBackend.results.entity(location, group));
533
+ }
534
+ for (const user of users) {
535
+ emit(pluginCatalogBackend.results.entity(location, user));
536
+ }
537
+ return true;
538
+ }
539
+ }
540
+
541
+ exports.MICROSOFT_GRAPH_GROUP_ID_ANNOTATION = MICROSOFT_GRAPH_GROUP_ID_ANNOTATION;
542
+ exports.MICROSOFT_GRAPH_TENANT_ID_ANNOTATION = MICROSOFT_GRAPH_TENANT_ID_ANNOTATION;
543
+ exports.MICROSOFT_GRAPH_USER_ID_ANNOTATION = MICROSOFT_GRAPH_USER_ID_ANNOTATION;
544
+ exports.MicrosoftGraphClient = MicrosoftGraphClient;
545
+ exports.MicrosoftGraphOrgReaderProcessor = MicrosoftGraphOrgReaderProcessor;
546
+ exports.defaultGroupTransformer = defaultGroupTransformer;
547
+ exports.defaultOrganizationTransformer = defaultOrganizationTransformer;
548
+ exports.defaultUserTransformer = defaultUserTransformer;
549
+ exports.normalizeEntityName = normalizeEntityName;
550
+ exports.readMicrosoftGraphConfig = readMicrosoftGraphConfig;
551
+ exports.readMicrosoftGraphOrg = readMicrosoftGraphOrg;
552
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":["../src/microsoftGraph/client.ts","../src/microsoftGraph/config.ts","../src/microsoftGraph/constants.ts","../src/microsoftGraph/helper.ts","../src/microsoftGraph/org.ts","../src/microsoftGraph/read.ts","../src/processors/MicrosoftGraphOrgReaderProcessor.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as msal from '@azure/msal-node';\nimport * as MicrosoftGraph from '@microsoft/microsoft-graph-types';\nimport fetch from 'cross-fetch';\nimport qs from 'qs';\nimport { MicrosoftGraphProviderConfig } from './config';\n\nexport type ODataQuery = {\n filter?: string;\n expand?: string[];\n select?: string[];\n};\n\nexport type GroupMember =\n | (MicrosoftGraph.Group & { '@odata.type': '#microsoft.graph.user' })\n | (MicrosoftGraph.User & { '@odata.type': '#microsoft.graph.group' });\n\nexport class MicrosoftGraphClient {\n static create(config: MicrosoftGraphProviderConfig): MicrosoftGraphClient {\n const clientConfig: msal.Configuration = {\n auth: {\n clientId: config.clientId,\n clientSecret: config.clientSecret,\n authority: `${config.authority}/${config.tenantId}`,\n },\n };\n const pca = new msal.ConfidentialClientApplication(clientConfig);\n return new MicrosoftGraphClient(config.target, pca);\n }\n\n constructor(\n private readonly baseUrl: string,\n private readonly pca: msal.ConfidentialClientApplication,\n ) {}\n\n async *requestCollection<T>(\n path: string,\n query?: ODataQuery,\n ): AsyncIterable<T> {\n let response = await this.requestApi(path, query);\n\n for (;;) {\n if (response.status !== 200) {\n await this.handleError(path, response);\n }\n\n const result = await response.json();\n const elements: T[] = result.value;\n\n yield* elements;\n\n // Follow cursor to the next page if one is available\n if (!result['@odata.nextLink']) {\n return;\n }\n\n response = await this.requestRaw(result['@odata.nextLink']);\n }\n }\n\n async requestApi(path: string, query?: ODataQuery): Promise<Response> {\n const queryString = qs.stringify(\n {\n $filter: query?.filter,\n $select: query?.select?.join(','),\n $expand: query?.expand?.join(','),\n },\n {\n addQueryPrefix: true,\n // Microsoft Graph doesn't like an encoded query string\n encode: false,\n },\n );\n\n return await this.requestRaw(`${this.baseUrl}/${path}${queryString}`);\n }\n\n async requestRaw(url: string): Promise<Response> {\n // Make sure that we always have a valid access token (might be cached)\n const token = await this.pca.acquireTokenByClientCredential({\n scopes: ['https://graph.microsoft.com/.default'],\n });\n\n if (!token) {\n throw new Error('Error while requesting token for Microsoft Graph');\n }\n\n return await fetch(url, {\n headers: {\n Authorization: `Bearer ${token.accessToken}`,\n },\n });\n }\n\n async getUserProfile(userId: string): Promise<MicrosoftGraph.User> {\n const response = await this.requestApi(`users/${userId}`);\n\n if (response.status !== 200) {\n await this.handleError('user profile', response);\n }\n\n return await response.json();\n }\n\n async getUserPhotoWithSizeLimit(\n userId: string,\n maxSize: number,\n ): Promise<string | undefined> {\n return await this.getPhotoWithSizeLimit('users', userId, maxSize);\n }\n\n async getUserPhoto(\n userId: string,\n sizeId?: string,\n ): Promise<string | undefined> {\n return await this.getPhoto('users', userId, sizeId);\n }\n\n async *getUsers(query?: ODataQuery): AsyncIterable<MicrosoftGraph.User> {\n yield* this.requestCollection<MicrosoftGraph.User>(`users`, query);\n }\n\n async getGroupPhotoWithSizeLimit(\n groupId: string,\n maxSize: number,\n ): Promise<string | undefined> {\n return await this.getPhotoWithSizeLimit('groups', groupId, maxSize);\n }\n\n async getGroupPhoto(\n groupId: string,\n sizeId?: string,\n ): Promise<string | undefined> {\n return await this.getPhoto('groups', groupId, sizeId);\n }\n\n async *getGroups(query?: ODataQuery): AsyncIterable<MicrosoftGraph.Group> {\n yield* this.requestCollection<MicrosoftGraph.Group>(`groups`, query);\n }\n\n async *getGroupMembers(groupId: string): AsyncIterable<GroupMember> {\n yield* this.requestCollection<GroupMember>(`groups/${groupId}/members`);\n }\n\n async getOrganization(\n tenantId: string,\n ): Promise<MicrosoftGraph.Organization> {\n const response = await this.requestApi(`organization/${tenantId}`);\n\n if (response.status !== 200) {\n await this.handleError(`organization/${tenantId}`, response);\n }\n\n return await response.json();\n }\n\n private async getPhotoWithSizeLimit(\n entityName: string,\n id: string,\n maxSize: number,\n ): Promise<string | undefined> {\n const response = await this.requestApi(`${entityName}/${id}/photos`);\n\n if (response.status === 404) {\n return undefined;\n } else if (response.status !== 200) {\n await this.handleError(`${entityName} photos`, response);\n }\n\n const result = await response.json();\n const photos = result.value as MicrosoftGraph.ProfilePhoto[];\n let selectedPhoto: MicrosoftGraph.ProfilePhoto | undefined = undefined;\n\n // Find the biggest picture that is smaller than the max size\n for (const p of photos) {\n if (\n !selectedPhoto ||\n (p.height! >= selectedPhoto.height! && p.height! <= maxSize)\n ) {\n selectedPhoto = p;\n }\n }\n\n if (!selectedPhoto) {\n return undefined;\n }\n\n return await this.getPhoto(entityName, id, selectedPhoto.id!);\n }\n\n private async getPhoto(\n entityName: string,\n id: string,\n sizeId?: string,\n ): Promise<string | undefined> {\n const path = sizeId\n ? `${entityName}/${id}/photos/${sizeId}/$value`\n : `${entityName}/${id}/photo/$value`;\n const response = await this.requestApi(path);\n\n if (response.status === 404) {\n return undefined;\n } else if (response.status !== 200) {\n await this.handleError('photo', response);\n }\n\n return `data:image/jpeg;base64,${Buffer.from(\n await response.arrayBuffer(),\n ).toString('base64')}`;\n }\n\n private async handleError(path: string, response: Response): Promise<void> {\n const result = await response.json();\n const error = result.error as MicrosoftGraph.PublicError;\n\n throw new Error(\n `Error while reading ${path} from Microsoft Graph: ${error.code} - ${error.message}`,\n );\n }\n}\n","/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Config } from '@backstage/config';\n\n/**\n * The configuration parameters for a single Microsoft Graph provider.\n */\nexport type MicrosoftGraphProviderConfig = {\n /**\n * The prefix of the target that this matches on, e.g.\n * \"https://graph.microsoft.com/v1.0\", with no trailing slash.\n */\n target: string;\n /**\n * The auth authority used.\n *\n * E.g. \"https://login.microsoftonline.com\"\n */\n authority?: string;\n /**\n * The tenant whose org data we are interested in.\n */\n tenantId: string;\n /**\n * The OAuth client ID to use for authenticating requests.\n */\n clientId: string;\n /**\n * The OAuth client secret to use for authenticating requests.\n *\n * @visibility secret\n */\n clientSecret: string;\n /**\n * The filter to apply to extract users.\n *\n * E.g. \"accountEnabled eq true and userType eq 'member'\"\n */\n userFilter?: string;\n /**\n * The filter to apply to extract groups.\n *\n * E.g. \"securityEnabled eq false and mailEnabled eq true\"\n */\n groupFilter?: string;\n};\n\nexport function readMicrosoftGraphConfig(\n config: Config,\n): MicrosoftGraphProviderConfig[] {\n const providers: MicrosoftGraphProviderConfig[] = [];\n const providerConfigs = config.getOptionalConfigArray('providers') ?? [];\n\n for (const providerConfig of providerConfigs) {\n const target = providerConfig.getString('target').replace(/\\/+$/, '');\n const authority =\n providerConfig.getOptionalString('authority')?.replace(/\\/+$/, '') ||\n 'https://login.microsoftonline.com';\n const tenantId = providerConfig.getString('tenantId');\n const clientId = providerConfig.getString('clientId');\n const clientSecret = providerConfig.getString('clientSecret');\n const userFilter = providerConfig.getOptionalString('userFilter');\n const groupFilter = providerConfig.getOptionalString('groupFilter');\n\n providers.push({\n target,\n authority,\n tenantId,\n clientId,\n clientSecret,\n userFilter,\n groupFilter,\n });\n }\n\n return providers;\n}\n","/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The tenant id used by the Microsoft Graph API\n */\nexport const MICROSOFT_GRAPH_TENANT_ID_ANNOTATION =\n 'graph.microsoft.com/tenant-id';\n\n/**\n * The group id used by the Microsoft Graph API\n */\nexport const MICROSOFT_GRAPH_GROUP_ID_ANNOTATION =\n 'graph.microsoft.com/group-id';\n\n/**\n * The user id used by the Microsoft Graph API\n */\nexport const MICROSOFT_GRAPH_USER_ID_ANNOTATION = 'graph.microsoft.com/user-id';\n","/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport function normalizeEntityName(name: string): string {\n return name\n .trim()\n .toLocaleLowerCase()\n .replace(/[^a-zA-Z0-9_\\-\\.]/g, '_');\n}\n","/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { GroupEntity, UserEntity } from '@backstage/catalog-model';\n\n// TODO: Copied from plugin-catalog-backend, but we could also export them from\n// there. Or move them to catalog-model.\n\nexport function buildOrgHierarchy(groups: GroupEntity[]) {\n const groupsByName = new Map(groups.map(g => [g.metadata.name, g]));\n\n //\n // Make sure that g.parent.children contain g\n //\n\n for (const group of groups) {\n const selfName = group.metadata.name;\n const parentName = group.spec.parent;\n if (parentName) {\n const parent = groupsByName.get(parentName);\n if (parent && !parent.spec.children.includes(selfName)) {\n parent.spec.children.push(selfName);\n }\n }\n }\n\n //\n // Make sure that g.children.parent is g\n //\n\n for (const group of groups) {\n const selfName = group.metadata.name;\n for (const childName of group.spec.children) {\n const child = groupsByName.get(childName);\n if (child && !child.spec.parent) {\n child.spec.parent = selfName;\n }\n }\n }\n}\n\n// Ensure that users have their transitive group memberships. Requires that\n// the groups were previously processed with buildOrgHierarchy()\nexport function buildMemberOf(groups: GroupEntity[], users: UserEntity[]) {\n const groupsByName = new Map(groups.map(g => [g.metadata.name, g]));\n\n users.forEach(user => {\n const transitiveMemberOf = new Set<string>();\n\n const todo = [\n ...user.spec.memberOf,\n ...groups\n .filter(g => g.spec.members?.includes(user.metadata.name))\n .map(g => g.metadata.name),\n ];\n\n for (;;) {\n const current = todo.pop();\n if (!current) {\n break;\n }\n\n if (!transitiveMemberOf.has(current)) {\n transitiveMemberOf.add(current);\n const group = groupsByName.get(current);\n if (group?.spec.parent) {\n todo.push(group.spec.parent);\n }\n }\n }\n\n user.spec.memberOf = [...transitiveMemberOf];\n });\n}\n","/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n GroupEntity,\n stringifyEntityRef,\n UserEntity,\n} from '@backstage/catalog-model';\nimport * as MicrosoftGraph from '@microsoft/microsoft-graph-types';\nimport limiterFactory from 'p-limit';\nimport { Logger } from 'winston';\nimport { MicrosoftGraphClient } from './client';\nimport {\n MICROSOFT_GRAPH_GROUP_ID_ANNOTATION,\n MICROSOFT_GRAPH_TENANT_ID_ANNOTATION,\n MICROSOFT_GRAPH_USER_ID_ANNOTATION,\n} from './constants';\nimport { normalizeEntityName } from './helper';\nimport { buildMemberOf, buildOrgHierarchy } from './org';\nimport {\n GroupTransformer,\n OrganizationTransformer,\n UserTransformer,\n} from './types';\n\nexport async function defaultUserTransformer(\n user: MicrosoftGraph.User,\n userPhoto?: string,\n): Promise<UserEntity | undefined> {\n if (!user.id || !user.displayName || !user.mail) {\n return undefined;\n }\n\n const name = normalizeEntityName(user.mail);\n const entity: UserEntity = {\n apiVersion: 'backstage.io/v1alpha1',\n kind: 'User',\n metadata: {\n name,\n annotations: {\n [MICROSOFT_GRAPH_USER_ID_ANNOTATION]: user.id!,\n },\n },\n spec: {\n profile: {\n displayName: user.displayName!,\n email: user.mail!,\n\n // TODO: Additional fields?\n // jobTitle: user.jobTitle || undefined,\n // officeLocation: user.officeLocation || undefined,\n // mobilePhone: user.mobilePhone || undefined,\n },\n memberOf: [],\n },\n };\n\n if (userPhoto) {\n entity.spec.profile!.picture = userPhoto;\n }\n\n return entity;\n}\n\nexport async function readMicrosoftGraphUsers(\n client: MicrosoftGraphClient,\n options: {\n userFilter?: string;\n transformer?: UserTransformer;\n logger: Logger;\n },\n): Promise<{\n users: UserEntity[]; // With all relations empty\n}> {\n const users: UserEntity[] = [];\n const limiter = limiterFactory(10);\n\n const transformer = options?.transformer ?? defaultUserTransformer;\n const promises: Promise<void>[] = [];\n\n for await (const user of client.getUsers({\n filter: options.userFilter,\n })) {\n // Process all users in parallel, otherwise it can take quite some time\n promises.push(\n limiter(async () => {\n let userPhoto;\n try {\n userPhoto = await client.getUserPhotoWithSizeLimit(\n user.id!,\n // We are limiting the photo size, as users with full resolution photos\n // can make the Backstage API slow\n 120,\n );\n } catch (e) {\n options.logger.warn(`Unable to load photo for ${user.id}`);\n }\n\n const entity = await transformer(user, userPhoto);\n\n if (!entity) {\n return;\n }\n\n users.push(entity);\n }),\n );\n }\n\n // Wait for all users and photos to be downloaded\n await Promise.all(promises);\n\n return { users };\n}\n\nexport async function defaultOrganizationTransformer(\n organization: MicrosoftGraph.Organization,\n): Promise<GroupEntity | undefined> {\n if (!organization.id || !organization.displayName) {\n return undefined;\n }\n\n const name = normalizeEntityName(organization.displayName!);\n return {\n apiVersion: 'backstage.io/v1alpha1',\n kind: 'Group',\n metadata: {\n name: name,\n description: organization.displayName!,\n annotations: {\n [MICROSOFT_GRAPH_TENANT_ID_ANNOTATION]: organization.id!,\n },\n },\n spec: {\n type: 'root',\n profile: {\n displayName: organization.displayName!,\n },\n children: [],\n },\n };\n}\n\nexport async function readMicrosoftGraphOrganization(\n client: MicrosoftGraphClient,\n tenantId: string,\n options?: { transformer?: OrganizationTransformer },\n): Promise<{\n rootGroup?: GroupEntity; // With all relations empty\n}> {\n // For now we expect a single root organization\n const organization = await client.getOrganization(tenantId);\n const transformer = options?.transformer ?? defaultOrganizationTransformer;\n const rootGroup = await transformer(organization);\n\n return { rootGroup };\n}\n\nexport async function defaultGroupTransformer(\n group: MicrosoftGraph.Group,\n groupPhoto?: string,\n): Promise<GroupEntity | undefined> {\n if (!group.id || !group.displayName) {\n return undefined;\n }\n\n const name = normalizeEntityName(group.mailNickname || group.displayName);\n const entity: GroupEntity = {\n apiVersion: 'backstage.io/v1alpha1',\n kind: 'Group',\n metadata: {\n name: name,\n annotations: {\n [MICROSOFT_GRAPH_GROUP_ID_ANNOTATION]: group.id,\n },\n },\n spec: {\n type: 'team',\n profile: {},\n children: [],\n },\n };\n\n if (group.description) {\n entity.metadata.description = group.description;\n }\n if (group.displayName) {\n entity.spec.profile!.displayName = group.displayName;\n }\n if (group.mail) {\n entity.spec.profile!.email = group.mail;\n }\n if (groupPhoto) {\n entity.spec.profile!.picture = groupPhoto;\n }\n\n return entity;\n}\n\nexport async function readMicrosoftGraphGroups(\n client: MicrosoftGraphClient,\n tenantId: string,\n options?: {\n groupFilter?: string;\n groupTransformer?: GroupTransformer;\n organizationTransformer?: OrganizationTransformer;\n },\n): Promise<{\n groups: GroupEntity[]; // With all relations empty\n rootGroup: GroupEntity | undefined; // With all relations empty\n groupMember: Map<string, Set<string>>;\n groupMemberOf: Map<string, Set<string>>;\n}> {\n const groups: GroupEntity[] = [];\n const groupMember: Map<string, Set<string>> = new Map();\n const groupMemberOf: Map<string, Set<string>> = new Map();\n const limiter = limiterFactory(10);\n\n const { rootGroup } = await readMicrosoftGraphOrganization(client, tenantId, {\n transformer: options?.organizationTransformer,\n });\n if (rootGroup) {\n groupMember.set(rootGroup.metadata.name, new Set<string>());\n groups.push(rootGroup);\n }\n\n const transformer = options?.groupTransformer ?? defaultGroupTransformer;\n const promises: Promise<void>[] = [];\n\n for await (const group of client.getGroups({\n filter: options?.groupFilter,\n })) {\n // Process all groups in parallel, otherwise it can take quite some time\n promises.push(\n limiter(async () => {\n // TODO: Loading groups photos doesn't work right now as Microsoft Graph\n // doesn't allows this yet: https://microsoftgraph.uservoice.com/forums/920506-microsoft-graph-feature-requests/suggestions/37884922-allow-application-to-set-or-update-a-group-s-photo\n /* const groupPhoto = await client.getGroupPhotoWithSizeLimit(\n group.id!,\n // We are limiting the photo size, as groups with full resolution photos\n // can make the Backstage API slow\n 120,\n );*/\n\n const entity = await transformer(group /* , groupPhoto*/);\n\n if (!entity) {\n return;\n }\n\n for await (const member of client.getGroupMembers(group.id!)) {\n if (!member.id) {\n continue;\n }\n\n if (member['@odata.type'] === '#microsoft.graph.user') {\n ensureItem(groupMemberOf, member.id, group.id!);\n }\n\n if (member['@odata.type'] === '#microsoft.graph.group') {\n ensureItem(groupMember, group.id!, member.id);\n }\n }\n\n groups.push(entity);\n }),\n );\n }\n\n // Wait for all group members and photos to be loaded\n await Promise.all(promises);\n\n return {\n groups,\n rootGroup,\n groupMember,\n groupMemberOf,\n };\n}\n\nexport function resolveRelations(\n rootGroup: GroupEntity | undefined,\n groups: GroupEntity[],\n users: UserEntity[],\n groupMember: Map<string, Set<string>>,\n groupMemberOf: Map<string, Set<string>>,\n) {\n // Build reference lookup tables, we reference them by the id the the graph\n const groupMap: Map<string, GroupEntity> = new Map(); // by group-id or tenant-id\n\n for (const group of groups) {\n if (group.metadata.annotations![MICROSOFT_GRAPH_GROUP_ID_ANNOTATION]) {\n groupMap.set(\n group.metadata.annotations![MICROSOFT_GRAPH_GROUP_ID_ANNOTATION],\n group,\n );\n }\n if (group.metadata.annotations![MICROSOFT_GRAPH_TENANT_ID_ANNOTATION]) {\n groupMap.set(\n group.metadata.annotations![MICROSOFT_GRAPH_TENANT_ID_ANNOTATION],\n group,\n );\n }\n }\n\n // Resolve all member relationships into the reverse direction\n const parentGroups = new Map<string, Set<string>>();\n\n groupMember.forEach((members, groupId) =>\n members.forEach(m => ensureItem(parentGroups, m, groupId)),\n );\n\n // Make sure every group (except root) has at least one parent. If the parent is missing, add the root.\n if (rootGroup) {\n const tenantId =\n rootGroup.metadata.annotations![MICROSOFT_GRAPH_TENANT_ID_ANNOTATION];\n\n groups.forEach(group => {\n const groupId =\n group.metadata.annotations![MICROSOFT_GRAPH_GROUP_ID_ANNOTATION];\n\n if (!groupId) {\n return;\n }\n\n if (retrieveItems(parentGroups, groupId).size === 0) {\n ensureItem(parentGroups, groupId, tenantId);\n ensureItem(groupMember, tenantId, groupId);\n }\n });\n }\n\n groups.forEach(group => {\n const id =\n group.metadata.annotations![MICROSOFT_GRAPH_GROUP_ID_ANNOTATION] ??\n group.metadata.annotations![MICROSOFT_GRAPH_TENANT_ID_ANNOTATION];\n\n retrieveItems(groupMember, id).forEach(m => {\n const childGroup = groupMap.get(m);\n if (childGroup) {\n group.spec.children.push(stringifyEntityRef(childGroup));\n }\n });\n\n retrieveItems(parentGroups, id).forEach(p => {\n const parentGroup = groupMap.get(p);\n if (parentGroup) {\n // TODO: Only having a single parent group might not match every companies model, but fine for now.\n group.spec.parent = stringifyEntityRef(parentGroup);\n }\n });\n });\n\n // Make sure that all groups have proper parents and children\n buildOrgHierarchy(groups);\n\n // Set relations for all users\n users.forEach(user => {\n const id = user.metadata.annotations![MICROSOFT_GRAPH_USER_ID_ANNOTATION];\n\n retrieveItems(groupMemberOf, id).forEach(p => {\n const parentGroup = groupMap.get(p);\n if (parentGroup) {\n user.spec.memberOf.push(stringifyEntityRef(parentGroup));\n }\n });\n });\n\n // Make sure all transitive memberships are available\n buildMemberOf(groups, users);\n}\n\nexport async function readMicrosoftGraphOrg(\n client: MicrosoftGraphClient,\n tenantId: string,\n options: {\n userFilter?: string;\n groupFilter?: string;\n userTransformer?: UserTransformer;\n groupTransformer?: GroupTransformer;\n organizationTransformer?: OrganizationTransformer;\n logger: Logger;\n },\n): Promise<{ users: UserEntity[]; groups: GroupEntity[] }> {\n const { users } = await readMicrosoftGraphUsers(client, {\n userFilter: options.userFilter,\n transformer: options.userTransformer,\n logger: options.logger,\n });\n const { groups, rootGroup, groupMember, groupMemberOf } =\n await readMicrosoftGraphGroups(client, tenantId, {\n groupFilter: options?.groupFilter,\n groupTransformer: options?.groupTransformer,\n organizationTransformer: options?.organizationTransformer,\n });\n\n resolveRelations(rootGroup, groups, users, groupMember, groupMemberOf);\n users.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));\n groups.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));\n\n return { users, groups };\n}\n\nfunction ensureItem(\n target: Map<string, Set<string>>,\n key: string,\n value: string,\n) {\n let set = target.get(key);\n if (!set) {\n set = new Set();\n target.set(key, set);\n }\n set!.add(value);\n}\n\nfunction retrieveItems(\n target: Map<string, Set<string>>,\n key: string,\n): Set<string> {\n return target.get(key) ?? new Set();\n}\n","/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { LocationSpec } from '@backstage/catalog-model';\nimport { Config } from '@backstage/config';\nimport {\n CatalogProcessor,\n CatalogProcessorEmit,\n results,\n} from '@backstage/plugin-catalog-backend';\nimport { Logger } from 'winston';\nimport {\n GroupTransformer,\n MicrosoftGraphClient,\n MicrosoftGraphProviderConfig,\n OrganizationTransformer,\n readMicrosoftGraphConfig,\n readMicrosoftGraphOrg,\n UserTransformer,\n} from '../microsoftGraph';\n\n/**\n * Extracts teams and users out of a the Microsoft Graph API.\n */\nexport class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor {\n private readonly providers: MicrosoftGraphProviderConfig[];\n private readonly logger: Logger;\n private readonly userTransformer?: UserTransformer;\n private readonly groupTransformer?: GroupTransformer;\n private readonly organizationTransformer?: OrganizationTransformer;\n\n static fromConfig(\n config: Config,\n options: {\n logger: Logger;\n userTransformer?: UserTransformer;\n groupTransformer?: GroupTransformer;\n organizationTransformer?: OrganizationTransformer;\n },\n ) {\n const c = config.getOptionalConfig('catalog.processors.microsoftGraphOrg');\n return new MicrosoftGraphOrgReaderProcessor({\n ...options,\n providers: c ? readMicrosoftGraphConfig(c) : [],\n });\n }\n\n constructor(options: {\n providers: MicrosoftGraphProviderConfig[];\n logger: Logger;\n userTransformer?: UserTransformer;\n groupTransformer?: GroupTransformer;\n organizationTransformer?: OrganizationTransformer;\n }) {\n this.providers = options.providers;\n this.logger = options.logger;\n this.userTransformer = options.userTransformer;\n this.groupTransformer = options.groupTransformer;\n this.organizationTransformer = options.organizationTransformer;\n }\n\n async readLocation(\n location: LocationSpec,\n _optional: boolean,\n emit: CatalogProcessorEmit,\n ): Promise<boolean> {\n if (location.type !== 'microsoft-graph-org') {\n return false;\n }\n\n const provider = this.providers.find(p =>\n location.target.startsWith(p.target),\n );\n if (!provider) {\n throw new Error(\n `There is no Microsoft Graph Org provider that matches ${location.target}. Please add a configuration entry for it under catalog.processors.microsoftGraphOrg.providers.`,\n );\n }\n\n // Read out all of the raw data\n const startTimestamp = Date.now();\n this.logger.info('Reading Microsoft Graph users and groups');\n\n // We create a client each time as we need one that matches the specific provider\n const client = MicrosoftGraphClient.create(provider);\n const { users, groups } = await readMicrosoftGraphOrg(\n client,\n provider.tenantId,\n {\n userFilter: provider.userFilter,\n groupFilter: provider.groupFilter,\n userTransformer: this.userTransformer,\n groupTransformer: this.groupTransformer,\n organizationTransformer: this.organizationTransformer,\n logger: this.logger,\n },\n );\n\n const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1);\n this.logger.debug(\n `Read ${users.length} users and ${groups.length} groups from Microsoft Graph in ${duration} seconds`,\n );\n\n // Done!\n for (const group of groups) {\n emit(results.entity(location, group));\n }\n for (const user of users) {\n emit(results.entity(location, user));\n }\n\n return true;\n }\n}\n"],"names":["msal","qs","fetch","limiterFactory","stringifyEntityRef","results"],"mappings":"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"}
@@ -0,0 +1,143 @@
1
+ import { UserEntity, GroupEntity, LocationSpec } from '@backstage/catalog-model';
2
+ import { Config } from '@backstage/config';
3
+ import { CatalogProcessor, CatalogProcessorEmit } from '@backstage/plugin-catalog-backend';
4
+ import { Logger } from 'winston';
5
+ import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
6
+ import * as msal from '@azure/msal-node';
7
+
8
+ /**
9
+ * The configuration parameters for a single Microsoft Graph provider.
10
+ */
11
+ declare type MicrosoftGraphProviderConfig = {
12
+ /**
13
+ * The prefix of the target that this matches on, e.g.
14
+ * "https://graph.microsoft.com/v1.0", with no trailing slash.
15
+ */
16
+ target: string;
17
+ /**
18
+ * The auth authority used.
19
+ *
20
+ * E.g. "https://login.microsoftonline.com"
21
+ */
22
+ authority?: string;
23
+ /**
24
+ * The tenant whose org data we are interested in.
25
+ */
26
+ tenantId: string;
27
+ /**
28
+ * The OAuth client ID to use for authenticating requests.
29
+ */
30
+ clientId: string;
31
+ /**
32
+ * The OAuth client secret to use for authenticating requests.
33
+ *
34
+ * @visibility secret
35
+ */
36
+ clientSecret: string;
37
+ /**
38
+ * The filter to apply to extract users.
39
+ *
40
+ * E.g. "accountEnabled eq true and userType eq 'member'"
41
+ */
42
+ userFilter?: string;
43
+ /**
44
+ * The filter to apply to extract groups.
45
+ *
46
+ * E.g. "securityEnabled eq false and mailEnabled eq true"
47
+ */
48
+ groupFilter?: string;
49
+ };
50
+ declare function readMicrosoftGraphConfig(config: Config): MicrosoftGraphProviderConfig[];
51
+
52
+ declare type ODataQuery = {
53
+ filter?: string;
54
+ expand?: string[];
55
+ select?: string[];
56
+ };
57
+ declare type GroupMember = (MicrosoftGraph.Group & {
58
+ '@odata.type': '#microsoft.graph.user';
59
+ }) | (MicrosoftGraph.User & {
60
+ '@odata.type': '#microsoft.graph.group';
61
+ });
62
+ declare class MicrosoftGraphClient {
63
+ private readonly baseUrl;
64
+ private readonly pca;
65
+ static create(config: MicrosoftGraphProviderConfig): MicrosoftGraphClient;
66
+ constructor(baseUrl: string, pca: msal.ConfidentialClientApplication);
67
+ requestCollection<T>(path: string, query?: ODataQuery): AsyncIterable<T>;
68
+ requestApi(path: string, query?: ODataQuery): Promise<Response>;
69
+ requestRaw(url: string): Promise<Response>;
70
+ getUserProfile(userId: string): Promise<MicrosoftGraph.User>;
71
+ getUserPhotoWithSizeLimit(userId: string, maxSize: number): Promise<string | undefined>;
72
+ getUserPhoto(userId: string, sizeId?: string): Promise<string | undefined>;
73
+ getUsers(query?: ODataQuery): AsyncIterable<MicrosoftGraph.User>;
74
+ getGroupPhotoWithSizeLimit(groupId: string, maxSize: number): Promise<string | undefined>;
75
+ getGroupPhoto(groupId: string, sizeId?: string): Promise<string | undefined>;
76
+ getGroups(query?: ODataQuery): AsyncIterable<MicrosoftGraph.Group>;
77
+ getGroupMembers(groupId: string): AsyncIterable<GroupMember>;
78
+ getOrganization(tenantId: string): Promise<MicrosoftGraph.Organization>;
79
+ private getPhotoWithSizeLimit;
80
+ private getPhoto;
81
+ private handleError;
82
+ }
83
+
84
+ /**
85
+ * The tenant id used by the Microsoft Graph API
86
+ */
87
+ declare const MICROSOFT_GRAPH_TENANT_ID_ANNOTATION = "graph.microsoft.com/tenant-id";
88
+ /**
89
+ * The group id used by the Microsoft Graph API
90
+ */
91
+ declare const MICROSOFT_GRAPH_GROUP_ID_ANNOTATION = "graph.microsoft.com/group-id";
92
+ /**
93
+ * The user id used by the Microsoft Graph API
94
+ */
95
+ declare const MICROSOFT_GRAPH_USER_ID_ANNOTATION = "graph.microsoft.com/user-id";
96
+
97
+ declare function normalizeEntityName(name: string): string;
98
+
99
+ declare type UserTransformer = (user: MicrosoftGraph.User, userPhoto?: string) => Promise<UserEntity | undefined>;
100
+ declare type OrganizationTransformer = (organization: MicrosoftGraph.Organization) => Promise<GroupEntity | undefined>;
101
+ declare type GroupTransformer = (group: MicrosoftGraph.Group, groupPhoto?: string) => Promise<GroupEntity | undefined>;
102
+
103
+ declare function defaultUserTransformer(user: MicrosoftGraph.User, userPhoto?: string): Promise<UserEntity | undefined>;
104
+ declare function defaultOrganizationTransformer(organization: MicrosoftGraph.Organization): Promise<GroupEntity | undefined>;
105
+ declare function defaultGroupTransformer(group: MicrosoftGraph.Group, groupPhoto?: string): Promise<GroupEntity | undefined>;
106
+ declare function readMicrosoftGraphOrg(client: MicrosoftGraphClient, tenantId: string, options: {
107
+ userFilter?: string;
108
+ groupFilter?: string;
109
+ userTransformer?: UserTransformer;
110
+ groupTransformer?: GroupTransformer;
111
+ organizationTransformer?: OrganizationTransformer;
112
+ logger: Logger;
113
+ }): Promise<{
114
+ users: UserEntity[];
115
+ groups: GroupEntity[];
116
+ }>;
117
+
118
+ /**
119
+ * Extracts teams and users out of a the Microsoft Graph API.
120
+ */
121
+ declare class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor {
122
+ private readonly providers;
123
+ private readonly logger;
124
+ private readonly userTransformer?;
125
+ private readonly groupTransformer?;
126
+ private readonly organizationTransformer?;
127
+ static fromConfig(config: Config, options: {
128
+ logger: Logger;
129
+ userTransformer?: UserTransformer;
130
+ groupTransformer?: GroupTransformer;
131
+ organizationTransformer?: OrganizationTransformer;
132
+ }): MicrosoftGraphOrgReaderProcessor;
133
+ constructor(options: {
134
+ providers: MicrosoftGraphProviderConfig[];
135
+ logger: Logger;
136
+ userTransformer?: UserTransformer;
137
+ groupTransformer?: GroupTransformer;
138
+ organizationTransformer?: OrganizationTransformer;
139
+ });
140
+ readLocation(location: LocationSpec, _optional: boolean, emit: CatalogProcessorEmit): Promise<boolean>;
141
+ }
142
+
143
+ export { GroupTransformer, MICROSOFT_GRAPH_GROUP_ID_ANNOTATION, MICROSOFT_GRAPH_TENANT_ID_ANNOTATION, MICROSOFT_GRAPH_USER_ID_ANNOTATION, MicrosoftGraphClient, MicrosoftGraphOrgReaderProcessor, MicrosoftGraphProviderConfig, OrganizationTransformer, UserTransformer, defaultGroupTransformer, defaultOrganizationTransformer, defaultUserTransformer, normalizeEntityName, readMicrosoftGraphConfig, readMicrosoftGraphOrg };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@backstage/plugin-catalog-backend-module-msgraph",
3
+ "description": "A Backstage catalog backend modules that helps integrate towards Microsoft Graph",
4
+ "version": "0.0.0-nightly-202181722143",
5
+ "main": "dist/index.cjs.js",
6
+ "types": "dist/index.d.ts",
7
+ "license": "Apache-2.0",
8
+ "private": false,
9
+ "publishConfig": {
10
+ "access": "public",
11
+ "main": "dist/index.cjs.js",
12
+ "types": "dist/index.d.ts"
13
+ },
14
+ "homepage": "https://backstage.io",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/backstage/backstage",
18
+ "directory": "plugins/catalog-backend-module-msgraph"
19
+ },
20
+ "keywords": [
21
+ "backstage"
22
+ ],
23
+ "scripts": {
24
+ "build": "backstage-cli backend:build",
25
+ "lint": "backstage-cli lint",
26
+ "test": "backstage-cli test",
27
+ "prepack": "backstage-cli prepack",
28
+ "postpack": "backstage-cli postpack",
29
+ "clean": "backstage-cli clean"
30
+ },
31
+ "dependencies": {
32
+ "@azure/msal-node": "^1.1.0",
33
+ "@backstage/catalog-model": "^0.9.0",
34
+ "@backstage/config": "^0.1.8",
35
+ "@backstage/plugin-catalog-backend": "^0.0.0-nightly-202181722143",
36
+ "@microsoft/microsoft-graph-types": "^1.25.0",
37
+ "cross-fetch": "^3.0.6",
38
+ "lodash": "^4.17.15",
39
+ "p-limit": "^3.0.2",
40
+ "winston": "^3.2.1",
41
+ "qs": "^6.9.4"
42
+ },
43
+ "devDependencies": {
44
+ "@backstage/backend-common": "^0.9.0",
45
+ "@backstage/cli": "^0.7.9",
46
+ "@backstage/test-utils": "^0.1.14",
47
+ "@types/lodash": "^4.14.151",
48
+ "msw": "^0.29.0"
49
+ },
50
+ "files": [
51
+ "dist",
52
+ "config.d.ts"
53
+ ],
54
+ "configSchema": "config.d.ts"
55
+ }