From fc2a5aa9255fc5e7474ce8c7844287606d889109 Mon Sep 17 00:00:00 2001 From: Berzan Yildiz Date: Thu, 21 May 2020 17:13:02 +0200 Subject: [PATCH] Added ReadAllauth modifiers back in --- .../{ => Nested}/NestedAuthModifier.swift | 0 .../NestedCreateAuthModifier.swift | 0 .../Nested/NestedReadAllAuthModifier.swift | 130 ++++++++++++++++++ .../Modifiers/Auth/ReadAllAuthModifier.swift | 108 +++++++++++++++ Sources/Corvus/Endpoints/Read/ReadAll.swift | 9 +- Tests/CorvusTests/AuthorizationTests.swift | 15 +- 6 files changed, 251 insertions(+), 11 deletions(-) rename Sources/Corvus/Endpoints/Modifiers/Auth/{ => Nested}/NestedAuthModifier.swift (100%) rename Sources/Corvus/Endpoints/Modifiers/Auth/{ => Nested}/NestedCreateAuthModifier.swift (100%) create mode 100644 Sources/Corvus/Endpoints/Modifiers/Auth/Nested/NestedReadAllAuthModifier.swift create mode 100644 Sources/Corvus/Endpoints/Modifiers/Auth/ReadAllAuthModifier.swift diff --git a/Sources/Corvus/Endpoints/Modifiers/Auth/NestedAuthModifier.swift b/Sources/Corvus/Endpoints/Modifiers/Auth/Nested/NestedAuthModifier.swift similarity index 100% rename from Sources/Corvus/Endpoints/Modifiers/Auth/NestedAuthModifier.swift rename to Sources/Corvus/Endpoints/Modifiers/Auth/Nested/NestedAuthModifier.swift diff --git a/Sources/Corvus/Endpoints/Modifiers/Auth/NestedCreateAuthModifier.swift b/Sources/Corvus/Endpoints/Modifiers/Auth/Nested/NestedCreateAuthModifier.swift similarity index 100% rename from Sources/Corvus/Endpoints/Modifiers/Auth/NestedCreateAuthModifier.swift rename to Sources/Corvus/Endpoints/Modifiers/Auth/Nested/NestedCreateAuthModifier.swift diff --git a/Sources/Corvus/Endpoints/Modifiers/Auth/Nested/NestedReadAllAuthModifier.swift b/Sources/Corvus/Endpoints/Modifiers/Auth/Nested/NestedReadAllAuthModifier.swift new file mode 100644 index 0000000..908ba49 --- /dev/null +++ b/Sources/Corvus/Endpoints/Modifiers/Auth/Nested/NestedReadAllAuthModifier.swift @@ -0,0 +1,130 @@ +import Vapor +import Fluent + +/// A class that wraps a component which utilizes an `.auth()` modifier. Differs +/// from `AuthModifier` by authenticating on the user of an intermediate parent +/// `I` of `A.QuerySubject`. Requires an object `U` that represents the user to +/// authorize. +public final class NestedReadAllAuthModifier< + A: AuthEndpoint, + I: CorvusModel, + U: CorvusModelAuthenticatable>: +ReadEndpoint { + + /// The return value of the `.query()`, so the type being operated on in + /// the current component. + public typealias QuerySubject = A.QuerySubject + + /// The `KeyPath` to the user property of the intermediate `I` which is to + /// be authenticated. + public typealias UserKeyPath = KeyPath< + I, + I.Parent + > + + /// The `KeyPath` to the intermediate `I` of the endpoint's `QuerySubject`. + public typealias IntermediateKeyPath = KeyPath< + A.QuerySubject, + A.QuerySubject.Parent + > + + /// The `AuthEndpoint` the `.auth()` modifier is attached to. + public let modifiedEndpoint: A + + /// The path to the property to authenticate for. + public let userKeyPath: UserKeyPath + + /// The path to the intermediate. + public let intermediateKeyPath: IntermediateKeyPath + + /// Initializes the modifier with its underlying `QueryEndpoint` and its + /// `auth` path, which is the keypath to the property to run authentication + /// for. + /// + /// - Parameters: + /// - queryEndpoint: The `QueryEndpoint` which the modifer is attached + /// to. + /// - intermediate: A `KeyPath` to the intermediate. + /// - user: A `KeyPath` which leads to the property to authenticate for. + /// - operationType: The HTTP method of the wrapped component. + public init( + _ authEndpoint: A, + intermediate: IntermediateKeyPath, + user: UserKeyPath + ) { + self.modifiedEndpoint = authEndpoint + self.intermediateKeyPath = intermediate + self.userKeyPath = user + } + + /// Returns the `queryEndpoint`'s query. + /// + /// - Parameter req: An incoming `Request`. + /// - Returns: A `QueryBuilder`, which represents a `Fluent` query defined + /// by the `queryEndpoint`. + /// - Throws: An `Abort` error if the item is not found. + public func query(_ req: Request) throws -> QueryBuilder { + try modifiedEndpoint.query(req) + } + + /// A method which checks if the user `U` supplied in the `Request` is + /// equal to the user belonging to the particular `QuerySubject`. + /// + /// - Parameter req: An incoming `Request`. + /// - Returns: An `EventLoopFuture` containing an eagerloaded value as + /// defined by `Element`. If authentication fails or a user is not found, + /// HTTP `.unauthorized` and `.notFound` are thrown respectively. + /// - Throws: An `Abort` error if an item is not found. + public func handler(_ req: Request) throws -> + EventLoopFuture<[QuerySubject]> + { + try query(req) + .with(intermediateKeyPath) { + $0.with(userKeyPath) + }.all() + .flatMapEachCompactThrowing { item -> QuerySubject? in + guard let intermediate = item[ + keyPath: self.intermediateKeyPath + ].value else { + throw Abort(.notFound) + } + + guard let user = intermediate[ + keyPath: self.userKeyPath + ].value else { + throw Abort(.notFound) + } + + guard let authorized = req.auth.get(U.self) else { + throw Abort(.unauthorized) + } + + if authorized.id == user.id { + return item + } else { + return nil + } + } + } +} + +/// An extension that adds a version of the `.auth()` modifier to components +/// conforming to `AuthEndpoint` that allows defining an intermediate type `I`. +extension ReadAll { + + /// A modifier used to make sure components only authorize requests where + /// the supplied user `U` is actually related to the `QuerySubject`. + /// + /// - Parameter intermediate: A `KeyPath` to the intermediate property. + /// - Parameter user: A `KeyPath` to the related user property from the + /// intermediate. + /// - Returns: An instance of a `AuthModifier` with the supplied `KeyPath` + /// to the user. + public func auth ( + _ intermediate: NestedReadAllAuthModifier + .IntermediateKeyPath, + _ user: NestedReadAllAuthModifier.UserKeyPath + ) -> NestedReadAllAuthModifier { + NestedReadAllAuthModifier(self, intermediate: intermediate, user: user) + } +} diff --git a/Sources/Corvus/Endpoints/Modifiers/Auth/ReadAllAuthModifier.swift b/Sources/Corvus/Endpoints/Modifiers/Auth/ReadAllAuthModifier.swift new file mode 100644 index 0000000..17b0b7f --- /dev/null +++ b/Sources/Corvus/Endpoints/Modifiers/Auth/ReadAllAuthModifier.swift @@ -0,0 +1,108 @@ +import Vapor +import Fluent + +/// A class that wraps a component which utilizes an `.auth()` modifier. Differs +/// from `AuthModifier` by authenticating on the user of an intermediate parent +/// `I` of `A.QuerySubject`. Requires an object `U` that represents the user to +/// authorize. +public final class ReadAllAuthModifier< + A: AuthEndpoint, + U: CorvusModelAuthenticatable>: +ReadEndpoint { + + /// The return value of the `.query()`, so the type being operated on in + /// the current component. + public typealias QuerySubject = A.QuerySubject + + /// The `KeyPath` to the user property of the intermediate `I` which is to + /// be authenticated. + public typealias UserKeyPath = KeyPath< + A.QuerySubject, + A.QuerySubject.Parent + > + + /// The `AuthEndpoint` the `.auth()` modifier is attached to. + public let modifiedEndpoint: A + + /// The path to the property to authenticate for. + public let userKeyPath: UserKeyPath + + + /// Initializes the modifier with its underlying `QueryEndpoint` and its + /// `auth` path, which is the keypath to the property to run authentication + /// for. + /// + /// - Parameters: + /// - queryEndpoint: The `QueryEndpoint` which the modifer is attached + /// to. + /// - user: A `KeyPath` which leads to the property to authenticate for. + /// - operationType: The HTTP method of the wrapped component. + public init( + _ authEndpoint: A, + user: UserKeyPath + ) { + self.modifiedEndpoint = authEndpoint + self.userKeyPath = user + } + + /// Returns the `queryEndpoint`'s query. + /// + /// - Parameter req: An incoming `Request`. + /// - Returns: A `QueryBuilder`, which represents a `Fluent` query defined + /// by the `queryEndpoint`. + /// - Throws: An `Abort` error if the item is not found. + public func query(_ req: Request) throws -> QueryBuilder { + try modifiedEndpoint.query(req) + } + + /// A method which checks if the user `U` supplied in the `Request` is + /// equal to the user belonging to the particular `QuerySubject`. + /// + /// - Parameter req: An incoming `Request`. + /// - Returns: An `EventLoopFuture` containing an eagerloaded value as + /// defined by `Element`. If authentication fails or a user is not found, + /// HTTP `.unauthorized` and `.notFound` are thrown respectively. + /// - Throws: An `Abort` error if an item is not found. + public func handler(_ req: Request) throws -> + EventLoopFuture<[QuerySubject]> + { + try query(req) + .with(userKeyPath) + .all() + .flatMapEachCompactThrowing { item -> QuerySubject? in + guard let user = item[ + keyPath: self.userKeyPath + ].value else { + throw Abort(.notFound) + } + + guard let authorized = req.auth.get(U.self) else { + throw Abort(.unauthorized) + } + + if authorized.id == user.id { + return item + } else { + return nil + } + } + } +} + +/// An extension that adds a version of the `.auth()` modifier to components +/// conforming to `AuthEndpoint` that allows defining an intermediate type `I`. +extension ReadAll { + + /// A modifier used to make sure components only authorize requests where + /// the supplied user `U` is actually related to the `QuerySubject`. + /// + /// - Parameter user: A `KeyPath` to the related user property from the + /// intermediate. + /// - Returns: An instance of a `AuthModifier` with the supplied `KeyPath` + /// to the user. + public func auth ( + _ user: ReadAllAuthModifier.UserKeyPath + ) -> ReadAllAuthModifier { + ReadAllAuthModifier(self, user: user) + } +} diff --git a/Sources/Corvus/Endpoints/Read/ReadAll.swift b/Sources/Corvus/Endpoints/Read/ReadAll.swift index 7acbf8f..01815b0 100644 --- a/Sources/Corvus/Endpoints/Read/ReadAll.swift +++ b/Sources/Corvus/Endpoints/Read/ReadAll.swift @@ -4,9 +4,12 @@ import Fluent /// A class that provides functionality to read all objects of a generic type /// `T` conforming to `CorvusModel`. public final class ReadAll: ReadEndpoint { - - /// The return type of the `.handler()`. + + /// The return type of the `.query()`. public typealias QuerySubject = T + + /// The return type of the `.handler()`. + public typealias Element = [T] /// A property that describes if only existing, only trashed or both objects /// should be read from the database. @@ -30,7 +33,7 @@ public final class ReadAll: ReadEndpoint { /// - Returns: An array of `QuerySubjects`. /// - Throws: An `Abort` error if something goes wrong. public func handler(_ req: Request) throws -> - EventLoopFuture<[QuerySubject]> + EventLoopFuture { switch target.option { case .existing: diff --git a/Tests/CorvusTests/AuthorizationTests.swift b/Tests/CorvusTests/AuthorizationTests.swift index 4880466..bba99ac 100644 --- a/Tests/CorvusTests/AuthorizationTests.swift +++ b/Tests/CorvusTests/AuthorizationTests.swift @@ -28,9 +28,7 @@ final class AuthorizationTests: CorvusTests { BearerAuthGroup("transactions") { Create().auth(\.$account, \.$user) - ReadAll() - .filter(\.$currency == "USD") - .auth(\.$account, \.$user) + ReadAll().auth(\.$account, \.$user) Group(transactionParameter) { ReadOne(transactionParameter) @@ -170,7 +168,8 @@ final class AuthorizationTests: CorvusTests { "Authorization": "\(user1.bearerToken())" ] ) { res in - XCTAssertEqual(res.status, .unauthorized) + let content = try res.content.decode([Transaction].self) + XCTAssertEqual(content, [transaction1]) } .test( .GET, @@ -178,10 +177,10 @@ final class AuthorizationTests: CorvusTests { headers: [ "Authorization": "\(user2.bearerToken())" ] - ) { res in - let content = try res.content.decode([Transaction].self) - XCTAssertEqual(content.count, 1) - } + ) { res in + let content = try res.content.decode([Transaction].self) + XCTAssertEqual(content, [transaction2]) + } } func testNestedCreateAuthModifier() throws {