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

Commit

Permalink
Added ReadAll auth modifier non nested
Browse files Browse the repository at this point in the history
  • Loading branch information
Berzan Yildiz committed Apr 17, 2020
1 parent c8dbea6 commit e7f1397
Show file tree
Hide file tree
Showing 3 changed files with 261 additions and 34 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 `T` that represents the user to
/// authorize.
public final class NestedReadAllAuthModifier<
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: NestedReadAllAuthModifier<ReadAll, I, T>
.IntermediateKeyPath,
_ user: NestedReadAllAuthModifier<ReadAll, I, T>.UserKeyPath
) -> NestedReadAllAuthModifier<ReadAll, I, T> {
NestedReadAllAuthModifier(self, intermediate: intermediate, user: user)
}
}
39 changes: 9 additions & 30 deletions Sources/Corvus/Endpoints/Modifiers/Auth/ReadAllAuthModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import Fluent
/// authorize.
public final class ReadAllAuthModifier<
A: AuthEndpoint,
I: CorvusModel,
T: CorvusModelAuthenticatable>:
AuthEndpoint, RestEndpointModifier {

Expand All @@ -18,24 +17,16 @@ AuthEndpoint, RestEndpointModifier {
/// 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>
A.QuerySubject.Parent<T>
>

/// 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
Expand All @@ -44,16 +35,13 @@ AuthEndpoint, RestEndpointModifier {
/// - 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
}

Expand All @@ -79,17 +67,10 @@ AuthEndpoint, RestEndpointModifier {
EventLoopFuture<[QuerySubject]>
{
try query(req)
.with(intermediateKeyPath) {
$0.with(userKeyPath)
}.all()
.with(userKeyPath)
.all()
.flatMapEachCompactThrowing { item -> QuerySubject? in
guard let intermediate = item[
keyPath: self.intermediateKeyPath
].value else {
throw Abort(.notFound)
}

guard let user = intermediate[
guard let user = item[
keyPath: self.userKeyPath
].value else {
throw Abort(.notFound)
Expand All @@ -115,15 +96,13 @@ 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)
public func auth<T: CorvusModelAuthenticatable> (
_ user: ReadAllAuthModifier<ReadAll, T>.UserKeyPath
) -> ReadAllAuthModifier<ReadAll, T> {
ReadAllAuthModifier(self, user: user)
}
}
126 changes: 122 additions & 4 deletions Tests/CorvusTests/AuthenticationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -892,8 +892,8 @@ final class AuthenticationTests: XCTestCase {
}
}

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

var content: Endpoint {
Group("api") {
Expand All @@ -914,7 +914,7 @@ final class AuthenticationTests: XCTestCase {

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

app.databases.use(.sqlite(.memory), as: .test, isDefault: true)
app.middleware.use(CorvusUser.authenticator())
Expand All @@ -924,7 +924,7 @@ final class AuthenticationTests: XCTestCase {

try app.autoMigrate().wait()

try app.register(collection: readAllAuthModifierTest)
try app.register(collection: nestedReadAllAuthModifierTest)

let user1 = CorvusUser(
username: "berzan",
Expand Down Expand Up @@ -1025,4 +1025,122 @@ final class AuthenticationTests: XCTestCase {
XCTAssertEqual(transactions.count, 2)
}
}

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

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

BasicAuthGroup<CorvusUser>("accounts") {
CRUD<SecureAccount>().auth(\.$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(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 account1: SecureAccount!
var account2: SecureAccount!

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)
account1 = SecureAccount(
name: "berzan1",
userID: userRes.id!
)
account2 = SecureAccount(
name: "berzan2",
userID: userRes.id!
)
}
)
.test(
.POST,
"/api/users",
headers: ["content-type": "application/json"],
body: user2.encode()
)
.test(
.POST,
"/api/accounts",
headers: [
"content-type": "application/json",
"Authorization": "Basic \(basic1)"
],
body: account1.encode()
)
.test(
.POST,
"/api/accounts",
headers: [
"content-type": "application/json",
"Authorization": "Basic \(basic1)"
],
body: account2.encode()
)
.test(
.GET,
"/api/accounts",
headers: [
"Authorization": "Basic \(basic2)"
]
) { res in
let accounts = try res.content.decode(
[SecureAccount].self
)

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

XCTAssertEqual(accounts.count, 2)
}
}
}

0 comments on commit e7f1397

Please sign in to comment.