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

Feature/auth support #20

Merged
merged 6 commits into from
Apr 14, 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
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ let package = Package(
// 💧 Vapor's ORM Framework.
.package(
url: "https://github.com/vapor/fluent.git",
from: "4.0.0-rc"
from: "4.0.0-rc.2"
),

// A database driver for testing.
.package(
url: "https://github.com/vapor/fluent-sqlite-driver.git",
from: "4.0.0-rc"
from: "4.0.0-rc.1.1"
)
],
targets: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ extension CorvusModelTokenAuthenticatable {
extension CorvusModelTokenAuthenticatable {

/// Provides a `Vapor` authenticator defined below.
/// - Parameter database: The database to authenticate.
/// - Returns: A `CorvusModelTokenAuthenticator`.
public static func authenticator(
database: DatabaseID? = nil
) -> CorvusModelTokenAuthenticator<Self> {
Expand Down Expand Up @@ -84,7 +86,7 @@ BearerAuthenticator
/// - Parameters:
/// - bearer: The bearer token passed in the request.
/// - request: The `Request` to be authenticated.
/// - Returns: The `User` the token belongs to.
/// - Returns: An empty `EventLoopFuture`.
public func authenticate(
bearer: BearerAuthorization,
for request: Request
Expand Down
4 changes: 4 additions & 0 deletions Sources/Corvus/Authentication/CorvusToken.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public struct CreateCorvusToken: Migration {
public init() {}

/// Prepares database fields and their value types.
/// - Parameter database: The database to authenticate.
/// - Returns: An empty `EventLoopFuture`.
public func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema(CorvusToken.schema)
.id()
Expand All @@ -62,6 +64,8 @@ public struct CreateCorvusToken: Migration {
}

/// Implements functionality to delete schema when database is reverted.
/// - Parameter database: The database to authenticate.
/// - Returns: An empty `EventLoopFuture`.
public func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema(CorvusToken.schema).delete()
}
Expand Down
5 changes: 5 additions & 0 deletions Sources/Corvus/Authentication/CorvusUser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public struct CreateCorvusUser: Migration {
public init() {}

/// Prepares database fields and their value types.
/// - Parameter database: The database to authenticate.
/// - Returns: An empty `EventLoopFuture`.
public func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema(CorvusUser.schema)
.id()
Expand All @@ -60,6 +62,8 @@ public struct CreateCorvusUser: Migration {
}

/// Implements functionality to delete schema when database is reverted.
/// - Parameter database: The database to authenticate.
/// - Returns: An empty `EventLoopFuture`.
public func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema(CorvusUser.schema).delete()
}
Expand All @@ -80,6 +84,7 @@ extension CorvusUser: CorvusModelAuthenticatable {
/// - Parameter password: The password to verify.
/// - Returns: True if the provided password matches the user's, false if
/// not.
/// - Throws: An error if encryption fails.
public func verify(password: String) throws -> Bool {
try Bcrypt.verify(password, created: self.passwordHash)
}
Expand Down
36 changes: 32 additions & 4 deletions Sources/Corvus/Endpoints/CRUD.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,33 @@ import Vapor

/// A class that contains Create, Read, Update and Delete functionality for a
/// generic type `T` conforming to `CorvusModel` grouped under a given path.
public final class CRUD<T: CorvusModel>: Endpoint {
public class CRUD<T: CorvusModel>: Endpoint {

/// The route path to the parameters.
let pathComponents: [PathComponent]

/// A property to generate route parameter placeholders.
let parameter = Parameter<T>()

/// Indicates wether soft delete should be included or not.
/// Indicates whether soft delete should be included or not.
let useSoftDelete: Bool

/// Initializes the component with one or more route path components.
///
/// - Parameter pathComponents: One or more `PathComponents` identifying the
/// path to the operations defined by the `CRUD` component.
/// - Parameter softDelete: Enable/Disable soft deletion of Models.
public init(_ pathComponents: PathComponent..., softDelete: Bool = true) {
public init(_ pathComponents: PathComponent..., softDelete: Bool = false) {
self.pathComponents = pathComponents
self.useSoftDelete = softDelete
}

/// Initializes the component with multiple route path components.
///
/// - Parameter pathComponents: Multiple `PathComponents` identifying the
/// path to the operations defined by the `CRUD` component.
/// - Parameter softDelete: Enable/Disable soft deletion of Models.
init(_ pathComponents: [PathComponent], softDelete: Bool = false) {
self.pathComponents = pathComponents
self.useSoftDelete = softDelete
}
Expand Down Expand Up @@ -52,7 +62,7 @@ public final class CRUD<T: CorvusModel>: Endpoint {
Group(parameter.id) {
ReadOne<T>(parameter.id)
Update<T>(parameter.id)
SoftDelete<T>(parameter.id)
Delete<T>(parameter.id, softDelete: true)
}

Group("trash") {
Expand All @@ -69,3 +79,21 @@ public final class CRUD<T: CorvusModel>: Endpoint {
}
}
}
/// An extension that adds the `.auth()` modifier to `CRUD` components.
extension CRUD {

/// A modifier used to make sure components only authorize requests where
/// the supplied user `T` is actually related to the `QuerySubject`.
///
/// - Parameter user: A `KeyPath` to the related user property.
/// - Returns: An instance of a `SecureCRUD` with the supplied `KeyPath` to
/// the user.
public func auth<A: CorvusModelAuthenticatable>(
bmikaili marked this conversation as resolved.
Show resolved Hide resolved
_ user: KeyPath<
T,
T.Parent<A>
>
) -> CRUD<T> {
SecureCRUD<T, A>(pathComponents, user: user)
}
}
2 changes: 1 addition & 1 deletion Sources/Corvus/Endpoints/Create/Create.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Vapor

/// A class that provides functionality to create objects of a generic type
/// `T` conforming to `CorvusModel`.
public final class Create<T: CorvusModel>: QueryEndpoint {
public final class Create<T: CorvusModel>: CreateEndpoint {

/// The return type of the `.handler()`.
public typealias QuerySubject = T
Expand Down
1 change: 1 addition & 0 deletions Sources/Corvus/Endpoints/Custom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public final class Custom<R: ResponseEncodable>: RestEndpoint {
///
/// - Parameter req: An incoming `Request`.
/// - Returns: An element of type `T`.
/// - Throws: An `Abort` error if something goes wrong.
public func handler(_ req: Request) throws -> EventLoopFuture<Element> {
try customHandler(req)
}
Expand Down
19 changes: 17 additions & 2 deletions Sources/Corvus/Endpoints/Delete/Delete.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,21 @@ public final class Delete<T: CorvusModel>: AuthEndpoint {

/// The id of the object to be deleted.
let id: PathComponent

/// Indicates whether soft delete should be included or not.
let useSoftDelete: Bool

/// The HTTP operation type of the component.
public let operationType: OperationType = .delete

/// Initializes the component with a given path parameter.
///
/// - Parameter id: A `PathComponent` which represents the ID of the item.
public init(_ id: PathComponent) {
/// - Parameter softDelete: Whether deletion should include soft deleted
/// items or not.
public init(_ id: PathComponent, softDelete: Bool = false) {
bmikaili marked this conversation as resolved.
Show resolved Hide resolved
self.id = id
self.useSoftDelete = softDelete
}

/// A method to find an item by an ID supplied in the `Request`.
Expand All @@ -39,6 +45,11 @@ public final class Delete<T: CorvusModel>: AuthEndpoint {
) else {
throw Abort(.badRequest)
}

if useSoftDelete {
return T.query(on: req.db).filter(\T._$id == itemId)
}

return T.query(on: req.db).withDeleted().filter(\T._$id == itemId)
}

Expand All @@ -52,7 +63,11 @@ public final class Delete<T: CorvusModel>: AuthEndpoint {
try query(req)
.first()
.unwrap(or: Abort(.notFound))
.flatMap { $0.delete(force: true, on: req.db) }
.flatMap {
self.useSoftDelete
? $0.delete(on: req.db)
: $0.delete(force: true, on: req.db)
}
.map { .ok }
}
}
57 changes: 0 additions & 57 deletions Sources/Corvus/Endpoints/Delete/SoftDelete.swift

This file was deleted.

2 changes: 1 addition & 1 deletion Sources/Corvus/Endpoints/Modifiers/Auth/AuthModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Fluent
public final class AuthModifier<
A: AuthEndpoint,
T: CorvusModelAuthenticatable>:
AuthEndpoint, RestEndpointModfier {
AuthEndpoint, RestEndpointModifier {

/// The return type for the `.handler()` modifier.
public typealias Element = A.Element
Expand Down
96 changes: 96 additions & 0 deletions Sources/Corvus/Endpoints/Modifiers/Auth/CreateAuthModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import Vapor
import Fluent

/// A class that wraps a `Create` component which utilizes an `.auth()`
/// modifier. That allows Corvus to chain modifiers, as it gets treated as any
/// other struct conforming to `CrateAuthEndpoint`. Requires an object `T` that
/// represents the user to authorize.
public final class CreateAuthModifier<
A: CreateEndpoint,
T: CorvusModelAuthenticatable>:
CreateEndpoint, RestEndpointModifier {

/// The return type for the `.handler()` modifier.
public typealias Element = A.Element

/// 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 `QuerySubject` which is to be
/// authenticated.
public typealias UserKeyPath = KeyPath<
A.QuerySubject,
A.QuerySubject.Parent<T>
>

/// The `ReadEndpoint` 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 `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<Element> {
let requestContent = try req.content.decode(A.QuerySubject.self)
let requestUser = requestContent[keyPath: self.userKeyPath]

guard let authorized = req.auth.get(T.self) else {
throw Abort(.unauthorized)
}

if authorized.id == requestUser.id {
return try modifiedEndpoint.handler(req)
} else {
return req.eventLoop.makeFailedFuture(Abort(.unauthorized))
}
}
}

/// An extension that adds the `.auth()` modifier to components conforming to
/// `CreateAuthEndpoint`.
extension CreateEndpoint {
bmikaili marked this conversation as resolved.
Show resolved Hide resolved

/// A modifier used to make sure components only authorize requests where
/// the supplied user `T` is actually related to the `QuerySubject`.
///
/// - Parameter user: A `KeyPath` to the related user property.
/// - Returns: An instance of a `CreateAuthModifier` with the supplied
/// `KeyPath` to the user.
public func auth<T: CorvusModelAuthenticatable>(
_ user: CreateAuthModifier<Self, T>.UserKeyPath
) -> CreateAuthModifier<Self, T> {
CreateAuthModifier(self, user: user)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Fluent
/// struct conforming to `AuthEndpoint`.
public final class UserAuthModifier<
A: AuthEndpoint
>: AuthEndpoint, RestEndpointModfier
>: AuthEndpoint, RestEndpointModifier
where A.QuerySubject: CorvusModelAuthenticatable {

/// The return type for the `.handler()` modifier.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Fluent
public final class ResponseModifier<
Endpoint: RestEndpoint,
Response: CorvusResponse>:
RestEndpointModfier where Endpoint.Element == Response.Item {
RestEndpointModifier where Endpoint.Element == Response.Item {

/// The `RestEndpoint` the `.respond(with:)` modifier is attached to.
public let modifiedEndpoint: Endpoint
Expand Down
Loading