Autorización Con Pundit
() translation by (you can also view the original English article)
Pundit es una herramienta que te permite restringir ciertas partes de tu aplicación Rails a usuarios autorizados. Hace esto proporcionándote ciertos ayudantes.
En este tutorial, construirás un blog que restringe partes tales como crear, actualizar y borrar artículos solo para usuarios autorizados.
Comenzando
Comienza generando una nueva aplicación Rails.
1 |
rails new pundit-blog -T
|
La bandera -T
le dice a Rails que genere la nueva aplicación sin la suite de prueba por defecto. Ejecutar el comando generará tu aplicación Rails e instalará los gems por defecto.
Continúa y agrega los siguientes gems a tu Gemfile. Estarás usando bootstrap-sass para el diseño de tu aplicación, y Devise manejará la autenticación de usuario.
1 |
#Gemfile |
2 |
|
3 |
... |
4 |
gem 'bootstrap-sass' |
5 |
gem 'devise' |
Ejecuta el comando para instalar el gem.
1 |
bundle install
|
Ahora renombra app/assets/stylesheets/application.css
a app/assets/stylesheets/application.scss
. Agrega las siguientes líneas de código para importar bootstrap.
1 |
#app/assets/stylesheets/application.scss |
2 |
|
3 |
... |
4 |
@import 'bootstrap-sprockets'; |
5 |
@import 'bootstrap'; |
Crea un parcial llamado _navigation.html.erb
para contener tu código de navegación; el parcial debería estar ubicado en el directorio app/views/layouts. Haz que el parcial luzca como lo que tengo abajo.
1 |
#app/views/layouts/_navigation.html.erb |
2 |
|
3 |
<nav class="navbar navbar-inverse"> |
4 |
<div class="container"> |
5 |
<div class="navbar-header"> |
6 |
<%= link_to 'Pundit Blog', root_path, class: 'navbar-brand' %> |
7 |
</div>
|
8 |
<div id="navbar"> |
9 |
|
10 |
<ul class="nav navbar-nav pull-right"> |
11 |
<li><% link_to 'Home', root_path %></li> |
12 |
<ul class="nav navbar-nav pull-right"> |
13 |
<% if user_signed_in? %> |
14 |
<li><%= current_user.email %></li> |
15 |
<li><%= link_to 'Log out', destroy_user_session_path, method: :delete %></li> |
16 |
<% else %> |
17 |
<li><%= link_to 'Log In', new_user_session_path %></li> |
18 |
<li><%= link_to 'Sign Up', new_user_registration_path %></li> |
19 |
<% end %> |
20 |
</ul>
|
21 |
</ul>
|
22 |
</div>
|
23 |
</nav>
|
Para que la navegación sea usada, necesitas generarla en tu diseño de aplicación. Modifica tu diseño de aplicación para que luzca como lo que tengo abajo.
1 |
#app/views/layouts/application.html.erb |
2 |
|
3 |
<!DOCTYPE html>
|
4 |
<html>
|
5 |
<head>
|
6 |
<title>Pundit-Blog</title> |
7 |
<%= csrf_meta_tags %> |
8 |
|
9 |
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> |
10 |
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> |
11 |
</head>
|
12 |
|
13 |
<body>
|
14 |
<%= render "layouts/navigation" %> |
15 |
<div id="flash"> |
16 |
<% flash.each do |key, value| %> |
17 |
<div class="flash <%= key %>"><%= value %></div> |
18 |
<% end %> |
19 |
</div>
|
20 |
<div class="container-fluid"> |
21 |
<%= yield %> |
22 |
</div>
|
23 |
</body>
|
24 |
</html>
|
General el Modelo de Usuario
Ejecuta el comando para instalar Devise.
1 |
rails generate devise:install |
Ahora genera tu modelo de Usuario.
1 |
rails generate devise User |
Migra tu base de datos.
1 |
rake db:migrate |
Genera Recursos de Artículo
Ejecuta el comando para generar tus recursos de Artículo.
1 |
rails generate scaffold Articles title:string body:text |
Esto generará tu ArticlesController
y Modelo de Artículo. También generará las vistas necesarias.
Ahora migra tu base de datos ejecutando:
1 |
rake db:migrate |
Abre app/views/articles/_form.html.erb
y hazlo lucir como lo que tengo abajo.
1 |
#app/views/articles/_form.html.erb |
2 |
|
3 |
<%= form_for(article) do |f| %> |
4 |
<% if article.errors.any? %> |
5 |
<div id="error_explanation"> |
6 |
<h2><%= pluralize(article.errors.count, "error") %> prohibited this article from being saved:</h2> |
7 |
|
8 |
<ul>
|
9 |
<% article.errors.full_messages.each do |message| %> |
10 |
<li><%= message %></li> |
11 |
<% end %> |
12 |
</ul>
|
13 |
</div>
|
14 |
<% end %> |
15 |
|
16 |
<div class="field"> |
17 |
<%= f.label :title %> |
18 |
<%= f.text_field :title %> |
19 |
</div>
|
20 |
|
21 |
<div class="field"> |
22 |
<%= f.label :body %> |
23 |
<%= f.text_area :body %> |
24 |
</div>
|
25 |
|
26 |
<div class="actions"> |
27 |
<%= f.submit %> |
28 |
</div>
|
29 |
<% end %> |
Para tu archivo index, debería lucir así.
1 |
#app/views/articles/index.html.erb |
2 |
|
3 |
<table class="table table-bordered table-striped table-condensed table-hover"> |
4 |
<thead>
|
5 |
<tr>
|
6 |
<th>Title</th> |
7 |
<th>Body</th> |
8 |
<th colspan="3"></th> |
9 |
</tr>
|
10 |
</thead>
|
11 |
|
12 |
<tbody>
|
13 |
<% @articles.each do |article| %> |
14 |
<tr>
|
15 |
<td><%= article.title %></td> |
16 |
<td><%= article.body %></td> |
17 |
<td><%= link_to 'Show', article %></td> |
18 |
<td><%= link_to 'Edit', edit_article_path(article) %></td> |
19 |
<td><%= link_to 'Destroy', article, method: :delete, data: { confirm: 'Are you sure?' } %></td> |
20 |
</tr>
|
21 |
<% end %> |
22 |
</tbody>
|
23 |
</table>
|
24 |
|
25 |
<br>
|
26 |
|
27 |
<%= link_to 'New article', new_article_path %> |
El código de arriba acomoda los artículos en la página index en un formato de tabla para hacerlo lucir presentable.
Abre tu archivo de rutas y agrega la rut apara los recursos de artículos.
1 |
#config/routes.rb |
2 |
|
3 |
... |
4 |
resources :articles |
5 |
root to: "articles#index" |
Integra Pundit
Agrega el gem Pundit a tu Gemfile.
1 |
#Gemfile |
2 |
|
3 |
... |
4 |
gem 'pundit' |
Ejecuta el comando para instalar.
1 |
bundle install
|
Integra Pundit a tu aplicación agregando la siguiente línea a tu ApplicationController.
1 |
#app/controllers/application_controller.rb |
2 |
|
3 |
... |
4 |
include Pundit |
5 |
... |
Ejecuta el generador de Pundit.
1 |
rails g pundit:install |
Esto generará una carpeta app/policies que contiene una clase base con políticas. Cada política es una clase Ruby básica.
Así es como luce la clase básica de política.
1 |
#app/policies/application_policy.rb |
2 |
|
3 |
class ApplicationPolicy |
4 |
attr_reader :user, :record |
5 |
|
6 |
def initialize(user, record) |
7 |
@user = user |
8 |
@record = record |
9 |
end |
10 |
|
11 |
def index? |
12 |
false |
13 |
end |
14 |
|
15 |
def show? |
16 |
scope.where(:id => record.id).exists? |
17 |
end |
18 |
|
19 |
def create? |
20 |
false |
21 |
end |
22 |
|
23 |
def new? |
24 |
create? |
25 |
end |
26 |
|
27 |
def update? |
28 |
false |
29 |
end |
30 |
|
31 |
def edit? |
32 |
update? |
33 |
end |
34 |
|
35 |
def destroy? |
36 |
false |
37 |
end |
38 |
|
39 |
def scope |
40 |
Pundit.policy_scope!(user, record.class) |
41 |
end |
42 |
|
43 |
class Scope |
44 |
attr_reader :user, :scope |
45 |
|
46 |
def initialize(user, scope) |
47 |
@user = user |
48 |
@scope = scope |
49 |
end |
50 |
|
51 |
def resolve |
52 |
scope |
53 |
end |
54 |
end |
55 |
end |
Crea la Política de Artículo
Ahora necesitas escribir tu propia política. Para este tutorial, quieres permitir solo usuarios registrados para que creen nuevos artículos. Además de eso, solo los creadores de un artículo deberían poder editar y borrar el artículo.
Para lograr esto, tu política de artículo lucirá así.
1 |
#app/policies/article_policy.rb |
2 |
|
3 |
class ArticlePolicy < ApplicationPolicy |
4 |
def index? |
5 |
true
|
6 |
end
|
7 |
|
8 |
def create? |
9 |
user.present? |
10 |
end
|
11 |
|
12 |
def update? |
13 |
return true if user.present? && user == article.user |
14 |
end
|
15 |
|
16 |
def destroy? |
17 |
return true if user.present? && user == article.user |
18 |
end
|
19 |
|
20 |
private
|
21 |
|
22 |
def article |
23 |
record
|
24 |
end
|
25 |
end
|
En lo de arriba, estás permitiendo a todos (usuarios registrados y no registrados) ver la página index. Para crear un nuevo artículo, un usuario tiene que estar registrado. Usas user.present?
para averiguar si el usuario que está intentando realizar la acción está registrado.
Para actualizar y borrar, quieres asegurarte de que solo el usuario que creó el artículo pueda realizar estas acciones.
En este punto, necesitas establecer una relación entre tu modelo de Artículo y Usuario.
Lo haces generando una nueva migración.
1 |
rails generate migration add_user_id_to_articles user:references |
Después, migra tu base de datos ejecutando el comando:
1 |
rake db:migrate |
Abre el modelo User y agrega la línea que sella la relación.
1 |
#app/models/user.rb |
2 |
|
3 |
... |
4 |
has_many :articles |
Tu modelo Article debería tener esto.
1 |
#app/models/article.rb |
2 |
|
3 |
... |
4 |
belongs_to :user |
Ahora necesitas actualizar tu ArticlesController
para que esté en sincronía con lo que has hecho hasta ahora.
1 |
#app/controllers/articles_controller.rb |
2 |
|
3 |
class ArticlesController < ApplicationController |
4 |
before_action :set_article, only: [:show, :edit, :update, :destroy] |
5 |
|
6 |
# GET /articles |
7 |
# GET /articles.json |
8 |
def index |
9 |
@articles = Article.all |
10 |
authorize @articles |
11 |
end
|
12 |
|
13 |
# GET /articles/1 |
14 |
# GET /articles/1.json |
15 |
def show |
16 |
end
|
17 |
|
18 |
# GET /articles/new |
19 |
def new |
20 |
@article = Article.new |
21 |
authorize @article |
22 |
end
|
23 |
|
24 |
# GET /articles/1/edit |
25 |
def edit |
26 |
end
|
27 |
|
28 |
# POST /articles |
29 |
# POST /articles.json |
30 |
def create |
31 |
@article = Article.new(article_params) |
32 |
@article.user = current_user |
33 |
authorize @article |
34 |
|
35 |
respond_to do |format| |
36 |
if @article.save |
37 |
format.html { redirect_to @article, notice: 'Article was successfully created.' } |
38 |
format.json { render :show, status: :created, location: @article } |
39 |
else
|
40 |
format.html { render :new } |
41 |
format.json { render json: @article.errors, status: :unprocessable_entity } |
42 |
end
|
43 |
end
|
44 |
end
|
45 |
|
46 |
# PATCH/PUT /articles/1 |
47 |
# PATCH/PUT /articles/1.json |
48 |
def update |
49 |
respond_to do |format| |
50 |
if @article.update(article_params) |
51 |
format.html { redirect_to @article, notice: 'Article was successfully updated.' } |
52 |
format.json { render :show, status: :ok, location: @article } |
53 |
else
|
54 |
format.html { render :edit } |
55 |
format.json { render json: @article.errors, status: :unprocessable_entity } |
56 |
end
|
57 |
end
|
58 |
end
|
59 |
|
60 |
# DELETE /articles/1 |
61 |
# DELETE /articles/1.json |
62 |
def destroy |
63 |
@article.destroy |
64 |
respond_to do |format| |
65 |
format.html { redirect_to articles_url, notice: 'Article was successfully destroyed.' } |
66 |
format.json { head :no_content } |
67 |
end
|
68 |
end
|
69 |
|
70 |
private
|
71 |
# Use callbacks to share common setup or constraints between actions. |
72 |
def set_article |
73 |
@article = Article.find(params[:id]) |
74 |
authorize @article |
75 |
end
|
76 |
|
77 |
# Never trust parameters from the scary internet, only allow the white list through. |
78 |
def article_params |
79 |
params.require(:article).permit(:title, :body, :user_id) |
80 |
end
|
81 |
end
|
En este punto en tu aplicación, has implementado satisfactoriamente políticas que restringen ciertas partes de tu aplicación a usuarios selectos.
Quieres agregar un mensaje de error estándar que muestre cuando un usuario no autorizado intenta acceder a una página restringida. Para hacerlo, agrega lo siguiente a tu ApplicationController
.
1 |
#app/controllers/application_controller.rb |
2 |
|
3 |
... |
4 |
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized |
5 |
|
6 |
private |
7 |
|
8 |
def user_not_authorized |
9 |
flash[:warning] = "You are not authorized to perform this action." |
10 |
redirect_to(request.referrer || root_path) |
11 |
end |
Este código simplemente genera un texto básico que le dice al usuario que no está autorizado para realizar la acción.
Ejecuta:
1 |
$ rails server
|
Para comenzar tu servidor Rails, apunta a tu navegador a https://localhost:3000
para ver lo que tienes.
Conclusión
En este tutorial, aprendiste cómo trabajar con Devise y Pundit. Pudiste crear políticas que permitieron solo a usuarios autorizados ver ciertas partes de la aplicación. También creaste un texto de error básico que muestra cuando un usuario no autorizado intenta acceder a una parte restringida de la aplicación.
Puedes aprender más sobre Pundit revisando la página de GitHub.