Skip to content

Commit c5642d9

Browse files
authored
Merge pull request #457 from mattpolzin/feature/447/security-scheme-changes
OAS 3.2.0 security scheme features
2 parents 0955c1a + ea16987 commit c5642d9

File tree

15 files changed

+922
-110
lines changed

15 files changed

+922
-110
lines changed

Sources/OpenAPIKit/Document/Document.swift

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,11 @@ internal func encodeSecurity<CodingKeys: CodingKey>(requirements security: [Open
857857
var securityContainer = container.nestedUnkeyedContainer(forKey: key)
858858
for securityRequirement in security {
859859
let securityKeysAndValues = securityRequirement
860-
.compactMap { keyValue in keyValue.key.name.map { ($0, keyValue.value) } }
860+
.compactMap { (key, value) in
861+
guard case .internal = key else { return (key.absoluteString, value) }
862+
863+
return key.name.map { ($0, value) }
864+
}
861865
let securityStringKeyedDict = Dictionary(
862866
securityKeysAndValues,
863867
uniquingKeysWith: { $1 }
@@ -866,36 +870,52 @@ internal func encodeSecurity<CodingKeys: CodingKey>(requirements security: [Open
866870
}
867871
}
868872

869-
internal func decodeSecurityRequirements<CodingKeys: CodingKey>(from container: KeyedDecodingContainer<CodingKeys>, forKey key: CodingKeys, given optionalComponents: OpenAPI.Components?) throws -> [OpenAPI.SecurityRequirement]? {
873+
internal func decodeSecurityRequirements<CodingKeys: CodingKey>(from container: KeyedDecodingContainer<CodingKeys>, forKey codingKey: CodingKeys, given optionalComponents: OpenAPI.Components?) throws -> [OpenAPI.SecurityRequirement]? {
870874
// A real mess here because we've got an Array of non-string-keyed
871875
// Dictionaries.
872-
if container.contains(key) {
873-
var securityContainer = try container.nestedUnkeyedContainer(forKey: key)
876+
if container.contains(codingKey) {
877+
var securityContainer = try container.nestedUnkeyedContainer(forKey: codingKey)
874878

875879
var securityRequirements = [OpenAPI.SecurityRequirement]()
876880
while !securityContainer.isAtEnd {
877881
let securityStringKeyedDict = try securityContainer.decode([String: [String]].self)
878882

879-
// convert to JSONReference keys
880-
let securityKeysAndValues = securityStringKeyedDict.map { (key, value) in
881-
(
882-
key: JSONReference<OpenAPI.SecurityScheme>.component(named: key),
883-
value: value
884-
)
885-
}
883+
// ultimately we end up with JSON references that may be internal
884+
// or external. we determine if they are internal by looking them
885+
// up in the components; if found, they are internal, otherwise,
886+
// they are external.
887+
let securityKeysAndValues: [(key: JSONReference<OpenAPI.SecurityScheme>, value: [String])]
886888

887889
if let components = optionalComponents {
888890
// check each key for validity against components.
889891
let foundInComponents = { (ref: JSONReference<OpenAPI.SecurityScheme>) -> Bool in
890892
return (try? components.contains(ref)) ?? false
891893
}
892-
guard securityKeysAndValues.map({ $0.key }).allSatisfy(foundInComponents) else {
894+
var foundBadKeys = false
895+
896+
securityKeysAndValues = securityStringKeyedDict.map { (key, value) in
897+
let componentKey = JSONReference<OpenAPI.SecurityScheme>.component(named: key)
898+
if foundInComponents(componentKey) {
899+
return (componentKey, value)
900+
}
901+
if let url = URL(string: key) {
902+
return (.external(url), value)
903+
}
904+
foundBadKeys = true
905+
return (componentKey, value)
906+
}
907+
908+
if foundBadKeys {
893909
throw GenericError(
894-
subjectName: key.stringValue,
895-
details: "Each key found in a Security Requirement dictionary must refer to a Security Scheme present in the Components dictionary",
896-
codingPath: container.codingPath + [key]
910+
subjectName: codingKey.stringValue,
911+
details: "Each key found in a Security Requirement dictionary must refer to a Security Scheme present in the Components dictionary or be a JSON reference to a security scheme found in another file",
912+
codingPath: container.codingPath + [codingKey]
897913
)
898914
}
915+
} else {
916+
securityKeysAndValues = securityStringKeyedDict.map { (key, value) in
917+
(JSONReference<OpenAPI.SecurityScheme>.component(named: key), value)
918+
}
899919
}
900920

901921
securityRequirements.append(Dictionary(securityKeysAndValues, uniquingKeysWith: { $1 }))
@@ -928,6 +948,10 @@ internal func validate(securityRequirements: [OpenAPI.SecurityRequirement], at p
928948
let securitySchemes = securityRequirements.flatMap { $0.keys }
929949

930950
for securityScheme in securitySchemes {
951+
if case .external = securityScheme {
952+
// external references are allowed as of OAS 3.2.0
953+
continue
954+
}
931955
guard components[securityScheme] != nil else {
932956
let schemeKey = securityScheme.name ?? securityScheme.absoluteString
933957
let keys = [
@@ -941,7 +965,7 @@ internal func validate(securityRequirements: [OpenAPI.SecurityRequirement], at p
941965

942966
throw GenericError(
943967
subjectName: schemeKey,
944-
details: "Each key found in a Security Requirement dictionary must refer to a Security Scheme present in the Components dictionary",
968+
details: "Each key found in a Security Requirement dictionary must refer to a Security Scheme present in the Components dictionary or be a JSON reference to a Security Scheme found in another file",
945969
codingPath: keys
946970
)
947971
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
//
2+
// OAuthFlows.swift
3+
//
4+
//
5+
// Created by Mathew Polzin on 1/23/20.
6+
//
7+
8+
import OpenAPIKitCore
9+
import Foundation
10+
11+
extension OpenAPI {
12+
/// OpenAPI Spec "Oauth Flows Object"
13+
///
14+
/// See [OpenAPI Oauth Flows Object](https://spec.openapis.org/oas/v3.0.4.html#oauth-flows-object).
15+
public struct OAuthFlows: HasConditionalWarnings, Sendable {
16+
public let implicit: Implicit?
17+
public let password: Password?
18+
public let clientCredentials: ClientCredentials?
19+
public let authorizationCode: AuthorizationCode?
20+
public let deviceAuthorization: DeviceAuthorization?
21+
22+
public let conditionalWarnings: [(any Condition, OpenAPI.Warning)]
23+
24+
public init(
25+
implicit: Implicit? = nil,
26+
password: Password? = nil,
27+
clientCredentials: ClientCredentials? = nil,
28+
authorizationCode: AuthorizationCode? = nil,
29+
deviceAuthorization: DeviceAuthorization? = nil
30+
) {
31+
self.implicit = implicit
32+
self.password = password
33+
self.clientCredentials = clientCredentials
34+
self.authorizationCode = authorizationCode
35+
self.deviceAuthorization = deviceAuthorization
36+
37+
self.conditionalWarnings = [
38+
nonNilVersionWarning(fieldName: "deviceAuthorization", value: deviceAuthorization, minimumVersion: .v3_2_0)
39+
].compactMap { $0 }
40+
}
41+
}
42+
}
43+
44+
extension OpenAPI.OAuthFlows: Equatable {
45+
public static func == (lhs: Self, rhs: Self) -> Bool {
46+
lhs.implicit == rhs.implicit
47+
&& lhs.password == rhs.password
48+
&& lhs.clientCredentials == rhs.clientCredentials
49+
&& lhs.authorizationCode == rhs.authorizationCode
50+
&& lhs.deviceAuthorization == rhs.deviceAuthorization
51+
}
52+
}
53+
54+
fileprivate func nonNilVersionWarning<Subject>(fieldName: String, value: Subject?, minimumVersion: OpenAPI.Document.Version) -> (any Condition, OpenAPI.Warning)? {
55+
value.map { _ in
56+
OpenAPI.Document.ConditionalWarnings.version(
57+
lessThan: minimumVersion,
58+
doesNotSupport: "The OAuthFlows \(fieldName) field"
59+
)
60+
}
61+
}
62+
63+
extension OpenAPI.OAuthFlows {
64+
@dynamicMemberLookup
65+
public struct DeviceAuthorization: Equatable, Sendable {
66+
private let common: CommonFields
67+
public let deviceAuthorizationUrl: URL
68+
public let tokenUrl: URL
69+
70+
public init(deviceAuthorizationUrl: URL, tokenUrl: URL, refreshUrl: URL? = nil, scopes: OrderedDictionary<Scope, ScopeDescription>) {
71+
self.deviceAuthorizationUrl = deviceAuthorizationUrl
72+
self.tokenUrl = tokenUrl
73+
common = .init(refreshUrl: refreshUrl, scopes: scopes)
74+
}
75+
76+
public subscript<T>(dynamicMember path: KeyPath<CommonFields, T>) -> T {
77+
return common[keyPath: path]
78+
}
79+
}
80+
}
81+
82+
// MARK: - Codable
83+
extension OpenAPI.OAuthFlows {
84+
private enum CodingKeys: String, CodingKey {
85+
case implicit
86+
case password
87+
case clientCredentials
88+
case authorizationCode
89+
case deviceAuthorization
90+
}
91+
}
92+
93+
extension OpenAPI.OAuthFlows.DeviceAuthorization {
94+
private enum CodingKeys: String, CodingKey {
95+
case deviceAuthorizationUrl
96+
case tokenUrl
97+
}
98+
}
99+
100+
extension OpenAPI.OAuthFlows: Encodable {
101+
public func encode(to encoder: Encoder) throws {
102+
var container = encoder.container(keyedBy: CodingKeys.self)
103+
104+
try container.encodeIfPresent(implicit, forKey: .implicit)
105+
try container.encodeIfPresent(password, forKey: .password)
106+
try container.encodeIfPresent(clientCredentials, forKey: .clientCredentials)
107+
try container.encodeIfPresent(authorizationCode, forKey: .authorizationCode)
108+
try container.encodeIfPresent(deviceAuthorization, forKey: .deviceAuthorization)
109+
}
110+
}
111+
112+
extension OpenAPI.OAuthFlows: Decodable {
113+
public init(from decoder: Decoder) throws {
114+
let container = try decoder.container(keyedBy: CodingKeys.self)
115+
116+
implicit = try container.decodeIfPresent(OpenAPI.OAuthFlows.Implicit.self, forKey: .implicit)
117+
password = try container.decodeIfPresent(OpenAPI.OAuthFlows.Password.self, forKey: .password)
118+
clientCredentials = try container.decodeIfPresent(OpenAPI.OAuthFlows.ClientCredentials.self, forKey: .clientCredentials)
119+
authorizationCode = try container.decodeIfPresent(OpenAPI.OAuthFlows.AuthorizationCode.self, forKey: .authorizationCode)
120+
deviceAuthorization = try container.decodeIfPresent(OpenAPI.OAuthFlows.DeviceAuthorization.self, forKey: .deviceAuthorization)
121+
122+
self.conditionalWarnings = [
123+
nonNilVersionWarning(fieldName: "deviceAuthorization", value: deviceAuthorization, minimumVersion: .v3_2_0)
124+
].compactMap { $0 }
125+
}
126+
}
127+
128+
extension OpenAPI.OAuthFlows.DeviceAuthorization: Encodable {
129+
public func encode(to encoder: Encoder) throws {
130+
try common.encode(to: encoder)
131+
132+
var container = encoder.container(keyedBy: CodingKeys.self)
133+
134+
try container.encode(tokenUrl.absoluteString, forKey: .tokenUrl)
135+
try container.encode(deviceAuthorizationUrl.absoluteString, forKey: .deviceAuthorizationUrl)
136+
}
137+
}
138+
139+
extension OpenAPI.OAuthFlows.DeviceAuthorization: Decodable {
140+
public init(from decoder: Decoder) throws {
141+
common = try OpenAPI.OAuthFlows.CommonFields(from: decoder)
142+
143+
let container = try decoder.container(keyedBy: CodingKeys.self)
144+
145+
tokenUrl = try container.decodeURLAsString(forKey: .tokenUrl)
146+
deviceAuthorizationUrl = try container.decodeURLAsString(forKey: .deviceAuthorizationUrl)
147+
}
148+
}
149+
150+
extension OpenAPI.OAuthFlows: Validatable {}
151+
extension OpenAPI.OAuthFlows.DeviceAuthorization: Validatable {}
152+
// The following conformances are found in Core
153+
// extension Shared.OAuthFlows.Implicit: Validatable {}
154+
// extension Shared.OAuthFlows.Password: Validatable {}
155+
// extension Shared.OAuthFlows.ClientCredentials: Validatable {}
156+
// extension Shared.OAuthFlows.AuthorizationCode: Validatable {}

0 commit comments

Comments
 (0)