Skip to content

Commit e664d66

Browse files
authored
fix(Core): use groupClaim in @auth rule for oidc (#847)
1 parent 4dd436c commit e664d66

File tree

5 files changed

+504
-17
lines changed

5 files changed

+504
-17
lines changed

Amplify.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@
147147
6BB7441023A9954900B0EB6C /* DispatchSource+MakeOneOff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB7440F23A9954900B0EB6C /* DispatchSource+MakeOneOff.swift */; };
148148
6BBECD7123ADA7E100C8DFBE /* AmplifyAWSServiceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BBECD7023ADA7E100C8DFBE /* AmplifyAWSServiceConfiguration.swift */; };
149149
6BBECD7423ADA9D100C8DFBE /* AmplifyAWSServiceConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BBECD7323ADA9D100C8DFBE /* AmplifyAWSServiceConfigurationTests.swift */; };
150+
6BDF15B62541262000B5BE69 /* ModelWithOwnerAuthAndGroupWithGroupClaim.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BDF15B52541262000B5BE69 /* ModelWithOwnerAuthAndGroupWithGroupClaim.swift */; };
151+
6BDF15B825412DE600B5BE69 /* ModelWithOwnerAuthAndMultiGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BDF15B725412DE600B5BE69 /* ModelWithOwnerAuthAndMultiGroup.swift */; };
150152
6BEE0817253114FD00133961 /* AWSAuthServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BEE0816253114FD00133961 /* AWSAuthServiceTests.swift */; };
151153
6BEE08192533CAA600133961 /* GraphQLRequestOwnerAndGroupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BEE08182533CAA600133961 /* GraphQLRequestOwnerAndGroupTests.swift */; };
152154
6BEE081C2533CCFA00133961 /* OGCScenarioBPost+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BEE081A2533CCFA00133961 /* OGCScenarioBPost+Schema.swift */; };
@@ -912,6 +914,8 @@
912914
6BB7440F23A9954900B0EB6C /* DispatchSource+MakeOneOff.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchSource+MakeOneOff.swift"; sourceTree = "<group>"; };
913915
6BBECD7023ADA7E100C8DFBE /* AmplifyAWSServiceConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmplifyAWSServiceConfiguration.swift; sourceTree = "<group>"; };
914916
6BBECD7323ADA9D100C8DFBE /* AmplifyAWSServiceConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmplifyAWSServiceConfigurationTests.swift; sourceTree = "<group>"; };
917+
6BDF15B52541262000B5BE69 /* ModelWithOwnerAuthAndGroupWithGroupClaim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelWithOwnerAuthAndGroupWithGroupClaim.swift; sourceTree = "<group>"; };
918+
6BDF15B725412DE600B5BE69 /* ModelWithOwnerAuthAndMultiGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelWithOwnerAuthAndMultiGroup.swift; sourceTree = "<group>"; };
915919
6BEE0816253114FD00133961 /* AWSAuthServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSAuthServiceTests.swift; sourceTree = "<group>"; };
916920
6BEE08182533CAA600133961 /* GraphQLRequestOwnerAndGroupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLRequestOwnerAndGroupTests.swift; sourceTree = "<group>"; };
917921
6BEE081A2533CCFA00133961 /* OGCScenarioBPost+Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OGCScenarioBPost+Schema.swift"; sourceTree = "<group>"; };
@@ -1822,6 +1826,8 @@
18221826
21A3FDAE24630D7F00E76120 /* ModelWithOwnerFieldAuthRuleTests.swift */,
18231827
21A3FDB32463C49F00E76120 /* ModelReadUpdateAuthRuleTests.swift */,
18241828
21A3FDB52464590600E76120 /* ModelMultipleOwnerAuthRuleTests.swift */,
1829+
6BDF15B52541262000B5BE69 /* ModelWithOwnerAuthAndGroupWithGroupClaim.swift */,
1830+
6BDF15B725412DE600B5BE69 /* ModelWithOwnerAuthAndMultiGroup.swift */,
18251831
);
18261832
path = AuthRuleDecorator;
18271833
sourceTree = "<group>";
@@ -4325,6 +4331,7 @@
43254331
2129BE48239489AC006363A1 /* MutationSyncMetadataTests.swift in Sources */,
43264332
216E45F1248E971E0035E3CE /* GraphQLRequestNonModelTests.swift in Sources */,
43274333
21A3FDAF24630D7F00E76120 /* ModelWithOwnerFieldAuthRuleTests.swift in Sources */,
4334+
6BDF15B825412DE600B5BE69 /* ModelWithOwnerAuthAndMultiGroup.swift in Sources */,
43284335
2183A56823EA4A8E00232880 /* GraphQLDeleteMutationTests.swift in Sources */,
43294336
21A3FDB42463C49F00E76120 /* ModelReadUpdateAuthRuleTests.swift in Sources */,
43304337
2183A56323EA4A7800232880 /* GraphQLSubscriptionTests.swift in Sources */,
@@ -4337,6 +4344,7 @@
43374344
6BBECD7423ADA9D100C8DFBE /* AmplifyAWSServiceConfigurationTests.swift in Sources */,
43384345
6B9F7C5225267E1500F1F71C /* MockAWSAuthUser.swift in Sources */,
43394346
21AD425A249C0D910016FE95 /* AnyModelTester.swift in Sources */,
4347+
6BDF15B62541262000B5BE69 /* ModelWithOwnerAuthAndGroupWithGroupClaim.swift in Sources */,
43404348
2129BE3C2394828B006363A1 /* GraphQLRequestModelTests.swift in Sources */,
43414349
2183A56523EA4A8400232880 /* GraphQLListQueryTests.swift in Sources */,
43424350
6BEE08192533CAA600133961 /* GraphQLRequestOwnerAndGroupTests.swift in Sources */,

AmplifyPlugins/Core/AWSPluginsCore/Model/Decorator/AuthRuleDecorator.swift

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public struct AuthRuleDecorator: ModelBasedGraphQLDocumentDecorator {
5151
return decorateDocument
5252
}
5353

54-
let readRestrictingStaticGroups = authRules.readRestrictingStaticGroups()
54+
let readRestrictingStaticGroups = authRules.groupClaimsToReadRestrictingStaticGroups()
5555
authRules.forEach { authRule in
5656
decorateDocument = decorateAuthStrategy(document: decorateDocument,
5757
authRule: authRule,
@@ -62,7 +62,7 @@ public struct AuthRuleDecorator: ModelBasedGraphQLDocumentDecorator {
6262

6363
private func decorateAuthStrategy(document: SingleDirectiveGraphQLDocument,
6464
authRule: AuthRule,
65-
readRestrictingStaticGroups: Set<String>) -> SingleDirectiveGraphQLDocument {
65+
readRestrictingStaticGroups: [String: Set<String>]) -> SingleDirectiveGraphQLDocument {
6666
guard authRule.allow == .owner,
6767
var selectionSet = document.selectionSet else {
6868
return document
@@ -71,10 +71,10 @@ public struct AuthRuleDecorator: ModelBasedGraphQLDocumentDecorator {
7171
let ownerField = authRule.getOwnerFieldOrDefault()
7272
selectionSet = appendOwnerFieldToSelectionSetIfNeeded(selectionSet: selectionSet, ownerField: ownerField)
7373

74-
if case let .subscription(_, claims) = input,
74+
if case let .subscription(_, claims) = input,
7575
authRule.isReadRestrictingOwner() &&
76-
isNotInReadRestrictingStaticGroup(readRestrictingStaticGroups,
77-
cognitoGroupsFrom(claims: claims)) {
76+
isNotInReadRestrictingStaticGroup(jwtTokenClaims: claims,
77+
readRestrictingStaticGroups: readRestrictingStaticGroups) {
7878
var inputs = document.inputs
7979
let identityClaimValue = resolveIdentityClaimValue(identityClaim: authRule.identityClaimOrDefault(),
8080
claims: claims)
@@ -86,15 +86,25 @@ public struct AuthRuleDecorator: ModelBasedGraphQLDocumentDecorator {
8686
return document.copy(selectionSet: selectionSet)
8787
}
8888

89-
private func isNotInReadRestrictingStaticGroup(_ readRestrictingStaticGroups: Set<String>,
90-
_ cognitoGroupsFromClaims: Set<String>) -> Bool {
91-
return (readRestrictingStaticGroups.isEmpty ||
92-
readRestrictingStaticGroups.isDisjoint(with: cognitoGroupsFromClaims))
89+
private func isNotInReadRestrictingStaticGroup(jwtTokenClaims: IdentityClaimsDictionary,
90+
readRestrictingStaticGroups: [String: Set<String>]) -> Bool {
91+
for (groupClaim, readRestrictingStaticGroupsPerClaim) in readRestrictingStaticGroups {
92+
let groupsFromClaim = groupsFrom(jwtTokenClaims: jwtTokenClaims, groupClaim: groupClaim)
93+
let doesNotBelongToGroupsFromClaim = readRestrictingStaticGroupsPerClaim.isEmpty ||
94+
readRestrictingStaticGroupsPerClaim.isDisjoint(with: groupsFromClaim)
95+
if doesNotBelongToGroupsFromClaim {
96+
continue
97+
} else {
98+
return false
99+
}
100+
}
101+
return true
93102
}
94103

95-
private func cognitoGroupsFrom(claims: IdentityClaimsDictionary) -> Set<String> {
104+
private func groupsFrom(jwtTokenClaims: IdentityClaimsDictionary,
105+
groupClaim: String) -> Set<String> {
96106
var groupSet = Set<String>()
97-
if let groups = (claims["cognito:groups"] as? NSArray) as Array? {
107+
if let groups = (jwtTokenClaims[groupClaim] as? NSArray) as Array? {
98108
for group in groups {
99109
if let groupString = group as? String {
100110
groupSet.insert(groupString)

AmplifyPlugins/Core/AWSPluginsCore/Model/Support/AuthRule+Extension.swift

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,47 @@ extension AuthRule {
4242
}
4343

4444
extension Array where Element == AuthRule {
45-
func readRestrictingStaticGroups() -> Set<String> {
46-
var readRestrictingStaticGroups = Set<String>()
45+
46+
// This function returns a map of all of the read restricting static groups defined for your app's schema
47+
// Example 1: Single group with implicit read restriction
48+
// {allow: groups, groups: ["Admins"]}
49+
// Returns:
50+
// {
51+
// "cognito:groups" : ["Admins"]
52+
// }
53+
//
54+
// Example 2: Multiple groups with only one having read restriction
55+
// {allow: groups, groups: ["Admins"], operations: [read, update, delete], groupClaim: "https://app1.com/claims/groups"}
56+
// {allow: groups, groups: ["Users"], operations: [create]}
57+
// Returns:
58+
// {
59+
// "https://app1.com/claims/groups" : ["Admins"]
60+
// }
61+
//
62+
// Example 3: Multiple groups with multiple group claims
63+
// {allow: groups, provider: oidc, groups: ["Admins"], groupClaim: "https://app1.com/claims/groups"}
64+
// {allow: groups, provider: oidc, groups: ["Moderators", "Editors"], groupClaim: "https://app2.com/claims/groups"}
65+
// Returns:
66+
// {
67+
// "https://app1.com/claims/groups" : ["Admins"],
68+
// "https://app2.com/claims/groups" : ["Moderators", "Editors"]
69+
// }
70+
//
71+
func groupClaimsToReadRestrictingStaticGroups() -> [String: Set<String>] {
72+
var readRestrictingStaticGroupsMap = [String: Set<String>]()
4773
let readRestrictingGroupRules = filter { $0.isReadRestrictingStaticGroup() }
48-
for groupRules in readRestrictingGroupRules {
49-
groupRules.groups.forEach { group in
50-
readRestrictingStaticGroups.insert(group)
74+
for groupRule in readRestrictingGroupRules {
75+
let groupClaim = groupRule.groupClaim ?? "cognito:groups"
76+
groupRule.groups.forEach { group in
77+
if var existingSet = readRestrictingStaticGroupsMap[groupClaim] {
78+
existingSet.insert(group)
79+
readRestrictingStaticGroupsMap[groupClaim] = existingSet
80+
} else {
81+
readRestrictingStaticGroupsMap[groupClaim] = [group]
82+
}
5183
}
5284
}
53-
return readRestrictingStaticGroups
85+
return readRestrictingStaticGroupsMap
5486
}
5587

5688
func readRestrictingOwnerRules() -> [AuthRule] {
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
//
2+
// Copyright 2018-2020 Amazon.com,
3+
// Inc. or its affiliates. All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import XCTest
9+
10+
@testable import Amplify
11+
@testable import AmplifyTestCommon
12+
@testable import AWSPluginsCore
13+
14+
/*
15+
type OIDCGroupPost
16+
@model
17+
@auth(
18+
rules: [
19+
{ allow: owner, provider: oidc, identityClaim: "sub"},
20+
{ allow: groups, provider: oidc, groups: ["Admins"],
21+
groupClaim: "https://myapp.com/claims/groups"}
22+
]
23+
) {
24+
id: ID!
25+
title: String!
26+
owner: String
27+
}
28+
*/
29+
30+
public struct OIDCGroupPost: Model {
31+
public let id: String
32+
public var title: String
33+
public var owner: String?
34+
35+
public init(id: String = UUID().uuidString,
36+
title: String,
37+
owner: String? = nil) {
38+
self.id = id
39+
self.title = title
40+
self.owner = owner
41+
}
42+
43+
// MARK: - CodingKeys
44+
public enum CodingKeys: String, ModelKey {
45+
case id
46+
case title
47+
case owner
48+
}
49+
50+
public static let keys = CodingKeys.self
51+
public static let schema = defineSchema { model in
52+
let oIDCGroupPost = OIDCGroupPost.keys
53+
54+
model.authRules = [
55+
rule(allow: .owner,
56+
ownerField: "owner",
57+
identityClaim: "sub",
58+
operations: [.create, .update, .delete, .read]),
59+
rule(allow: .groups,
60+
groupClaim: "https://myapp.com/claims/groups",
61+
groups: ["Admins"],
62+
operations: [.create, .update, .delete, .read])
63+
]
64+
65+
model.pluralName = "OIDCGroupPosts"
66+
67+
model.fields(
68+
.id(),
69+
.field(oIDCGroupPost.title, is: .required, ofType: .string),
70+
.field(oIDCGroupPost.owner, is: .optional, ofType: .string)
71+
)
72+
}
73+
}
74+
75+
class ModelWithOwnerAuthAndGroupWithGroupClaim: XCTestCase {
76+
override func setUp() {
77+
ModelRegistry.register(modelType: OIDCGroupPost.self)
78+
}
79+
80+
override func tearDown() {
81+
ModelRegistry.reset()
82+
}
83+
84+
func testOnCreateSubscription_NoGroupInfoPassed() {
85+
let claims = ["username": "user1",
86+
"sub": "123e4567-dead-beef-a456-426614174000"] as IdentityClaimsDictionary
87+
var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelType: OIDCGroupPost.self,
88+
operationType: .subscription)
89+
documentBuilder.add(decorator: DirectiveNameDecorator(type: .onCreate))
90+
documentBuilder.add(decorator: AuthRuleDecorator(.subscription(.onCreate, claims)))
91+
let document = documentBuilder.build()
92+
let expectedQueryDocument = """
93+
subscription OnCreateOIDCGroupPost($owner: String!) {
94+
onCreateOIDCGroupPost(owner: $owner) {
95+
id
96+
owner
97+
title
98+
__typename
99+
}
100+
}
101+
"""
102+
XCTAssertEqual(document.name, "onCreateOIDCGroupPost")
103+
XCTAssertEqual(document.stringValue, expectedQueryDocument)
104+
guard let variables = document.variables else {
105+
XCTFail("The document doesn't contain variables")
106+
return
107+
}
108+
XCTAssertEqual(variables["owner"] as? String, "123e4567-dead-beef-a456-426614174000")
109+
}
110+
111+
func testOnCreateSubscription_InAdminsGroup() {
112+
let claims = ["username": "user1",
113+
"sub": "123e4567-dead-beef-a456-426614174000",
114+
"https://myapp.com/claims/groups": ["Admins"]] as IdentityClaimsDictionary
115+
var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelType: OIDCGroupPost.self,
116+
operationType: .subscription)
117+
documentBuilder.add(decorator: DirectiveNameDecorator(type: .onCreate))
118+
documentBuilder.add(decorator: AuthRuleDecorator(.subscription(.onCreate, claims)))
119+
let document = documentBuilder.build()
120+
let expectedQueryDocument = """
121+
subscription OnCreateOIDCGroupPost {
122+
onCreateOIDCGroupPost {
123+
id
124+
owner
125+
title
126+
__typename
127+
}
128+
}
129+
"""
130+
XCTAssertEqual(document.name, "onCreateOIDCGroupPost")
131+
XCTAssertEqual(document.stringValue, expectedQueryDocument)
132+
XCTAssert(document.variables.isEmpty)
133+
}
134+
135+
func testOnCreateSubscription_InAdminsGroupAndAnother() {
136+
let claims = ["username": "user1",
137+
"sub": "123e4567-dead-beef-a456-426614174000",
138+
"https://myapp.com/claims/groups": ["Admins", "Users"]] as IdentityClaimsDictionary
139+
var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelType: OIDCGroupPost.self,
140+
operationType: .subscription)
141+
documentBuilder.add(decorator: DirectiveNameDecorator(type: .onCreate))
142+
documentBuilder.add(decorator: AuthRuleDecorator(.subscription(.onCreate, claims)))
143+
let document = documentBuilder.build()
144+
let expectedQueryDocument = """
145+
subscription OnCreateOIDCGroupPost {
146+
onCreateOIDCGroupPost {
147+
id
148+
owner
149+
title
150+
__typename
151+
}
152+
}
153+
"""
154+
XCTAssertEqual(document.name, "onCreateOIDCGroupPost")
155+
XCTAssertEqual(document.stringValue, expectedQueryDocument)
156+
XCTAssert(document.variables.isEmpty)
157+
}
158+
159+
func testOnCreateSubscription_NotInAdminsGroup() {
160+
let claims = ["username": "user1",
161+
"sub": "123e4567-dead-beef-a456-426614174000",
162+
"https://myapp.com/claims/groups": ["Users"]] as IdentityClaimsDictionary
163+
var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelType: OIDCGroupPost.self,
164+
operationType: .subscription)
165+
documentBuilder.add(decorator: DirectiveNameDecorator(type: .onCreate))
166+
documentBuilder.add(decorator: AuthRuleDecorator(.subscription(.onCreate, claims)))
167+
let document = documentBuilder.build()
168+
let expectedQueryDocument = """
169+
subscription OnCreateOIDCGroupPost($owner: String!) {
170+
onCreateOIDCGroupPost(owner: $owner) {
171+
id
172+
owner
173+
title
174+
__typename
175+
}
176+
}
177+
"""
178+
XCTAssertEqual(document.name, "onCreateOIDCGroupPost")
179+
XCTAssertEqual(document.stringValue, expectedQueryDocument)
180+
guard let variables = document.variables else {
181+
XCTFail("The document doesn't contain variables")
182+
return
183+
}
184+
XCTAssertEqual(variables["owner"] as? String, "123e4567-dead-beef-a456-426614174000")
185+
}
186+
}

0 commit comments

Comments
 (0)