Vapor 3 package to define permissions and authorize authenticated users to do actions on resources.
Add the package to your dependencies in Package.swift
.package(url: "https://github.com/zdnk/Authorized.git", from: "1.0.0-alpha.1")and in Terminal run swift package resolve.
If you are using Xcode for development: swift package generate-xcodeproj.
Check example application in Example/ of this repository, or see Usage section below.
.deny takes precedence before .allow. If any rule results in .deny, the authorization fails even if all other rules respond with .allow.
Everything begins with:
import Authorizedstruct Post: Resource { // Probably also conforms to Fluent.Model
enum Action: String, ResourceAction {
case create
case delete
}
var id: Int?
let authorId: User.ID
}struct User: Authorizable { // Probably also conforms to Fluent.Model and Authenticatable
var id: Int?
let username: String
}struct PostPolicy: ResourcePolicy {
typealias Model = Post
// This function is required to return the policy configuration.
// Think of it as a mapping of actions to functions
func rules() -> ResourceRules<Post> {
var rules = ResourceRules<Post>()
rules.add(self.create, for: .create)
rules.add(self.delete, for: .delete)
return rules
}
// Define functions that will resolve the permission
func create(as user: User, on container: Container) -> Future<PermissionResolution> {
// Allow everyone to create Posts
return container.future(.allow)
}
func delete(post: Post, as user: User, on container: Container) throws -> Future<PermissionResolution> {
// Allow only authors of the post to delete them
let result = try post.authorId == user.requireID()
return container.future(result ? .allow : .deny)
}
}You need to register the service in your configure.swift.
// Register provider
try services.register(AuthorizationProvider())
// Initialize configuration - needs to be mutable (var)
var auth = AuthorizationConfig()
// Add policies to the configuration
auth.add(policy: PostPolicy())
// Now register the configuration to the services
services.register(auth)One possible way can be like the example below, for more options, please check the API section.
/// DELETE /posts/{id}
func delete(_ req: Request) -> Future<HTTPStatus> {
return req.parameters.next(Post.self)
// Check if there is someone authenticated of type User,
// and verify if this specific User has permission
// to remove this specific Post
.authorize(.delete, as: User.self, on: req) // returns Future<Post>
.flatMap { post in
return post.delete(on: req)
}
.transform(to: HTTPStatus.noContent)
}or
/// DELETE /posts/{id}
func delete(_ req: Request) -> Future<HTTPStatus> {
let user = try self.requireAuthenticated(User.self)
return req.parameters.next(Post.self)
.authorize(.delete, as: user, on: req) // returns Future<Post>
.flatMap { post in
return post.delete(on: req)
}
.transform(to: HTTPStatus.noContent)
}Both examples are using Vapors Authentication library so the User needs to conform to Authenticatable.
Also it relies on Post being a Model.
There are several extensions available on Vapors and Swift NIOs types to help you easily authorize users and actions on resources.
extension Request {
public func authorize<A, R>(_: A.Type, _ resource: R, _ action: R.Action) throws -> Future<R> where A : Authenticatable, A : Authorizable, R : Resource
public func authorize<A, R>(_ user: A, _ resource: R, _ action: R.Action) throws -> Future<R> where A : Authorizable, R : Resource
public func authorize<A, R>(_: A.Type, _ resource: R.Type, _ action: R.Action) throws -> Future<Void> where A : Authenticatable, A : Authorizable, R : Resource
public func authorize<A, R>(_ user: A, _ resource: R.Type, _ action: R.Action) throws -> Future<Void> where A : Authorizable, R : Resource
}extension EventLoopFuture where T : Resource {
public func authorize<A>(_ action: T.Action, as user: A, on container: Container) -> Future<T> where A : Authorizable
public func authorize<A>(_ action: T.Action, as user: A.Type, on request: Request) -> Future<T> where A : Authenticatable, A : Authorizable
}extension Authorizable {
public func can<R>(_: R.Type, _ action: R.Action, on container: Container) throws -> Future<Bool> where R : Resource
public func authorize<R>(_: R.Type, _ action: R.Action, on container: Container) throws -> Future<Void> where R : Resource
public func can<R>(_ resource: R, _ action: R.Action, on container: Container) throws -> Future<Bool> where R : Resource
public func authorize<R>(_ resource: R, _ action: R.Action, on container: Container) throws -> Future<R> where R : Resource
}If you need to allow everything for some specific user, deny everything or any other global behavior, you can define it like this:
struct AdminRolePolicy: Policy {
func configure(in config: PermissionDefining) throws {
config.before { (context) -> EventLoopFuture<PermissionResolution?> in
guard let user = context.user as? User else {
// passing `nil` means continue executing with default behavior
return context.container.future(nil)
}
if user.role == .admin {
// Admins can do anything!
// passing `.allow` or `.deny` will cause the authorization to fail early
// and skips executing the regular rules
return context.container.future(.allow)
}
// passing `nil` means continue executing with default behavior
return context.container.future(nil)
}
}
}In configure.swift:
auth.add(policy: AdminRolePolicy())Did you find a bug or would like to see new feature implemented? Great! Please open new issue or create pull request :)