Skip to content
Open
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
112 changes: 112 additions & 0 deletions Sources/OpenAPIRuntime/Interface/SecurityRequirement.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftOpenAPIGenerator open source project
//
// Copyright (c) 2026 Apple Inc. and the SwiftOpenAPIGenerator project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// A security requirement of an OpenAPI operation.
///
/// A security requirement is an *AND-group* of security schemes: to satisfy the
/// requirement, a request must satisfy *every* scheme in ``schemes``.
///
/// An operation carries an array of security requirements, which is an
/// *OR-group* of alternatives: satisfying any one requirement in the array
/// authorizes the request. An empty array (`[]`) means the operation explicitly
/// opts out of security and is public.
public struct SecurityRequirement: Sendable, Hashable {

/// The security schemes that must all be satisfied to fulfill this requirement.
public var schemes: [Scheme]

/// Creates a new security requirement.
/// - Parameter schemes: The security schemes that must all be satisfied.
public init(schemes: [Scheme]) { self.schemes = schemes }

/// A named security scheme used by an operation, together with the scopes
/// the operation requires for it.
public struct Scheme: Sendable, Hashable {

/// The name of the security scheme, as declared in the OpenAPI
/// document's `components.securitySchemes`.
public var name: String

/// The kind of the security scheme, describing how and where a
/// credential is provided.
public var kind: Kind

/// The scopes the operation requires for this scheme.
///
/// Meaningful only for ``Kind/oauth2`` and ``Kind/openIdConnect(url:)``
/// schemes; empty for all others.
public var scopes: [String]

/// Creates a new security scheme reference.
/// - Parameters:
/// - name: The name of the security scheme.
/// - kind: The kind of the security scheme.
/// - scopes: The scopes the operation requires for this scheme.
public init(name: String, kind: Kind, scopes: [String]) {
self.name = name
self.kind = kind
self.scopes = scopes
}

/// The kind of an OpenAPI security scheme.
///
/// Models the security scheme types defined by the OpenAPI
/// Specification. Describes the scheme definition — how a credential is
/// carried — not the per-operation scopes, which live on
/// ``Scheme/scopes``.
public enum Kind: Sendable, Hashable {

/// An API key carried in a header field, query item, or cookie.
/// - Parameters:
/// - name: The name of the header field, query item, or cookie.
/// - location: Where the API key is carried in the request.
case apiKey(name: String, location: Location)

/// An HTTP authentication scheme, for example `basic` or `bearer`.
/// - Parameters:
/// - scheme: The HTTP Authorization scheme, lowercased, for
/// example `bearer`.
/// - bearerFormat: A hint describing the bearer token format, if
/// provided by the document.
case http(scheme: String, bearerFormat: String?)

/// An OAuth 2.0 security scheme.
///
/// The scopes the operation requires are carried on
/// ``Scheme/scopes``.
case oauth2

/// An OpenID Connect security scheme.
/// - Parameter url: The OpenID Connect discovery URL, as written in
/// the OpenAPI document.
case openIdConnect(url: String)

/// A mutual-TLS security scheme (OpenAPI 3.1).
case mutualTLS
}

/// The location in an HTTP request where a security credential is carried.
public enum Location: String, Sendable, Hashable {

/// Carried in a URL query item.
case query

/// Carried in an HTTP header field.
case header

/// Carried in a cookie.
case cookie
}
}
}
103 changes: 103 additions & 0 deletions Tests/OpenAPIRuntimeTests/Interface/Test_SecurityRequirement.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftOpenAPIGenerator open source project
//
// Copyright (c) 2026 Apple Inc. and the SwiftOpenAPIGenerator project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import XCTest
@testable import OpenAPIRuntime

final class Test_SecurityRequirement: XCTestCase {

func testLocationRawValues() {
XCTAssertEqual(SecurityRequirement.Scheme.Location.query.rawValue, "query")
XCTAssertEqual(SecurityRequirement.Scheme.Location.header.rawValue, "header")
XCTAssertEqual(SecurityRequirement.Scheme.Location.cookie.rawValue, "cookie")
}

func testEachKind() {
// Construct one value per kind and assert stored properties round-trip.
let apiKey = SecurityRequirement.Scheme(
name: "apiKeyAuth",
kind: .apiKey(name: "X-API-Key", location: .header),
scopes: []
)
guard case let .apiKey(name, location) = apiKey.kind else { return XCTFail("expected apiKey") }
XCTAssertEqual(name, "X-API-Key")
XCTAssertEqual(location, .header)

let bearer = SecurityRequirement.Scheme(
name: "bearerAuth",
kind: .http(scheme: "bearer", bearerFormat: "JWT"),
scopes: []
)
guard case let .http(scheme, format) = bearer.kind else { return XCTFail("expected http") }
XCTAssertEqual(scheme, "bearer")
XCTAssertEqual(format, "JWT")

let basic = SecurityRequirement.Scheme(
name: "basicAuth",
kind: .http(scheme: "basic", bearerFormat: nil),
scopes: []
)
guard case .http(_, let noFormat) = basic.kind else { return XCTFail("expected http") }
XCTAssertNil(noFormat)

let oauth = SecurityRequirement.Scheme(name: "oauth", kind: .oauth2, scopes: ["read", "write"])
XCTAssertEqual(oauth.scopes, ["read", "write"])

let oidc = SecurityRequirement.Scheme(
name: "oidc",
kind: .openIdConnect(url: "https://example.com/.well-known/openid-configuration"),
scopes: ["openid"]
)
guard case let .openIdConnect(url) = oidc.kind else { return XCTFail("expected openIdConnect") }
XCTAssertEqual(url, "https://example.com/.well-known/openid-configuration")

let mtls = SecurityRequirement.Scheme(name: "mtls", kind: .mutualTLS, scopes: [])
XCTAssertEqual(mtls.kind, .mutualTLS)
}

func testHashableAndSetDeduplication() {
let a = SecurityRequirement(schemes: [.init(name: "oauth", kind: .oauth2, scopes: [])])
let b = SecurityRequirement(schemes: [.init(name: "oauth", kind: .oauth2, scopes: [])])
let differentScopes = SecurityRequirement(schemes: [.init(name: "oauth", kind: .oauth2, scopes: ["read"])])
let differentName = SecurityRequirement(schemes: [.init(name: "other", kind: .oauth2, scopes: [])])

XCTAssertEqual(a, b)
XCTAssertEqual(a.hashValue, b.hashValue)
XCTAssertNotEqual(a, differentScopes)
XCTAssertNotEqual(a, differentName)
XCTAssertEqual(Set([a, b, differentScopes, differentName]).count, 3)
}

func testEmitContract() {
// Locks the exact literal the generator emits for a single-scheme OAuth2 operation.
let requirements: [SecurityRequirement] = [
.init(schemes: [.init(name: "OAuth2PasswordBearer", kind: .oauth2, scopes: [])])
]
XCTAssertEqual(
requirements,
[
SecurityRequirement(schemes: [
SecurityRequirement.Scheme(name: "OAuth2PasswordBearer", kind: .oauth2, scopes: [])
])
]
)
}

func testOrAndSemanticsAreDistinct() {
// `[]` (public opt-out) is a different value from a requirement with no schemes.
let publicOptOut: [SecurityRequirement] = []
let emptyAndGroup: [SecurityRequirement] = [.init(schemes: [])]
XCTAssertNotEqual(publicOptOut, emptyAndGroup)
}
}