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
26 changes: 23 additions & 3 deletions src/node/handler/RestAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,16 @@ const prepareDefinition = (mapping: Map<string, Record<string, RestAPIMapping>>,
"in": string

},
"apiKeyAlias"?: {
"type": string,
"name": string,
"in": string
},
"apiKeyHeader"?: {
"type": string,
"name": string,
"in": string
},
"sso"?: {
"type": string,
"flows": {
Expand Down Expand Up @@ -255,10 +265,20 @@ const prepareDefinition = (mapping: Map<string, Record<string, RestAPIMapping>>,
}

if (authenticationMethod === "apikey") {
definitions.components.securitySchemes.apiKeyAlias = {
type: "apiKey",
name: "api_key",
in: "query",
};
definitions.components.securitySchemes.apiKeyHeader = {
type: "apiKey",
name: "apikey",
in: "header",
};
definitions.security = [
{
"apiKey": []
}
{"apiKey": []},
{"apiKeyAlias": []},
{"apiKeyHeader": []},
]
} else if (authenticationMethod === "sso") {
definitions.components.securitySchemes.sso = {
Expand Down
63 changes: 42 additions & 21 deletions src/node/hooks/express/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,26 +482,44 @@ const generateDefinitionForVersion = (version:string, style = APIPathStyle.FLAT)
responses: {
...defaultResponses,
},
securitySchemes: {
openid: {
type: "oauth2",
flows: {
authorizationCode: {
authorizationUrl: settings.sso.issuer+"/oidc/auth",
tokenUrl: settings.sso.issuer+"/oidc/token",
scopes: {
openid: "openid",
profile: "profile",
email: "email",
admin: "admin"
}
}
securitySchemes: {} as Record<string, any>,
},
security: [] as Array<Record<string, string[]>>,
};

if (settings.authenticationMethod === 'apikey') {
definition.components.securitySchemes.apiKey = {
type: 'apiKey', name: 'apikey', in: 'query',
};
definition.components.securitySchemes.apiKeyAlias = {
type: 'apiKey', name: 'api_key', in: 'query',
};
definition.components.securitySchemes.apiKeyHeader = {
type: 'apiKey', name: 'apikey', in: 'header',
};
definition.security = [
{apiKey: []},
{apiKeyAlias: []},
{apiKeyHeader: []},
];
} else {
definition.components.securitySchemes.openid = {
type: 'oauth2',
flows: {
authorizationCode: {
authorizationUrl: settings.sso.issuer + '/oidc/auth',
tokenUrl: settings.sso.issuer + '/oidc/token',
scopes: {
openid: 'openid',
profile: 'profile',
email: 'email',
admin: 'admin',
},
},
},
},
security: [{openid: []}],
};
};
definition.security = [{openid: []}];
}

// build operations
for (const funcName of Object.keys(apiHandler.version[version])) {
Expand Down Expand Up @@ -566,22 +584,25 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
for (const style of [APIPathStyle.FLAT, APIPathStyle.REST]) {
const apiRoot = getApiRootForVersion(version, style);

// generate openapi definition for this API version
// generate openapi definition for this API version (used for openapi-backend routing)
const definition = generateDefinitionForVersion(version, style);

// serve version specific openapi definition
// serve version specific openapi definition; regenerate per request so runtime
// settings (e.g. authenticationMethod) are reflected
app.get(`${apiRoot}/openapi.json`, (req:any, res:any) => {
// For openapi definitions, wide CORS is probably fine
res.header('Access-Control-Allow-Origin', '*');
res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]});
const liveDefinition = generateDefinitionForVersion(version, style);
res.json({...liveDefinition, servers: [generateServerForApiVersion(apiRoot, req)]});
});

// serve latest openapi definition file under /api/openapi.json
const isLatestAPIVersion = version === apiHandler.latestApiVersion;
if (isLatestAPIVersion) {
app.get(`/${style}/openapi.json`, (req:any, res:any) => {
res.header('Access-Control-Allow-Origin', '*');
res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]});
const liveDefinition = generateDefinitionForVersion(version, style);
res.json({...liveDefinition, servers: [generateServerForApiVersion(apiRoot, req)]});
});
}

Expand Down
58 changes: 58 additions & 0 deletions src/tests/backend/specs/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

const common = require('../../common');
const validateOpenAPI = require('openapi-schema-validation').validate;
import settings from '../../../../node/utils/Settings';

let agent: any;
let apiVersion = 1;
Expand Down Expand Up @@ -54,4 +55,61 @@ describe(__filename, function () {
}
});
});

describe('security schemes with authenticationMethod=apikey', function () {
let originalAuthMethod: string;

before(function () {
originalAuthMethod = settings.authenticationMethod;
settings.authenticationMethod = 'apikey';
});

after(function () {
settings.authenticationMethod = originalAuthMethod;
});

it('/api-docs.json documents apikey query param (primary name)', async function () {
const res = await agent.get('/api-docs.json').expect(200);
const schemes = res.body.components.securitySchemes;
const apiKeyQuery = Object.values(schemes).find(
(s: any) => s.type === 'apiKey' && s.in === 'query' && s.name === 'apikey');
if (!apiKeyQuery) {
throw new Error(`Expected apiKey query param 'apikey' in securitySchemes: ` +
`${JSON.stringify(schemes)}`);
}
});

it('/api-docs.json documents api_key query param alias', async function () {
const res = await agent.get('/api-docs.json').expect(200);
const schemes = res.body.components.securitySchemes;
const apiKeyQueryAlias = Object.values(schemes).find(
(s: any) => s.type === 'apiKey' && s.in === 'query' && s.name === 'api_key');
if (!apiKeyQueryAlias) {
throw new Error(`Expected apiKey query param 'api_key' in securitySchemes: ` +
`${JSON.stringify(schemes)}`);
}
});

it('/api-docs.json documents apikey header', async function () {
const res = await agent.get('/api-docs.json').expect(200);
const schemes = res.body.components.securitySchemes;
const apiKeyHeader = Object.values(schemes).find(
(s: any) => s.type === 'apiKey' && s.in === 'header' && s.name === 'apikey');
if (!apiKeyHeader) {
throw new Error(`Expected apiKey header 'apikey' in securitySchemes: ` +
`${JSON.stringify(schemes)}`);
}
});

it('/api/openapi.json exposes apiKey security in apikey mode', async function () {
this.timeout(15000);
const res = await agent.get('/api/openapi.json').expect(200);
const schemes = res.body.components.securitySchemes;
const hasApiKey = Object.values(schemes).some((s: any) => s.type === 'apiKey');
if (!hasApiKey) {
throw new Error(`Expected at least one apiKey securityScheme in ` +
`/api/openapi.json, got: ${JSON.stringify(schemes)}`);
}
});
});
});
Loading