Last active
July 6, 2022 14:01
-
-
Save serradura/050cd51b3902c2ef02de95e9e7e6ac9b to your computer and use it in GitHub Desktop.
Rails + u-authorization
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'bundler/inline' | |
gemfile(true) do | |
source 'https://rubygems.org' | |
gem 'pry', '~> 0.13.1' | |
gem 'rails', '~> 6.0', '>= 6.0.3.4' | |
gem 'sqlite3', '~> 1.4', '>= 1.4.2' | |
gem 'u-authorization', '~> 2.3' | |
end | |
# == config/boot == | |
require 'active_record' | |
require 'active_support/all' | |
require 'action_controller/railtie' | |
# == config/application == | |
class SigleFileApp < Rails::Application | |
config.eager_load = 'development' | |
config.consider_all_requests_local = true | |
secrets.secret_token = SecureRandom.base58(64) | |
secrets.secret_key_base = SecureRandom.base58(64) | |
config.logger = Logger.new($stdout) | |
Rails.logger = config.logger | |
end | |
# == config/routes == | |
SigleFileApp.routes.append do | |
resources :users, only: [:create, :index] | |
resources :tasks | |
end | |
# == db/schema == | |
ActiveRecord::Base.establish_connection( | |
host: 'localhost', | |
adapter: 'sqlite3', | |
database: 'test1.db' | |
) | |
ActiveRecord::Schema.define do | |
create_table :roles, force: true do |t| | |
t.column :name, :string, null: false, index: { unique: true } | |
t.column :permissions, :text, null: false | |
t.timestamps | |
end | |
create_table :users, force: true do |t| | |
t.column :email, :string, null: false, index: { unique: true } | |
t.references :role, null: false, index: true | |
t.timestamps | |
end | |
create_table :tasks, force: true do |t| | |
t.column :description, :string | |
t.column :completed_at, :datetime | |
t.references :user, null: false, index: true | |
t.timestamps | |
end | |
end | |
# == app/models == | |
class ApplicationRecord < ActiveRecord::Base | |
self.abstract_class = true | |
def self.serialize_as_json(arg) | |
as_json = const_get(:SerializeAsJson, false) | |
return as_json.call(arg) if arg.is_a?(self) | |
return arg.map(&as_json) if arg.is_a?(ActiveRecord::Relation) && arg.model == self | |
end | |
end | |
# app/models/roles | |
module Roles | |
module Name | |
ADMIN = 'admin' | |
USER = 'user' | |
end | |
end | |
module Roles | |
module Action | |
ACCESS = 'access' | |
CREATE = 'create' | |
end | |
end | |
module Roles | |
class DefaultPolicy < Micro::Authorization::Policy | |
def allow_access? | |
permissions.to?(Action::ACCESS) | |
end | |
end | |
end | |
module Roles | |
module Permissions | |
DEFAULTS = { | |
Name::ADMIN => { | |
Action::ACCESS => { 'any' => true }, | |
Action::CREATE => { 'any' => true } | |
}, | |
Name::USER => { | |
Action::ACCESS => { 'except' => ['users.index'] }, | |
Action::CREATE => { 'only' => ['tasks'] } | |
} | |
} | |
def self.each_default; DEFAULTS.each { |name, permissions| yield(name, permissions) }; end | |
end | |
end | |
# app/models/role.rb | |
class Role < ApplicationRecord | |
has_many :users | |
serialize :permissions, Hash | |
validates :name, presence: true, uniqueness: true | |
validates :permissions, presence: true | |
def self.to_admin(truthy) | |
find_by!(name: truthy ? Roles::Name::ADMIN : Roles::Name::USER) | |
end | |
end | |
# app/models/user.rb | |
class User < ApplicationRecord | |
belongs_to :role | |
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, uniqueness: true | |
delegate :name, to: :role, prefix: true | |
SerializeAsJson = -> user { user.as_json(methods: :role_name, except: :role_id) } | |
end | |
# app/models/tasks | |
class Tasks | |
class Policy < Micro::Authorization::Policy | |
def apply_scope(relation) | |
relation.where(user_id: user.id) | |
end | |
end | |
end | |
# app/models/task.rb | |
class Task < ApplicationRecord | |
belongs_to :user | |
validates :description, presence: true | |
SerializeAsJson = -> task { task.as_json(except: :user_id) } | |
end | |
# == app/controllers == | |
class ApplicationController < ActionController::API | |
private | |
def current_user | |
return @current_user if defined?(@current_user) | |
@current_user = User.find_by(email: params[:user_email]) | |
end | |
def authenticate_user! | |
return true if current_user | |
render(status: 401, json: {}) and return false | |
end | |
def authenticate_and_authorize_user! | |
return unless authenticate_user! | |
return if current_authorization.policy.allow_access? | |
render(status: 403, json: {}) | |
end | |
def current_authorization | |
@current_authorization ||= Micro::Authorization::Model.build( | |
permissions: current_user.role.permissions, | |
policies: { default: Roles::DefaultPolicy }, | |
context: { | |
user: current_user, | |
to_permit: [controller_name, action_name] | |
} | |
) | |
end | |
end | |
# app/controllers/users_controller.rb | |
class UsersController < ApplicationController | |
before_action :authenticate_and_authorize_user!, only: :index | |
def index | |
render status: 200, json: ::User.serialize_as_json(User.all) | |
end | |
def create | |
user_params = params.require(:user).permit(:email, :admin) | |
role = fetch_role(user_params) | |
user = ::User.create(email: user_params[:email], role: role) | |
if user.persisted? | |
render status: 200, json: ::User.serialize_as_json(user) | |
else | |
render status: 422, json: { error: user.errors.as_json } | |
end | |
rescue ActionController::ParameterMissing => e | |
render status: 400, json: { error: e.message } | |
end | |
private | |
def create_as_admin?(user_params) | |
String(user_params[:admin]) =~ /true/i | |
end | |
def fetch_role(user_params) | |
Role.to_admin(create_as_admin?(user_params)) | |
end | |
end | |
# app/controllers/tasks_controller.rb | |
class TasksController < ApplicationController | |
before_action :authenticate_and_authorize_user! | |
before_action :add_user_policy | |
def index | |
render status: 200, json: Task.serialize_as_json(relation) | |
end | |
def create | |
task_params = params.require(:task).permit(:description) | |
task = relation.create(task_params) | |
if task.persisted? | |
render status: 200, json: Task.serialize_as_json(task) | |
else | |
render status: 422, json: { error: task.errors.as_json } | |
end | |
rescue ActionController::ParameterMissing => e | |
render status: 400, json: { error: e.message } | |
end | |
private | |
def relation | |
current_policy.apply_scope(Task.all) | |
end | |
def current_policy | |
@current_policy ||= current_authorization.policy(:tasks) | |
end | |
def add_user_policy | |
current_authorization.add_policy(:tasks, Tasks::Policy) | |
end | |
end | |
# == run == | |
Roles::Permissions.each_default do |name, permissions| | |
Role.create_with(permissions: permissions).find_or_create_by(name: name) | |
end | |
SigleFileApp.initialize! | |
Rack::Server.new(app: SigleFileApp, Port: 4000).start | |
=begin | |
# Creating users | |
curl -i -H 'Accept: application/json' -X POST -d 'user[email][email protected]' localhost:4000/users | |
curl -i -H 'Accept: application/json' -X POST -d 'user[admin]=true&user[email][email protected]' localhost:4000/users | |
# Listing users | |
curl -i -H 'Accept: application/json' -X GET localhost:4000/users\[email protected] | |
curl -i -H 'Accept: application/json' -X GET localhost:4000/users\[email protected] | |
# Creating and listing tasks | |
## using the user | |
curl -i -H 'Accept: application/json' -X POST -d 'task[description]=foo' localhost:4000/tasks\[email protected] | |
curl -i -H 'Accept: application/json' -X GET localhost:4000/tasks\[email protected] | |
## using the admin | |
curl -i -H 'Accept: application/json' -X POST -d 'task[description]=foo' localhost:4000/tasks\[email protected] | |
curl -i -H 'Accept: application/json' -X GET localhost:4000/tasks\[email protected] | |
=end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment