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

Added ReadAllauth modifiers back in #38

Merged
merged 1 commit into from
May 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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