Role Based Authorisation (RBA) plugin for Genie.jl
The GenieAuthorisation.jl
package is a role based authorisation plugin for Genie.jl
, the highly productive Julia web framework.
As such, it requires installation within the environment of a Genie.jl
MVC application, allowing the plugin to install
its files.
GenieAuthorisation.jl
works in tandem with GenieAuthentication.jl
, the authentication plugin for Genie.jl
apps.
In fact, GenieAuthorisation.jl
adds an authorisation layer on top of the authentication features provided by GenieAuthentication.jl
.
As such, first, please make sure that GenieAuthentication.jl
is configured for your Genie.jl
application, following the
instructions at: https://github.com/GenieFramework/GenieAuthentication.jl
With the GenieAuthentication.jl
plugin installed, make sure you configure authenticated access to the areas you want to
further protect with role based authorisation. So the first step is to add user authentication to the app. Then refine access via
authorisation.
Now that your application supports user authentication, it's time to add user authorisation.
First, add the plugin (make sure you are in the environment of the Genie.jl
app you want to protect with autorisation):
julia> ]
(MyGenieApp) pkg> add GenieAuthorisation
Once added, we can use its install
function to add its files to the Genie.jl
app (required only upon installation):
julia> using GenieAuthorisation
julia> GenieAuthorisation.install(@__DIR__)
The above command will set up the plugin's files within your Genie.jl
app (will potentially add new views, controllers, models, migrations, initializers, etc).
The bulk of the authorisation features are provided by the package itself together with a series of database migrations which set up the database tables needed to configure the RBA system.
The plugin needs DB support to store its configuration (roles, permissions and various relations).
You will find 4 new migration files within the db/migrations/
folder. We need to run then, either by running all the migrations:
julia> using SearchLight
julia> SearchLight.Migration.all_up!!()
Or by individually running the 4 migrations:
julia> using SearchLight
julia> SearchLight.Migration.up("CreateTableRoles")
julia> SearchLight.Migration.up("CreateTablePermissions")
julia> SearchLight.Migration.up("CreateTableRolesUsers")
julia> SearchLight.Migration.up("CreateTablePermissionsRoles")
This will create all the necessary table.
A role based authorisation system implements access control through permissions. That is, certain features are accessible
only for users that have the necessary permission. For instance, this is how we require authorisation for a user_admin
permission
at route level:
route("/admin/users") do; @authorised!("users_admin")
# code can be accessed only by users with the `users_admin` permission
end
Permissions, however, are not assigned directly to users, but to roles. As such, a role can have multiple permissions - like
for example an admin
role would have all the possible permissions. Finally, the users are assigned roles - getting access
to the role's respective permissions. A role can have any number of permissions and a user can have any number of roles.
GenieAuthorisation.jl
exposes an API which makes checking users permissions straightforward, without needing to handle
the actual roles. However, the roles make permission assignment manageable: we bundle permissions
into roles and then assign the roles to the users. This way, when we need to remove permissions from a user, we just
remove the role and eliminate the risk of failing to remove all the forbidden permissions.
Given that permissions and roles are stored in the database, we use SearchLight.jl
to manage the data:
using GenieAuthorisation
# Create two roles, "user" and "admin"
for r in ["user", "admin"]
findone_or_create(Role, name = r) |> save!
end
# Create some permissions
for p in ["create", "read", "update", "delete"]
findone_or_create(Permission, name = p) |> save!
end
Now that the roles and the permissions are created, we need to assign permissions to roles:
using GenieAuthorisation
assign_permission(findone(Role, name = "admin"), findone(Permission, name = "create"))
assign_permission(findone(Role, name = "admin"), findone(Permission, name = "read"))
assign_permission(findone(Role, name = "admin"), findone(Permission, name = "update"))
assign_permission(findone(Role, name = "admin"), findone(Permission, name = "delete"))
assign_permission(findone(Role, name = "user"), findone(Permission, name = "read"))
We have assigned "create", "read", "update" and "delete" permissions to the admin
role, and "read" permissions to the
user
role.
Now we need to assign roles to our users -- for example making the user with the username essenciary
an "admin":
using GenieAuthorisation, GenieAuthentication, Users
assign_role(findone(User, username = "essenciary"), findone(Role, name = "admin"))
HEADS UP
Users must be explicitly assigned roles in order to have any permissions. Permissions are made available only through roles and this means that users without a role do not have any kind of permissions.
It makes sense to automate role assignment, for example by assigning a default basic role upon user registration.
Once we have permissions, roles, and users, and we have defined the relationships between them, we can enforce user authorisation within the app.
The @authorised!
macro checks that the current user has the <permission>
permission - if not, an exception is automatically
thrown, stopping the current thread of execution:
using GenieAuthorisation
route("/admin/users/delete/:user_id") do; @authorised!("delete")
# code can be accessed only by users with the `users_admin` permission
end
We can also use the has_permission
function to check if a user has the necessary permissions. The function returns a
boolean
allowing us to implement conditional logic based on the status of the authorisation:
<% if has_permission(current_user(), "update") || has_permission(current_user(), "delete") %>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" data-toggle="dropdown">User management</a>
<div class="dropdown-menu">
<% if has_permission(current_user(), "update") %>
<a class="dropdown-item" href="/admin/users/edit/:user_id">Edit user</a>
<% end %>
<% if has_permission(current_user(), "delete") %>
<a class="dropdown-item" href="/admin/users/delete/:user_id">Delete user</a>
<% end %>
</div>
</li>
<% end %>
Deleting authorisation is done by removing the relationships stored in the database, using the SearchLight.jl
API.
For example, to remove a permission from a role:
using SearchLight, GenieAuthorisation
Relationship!(findone(Role, name = "admin"), findone(Permission, name = "delete")) |> delete
Or a role from a user:
using SearchLight, GenieAuthorisation, GenieAuthentication, Users
Relationship!(findone(User, username = "essenciary"), findone(Role, name = "admin")) |> delete
Finally, to remove roles or permissions, we delete the respective entities:
using SearchLight, GenieAuthorisation
# remove the `delete` permission
delete(findone(Permission, name = "delete"))
# remove the `admin` role
delete(findone(Role, name = "admin"))