From 57487ef92422a054cd1c7ee31d027e67ca7cecf1 Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Wed, 15 Feb 2017 16:01:02 +0100 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 +++ Package.swift | 16 ++++++ Sources/Authentication/AuthError.swift | 11 ++++ .../Authenticator/Authenticator.swift | 8 +++ .../Authentication/Authenticator/Realm.swift | 13 +++++ .../Authenticator/User+Authenticator.swift | 52 +++++++++++++++++++ .../Authentication/CacheSessionManager.swift | 41 +++++++++++++++ .../Credentials/Identifier.swift | 16 ++++++ .../Authentication/Header/Authorization.swift | 7 +++ Sources/Authentication/Header/Basic.swift | 26 ++++++++++ Sources/Authentication/Header/Bearer.swift | 14 +++++ Sources/Authentication/Request+Subject.swift | 7 +++ Sources/Authentication/User/User.swift | 10 ++++ Sources/Authorization/Test.swift | 1 + Tests/AuthenticationTests/ExampleTests.swift | 13 +++++ .../Utilities/AuthUser.swift | 42 +++++++++++++++ Tests/LinuxMain.swift | 6 +++ 17 files changed, 289 insertions(+) create mode 100644 .gitignore create mode 100644 Package.swift create mode 100644 Sources/Authentication/AuthError.swift create mode 100644 Sources/Authentication/Authenticator/Authenticator.swift create mode 100644 Sources/Authentication/Authenticator/Realm.swift create mode 100644 Sources/Authentication/Authenticator/User+Authenticator.swift create mode 100644 Sources/Authentication/CacheSessionManager.swift create mode 100644 Sources/Authentication/Credentials/Identifier.swift create mode 100644 Sources/Authentication/Header/Authorization.swift create mode 100644 Sources/Authentication/Header/Basic.swift create mode 100644 Sources/Authentication/Header/Bearer.swift create mode 100644 Sources/Authentication/Request+Subject.swift create mode 100644 Sources/Authentication/User/User.swift create mode 100644 Sources/Authorization/Test.swift create mode 100644 Tests/AuthenticationTests/ExampleTests.swift create mode 100644 Tests/AuthenticationTests/Utilities/AuthUser.swift create mode 100644 Tests/LinuxMain.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb33e53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +Package.pins + diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..5183139 --- /dev/null +++ b/Package.swift @@ -0,0 +1,16 @@ +import PackageDescription + +let package = Package( + name: "Auth", + targets: [ + Target(name: "Authentication"), + Target(name: "Authorization", dependencies: ["Authentication"]), + ], + dependencies: [ + // A security framework for Swift. + .Package(url: "https://github.com/stormpath/Turnstile.git", majorVersion: 1), + + // Swift models, relationships, and querying for NoSQL and SQL databases. + .Package(url: "https://github.com/vapor/fluent.git", Version(2,0,0, prereleaseIdentifiers: ["alpha"])) + ] +) diff --git a/Sources/Authentication/AuthError.swift b/Sources/Authentication/AuthError.swift new file mode 100644 index 0000000..c2943d1 --- /dev/null +++ b/Sources/Authentication/AuthError.swift @@ -0,0 +1,11 @@ +public enum AuthError: Swift.Error { + case noSubject + case invalidAccountType + case invalidBasicAuthorization + case invalidBearerAuthorization + case noAuthorizationHeader + case notAuthenticated + case invalidIdentifier + case invalidCredentials + case unsupportedCredentials +} diff --git a/Sources/Authentication/Authenticator/Authenticator.swift b/Sources/Authentication/Authenticator/Authenticator.swift new file mode 100644 index 0000000..0cd7204 --- /dev/null +++ b/Sources/Authentication/Authenticator/Authenticator.swift @@ -0,0 +1,8 @@ +import Turnstile + +@_exported import protocol Turnstile.Credentials + +public protocol Authenticator { + static func authenticate(credentials: Credentials) throws -> User + static func register(credentials: Credentials) throws -> User +} diff --git a/Sources/Authentication/Authenticator/Realm.swift b/Sources/Authentication/Authenticator/Realm.swift new file mode 100644 index 0000000..bb79493 --- /dev/null +++ b/Sources/Authentication/Authenticator/Realm.swift @@ -0,0 +1,13 @@ +import Turnstile + +public final class AuthenticatorRealm: Realm { + public init(_ a: A.Type = A.self) { } + + public func authenticate(credentials: Credentials) throws -> Account { + return try A.authenticate(credentials: credentials) + } + + public func register(credentials: Credentials) throws -> Account { + return try A.register(credentials: credentials) + } +} diff --git a/Sources/Authentication/Authenticator/User+Authenticator.swift b/Sources/Authentication/Authenticator/User+Authenticator.swift new file mode 100644 index 0000000..69ac537 --- /dev/null +++ b/Sources/Authentication/Authenticator/User+Authenticator.swift @@ -0,0 +1,52 @@ +import Turnstile + +extension User { + public static func authenticate(credentials: Credentials) throws -> User { + if let apiKey = credentials as? APIKey { + return try authenticate(apiKey: apiKey) + } else if let accessToken = credentials as? AccessToken { + return try authenticate(accessToken: accessToken) + } else if let identifier = credentials as? Identifier { + return try authenticate(identifier: identifier) + } else { + throw AuthError.unsupportedCredentials + } + } + + public static func authenticate(apiKey: APIKey) throws -> User { + guard + let match = try Self + .query() + .filter("api_key_id", apiKey.id) + .filter("api_key_secret", apiKey.secret) + .first() + else { + throw AuthError.invalidCredentials + } + + return match + } + + public static func authenticate(accessToken: AccessToken) throws -> User { + guard + let match = try Self + .query() + .filter("access_token", accessToken.string) + .first() + else { + throw AuthError.invalidCredentials + } + + return match + } + + public static func authenticate(identifier: Identifier) throws -> User { + guard + let match = try Self.find(identifier.id) + else { + throw AuthError.invalidCredentials + } + + return match + } +} diff --git a/Sources/Authentication/CacheSessionManager.swift b/Sources/Authentication/CacheSessionManager.swift new file mode 100644 index 0000000..ef8dc84 --- /dev/null +++ b/Sources/Authentication/CacheSessionManager.swift @@ -0,0 +1,41 @@ +// PROVIDER STUFF +//import Turnstile +//import Random +//import Cache +// +//public final class CacheSessionManager: SessionManager { +// private let cache: CacheProtocol +// private let realm: Realm +// +// public init(cache: CacheProtocol, realm: Realm) { +// self.cache = cache +// self.realm = realm +// } +// +// /** +// Gets the user for the current session identifier. +// */ +// public func restoreAccount(fromSessionID identifier: String) throws -> Account { +// guard let id = try cache.get(identifier) else { +// throw AuthError.invalidIdentifier +// } +// +// return try realm.authenticate(credentials: Identifier(id: id)) +// } +// +// /** +// Creates a session for a given Subject object and returns the identifier. +// */ +// public func createSession(account: Account) -> String { +// let identifier = CryptoRandom.bytes(16).base64Encoded.string +// try? cache.set(identifier, account.uniqueID) +// return identifier +// } +// +// /** +// Destroys the session for a session identifier. +// */ +// public func destroySession(identifier: String) { +// try? cache.delete(identifier) +// } +//} diff --git a/Sources/Authentication/Credentials/Identifier.swift b/Sources/Authentication/Credentials/Identifier.swift new file mode 100644 index 0000000..60d5dc6 --- /dev/null +++ b/Sources/Authentication/Credentials/Identifier.swift @@ -0,0 +1,16 @@ +import Node +import Turnstile + +public struct Identifier: Credentials { + public let id: Node + + public init(id: Node) { + self.id = id + } +} + +extension Identifier { + public init(id: NodeRepresentable) throws { + self.init(id: try id.makeNode()) + } +} diff --git a/Sources/Authentication/Header/Authorization.swift b/Sources/Authentication/Header/Authorization.swift new file mode 100644 index 0000000..78cb620 --- /dev/null +++ b/Sources/Authentication/Header/Authorization.swift @@ -0,0 +1,7 @@ +public struct Authorization { + public let header: String + + public init(header: String) { + self.header = header + } +} diff --git a/Sources/Authentication/Header/Basic.swift b/Sources/Authentication/Header/Basic.swift new file mode 100644 index 0000000..b0232f5 --- /dev/null +++ b/Sources/Authentication/Header/Basic.swift @@ -0,0 +1,26 @@ +import Turnstile +import Foundation +import Core + +@_exported import class Turnstile.APIKey + +extension Authorization { + public var basic: APIKey? { + guard let range = header.range(of: "Basic ") else { + return nil + } + + let token = header.substring(from: range.upperBound) + + + let decodedToken = token.makeBytes().base64Decoded.string + guard let separatorRange = decodedToken.range(of: ":") else { + return nil + } + + let apiKeyID = decodedToken.substring(to: separatorRange.lowerBound) + let apiKeySecret = decodedToken.substring(from: separatorRange.upperBound) + + return APIKey(id: apiKeyID, secret: apiKeySecret) + } +} diff --git a/Sources/Authentication/Header/Bearer.swift b/Sources/Authentication/Header/Bearer.swift new file mode 100644 index 0000000..ec4bc65 --- /dev/null +++ b/Sources/Authentication/Header/Bearer.swift @@ -0,0 +1,14 @@ +import Turnstile + +@_exported import class Turnstile.AccessToken + +extension Authorization { + public var bearer: AccessToken? { + guard let range = header.range(of: "Bearer ") else { + return nil + } + + let token = header.substring(from: range.upperBound) + return AccessToken(string: token) + } +} diff --git a/Sources/Authentication/Request+Subject.swift b/Sources/Authentication/Request+Subject.swift new file mode 100644 index 0000000..e1011a1 --- /dev/null +++ b/Sources/Authentication/Request+Subject.swift @@ -0,0 +1,7 @@ +import Turnstile + +extension Subject { + var sessionIdentifier: String? { + return authDetails?.sessionID + } +} diff --git a/Sources/Authentication/User/User.swift b/Sources/Authentication/User/User.swift new file mode 100644 index 0000000..4293a1b --- /dev/null +++ b/Sources/Authentication/User/User.swift @@ -0,0 +1,10 @@ +import Fluent +import Turnstile + +public protocol User: Entity, Account, Authenticator { } + +extension User { + public var uniqueID: String { + return id?.string ?? "" + } +} diff --git a/Sources/Authorization/Test.swift b/Sources/Authorization/Test.swift new file mode 100644 index 0000000..1f5de6b --- /dev/null +++ b/Sources/Authorization/Test.swift @@ -0,0 +1 @@ +public class Test { } \ No newline at end of file diff --git a/Tests/AuthenticationTests/ExampleTests.swift b/Tests/AuthenticationTests/ExampleTests.swift new file mode 100644 index 0000000..cd11876 --- /dev/null +++ b/Tests/AuthenticationTests/ExampleTests.swift @@ -0,0 +1,13 @@ +import XCTest + +@testable import Authentication + +class ExampleTests: XCTestCase { + static let allTests = [ + ("testExample", testExample), + ] + + func testExample() throws { + + } +} diff --git a/Tests/AuthenticationTests/Utilities/AuthUser.swift b/Tests/AuthenticationTests/Utilities/AuthUser.swift new file mode 100644 index 0000000..dc3fd8a --- /dev/null +++ b/Tests/AuthenticationTests/Utilities/AuthUser.swift @@ -0,0 +1,42 @@ +import Authentication +import Node +import Fluent + +final class AuthUser: User { + var id: Node? + var name: String + var exists: Bool = false + + init(name: String) { + self.name = name + } + + init(node: Node, in context: Context) throws { + self.id = nil + self.name = try node.extract("name") + } + + func makeNode(context: Context) throws -> Node { + return try Node(node: [ + "id": id, + "name": name + ]) + } + + static func prepare(_ database: Database) throws { + + } + + static func revert(_ database: Database) throws { + + } + + static func authenticate(credentials: Credentials) throws -> Authentication.User { + return AuthUser(name: "test") + } + + + static func register(credentials: Credentials) throws -> Authentication.User { + fatalError("\(#function) not supported") + } +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..72833a6 --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,6 @@ +import XCTest +@testable import AuthenticationTests + +XCTMain([ + testCase(AuthenticationTests.allTests), +]) From 54c09275f5e23a93777fc9e3264d6bbd4bafef40 Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Wed, 15 Feb 2017 17:18:51 +0100 Subject: [PATCH 2/4] custom credentials type --- Package.swift | 3 - Sources/Authentication/AuthError.swift | 2 +- .../Authenticator/Authenticator.swift | 60 ++++++++- .../Authentication/Authenticator/Realm.swift | 13 -- .../Authenticator/User+Authenticator.swift | 60 +++------ .../Credentials/Credentials.swift | 34 +++++ .../Credentials/Identifier.swift | 15 ++- .../Authentication/Credentials/Token.swift | 25 ++++ .../Credentials/UsernamePassword.swift | 68 ++++++++++ Sources/Authentication/Header/Basic.swift | 8 +- Sources/Authentication/Header/Bearer.swift | 8 +- Sources/Authentication/Request+Subject.swift | 7 -- Sources/Authentication/User/User.swift | 9 +- Sources/Authorization/Authorizable.swift | 118 ++++++++++++++++++ Sources/Authorization/Permission.swift | 5 + Sources/Authorization/Test.swift | 1 - .../Utilities/AuthUser.swift | 9 -- 17 files changed, 336 insertions(+), 109 deletions(-) delete mode 100644 Sources/Authentication/Authenticator/Realm.swift create mode 100644 Sources/Authentication/Credentials/Credentials.swift create mode 100644 Sources/Authentication/Credentials/Token.swift create mode 100644 Sources/Authentication/Credentials/UsernamePassword.swift delete mode 100644 Sources/Authentication/Request+Subject.swift create mode 100644 Sources/Authorization/Authorizable.swift create mode 100644 Sources/Authorization/Permission.swift delete mode 100644 Sources/Authorization/Test.swift diff --git a/Package.swift b/Package.swift index 5183139..5d96629 100644 --- a/Package.swift +++ b/Package.swift @@ -7,9 +7,6 @@ let package = Package( Target(name: "Authorization", dependencies: ["Authentication"]), ], dependencies: [ - // A security framework for Swift. - .Package(url: "https://github.com/stormpath/Turnstile.git", majorVersion: 1), - // Swift models, relationships, and querying for NoSQL and SQL databases. .Package(url: "https://github.com/vapor/fluent.git", Version(2,0,0, prereleaseIdentifiers: ["alpha"])) ] diff --git a/Sources/Authentication/AuthError.swift b/Sources/Authentication/AuthError.swift index c2943d1..be343c0 100644 --- a/Sources/Authentication/AuthError.swift +++ b/Sources/Authentication/AuthError.swift @@ -1,4 +1,4 @@ -public enum AuthError: Swift.Error { +public enum AuthenticationError: Swift.Error { case noSubject case invalidAccountType case invalidBasicAuthorization diff --git a/Sources/Authentication/Authenticator/Authenticator.swift b/Sources/Authentication/Authenticator/Authenticator.swift index 0cd7204..4b364a0 100644 --- a/Sources/Authentication/Authenticator/Authenticator.swift +++ b/Sources/Authentication/Authenticator/Authenticator.swift @@ -1,8 +1,58 @@ -import Turnstile +public protocol Authenticatable { + // MARK: General + static func authenticate(_: Credentials) throws -> Self -@_exported import protocol Turnstile.Credentials + // MARK: Identifier + static func authenticate(_: Identifier) throws -> Self -public protocol Authenticator { - static func authenticate(credentials: Credentials) throws -> User - static func register(credentials: Credentials) throws -> User + // MARK: Username / Password + /// Return the user matching the supplied + /// username and password + static func authenticate(_: UsernamePassword) throws -> Self + + /// The entity's raw or hashed password + var password: String? { get } + + /// The key under which the user's username, + /// email, or other identifing value is stored. + static var usernameKey: String { get } + + /// The key under which the user's password + /// is stored. + static var passwordKey: String { get } + + + // MARK: Token + /// Returns the user matching the supplied token. + static func authenticate(_: Token) throws -> Self + + /// The token entity that contains a foreign key + /// pointer to the user table + static var tokenType: TokenCredentialEntity.Type? { get } + + // MARK: Custom + /// Returns the user matching the custom credential. + static func authenticate(custom: Any) throws -> Self +} + +extension User { + /// See User.password in Protocol + public var password: String? { + return nil + } + + /// See User.usernameKey in Protocol + public static var usernameKey: String { + return "username" + } + + /// See User.passwordKey in Protocol + public static var passwordKey: String { + return "password" + } + + /// See User.tokenType in Protocol + public static var tokenType: TokenCredentialEntity.Type? { + return nil + } } diff --git a/Sources/Authentication/Authenticator/Realm.swift b/Sources/Authentication/Authenticator/Realm.swift deleted file mode 100644 index bb79493..0000000 --- a/Sources/Authentication/Authenticator/Realm.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Turnstile - -public final class AuthenticatorRealm: Realm { - public init(_ a: A.Type = A.self) { } - - public func authenticate(credentials: Credentials) throws -> Account { - return try A.authenticate(credentials: credentials) - } - - public func register(credentials: Credentials) throws -> Account { - return try A.register(credentials: credentials) - } -} diff --git a/Sources/Authentication/Authenticator/User+Authenticator.swift b/Sources/Authentication/Authenticator/User+Authenticator.swift index 69ac537..66313f5 100644 --- a/Sources/Authentication/Authenticator/User+Authenticator.swift +++ b/Sources/Authentication/Authenticator/User+Authenticator.swift @@ -1,52 +1,24 @@ -import Turnstile - extension User { - public static func authenticate(credentials: Credentials) throws -> User { - if let apiKey = credentials as? APIKey { - return try authenticate(apiKey: apiKey) - } else if let accessToken = credentials as? AccessToken { - return try authenticate(accessToken: accessToken) - } else if let identifier = credentials as? Identifier { - return try authenticate(identifier: identifier) - } else { - throw AuthError.unsupportedCredentials - } - } + public static func authenticate(_ credentials: Credentials) throws -> Self { + let user: Self - public static func authenticate(apiKey: APIKey) throws -> User { - guard - let match = try Self - .query() - .filter("api_key_id", apiKey.id) - .filter("api_key_secret", apiKey.secret) - .first() - else { - throw AuthError.invalidCredentials - } - - return match - } - - public static func authenticate(accessToken: AccessToken) throws -> User { - guard - let match = try Self - .query() - .filter("access_token", accessToken.string) - .first() - else { - throw AuthError.invalidCredentials + switch credentials { + case .identifier(let id): + user = try authenticate(id) + case .password(let usernamePassword): + user = try authenticate(usernamePassword) + case .token(let token): + user = try authenticate(token) + case .custom: + throw AuthenticationError.unsupportedCredentials } - return match + return user } +} - public static func authenticate(identifier: Identifier) throws -> User { - guard - let match = try Self.find(identifier.id) - else { - throw AuthError.invalidCredentials - } - - return match +extension User { + public static func authenticate(custom: Any) throws -> Self { + throw AuthenticationError.unsupportedCredentials } } diff --git a/Sources/Authentication/Credentials/Credentials.swift b/Sources/Authentication/Credentials/Credentials.swift new file mode 100644 index 0000000..727d98f --- /dev/null +++ b/Sources/Authentication/Credentials/Credentials.swift @@ -0,0 +1,34 @@ +import Node + +public enum Credentials { + case identifier(Identifier) + case password(UsernamePassword) + case token(Token) + case custom(Any) + + public init(token: String) { + let token = Token(token: token) + self = .token(token) + } + + public init(identifier: Node) { + let identifier = Identifier(id: identifier) + self = .identifier(identifier) + } + + + public init(username: String, password: String, verifier: PasswordVerifier? = nil) { + let creds = UsernamePassword( + username: username, + password: password, + verifier: verifier + ) + + self = .password(creds) + } + + + public init(custom: Any) { + self = .custom(custom) + } +} diff --git a/Sources/Authentication/Credentials/Identifier.swift b/Sources/Authentication/Credentials/Identifier.swift index 60d5dc6..1d81561 100644 --- a/Sources/Authentication/Credentials/Identifier.swift +++ b/Sources/Authentication/Credentials/Identifier.swift @@ -1,16 +1,19 @@ import Node -import Turnstile -public struct Identifier: Credentials { - public let id: Node +public struct Identifier { + let id: Node public init(id: Node) { self.id = id } } -extension Identifier { - public init(id: NodeRepresentable) throws { - self.init(id: try id.makeNode()) +extension User { + public static func authenticate(_ id: Identifier) throws -> Self { + guard let match = try Self.find(id.id) else { + throw AuthenticationError.invalidCredentials + } + + return match } } diff --git a/Sources/Authentication/Credentials/Token.swift b/Sources/Authentication/Credentials/Token.swift new file mode 100644 index 0000000..eb7a938 --- /dev/null +++ b/Sources/Authentication/Credentials/Token.swift @@ -0,0 +1,25 @@ +public struct Token { + public let token: String + + public init(token: String) { + self.token = token + } +} + +extension User { + public static func authenticate(_ token: Token) throws -> Self { + // guard let tokenType = tokenType else { + // throw AuthError.unsupportedCredentials + // } + + // guard let user = try Self.query() + // .union(tokenType) + // .filter(tokenType, tokenType.tokenKey, token) + // .first() + // else { + // throw AuthError.unsupportedCredentials + // } + + throw AuthenticationError.unsupportedCredentials + } +} diff --git a/Sources/Authentication/Credentials/UsernamePassword.swift b/Sources/Authentication/Credentials/UsernamePassword.swift new file mode 100644 index 0000000..4c42522 --- /dev/null +++ b/Sources/Authentication/Credentials/UsernamePassword.swift @@ -0,0 +1,68 @@ +public struct UsernamePassword { + public let username: String + public let password: String + public let verifier: PasswordVerifier? + + public init(username: String, password: String, verifier: PasswordVerifier? = nil) { + self.username = username + self.password = password + self.verifier = verifier + } +} + + +public protocol PasswordVerifier { + func verify(password: String, matchesPassword: String) throws -> Bool +} + +extension User { + public static func authenticate(_ creds: UsernamePassword) throws -> Self { + let user: Self + + if let verifier = creds.verifier { + guard let match = try Self + .query() + .filter(usernameKey, creds.username) + .first() + else { + throw AuthenticationError.invalidCredentials + } + + guard let matchPassword = match.password else { + throw AuthenticationError.invalidCredentials + } + + guard try verifier.verify( + password: creds.password, + matchesPassword: matchPassword + ) else { + throw AuthenticationError.invalidCredentials + } + + user = match + } else { + guard let match = try Self + .query() + .filter(usernameKey, creds.username) + .filter(passwordKey, creds.password) + .first() + else { + throw AuthenticationError.invalidCredentials + } + + user = match + } + + return user + } +} + + + + +import Fluent + +public protocol TokenCredentialEntity: Entity { + func authenticationUser() throws -> User + static var tokenKey: String { get } +} diff --git a/Sources/Authentication/Header/Basic.swift b/Sources/Authentication/Header/Basic.swift index b0232f5..62be6f4 100644 --- a/Sources/Authentication/Header/Basic.swift +++ b/Sources/Authentication/Header/Basic.swift @@ -1,11 +1,7 @@ -import Turnstile -import Foundation import Core -@_exported import class Turnstile.APIKey - extension Authorization { - public var basic: APIKey? { + public var basic: Credentials? { guard let range = header.range(of: "Basic ") else { return nil } @@ -21,6 +17,6 @@ extension Authorization { let apiKeyID = decodedToken.substring(to: separatorRange.lowerBound) let apiKeySecret = decodedToken.substring(from: separatorRange.upperBound) - return APIKey(id: apiKeyID, secret: apiKeySecret) + return Credentials(username: apiKeyID, password: apiKeySecret) } } diff --git a/Sources/Authentication/Header/Bearer.swift b/Sources/Authentication/Header/Bearer.swift index ec4bc65..5969535 100644 --- a/Sources/Authentication/Header/Bearer.swift +++ b/Sources/Authentication/Header/Bearer.swift @@ -1,14 +1,10 @@ -import Turnstile - -@_exported import class Turnstile.AccessToken - extension Authorization { - public var bearer: AccessToken? { + public var bearer: Credentials? { guard let range = header.range(of: "Bearer ") else { return nil } let token = header.substring(from: range.upperBound) - return AccessToken(string: token) + return Credentials(token: token) } } diff --git a/Sources/Authentication/Request+Subject.swift b/Sources/Authentication/Request+Subject.swift deleted file mode 100644 index e1011a1..0000000 --- a/Sources/Authentication/Request+Subject.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Turnstile - -extension Subject { - var sessionIdentifier: String? { - return authDetails?.sessionID - } -} diff --git a/Sources/Authentication/User/User.swift b/Sources/Authentication/User/User.swift index 4293a1b..757d3b3 100644 --- a/Sources/Authentication/User/User.swift +++ b/Sources/Authentication/User/User.swift @@ -1,10 +1,3 @@ import Fluent -import Turnstile -public protocol User: Entity, Account, Authenticator { } - -extension User { - public var uniqueID: String { - return id?.string ?? "" - } -} +public protocol User: Entity, Authenticatable {} diff --git a/Sources/Authorization/Authorizable.swift b/Sources/Authorization/Authorizable.swift new file mode 100644 index 0000000..3911269 --- /dev/null +++ b/Sources/Authorization/Authorizable.swift @@ -0,0 +1,118 @@ +import Fluent + +public protocol Authorizable: Entity { + +} + +public protocol Pivot { + associatedtype Left = Entity + associatedtype Right = Entity + + static func contains(left: Left, right: Right) throws -> Bool + static func attach(left: Left, right: Right) throws + static func detach(left: Left, right: Right) throws +} + +extension Pivot where Self: Entity { + // implement some of the contains, attach, detach methods +} + +extension Authorizable { + public func isAuthorized< + PermissionType: Permission, + PivotType: Pivot + >( + to permission: PermissionType, + withPivot: PivotType.Type = PivotType.self + ) throws -> Bool + where + PivotType.Left == Self, + PivotType.Right == PermissionType + { + guard let permission = try PermissionType.query().filter("key", permission.key).first() else { + throw AuthorizationError.unknownPermissionKey + } + + return try PivotType.contains(left: self, right: permission) + } + + + public func assertAuthorization< + PermissionType: Permission, + PivotType: Pivot + >( + to permission: PermissionType, + withPivot pivot: PivotType.Type = PivotType.self + ) throws + where + PivotType.Left == Self, + PivotType.Right == PermissionType + { + guard try isAuthorized(to: permission, withPivot: pivot) else { + throw AuthorizationError.notAuthorized + } + } +} + +// MARK: Triple + +public protocol TriplePivot { + associatedtype Left = Entity + associatedtype Middle = Entity + associatedtype Right = Entity + + static func contains(left: Left, middle: Middle, right: Right) throws -> Bool + static func attach(left: Left, middle: Middle, right: Right) throws + static func detach(left: Left, middle: Middle, right: Right) throws +} + +extension Authorizable { + + public func isAuthorized< + PermissionType: Permission, + MiddleEntityType: Entity, + PivotType: TriplePivot + >( + to permission: PermissionType, + _ item: MiddleEntityType, + withPivot: PivotType.Type = PivotType.self + ) throws -> Bool + where + PivotType.Left == Self, + PivotType.Middle == MiddleEntityType, + PivotType.Right == PermissionType + { + guard let permission = try PermissionType.query().filter("key", permission.key).first() else { + throw AuthorizationError.unknownPermissionKey + } + + return try PivotType.contains(left: self, middle: item, right: permission) + } + + public func assertAuthorization< + PermissionType: Permission, + MiddleEntityType: Entity, + PivotType: TriplePivot + >( + to permission: PermissionType, + _ item: MiddleEntityType, + withPivot pivot: PivotType.Type = PivotType.self + ) throws + where + PivotType.Left == Self, + PivotType.Middle == MiddleEntityType, + PivotType.Right == PermissionType + { + guard try isAuthorized(to: permission, item, withPivot: pivot) else { + throw AuthorizationError.notAuthorized + } + } +} + +public enum AuthorizationError: Error { + case authorizableIdentifierRequired + case unknownPermissionKey + case permissionIdentifierRequired + case notAuthorized + case unspecified(Error) +} diff --git a/Sources/Authorization/Permission.swift b/Sources/Authorization/Permission.swift new file mode 100644 index 0000000..3e416a0 --- /dev/null +++ b/Sources/Authorization/Permission.swift @@ -0,0 +1,5 @@ +import Fluent + +public protocol Permission: Entity { + var key: String { get } +} diff --git a/Sources/Authorization/Test.swift b/Sources/Authorization/Test.swift deleted file mode 100644 index 1f5de6b..0000000 --- a/Sources/Authorization/Test.swift +++ /dev/null @@ -1 +0,0 @@ -public class Test { } \ No newline at end of file diff --git a/Tests/AuthenticationTests/Utilities/AuthUser.swift b/Tests/AuthenticationTests/Utilities/AuthUser.swift index dc3fd8a..ab65f3f 100644 --- a/Tests/AuthenticationTests/Utilities/AuthUser.swift +++ b/Tests/AuthenticationTests/Utilities/AuthUser.swift @@ -30,13 +30,4 @@ final class AuthUser: User { static func revert(_ database: Database) throws { } - - static func authenticate(credentials: Credentials) throws -> Authentication.User { - return AuthUser(name: "test") - } - - - static func register(credentials: Credentials) throws -> Authentication.User { - fatalError("\(#function) not supported") - } } From 6ae316ba88d99bb287da2965e0e0310744c78360 Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Thu, 16 Feb 2017 10:24:31 +0100 Subject: [PATCH 3/4] fluent cleanup --- ...hError.swift => AuthenticationError.swift} | 1 - .../Authenticator/Authenticator.swift | 58 ---------------- .../Authenticator/User+Authenticator.swift | 24 ------- .../Authentication/CacheSessionManager.swift | 41 ------------ .../Credentials/Authenticatable.swift | 1 + .../Credentials/Credentials.swift | 35 +--------- .../Authentication/Credentials/Custom.swift | 6 ++ .../Credentials/Identifier.swift | 18 ++++- .../Authentication/Credentials/Token.swift | 66 ++++++++++++++----- .../Credentials/UsernamePassword.swift | 41 ++++++++---- Sources/Authentication/Header/Basic.swift | 4 +- Sources/Authentication/Header/Bearer.swift | 4 +- Sources/Authentication/User/User.swift | 2 +- Tests/AuthenticationTests/ExampleTests.swift | 13 ++++ .../Utilities/AuthUser.swift | 23 ++++++- 15 files changed, 139 insertions(+), 198 deletions(-) rename Sources/Authentication/{AuthError.swift => AuthenticationError.swift} (93%) delete mode 100644 Sources/Authentication/Authenticator/Authenticator.swift delete mode 100644 Sources/Authentication/Authenticator/User+Authenticator.swift delete mode 100644 Sources/Authentication/CacheSessionManager.swift create mode 100644 Sources/Authentication/Credentials/Authenticatable.swift create mode 100644 Sources/Authentication/Credentials/Custom.swift diff --git a/Sources/Authentication/AuthError.swift b/Sources/Authentication/AuthenticationError.swift similarity index 93% rename from Sources/Authentication/AuthError.swift rename to Sources/Authentication/AuthenticationError.swift index be343c0..571dfba 100644 --- a/Sources/Authentication/AuthError.swift +++ b/Sources/Authentication/AuthenticationError.swift @@ -1,5 +1,4 @@ public enum AuthenticationError: Swift.Error { - case noSubject case invalidAccountType case invalidBasicAuthorization case invalidBearerAuthorization diff --git a/Sources/Authentication/Authenticator/Authenticator.swift b/Sources/Authentication/Authenticator/Authenticator.swift deleted file mode 100644 index 4b364a0..0000000 --- a/Sources/Authentication/Authenticator/Authenticator.swift +++ /dev/null @@ -1,58 +0,0 @@ -public protocol Authenticatable { - // MARK: General - static func authenticate(_: Credentials) throws -> Self - - // MARK: Identifier - static func authenticate(_: Identifier) throws -> Self - - // MARK: Username / Password - /// Return the user matching the supplied - /// username and password - static func authenticate(_: UsernamePassword) throws -> Self - - /// The entity's raw or hashed password - var password: String? { get } - - /// The key under which the user's username, - /// email, or other identifing value is stored. - static var usernameKey: String { get } - - /// The key under which the user's password - /// is stored. - static var passwordKey: String { get } - - - // MARK: Token - /// Returns the user matching the supplied token. - static func authenticate(_: Token) throws -> Self - - /// The token entity that contains a foreign key - /// pointer to the user table - static var tokenType: TokenCredentialEntity.Type? { get } - - // MARK: Custom - /// Returns the user matching the custom credential. - static func authenticate(custom: Any) throws -> Self -} - -extension User { - /// See User.password in Protocol - public var password: String? { - return nil - } - - /// See User.usernameKey in Protocol - public static var usernameKey: String { - return "username" - } - - /// See User.passwordKey in Protocol - public static var passwordKey: String { - return "password" - } - - /// See User.tokenType in Protocol - public static var tokenType: TokenCredentialEntity.Type? { - return nil - } -} diff --git a/Sources/Authentication/Authenticator/User+Authenticator.swift b/Sources/Authentication/Authenticator/User+Authenticator.swift deleted file mode 100644 index 66313f5..0000000 --- a/Sources/Authentication/Authenticator/User+Authenticator.swift +++ /dev/null @@ -1,24 +0,0 @@ -extension User { - public static func authenticate(_ credentials: Credentials) throws -> Self { - let user: Self - - switch credentials { - case .identifier(let id): - user = try authenticate(id) - case .password(let usernamePassword): - user = try authenticate(usernamePassword) - case .token(let token): - user = try authenticate(token) - case .custom: - throw AuthenticationError.unsupportedCredentials - } - - return user - } -} - -extension User { - public static func authenticate(custom: Any) throws -> Self { - throw AuthenticationError.unsupportedCredentials - } -} diff --git a/Sources/Authentication/CacheSessionManager.swift b/Sources/Authentication/CacheSessionManager.swift deleted file mode 100644 index ef8dc84..0000000 --- a/Sources/Authentication/CacheSessionManager.swift +++ /dev/null @@ -1,41 +0,0 @@ -// PROVIDER STUFF -//import Turnstile -//import Random -//import Cache -// -//public final class CacheSessionManager: SessionManager { -// private let cache: CacheProtocol -// private let realm: Realm -// -// public init(cache: CacheProtocol, realm: Realm) { -// self.cache = cache -// self.realm = realm -// } -// -// /** -// Gets the user for the current session identifier. -// */ -// public func restoreAccount(fromSessionID identifier: String) throws -> Account { -// guard let id = try cache.get(identifier) else { -// throw AuthError.invalidIdentifier -// } -// -// return try realm.authenticate(credentials: Identifier(id: id)) -// } -// -// /** -// Creates a session for a given Subject object and returns the identifier. -// */ -// public func createSession(account: Account) -> String { -// let identifier = CryptoRandom.bytes(16).base64Encoded.string -// try? cache.set(identifier, account.uniqueID) -// return identifier -// } -// -// /** -// Destroys the session for a session identifier. -// */ -// public func destroySession(identifier: String) { -// try? cache.delete(identifier) -// } -//} diff --git a/Sources/Authentication/Credentials/Authenticatable.swift b/Sources/Authentication/Credentials/Authenticatable.swift new file mode 100644 index 0000000..8a37c94 --- /dev/null +++ b/Sources/Authentication/Credentials/Authenticatable.swift @@ -0,0 +1 @@ +public protocol Authenticatable { } diff --git a/Sources/Authentication/Credentials/Credentials.swift b/Sources/Authentication/Credentials/Credentials.swift index 727d98f..007f32f 100644 --- a/Sources/Authentication/Credentials/Credentials.swift +++ b/Sources/Authentication/Credentials/Credentials.swift @@ -1,34 +1 @@ -import Node - -public enum Credentials { - case identifier(Identifier) - case password(UsernamePassword) - case token(Token) - case custom(Any) - - public init(token: String) { - let token = Token(token: token) - self = .token(token) - } - - public init(identifier: Node) { - let identifier = Identifier(id: identifier) - self = .identifier(identifier) - } - - - public init(username: String, password: String, verifier: PasswordVerifier? = nil) { - let creds = UsernamePassword( - username: username, - password: password, - verifier: verifier - ) - - self = .password(creds) - } - - - public init(custom: Any) { - self = .custom(custom) - } -} +public protocol Crendentials { } diff --git a/Sources/Authentication/Credentials/Custom.swift b/Sources/Authentication/Credentials/Custom.swift new file mode 100644 index 0000000..87bcd47 --- /dev/null +++ b/Sources/Authentication/Credentials/Custom.swift @@ -0,0 +1,6 @@ +// MARK: Authenticatable + +public protocol CustomAuthenticatable: Authenticatable { + /// Returns the user matching the custom credential. + static func authenticate(custom: Crendentials) throws -> Self +} diff --git a/Sources/Authentication/Credentials/Identifier.swift b/Sources/Authentication/Credentials/Identifier.swift index 1d81561..5629a6f 100644 --- a/Sources/Authentication/Credentials/Identifier.swift +++ b/Sources/Authentication/Credentials/Identifier.swift @@ -1,6 +1,8 @@ +// MARK: Data structure + import Node -public struct Identifier { +public struct Identifier: Crendentials { let id: Node public init(id: Node) { @@ -8,7 +10,19 @@ public struct Identifier { } } -extension User { +// MARK: Authenticatable + +public protocol IdentifierAuthenticatable { + /// Return the user with the supplied id. + static func authenticate(_: Identifier) throws -> Self +} + + +// MARK: Entity conformance + +import Fluent + +extension IdentifierAuthenticatable where Self: Entity { public static func authenticate(_ id: Identifier) throws -> Self { guard let match = try Self.find(id.id) else { throw AuthenticationError.invalidCredentials diff --git a/Sources/Authentication/Credentials/Token.swift b/Sources/Authentication/Credentials/Token.swift index eb7a938..d503018 100644 --- a/Sources/Authentication/Credentials/Token.swift +++ b/Sources/Authentication/Credentials/Token.swift @@ -1,25 +1,55 @@ -public struct Token { - public let token: String +// MARK: Data structure - public init(token: String) { - self.token = token +public struct Token: Crendentials { + public let string: String + + public init(string: String) { + self.string = string + } +} + +// MARK: Authenticatable + +public protocol TokenAuthenticatable: Authenticatable { + /// The token entity that contains a foreign key + /// pointer to the user table + associatedtype TokenType: TokenProtocol + + /// Returns the user matching the supplied token. + static func authenticate(_: Token) throws -> Self +} + +public protocol TokenProtocol { + static var tokenKey: String { get } + static func findUser(for: Token) throws -> U +} + +extension TokenProtocol { + public static var tokenKey: String { + return "token" } } -extension User { +// MARK: Entity conformance + +import Fluent + +extension TokenAuthenticatable where Self: Entity { public static func authenticate(_ token: Token) throws -> Self { - // guard let tokenType = tokenType else { - // throw AuthError.unsupportedCredentials - // } - - // guard let user = try Self.query() - // .union(tokenType) - // .filter(tokenType, tokenType.tokenKey, token) - // .first() - // else { - // throw AuthError.unsupportedCredentials - // } - - throw AuthenticationError.unsupportedCredentials + return try TokenType.findUser(for: token) + } +} + +extension TokenProtocol where Self: Entity { + public static func findUser(for token: Token) throws -> U { + guard let user = try U.query() + .join(self) + .filter(self, tokenKey, token.string) + .first() + else { + throw AuthenticationError.invalidCredentials + } + + return user } } diff --git a/Sources/Authentication/Credentials/UsernamePassword.swift b/Sources/Authentication/Credentials/UsernamePassword.swift index 4c42522..71fac96 100644 --- a/Sources/Authentication/Credentials/UsernamePassword.swift +++ b/Sources/Authentication/Credentials/UsernamePassword.swift @@ -1,4 +1,6 @@ -public struct UsernamePassword { +// MARK: Data structure + +public struct Password: Crendentials { public let username: String public let password: String public let verifier: PasswordVerifier? @@ -10,13 +12,36 @@ public struct UsernamePassword { } } +// MARK: Authenticatable + +public protocol PasswordAuthenticatable: Authenticatable { + // MARK: Username / Password + /// Return the user matching the supplied + /// username and password + static func authenticate(_: Password) throws -> Self + + /// The entity's raw or hashed password + var password: String? { get } + + /// The key under which the user's username, + /// email, or other identifing value is stored. + static var usernameKey: String { get } + + /// The key under which the user's password + /// is stored. + static var passwordKey: String { get } +} public protocol PasswordVerifier { func verify(password: String, matchesPassword: String) throws -> Bool } -extension User { - public static func authenticate(_ creds: UsernamePassword) throws -> Self { +// MARK: Entity conformance + +import Fluent + +extension PasswordAuthenticatable where Self: Entity { + public static func authenticate(_ creds: Password) throws -> Self { let user: Self if let verifier = creds.verifier { @@ -56,13 +81,3 @@ extension User { return user } } - - - - -import Fluent - -public protocol TokenCredentialEntity: Entity { - func authenticationUser() throws -> User - static var tokenKey: String { get } -} diff --git a/Sources/Authentication/Header/Basic.swift b/Sources/Authentication/Header/Basic.swift index 62be6f4..cc386d9 100644 --- a/Sources/Authentication/Header/Basic.swift +++ b/Sources/Authentication/Header/Basic.swift @@ -1,7 +1,7 @@ import Core extension Authorization { - public var basic: Credentials? { + public var basic: Password? { guard let range = header.range(of: "Basic ") else { return nil } @@ -17,6 +17,6 @@ extension Authorization { let apiKeyID = decodedToken.substring(to: separatorRange.lowerBound) let apiKeySecret = decodedToken.substring(from: separatorRange.upperBound) - return Credentials(username: apiKeyID, password: apiKeySecret) + return Password(username: apiKeyID, password: apiKeySecret) } } diff --git a/Sources/Authentication/Header/Bearer.swift b/Sources/Authentication/Header/Bearer.swift index 5969535..6fff8de 100644 --- a/Sources/Authentication/Header/Bearer.swift +++ b/Sources/Authentication/Header/Bearer.swift @@ -1,10 +1,10 @@ extension Authorization { - public var bearer: Credentials? { + public var bearer: Token? { guard let range = header.range(of: "Bearer ") else { return nil } let token = header.substring(from: range.upperBound) - return Credentials(token: token) + return Token(string: token) } } diff --git a/Sources/Authentication/User/User.swift b/Sources/Authentication/User/User.swift index 757d3b3..e63c601 100644 --- a/Sources/Authentication/User/User.swift +++ b/Sources/Authentication/User/User.swift @@ -1,3 +1,3 @@ import Fluent -public protocol User: Entity, Authenticatable {} +// public protocol User: Entity {} diff --git a/Tests/AuthenticationTests/ExampleTests.swift b/Tests/AuthenticationTests/ExampleTests.swift index cd11876..dc2f34b 100644 --- a/Tests/AuthenticationTests/ExampleTests.swift +++ b/Tests/AuthenticationTests/ExampleTests.swift @@ -1,4 +1,5 @@ import XCTest +import Node @testable import Authentication @@ -8,6 +9,18 @@ class ExampleTests: XCTestCase { ] func testExample() throws { + do { + let id = Identifier(id: Node.string("5")) + let token = Token(string: "5") + let user1 = try AuthUser.authenticate(id) + XCTAssertEqual(user1.name, "5") + + let user2 = try AuthUser.authenticate(token) + XCTAssertEqual(user2.name, "6") + } catch { + XCTFail("\(error)") + + } } } diff --git a/Tests/AuthenticationTests/Utilities/AuthUser.swift b/Tests/AuthenticationTests/Utilities/AuthUser.swift index ab65f3f..ad190a9 100644 --- a/Tests/AuthenticationTests/Utilities/AuthUser.swift +++ b/Tests/AuthenticationTests/Utilities/AuthUser.swift @@ -2,7 +2,7 @@ import Authentication import Node import Fluent -final class AuthUser: User { +final class AuthUser: Entity { var id: Node? var name: String var exists: Bool = false @@ -24,10 +24,29 @@ final class AuthUser: User { } static func prepare(_ database: Database) throws { - } static func revert(_ database: Database) throws { } } + +extension AuthUser: IdentifierAuthenticatable { + static func authenticate(_ id: Identifier) throws -> Self { + return self.init(name: "5") + } +} + +extension AuthUser: TokenAuthenticatable { + typealias TokenType = AuthUser + + static func authenticate(_ id: Token) throws -> Self { + return self.init(name: "6") + } +} + +extension AuthUser: TokenProtocol { + static var tokenKey: String { + return "" + } +} From bdad50d1cd842684f916147ab90b109b707237ba Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Thu, 16 Feb 2017 10:56:27 +0100 Subject: [PATCH 4/4] comments --- .../Authentication/AuthenticationError.swift | 29 +++++++- .../Credentials/UsernamePassword.swift | 28 ++++++-- Sources/Authorization/Authorizable.swift | 72 ++++++------------- .../Authorization/AuthorizationError.swift | 22 ++++++ 4 files changed, 91 insertions(+), 60 deletions(-) create mode 100644 Sources/Authorization/AuthorizationError.swift diff --git a/Sources/Authentication/AuthenticationError.swift b/Sources/Authentication/AuthenticationError.swift index 571dfba..bbec97f 100644 --- a/Sources/Authentication/AuthenticationError.swift +++ b/Sources/Authentication/AuthenticationError.swift @@ -1,10 +1,35 @@ public enum AuthenticationError: Swift.Error { - case invalidAccountType case invalidBasicAuthorization case invalidBearerAuthorization case noAuthorizationHeader case notAuthenticated - case invalidIdentifier case invalidCredentials case unsupportedCredentials + case unspecified(Error) } + +extension AuthenticationError: CustomStringConvertible { + public var description: String { + let reason: String + + switch self { + case .invalidBasicAuthorization: + reason = "Invalid Authorization Basic header" + case .invalidBearerAuthorization: + reason = "Invalid Authorization Bearer header" + case .noAuthorizationHeader: + reason = "No authorization header" + case .notAuthenticated: + reason = "Not authenticated" + case .invalidCredentials: + reason = "Invalid credentials" + case .unsupportedCredentials: + reason = "Unsupported credentials" + case .unspecified(let error): + reason = "\(error)" + } + + return "Authentication error: \(reason)" + } +} + diff --git a/Sources/Authentication/Credentials/UsernamePassword.swift b/Sources/Authentication/Credentials/UsernamePassword.swift index 71fac96..a815a9b 100644 --- a/Sources/Authentication/Credentials/UsernamePassword.swift +++ b/Sources/Authentication/Credentials/UsernamePassword.swift @@ -20,8 +20,10 @@ public protocol PasswordAuthenticatable: Authenticatable { /// username and password static func authenticate(_: Password) throws -> Self - /// The entity's raw or hashed password - var password: String? { get } + /// The entity's hashed password used for + /// validating against Password credentials + /// with a PasswordVerifier + var hashedPassword: String? { get } /// The key under which the user's username, /// email, or other identifing value is stored. @@ -32,8 +34,22 @@ public protocol PasswordAuthenticatable: Authenticatable { static var passwordKey: String { get } } +extension PasswordAuthenticatable { + public static var usernameKey: String { + return "email" + } + + public static var passwordKey: String { + return "password" + } + + public var hashedPassword: String? { + return nil + } +} + public protocol PasswordVerifier { - func verify(password: String, matchesPassword: String) throws -> Bool + func verify(password: String, matchesHash: String) throws -> Bool } // MARK: Entity conformance @@ -53,14 +69,14 @@ extension PasswordAuthenticatable where Self: Entity { throw AuthenticationError.invalidCredentials } - guard let matchPassword = match.password else { + guard let hash = match.hashedPassword else { throw AuthenticationError.invalidCredentials } guard try verifier.verify( password: creds.password, - matchesPassword: matchPassword - ) else { + matchesHash: hash + ) else { throw AuthenticationError.invalidCredentials } diff --git a/Sources/Authorization/Authorizable.swift b/Sources/Authorization/Authorizable.swift index 3911269..8dcec98 100644 --- a/Sources/Authorization/Authorizable.swift +++ b/Sources/Authorization/Authorizable.swift @@ -4,23 +4,10 @@ public protocol Authorizable: Entity { } -public protocol Pivot { - associatedtype Left = Entity - associatedtype Right = Entity - - static func contains(left: Left, right: Right) throws -> Bool - static func attach(left: Left, right: Right) throws - static func detach(left: Left, right: Right) throws -} - -extension Pivot where Self: Entity { - // implement some of the contains, attach, detach methods -} - extension Authorizable { public func isAuthorized< PermissionType: Permission, - PivotType: Pivot + PivotType: PivotProtocol & Entity >( to permission: PermissionType, withPivot: PivotType.Type = PivotType.self @@ -30,16 +17,16 @@ extension Authorizable { PivotType.Right == PermissionType { guard let permission = try PermissionType.query().filter("key", permission.key).first() else { - throw AuthorizationError.unknownPermissionKey + throw AuthorizationError.unknownPermission } - return try PivotType.contains(left: self, right: permission) + return try PivotType.related(self, permission) } public func assertAuthorization< PermissionType: Permission, - PivotType: Pivot + PivotType: PivotProtocol & Entity >( to permission: PermissionType, withPivot pivot: PivotType.Type = PivotType.self @@ -48,71 +35,52 @@ extension Authorizable { PivotType.Left == Self, PivotType.Right == PermissionType { - guard try isAuthorized(to: permission, withPivot: pivot) else { + guard try isAuthorized(to: permission, withPivot: PivotType.self) else { throw AuthorizationError.notAuthorized } } } -// MARK: Triple - -public protocol TriplePivot { - associatedtype Left = Entity - associatedtype Middle = Entity - associatedtype Right = Entity - - static func contains(left: Left, middle: Middle, right: Right) throws -> Bool - static func attach(left: Left, middle: Middle, right: Right) throws - static func detach(left: Left, middle: Middle, right: Right) throws -} - extension Authorizable { - public func isAuthorized< PermissionType: Permission, - MiddleEntityType: Entity, - PivotType: TriplePivot + MiddleType: Entity, + PivotType: PivotProtocol & Entity >( to permission: PermissionType, - _ item: MiddleEntityType, + _ item: MiddleType, withPivot: PivotType.Type = PivotType.self ) throws -> Bool where - PivotType.Left == Self, - PivotType.Middle == MiddleEntityType, + PivotType.Left: PivotProtocol, + PivotType.Left.Left == Self, + PivotType.Left.Right == MiddleType, PivotType.Right == PermissionType { guard let permission = try PermissionType.query().filter("key", permission.key).first() else { - throw AuthorizationError.unknownPermissionKey + throw AuthorizationError.unknownPermission } - return try PivotType.contains(left: self, middle: item, right: permission) + return try PivotType.related(left: self, middle: item, right: permission) } public func assertAuthorization< PermissionType: Permission, - MiddleEntityType: Entity, - PivotType: TriplePivot + MiddleType: Entity, + PivotType: PivotProtocol & Entity >( to permission: PermissionType, - _ item: MiddleEntityType, + _ item: MiddleType, withPivot pivot: PivotType.Type = PivotType.self ) throws where - PivotType.Left == Self, - PivotType.Middle == MiddleEntityType, + PivotType.Left: PivotProtocol, + PivotType.Left.Left == Self, + PivotType.Left.Right == MiddleType, PivotType.Right == PermissionType { - guard try isAuthorized(to: permission, item, withPivot: pivot) else { + guard try isAuthorized(to: permission, item, withPivot: PivotType.self) else { throw AuthorizationError.notAuthorized } } } - -public enum AuthorizationError: Error { - case authorizableIdentifierRequired - case unknownPermissionKey - case permissionIdentifierRequired - case notAuthorized - case unspecified(Error) -} diff --git a/Sources/Authorization/AuthorizationError.swift b/Sources/Authorization/AuthorizationError.swift new file mode 100644 index 0000000..a2dccff --- /dev/null +++ b/Sources/Authorization/AuthorizationError.swift @@ -0,0 +1,22 @@ +public enum AuthorizationError: Error { + case unknownPermission + case notAuthorized + case unspecified(Error) +} + +extension AuthorizationError: CustomStringConvertible { + public var description: String { + let reason: String + + switch self { + case .unknownPermission: + reason = "Permission not recognized." + case .notAuthorized: + reason = "Not authorized" + case .unspecified(let error): + reason = "\(error)" + } + + return "Authorization error: \(reason)" + } +}