diff --git a/GUIDELINES.md b/GUIDELINES.md index 273d1838..9eb09c87 100644 --- a/GUIDELINES.md +++ b/GUIDELINES.md @@ -41,6 +41,7 @@ Detta dokument specificerar reglerna som verktyget tillämpar. - [ID: DOK.17](#id-dok17) - [ID: DOK.19](#id-dok19) - [ID: DOK.20](#id-dok20) + - [ID: DOK.21](#id-dok21) 2. [Område: Datum- och tidsformat](#område-datum--och-tidsformat) - [ID: DOT.01](#id-dot01) - [ID: DOT.04](#id-dot04) @@ -93,7 +94,7 @@ Detta dokument specificerar reglerna som verktyget tillämpar. ## Område: Dokumentation -**Täckningsgrad: 46%** +**Täckningsgrad: 50%** ### ID: DOK.01 @@ -405,6 +406,33 @@ I exemplet ovan, så exemplifieras regeln med GET samt en POST operation, där r --- +### ID: DOK.21 + +**Krav:** Krav på autentisering SKALL anges i specifikationen. + +**Typ:** SKALL + +**JSON Path Plus-uttryck:** + +``` +$ +``` + +**Förklaring:** +Regeln förutsätter att det finns minst en förekomst av objektet `security`, antingen på rot- eller operationsnivå. + +**Exempel:** + +![Exempelbild som visar var security-objektet kan existera i en OpenAPI description](images/dok21-1.png) + +_Security-objektet kan existera på antingen rot- eller operationsnivå, eller båda._ + +![Exempelbild som visar att security-objektet också kan användas när API:et saknar säkerhet](images/dok21-2.png) + +_Om säkerhet saknas så bör det signaleras genom att tilldela security-objektet en tom array._ + +--- + ## Område: Datum- och tidsformat **Täckningsgrad: 50%** diff --git a/REUSE.toml b/REUSE.toml index 39891cc0..77de43ea 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -30,6 +30,8 @@ path = [ "images/dok17.png", "images/dok19.png", "images/dok20.png", + "images/dok21-1.png", + "images/dok21-2.png", "images/dok3.png", "images/dok6-1.png", "images/dok6-2.png", diff --git a/images/dok21-1.png b/images/dok21-1.png new file mode 100644 index 00000000..2ef708e7 Binary files /dev/null and b/images/dok21-1.png differ diff --git a/images/dok21-2.png b/images/dok21-2.png new file mode 100644 index 00000000..f3ed040e Binary files /dev/null and b/images/dok21-2.png differ diff --git a/src/rulesets/DokRules.ts b/src/rulesets/DokRules.ts index 4b4ad446..fc618ff2 100644 --- a/src/rulesets/DokRules.ts +++ b/src/rulesets/DokRules.ts @@ -9,6 +9,13 @@ import { DiagnosticSeverity } from '@stoplight/types'; import { Dok03Base } from './rulesetUtil.js'; import { Dok15Base } from './rulesetUtil.js'; import { commonEnglishWords, commonSwedishWords } from './constants/CommonWords.js'; +import { + OpenAPIObject, + OperationObject, + PathItemObject, + PathsObject, + SecurityRequirementObject, +} from '../types/openapi-3.0.js'; const moduleName: string = 'DokRules.js'; export class Dok15Get extends Dok15Base { @@ -426,7 +433,71 @@ export class Dok19 extends BaseRuleset { } severity = DiagnosticSeverity.Error; } +export class Dok21 extends BaseRuleset { + static customProperties: CustomProperties = { + område: 'Dokumentation', + id: 'DOK.21', + }; + given = '$'; + message = 'Krav på autentisering SKALL anges i specifikationen.'; + then = [ + { + function: (targetVal: OpenAPIObject, _opts: string, paths: string[]) => { + const rootSecurity: SecurityRequirementObject[] | undefined = targetVal.security; + + if (rootSecurity) { + if (Array.isArray(rootSecurity)) { + return []; + } + } + + const hasSecurityInAnyPath = (paths: PathsObject | undefined): boolean => { + if (!paths) return false; + return Object.values(paths).some((pathItem: PathItemObject | undefined) => { + if (!pathItem) return false; + + return (Object.keys(pathItem) as (keyof PathItemObject)[]) + .filter((k) => ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'].includes(k)) + .some((k) => { + const op = pathItem[k] as OperationObject | undefined; + return op?.security; + }); + }); + }; + if (hasSecurityInAnyPath(targetVal.paths)) { + return []; + } + + return [ + { + message: this.message, + severity: this.severity, + paths: paths, + }, + ]; + }, + }, + { + function: (targetVal: string, _opts: string, paths: string[]) => { + this.trackRuleExecutionHandler( + JSON.stringify(targetVal, null, 2), + _opts, + paths, + this.severity, + this.constructor.name, + moduleName, + Dok21.customProperties, + ); + }, + }, + ]; + constructor() { + super(); + super.initializeFormats(['OAS3']); + } + severity = DiagnosticSeverity.Error; +} export class Dok01 extends BaseRuleset { static customProperties: CustomProperties = { område: 'Dokumentation', diff --git a/src/types/openapi-3.0.d.ts b/src/types/openapi-3.0.d.ts new file mode 100644 index 00000000..aa2ca438 --- /dev/null +++ b/src/types/openapi-3.0.d.ts @@ -0,0 +1,276 @@ +// SPDX-FileCopyrightText: 2025 Digg - Agency for Digital Government +// +// SPDX-License-Identifier: EUPL-1.2 + +/** + * TypeScript type definitions for OpenAPI 3.2.0 + * Based on: https://spec.openapis.org/oas/v3.2.0.html + */ + +export interface OpenAPIObject { + openapi: string; + info: InfoObject; + servers?: ServerObject[]; + paths?: PathsObject; + webhooks?: Record; + components?: ComponentsObject; + security?: SecurityRequirementObject[]; + tags?: TagObject[]; + externalDocs?: ExternalDocumentationObject; +} + +export interface InfoObject { + title: string; + summary?: string; + description?: string; + termsOfService?: string; + contact?: ContactObject; + license?: LicenseObject; + version: string; +} + +export interface ContactObject { + name?: string; + url?: string; + email?: string; +} + +export interface LicenseObject { + name: string; + identifier?: string; + url?: string; +} + +export interface ServerObject { + url: string; + description?: string; + name?: string; + variables?: Record; +} + +export interface ServerVariableObject { + enum?: string[]; + default: string; + description?: string; +} + +export interface PathsObject { + [path: string]: PathItemObject | undefined; +} + +export interface PathItemObject { + $ref?: string; + summary?: string; + description?: string; + get?: OperationObject; + put?: OperationObject; + post?: OperationObject; + delete?: OperationObject; + options?: OperationObject; + head?: OperationObject; + patch?: OperationObject; + trace?: OperationObject; + servers?: ServerObject[]; + parameters?: (ParameterObject | ReferenceObject)[]; + additionalOperations?: Record; +} + +export interface OperationObject { + tags?: string[]; + summary?: string; + description?: string; + externalDocs?: ExternalDocumentationObject; + operationId?: string; + parameters?: (ParameterObject | ReferenceObject)[]; + requestBody?: RequestBodyObject | ReferenceObject; + responses: ResponsesObject; + callbacks?: Record; + deprecated?: boolean; + security?: SecurityRequirementObject[]; + servers?: ServerObject[]; +} + +export interface ExternalDocumentationObject { + description?: string; + url: string; +} + +export interface ParameterObject { + name: string; + in: 'query' | 'header' | 'path' | 'cookie'; + description?: string; + required?: boolean; + deprecated?: boolean; + allowEmptyValue?: boolean; + + style?: string; + explode?: boolean; + allowReserved?: boolean; + schema?: SchemaObject | ReferenceObject; + example?: any; + examples?: Record; + content?: Record; +} + +export interface RequestBodyObject { + description?: string; + content: Record; + required?: boolean; +} + +export interface MediaTypeObject { + schema?: SchemaObject | ReferenceObject; + example?: any; + examples?: Record; + encoding?: Record; +} + +export interface EncodingObject { + contentType?: string; + headers?: Record; + style?: string; + explode?: boolean; + allowReserved?: boolean; +} + +export interface ResponsesObject { + [statusCode: string]: ResponseObject | ReferenceObject; +} + +export interface ResponseObject { + description: string; + headers?: Record; + content?: Record; + links?: Record; +} + +export interface CallbackObject { + [expression: string]: PathItemObject; +} + +export interface ExampleObject { + summary?: string; + description?: string; + value?: any; + externalValue?: string; +} + +export interface LinkObject { + operationRef?: string; + operationId?: string; + parameters?: Record; + requestBody?: any; + description?: string; + server?: ServerObject; +} + +export interface HeaderObject extends Omit {} + +export interface TagObject { + name: string; + summary?: string; + description?: string; + externalDocs?: ExternalDocumentationObject; + parent?: string; + kind?: string; +} + +export interface ReferenceObject { + $ref: string; +} + +export interface SchemaObject { + title?: string; + multipleOf?: number; + maximum?: number; + exclusiveMaximum?: boolean; + minimum?: number; + exclusiveMinimum?: boolean; + maxLength?: number; + minLength?: number; + pattern?: string; + maxItems?: number; + minItems?: number; + uniqueItems?: boolean; + maxProperties?: number; + minProperties?: number; + required?: string[]; + enum?: any[]; + + type?: string; + allOf?: (SchemaObject | ReferenceObject)[]; + oneOf?: (SchemaObject | ReferenceObject)[]; + anyOf?: (SchemaObject | ReferenceObject)[]; + not?: SchemaObject | ReferenceObject; + items?: SchemaObject | ReferenceObject; + properties?: Record; + additionalProperties?: boolean | SchemaObject | ReferenceObject; + description?: string; + format?: string; + default?: any; + nullable?: boolean; + discriminator?: DiscriminatorObject; + readOnly?: boolean; + writeOnly?: boolean; + xml?: XMLObject; + externalDocs?: ExternalDocumentationObject; + example?: any; + deprecated?: boolean; +} + +export interface DiscriminatorObject { + propertyName: string; + mapping?: Record; +} + +export interface XMLObject { + name?: string; + namespace?: string; + prefix?: string; + attribute?: boolean; + wrapped?: boolean; + + nodeType?: 'element' | 'attribute' | 'text' | 'cdata' | 'none'; +} + +export interface SecurityRequirementObject { + [name: string]: string[]; +} + +export interface ComponentsObject { + schemas?: Record; + responses?: Record; + parameters?: Record; + examples?: Record; + requestBodies?: Record; + headers?: Record; + securitySchemes?: Record; + links?: Record; + callbacks?: Record; + webhooks?: Record; +} + +export interface SecuritySchemeObject { + type: 'apiKey' | 'http' | 'oauth2' | 'openIdConnect'; + description?: string; + name?: string; + in?: 'query' | 'header' | 'cookie'; + scheme?: string; + bearerFormat?: string; + flows?: OAuthFlowsObject; + openIdConnectUrl?: string; +} + +export interface OAuthFlowsObject { + implicit?: OAuthFlowObject; + password?: OAuthFlowObject; + clientCredentials?: OAuthFlowObject; + authorizationCode?: OAuthFlowObject; +} + +export interface OAuthFlowObject { + authorizationUrl?: string; + tokenUrl?: string; + refreshUrl?: string; + scopes: Record; +} diff --git a/tests/unit/dok.test.ts b/tests/unit/dok.test.ts index 06e2ff31..571559d4 100644 --- a/tests/unit/dok.test.ts +++ b/tests/unit/dok.test.ts @@ -510,7 +510,105 @@ testRule('Dok19', [ ], }, ]); - +testRule('Dok21', [ + { + name: 'ogiltigt testfall - security saknas helt', + document: { + openapi: '3.1.0', + info: { version: '1.0' }, + servers: [{ url: 'https://example.com/my-api/v1' }], + paths: { + '/pets': { + get: { + description: 'Retrieve all pets', + }, + }, + }, + }, + errors: [ + { + message: 'Krav på autentisering SKALL anges i specifikationen.', + severity: DiagnosticSeverity.Error, + }, + ], + }, + { + name: 'giltigt testfall - security finns på rootnivå och består utav en tom array', + document: { + openapi: '3.1.0', + info: { version: '1.0' }, + servers: [{ url: 'https://example.com/my-api/v1' }], + security: [], + paths: { + '/pets': { + get: { + description: 'Retrieve all pets', + }, + }, + }, + }, + errors: [], + }, + { + name: 'giltigt testfall - security finns på rootnivå och innehåller ett objekt', + document: { + openapi: '3.1.0', + info: { version: '1.0' }, + servers: [{ url: 'https://example.com/my-api/v1' }], + security: [ + { + OAuth2: [], + }, + ], + paths: { + '/pets': { + get: { + description: 'Retrieve all pets', + }, + }, + }, + }, + errors: [], + }, + { + name: 'giltigt testfall - security finns på operationsnivå och består utav en tom array', + document: { + openapi: '3.1.0', + info: { version: '1.0' }, + servers: [{ url: 'https://example.com/my-api/v1' }], + paths: { + '/pets': { + get: { + description: 'Retrieve all pets', + security: [], + }, + }, + }, + }, + errors: [], + }, + { + name: 'giltigt testfall - security finns på operationsnivå och innehåller ett objekt', + document: { + openapi: '3.1.0', + info: { version: '1.0' }, + servers: [{ url: 'https://example.com/my-api/v1' }], + paths: { + '/pets': { + get: { + description: 'Retrieve all pets', + security: [ + { + OAuth2: [], + }, + ], + }, + }, + }, + }, + errors: [], + }, +]); testRule('Dok01', [ { name: 'giltigt testfall', diff --git a/tests/util/rulesetTest.ts b/tests/util/rulesetTest.ts index 2f2a4851..f149c02c 100644 --- a/tests/util/rulesetTest.ts +++ b/tests/util/rulesetTest.ts @@ -72,6 +72,7 @@ const ruleTypes = [ DokRules.Dok15ReqBody, DotRules.Dot04, DokRules.Dok19, + DokRules.Dok21, DokRules.Dok01, DokRules.Dok11, DokRules.Dok17,