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

Commit

Permalink
Added ReadAllAuthModifier
Browse files Browse the repository at this point in the history
  • Loading branch information
Berzan Yildiz committed Apr 17, 2020
1 parent 78dd5da commit c8dbea6
Show file tree
Hide file tree
Showing 3 changed files with 281 additions and 22 deletions.
40 changes: 18 additions & 22 deletions Sources/Corvus/Endpoints/Modifiers/Auth/NestedAuthModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,31 +80,27 @@ AuthEndpoint, RestEndpointModifier {
/// - Throws: An `Abort` error if an item is not found.
public func handler(_ req: Request) throws -> EventLoopFuture<Element> {
let users = try query(req)
.with(intermediateKeyPath)
.all()
.mapEach {
$0[keyPath: self.intermediateKeyPath].value
}.map {
$0.first
}
.unwrap(or: Abort(.internalServerError))
.unwrap(or: Abort(.internalServerError))
.flatMap {
I.query(on: req.db)
.filter(\I._$id == $0.id!)
.with(self.userKeyPath)
.all()
.mapEach {
$0[keyPath: self.userKeyPath].value
}
}

let authorized: EventLoopFuture<[Bool]> = users
.mapEachThrowing { optionalUser throws -> Bool in
guard let user = optionalUser else {
.with(intermediateKeyPath) {
$0.with(userKeyPath)
}.all()
.mapEachThrowing { item -> T 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)
}

return user
}

let authorized: EventLoopFuture<[Bool]> = users
.mapEachThrowing { user throws -> Bool in
guard let authorized = req.auth.get(T.self) else {
throw Abort(.unauthorized)
}
Expand Down
129 changes: 129 additions & 0 deletions Sources/Corvus/Endpoints/Modifiers/Auth/ReadAllAuthModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
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 `T` that represents the user to
/// authorize.
public final class ReadAllAuthModifier<
A: AuthEndpoint,
I: CorvusModel,
T: CorvusModelAuthenticatable>:
AuthEndpoint, RestEndpointModifier {

/// 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<T>
>

/// 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 `T` 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(T.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 `T` 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, T: CorvusModelAuthenticatable> (
_ intermediate: ReadAllAuthModifier<ReadAll, I, T>.IntermediateKeyPath,
_ user: ReadAllAuthModifier<ReadAll, I, T>.UserKeyPath
) -> ReadAllAuthModifier<ReadAll, I, T> {
ReadAllAuthModifier(self, intermediate: intermediate, user: user)
}
}
134 changes: 134 additions & 0 deletions Tests/CorvusTests/AuthenticationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -891,4 +891,138 @@ final class AuthenticationTests: XCTestCase {
XCTAssertEqualJSON(res.body.string, transaction)
}
}

func testReadAllAuthModifier() throws {
final class ReadAllAuthModifierTest: RestApi {

var content: Endpoint {
Group("api") {
CRUD<CorvusUser>("users", softDelete: false)

Group("accounts") {
Create<SecureAccount>()
}

BasicAuthGroup<CorvusUser>("transactions") {
Create<SecureTransaction>()
ReadAll<SecureTransaction>()
.auth(\.$account, \.$user)
}
}
}
}

let app = Application(.testing)
defer { app.shutdown() }
let readAllAuthModifierTest = ReadAllAuthModifierTest()

app.databases.use(.sqlite(.memory), as: .test, isDefault: true)
app.middleware.use(CorvusUser.authenticator())
app.migrations.add(CreateSecureAccount())
app.migrations.add(CreateSecureTransaction())
app.migrations.add(CreateCorvusUser())

try app.autoMigrate().wait()

try app.register(collection: readAllAuthModifierTest)

let user1 = CorvusUser(
username: "berzan",
passwordHash: try Bcrypt.hash("pass")
)

let user2 = CorvusUser(
username: "paul",
passwordHash: try Bcrypt.hash("pass")
)

var account: SecureAccount!
var transaction: SecureTransaction!

let basic1 = "berzan:pass"
.data(using: .utf8)!
.base64EncodedString()

let basic2 = "paul:pass"
.data(using: .utf8)!
.base64EncodedString()

try app.testable()
.test(
.POST,
"/api/users",
headers: ["content-type": "application/json"],
body: user1.encode(),
afterResponse: { res in
let userRes = try res.content.decode(CorvusUser.self)
account = SecureAccount(
name: "berzan",
userID: userRes.id!
)
}
)
.test(
.POST,
"/api/users",
headers: ["content-type": "application/json"],
body: user2.encode()
)
.test(
.POST,
"/api/accounts",
headers: ["content-type": "application/json"],
body: account.encode()
) { res in
let accountRes = try res.content.decode(SecureAccount.self)
transaction = SecureTransaction(
amount: 42.0,
currency: "",
accountID: accountRes.id!
)
}
.test(
.POST,
"/api/transactions",
headers: [
"content-type": "application/json",
"Authorization": "Basic \(basic1)"
],
body: transaction.encode()
)
.test(
.POST,
"/api/transactions",
headers: [
"content-type": "application/json",
"Authorization": "Basic \(basic1)"
],
body: transaction.encode()
)
.test(
.GET,
"/api/transactions",
headers: [
"Authorization": "Basic \(basic2)"
]
) { res in
let transactions = try res.content.decode(
[SecureTransaction].self
)

XCTAssertEqual(transactions.count, 0)
}
.test(
.GET,
"/api/transactions",
headers: [
"Authorization": "Basic \(basic1)"
]
) { res in
let transactions = try res.content.decode(
[SecureTransaction].self
)

XCTAssertEqual(transactions.count, 2)
}
}
}

0 comments on commit c8dbea6

Please sign in to comment.