Skip to content

Commit 977ed9c

Browse files
committed
Add support for non internal non components references to security schemes
1 parent 5f279d8 commit 977ed9c

File tree

3 files changed

+193
-27
lines changed

3 files changed

+193
-27
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
}

Tests/OpenAPIKitErrorReportingTests/SecuritySchemeErrorTests.swift

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,13 @@ import OpenAPIKit
1111
@preconcurrency import Yams
1212

1313
final class SecuritySchemeErrorTests: XCTestCase {
14-
func test_missingSecuritySchemeError() {
15-
// missing as-in not found in the Components Object
14+
func test_missingSecuritySchemeError() throws {
15+
#if os(Linux) && compiler(>=6.0) && compiler(<6.1)
16+
throw XCTSkip("Swift bug causes no exception in this test case for just one Swift 6 version (6.0)")
17+
#endif
18+
19+
// missing as-in not found in the Components Object nor a valid external
20+
// URL
1621
let documentYML =
1722
"""
1823
openapi: 3.1.0
@@ -21,23 +26,25 @@ final class SecuritySchemeErrorTests: XCTestCase {
2126
version: 1.0
2227
paths: {}
2328
components: {}
24-
security:
25-
- missing: []
29+
security: [
30+
"": []
31+
]
2632
"""
2733

2834
XCTAssertThrowsError(try testDecoder.decode(OpenAPI.Document.self, from: documentYML)) { error in
2935

3036
let openAPIError = OpenAPI.Error(from: error)
3137

32-
XCTAssertEqual(openAPIError.localizedDescription, "Problem encountered when parsing `security` in the root Document object: Each key found in a Security Requirement dictionary must refer to a Security Scheme present in the Components dictionary.")
38+
XCTAssertEqual(openAPIError.localizedDescription, "Problem encountered when parsing `security` in the root Document object: 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.")
3339
XCTAssertEqual(openAPIError.codingPath.map { $0.stringValue }, [
3440
"security"
3541
])
3642
}
3743
}
3844

3945
func test_missingSecuritySchemeInPathsError() {
40-
// missing as-in not found in the Components Object
46+
// missing as-in not found in the Components Object nor a valid external
47+
// URL
4148
let documentYML =
4249
"""
4350
openapi: 3.1.0
@@ -49,7 +56,7 @@ final class SecuritySchemeErrorTests: XCTestCase {
4956
"get": {
5057
"responses": {},
5158
"security": [
52-
"hello": []
59+
"": []
5360
]
5461
}
5562
}
@@ -61,13 +68,13 @@ final class SecuritySchemeErrorTests: XCTestCase {
6168

6269
let openAPIError = OpenAPI.Error(from: error)
6370

64-
XCTAssertEqual(openAPIError.localizedDescription, "Problem encountered when parsing `hello` in Document.paths['/hello/world'].get.security: Each key found in a Security Requirement dictionary must refer to a Security Scheme present in the Components dictionary.")
71+
XCTAssertEqual(openAPIError.localizedDescription, "Problem encountered when parsing `` in Document.paths['/hello/world'].get.security: 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.")
6572
XCTAssertEqual(openAPIError.codingPath.map { $0.stringValue }, [
6673
"paths",
6774
"/hello/world",
6875
"get",
6976
"security",
70-
"hello"
77+
""
7178
])
7279
}
7380
}

Tests/OpenAPIKitTests/Document/DocumentTests.swift

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ final class DocumentTests: XCTestCase {
4040
tags: ["hi"],
4141
externalDocs: .init(url: URL(string: "https://google.com")!)
4242
)
43+
44+
// construct with external reference to a security scheme
45+
let _ = OpenAPI.Document(
46+
info: .init(title: "hi", version: "1.0"),
47+
servers: [],
48+
paths: [:],
49+
components: .noComponents,
50+
security: [
51+
[.external(URL(string: "https://example.com/security.yml")!): []]
52+
]
53+
)
4354
}
4455

4556
func test_initOASVersions() {
@@ -846,7 +857,10 @@ extension DocumentTests {
846857
components: .direct(
847858
securitySchemes: ["security": .init(type: .apiKey(name: "key", location: .header))]
848859
),
849-
security: [[.component( named: "security"):[]]]
860+
security: [
861+
[.component(named: "security"): []],
862+
[.external(URL(string: "https://example.com/security.yml")!): []]
863+
]
850864
)
851865
let encodedDocument = try orderUnstableTestStringFromEncoding(of: document)
852866

@@ -872,6 +886,11 @@ extension DocumentTests {
872886
{
873887
"security" : [
874888
889+
]
890+
},
891+
{
892+
"https:\\/\\/example.com\\/security.yml" : [
893+
875894
]
876895
}
877896
]
@@ -880,7 +899,7 @@ extension DocumentTests {
880899
)
881900
}
882901

883-
func test_specifySecurity_decode() throws {
902+
func test_specifyInternalSecurity_decode() throws {
884903
let documentData =
885904
"""
886905
{
@@ -926,6 +945,122 @@ extension DocumentTests {
926945
)
927946
}
928947

948+
func test_securityNotFoundInComponentsIsExternal_decode() throws {
949+
let documentData =
950+
"""
951+
{
952+
"components" : {
953+
},
954+
"info" : {
955+
"title" : "API",
956+
"version" : "1.0"
957+
},
958+
"openapi" : "3.1.1",
959+
"paths" : {
960+
961+
},
962+
"security" : [
963+
{
964+
"security" : [
965+
966+
]
967+
}
968+
]
969+
}
970+
""".data(using: .utf8)!
971+
let document = try orderUnstableDecode(OpenAPI.Document.self, from: documentData)
972+
973+
XCTAssertEqual(
974+
document,
975+
OpenAPI.Document(
976+
info: .init(title: "API", version: "1.0"),
977+
servers: [],
978+
paths: [:],
979+
components: .noComponents,
980+
security: [[.external(URL(string: "security")!):[]]]
981+
)
982+
)
983+
}
984+
985+
func test_specifyExternalSecurity_decode() throws {
986+
let documentData =
987+
"""
988+
{
989+
"components" : {
990+
"securitySchemes" : {
991+
"security" : {
992+
"in" : "header",
993+
"name" : "key",
994+
"type" : "apiKey"
995+
}
996+
}
997+
},
998+
"info" : {
999+
"title" : "API",
1000+
"version" : "1.0"
1001+
},
1002+
"openapi" : "3.1.1",
1003+
"paths" : {
1004+
1005+
},
1006+
"security" : [
1007+
{
1008+
"https://example.com/security.yml": []
1009+
}
1010+
]
1011+
}
1012+
""".data(using: .utf8)!
1013+
let document = try orderUnstableDecode(OpenAPI.Document.self, from: documentData)
1014+
1015+
XCTAssertEqual(
1016+
document,
1017+
OpenAPI.Document(
1018+
info: .init(title: "API", version: "1.0"),
1019+
servers: [],
1020+
paths: [:],
1021+
components: .direct(
1022+
securitySchemes: ["security": .init(type: .apiKey(name: "key", location: .header))]
1023+
),
1024+
security: [[.external(URL(string: "https://example.com/security.yml")!): []]]
1025+
)
1026+
)
1027+
}
1028+
1029+
func test_specifyInvalidSecurity_decode() throws {
1030+
let documentData =
1031+
"""
1032+
{
1033+
"components" : {
1034+
"securitySchemes" : {
1035+
"security" : {
1036+
"in" : "header",
1037+
"name" : "key",
1038+
"type" : "apiKey"
1039+
}
1040+
}
1041+
},
1042+
"info" : {
1043+
"title" : "API",
1044+
"version" : "1.0"
1045+
},
1046+
"openapi" : "3.1.1",
1047+
"paths" : {
1048+
1049+
},
1050+
"security" : [
1051+
{
1052+
"$$$://(capture) <not &a valid>% @url://::**$[]": []
1053+
}
1054+
]
1055+
}
1056+
""".data(using: .utf8)!
1057+
XCTAssertThrowsError(
1058+
try orderUnstableDecode(OpenAPI.Document.self, from: documentData)
1059+
) { error in
1060+
XCTAssertEqual(OpenAPI.Error(from: error).localizedDescription, "Problem encountered when parsing `security` in the root Document object: 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.")
1061+
}
1062+
}
1063+
9291064
func test_specifyTags_encode() throws {
9301065
let document = OpenAPI.Document(
9311066
info: .init(title: "API", version: "1.0"),

0 commit comments

Comments
 (0)