Skip to content
This repository was archived by the owner on Sep 7, 2021. It is now read-only.

Commit

Permalink
Added ReadAllauth modifiers back in (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
Berzan Yildiz authored May 21, 2020
1 parent 4392c62 commit 66579c1
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -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<U>
>

/// The `KeyPath` to the intermediate `I` of the endpoint's `QuerySubject`.
public typealias IntermediateKeyPath = KeyPath<
A.QuerySubject,
A.QuerySubject.Parent<I>
>

/// 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<QuerySubject> {
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<I: CorvusModel, U: CorvusModelAuthenticatable> (
_ intermediate: NestedReadAllAuthModifier<ReadAll, I, U>
.IntermediateKeyPath,
_ user: NestedReadAllAuthModifier<ReadAll, I, U>.UserKeyPath
) -> NestedReadAllAuthModifier<ReadAll, I, U> {
NestedReadAllAuthModifier(self, intermediate: intermediate, user: user)
}
}
108 changes: 108 additions & 0 deletions Sources/Corvus/Endpoints/Modifiers/Auth/ReadAllAuthModifier.swift
Original file line number Diff line number Diff line change
@@ -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<U>
>

/// 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<QuerySubject> {
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<U: CorvusModelAuthenticatable> (
_ user: ReadAllAuthModifier<ReadAll, U>.UserKeyPath
) -> ReadAllAuthModifier<ReadAll, U> {
ReadAllAuthModifier(self, user: user)
}
}
9 changes: 6 additions & 3 deletions Sources/Corvus/Endpoints/Read/ReadAll.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: CorvusModel>: 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.
Expand All @@ -30,7 +33,7 @@ public final class ReadAll<T: CorvusModel>: ReadEndpoint {
/// - Returns: An array of `QuerySubjects`.
/// - Throws: An `Abort` error if something goes wrong.
public func handler(_ req: Request) throws ->
EventLoopFuture<[QuerySubject]>
EventLoopFuture<Element>
{
switch target.option {
case .existing:
Expand Down
15 changes: 7 additions & 8 deletions Tests/CorvusTests/AuthorizationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ final class AuthorizationTests: CorvusTests {

BearerAuthGroup<CorvusToken>("transactions") {
Create<Transaction>().auth(\.$account, \.$user)
ReadAll<Transaction>()
.filter(\.$currency == "USD")
.auth(\.$account, \.$user)
ReadAll<Transaction>().auth(\.$account, \.$user)

Group(transactionParameter) {
ReadOne<Transaction>(transactionParameter)
Expand Down Expand Up @@ -170,18 +168,19 @@ 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,
"/api/transactions",
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 {
Expand Down

0 comments on commit 66579c1

Please sign in to comment.