diff --git a/api/Gemfile b/api/Gemfile index c38971f1e..9f6428e75 100644 --- a/api/Gemfile +++ b/api/Gemfile @@ -19,6 +19,7 @@ gem 'mimemagic', '~> 0.3' gem 'octokit', '~> 4.7' gem 'paranoia', '~> 2.4' gem 'pdfkit', '~> 0.8.2' +gem 'pundit', '~> 1.1' gem 'rack-cors', require: 'rack/cors' gem 'redcarpet', '~> 3.4.0' gem 'redis-rails', '~> 5.0.2' diff --git a/api/Gemfile.lock b/api/Gemfile.lock index f2e95fc95..ca371f1a5 100644 --- a/api/Gemfile.lock +++ b/api/Gemfile.lock @@ -158,6 +158,8 @@ GEM method_source (~> 0.9.0) public_suffix (3.0.1) puma (3.11.0) + pundit (1.1.0) + activesupport (>= 3.0.0) rack (2.0.3) rack-cors (1.0.2) rack-protection (2.0.0) @@ -327,6 +329,7 @@ DEPENDENCIES pdfkit (~> 0.8.2) pg (~> 0.18.4) puma (~> 3.0) + pundit (~> 1.1) rack-cors rails (~> 5.1) redcarpet (~> 3.4.0) diff --git a/api/app/controllers/application_controller.rb b/api/app/controllers/application_controller.rb index 13c271fb6..156e6dde9 100644 --- a/api/app/controllers/application_controller.rb +++ b/api/app/controllers/application_controller.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# See V1::ApiController for base controller of API V1. class ApplicationController < ActionController::API end diff --git a/api/app/controllers/v1/api_controller.rb b/api/app/controllers/v1/api_controller.rb index 2fd9c156e..0b20e0c19 100644 --- a/api/app/controllers/v1/api_controller.rb +++ b/api/app/controllers/v1/api_controller.rb @@ -2,6 +2,11 @@ module V1 class ApiController < ApplicationController + include Pundit + + rescue_from Pundit::NotAuthorizedError, with: :render_access_denied + rescue_from ActiveRecord::RecordNotFound, with: :render_not_found + def render_success(obj = { success: true }, status = 200) render json: obj, status: status end diff --git a/api/app/controllers/v1/concerns/user_auth.rb b/api/app/controllers/v1/concerns/user_auth.rb index f0f70a9eb..e1d5063dd 100644 --- a/api/app/controllers/v1/concerns/user_auth.rb +++ b/api/app/controllers/v1/concerns/user_auth.rb @@ -28,6 +28,10 @@ def authenticate_user end end + def current_user + @user + end + protected def render_unauthenticated diff --git a/api/app/controllers/v1/leader_profiles_controller.rb b/api/app/controllers/v1/leader_profiles_controller.rb index 50c10ff08..2c1f516f3 100644 --- a/api/app/controllers/v1/leader_profiles_controller.rb +++ b/api/app/controllers/v1/leader_profiles_controller.rb @@ -5,19 +5,15 @@ class LeaderProfilesController < ApiController include UserAuth def show - profile = LeaderProfile.find_by(id: params[:id]) - - return render_not_found unless profile - return render_access_denied if profile.user != @user + profile = LeaderProfile.find(params[:id]) + authorize profile render_success(profile) end def update - profile = LeaderProfile.find_by(id: params[:id]) - - return render_not_found unless profile - return render_access_denied if profile.user != @user + profile = LeaderProfile.find(params[:id]) + authorize profile if profile.submitted_at.present? return render_field_error( diff --git a/api/app/controllers/v1/new_club_applications_controller.rb b/api/app/controllers/v1/new_club_applications_controller.rb index 4872853b2..190e894b0 100644 --- a/api/app/controllers/v1/new_club_applications_controller.rb +++ b/api/app/controllers/v1/new_club_applications_controller.rb @@ -4,6 +4,13 @@ module V1 class NewClubApplicationsController < ApiController include UserAuth + # All applications + def full_index + return render_access_denied unless @user.admin? + render_success NewClubApplication.all + end + + # Applications for a specific user def index if params[:user_id] == @user.id.to_s render_success(@user.new_club_applications) @@ -13,28 +20,21 @@ def index end def show - application = NewClubApplication.find_by(id: params[:id]) - - return render_not_found unless application + application = NewClubApplication.find(params[:id]) + authorize application - if application.users.include? @user - render_success(application) - else - render_access_denied - end + render_success(application) end def create - c = NewClubApplication.create(users: [@user], - point_of_contact: @user) + c = NewClubApplication.create(users: [@user], point_of_contact: @user) render_success(c, 201) end def update c = NewClubApplication.find(params[:id]) - - return render_access_denied unless c.users.include? @user + authorize c if c.update_attributes(club_application_params) render_success(c) @@ -44,10 +44,8 @@ def update end def add_user - app = NewClubApplication.find_by(id: params[:new_club_application_id]) - - return render_not_found unless app - return render_access_denied unless app.users.include? @user + app = NewClubApplication.find(params[:new_club_application_id]) + authorize app if app.submitted_at.present? return render_field_error(:base, 'cannot edit application after submit') @@ -73,14 +71,10 @@ def add_user end def remove_user - app = NewClubApplication.find_by(id: params[:new_club_application_id]) - to_remove = User.find_by(id: params[:user_id]) - - return render_not_found unless app && to_remove + app = NewClubApplication.find(params[:new_club_application_id]) + to_remove = User.find(params[:user_id]) - return render_access_denied unless app.users.include? @user - - return render_access_denied unless app.point_of_contact == @user + authorize app if app.submitted_at.present? return render_field_error(:base, 'cannot edit application after submit') @@ -99,10 +93,8 @@ def remove_user end def submit - app = NewClubApplication.find_by(id: params[:new_club_application_id]) - - return render_not_found unless app - return render_access_denied unless app.users.include? @user + app = NewClubApplication.find(params[:new_club_application_id]) + authorize app if app.submit! render_success(app) diff --git a/api/app/models/user.rb b/api/app/models/user.rb index d666615e4..a202dadd8 100644 --- a/api/app/models/user.rb +++ b/api/app/models/user.rb @@ -34,4 +34,16 @@ def generate_auth_token! break unless User.find_by(auth_token: auth_token) end end + + def make_admin! + self.admin_at = Time.current + end + + def remove_admin! + self.admin_at = nil + end + + def admin? + admin_at.present? + end end diff --git a/api/app/policies/application_policy.rb b/api/app/policies/application_policy.rb new file mode 100644 index 000000000..c1f09aae9 --- /dev/null +++ b/api/app/policies/application_policy.rb @@ -0,0 +1,55 @@ +# 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? + scope.where(id: record.id).exists? + end + + def create? + false + end + + def new? + create? + end + + def update? + false + end + + def edit? + update? + end + + def destroy? + false + end + + def scope + Pundit.policy_scope!(user, record.class) + end + + class Scope + attr_reader :user, :scope + + def initialize(user, scope) + @user = user + @scope = scope + end + + def resolve + scope + end + end +end diff --git a/api/app/policies/leader_profile_policy.rb b/api/app/policies/leader_profile_policy.rb new file mode 100644 index 000000000..9a4f0464c --- /dev/null +++ b/api/app/policies/leader_profile_policy.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class LeaderProfilePolicy < ApplicationPolicy + def show? + owns_profile? + end + + def update? + owns_profile? + end + + private + + def owns_profile? + record.user == user + end +end diff --git a/api/app/policies/new_club_application_policy.rb b/api/app/policies/new_club_application_policy.rb new file mode 100644 index 000000000..f06032167 --- /dev/null +++ b/api/app/policies/new_club_application_policy.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class NewClubApplicationPolicy < ApplicationPolicy + def show? + user_added? + end + + def update? + user_added? + end + + def add_user? + user_added? + end + + def remove_user? + user_added? && record.point_of_contact == user + end + + def submit? + user_added? + end + + private + + def user_added? + record.users.include? user + end +end diff --git a/api/config/routes.rb b/api/config/routes.rb index 1a381787b..6a4a6c2f1 100644 --- a/api/config/routes.rb +++ b/api/config/routes.rb @@ -18,6 +18,8 @@ resources :donations, only: [:create] resources :club_applications, only: [:create] + + get '/new_club_applications', to: 'new_club_applications#full_index' resources :new_club_applications, only: %i[show update] do post 'add_user' delete 'remove_user' diff --git a/api/db/migrate/20180127102450_add_admin_at_to_user.rb b/api/db/migrate/20180127102450_add_admin_at_to_user.rb new file mode 100644 index 000000000..42c9a13a8 --- /dev/null +++ b/api/db/migrate/20180127102450_add_admin_at_to_user.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddAdminAtToUser < ActiveRecord::Migration[5.1] + def change + add_column :users, :admin_at, :datetime + end +end diff --git a/api/db/schema.rb b/api/db/schema.rb index b2a3a076a..95cf699d5 100644 --- a/api/db/schema.rb +++ b/api/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180127084614) do +ActiveRecord::Schema.define(version: 20180127102450) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -341,6 +341,7 @@ t.datetime "auth_token_generation" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.datetime "admin_at" end add_foreign_key "athul_clubs", "clubs" diff --git a/api/spec/models/user_spec.rb b/api/spec/models/user_spec.rb index 2d5a84dc9..448d39b55 100644 --- a/api/spec/models/user_spec.rb +++ b/api/spec/models/user_spec.rb @@ -7,7 +7,10 @@ it { should have_db_column :email } it { should have_db_column :login_code } + it { should have_db_column :login_code_generation } it { should have_db_column :auth_token } + it { should have_db_column :auth_token_generation } + it { should have_db_column :admin_at } it { should validate_presence_of :email } it { should validate_email_format_of :email } @@ -57,4 +60,24 @@ # changes every time expect { subject.generate_auth_token! }.to change { subject.auth_token } end + + example ':make_admin!' do + subject.admin_at = nil + + subject.make_admin! + + expect(subject.admin_at).to be_within(1.second).of(Time.current) + expect(subject.admin?).to eq(true) + end + + example ':remove_admin!' do + subject.admin_at = nil + + subject.make_admin! + expect(subject.admin?).to eq(true) + + subject.remove_admin! + expect(subject.admin_at).to eq(nil) + expect(subject.admin?).to eq(false) + end end diff --git a/api/spec/requests/v1/new_club_applications_spec.rb b/api/spec/requests/v1/new_club_applications_spec.rb index ca192979d..545467f0c 100644 --- a/api/spec/requests/v1/new_club_applications_spec.rb +++ b/api/spec/requests/v1/new_club_applications_spec.rb @@ -6,6 +6,33 @@ let(:user) { create(:user_authed) } let(:auth_headers) { { 'Authorization': "Bearer #{user.auth_token}" } } + describe 'GET /v1/new_club_applications' do + it 'requires authentication' do + get '/v1/new_club_applications' + expect(response.status).to eq(401) + end + + it 'requires admin access' do + get '/v1/new_club_applications', headers: auth_headers + expect(response.status).to eq(403) + end + + it 'lists all applications' do + my_app = create(:new_club_application) + my_app.users << user + + create(:new_club_application) # someone else's application + + # make user an admin + user.make_admin! && user.save + + get '/v1/new_club_applications', headers: auth_headers + expect(response.status).to eq(200) + + expect(json.length).to eq(2) + end + end + describe 'GET /v1/users/:id/new_club_applications' do it 'requires authentication' do get "/v1/users/#{user.id}/new_club_applications"