From 622525cd99dfce41084c18bbe4ec32fb632d9f1d Mon Sep 17 00:00:00 2001 From: Ray Gunawidjaja <33289654+rayguna@users.noreply.github.com> Date: Mon, 15 Jul 2024 13:57:25 +0000 Subject: [PATCH 01/17] Updated README.md. --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 091560cf..caeadb7a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,25 @@ # industrial-auth-1 Target: https://industrial-auth-1.matchthetarget.com/ + +Video: https://share.descript.com/view/qqL5sX534E1 + +Lesson: https://learn.firstdraft.com/lessons/201-photogram-industrial-authorization + +### A. Objectives: +- Hide certain routes, such as: + - /comments + - /photos + - /likes + - /follow_requests + - /ID/edit +- Make private profiles private. +- Using scaffold and leaving all routes will make the above routes available. + +### B. Tasks + +Hide the links and lock the ability to do the following: + - delete and edit photos and comments that don't belong to the user. + - view other user's posts and like section unless the profile is not private or the user is an accepted follower. + - see other user's pending follow requests. + - see a collection of photos of another user's leaders (feed, discover). For instance, /carol/feed and /carol/discover should not be visitable by other users, but Carol herself. From af63ba260d4679a89777c341155fb7b8b944f25d Mon Sep 17 00:00:00 2001 From: Ray Gunawidjaja <33289654+rayguna@users.noreply.github.com> Date: Mon, 15 Jul 2024 14:03:21 +0000 Subject: [PATCH 02/17] Updated README.md. --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index caeadb7a..cc728eb9 100644 --- a/README.md +++ b/README.md @@ -23,3 +23,17 @@ Hide the links and lock the ability to do the following: - view other user's posts and like section unless the profile is not private or the user is an accepted follower. - see other user's pending follow requests. - see a collection of photos of another user's leaders (feed, discover). For instance, /carol/feed and /carol/discover should not be visitable by other users, but Carol herself. + +### C. Approach + +1. Consider each and every route and action a user can take. +2. Solve security holes with: +- Filters: before_action and skip_before_action (https://guides.rubyonrails.org/action_controller_overview.html#filters). +- Redirecting: redirect_to and redirect_back (https://api.rubyonrails.org/v6.1.0/classes/ActionController/Redirecting.html). +- Devise’s current_user method. +- Ruby’s if/else statements. +- Deleting or limiting routes with only: and except: after resources. + +### D. Step-by-step Procedure + + From 2ccd418112f325905ba5ba4ed02cf728e0d94be9 Mon Sep 17 00:00:00 2001 From: Ray Gunawidjaja <33289654+rayguna@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:13:48 +0000 Subject: [PATCH 03/17] Incorporated except and only to limit routes. --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ config/routes.rb | 8 ++++---- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cc728eb9..97584954 100644 --- a/README.md +++ b/README.md @@ -36,4 +36,54 @@ Hide the links and lock the ability to do the following: ### D. Step-by-step Procedure +#### D1. Limit routes using `except` and `only`. +1. Populate tables with `rails sample_data`. +2. Run server with `bin/dev` and login as alice. +3. (5-11 min) There are several GET actions available for each route. Here are some of it: + - index + - show + - new + - edit + - create + - destroy + - update +4. We can limit routes by adding except: or only: parameters. + +For the follow_request route, we only allow a user to: create, update, and destroy. Other actions, such as ubdex, show, new, and edit are not allowed. In this case,let's change the routes.rb as follows. + +``` +# config/routes.rb + +Rails.application.routes.draw do + # ... + resources :comments + resources :follow_requests, except: [:index, :show, :new, :edit] + # ... +``` + +5. For likes, we only allow a user to create and destroy. + +``` +# config/routes.rb + +Rails.application.routes.draw do + # ... + resources :follow_requests, except: [:index, :show, :new, :edit] + resources :likes, only: [:create, :destroy] + # ... +``` + +6. For ohotos, we need most of the routes, but the index: + +``` +# config/routes.rb + +Rails.application.routes.draw do + # ... + resources :follow_requests, except: [:index, :show, :new, :edit] + resources :likes, only: [:create, :destroy] + resources :photos, except: [:index] + # ... +``` +However, you can delete not just your photos, but other people's as well. diff --git a/config/routes.rb b/config/routes.rb index 47050a54..0c5e7bbd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,9 +4,9 @@ devise_for :users resources :comments - resources :follow_requests - resources :likes - resources :photos + resources :follow_requests, except: [:index, :show, :new, :edit] + resources :likes, only: [:create, :destroy] + resources :photos [:index] get ":username" => "users#show", as: :user get ":username/liked" => "users#liked", as: :liked @@ -14,4 +14,4 @@ get ":username/discover" => "users#discover", as: :discover get ":username/followers" => "users#followers", as: :followers get ":username/following" => "users#following", as: :following -end \ No newline at end of file +end From ac7ffa296e8f2c2761d5357af2b7e92b13929bcd Mon Sep 17 00:00:00 2001 From: Ray Gunawidjaja <33289654+rayguna@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:15:56 +0000 Subject: [PATCH 04/17] added the missing except: parameter within routes.rb. --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 0c5e7bbd..53545094 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,7 +6,7 @@ resources :comments resources :follow_requests, except: [:index, :show, :new, :edit] resources :likes, only: [:create, :destroy] - resources :photos [:index] + resources :photos, except: [:index] get ":username" => "users#show", as: :user get ":username/liked" => "users#liked", as: :liked From c1f3c890ffeefca37d91ba5877a62475667dd857 Mon Sep 17 00:00:00 2001 From: Ray Gunawidjaja <33289654+rayguna@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:44:28 +0000 Subject: [PATCH 05/17] Incorporated authorization in controller with before_action. --- README.md | 62 ++++++++++++++++++++++++++++ app/controllers/photos_controller.rb | 7 ++++ 2 files changed, 69 insertions(+) diff --git a/README.md b/README.md index 97584954..4d7759fb 100644 --- a/README.md +++ b/README.md @@ -87,3 +87,65 @@ Rails.application.routes.draw do # ... ``` However, you can delete not just your photos, but other people's as well. + +#### D2. Authorization in controller with before_action + +1. Controller is one level below the routing, where we can restrict and fix photo deletion to allow a user to only delete his/her photo and not other users'. +2. One way to restrict a user from deleting another user's photo is by adding an if-else statement within app/controllers/photos_controller.rb, as follows. + +``` + # ... + def destroy + if current_user == @photo.owner + @photo.destroy + + respond_to do |format| + format.html { redirect_back fallback_location: root_url, notice: "Photo was successfully destroyed." } + format.json { head :no_content } + end + else + redirect_back(fallback_location: root_url, notice: "Nice try, but that is not your photo.") + end + end + # ... +``` + +3. Alternatively, check the ownership on some other actions.using before_action as follows. Use this approach in place of the previous one. + +``` +# app/controllers/photos_controller.rb + +class PhotosController < ApplicationController + before_action :set_photo, only: %i[ show edit update destroy ] + before_action :ensure_current_user_is_owner, only: [:destroy, :update, :edit] + # ... + private + # Use callbacks to share common setup or constraints between actions. + def set_photo + @photo = Photo.find(params[:id]) + end + + def ensure_current_user_is_owner + if current_user != @photo.owner + redirect_back fallback_location: root_url, alert: "You're not authorized for that." + end + end + # ... +end +``` + + +#### D3. Conditionals in the view templates + + +#### D4. Hiding private users + + +#### D5. Commenting on a photo + + +#### D6. Industrial authorization with pundit + +The above changes are considered "painful". In the next lesson, we will learn to use some shortcuts with the pundit gem. + +*** diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb index 78e53163..b8fe34db 100644 --- a/app/controllers/photos_controller.rb +++ b/app/controllers/photos_controller.rb @@ -1,5 +1,6 @@ class PhotosController < ApplicationController before_action :set_photo, only: %i[ show edit update destroy ] + before_action :ensure_current_user_is_owner, only: [:destroy, :update, :edit] # GET /photos or /photos.json def index @@ -63,6 +64,12 @@ def set_photo @photo = Photo.find(params[:id]) end + def ensure_current_user_is_owner + if current_user != @photo.owner + redirect_back fallback_location: root_url, alert: "You're not authorized for that." + end + end + # Only allow a list of trusted parameters through. def photo_params params.require(:photo).permit(:image, :comments_count, :likes_count, :caption, :owner_id) From 12195a085dbbbbd2b56241384a8942829c5a653f Mon Sep 17 00:00:00 2001 From: Ray Gunawidjaja <33289654+rayguna@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:55:10 +0000 Subject: [PATCH 06/17] Incorporated conditionals in the view templates. --- README.md | 30 +++++++++++++++++++++++++++++- app/views/photos/_photo.html.erb | 12 +++++++----- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4d7759fb..4e6de0d0 100644 --- a/README.md +++ b/README.md @@ -134,9 +134,37 @@ class PhotosController < ApplicationController end ``` - #### D3. Conditionals in the view templates +1. On the html page, let's hide the links/buttons/icons that are not available to the user (e.g., the edit or delete font awesome links to photos should not be visible to the users). +2. Use the conditional statement to show these buttons only if the current_user is the photo owner. + +``` + + +
+
+

+ <%= image_tag photo.owner.avatar_image, class: "rounded-circle mr-2", width: 36 %> + + <%= link_to photo.owner.username, user_path(photo.owner.username), class: "text-dark" %> +

+ +
+ <% if current_user == photo.owner %> + <%= link_to edit_photo_path(photo), class: "btn btn-link btn-sm text-muted" do %> + + <% end %> + + <%= link_to photo, method: :delete, class: "btn btn-link btn-sm text-muted" do %> + + <% end %> + <% end %> + +
+
+ +``` #### D4. Hiding private users diff --git a/app/views/photos/_photo.html.erb b/app/views/photos/_photo.html.erb index f0de50b8..d553af45 100644 --- a/app/views/photos/_photo.html.erb +++ b/app/views/photos/_photo.html.erb @@ -7,12 +7,14 @@
- <%= link_to edit_photo_path(photo), class: "btn btn-link btn-sm text-muted" do %> - - <% end %> + <% if current_user == photo.owner %> + <%= link_to edit_photo_path(photo), class: "btn btn-link btn-sm text-muted" do %> + + <% end %> - <%= link_to photo, data: { turbo_method: :delete }, class: "btn btn-link btn-sm text-muted" do %> - + <%= link_to photo, method: :delete, class: "btn btn-link btn-sm text-muted" do %> + + <% end %> <% end %>
From c2b7bbdaccba7aff377d0c3ae972eb4a6c6c9e0f Mon Sep 17 00:00:00 2001 From: Ray Gunawidjaja <33289654+rayguna@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:04:55 +0000 Subject: [PATCH 07/17] Hide private users using conditionals. --- README.md | 30 ++++++++++++++++++++++++++++++ app/views/users/show.html.erb | 20 ++++++++++++-------- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4e6de0d0..e0e3856a 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,36 @@ end #### D4. Hiding private users +1. Use conditionals in the views/users/show.html.erb page to hide private users that are set to "Private" or not being followed. We encapsulate this within the variable truevalue which includes three conditions. + +``` + + +
+
+ <%= render "users/user", user: @user %> +
+
+ +<% truevalue = current_user == @user || !@user.private? || current_user.leaders.include?(@user) %> + +<% if truevalue %> +
+
+ <%= render "users/profile_nav", user: @user %> +
+
+ + <% @user.own_photos.each do |photo| %> +
+
+ <%= render "photos/photo", photo: photo %> +
+
+ <% end %> +<% end %> +``` + #### D5. Commenting on a photo diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 5656d7d5..53457e82 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -4,16 +4,20 @@ -
-
- <%= render "users/profile_nav", user: @user %> -
-
+<% truevalue = current_user == @user || !@user.private? || current_user.leaders.include?(@user) %> -<% @user.own_photos.each do |photo| %> -
+<% if truevalue %> +
- <%= render "photos/photo", photo: photo %> + <%= render "users/profile_nav", user: @user %>
+ + <% @user.own_photos.each do |photo| %> +
+
+ <%= render "photos/photo", photo: photo %> +
+
+ <% end %> <% end %> From 520d1b3c0a9e65da4d16142d260bb8d835b3081c Mon Sep 17 00:00:00 2001 From: Ray Gunawidjaja <33289654+rayguna@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:38:52 +0000 Subject: [PATCH 08/17] Restrict comment to allow only commenting own photos, leaders' photos, or public users' photos. --- README.md | 32 ++++++++++++++++++++++++++ app/controllers/comments_controller.rb | 8 +++++++ app/models/comment.rb | 1 + 3 files changed, 41 insertions(+) diff --git a/README.md b/README.md index e0e3856a..fa8fbad1 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,38 @@ end #### D5. Commenting on a photo +1. Let's enable users to comment only public photos or photos of their leaders and not private photos. We will not allow comments if the current user is not the photo owner, the photo owner profile is private, and the current user does not follow the photo owner. Add a private method which is called before action: + +``` +# app/controllers/comments_controller.rb + +class CommentsController < ApplicationController + before_action :set_comment, only: %i[ show edit update destroy ] + before_action :is_an_authorized_user, only: [:destroy, :create] + # ... + def is_an_authorized_user + @photo = Photo.find(params.fetch(:comment).fetch(:photo_id)) + if current_user != @photo.owner && @photo.owner.private? && !current_user.leaders.include?(@photo.owner) + redirect_back fallback_location: root_url, alert: "Not authorized" + end + end + # ... +end +``` + +2. Also, link comment table to owner through photo, so the attributes can be accessed: + +``` +# app/models/comment.rb + +class Comment < ApplicationRecord + belongs_to :author, class_name: "User", counter_cache: true + belongs_to :photo, counter_cache: true + has_one :owner, through: :photo + + validates :body, presence: true +end +``` #### D6. Industrial authorization with pundit diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 046a8e5d..3d65150d 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -1,5 +1,6 @@ class CommentsController < ApplicationController before_action :set_comment, only: %i[ show edit update destroy ] + before_action :is_an_authorized_user, only: [:destroy, :create] # GET /comments or /comments.json def index @@ -67,4 +68,11 @@ def set_comment def comment_params params.require(:comment).permit(:author_id, :photo_id, :body) end + + def is_an_authorized_user + @photo = Photo.find(params.fetch(:comment).fetch(:photo_id)) + if current_user != @photo.owner && @photo.owner.private? && !current_user.leaders.include?(@photo.owner) + redirect_back fallback_location: root_url, alert: "Not authorized" + end + end end diff --git a/app/models/comment.rb b/app/models/comment.rb index 14a8eb00..0761b0e8 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -22,6 +22,7 @@ class Comment < ApplicationRecord belongs_to :author, class_name: "User", counter_cache: true belongs_to :photo, counter_cache: true + has_one :owner, through: :photo validates :body, presence: true end From 1bfa973f56893e5dd434a603fbe2becc53aa0b77 Mon Sep 17 00:00:00 2001 From: Ray Gunawidjaja <33289654+rayguna@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:41:48 +0000 Subject: [PATCH 09/17] Updated README.md. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index fa8fbad1..c4b7d62d 100644 --- a/README.md +++ b/README.md @@ -239,3 +239,6 @@ end The above changes are considered "painful". In the next lesson, we will learn to use some shortcuts with the pundit gem. *** + +Notes: +- One security flaw in this app is that the /rails/db page is accessible. From f4c494d3244962c0eec823eea34d89184f8f0dd4 Mon Sep 17 00:00:00 2001 From: Ray Gunawidjaja <33289654+rayguna@users.noreply.github.com> Date: Mon, 15 Jul 2024 18:04:27 +0000 Subject: [PATCH 10/17] Updated README.md. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index c4b7d62d..14cc3f87 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,11 @@ end The above changes are considered "painful". In the next lesson, we will learn to use some shortcuts with the pundit gem. +### E. Create a pull request + +1. First, create a branch from the head with: `git checkout -b rg_photogram_industrial_authorization`. +2. Switch main to the earliest version of the app with: `git checkout 46772ee`. + *** Notes: From 1f914c909e2819a8024d80c7b21519891388a3c3 Mon Sep 17 00:00:00 2001 From: Ray Gunawidjaja <33289654+rayguna@users.noreply.github.com> Date: Tue, 16 Jul 2024 16:16:31 +0000 Subject: [PATCH 11/17] updated README.md. --- README.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 14cc3f87..39b72bca 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Video: https://share.descript.com/view/qqL5sX534E1 Lesson: https://learn.firstdraft.com/lessons/201-photogram-industrial-authorization +## I. Photogram Industrial Authorization - the long way + ### A. Objectives: - Hide certain routes, such as: - /comments @@ -234,14 +236,25 @@ class Comment < ApplicationRecord end ``` -#### D6. Industrial authorization with pundit +#### E. Create a pull request -The above changes are considered "painful". In the next lesson, we will learn to use some shortcuts with the pundit gem. +1. First, create a branch from the head with: `git checkout -b rg_photogram_industrial_authorization`. Publish the branch. +2. Switch main to the earliest version of the app with: `git checkout 46772ee`. -### E. Create a pull request +## II. Industrial Authorization Using Pundit -1. First, create a branch from the head with: `git checkout -b rg_photogram_industrial_authorization`. -2. Switch main to the earliest version of the app with: `git checkout 46772ee`. +1. The above changes are considered "painful". In the next lesson, we will learn to use some shortcuts with the pundit gem. +2. create a new branch: `git checkout -b rg_authorization_with_pundit`. Publish the branch. + +#### Objectives + +1. create Pundit policies +2. use before_action +3. raise an exception +4. using Pundit helper method to gain access to the methods in all other controllers +5. incorporate Pundit into views +6. implement inheritance, aliasing, etc using Ruby +7. secure-by-default *** From 9e56cb703d1e3c83c276ad56cf1f20d350899e1b Mon Sep 17 00:00:00 2001 From: Ray Gunawidjaja <33289654+rayguna@users.noreply.github.com> Date: Tue, 16 Jul 2024 16:29:57 +0000 Subject: [PATCH 12/17] Update README.md. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 39b72bca..35252885 100644 --- a/README.md +++ b/README.md @@ -239,7 +239,8 @@ end #### E. Create a pull request 1. First, create a branch from the head with: `git checkout -b rg_photogram_industrial_authorization`. Publish the branch. -2. Switch main to the earliest version of the app with: `git checkout 46772ee`. +2. Switch main to the earliest version of the app with: `git reset --hard 46772ee`. +3. Update main `git push origin main --force`. ## II. Industrial Authorization Using Pundit From 3f4cc01fdd4719de70a52f9fd7fe391bb76385a2 Mon Sep 17 00:00:00 2001 From: Ray Gunawidjaja <33289654+rayguna@users.noreply.github.com> Date: Tue, 16 Jul 2024 17:28:57 +0000 Subject: [PATCH 13/17] created Pundit policies. --- Gemfile | 2 + Gemfile.lock | 3 ++ README.md | 87 ++++++++++++++++++++++++++++++++++++ app/policies/photo_policy.rb | 20 +++++++++ 4 files changed, 112 insertions(+) create mode 100644 app/policies/photo_policy.rb diff --git a/Gemfile b/Gemfile index 9eebefb9..0a393dce 100644 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,8 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby "3.2.1" +gem "pundit" + gem "simple_form" # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" diff --git a/Gemfile.lock b/Gemfile.lock index 80234896..68f9395d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -235,6 +235,8 @@ GEM public_suffix (5.0.4) puma (5.6.7) nio4r (~> 2.0) + pundit (2.3.2) + activesupport (>= 3.0.0) racc (1.6.2) rack (2.2.8) rack-protection (3.0.6) @@ -427,6 +429,7 @@ DEPENDENCIES pg (~> 1.1) pry-rails puma (~> 5.0) + pundit rails (~> 7.0.4, >= 7.0.4.3) rails-erd rails_db diff --git a/README.md b/README.md index 35252885..cf638500 100644 --- a/README.md +++ b/README.md @@ -244,9 +244,15 @@ end ## II. Industrial Authorization Using Pundit +Create a new branch 1. The above changes are considered "painful". In the next lesson, we will learn to use some shortcuts with the pundit gem. 2. create a new branch: `git checkout -b rg_authorization_with_pundit`. Publish the branch. +Move main to a certain branch in three steps: +1. git checkout main +2. git reset --hard +3. git push origin main --force + #### Objectives 1. create Pundit policies @@ -259,5 +265,86 @@ end *** +#### A. Create Pundit policies + +1. Install pundit as follows: + - add gem "pundit" to your Gemfile + - type: `bundle install`. + +2. Create a folder within app/ called policies/, and within it create a file called photo_policy.rb and a class and initialize and show methods as follows. + + +``` +# app/policies/photo_policy.rb + +class PhotoPolicy + attr_reader :user, :photo + + def initialize(user, photo) + @user = user + @photo = photo + end +end + +# Our policy is that a photo should only be seen by the owner or followers +# of the owner, unless the owner is not private in which case anyone can +# see it +def show? + user == photo.owner || + !photo.owner.private? || + photo.owner.followers.include?(user) + end +end +``` + +3. Populate tables with `rake sample_data`. + +4. Test if the policy is applied successfully as follows: +- First, get two users: + +``` +[1] pry(main)> alice = User.first +=> # +[2] pry(main)> bob = User.second +=> # +``` + +- Check followers and private status: + +``` +[3] pry(main)> alice.followers.include?(bob) +=> false +[4] pry(main)> alice.private? +=> true +``` + +- Get one of alice's photo: + +``` +photo = alice.own_photos.first +``` + +- First, we instantiate a policy for Alice, policy_a, and based on our .show? method, we check the visibility: + +``` +[6] pry(main)> policy_a = PhotoPolicy.new(alice, photo) +=> # +[7] pry(main)> policy_a.show? +=> true +``` + +- Do the same for Bob: + +``` +[8] pry(main)> policy_b = PhotoPolicy.new(bob, photo) +=> # +[9] pry(main)> policy_b.show? +=> false +``` + +#### B. Use before_action + +1. + Notes: - One security flaw in this app is that the /rails/db page is accessible. diff --git a/app/policies/photo_policy.rb b/app/policies/photo_policy.rb new file mode 100644 index 00000000..d8c56576 --- /dev/null +++ b/app/policies/photo_policy.rb @@ -0,0 +1,20 @@ +# app/policies/photo_policy.rb + +class PhotoPolicy + attr_reader :user, :photo + + def initialize(user, photo) + @user = user + @photo = photo + end + + # Our policy is that a photo should only be seen by the owner or followers + # of the owner, unless the owner is not private in which case anyone can + # see it + def show? + user == photo.owner || + !photo.owner.private? || + photo.owner.followers.include?(user) + end + +end From 48bff2ce343df04601c97d660aa288b72cc7250f Mon Sep 17 00:00:00 2001 From: Ray Gunawidjaja <33289654+rayguna@users.noreply.github.com> Date: Tue, 16 Jul 2024 17:42:00 +0000 Subject: [PATCH 14/17] added before_action to point to the method called ensure_user_is_authorized. --- README.md | 34 +++++++++++++++++++++++++++- app/controllers/photos_controller.rb | 7 ++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cf638500..a1b9c3e9 100644 --- a/README.md +++ b/README.md @@ -344,7 +344,39 @@ photo = alice.own_photos.first #### B. Use before_action -1. +1. Apply before_action as follows: + +``` +# app/controllers/photos_controller.rb + +class PhotosController < ApplicationController + before_action :set_photo, only: %i[ show edit update destroy ] + before_action :ensure_current_user_is_owner, only: [:destroy, :update, :edit] + before_action :ensure_user_is_authorized, only: [:show] + + # ... + private + # Use callbacks to share common setup or constraints between actions. + def set_photo + @photo = Photo.find(params[:id]) + end + + def ensure_current_user_is_owner + if current_user != @photo.owner + redirect_back fallback_location: root_url, alert: "You're not authorized for that." + end + end + + def ensure_user_is_authorized + if !PhotoPolicy.new(current_user, @photo).show? + redirect_back fallback_location: root_url + end + end + # ... +end +``` + +Navigating into a private user's photo should not be allowed. For example, looking at rails/db, we know that alethia is a private user and alexis is not private.Sign in with alexis and try to look at alethia's photo by vising https://urban-spoon-wgr7j6ggj7fvjxr-3000.app.github.dev/alethia. In this case, no pictures are shown. Notes: - One security flaw in this app is that the /rails/db page is accessible. diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb index b8fe34db..e3ec47df 100644 --- a/app/controllers/photos_controller.rb +++ b/app/controllers/photos_controller.rb @@ -1,6 +1,7 @@ class PhotosController < ApplicationController before_action :set_photo, only: %i[ show edit update destroy ] before_action :ensure_current_user_is_owner, only: [:destroy, :update, :edit] + before_action :ensure_user_is_authorized, only: [:show] # GET /photos or /photos.json def index @@ -70,6 +71,12 @@ def ensure_current_user_is_owner end end + def ensure_user_is_authorized + if !PhotoPolicy.new(current_user, @photo).show? + redirect_back fallback_location: root_url + end + end + # Only allow a list of trusted parameters through. def photo_params params.require(:photo).permit(:image, :comments_count, :likes_count, :caption, :owner_id) From 8b40d2a0705928404ff15a78a7c13d5213a306c7 Mon Sep 17 00:00:00 2001 From: Ray Gunawidjaja <33289654+rayguna@users.noreply.github.com> Date: Tue, 16 Jul 2024 18:16:37 +0000 Subject: [PATCH 15/17] added an exception to unauthorized users. --- README.md | 44 ++++++++++++++++++++++- app/controllers/application_controller.rb | 10 ++++++ app/controllers/photos_controller.rb | 2 +- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a1b9c3e9..c61f56f2 100644 --- a/README.md +++ b/README.md @@ -376,7 +376,49 @@ class PhotosController < ApplicationController end ``` -Navigating into a private user's photo should not be allowed. For example, looking at rails/db, we know that alethia is a private user and alexis is not private.Sign in with alexis and try to look at alethia's photo by vising https://urban-spoon-wgr7j6ggj7fvjxr-3000.app.github.dev/alethia. In this case, no pictures are shown. +Navigating into a private user's photo should not be allowed. For example, looking at rails/db, we know that alethia is a private user and alexis is not private. Sign in with alexis and try to look at alethia's photo by visiting https://urban-spoon-wgr7j6ggj7fvjxr-3000.app.github.dev/alethia. In this case, no pictures are shown. + + +#### C. Raising An Exception + +1. Let's raise an exception and generate an error message instead of redirecting if the current_user is not authorized as follows. + +``` +# app/controllers/photos_controller.rb + +class PhotosController < ApplicationController + # ... + before_action :ensure_user_is_authorized, only: [:show] + # ... + def ensure_user_is_authorized + if !PhotoPolicy.new(current_user, @photo).show? + raise Pundit::NotAuthorizedError, "not allowed" + end + end + # ... +end +``` + +I tried to visit https://urban-spoon-wgr7j6ggj7fvjxr-3000.app.github.dev/alethia/liked, but no exception was raised. + +- Alternatively, we can redirect with a flash message, as follows: + +``` +# app/controllers/application_controller.rb + +class ApplicationController + # ... + rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized + + private + + def user_not_authorized + flash[:alert] = "You are not authorized to perform this action." + + redirect_back fallback_location: root_url + end +end +``` Notes: - One security flaw in this app is that the /rails/db page is accessible. diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index bd664b1d..4ec4b273 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -3,10 +3,20 @@ class ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller? + rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized + protected def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys: [:username, :private, :name, :bio, :website, :avatar_image]) devise_parameter_sanitizer.permit(:account_update, keys: [:username, :private, :name, :bio, :website, :avatar_image]) end + + private + + def user_not_authorized + flash[:alert] = "You are not authorized to perform this action." + + redirect_back fallback_location: root_url + end end diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb index e3ec47df..a192c69f 100644 --- a/app/controllers/photos_controller.rb +++ b/app/controllers/photos_controller.rb @@ -73,7 +73,7 @@ def ensure_current_user_is_owner def ensure_user_is_authorized if !PhotoPolicy.new(current_user, @photo).show? - redirect_back fallback_location: root_url + raise Pundit::NotAuthorizedError, "not allowed" end end From 7ecef0d38b91b238cee3d261ebb1b574062aff10 Mon Sep 17 00:00:00 2001 From: Ray Gunawidjaja <33289654+rayguna@users.noreply.github.com> Date: Tue, 16 Jul 2024 19:07:04 +0000 Subject: [PATCH 16/17] updated README.md. --- README.md | 18 +++++++- app/controllers/application_controller.rb | 5 +++ app/controllers/comments_controller.rb | 5 ++- app/controllers/photos_controller.rb | 3 +- app/policies/application_policy.rb | 53 +++++++++++++++++++++++ app/policies/comment_policy.rb | 13 ++++++ app/policies/photo_policy.rb | 2 + 7 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 app/policies/application_policy.rb create mode 100644 app/policies/comment_policy.rb diff --git a/README.md b/README.md index c61f56f2..2da16675 100644 --- a/README.md +++ b/README.md @@ -270,8 +270,24 @@ Move main to a certain branch in three steps: 1. Install pundit as follows: - add gem "pundit" to your Gemfile - type: `bundle install`. + - type in the terminal `rails g pundit:install`. + - Add `include Pundit`: -2. Create a folder within app/ called policies/, and within it create a file called photo_policy.rb and a class and initialize and show methods as follows. + ``` + # app/controllers/application_controller.rb + +class ApplicationController + include Pundit +# ... + ``` + +2. Like Ajax, implementing pundit is a three-step process: + Step 1: Add before_action and/or before action to point to pundit authorization in the respectigve controllers, including the application_controller. + Step 2: Create the individual policy for each table controller (comments_policy, follow_requests_policy, likes_policy, etc.). + Step 3: Define the respective methods within each policy to return a true or false output. + + +3. Create a folder within app/ called policies/, and within it create a file called photo_policy.rb and a class and initialize and show methods as follows. ``` diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4ec4b273..0de6d1f6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,10 +1,15 @@ class ApplicationController < ActionController::Base + include Pundit before_action :authenticate_user! before_action :configure_permitted_parameters, if: :devise_controller? rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized + + # after_action :verify_authorized, unless: :devise_controller? + # after_action :verify_policy_scoped, only: :index, unless: :devise_controller? + protected def configure_permitted_parameters diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 3d65150d..bca75c8f 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -1,7 +1,10 @@ class CommentsController < ApplicationController before_action :set_comment, only: %i[ show edit update destroy ] - before_action :is_an_authorized_user, only: [:destroy, :create] + # before_action :is_an_authorized_user, only: [:destroy, :create] + before_action { authorize @comment || Comment} + + # GET /comments or /comments.json def index @comments = Comment.all diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb index a192c69f..69b88c4b 100644 --- a/app/controllers/photos_controller.rb +++ b/app/controllers/photos_controller.rb @@ -2,7 +2,8 @@ class PhotosController < ApplicationController before_action :set_photo, only: %i[ show edit update destroy ] before_action :ensure_current_user_is_owner, only: [:destroy, :update, :edit] before_action :ensure_user_is_authorized, only: [:show] - + before_action { authorize @photo || Photo } + # GET /photos or /photos.json def index @photos = Photo.all diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb new file mode 100644 index 00000000..be644fe3 --- /dev/null +++ b/app/policies/application_policy.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class ApplicationPolicy + attr_reader :user, :record + + def initialize(user, record) + @user = user + @record = record + end + + def index? + false + end + + def show? + false + end + + def create? + false + end + + def new? + create? + end + + def update? + false + end + + def edit? + update? + end + + def destroy? + false + end + + class Scope + def initialize(user, scope) + @user = user + @scope = scope + end + + def resolve + raise NoMethodError, "You must define #resolve in #{self.class}" + end + + private + + attr_reader :user, :scope + end +end diff --git a/app/policies/comment_policy.rb b/app/policies/comment_policy.rb new file mode 100644 index 00000000..2b76da37 --- /dev/null +++ b/app/policies/comment_policy.rb @@ -0,0 +1,13 @@ +class CommentPolicy + attr_reader :user, :comment + + def initialize(user, comment) + @user = user + @comment = comment + end + + def edit? + user == comment.author + end + +end diff --git a/app/policies/photo_policy.rb b/app/policies/photo_policy.rb index d8c56576..2bf4eca7 100644 --- a/app/policies/photo_policy.rb +++ b/app/policies/photo_policy.rb @@ -17,4 +17,6 @@ def show? photo.owner.followers.include?(user) end + + end From 30bf46b11f7c7622520d90f4342f2ff264c695b5 Mon Sep 17 00:00:00 2001 From: Ray Gunawidjaja <33289654+rayguna@users.noreply.github.com> Date: Tue, 16 Jul 2024 22:07:30 +0000 Subject: [PATCH 17/17] Fixing the policy.rb files to correctly link them to the controller files. --- README.md | 144 +++++++++++++++--- app/controllers/follow_requests_controller.rb | 2 + app/controllers/photos_controller.rb | 9 +- app/policies/comment_policy.rb | 19 +++ app/policies/follow_request_policy.rb | 26 ++++ app/policies/photo_policy.rb | 16 +- app/policies/user_policy.rb | 24 +++ app/views/users/show.html.erb | 4 +- 8 files changed, 211 insertions(+), 33 deletions(-) create mode 100644 app/policies/follow_request_policy.rb create mode 100644 app/policies/user_policy.rb diff --git a/README.md b/README.md index 2da16675..4a11f732 100644 --- a/README.md +++ b/README.md @@ -206,26 +206,26 @@ end 1. Let's enable users to comment only public photos or photos of their leaders and not private photos. We will not allow comments if the current user is not the photo owner, the photo owner profile is private, and the current user does not follow the photo owner. Add a private method which is called before action: ``` -# app/controllers/comments_controller.rb +#app/controllers/comments_controller.rb class CommentsController < ApplicationController before_action :set_comment, only: %i[ show edit update destroy ] before_action :is_an_authorized_user, only: [:destroy, :create] - # ... + #... def is_an_authorized_user @photo = Photo.find(params.fetch(:comment).fetch(:photo_id)) if current_user != @photo.owner && @photo.owner.private? && !current_user.leaders.include?(@photo.owner) redirect_back fallback_location: root_url, alert: "Not authorized" end end - # ... + #... end ``` 2. Also, link comment table to owner through photo, so the attributes can be accessed: ``` -# app/models/comment.rb +#app/models/comment.rb class Comment < ApplicationRecord belongs_to :author, class_name: "User", counter_cache: true @@ -274,15 +274,16 @@ Move main to a certain branch in three steps: - Add `include Pundit`: ``` - # app/controllers/application_controller.rb + #app/controllers/application_controller.rb class ApplicationController include Pundit -# ... +#... ``` 2. Like Ajax, implementing pundit is a three-step process: - Step 1: Add before_action and/or before action to point to pundit authorization in the respectigve controllers, including the application_controller. + Step 1: Add before_action to point to pundit authorization in the respective controllers, including the application_controller. + Declare the same instances and objects declared within the corresponding controller in the _policy.rb file. Step 2: Create the individual policy for each table controller (comments_policy, follow_requests_policy, likes_policy, etc.). Step 3: Define the respective methods within each policy to return a true or false output. @@ -291,7 +292,7 @@ class ApplicationController ``` -# app/policies/photo_policy.rb +#app/policies/photo_policy.rb class PhotoPolicy attr_reader :user, :photo @@ -302,9 +303,10 @@ class PhotoPolicy end end -# Our policy is that a photo should only be seen by the owner or followers -# of the owner, unless the owner is not private in which case anyone can -# see it +#Our policy is that a photo should only be seen by the owner or followers +#of the owner, unless the owner is not private in which case anyone can +#see it + def show? user == photo.owner || !photo.owner.private? || @@ -363,14 +365,14 @@ photo = alice.own_photos.first 1. Apply before_action as follows: ``` -# app/controllers/photos_controller.rb +#app/controllers/photos_controller.rb class PhotosController < ApplicationController before_action :set_photo, only: %i[ show edit update destroy ] before_action :ensure_current_user_is_owner, only: [:destroy, :update, :edit] before_action :ensure_user_is_authorized, only: [:show] - # ... + #... private # Use callbacks to share common setup or constraints between actions. def set_photo @@ -388,7 +390,7 @@ class PhotosController < ApplicationController redirect_back fallback_location: root_url end end - # ... + #... end ``` @@ -400,18 +402,18 @@ Navigating into a private user's photo should not be allowed. For example, looki 1. Let's raise an exception and generate an error message instead of redirecting if the current_user is not authorized as follows. ``` -# app/controllers/photos_controller.rb +#app/controllers/photos_controller.rb class PhotosController < ApplicationController - # ... + #... before_action :ensure_user_is_authorized, only: [:show] - # ... + #... def ensure_user_is_authorized if !PhotoPolicy.new(current_user, @photo).show? raise Pundit::NotAuthorizedError, "not allowed" end end - # ... + #... end ``` @@ -420,10 +422,10 @@ I tried to visit https://urban-spoon-wgr7j6ggj7fvjxr-3000.app.github.dev/alethia - Alternatively, we can redirect with a flash message, as follows: ``` -# app/controllers/application_controller.rb +#app/controllers/application_controller.rb class ApplicationController - # ... + #... rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized private @@ -436,5 +438,105 @@ class ApplicationController end ``` -Notes: +- Another shorthandthat we can adapt using Pundit is changing: + +``` +#app/controllers/photos_controller.rb + +class PhotosController < ApplicationController + #... + before_action :ensure_user_is_authorized, only: [:show] + #... + def ensure_user_is_authorized + if !PhotoPolicy.new(current_user, @photo).show? + raise Pundit::NotAuthorizedError, "not allowed" + end + end + #... +end +``` + +with + +``` +#app/controllers/photos_controller.rb + +class PhotosController < ApplicationController + #... + def show + authorize @photo + end + #... +end +``` + +When you pass the authorize method an instance of Photo: + +- It assumes there is a class called PhotoPolicy in app/policies. + +- It assumes there is a method called current_user. + +- It passes current_user as the first argument and whatever you pass to authorize as the second argument to a new instance of PhotoPolicy. + +- It calls a method named after the action with a ? appended on the new policy instance. + +- If it gets back false, it raises Pundit::NotAuthorizedError. + +To dos: +- For ecah table, create the corresponding _policy.rb files. +- update the _controller files. +- update the relevant methods within the _policy files as it applies to the allowed actions. For example, for photos the relevant actions are create and destroy (like and un-like). For follow_requestsm the relevant actions are create, destroy, and update (request/un-request). + +#### D. Views with Pundit + +1. Define a show? method in our user’s policy, like so: + +``` +# app/policies/user_policy.rb + +class UserPolicy + attr_reader :current_user, :user + + def initialize(current_user, user) + @current_user = current_user + @user = user + end + + def show? + user == current_user || + !user.private? || + user.followers.include?(current_user) + end + + def feed? + true + end + +end +``` + +Change the view template conditional from: + +``` + + + +<% if current_user == @user || !@user.private? || current_user.leaders.include?(@user) %> + +<% end %> +``` + +To: + +``` + + + +<% if policy(@user).show? %> + +<% end %> +``` + + +#### Notes: - One security flaw in this app is that the /rails/db page is accessible. diff --git a/app/controllers/follow_requests_controller.rb b/app/controllers/follow_requests_controller.rb index 9c30da7c..7df37fe1 100644 --- a/app/controllers/follow_requests_controller.rb +++ b/app/controllers/follow_requests_controller.rb @@ -1,6 +1,8 @@ class FollowRequestsController < ApplicationController before_action :set_follow_request, only: %i[ show edit update destroy ] + before_action { authorize @follow_requests || FollowRequests } + # GET /follow_requests or /follow_requests.json def index @follow_requests = FollowRequest.all diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb index 69b88c4b..64f787ee 100644 --- a/app/controllers/photos_controller.rb +++ b/app/controllers/photos_controller.rb @@ -1,7 +1,7 @@ class PhotosController < ApplicationController before_action :set_photo, only: %i[ show edit update destroy ] before_action :ensure_current_user_is_owner, only: [:destroy, :update, :edit] - before_action :ensure_user_is_authorized, only: [:show] + before_action { authorize @photo || Photo } # GET /photos or /photos.json @@ -11,6 +11,7 @@ def index # GET /photos/1 or /photos/1.json def show + authorize @photo end # GET /photos/new @@ -72,12 +73,6 @@ def ensure_current_user_is_owner end end - def ensure_user_is_authorized - if !PhotoPolicy.new(current_user, @photo).show? - raise Pundit::NotAuthorizedError, "not allowed" - end - end - # Only allow a list of trusted parameters through. def photo_params params.require(:photo).permit(:image, :comments_count, :likes_count, :caption, :owner_id) diff --git a/app/policies/comment_policy.rb b/app/policies/comment_policy.rb index 2b76da37..def231b0 100644 --- a/app/policies/comment_policy.rb +++ b/app/policies/comment_policy.rb @@ -6,8 +6,27 @@ def initialize(user, comment) @comment = comment end + #methods: create, destroy, edit, update + + #A user can make comments if they own the photo, the photo is public, or they are followers. + def new? + user == photo.owner || + !photo.owner.private? || + photo.owner.followers.include?(user) + end + + + #only the author can destroy, edit, or update + def destroy? + user == comment.author + end + def edit? user == comment.author end + def update? + user == comment.author + end + end diff --git a/app/policies/follow_request_policy.rb b/app/policies/follow_request_policy.rb new file mode 100644 index 00000000..7c33e23a --- /dev/null +++ b/app/policies/follow_request_policy.rb @@ -0,0 +1,26 @@ +class FollowRequest + attr_reader :current_user, :user + +def initialize(current_user, user) + @current_user = current_user + @user = user +end + +#methods: create, destroy, and update + +#only current_user can create a request + + +def new? + user == current_user +end + +#only current user can destroy the request +def destroy? + user == current_user +end + +#only current user can modify the request +def update? + user == current_user +end diff --git a/app/policies/photo_policy.rb b/app/policies/photo_policy.rb index 2bf4eca7..701c2c64 100644 --- a/app/policies/photo_policy.rb +++ b/app/policies/photo_policy.rb @@ -8,15 +8,27 @@ def initialize(user, photo) @photo = photo end + #methods: create, destroy, show + # Our policy is that a photo should only be seen by the owner or followers # of the owner, unless the owner is not private in which case anyone can # see it + + #only current user can create a new photo + def new? + user == current_user + end + + #only owner can destroy photo + def destroy? + user == photo.owner + end + + #only owner, non-private photo, and follower can view photos def show? user == photo.owner || !photo.owner.private? || photo.owner.followers.include?(user) end - - end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb new file mode 100644 index 00000000..4f9a31e0 --- /dev/null +++ b/app/policies/user_policy.rb @@ -0,0 +1,24 @@ +# app/policies/user_policy.rb + +class UserPolicy + attr_reader :current_user, :user + + def initialize(current_user, user) + @current_user = current_user + @user = user + end + + + #methods: view and feed + + def show? + user == current_user || + !user.private? || + user.followers.include?(current_user) + end + + def feed? + true + end + +end diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 53457e82..ceecc1ed 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -4,9 +4,7 @@
-<% truevalue = current_user == @user || !@user.private? || current_user.leaders.include?(@user) %> - -<% if truevalue %> +<% if policy(@user).show? %>
<%= render "users/profile_nav", user: @user %>