Skip to content

Instantly share code, notes, and snippets.

@serradura
Last active July 6, 2022 14:01
Show Gist options
  • Save serradura/050cd51b3902c2ef02de95e9e7e6ac9b to your computer and use it in GitHub Desktop.
Save serradura/050cd51b3902c2ef02de95e9e7e6ac9b to your computer and use it in GitHub Desktop.
Rails + u-authorization
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
require 'bundler/inline'
gemfile(true) do
source 'https://rubygems.org'
gem 'pry', '~> 0.13.1'
gem 'u-authorization', '~> 2.3'
end
module Roles
ADMIN = 'admin'
USER = 'user'
PERMISSIONS = {
ADMIN => { 'access' => { 'any' => true } , 'create' => { 'any' => true } , 'edit' => { 'any' => true } },
USER => { 'access' => { 'except' => ['users'] }, 'create' => { 'only' => ['tasks'] }, 'edit' => { 'only' => ['tasks'] } }
}
end
class TaskPolicy < Micro::Authorization::Policy
def edit?(record)
user.id == record.user_id
end
end
user = OpenStruct.new(id: 1, role: Roles::USER)
authorization = Micro::Authorization::Model.build(
permissions: Roles::PERMISSIONS.fetch(user.role),
policies: { tasks: TaskPolicy },
context: {
user: user,
to_permit: ['tasks', 'index']
}
)
task1 = OpenStruct.new(id: 1, description: 'Buy milk', user_id: user.id)
task2 = OpenStruct.new(id: 2, description: 'Buy coffee', user_id: 2)
authorization.permissions.to?('access') # true
authorization.policy(:tasks).edit?(task1) # true
authorization.to(:tasks).edit?(task1) # true
authorization.policy(:tasks).edit?(task2) # false
authorization.to(:tasks).edit?(task2) # false
binding.pry
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment