diff --git a/api/src/app.ts b/api/src/app.ts index 94d937a5..63737407 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -32,6 +32,7 @@ import threatsV1 from "./resources/gram/v1/threats/index.js"; import tokenV1 from "./resources/gram/v1/token/index.js"; import userV1 from "./resources/gram/v1/user/index.js"; import errorHandler from "./middlewares/errorHandler.js"; +import { getTeam } from "./resources/gram/v1/team/get.js"; import { initSentry } from "./util/sentry.js"; import { retryReviewApproval } from "./resources/gram/v1/admin/retryReviewApproval.js"; import { config } from "@gram/core/dist/config/index.js"; @@ -220,6 +221,9 @@ export async function createApp(dal: DataAccessLayer) { errorWrap(systemPropertyRoutes.properties) ); + // Team + authenticatedRoutes.get("/teams/:id", cache, errorWrap(getTeam(dal))); + // Component Classes authenticatedRoutes.get( "/component-class", diff --git a/api/src/resources/gram/v1/team/get.spec.ts b/api/src/resources/gram/v1/team/get.spec.ts new file mode 100644 index 00000000..7d6cab60 --- /dev/null +++ b/api/src/resources/gram/v1/team/get.spec.ts @@ -0,0 +1,36 @@ +import request from "supertest"; +import { createTestApp } from "../../../../test-util/app.js"; +import { sampleTeam } from "../../../../test-util/sampleTeam.js"; +import { sampleUserToken } from "../../../../test-util/sampleTokens.js"; + +const token = await sampleUserToken(); + +describe("team.get", () => { + let app: any; + + beforeAll(async () => { + ({ app } = await createTestApp()); + }); + + it("should return 401 on un-authenticated request", async () => { + const res = await request(app).get("/api/v1/teams/123"); + expect(res.status).toBe(401); + }); + + it("should return 404 on invalid team id", async () => { + const res = await request(app) + .get("/api/v1/teams/234") + .set("Authorization", token); + + expect(res.status).toBe(404); + }); + + it("should return 200 with dummy data", async () => { + const res = await request(app) + .get("/api/v1/teams/" + sampleTeam.id) + .set("Authorization", token); + + expect(res.status).toBe(200); + expect(res.body.team).toEqual(sampleTeam); + }); +}); diff --git a/api/src/resources/gram/v1/team/get.ts b/api/src/resources/gram/v1/team/get.ts new file mode 100644 index 00000000..2409795a --- /dev/null +++ b/api/src/resources/gram/v1/team/get.ts @@ -0,0 +1,16 @@ +import { DataAccessLayer } from "@gram/core/dist/data/dal.js"; +import { Request, Response } from "express"; + +export function getTeam(dal: DataAccessLayer) { + return async (req: Request, res: Response) => { + const id = req.params.id; + + const team = await dal.teamHandler.getTeam({ currentRequest: req }, id); + + if (team === null) { + return res.sendStatus(404); + } + + return res.json({ team }); + }; +} diff --git a/api/src/test-util/TestTeamProvider.ts b/api/src/test-util/TestTeamProvider.ts new file mode 100644 index 00000000..870776e9 --- /dev/null +++ b/api/src/test-util/TestTeamProvider.ts @@ -0,0 +1,32 @@ +import { TeamProvider } from "@gram/core/dist/auth/TeamProvider.js"; +import { Team } from "@gram/core/dist/auth/models/Team.js"; +import { RequestContext } from "@gram/core/dist/data/providers/RequestContext.js"; +import { + sampleAdmin, + sampleOtherUser, + sampleReviewer, + sampleUser, +} from "./sampleUser.js"; +import { sampleTeam, sampleOtherTeam, teams } from "./sampleTeam.js"; + +export class TestTeamProvider implements TeamProvider { + private teamMap: Map; + + constructor() { + this.teamMap = new Map([ + [sampleUser.sub, [sampleTeam.id]], + [sampleOtherUser.sub, [sampleOtherTeam.id]], + [sampleReviewer.sub, [sampleOtherTeam.id]], + [sampleAdmin.sub, [sampleTeam.id, sampleOtherTeam.id]], + ]); + } + async lookup(ctx: RequestContext, teamIds: string[]): Promise { + return teamIds + .map((tid) => teams.find((team) => team.id === tid)) + .filter((t) => t) as Team[]; + } + async getTeamsForUser(ctx: RequestContext, userId: string): Promise { + return this.lookup(ctx, this.teamMap.get(userId) || []); + } + key: string = "test"; +} diff --git a/api/src/test-util/sampleTeam.ts b/api/src/test-util/sampleTeam.ts index 547e50ab..9615cd19 100644 --- a/api/src/test-util/sampleTeam.ts +++ b/api/src/test-util/sampleTeam.ts @@ -11,3 +11,5 @@ export const sampleOtherTeam: Team = { name: "other team", email: "bb", }; + +export const teams = [sampleTeam, sampleOtherTeam]; diff --git a/api/src/test-util/testConfig.ts b/api/src/test-util/testConfig.ts index bd24c808..de8afa87 100644 --- a/api/src/test-util/testConfig.ts +++ b/api/src/test-util/testConfig.ts @@ -5,12 +5,13 @@ import type { GramConfiguration, Providers, } from "@gram/core/dist/config/GramConfiguration.js"; +import { ComponentClass } from "@gram/core/dist/data/component-classes/index.js"; import type { DataAccessLayer } from "@gram/core/dist/data/dal.js"; +import classes from "./classes.js"; import { testReviewerProvider } from "./sampleReviewer.js"; +import { TestTeamProvider } from "./TestTeamProvider.js"; import { testUserProvider } from "./sampleUser.js"; import { testSystemProvider } from "./system.js"; -import { ComponentClass } from "@gram/core/dist/data/component-classes/index.js"; -import classes from "./classes.js"; export const testConfig: GramConfiguration = { appPort: 8080, @@ -98,6 +99,7 @@ export const testConfig: GramConfiguration = { userProvider: testUserProvider, systemProvider: testSystemProvider, suggestionSources: [], + teamProvider: new TestTeamProvider(), }; }, }; diff --git a/app/src/api/gram/api.js b/app/src/api/gram/api.js index c2723b7b..c78abfbe 100644 --- a/app/src/api/gram/api.js +++ b/app/src/api/gram/api.js @@ -16,6 +16,7 @@ export const api = createApi({ "Suggestions", "System", "Templates", + "Team", "Threats", "User", ], diff --git a/app/src/api/gram/team.js b/app/src/api/gram/team.js new file mode 100644 index 00000000..b0b54177 --- /dev/null +++ b/app/src/api/gram/team.js @@ -0,0 +1,14 @@ +import { api } from "./api"; + +const teamApi = api.injectEndpoints({ + endpoints: (build) => ({ + getTeam: build.query({ + query: ({ teamId }) => ({ + url: `/teams/${teamId}`, + }), + transformResponse: (response, meta, arg) => response.team, + }), + }), +}); + +export const { useGetTeamQuery } = teamApi; diff --git a/app/src/components/systems/Systems.css b/app/src/components/systems/Systems.css index e5233514..197ca32d 100644 --- a/app/src/components/systems/Systems.css +++ b/app/src/components/systems/Systems.css @@ -31,3 +31,8 @@ div#systems .visualize:focus { div#systems .visualize:hover:after { content: "Visualize All Systems"; } + +h5 a { + color: #eee; + text-decoration: none; +} diff --git a/app/src/components/systems/TeamSystems/TeamSystemsPageList.js b/app/src/components/systems/TeamSystems/TeamSystemsPageList.js index d48be056..01bbdcff 100644 --- a/app/src/components/systems/TeamSystems/TeamSystemsPageList.js +++ b/app/src/components/systems/TeamSystems/TeamSystemsPageList.js @@ -14,6 +14,7 @@ import { useListSystemsQuery } from "../../../api/gram/system"; import { SystemComplianceBadge } from "../../elements/SystemComplianceBadge"; import Loading from "../../loading"; import "../Systems.css"; +import { useGetTeamQuery } from "../../../api/gram/team"; export function TeamSystemsPageList({ teamName, @@ -26,17 +27,14 @@ export function TeamSystemsPageList({ const opts = { filter: "team", teamId, pagesize, page }; const { data, isLoading, isFetching } = useListSystemsQuery(opts); - - // Because too lazy to write a new endpoint to fetch team name. - const dynamicTeamName = - data && data.systems.length > 0 && data.systems[0].owners.length > 0 - ? data.systems[0].owners[0].name - : "Team"; + const { data: team } = useGetTeamQuery({ teamId }); return ( - {teamName || dynamicTeamName} + + {team?.name} + {isLoading || isFetching ? ( diff --git a/config/default.ts b/config/default.ts index 7ab60dcc..adf97d1d 100644 --- a/config/default.ts +++ b/config/default.ts @@ -23,6 +23,8 @@ import { StaticAuthzProvider } from "./providers/static/StaticAuthzProvider.js"; import { StaticReviewerProvider } from "./providers/static/StaticReviewerProvider.js"; import { StaticSystemProvider } from "./providers/static/StaticSystemProvider.js"; import { StaticUserProvider } from "./providers/static/StaticUserProvider.js"; +import { Team } from "@gram/core/dist/auth/models/Team.js"; +import { StaticTeamProvider } from "./providers/static/StaticTeamProvider.js"; export const defaultConfig: GramConfiguration = { appPort: 8080, @@ -132,19 +134,38 @@ export const defaultConfig: GramConfiguration = { slackUrl: "", }; + const sampleTeams: Team[] = [ + { + id: "frontend", + name: "Frontend Team", + email: "frontend@localhost", + }, + { + id: "backend", + name: "Backend Team", + email: "backend@localhost", + }, + ]; + + const teamMap: Map = new Map([ + ["user@localhost", ["frontend"]], + ["reviewer@localhost", ["backend"]], + ["admin@localhost", ["backend", "frontend"]], + ]); + const sampleSystems: System[] = [ new System( "web", "Website", "Website", - [], + [sampleTeams[0]], "The main website of the org" ), new System( "order-api", "Order API", "Order API", - [], + [sampleTeams[1]], "Backend API for receiving orders" ), ]; @@ -178,6 +199,7 @@ export const defaultConfig: GramConfiguration = { userProvider: new StaticUserProvider(sampleUsers), systemProvider: new StaticSystemProvider(sampleSystems), suggestionSources: [new ThreatLibSuggestionProvider()], + teamProvider: new StaticTeamProvider(sampleTeams, teamMap), }; }, }; diff --git a/config/providers/static/StaticTeamProvider.ts b/config/providers/static/StaticTeamProvider.ts new file mode 100644 index 00000000..fd367d50 --- /dev/null +++ b/config/providers/static/StaticTeamProvider.ts @@ -0,0 +1,24 @@ +import { TeamProvider } from "@gram/core/dist/auth/TeamProvider.js"; +import { Team } from "@gram/core/dist/auth/models/Team.js"; + +import { RequestContext } from "@gram/core/dist/data/providers/RequestContext.js"; + +export class StaticTeamProvider implements TeamProvider { + constructor( + public teams: Team[], + public UserIdToTeamIds: Map + ) {} + + async lookup(ctx: RequestContext, teamIds: string[]): Promise { + return teamIds + .map((tid) => this.teams.find((team) => team.id === tid)) + .filter((t) => t) as Team[]; + } + + async getTeamsForUser(ctx: RequestContext, userId: string): Promise { + const teamIds = this.UserIdToTeamIds.get(userId) || []; + return this.lookup(ctx, teamIds); + } + + key: string = "static"; +}