From 83f331caf51e658e37bb4c33a867886f4376bf95 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Wed, 17 Jun 2020 10:48:12 +0200 Subject: [PATCH 1/2] Add static json adapter to serve user groups --- .../src/adapters/StaticJsonAdapter.ts | 87 +++++++++++++++++++ .../src/adapters/data/userGroups.json | 20 +++++ .../identity-backend/src/adapters/index.ts | 17 ++++ .../identity-backend/src/adapters/types.ts | 43 +++++++++ .../identity-backend/src/service/router.ts | 18 ++-- 5 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 plugins/identity-backend/src/adapters/StaticJsonAdapter.ts create mode 100644 plugins/identity-backend/src/adapters/data/userGroups.json create mode 100644 plugins/identity-backend/src/adapters/index.ts create mode 100644 plugins/identity-backend/src/adapters/types.ts diff --git a/plugins/identity-backend/src/adapters/StaticJsonAdapter.ts b/plugins/identity-backend/src/adapters/StaticJsonAdapter.ts new file mode 100644 index 0000000000..a88e561ee6 --- /dev/null +++ b/plugins/identity-backend/src/adapters/StaticJsonAdapter.ts @@ -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 { Group, GroupsJson, GroupsResponse, IdentityApiAdapter } from './types'; +import fs from 'fs-extra'; +import express from 'express'; +import path from 'path'; + +const GROUPS_JSON_FILE = path.join(__dirname, 'data', 'userGroups.json'); + +export class StaticJsonAdapter implements IdentityApiAdapter { + private readonly groups: Group[]; + + constructor() { + const groupsJson: GroupsJson = fs.readJsonSync(GROUPS_JSON_FILE, { + encoding: 'utf8', + }); + this.groups = groupsJson.groups; + } + + getUserGroups( + req: express.Request, + res: express.Response, + ): express.Response { + const user = req.params.user; + const type = req.query.type?.toString() ?? ''; + + const userGroups = this._getUserGroups(this.groups, user); + const groups = this.filterGroupsByType(userGroups, type); + return res.json({ groups }); + } + + _getUserGroups(groups: Group[], user: string) { + const userGroups: Set = new Set(); + groups.forEach(group => { + if (this.isUserInGroup(group, user)) { + userGroups.add(group); + } + + if (group.children) { + const userSubGroups = this._getUserGroups(group.children, user) ?? []; + const isUserInSubGroup = Boolean(userSubGroups.length); + if (isUserInSubGroup) { + userGroups.add(group); + } + userSubGroups.forEach(subGroup => userGroups.add(subGroup)); + } + }); + return Array.from(userGroups); + } + + private filterGroupsByType = (userGroups: Group[], type: string) => { + const groups = type + ? userGroups + .filter((group: Group) => group.type === type) + .map(group => ({ name: group.name, type: group.type })) + : userGroups.map(group => ({ + name: group.name, + type: group.type, + })); + return groups; + }; + + private isUserInGroup = (group: Group, user: string): boolean => { + if (group.members) { + const groupMembers = group.members; + const groupsWithUser = groupMembers.filter( + member => member.name === user, + ); + return Boolean(groupsWithUser.length); + } + return false; + }; +} diff --git a/plugins/identity-backend/src/adapters/data/userGroups.json b/plugins/identity-backend/src/adapters/data/userGroups.json new file mode 100644 index 0000000000..9ab83480df --- /dev/null +++ b/plugins/identity-backend/src/adapters/data/userGroups.json @@ -0,0 +1,20 @@ +{ + "groups": [ + { + "name": "engineering", + "type": "org", + "children": [ + { + "name": "authentication", + "type": "team", + "members": [{ "name": "kent" }, { "name": "dobbs" }] + }, + { + "name": "checkout", + "type": "team", + "members": [{ "name": "don" }, { "name": "abramev" }] + } + ] + } + ] +} diff --git a/plugins/identity-backend/src/adapters/index.ts b/plugins/identity-backend/src/adapters/index.ts new file mode 100644 index 0000000000..357391a25b --- /dev/null +++ b/plugins/identity-backend/src/adapters/index.ts @@ -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 * from './StaticJsonAdapter'; diff --git a/plugins/identity-backend/src/adapters/types.ts b/plugins/identity-backend/src/adapters/types.ts new file mode 100644 index 0000000000..85719205f8 --- /dev/null +++ b/plugins/identity-backend/src/adapters/types.ts @@ -0,0 +1,43 @@ +/* + * 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 express from 'express'; + +export type User = { + name: string; +}; + +export type Group = { + name: string; + type: string; + members?: User[]; + children?: Group[]; +}; + +export type GroupsJson = { + groups: Group[]; +}; + +export type GroupsResponse = { + groups: Group[]; +}; + +export interface IdentityApiAdapter { + getUserGroups( + req: express.Request, + res: express.Response, + ): express.Response; +} diff --git a/plugins/identity-backend/src/service/router.ts b/plugins/identity-backend/src/service/router.ts index 52e73db0fb..ac6267ba75 100644 --- a/plugins/identity-backend/src/service/router.ts +++ b/plugins/identity-backend/src/service/router.ts @@ -17,21 +17,25 @@ import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; +import { StaticJsonAdapter } from '../adapters'; +import { IdentityApiAdapter } from '../adapters/types'; export interface RouterOptions { logger: Logger; } +const makeRouter = (adapter: IdentityApiAdapter): express.Router => { + const router = Router(); + router.get('/users/:user/groups', adapter.getUserGroups.bind(adapter)); + return router; +}; + export async function createRouter( options: RouterOptions, ): Promise { - const router = Router(); const logger = options.logger; - router.use('/ping', (_, res) => { - logger.info('heartbeat for identity service'); - res.send('pong'); - }); - - return router; + logger.info('Initializing identity provider'); + const adapter = new StaticJsonAdapter(); + return makeRouter(adapter); } From 42ec00922801b7033d4fa2007fb07d0ef4e12711 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Wed, 17 Jun 2020 14:18:40 +0200 Subject: [PATCH 2/2] refactor IdentityApi --- .../src/adapters/StaticJsonAdapter.ts | 28 ++++++++++--------- .../identity-backend/src/adapters/types.ts | 13 ++++----- .../identity-backend/src/service/router.ts | 14 +++++++--- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/plugins/identity-backend/src/adapters/StaticJsonAdapter.ts b/plugins/identity-backend/src/adapters/StaticJsonAdapter.ts index a88e561ee6..cd2a40ea78 100644 --- a/plugins/identity-backend/src/adapters/StaticJsonAdapter.ts +++ b/plugins/identity-backend/src/adapters/StaticJsonAdapter.ts @@ -14,14 +14,19 @@ * limitations under the License. */ -import { Group, GroupsJson, GroupsResponse, IdentityApiAdapter } from './types'; +import { + Group, + GroupsJson, + GroupsResponse, + IdentityApi, + GroupsRequest, +} from './types'; import fs from 'fs-extra'; -import express from 'express'; import path from 'path'; const GROUPS_JSON_FILE = path.join(__dirname, 'data', 'userGroups.json'); -export class StaticJsonAdapter implements IdentityApiAdapter { +export class StaticJsonAdapter implements IdentityApi { private readonly groups: Group[]; constructor() { @@ -31,16 +36,13 @@ export class StaticJsonAdapter implements IdentityApiAdapter { this.groups = groupsJson.groups; } - getUserGroups( - req: express.Request, - res: express.Response, - ): express.Response { - const user = req.params.user; - const type = req.query.type?.toString() ?? ''; - - const userGroups = this._getUserGroups(this.groups, user); - const groups = this.filterGroupsByType(userGroups, type); - return res.json({ groups }); + getUserGroups(req: GroupsRequest): Promise { + return new Promise(resolve => { + const { user, type } = req; + const userGroups = this._getUserGroups(this.groups, user); + const groups = this.filterGroupsByType(userGroups, type); + resolve({ groups }); + }); } _getUserGroups(groups: Group[], user: string) { diff --git a/plugins/identity-backend/src/adapters/types.ts b/plugins/identity-backend/src/adapters/types.ts index 85719205f8..59fd1324d1 100644 --- a/plugins/identity-backend/src/adapters/types.ts +++ b/plugins/identity-backend/src/adapters/types.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import express from 'express'; - export type User = { name: string; }; @@ -35,9 +33,10 @@ export type GroupsResponse = { groups: Group[]; }; -export interface IdentityApiAdapter { - getUserGroups( - req: express.Request, - res: express.Response, - ): express.Response; +export type GroupsRequest = { + user: string; + type: string; +}; +export interface IdentityApi { + getUserGroups(req: GroupsRequest): Promise; } diff --git a/plugins/identity-backend/src/service/router.ts b/plugins/identity-backend/src/service/router.ts index ac6267ba75..256ef6ce3c 100644 --- a/plugins/identity-backend/src/service/router.ts +++ b/plugins/identity-backend/src/service/router.ts @@ -18,15 +18,21 @@ import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import { StaticJsonAdapter } from '../adapters'; -import { IdentityApiAdapter } from '../adapters/types'; +import { IdentityApi } from '../adapters/types'; export interface RouterOptions { logger: Logger; } -const makeRouter = (adapter: IdentityApiAdapter): express.Router => { +const makeRouter = (adapter: IdentityApi): express.Router => { const router = Router(); - router.get('/users/:user/groups', adapter.getUserGroups.bind(adapter)); + router.get('/users/:user/groups', async (req, res) => { + const user = req.params.user; + const type = req.query.type?.toString() ?? ''; + + const response = await adapter.getUserGroups({ user, type }); + res.send(response); + }); return router; }; @@ -35,7 +41,7 @@ export async function createRouter( ): Promise { const logger = options.logger; - logger.info('Initializing identity provider'); + logger.info('Initializing identity API backend'); const adapter = new StaticJsonAdapter(); return makeRouter(adapter); }