Merge pull request #5854 from SDA-SE/feat/msgraph2

Split `MicrosoftGraphOrgReaderProcessor` into a separate package and make it customizable
This commit is contained in:
Ben Lambert
2021-06-18 11:20:52 +02:00
committed by GitHub
35 changed files with 1008 additions and 220 deletions
+25
View File
@@ -0,0 +1,25 @@
---
'@backstage/plugin-catalog-backend': patch
'@backstage/plugin-catalog-backend-module-msgraph': patch
---
Move `MicrosoftGraphOrgReaderProcessor` from `@backstage/plugin-catalog-backend`
to `@backstage/plugin-catalog-backend-module-msgraph`.
The `MicrosoftGraphOrgReaderProcessor` isn't registered by default anymore, if
you want to continue using it you have to register it manually at the catalog
builder:
1. Add dependency to `@backstage/plugin-catalog-backend-module-msgraph` to the `package.json` of your backend.
2. Add the processor to the catalog builder:
```typescript
// packages/backend/src/plugins/catalog.ts
builder.addProcessor(
MicrosoftGraphOrgReaderProcessor.fromConfig(config, {
logger,
}),
);
```
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).
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-catalog-backend-module-msgraph': patch
---
Allow customizations of `MicrosoftGraphOrgReaderProcessor` by passing an
optional `groupTransformer`, `userTransformer`, and `organizationTransformer`.
+1
View File
@@ -149,6 +149,7 @@ Mkdocs
monorepo
Monorepo
monorepos
msgraph
msw
mysql
namespace
+2 -2
View File
@@ -6,8 +6,8 @@ sidebar_label: Locations
description: Integrating source code stored in Azure DevOps into the Backstage catalog
---
The Azure integration supports loading catalog entities from Azure DevOps.
Entities can be added to
The Azure DevOps integration supports loading catalog entities from Azure
DevOps. Entities can be added to
[static catalog configuration](../../features/software-catalog/configuration.md),
or registered with the
[catalog-import](https://github.com/backstage/backstage/tree/master/plugins/catalog-import)
+14
View File
@@ -0,0 +1,14 @@
---
id: org
title: Microsoft Azure Active Directory Organizational Data
sidebar_label: Org Data
# prettier-ignore
description: Importing users and groups from a Microsoft Azure Active Directory into Backstage
---
The Backstage catalog can be set up to ingest organizational data - users and
teams - directly from an tenant in Microsoft Azure Active Directory via the
Microsoft Graph API.
More details on this are available in 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).
+2 -2
View File
@@ -102,8 +102,8 @@
"integrations/index",
{
"type": "subcategory",
"label": "Azure DevOps",
"ids": ["integrations/azure/locations"]
"label": "Azure",
"ids": ["integrations/azure/locations", "integrations/azure/org"]
},
{
"type": "subcategory",
+2 -1
View File
@@ -75,8 +75,9 @@ nav:
- FAQ: 'features/techdocs/FAQ.md'
- Integrations:
- Overview: 'integrations/index.md'
- Azure DevOps:
- Azure:
- Locations: 'integrations/azure/locations.md'
- Org Data: 'integrations/azure/org.md'
- Bitbucket:
- Locations: 'integrations/bitbucket/locations.md'
- Discovery: 'integrations/bitbucket/discovery.md'
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
@@ -0,0 +1,116 @@
# Catalog Backend Module for Microsoft Graph
This is an extension module to the `plugin-catalog-backend` plugin, providing a
`MicrosoftGraphOrgReaderProcessor` that can be used to ingest organization data
from the Microsoft Graph API. This processor is useful if you want to import
users and groups from Azure Active Directory or Office 365.
## Getting Started
1. The processor is not installed by default, therefore you have to add a
dependency to `@backstage/plugin-catalog-backend-module-msgraph` to your
backend package.
```bash
# From your Backstage root directory
cd packages/backend
yarn add @backstage/plugin-catalog-backend-module-msgraph
```
2. The `MicrosoftGraphOrgReaderProcessor` is not registered by default, so you
have to register it in the catalog plugin:
```typescript
// packages/backend/src/plugins/catalog.ts
builder.addProcessor(
MicrosoftGraphOrgReaderProcessor.fromConfig(config, {
logger,
}),
);
```
3. Create or use an existing App registration in the [Microsoft Azure Portal](https://portal.azure.com/).
The App registration requires at least the API permissions `Group.Read.All`,
`GroupMember.Read.All`, `User.Read` and `User.Read.All` for Microsoft Graph
(if you still run into errors about insufficient privileges, add
`Team.ReadBasic.All` and `TeamMember.Read.All` too).
4. Configure the processor:
```yaml
# app-config.yaml
catalog:
processors:
microsoftGraphOrg:
providers:
- target: https://graph.microsoft.com/v1.0
authority: https://login.microsoftonline.com
# If you don't know you tenantId, you can use Microsoft Graph Explorer
# to query it
tenantId: ${MICROSOFT_GRAPH_TENANT_ID}
# Client Id and Secret can be created under Certificates & secrets in
# the App registration in the Microsoft Azure Portal.
clientId: ${MICROSOFT_GRAPH_CLIENT_ID}
clientSecret: ${MICROSOFT_GRAPH_CLIENT_SECRET_TOKEN}
# Optional filter for user, see Microsoft Graph API for the syntax
# See https://docs.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0#properties
# and for the syntax https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter
userFilter: accountEnabled eq true and userType eq 'member'
# Optional filter for group, see Microsoft Graph API for the syntax
# See https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0#properties
groupFilter: securityEnabled eq false and mailEnabled eq true and groupTypes/any(c:c+eq+'Unified')
```
5. Add a location that ingests from Microsoft Graph:
```yaml
# app-config.yaml
catalog:
locations:
- type: microsoft-graph-org
target: https://graph.microsoft.com/v1.0
# If you catalog doesn't allow to import Group and User entities by
# default, allow them here
rules:
- allow: [Group, User]
```
## Customize the Processor
In case you want to customize the ingested entities, the `MicrosoftGraphOrgReaderProcessor`
allows to pass transformers for users, groups and the organization.
1. Create a transformer:
```ts
export async function myGroupTransformer(
group: MicrosoftGraph.Group,
groupPhoto?: string,
): Promise<GroupEntity | undefined> {
if (
((group as unknown) as {
creationOptions: string[];
}).creationOptions.includes('ProvisionGroupHomepage')
) {
return undefined;
}
// Transformations may change namespace, change entity naming pattern, fill
// profile with more or other details...
// Create the group entity on your own, or wrap the default transformer
return await defaultGroupTransformer(group, groupPhoto);
}
```
2. Configure the processor with the transformer:
```ts
builder.addProcessor(
MicrosoftGraphOrgReaderProcessor.fromConfig(config, {
logger,
groupTransformer: myGroupTransformer,
}),
);
```
@@ -0,0 +1,121 @@
## API Report File for "@backstage/plugin-catalog-backend-module-msgraph"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { CatalogProcessor } from '@backstage/plugin-catalog-backend';
import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend';
import { Config } from '@backstage/config';
import { GroupEntity } from '@backstage/catalog-model';
import { LocationSpec } from '@backstage/catalog-model';
import { Logger } from 'winston';
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
import * as msal from '@azure/msal-node';
import { UserEntity } from '@backstage/catalog-model';
// @public (undocumented)
export function defaultGroupTransformer(group: MicrosoftGraph.Group, groupPhoto?: string): Promise<GroupEntity | undefined>;
// @public (undocumented)
export function defaultOrganizationTransformer(organization: MicrosoftGraph.Organization): Promise<GroupEntity | undefined>;
// @public (undocumented)
export function defaultUserTransformer(user: MicrosoftGraph.User, userPhoto?: string): Promise<UserEntity | undefined>;
// @public (undocumented)
export type GroupTransformer = (group: MicrosoftGraph.Group, groupPhoto?: string) => Promise<GroupEntity | undefined>;
// @public
export const MICROSOFT_GRAPH_GROUP_ID_ANNOTATION = "graph.microsoft.com/group-id";
// @public
export const MICROSOFT_GRAPH_TENANT_ID_ANNOTATION = "graph.microsoft.com/tenant-id";
// @public
export const MICROSOFT_GRAPH_USER_ID_ANNOTATION = "graph.microsoft.com/user-id";
// @public (undocumented)
export class MicrosoftGraphClient {
constructor(baseUrl: string, pca: msal.ConfidentialClientApplication);
// (undocumented)
static create(config: MicrosoftGraphProviderConfig): MicrosoftGraphClient;
// (undocumented)
getGroupMembers(groupId: string): AsyncIterable<GroupMember>;
// (undocumented)
getGroupPhoto(groupId: string, sizeId?: string): Promise<string | undefined>;
// (undocumented)
getGroupPhotoWithSizeLimit(groupId: string, maxSize: number): Promise<string | undefined>;
// (undocumented)
getGroups(query?: ODataQuery): AsyncIterable<MicrosoftGraph.Group>;
// (undocumented)
getOrganization(tenantId: string): Promise<MicrosoftGraph.Organization>;
// (undocumented)
getUserPhoto(userId: string, sizeId?: string): Promise<string | undefined>;
// (undocumented)
getUserPhotoWithSizeLimit(userId: string, maxSize: number): Promise<string | undefined>;
// (undocumented)
getUserProfile(userId: string): Promise<MicrosoftGraph.User>;
// (undocumented)
getUsers(query?: ODataQuery): AsyncIterable<MicrosoftGraph.User>;
// (undocumented)
requestApi(path: string, query?: ODataQuery): Promise<Response>;
// (undocumented)
requestCollection<T>(path: string, query?: ODataQuery): AsyncIterable<T>;
// (undocumented)
requestRaw(url: string): Promise<Response>;
}
// @public
export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor {
constructor(options: {
providers: MicrosoftGraphProviderConfig[];
logger: Logger;
groupTransformer?: GroupTransformer;
});
// (undocumented)
static fromConfig(config: Config, options: {
logger: Logger;
groupTransformer?: GroupTransformer;
}): MicrosoftGraphOrgReaderProcessor;
// (undocumented)
readLocation(location: LocationSpec, _optional: boolean, emit: CatalogProcessorEmit): Promise<boolean>;
}
// @public
export type MicrosoftGraphProviderConfig = {
target: string;
authority?: string;
tenantId: string;
clientId: string;
clientSecret: string;
userFilter?: string;
groupFilter?: string;
};
// @public (undocumented)
export function normalizeEntityName(name: string): string;
// @public (undocumented)
export type OrganizationTransformer = (organization: MicrosoftGraph.Organization) => Promise<GroupEntity | undefined>;
// @public (undocumented)
export function readMicrosoftGraphConfig(config: Config): MicrosoftGraphProviderConfig[];
// @public (undocumented)
export function readMicrosoftGraphOrg(client: MicrosoftGraphClient, tenantId: string, options?: {
userFilter?: string;
groupFilter?: string;
groupTransformer?: GroupTransformer;
}): Promise<{
users: UserEntity[];
groups: GroupEntity[];
}>;
// @public (undocumented)
export type UserTransformer = (user: MicrosoftGraph.User, userPhoto?: string) => Promise<UserEntity | undefined>;
// (No @packageDocumentation comment for this package)
```
+80
View File
@@ -0,0 +1,80 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface Config {
/**
* Configuration options for the catalog plugin.
*/
catalog?: {
/**
* List of processor-specific options and attributes
*/
processors?: {
/**
* MicrosoftGraphOrgReaderProcessor configuration
*/
microsoftGraphOrg?: {
/**
* The configuration parameters for each single Microsoft Graph provider.
*/
providers: Array<{
/**
* The prefix of the target that this matches on, e.g.
* "https://graph.microsoft.com/v1.0", with no trailing slash.
*/
target: string;
/**
* The auth authority used.
*
* Default value "https://login.microsoftonline.com"
*/
authority?: string;
/**
* The tenant whose org data we are interested in.
*/
tenantId: string;
/**
* The OAuth client ID to use for authenticating requests.
*/
clientId: string;
/**
* The OAuth client secret to use for authenticating requests.
*
* @visibility secret
*/
clientSecret: string;
// TODO: Consider not making these config options and pass them in the
// constructor instead. They are probably not environment specifc, so
// they could also be configured "in code".
/**
* The filter to apply to extract users.
*
* E.g. "accountEnabled eq true and userType eq 'member'"
*/
userFilter?: string;
/**
* The filter to apply to extract groups.
*
* E.g. "securityEnabled eq false and mailEnabled eq true"
*/
groupFilter?: string;
}>;
};
};
};
}
@@ -0,0 +1,53 @@
{
"name": "@backstage/plugin-catalog-backend-module-msgraph",
"version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": false,
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/catalog-backend-module-msgraph"
},
"keywords": [
"backstage"
],
"scripts": {
"build": "backstage-cli backend:build",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@azure/msal-node": "^1.1.0",
"@backstage/catalog-model": "^0.8.3",
"@backstage/config": "^0.1.5",
"@backstage/plugin-catalog-backend": "^0.10.2",
"@microsoft/microsoft-graph-types": "^1.25.0",
"cross-fetch": "^3.0.6",
"lodash": "^4.17.15",
"p-limit": "^3.0.2",
"winston": "^3.2.1",
"qs": "^6.9.4"
},
"devDependencies": {
"@backstage/cli": "^0.7.1",
"@backstage/test-utils": "^0.1.13",
"@types/lodash": "^4.14.151",
"msw": "^0.21.2"
},
"files": [
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts"
}
@@ -0,0 +1,18 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './processors';
export * from './microsoftGraph';
@@ -0,0 +1,29 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { normalizeEntityName } from './helper';
describe('normalizeEntityName', () => {
it('should normalize name to valid entity name', () => {
expect(normalizeEntityName('User Name')).toBe('user_name');
});
it('should normalize e-mail to valid entity name', () => {
expect(normalizeEntityName('user.name@example.com')).toBe(
'user.name_example.com',
);
});
});
@@ -0,0 +1,22 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export function normalizeEntityName(name: string): string {
return name
.trim()
.toLocaleLowerCase()
.replace(/[^a-zA-Z0-9_\-\.]/g, '_');
}
@@ -14,11 +14,22 @@
* limitations under the License.
*/
export { MicrosoftGraphClient } from './client';
export type { MicrosoftGraphProviderConfig } from './config';
export { readMicrosoftGraphConfig } from './config';
export { readMicrosoftGraphOrg } from './read';
export type { MicrosoftGraphProviderConfig } from './config';
export {
MICROSOFT_GRAPH_GROUP_ID_ANNOTATION,
MICROSOFT_GRAPH_TENANT_ID_ANNOTATION,
MICROSOFT_GRAPH_USER_ID_ANNOTATION,
} from './constants';
export { normalizeEntityName } from './helper';
export {
defaultGroupTransformer,
defaultOrganizationTransformer,
defaultUserTransformer,
readMicrosoftGraphOrg,
} from './read';
export type {
GroupTransformer,
OrganizationTransformer,
UserTransformer,
} from './types';
@@ -0,0 +1,94 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
import { buildMemberOf, buildOrgHierarchy } from './org';
function g(
name: string,
parent: string | undefined,
children: string[],
): GroupEntity {
return {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: { name },
spec: { type: 'team', parent, children },
};
}
describe('buildOrgHierarchy', () => {
it('adds groups to their parent.children', () => {
const a = g('a', undefined, []);
const b = g('b', 'a', []);
const c = g('c', 'b', []);
const d = g('d', 'a', []);
buildOrgHierarchy([a, b, c, d]);
expect(a.spec.children).toEqual(expect.arrayContaining(['b', 'd']));
expect(b.spec.children).toEqual(expect.arrayContaining(['c']));
expect(c.spec.children).toEqual([]);
expect(d.spec.children).toEqual([]);
});
it('sets parent of groups children', () => {
const a = g('a', undefined, ['b', 'd']);
const b = g('b', undefined, ['c']);
const c = g('c', undefined, []);
const d = g('d', undefined, []);
buildOrgHierarchy([a, b, c, d]);
expect(a.spec.parent).toBeUndefined();
expect(b.spec.parent).toBe('a');
expect(c.spec.parent).toBe('b');
expect(d.spec.parent).toBe('a');
});
});
describe('buildMemberOf', () => {
it('fills indirect member of groups', () => {
const a = g('a', undefined, []);
const b = g('b', 'a', []);
const c = g('c', 'b', []);
const u: UserEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: { name: 'n' },
spec: { profile: {}, memberOf: ['c'] },
};
const groups = [a, b, c];
buildOrgHierarchy(groups);
buildMemberOf(groups, [u]);
expect(u.spec.memberOf).toEqual(expect.arrayContaining(['a', 'b', 'c']));
});
it('takes group spec.members into account', () => {
const a = g('a', undefined, []);
const b = g('b', 'a', []);
const c = g('c', 'b', []);
c.spec.members = ['n'];
const u: UserEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: { name: 'n' },
spec: { profile: {}, memberOf: [] },
};
const groups = [a, b, c];
buildOrgHierarchy(groups);
buildMemberOf(groups, [u]);
expect(u.spec.memberOf).toEqual(expect.arrayContaining(['a', 'b', 'c']));
});
});
@@ -0,0 +1,87 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
// TODO: Copied from plugin-catalog-backend, but we could also export them from
// there. Or move them to catalog-model.
export function buildOrgHierarchy(groups: GroupEntity[]) {
const groupsByName = new Map(groups.map(g => [g.metadata.name, g]));
//
// Make sure that g.parent.children contain g
//
for (const group of groups) {
const selfName = group.metadata.name;
const parentName = group.spec.parent;
if (parentName) {
const parent = groupsByName.get(parentName);
if (parent && !parent.spec.children.includes(selfName)) {
parent.spec.children.push(selfName);
}
}
}
//
// Make sure that g.children.parent is g
//
for (const group of groups) {
const selfName = group.metadata.name;
for (const childName of group.spec.children) {
const child = groupsByName.get(childName);
if (child && !child.spec.parent) {
child.spec.parent = selfName;
}
}
}
}
// Ensure that users have their transitive group memberships. Requires that
// the groups were previously processed with buildOrgHierarchy()
export function buildMemberOf(groups: GroupEntity[], users: UserEntity[]) {
const groupsByName = new Map(groups.map(g => [g.metadata.name, g]));
users.forEach(user => {
const transitiveMemberOf = new Set<string>();
const todo = [
...user.spec.memberOf,
...groups
.filter(g => g.spec.members?.includes(user.metadata.name))
.map(g => g.metadata.name),
];
for (;;) {
const current = todo.pop();
if (!current) {
break;
}
if (!transitiveMemberOf.has(current)) {
transitiveMemberOf.add(current);
const group = groupsByName.get(current);
if (group?.spec.parent) {
todo.push(group.spec.parent);
}
}
}
user.spec.memberOf = [...transitiveMemberOf];
});
}
@@ -16,17 +16,15 @@
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
import merge from 'lodash/merge';
import { RecursivePartial } from '../../../util';
import { GroupMember, MicrosoftGraphClient } from './client';
import {
normalizeEntityName,
readMicrosoftGraphGroups,
readMicrosoftGraphOrganization,
readMicrosoftGraphUsers,
resolveRelations,
} from './read';
function user(data: RecursivePartial<UserEntity>): UserEntity {
function user(data: Partial<UserEntity>): UserEntity {
return merge(
{},
{
@@ -39,7 +37,7 @@ function user(data: RecursivePartial<UserEntity>): UserEntity {
);
}
function group(data: RecursivePartial<GroupEntity>): GroupEntity {
function group(data: Partial<GroupEntity>): GroupEntity {
return merge(
{},
{
@@ -69,18 +67,6 @@ describe('read microsoft graph', () => {
afterEach(() => jest.resetAllMocks());
describe('normalizeEntityName', () => {
it('should normalize name to valid entity name', () => {
expect(normalizeEntityName('User Name')).toBe('user_name');
});
it('should normalize e-mail to valid entity name', () => {
expect(normalizeEntityName('user.name@example.com')).toBe(
'user.name_example.com',
);
});
});
describe('readMicrosoftGraphUsers', () => {
it('should read users', async () => {
async function* getExampleUsers() {
@@ -114,6 +100,7 @@ describe('read microsoft graph', () => {
email: 'user.name@example.com',
picture: 'data:image/jpeg;base64,...',
},
memberOf: [],
},
}),
]);
@@ -121,7 +108,6 @@ describe('read microsoft graph', () => {
expect(client.getUsers).toBeCalledTimes(1);
expect(client.getUsers).toBeCalledWith({
filter: 'accountEnabled eq true',
select: ['id', 'displayName', 'mail'],
});
expect(client.getUserPhotoWithSizeLimit).toBeCalledTimes(1);
expect(client.getUserPhotoWithSizeLimit).toBeCalledWith('userid', 120);
@@ -154,6 +140,7 @@ describe('read microsoft graph', () => {
profile: {
displayName: 'Organization Name',
},
children: [],
},
}),
);
@@ -161,6 +148,24 @@ describe('read microsoft graph', () => {
expect(client.getOrganization).toBeCalledTimes(1);
expect(client.getOrganization).toBeCalledWith('tenantid');
});
it('should read organization with custom transformer', async () => {
client.getOrganization.mockResolvedValue({
id: 'tenantid',
displayName: 'Organization Name',
});
const { rootGroup } = await readMicrosoftGraphOrganization(
client,
'tenantid',
{ transformer: async _ => undefined },
);
expect(rootGroup).toEqual(undefined);
expect(client.getOrganization).toBeCalledTimes(1);
expect(client.getOrganization).toBeCalledWith('tenantid');
});
});
describe('readMicrosoftGraphGroups', () => {
@@ -217,6 +222,7 @@ describe('read microsoft graph', () => {
profile: {
displayName: 'Organization Name',
},
children: [],
},
});
expect(groups).toEqual([
@@ -234,10 +240,11 @@ describe('read microsoft graph', () => {
profile: {
displayName: 'Group Name',
email: 'group@example.com',
// TODO: Loading groups doesn't work right now as Microsoft Graph
// doesn't allows this yet
// TODO: Loading groups photos doesn't work right now as Microsoft
// Graph doesn't allows this yet
/* picture: 'data:image/jpeg;base64,...',*/
},
children: [],
},
}),
]);
@@ -249,11 +256,10 @@ describe('read microsoft graph', () => {
expect(client.getGroups).toBeCalledTimes(1);
expect(client.getGroups).toBeCalledWith({
filter: 'securityEnabled eq false',
select: ['id', 'displayName', 'description', 'mail', 'mailNickname'],
});
expect(client.getGroupMembers).toBeCalledTimes(1);
expect(client.getGroupMembers).toBeCalledWith('groupid');
// TODO: Loading groups doesn't work right now as Microsoft Graph
// TODO: Loading groups photos doesn't work right now as Microsoft Graph
// doesn't allows this yet
// expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1);
// expect(client.getGroupPhotoWithSizeLimit).toBeCalledWith('groupid', 120);
@@ -271,6 +277,7 @@ describe('read microsoft graph', () => {
},
spec: {
type: 'root',
children: [],
},
});
const groupA = group({
@@ -328,20 +335,26 @@ describe('read microsoft graph', () => {
expect(rootGroup.spec.parent).toBeUndefined();
expect(rootGroup.spec.children).toEqual(
expect.arrayContaining(['a', 'b']),
expect.arrayContaining(['group:default/a', 'group:default/b']),
);
expect(groupA.spec.parent).toEqual('root');
expect(groupA.spec.parent).toEqual('group:default/root');
expect(groupA.spec.children).toEqual(expect.arrayContaining([]));
expect(groupB.spec.parent).toEqual('root');
expect(groupB.spec.children).toEqual(expect.arrayContaining(['c']));
expect(groupB.spec.parent).toEqual('group:default/root');
expect(groupB.spec.children).toEqual(
expect.arrayContaining(['group:default/c']),
);
expect(groupC.spec.parent).toEqual('b');
expect(groupC.spec.parent).toEqual('group:default/b');
expect(groupC.spec.children).toEqual(expect.arrayContaining([]));
expect(user1.spec.memberOf).toEqual(expect.arrayContaining(['a']));
expect(user2.spec.memberOf).toEqual(expect.arrayContaining(['b', 'c']));
expect(user1.spec.memberOf).toEqual(
expect.arrayContaining(['group:default/a']),
);
expect(user2.spec.memberOf).toEqual(
expect.arrayContaining(['group:default/c']),
);
});
});
});
@@ -13,95 +13,117 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
import {
GroupEntity,
stringifyEntityRef,
UserEntity,
} from '@backstage/catalog-model';
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
import limiterFactory from 'p-limit';
import { buildMemberOf, buildOrgHierarchy } from '../util/org';
import { MicrosoftGraphClient } from './client';
import {
MICROSOFT_GRAPH_GROUP_ID_ANNOTATION,
MICROSOFT_GRAPH_TENANT_ID_ANNOTATION,
MICROSOFT_GRAPH_USER_ID_ANNOTATION,
} from './constants';
import { normalizeEntityName } from './helper';
import { buildMemberOf, buildOrgHierarchy } from './org';
import {
GroupTransformer,
OrganizationTransformer,
UserTransformer,
} from './types';
export function normalizeEntityName(name: string): string {
return name
.trim()
.toLocaleLowerCase()
.replace(/[^a-zA-Z0-9_\-\.]/g, '_');
export async function defaultUserTransformer(
user: MicrosoftGraph.User,
userPhoto?: string,
): Promise<UserEntity | undefined> {
if (!user.id || !user.displayName || !user.mail) {
return undefined;
}
const name = normalizeEntityName(user.mail);
const entity: UserEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: {
name,
annotations: {
[MICROSOFT_GRAPH_USER_ID_ANNOTATION]: user.id!,
},
},
spec: {
profile: {
displayName: user.displayName!,
email: user.mail!,
// TODO: Additional fields?
// jobTitle: user.jobTitle || undefined,
// officeLocation: user.officeLocation || undefined,
// mobilePhone: user.mobilePhone || undefined,
},
memberOf: [],
},
};
if (userPhoto) {
entity.spec.profile!.picture = userPhoto;
}
return entity;
}
export async function readMicrosoftGraphUsers(
client: MicrosoftGraphClient,
options?: { userFilter?: string },
options?: { userFilter?: string; transformer?: UserTransformer },
): Promise<{
users: UserEntity[]; // With all relations empty
}> {
const entities: UserEntity[] = [];
const promises: Promise<void>[] = [];
const users: UserEntity[] = [];
const limiter = limiterFactory(10);
const transformer = options?.transformer ?? defaultUserTransformer;
const promises: Promise<void>[] = [];
for await (const user of client.getUsers({
filter: options?.userFilter,
select: ['id', 'displayName', 'mail'],
})) {
if (!user.id || !user.displayName || !user.mail) {
continue;
}
// Process all users in parallel, otherwise it can take quite some time
promises.push(
limiter(async () => {
const userPhoto = await client.getUserPhotoWithSizeLimit(
user.id!,
// We are limiting the photo size, as users with full resolution photos
// can make the Backstage API slow
120,
);
const name = normalizeEntityName(user.mail);
const entity: UserEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: {
name,
annotations: {
[MICROSOFT_GRAPH_USER_ID_ANNOTATION]: user.id!,
},
},
spec: {
profile: {
displayName: user.displayName!,
email: user.mail!,
const entity = await transformer(user, userPhoto);
// TODO: Additional fields?
// jobTitle: user.jobTitle || undefined,
// officeLocation: user.officeLocation || undefined,
// mobilePhone: user.mobilePhone || undefined,
},
memberOf: [],
},
};
if (!entity) {
return;
}
// Download the photos in parallel, otherwise it can take quite some time
const loadPhoto = limiter(async () => {
entity.spec.profile!.picture = await client.getUserPhotoWithSizeLimit(
user.id!,
// We are limiting the photo size, as users with full resolution photos
// can make the Backstage API slow
120,
);
});
promises.push(loadPhoto);
entities.push(entity);
users.push(entity);
}),
);
}
// Wait for all photos to be downloaded
// Wait for all users and photos to be downloaded
await Promise.all(promises);
return { users: entities };
return { users };
}
export async function readMicrosoftGraphOrganization(
client: MicrosoftGraphClient,
tenantId: string,
): Promise<{
rootGroup: GroupEntity; // With all relations empty
}> {
// For now we expect a single root organization
const organization = await client.getOrganization(tenantId);
export async function defaultOrganizationTransformer(
organization: MicrosoftGraph.Organization,
): Promise<GroupEntity | undefined> {
if (!organization.id || !organization.displayName) {
return undefined;
}
const name = normalizeEntityName(organization.displayName!);
const rootGroup: GroupEntity = {
return {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
@@ -119,14 +141,68 @@ export async function readMicrosoftGraphOrganization(
children: [],
},
};
}
export async function readMicrosoftGraphOrganization(
client: MicrosoftGraphClient,
tenantId: string,
options?: { transformer?: OrganizationTransformer },
): Promise<{
rootGroup?: GroupEntity; // With all relations empty
}> {
// For now we expect a single root organization
const organization = await client.getOrganization(tenantId);
const transformer = options?.transformer ?? defaultOrganizationTransformer;
const rootGroup = await transformer(organization);
return { rootGroup };
}
export async function defaultGroupTransformer(
group: MicrosoftGraph.Group,
groupPhoto?: string,
): Promise<GroupEntity | undefined> {
if (!group.id || !group.displayName) {
return undefined;
}
const name = normalizeEntityName(group.mailNickname || group.displayName);
const entity: GroupEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
name: name,
annotations: {
[MICROSOFT_GRAPH_GROUP_ID_ANNOTATION]: group.id,
},
},
spec: {
type: 'team',
profile: {},
children: [],
},
};
if (group.description) {
entity.metadata.description = group.description;
}
if (group.displayName) {
entity.spec.profile!.displayName = group.displayName;
}
if (group.mail) {
entity.spec.profile!.email = group.mail;
}
if (groupPhoto) {
entity.spec.profile!.picture = groupPhoto;
}
return entity;
}
export async function readMicrosoftGraphGroups(
client: MicrosoftGraphClient,
tenantId: string,
options?: { groupFilter?: string },
options?: { groupFilter?: string; transformer?: GroupTransformer },
): Promise<{
groups: GroupEntity[]; // With all relations empty
rootGroup: GroupEntity | undefined; // With all relations empty
@@ -139,78 +215,52 @@ export async function readMicrosoftGraphGroups(
const limiter = limiterFactory(10);
const { rootGroup } = await readMicrosoftGraphOrganization(client, tenantId);
groupMember.set(rootGroup.metadata.name, new Set<string>());
groups.push(rootGroup);
if (rootGroup) {
groupMember.set(rootGroup.metadata.name, new Set<string>());
groups.push(rootGroup);
}
const transformer = options?.transformer ?? defaultGroupTransformer;
const promises: Promise<void>[] = [];
for await (const group of client.getGroups({
filter: options?.groupFilter,
select: ['id', 'displayName', 'description', 'mail', 'mailNickname'],
})) {
if (!group.id || !group.displayName) {
continue;
}
// Process all groups in parallel, otherwise it can take quite some time
promises.push(
limiter(async () => {
// TODO: Loading groups photos doesn't work right now as Microsoft Graph
// 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
/* const groupPhoto = await client.getGroupPhotoWithSizeLimit(
group.id!,
// We are limiting the photo size, as groups with full resolution photos
// can make the Backstage API slow
120,
);*/
const name = normalizeEntityName(group.mailNickname || group.displayName);
const entity: GroupEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
name: name,
annotations: {
[MICROSOFT_GRAPH_GROUP_ID_ANNOTATION]: group.id,
},
},
spec: {
type: 'team',
profile: {},
children: [],
},
};
const entity = await transformer(group /* , groupPhoto*/);
if (group.description) {
entity.metadata.description = group.description;
}
if (group.displayName) {
entity.spec.profile!.displayName = group.displayName;
}
if (group.mail) {
entity.spec.profile!.email = group.mail;
}
// Download the members in parallel, otherwise it can take quite some time
const loadGroupMembers = limiter(async () => {
for await (const member of client.getGroupMembers(group.id!)) {
if (!member.id) {
continue;
if (!entity) {
return;
}
if (member['@odata.type'] === '#microsoft.graph.user') {
ensureItem(groupMemberOf, member.id, group.id!);
for await (const member of client.getGroupMembers(group.id!)) {
if (!member.id) {
continue;
}
if (member['@odata.type'] === '#microsoft.graph.user') {
ensureItem(groupMemberOf, member.id, group.id!);
}
if (member['@odata.type'] === '#microsoft.graph.group') {
ensureItem(groupMember, group.id!, member.id);
}
}
if (member['@odata.type'] === '#microsoft.graph.group') {
ensureItem(groupMember, group.id!, member.id);
}
}
});
// TODO: Loading groups doesn't work right now as Microsoft Graph 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
/*/ / Download the photos in parallel, otherwise it can take quite some time
const loadPhoto = limiter(async () => {
entity.spec.profile!.picture = await client.getGroupPhotoWithSizeLimit(
group.id!,
// We are limiting the photo size, as groups with full resolution photos
// can make the Backstage API slow
120,
);
});
promises.push(loadPhoto);*/
promises.push(loadGroupMembers);
groups.push(entity);
groups.push(entity);
}),
);
}
// Wait for all group members and photos to be loaded
@@ -286,7 +336,7 @@ export function resolveRelations(
retrieveItems(groupMember, id).forEach(m => {
const childGroup = groupMap.get(m);
if (childGroup) {
group.spec.children.push(childGroup.metadata.name);
group.spec.children.push(stringifyEntityRef(childGroup));
}
});
@@ -294,7 +344,7 @@ export function resolveRelations(
const parentGroup = groupMap.get(p);
if (parentGroup) {
// TODO: Only having a single parent group might not match every companies model, but fine for now.
group.spec.parent = parentGroup.metadata.name;
group.spec.parent = stringifyEntityRef(parentGroup);
}
});
});
@@ -309,7 +359,7 @@ export function resolveRelations(
retrieveItems(groupMemberOf, id).forEach(p => {
const parentGroup = groupMap.get(p);
if (parentGroup) {
user.spec.memberOf.push(parentGroup.metadata.name);
user.spec.memberOf.push(stringifyEntityRef(parentGroup));
}
});
});
@@ -321,7 +371,11 @@ export function resolveRelations(
export async function readMicrosoftGraphOrg(
client: MicrosoftGraphClient,
tenantId: string,
options?: { userFilter?: string; groupFilter?: string },
options?: {
userFilter?: string;
groupFilter?: string;
groupTransformer?: GroupTransformer;
},
): Promise<{ users: UserEntity[]; groups: GroupEntity[] }> {
const { users } = await readMicrosoftGraphUsers(client, {
userFilter: options?.userFilter,
@@ -333,6 +387,7 @@ export async function readMicrosoftGraphOrg(
groupMemberOf,
} = await readMicrosoftGraphGroups(client, tenantId, {
groupFilter: options?.groupFilter,
transformer: options?.groupTransformer,
});
resolveRelations(rootGroup, groups, users, groupMember, groupMemberOf);
@@ -0,0 +1,32 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
export type UserTransformer = (
user: MicrosoftGraph.User,
userPhoto?: string,
) => Promise<UserEntity | undefined>;
export type OrganizationTransformer = (
organization: MicrosoftGraph.Organization,
) => Promise<GroupEntity | undefined>;
export type GroupTransformer = (
group: MicrosoftGraph.Group,
groupPhoto?: string,
) => Promise<GroupEntity | undefined>;
@@ -16,24 +16,32 @@
import { LocationSpec } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import {
CatalogProcessor,
CatalogProcessorEmit,
results,
} from '@backstage/plugin-catalog-backend';
import { Logger } from 'winston';
import {
GroupTransformer,
MicrosoftGraphClient,
MicrosoftGraphProviderConfig,
readMicrosoftGraphConfig,
readMicrosoftGraphOrg,
} from './microsoftGraph';
import * as results from './results';
import { CatalogProcessor, CatalogProcessorEmit } from './types';
} from '../microsoftGraph';
/**
* Extracts teams and users out of an LDAP server.
* Extracts teams and users out of a the Microsoft Graph API.
*/
export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor {
private readonly providers: MicrosoftGraphProviderConfig[];
private readonly logger: Logger;
private readonly groupTransformer?: GroupTransformer;
static fromConfig(config: Config, options: { logger: Logger }) {
static fromConfig(
config: Config,
options: { logger: Logger; groupTransformer?: GroupTransformer },
) {
const c = config.getOptionalConfig('catalog.processors.microsoftGraphOrg');
return new MicrosoftGraphOrgReaderProcessor({
...options,
@@ -44,9 +52,11 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor {
constructor(options: {
providers: MicrosoftGraphProviderConfig[];
logger: Logger;
groupTransformer?: GroupTransformer;
}) {
this.providers = options.providers;
this.logger = options.logger;
this.groupTransformer = options.groupTransformer;
}
async readLocation(
@@ -79,6 +89,7 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor {
{
userFilter: provider.userFilter,
groupFilter: provider.groupFilter,
groupTransformer: this.groupTransformer,
},
);
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { MicrosoftGraphOrgReaderProcessor } from './MicrosoftGraphOrgReaderProcessor';
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {};
-48
View File
@@ -362,54 +362,6 @@ export interface Config {
roleArn?: string;
};
};
/**
* MicrosoftGraphOrgReaderProcessor configuration
*/
microsoftGraphOrg?: {
/**
* The configuration parameters for each single Microsoft Graph provider.
*/
providers: Array<{
/**
* The prefix of the target that this matches on, e.g.
* "https://graph.microsoft.com/v1.0", with no trailing slash.
*/
target: string;
/**
* The auth authority used.
*
* Default value "https://login.microsoftonline.com"
*/
authority?: string;
/**
* The tenant whose org data we are interested in.
*/
tenantId: string;
/**
* The OAuth client ID to use for authenticating requests.
*/
clientId: string;
/**
* The OAuth client secret to use for authenticating requests.
*
* @visibility secret
*/
clientSecret: string;
/**
* The filter to apply to extract users.
*
* E.g. "accountEnabled eq true and userType eq 'member'"
*/
userFilter?: string;
/**
* The filter to apply to extract groups.
*
* E.g. "securityEnabled eq false and mailEnabled eq true"
*/
groupFilter?: string;
}>;
};
};
};
}
-2
View File
@@ -29,7 +29,6 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@azure/msal-node": "^1.0.0-beta.3",
"@backstage/backend-common": "^0.8.3",
"@backstage/catalog-client": "^0.3.13",
"@backstage/catalog-model": "^0.8.3",
@@ -38,7 +37,6 @@
"@backstage/integration": "^0.5.6",
"@backstage/plugin-search-backend-node": "^0.2.1",
"@backstage/search-common": "^0.1.2",
"@microsoft/microsoft-graph-types": "^1.25.0",
"@octokit/graphql": "^4.5.8",
"@types/express": "^4.17.6",
"@types/ldapjs": "^1.0.9",
@@ -27,7 +27,6 @@ export { GithubDiscoveryProcessor } from './GithubDiscoveryProcessor';
export { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor';
export { LdapOrgReaderProcessor } from './LdapOrgReaderProcessor';
export { LocationEntityProcessor } from './LocationEntityProcessor';
export { MicrosoftGraphOrgReaderProcessor } from './MicrosoftGraphOrgReaderProcessor';
export { PlaceholderProcessor } from './PlaceholderProcessor';
export type { PlaceholderResolver } from './PlaceholderProcessor';
export { StaticLocationProcessor } from './StaticLocationProcessor';
@@ -50,7 +50,6 @@ import {
GithubDiscoveryProcessor,
GithubOrgReaderProcessor,
LdapOrgReaderProcessor,
MicrosoftGraphOrgReaderProcessor,
PlaceholderProcessor,
PlaceholderResolver,
UrlReaderProcessor,
@@ -373,7 +372,6 @@ export class NextCatalogBuilder {
GithubDiscoveryProcessor.fromConfig(config, { logger }),
GithubOrgReaderProcessor.fromConfig(config, { logger }),
LdapOrgReaderProcessor.fromConfig(config, { logger }),
MicrosoftGraphOrgReaderProcessor.fromConfig(config, { logger }),
new UrlReaderProcessor({ reader, logger }),
CodeOwnersProcessor.fromConfig(config, { logger, reader }),
new AnnotateLocationEntityProcessor({ integrations }),
@@ -51,7 +51,6 @@ import {
LdapOrgReaderProcessor,
LocationEntityProcessor,
LocationReaders,
MicrosoftGraphOrgReaderProcessor,
PlaceholderProcessor,
PlaceholderResolver,
StaticLocationProcessor,
@@ -319,7 +318,6 @@ export class CatalogBuilder {
GithubDiscoveryProcessor.fromConfig(config, { logger }),
GithubOrgReaderProcessor.fromConfig(config, { logger }),
LdapOrgReaderProcessor.fromConfig(config, { logger }),
MicrosoftGraphOrgReaderProcessor.fromConfig(config, { logger }),
new UrlReaderProcessor({ reader, logger }),
CodeOwnersProcessor.fromConfig(config, { logger, reader }),
new LocationEntityProcessor({ integrations }),
+18 -1
View File
@@ -218,7 +218,14 @@
dependencies:
debug "^4.1.1"
"@azure/msal-node@1.0.0-beta.3", "@azure/msal-node@^1.0.0-beta.3":
"@azure/msal-common@^4.3.0":
version "4.3.0"
resolved "https://registry.npmjs.org/@azure/msal-common/-/msal-common-4.3.0.tgz#b540e92748656724088bf77192e59943a93135bc"
integrity sha512-jFqUWe83wVb6O8cNGGBFg2QlKvqM1ezUgJTEV7kIsAPX0RXhGFE4B1DLNt6hCnkTXDbw+KGW0zgxOEr4MJQwLw==
dependencies:
debug "^4.1.1"
"@azure/msal-node@1.0.0-beta.3":
version "1.0.0-beta.3"
resolved "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-beta.3.tgz#c84c7948028b39e48b901f5fac35bdedcbc8772e"
integrity sha512-/KfYRfrsOIrZONvo/0Vi5umuqbPBtCWNtmRvkse64uI0C4CP/W4WXwRD42VMws/8LtKvr1I5rYlYgFzt5zDz/A==
@@ -228,6 +235,16 @@
jsonwebtoken "^8.5.1"
uuid "^8.3.0"
"@azure/msal-node@^1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.1.0.tgz#e472cfadead169f8832066ae6c2d6b8eef4e89e4"
integrity sha512-gMO9aZdWOzufp1PcdD5ID25DdS9eInxgeCqx4Tk8PVU6Z7RxJQhoMzS64cJhGdpYgeIQwKljtF0CLCcPFxew/w==
dependencies:
"@azure/msal-common" "^4.3.0"
axios "^0.21.1"
jsonwebtoken "^8.5.1"
uuid "^8.3.0"
"@azure/storage-blob@^12.4.0":
version "12.4.0"
resolved "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.4.0.tgz#7127ddd9f413105e2c3688691bc4c6245d0806b3"