mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-05-04 19:56:37 +02:00
docs(openapi): document apikey auth in openapi.json (#7534)
* docs(openapi): document apikey auth in openapi.json (#7532) The API accepts the key via ?apikey=, ?api_key=, or the apikey header, but only ?apikey= was advertised in /api-docs.json. /api/{version}/openapi.json was worse: it hardcoded an OAuth2 scheme even when Etherpad was started in apikey auth mode. Switch both generators on settings.authenticationMethod and publish apiKey schemes for the query (apikey, api_key) and header (apikey) variants. The openapi.ts definition is now regenerated per request so runtime settings are reflected. The raw authorization: <key> header still works in code but is deliberately not documented — pinning it in the spec would ossify a quirk. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(openapi): add apiKeyAlias/apiKeyHeader conditionally in RestAPI.ts In SSO mode, apiKeyAlias and apiKeyHeader were always present in securitySchemes even though they're only relevant when authenticationMethod is 'apikey'. Mirror the pattern used for the sso scheme: add these two schemes dynamically inside the apikey branch, and mark them optional in the TypeScript type annotation. Agent-Logs-Url: https://github.com/ether/etherpad/sessions/1d440432-7389-462e-9aac-9a3c027640e8 Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com>
This commit is contained in:
parent
0206e0447c
commit
66f49bb808
@ -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": {
|
||||
@ -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 = {
|
||||
|
||||
@ -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])) {
|
||||
@ -566,14 +584,16 @@ 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
|
||||
@ -581,7 +601,8 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||
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)]});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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…
x
Reference in New Issue
Block a user