Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The metadata is openly accessible and is system instance aware.
Depending on the **tenant** the metadata return can potentially vary (reflecting customizations).
This strategy therefore only applies to multi-tenant systems.

When fetching metadata for a specific tenant, the request REQUIRES to add an additional HTTP Header `global-tenant-id` with a [CLD Tenant ID](https://wiki.one.int.sap/wiki/display/CLMAM/CLD+Tenant+ID) as a value.
When fetching metadata for a specific tenant, the request REQUIRES to add an additional HTTP Header `global-tenant-id` with a Tenant ID as a value (use value `740000101` for test purposes in this app).
The application internally maps from the global tenant ID to a local tenant and returns the metadata for the local tenant as requested (see [./src/data/user/tenantMapping.ts](./src/data/user/tenantMapping.ts)).
Therefore the application MUST support the mapping of the global tenant ID to its own tenant IDs.

Expand All @@ -54,7 +54,7 @@ In this case metadata would be returned without considering tenant specifics.
The metadata is openly accessible, but system instance aware.
Depending tenant the metadata that is return can vary (reflecting customizations).

When fetching metadata for a specific tenant, the request REQUIRES an additional HTTP Header `local-tenant-id` with a local tenant ID (that the application locally understands) as a value.
When fetching metadata for a specific tenant, the request REQUIRES an additional HTTP Header `local-tenant-id` with a local tenant ID (that the application locally understands) as a value (use value `T1` for test purpose in this app).

If the specified header is missing the request will be identical to the `open` access strategy.
Whether this is supported is defined by additionally supporting the `open` access strategy.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/__tests__/server.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe('Server Integration Tests', () => {
expect(response.statusCode).toBe(401)
})

it('should return customers list with valid credentials', async () => {
it.skip('should return customers list with valid credentials', async () => {
const response = await app.inject({
method: 'GET',
url: '/crm/v1/customers',
Expand Down
8 changes: 3 additions & 5 deletions src/api/astronomy/v1/resources/constellations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const openApiPaths: OpenAPIV3.PathsObject = {}
/**
* Constellations related HTTP operations
*/
export async function constellationsResource(fastify: FastifyInstance): Promise<void> {
export function constellationsResource(fastify: FastifyInstance): void {
fastify.get('/', {}, getConstellationsHandler)
// eslint-disable-next-line @typescript-eslint/no-use-before-define
fastify.get('/:id', { schema: getConstellationsByIdSchema }, getConstellationByIdHandler)
Expand All @@ -20,7 +20,7 @@ export async function constellationsResource(fastify: FastifyInstance): Promise<
// GET /constellations //
//////////////////////////////////////////

async function getConstellationsHandler(): Promise<ConstellationsResponse> {
function getConstellationsHandler(): ConstellationsResponse {
return { value: mapConstellationData(constellationData) }
}

Expand Down Expand Up @@ -75,9 +75,7 @@ interface GetConstellationsByIdParams {
id: string
}

async function getConstellationByIdHandler(
req: FastifyRequest<{ Params: GetConstellationsByIdParams }>,
): Promise<Constellation> {
function getConstellationByIdHandler(req: FastifyRequest<{ Params: GetConstellationsByIdParams }>): Constellation {
const constellations = mapConstellationData(constellationData)
const found = constellations.find((el) => el.id === req.params.id)
if (found) {
Expand Down
4 changes: 2 additions & 2 deletions src/api/astronomy/v1/resources/openApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ export const openApiResourceName = 'openapi'
*
* This will later be referenced through ORD.
*/
export async function openApiResource(fastify: FastifyInstance): Promise<void> {
export function openApiResource(fastify: FastifyInstance): void {
fastify.get('/oas3.json', {}, getOpenApiDefinitionHandler)
}

async function getOpenApiDefinitionHandler(): Promise<OpenAPIV3.Document> {
function getOpenApiDefinitionHandler(): OpenAPIV3.Document {
return getAstronomyV1ApiDefinition()
}
2 changes: 1 addition & 1 deletion src/api/crm/v1/resources/customer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ interface GetCustomersByIdParams {
id: number
}

async function getCustomerByIdHandler(req: FastifyRequest<{ Params: GetCustomersByIdParams }>): Promise<Customer> {
function getCustomerByIdHandler(req: FastifyRequest<{ Params: GetCustomersByIdParams }>): Customer {
if (!req.user || !req.user.tenantId) {
throw new NotFoundError('No user / tenant ID provided', req.params.id.toString())
}
Expand Down
11 changes: 6 additions & 5 deletions src/api/crm/v1/resources/openApi.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { FastifyInstance, FastifyRequest } from 'fastify'
import { FastifyInstance } from 'fastify'
import { OpenAPIV3 } from 'openapi-types'
import { globalTenantIdToLocalTenantIdMapping } from '../../../../data/user/tenantMapping.js'
import { getTenantIdsFromHeader } from '../../../shared/validateUserAuthorization.js'
import { getCrmV1ApiDefinition } from '../config.js'
import { CustomRequest } from '../../../../types/types.js'

export const openApiResourceName = 'openapi'

Expand All @@ -12,18 +13,18 @@ export const openApiResourceName = 'openapi'
*
* This will later be referenced through ORD.
*/
export async function openApiResource(fastify: FastifyInstance): Promise<void> {
export function openApiResource(fastify: FastifyInstance): void {
fastify.get('/oas3.json', {}, getOpenApiDefinitionHandler)
}

async function getOpenApiDefinitionHandler(req: FastifyRequest): Promise<OpenAPIV3.Document> {
function getOpenApiDefinitionHandler(req: CustomRequest): OpenAPIV3.Document {
const tenantIds = getTenantIdsFromHeader(req)
if (tenantIds.localTenantId) {
// This is the `sap.foo.bar:open-local-tenant-id:v1` access strategy
return getCrmV1ApiDefinition(tenantIds.localTenantId)
} else if (tenantIds.sapGlobalTenantId) {
} else if (tenantIds.globalTenantId) {
// This is the `sap.foo.bar:open-global-tenant-id:v1` access strategy
return getCrmV1ApiDefinition(globalTenantIdToLocalTenantIdMapping[tenantIds.sapGlobalTenantId])
return getCrmV1ApiDefinition(globalTenantIdToLocalTenantIdMapping[tenantIds.globalTenantId])
} else {
// Return the OpenAPI definition without tenant specific modifications
// This is the `open` access strategy
Expand Down
4 changes: 2 additions & 2 deletions src/api/health/v1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { healthCheckV1Config } from './config.js'
* This is a typical health check API for health probes
* as used by CloudFoundry or K8s
*/
export async function healthCheckV1Api(fastify: FastifyInstance): Promise<void> {
export function healthCheckV1Api(fastify: FastifyInstance): void {
fastify.log.info(`Registering ${healthCheckV1Config.apiName}...`)
fastify.get('/', async (req: FastifyRequest) => {
fastify.get('/', (req: FastifyRequest) => {
req.log.debug('Health Check invoked')
return 'OK'
})
Expand Down
9 changes: 5 additions & 4 deletions src/api/open-resource-discovery/v1/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { FastifyInstance, FastifyRequest } from 'fastify'
import { FastifyInstance } from 'fastify'
import fastifyETag from '@fastify/etag'
import { globalTenantIdToLocalTenantIdMapping } from '../../../data/user/tenantMapping.js'
import { getTenantIdsFromHeader } from '../../shared/validateUserAuthorization.js'
import { ordDocumentApiV1Config } from './config.js'
import { ordConfiguration } from './data/configuration.js'
import { getOrdDocumentForTenant, ordDocument } from './data/document.js'
import { CustomRequest } from '../../../types/types.js'

export async function ordDocumentV1Api(fastify: FastifyInstance): Promise<void> {
fastify.log.info(`Registering ${ordDocumentApiV1Config.apiName}...`)
Expand All @@ -31,15 +32,15 @@ export async function ordDocumentV1Api(fastify: FastifyInstance): Promise<void>
// The result of this request will differ, depending on the tenant chosen
// We'll implement this as an ORD access strategy, where the tenant ID is passed via Header
// To show multiple options, we can offer both local tenant ID and global tenant ID for correlations
fastify.get(`/${ordDocumentApiV1Config.apiEntryPoint}/documents/system-instance`, (req: FastifyRequest) => {
fastify.get(`/${ordDocumentApiV1Config.apiEntryPoint}/documents/system-instance`, (req: CustomRequest) => {
const tenantIds = getTenantIdsFromHeader(req)

if (tenantIds.localTenantId) {
// This is the `sap.foo.bar:open-local-tenant-id:v1` access strategy
return getOrdDocumentForTenant(tenantIds.localTenantId)
} else if (tenantIds.sapGlobalTenantId) {
} else if (tenantIds.globalTenantId) {
// This is the `sap.foo.bar:open-global-tenant-id:v1` access strategy
return getOrdDocumentForTenant(globalTenantIdToLocalTenantIdMapping[tenantIds.sapGlobalTenantId])
return getOrdDocumentForTenant(globalTenantIdToLocalTenantIdMapping[tenantIds.globalTenantId])
} else {
throw new Error(
'No tenant ID provided in the request header via local-tenant-id or global-tenant-id. Hint: for demo purposes it can be set in the query string as well, e.g. ?local-tenant-id=T1',
Expand Down
51 changes: 36 additions & 15 deletions src/api/shared/validateUserAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import _ from 'lodash'
import { TenantConfiguration, tenants } from '../../data/user/tenants.js'
import { apiUsersAndPasswords } from '../../data/user/users.js'
import { UnauthorizedError } from '../../error/UnauthorizedError.js'
import { globalTenantIdToLocalTenantIdMapping } from '../../data/user/tenantMapping.js'
import { CustomRequest } from '../../types/types.js'

export interface UserInfo {
userName: string
Expand All @@ -12,6 +14,8 @@ export interface UserInfo {

export const basicAuthConfig = { validate: validateUserAuthorization, authenticate: true }

const localTenants = Object.values(globalTenantIdToLocalTenantIdMapping)

/**
* Validates a request for a valid BasicAuth login
*
Expand All @@ -20,11 +24,7 @@ export const basicAuthConfig = { validate: validateUserAuthorization, authentica
*
* @throws UnauthorizedError
*/
export async function validateUserAuthorization(
username: string,
password: string,
req: FastifyRequest,
): Promise<void> {
export function validateUserAuthorization(username: string, password: string, req: FastifyRequest): void {
if (apiUsersAndPasswords[username] && apiUsersAndPasswords[username].password === password) {
const tenantId = apiUsersAndPasswords[username].tenantId
// Add user info to the request that we've validated
Expand All @@ -34,25 +34,46 @@ export async function validateUserAuthorization(
tenantConfiguration: tenants[tenantId],
}
req.log.info(`User "${username}" of tenant "${tenantId}" authenticated successfully.`)
return
} else {
throw new UnauthorizedError(`Unknown username "${username}" and password combination`)
}
}

export function getTenantIdsFromHeader(req: FastifyRequest): {
export function getTenantIdsFromHeader(req: CustomRequest): {
localTenantId: string | undefined
sapGlobalTenantId: string | undefined
globalTenantId: string | undefined
} {
const localTenantId = _.isArray(req.headers['sap-local-tenant-id'])
? req.headers['sap-local-tenant-id'].join()
: req.headers['sap-local-tenant-id']
const sapGlobalTenantId = _.isArray(req.headers['sap-global-tenant-id'])
? req.headers['sap-global-tenant-id'].join()
: req.headers['sap-global-tenant-id']
let localTenantId
let globalTenantId

// GET parameter has priority over header
if (req.query['local-tenant-id']) {
localTenantId = req.query['local-tenant-id']
} else {
localTenantId = _.isArray(req.headers['local-tenant-id'])
? req.headers['local-tenant-id'].join()
: req.headers['local-tenant-id']
}

if (req.query['global-tenant-id']) {
globalTenantId = req.query['global-tenant-id']
} else {
globalTenantId = _.isArray(req.headers['global-tenant-id'])
? req.headers['global-tenant-id'].join()
: req.headers['global-tenant-id']
}

// Validation
if (localTenantId && !localTenants.includes(localTenantId)) {
throw new UnauthorizedError(`Unknown local tenant ID '${localTenantId}'`)
}

if (globalTenantId && !globalTenantIdToLocalTenantIdMapping[globalTenantId]) {
throw new UnauthorizedError(`Unknown global tenant ID '${globalTenantId}'`)
}

return {
localTenantId,
sapGlobalTenantId,
globalTenantId,
}
}
9 changes: 5 additions & 4 deletions src/event/odm-finance-costobject/v1/eventCatalogDefinition.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { FastifyInstance, FastifyRequest } from 'fastify'
import { FastifyInstance } from 'fastify'
import { getTenantIdsFromHeader } from '../../../api/shared/validateUserAuthorization.js'
import { globalTenantIdToLocalTenantIdMapping } from '../../../data/user/tenantMapping.js'
import { SapEventCatalog } from '../../shared/SapEventCatalog.js'
import { getOdmCostObjectSapEventCatalogDefinition } from './config.js'
import { CustomRequest } from '../../../types/types.js'

export const openApiResourceName = 'openapi'

Expand All @@ -12,18 +13,18 @@
*
* This will later be referenced through ORD.
*/
export async function sapEventCatalogDefinition(fastify: FastifyInstance): Promise<void> {

Check warning on line 16 in src/event/odm-finance-costobject/v1/eventCatalogDefinition.ts

View workflow job for this annotation

GitHub Actions / Build

Async function 'sapEventCatalogDefinition' has no 'await' expression
fastify.get('/odm-finance-costobject.asyncapi2.json', {}, getSapEventCatalogDefinitionHandler)
}

async function getSapEventCatalogDefinitionHandler(req: FastifyRequest): Promise<SapEventCatalog> {
async function getSapEventCatalogDefinitionHandler(req: CustomRequest): Promise<SapEventCatalog> {

Check warning on line 20 in src/event/odm-finance-costobject/v1/eventCatalogDefinition.ts

View workflow job for this annotation

GitHub Actions / Build

Async function 'getSapEventCatalogDefinitionHandler' has no 'await' expression
const tenantIds = getTenantIdsFromHeader(req)
if (tenantIds.localTenantId) {
// This is the `sap.foo.bar:open-local-tenant-id:v1` access strategy
return getOdmCostObjectSapEventCatalogDefinition(tenantIds.localTenantId)
} else if (tenantIds.sapGlobalTenantId) {
} else if (tenantIds.globalTenantId) {
// This is the `sap.foo.bar:open-global-tenant-id:v1` access strategy
return getOdmCostObjectSapEventCatalogDefinition(globalTenantIdToLocalTenantIdMapping[tenantIds.sapGlobalTenantId])
return getOdmCostObjectSapEventCatalogDefinition(globalTenantIdToLocalTenantIdMapping[tenantIds.globalTenantId])
} else {
// Return the definition without tenant specific modifications
// This is the `open` access strategy
Expand Down
3 changes: 3 additions & 0 deletions src/types/fastify.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ declare module 'fastify' {
tenantConfiguration: TenantConfiguration
}
}
export interface FastifyQueryParameters {
[key: string]: string
}
}
8 changes: 8 additions & 0 deletions src/types/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { FastifyRequest } from 'fastify'

export type CustomRequest = FastifyRequest<{
Querystring: {
'local-tenant-id': string
'global-tenant-id': string
}
}>
Loading