Skip to content

Commit

Permalink
fix(datastore): filter authrules with invalid ownerfield
Browse files Browse the repository at this point in the history
  • Loading branch information
lawmicha committed Sep 18, 2023
1 parent 9a05bdd commit 3bf5780
Show file tree
Hide file tree
Showing 13 changed files with 314 additions and 124 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,13 @@ public struct AuthRuleDecorator: ModelBasedGraphQLDocumentDecorator {

public func decorate(_ document: SingleDirectiveGraphQLDocument,
modelSchema: ModelSchema) -> SingleDirectiveGraphQLDocument {
let authRules = modelSchema.authRules.filterBy(authType: authType)
let authRules = modelSchema.authRules
.filterBy(authType: authType)
.filterBy(ownerFieldType: .string, modelSchema: modelSchema)
guard !authRules.isEmpty else {
return document
}
var decorateDocument = document
if authRules.readRestrictingOwnerRules().count > 1 {
log.error("""
Detected multiple owner type auth rules \
with a READ operation. We currently do not support this use case. Please \
limit your type to just one owner auth rule with a READ operation restriction.
""")
return decorateDocument
}

let readRestrictingStaticGroups = authRules.groupClaimsToReadRestrictingStaticGroups()
authRules.forEach { authRule in
Expand Down Expand Up @@ -179,6 +173,15 @@ public struct AuthRuleDecorator: ModelBasedGraphQLDocumentDecorator {
}
}

private extension AuthRule {
func ownerField(inSchema schema: ModelSchema) -> ModelField? {
guard let fieldName = self.ownerField else {
return nil
}
return schema.field(withName: fieldName)
}
}

private extension AuthRules {
func filterBy(authType: AWSAuthorizationType?) -> AuthRules {
guard let authType = authType else {
Expand All @@ -195,6 +198,21 @@ private extension AuthRules {
return authType == provider.toAWSAuthorizationType()
}
}

func filterBy(ownerFieldType: ModelFieldType,
modelSchema: ModelSchema) -> AuthRules {
return filter {
guard let modelField = $0.ownerField(inSchema: modelSchema) else {
// if we couldn't find the owner field means it has been implicitly
// declared in the model schema, therefore has the correct type "string"
return true
}
if case .string = modelField.type {
return true
}
return false
}
}
}

extension AuthRuleDecorator: DefaultLogger {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,31 +71,7 @@ class ModelMultipleOwnerAuthRuleTests: XCTestCase {
override func tearDown() {
ModelRegistry.reset()
}
// This is a test case to demostrate if we attempt to use a model with multiple auth rules
// with a read operation, we effectively create a subscription without decorating it with auth.
// We should delete this use case when the AppSync service supports this use case.
func testUnsupportedModelMultipleOwner_CreateMutation() {
var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelType: ModelMultipleOwner.self,
operationType: .mutation)
documentBuilder.add(decorator: DirectiveNameDecorator(type: .create))
documentBuilder.add(decorator: AuthRuleDecorator(.mutation))
let document = documentBuilder.build()
let expectedQueryDocument = """
mutation CreateModelMultipleOwner {
createModelMultipleOwner {
id
content
editors
__typename
}
}
"""
XCTAssertEqual(document.name, "createModelMultipleOwner")
XCTAssertEqual(document.stringValue, expectedQueryDocument)
XCTAssertTrue(document.variables.isEmpty)
}

/*

// Ensure that the `owner` field is added to the model fields
func testModelMultipleOwner_CreateMutation() {
var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelSchema: ModelMultipleOwner.schema,
Expand Down Expand Up @@ -219,7 +195,7 @@ class ModelMultipleOwnerAuthRuleTests: XCTestCase {
operationType: .query)
documentBuilder.add(decorator: DirectiveNameDecorator(type: .sync))
documentBuilder.add(decorator: PaginationDecorator())
documentBuilder.add(decorator: ConflictResolutionDecorator())
documentBuilder.add(decorator: ConflictResolutionDecorator(graphQLType: .query))
documentBuilder.add(decorator: AuthRuleDecorator(.query))
let document = documentBuilder.build()
let expectedQueryDocument = """
Expand All @@ -243,7 +219,7 @@ class ModelMultipleOwnerAuthRuleTests: XCTestCase {
XCTAssertEqual(document.name, "syncModelMultipleOwners")
XCTAssertEqual(document.stringValue, expectedQueryDocument)
}

// Only the 'owner' inherently has `.create` operation, requiring the subscription operation to contain the input
func testModelMultipleOwner_OnCreateSubscription() {
let claims = ["username": "user1",
Expand All @@ -254,8 +230,8 @@ class ModelMultipleOwnerAuthRuleTests: XCTestCase {
documentBuilder.add(decorator: AuthRuleDecorator(.subscription(.onCreate, claims)))
let document = documentBuilder.build()
let expectedQueryDocument = """
subscription OnCreateModelMultipleOwner($editors: String!, $owner: String!) {
onCreateModelMultipleOwner(editors: $editors, owner: $owner) {
subscription OnCreateModelMultipleOwner($owner: String!) {
onCreateModelMultipleOwner(owner: $owner) {
id
content
editors
Expand Down Expand Up @@ -283,8 +259,8 @@ class ModelMultipleOwnerAuthRuleTests: XCTestCase {
documentBuilder.add(decorator: AuthRuleDecorator(.subscription(.onUpdate, claims)))
let document = documentBuilder.build()
let expectedQueryDocument = """
subscription OnUpdateModelMultipleOwner($editors: String!, $owner: String!) {
onUpdateModelMultipleOwner(editors: $editors, owner: $owner) {
subscription OnUpdateModelMultipleOwner($owner: String!) {
onUpdateModelMultipleOwner(owner: $owner) {
id
content
editors
Expand All @@ -300,7 +276,6 @@ class ModelMultipleOwnerAuthRuleTests: XCTestCase {
return
}
XCTAssertEqual(variables["owner"] as? String, "user1")
XCTAssertEqual(variables["editors"] as? String, "user1")
}

// Only the 'owner' inherently has `.delete` operation, requiring the subscription operation to contain the input
Expand All @@ -313,8 +288,8 @@ class ModelMultipleOwnerAuthRuleTests: XCTestCase {
documentBuilder.add(decorator: AuthRuleDecorator(.subscription(.onDelete, claims)))
let document = documentBuilder.build()
let expectedQueryDocument = """
subscription OnDeleteModelMultipleOwner($editors: String!, $owner: String!) {
onDeleteModelMultipleOwner(editors: $editors, owner: $owner) {
subscription OnDeleteModelMultipleOwner($owner: String!) {
onDeleteModelMultipleOwner(owner: $owner) {
id
content
editors
Expand All @@ -331,5 +306,5 @@ class ModelMultipleOwnerAuthRuleTests: XCTestCase {
}
XCTAssertEqual(variables["owner"] as? String, "user1")
}
*/

}
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,47 @@ class AWSDataStoreCategoryPluginAuthOwnerIntegrationTests: AWSDataStoreAuthBaseT
}

let todo = TodoExplicitOwnerField(content: "content")

// Mutations
try await assertMutations(model: todo, expectations) { error in
XCTFail("Error mutation \(error)")
}


assertUsedAuthTypes([.amazonCognitoUserPools])
}

/// Given: a user signed in with CognitoUserPools, a model with multiple rules with
/// explicit owner field
/// When: DataStore query/mutation operations are sent with CognitoUserPools
/// Then: DataStore is successfully initialized, query returns a result,
/// mutation is processed for an authenticated users
func testExplicitMultipleOwner() async throws {
try await setup(withModels: ExplicitMultipleOwnerModelRegistration(),
testType: .defaultAuthCognito)

try await signIn(user: user1)

let expectations = makeExpectations()

try await assertDataStoreReady(expectations)

// Query
try await assertQuerySuccess(modelType: TodoCognitoMultiOwner.self,
expectations) { error in
XCTFail("Error query \(error)")
}

let post = TodoCognitoMultiOwner(title: "title")

// Mutations
try await assertMutations(model: post, expectations) { error in
XCTFail("Error mutation \(error)")
}

assertUsedAuthTypes([.amazonCognitoUserPools])
}


/// Given: a user signed in with CognitoUserPools, a model with an implicit owner field
/// When: DataStore query/mutation operations are sent with CognitoUserPools
/// Then: DataStore is successfully initialized, query returns a result,
Expand Down Expand Up @@ -187,6 +219,13 @@ extension AWSDataStoreCategoryPluginAuthOwnerIntegrationTests {
}
}

struct ExplicitMultipleOwnerModelRegistration: AmplifyModelRegistration {
public let version: String = "version"
func registerModels(registry: ModelRegistry.Type) {
ModelRegistry.register(modelType: TodoCognitoMultiOwner.self)
}
}

struct ImplicitOwnerModelRegistration: AmplifyModelRegistration {
public let version: String = "version"
func registerModels(registry: ModelRegistry.Type) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

// swiftlint:disable all
import Amplify
import Foundation

extension TodoCognitoMultiOwner {
// MARK: - CodingKeys
public enum CodingKeys: String, ModelKey {
case id
case title
case content
case owner
case editors
case createdAt
case updatedAt
}

public static let keys = CodingKeys.self
// MARK: - ModelSchema

public static let schema = defineSchema { model in
let todoCognitoMultiOwner = TodoCognitoMultiOwner.keys

model.authRules = [
rule(allow: .owner, ownerField: "owner", identityClaim: "cognito:username", provider: .userPools, operations: [.create, .update, .delete, .read]),
rule(allow: .owner, ownerField: "editors", identityClaim: "cognito:username", provider: .userPools, operations: [.update, .read])
]

model.listPluralName = "TodoCognitoMultiOwners"
model.syncPluralName = "TodoCognitoMultiOwners"

model.attributes(
.primaryKey(fields: [todoCognitoMultiOwner.id])
)

model.fields(
.field(todoCognitoMultiOwner.id, is: .required, ofType: .string),
.field(todoCognitoMultiOwner.title, is: .required, ofType: .string),
.field(todoCognitoMultiOwner.content, is: .optional, ofType: .string),
.field(todoCognitoMultiOwner.owner, is: .optional, ofType: .string),
.field(todoCognitoMultiOwner.editors, is: .optional, ofType: .embeddedCollection(of: String.self)),
.field(todoCognitoMultiOwner.createdAt, is: .optional, isReadOnly: true, ofType: .dateTime),
.field(todoCognitoMultiOwner.updatedAt, is: .optional, isReadOnly: true, ofType: .dateTime)
)
}
}

extension TodoCognitoMultiOwner: ModelIdentifiable {
public typealias IdentifierFormat = ModelIdentifierFormat.Default
public typealias IdentifierProtocol = DefaultModelIdentifier<Self>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

// swiftlint:disable all
import Amplify
import Foundation

public struct TodoCognitoMultiOwner: Model {
public let id: String
public var title: String
public var content: String?
public var owner: String?
public var editors: [String?]?
public var createdAt: Temporal.DateTime?
public var updatedAt: Temporal.DateTime?

public init(id: String = UUID().uuidString,
title: String,
content: String? = nil,
owner: String? = nil,
editors: [String?]? = nil) {
self.init(id: id,
title: title,
content: content,
owner: owner,
editors: editors,
createdAt: nil,
updatedAt: nil)
}
internal init(id: String = UUID().uuidString,
title: String,
content: String? = nil,
owner: String? = nil,
editors: [String?]? = nil,
createdAt: Temporal.DateTime? = nil,
updatedAt: Temporal.DateTime? = nil) {
self.id = id
self.title = title
self.content = content
self.owner = owner
self.editors = editors
self.createdAt = createdAt
self.updatedAt = updatedAt
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,34 @@ extension TodoCognitoPrivate {
case createdAt
case updatedAt
}

public static let keys = CodingKeys.self
// MARK: - ModelSchema

public static let schema = defineSchema { model in
let todoCognitoPrivate = TodoCognitoPrivate.keys

model.authRules = [
rule(allow: .private, provider: .userPools, operations: [.create, .update, .delete, .read])
rule(allow: .private, operations: [.create, .update, .delete, .read])
]

model.pluralName = "TodoCognitoPrivates"


model.listPluralName = "TodoCognitoPrivates"
model.syncPluralName = "TodoCognitoPrivates"

model.attributes(
.primaryKey(fields: [todoCognitoPrivate.id])
)

model.fields(
.id(),
.field(todoCognitoPrivate.id, is: .required, ofType: .string),
.field(todoCognitoPrivate.title, is: .required, ofType: .string),
.field(todoCognitoPrivate.createdAt, is: .optional, isReadOnly: true, ofType: .dateTime),
.field(todoCognitoPrivate.updatedAt, is: .optional, isReadOnly: true, ofType: .dateTime)
)
}
}

extension TodoCognitoPrivate: ModelIdentifiable {
public typealias IdentifierFormat = ModelIdentifierFormat.Default
public typealias IdentifierProtocol = DefaultModelIdentifier<Self>
}
Loading

0 comments on commit 3bf5780

Please sign in to comment.