From 83f331caf51e658e37bb4c33a867886f4376bf95 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Wed, 17 Jun 2020 10:48:12 +0200 Subject: [PATCH] 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); }