Skip to content

Commit

Permalink
Merge pull request #1007 from gchq/feature/entity-lookup
Browse files Browse the repository at this point in the history
Initial work on entity lookup
  • Loading branch information
a3957273 authored Jan 15, 2024
2 parents 08f2688 + 21029af commit 0dd28ff
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 5 deletions.
12 changes: 9 additions & 3 deletions backend/src/connectors/v2/authentication/Base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,24 @@ export const Roles = {
}
export type RoleKeys = (typeof Roles)[keyof typeof Roles]

export interface UserInformation {
name?: string
organisation?: string
email?: string
}

export abstract class BaseAuthenticationConnector {
abstract getUserFromReq(req: Request): Promise<User>
abstract hasRole(user: UserDoc, role: RoleKeys): Promise<boolean>

abstract queryEntities(query: string): Promise<Array<{ kind: string; id: string }>>
abstract getEntities(user: UserDoc): Promise<Array<string>>
abstract getUserInformation(userEntity: string): Promise<{ email: string }>
abstract getUserInformation(userEntity: string): Promise<UserInformation>
abstract getEntityMembers(entity: string): Promise<Array<string>>

async getUserInformationList(entity: string): Promise<Promise<{ email: string }>[]> {
async getUserInformationList(entity: string): Promise<UserInformation[]> {
const entities = await this.getEntityMembers(entity)
return entities.map((member) => this.getUserInformation(member))
return Promise.all(entities.map((member) => this.getUserInformation(member)))
}
async getUserModelRoles(user: UserDoc, model: ModelDoc) {
const entities = await this.getEntities(user)
Expand Down
6 changes: 4 additions & 2 deletions backend/src/connectors/v2/authentication/silly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Request } from 'express'

import { UserDoc } from '../../../models/v2/User.js'
import { fromEntity, toEntity } from '../../../utils/v2/entity.js'
import { BaseAuthenticationConnector, RoleKeys, Roles } from './Base.js'
import { BaseAuthenticationConnector, RoleKeys, Roles, UserInformation } from './Base.js'

const SillyEntityKind = {
User: 'user',
Expand Down Expand Up @@ -52,7 +52,7 @@ export class SillyAuthenticationConnector extends BaseAuthenticationConnector {
return [toEntity(SillyEntityKind.User, user.dn)]
}

async getUserInformation(entity: string): Promise<{ email: string }> {
async getUserInformation(entity: string): Promise<UserInformation> {
const { kind, value } = fromEntity(entity)

if (kind !== SillyEntityKind.User) {
Expand All @@ -61,6 +61,8 @@ export class SillyAuthenticationConnector extends BaseAuthenticationConnector {

return {
email: `${value}@example.com`,
name: 'Joe Bloggs',
organisation: 'Acme Corp',
}
}

Expand Down
2 changes: 2 additions & 0 deletions backend/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
} from './routes/v1/version.js'
import { getCurrentUser } from './routes/v2/entities/getCurrentUser.js'
import { getEntities } from './routes/v2/entities/getEntities.js'
import { getEntityLookup } from './routes/v2/entities/getEntityLookup.js'
import { deleteAccessRequest } from './routes/v2/model/accessRequest/deleteAccessRequest.js'
import { getAccessRequest } from './routes/v2/model/accessRequest/getAccessRequest.js'
import { getModelAccessRequests } from './routes/v2/model/accessRequest/getModelAccessRequests.js'
Expand Down Expand Up @@ -276,6 +277,7 @@ if (config.experimental.v2) {

server.get('/api/v2/entities', ...getEntities)
server.get('/api/v2/entities/me', ...getCurrentUser)
server.get('/api/v2/entity/:dn/lookup', ...getEntityLookup)

server.get('/api/v2/config/ui', ...getUiConfigV2)

Expand Down
50 changes: 50 additions & 0 deletions backend/src/routes/v2/entities/getEntityLookup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import bodyParser from 'body-parser'
import { Request, Response } from 'express'
import { z } from 'zod'

import { UserInformation } from '../../../connectors/v2/authentication/Base.js'
import authentication from '../../../connectors/v2/authentication/index.js'
import { registerPath, UserInformationSchema } from '../../../services/v2/specification.js'
import { toEntity } from '../../../utils/v2/entity.js'
import { parse } from '../../../utils/v2/validate.js'

export const getEntityLookupSchema = z.object({
params: z.object({
dn: z.string(),
}),
})

registerPath({
method: 'get',
path: '/api/v2/entity/{dn}/lookup',
tags: ['user'],
description: 'Get information about an entity',
schema: getEntityLookupSchema,
responses: {
200: {
description: 'Information about the provided entity.',
content: {
'application/json': {
schema: z.object({ entity: UserInformationSchema }),
},
},
},
},
})

interface GetEntityLookup {
entity: UserInformation
}

export const getEntityLookup = [
bodyParser.json(),
async (req: Request, res: Response<GetEntityLookup>) => {
const {
params: { dn },
} = parse(req, getEntityLookupSchema)

const information = await authentication.getUserInformation(toEntity('user', dn))

return res.json({ entity: information })
},
]
6 changes: 6 additions & 0 deletions backend/src/services/v2/specification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,9 @@ export const webhookInterfaceSchema = z.object({
createdAt: z.string().openapi({ example: new Date().toISOString() }),
updatedAt: z.string().openapi({ example: new Date().toISOString() }),
})

export const UserInformationSchema = z.object({
email: z.string().optional().openapi({ example: '[email protected]' }),
name: z.string().optional().openapi({ example: 'Joe Bloggs' }),
organisation: z.string().optional().openapi({ example: 'Acme Corp' }),
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`routes > entities > getEntityLookup > 200 > ok 1`] = `
[
[
"user:userdn",
],
]
`;

exports[`routes > entities > getEntityLookup > 200 > ok 2`] = `
{
"entity": {
"email": "[email protected]",
"name": "Joe Bloggs",
"organisation": "Acme Corp",
},
}
`;
38 changes: 38 additions & 0 deletions backend/test/routes/entities/getEntityLookup.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, expect, test, vi } from 'vitest'

import { testGet } from '../../testUtils/routes.js'

vi.mock('../../../src/utils/config.js')
vi.mock('../../../src/utils/user.js')
vi.mock('../../../src/utils/v2/config.js')

const authenticationMocks = vi.hoisted(() => ({
getUserFromReq: vi.fn(() => ({
dn: 'user',
})),
getUserInformation: vi.fn(() => ({
email: `[email protected]`,
name: 'Joe Bloggs',
organisation: 'Acme Corp',
})),
}))
vi.mock('../../../src/connectors/v2/authentication/index.js', async () => ({
default: authenticationMocks,
}))

vi.mock('../../../src/utils/v2/entity.js', async () => ({
toEntity: vi.fn(() => 'user:userdn'),
}))

describe('routes > entities > getEntityLookup', () => {
test('200 > ok', async () => {
vi.mock('../../../src/services/v2/model.js', () => ({
getModelById: vi.fn(() => ({ _id: 'test' })),
}))
const res = await testGet(`/api/v2/entity/userdn/lookup`)

expect(res.statusCode).toBe(200)
expect(authenticationMocks.getUserInformation.mock.calls).matchSnapshot()
expect(res.body).matchSnapshot()
})
})

0 comments on commit 0dd28ff

Please sign in to comment.