From da72ace374a9808729a0581414e04c41189e3970 Mon Sep 17 00:00:00 2001 From: yuenmichelle1 Date: Thu, 7 Sep 2023 10:34:09 -0500 Subject: [PATCH] Classification user groups authorization policies (#27) * individual stats breakdown per group. add session time to user group projects breakdown. * update sort of group member stats breakdown * Update user_group_member_stats_breakdown_serializer.rb * add specs user group classification count controller for individual stats breakdown stats * add specs for group member breakdown query stats * add spec for serializer * update session time to be included with top contributors * update staging credentials to include staging panoptes client credentials. add initial logic for queried user group context policy * add comment to show reference of where to find diff types of user group stats visibilities * show stats if panoptes admin * Update queried_user_group_context_policy.rb * update group admin check to check for group_admin * update user group controller specs * add queried user group context policy spec * Update user_group_classification_count_controller.rb * update validations to allow non logged in users to view publicly visible stats and update specs * fix accidental spacing issue * Update staging.yml.enc * Update spec/support/authentication_helpers.rb Co-authored-by: Zach Wolfenbarger * Update authentication_helpers.rb --------- Co-authored-by: Zach Wolfenbarger --- ...r_group_classification_count_controller.rb | 43 +- .../queried_user_group_context_policy.rb | 70 +++ config/credentials/staging.yml.enc | 2 +- ...er_classification_count_controller_spec.rb | 17 +- ...up_classification_count_controller_spec.rb | 435 +++++++++++++++++- .../queried_user_group_context_policy_spec.rb | 160 +++++++ spec/support/auth_header_validator.rb | 16 + spec/support/authentication_helpers.rb | 28 +- 8 files changed, 735 insertions(+), 36 deletions(-) create mode 100644 app/policies/queried_user_group_context_policy.rb create mode 100644 spec/policies/queried_user_group_context_policy_spec.rb create mode 100644 spec/support/auth_header_validator.rb diff --git a/app/controllers/user_group_classification_count_controller.rb b/app/controllers/user_group_classification_count_controller.rb index fa15afc..a4218e0 100644 --- a/app/controllers/user_group_classification_count_controller.rb +++ b/app/controllers/user_group_classification_count_controller.rb @@ -3,14 +3,15 @@ class UserGroupClassificationCountController < ApplicationController before_action :validate_params before_action :sanitize_params - # before_action :require_login + before_action :group_require_login def query - # TODO: Skipping Auth for now, Will introduce this in a separate PR - # pundit policy for user groups, will probably look something like below: - # current_user['queried_user_group_id'] = params[:id] - # authorize :queried_user_group_context, :show? - skip_authorization + if authorization_required_for_group? + authorize_user_to_query_group_stats + else + skip_authorization + end + if params[:individual_stats_breakdown] group_member_classification_counts = CountGroupMemberBreakdown.new.call(group_classification_count_params) @@ -23,12 +24,40 @@ def query project_contributions = CountGroupProjectContributions.new.call(group_classification_count_params) unless params[:project_id] || params[:workflow_id] render json: UserGroupClassificationCountsSerializer.new(group_classification_counts, group_active_user_classification_counts, project_contributions), serializer_options: serializer_opts_from_params - end end private + def authorize_user_to_query_group_stats + current_user['user_group_stats_visibility'] = group_stats_visibility + current_user['individual_stats_breakdown'] = params[:individual_stats_breakdown] + current_user['user_membership'] = current_user_membership + authorize :queried_user_group_context, :show? + end + + def authorization_required_for_group? + (params[:individual_stats_breakdown] && group_stats_visibility != 'public_show_all') || group_stats_visibility.include?('private') + end + + def group_require_login + require_login if authorization_required_for_group? + end + + def current_user_membership + url = "/memberships?user_id=#{current_user['id']}&user_group_id=#{params[:id]}" + client.panoptes.get(url)['memberships'][0] + end + + def queried_user_group + url = "/user_groups/#{params[:id]}" + panoptes_application_client.panoptes.get(url)['user_groups'][0] + end + + def group_stats_visibility + queried_user_group['stats_visibility'] + end + def validate_params super raise ValidationError, 'Cannot query individual stats breakdown with anything else EXCEPT start_date and end_date' if params[:individual_stats_breakdown] && non_date_range_params diff --git a/app/policies/queried_user_group_context_policy.rb b/app/policies/queried_user_group_context_policy.rb new file mode 100644 index 0000000..f51fabc --- /dev/null +++ b/app/policies/queried_user_group_context_policy.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +class QueriedUserGroupContextPolicy < ApplicationPolicy + attr_reader :user + + def initialize(user, _record) + super + @user = user + end + + def show? + return true if panoptes_admin? + + if individual_stats_breakdown_requested? + show_ind_stats_breakdown? + else + show_group_aggregate_stats? + end + end + + def show_group_aggregate_stats? + # For types of group stats visibilities see: https://github.com/zooniverse/eras/wiki/(Panoptes)-User-Groups-Stats-Visibilities + + case group_stats_visibility + when 'public_show_all', 'public_agg_show_ind_if_member', 'public_agg_only' + true + when 'private_show_agg_and_ind', 'private_agg_only' + group_member? + else + false + end + end + + def show_ind_stats_breakdown? + case group_stats_visibility + when 'public_show_all' + true + when 'public_agg_show_ind_if_member', 'private_show_agg_and_ind' + group_member? + when 'public_agg_only', 'private_agg_only' + group_admin? + else + false + end + end + + def group_member? + current_user_membership && !current_user_roles.empty? + end + + def group_admin? + group_member? && current_user_roles.include?('group_admin') + end + + def current_user_roles + current_user_membership['roles'] + end + + def current_user_membership + user['user_membership'] + end + + def individual_stats_breakdown_requested? + user['individual_stats_breakdown'] || false + end + + def group_stats_visibility + user['user_group_stats_visibility'] + end +end diff --git a/config/credentials/staging.yml.enc b/config/credentials/staging.yml.enc index 3af1018..6fa9665 100644 --- a/config/credentials/staging.yml.enc +++ b/config/credentials/staging.yml.enc @@ -1 +1 @@ -DE7jRIWwMnbmRiQRJ0SmsOae9uRB5sniKrwXf7F8ZuvfmLeo16iJbHLX+QtRkTG3hsBdJtjYA1fek7cfsLi78ThSon8Ovzaw+opS6h+mMBOT2takrxtlD1kYDYjupHbsTt5zb3MJEzLYXy+8utpy2wOunq/e8jXg28i0asWONbevnd0ieq/E8aVpSmNLNPlE6b4JeQA0Gk+aa1gKVnNPK9ByqWxaXieDimNiBQa90rmO6hdpvIVcDl6FbbZC3+SqOTab5yOdkZudiRvrcHnDc4fdb03V/hvPbk0vXSoL0dr1SsLlCjTgzkciU56NzJ75z7k9pd8GJ5qQnmJnNIgtOjJyK85t8jcVOhqARI3UihE6QXXbHS5BmhB2ssJ7gi031qpLtM6Iax60zhg9TgWU8mothUE3/b8IONQgEGnLwnnF5KoW4QXkQe50vM+ar0Os6bWEh4adfmjXV5MJGknZ+kBa4tWifDXMWS38+uLDaPmtaB4uWx+iQB0UAxYn/+wFlXWi59nOFf+s+85iIuVX6wHyUNSMdnnMZrGhhfBqQ7GC82Jmelu9dVA9TkuMz6UY8Ts1NATn6HYOVu9VWYcxqLpynbIdbNClKNO4w/lGLw==--P8SvT9334PWTh9EK--oeM/Hj0aEswh7ipr+Rv2QQ== \ No newline at end of file +PGbmdbmi4qGqkpAPoEEdruRtfzTdqrFUuxKZyKkHogeNKi3V3XRYY7xCIpiVzaOk9x2P88UnPSi3uQJX00YFC5g11ta00WuH9WmPEjr9zSrpXTQ5Vou+dKoTszP2nPauNwUPkrYTM2q3Ew2691GspeXK7D6I+t0Oi0uVckI2C7RJknpcaqxDUwOVV4idGczyenMKPYPoOQ9rBogwG2JvG1YBsS6xul87lHyYqmVB7a2KmAxMLPNFbTwT3ASuwreDZyrG6e2oyXMBG1tFtnyX3wcPkj9A02oTN4mLxoaBVRVQS4T43sFqbGawlDPUdHZbI5B6+ekWpqgMIBQXkvqORyUTmfMGhbs3TEuNYEfKBkO1fo3+B2XtGNOknJPIavwVvaUFt2KYRh2Pmdk2ew7VA9FTt6mzTy6QewA6lA4vPfs01flrRzGDx/+DJCydL+UXX04izUzyH2eul0NWAPTKJ0btocok0GCcJb5mXktgECJXBKWO9j/90UVeupaxetpGp/SsfMjtjildK9M0X33ehmfzeWTqmFp+2mnfQo5R8p9XPsX/eVpvGwogSS6dDXIjYixlM0lhOeRq6loWguGRPLY5Fv7Uir7iMoc5vh/mk/Ivaqm+Xf7MkWCZ5QRj+FM9UYczp8C5nX3ndL7WJyfUDRPZR8gRLRZoIIKKznC/icw05IqysCcLBI8wQpl7FGLN760IrlWHWwz4lT4DbeIn7sZ7OoLimYkw/x7QGmOZzpSLGed589D3BsERsHhNXPBVKVaWhReenYgME/u/nfV+dwmE0cPJVxmYGllDdZKEeqtPOSwWK5vEksvCAVowwwADjZNCDVaeEdQ6c+y1--MUB9zAKbMkjYaijP--jfzUVjic0hyW9gGZ6Wv58g== \ No newline at end of file diff --git a/spec/controllers/user_classification_count_controller_spec.rb b/spec/controllers/user_classification_count_controller_spec.rb index eb49985..d8e754e 100644 --- a/spec/controllers/user_classification_count_controller_spec.rb +++ b/spec/controllers/user_classification_count_controller_spec.rb @@ -69,19 +69,10 @@ end context 'missing token' do - it 'returns a 403 missing authorization header' do - get :query, params: { id: classification_event.user_id.to_s } - expected_response = { error: 'Missing Authorization header' } - expect(response.status).to eq(403) - expect(response.body).to eq(expected_response.to_json) - end - - it 'returns a 403 missing when missing bearer token' do - request.headers['Authorization'] = 'asjdhaskdhsa' - get :query, params: { id: classification_event.user_id.to_s } - expected_response = { error: 'Missing Bearer token' } - expect(response.status).to eq(403) - expect(response.body).to eq(expected_response.to_json) + it_behaves_like 'returns 403 when authorization header is invalid' do + before(:each) { + get :query, params: { id: classification_event.user_id.to_s } + } end end diff --git a/spec/controllers/user_group_classification_count_controller_spec.rb b/spec/controllers/user_group_classification_count_controller_spec.rb index 8a0ad13..5470ae0 100644 --- a/spec/controllers/user_group_classification_count_controller_spec.rb +++ b/spec/controllers/user_group_classification_count_controller_spec.rb @@ -3,10 +3,12 @@ require 'rails_helper' RSpec.describe UserGroupClassificationCountController do + include AuthenticationHelpers + describe 'GET query' do let!(:classification_user_group) { create(:classification_user_group) } - context 'individual_stats_breakdown is false/not a param' do + shared_examples 'shows group aggregate stats' do it 'returns total_count, time_spent, active_users, and project_contributions of user group' do get :query, params: { id: classification_user_group.user_group_id.to_s } expect(response.status).to eq(200) @@ -16,32 +18,427 @@ expect(response_body['active_users']).to eq(1) expect(response_body['project_contributions'].length).to eq(1) end + end - it 'does not compute project_contributions when params[:project_id] given' do - get :query, params: { id: classification_user_group.user_group_id.to_s, project_id: 2 } + shared_examples 'shows group individual stats breakdown' do + it 'returns group_member_stats_breakdown' do + get :query, params: { id: classification_user_group.user_group_id.to_s, individual_stats_breakdown: true } expect(response.status).to eq(200) response_body = JSON.parse(response.body) - expect(response_body).not_to have_key('project_contributions') + expect(response_body).to have_key('group_member_stats_breakdown') end + end - it 'does not compute project_contributions when params[:workflow_id] given' do - get :query, params: { id: classification_user_group.user_group_id.to_s, workflow_id: 2 } - expect(response.status).to eq(200) - response_body = JSON.parse(response.body) - expect(response_body).not_to have_key('project_contributions') + shared_context 'user_group member' do + before(:each) { + authenticate_with_membership!(classification_user_group, [membership(classification_user_group)]) + } + end + + shared_context 'user_group admin' do + before(:each) { + authenticate_with_membership!(classification_user_group, [membership(classification_user_group, 'group_admin')]) + } + end + + shared_context 'zooniverse admin' do + before(:each) { + authenticate_with_membership!(classification_user_group, [], is_panoptes_admin: true) + } + end + + shared_context 'user group with stats_visibility' do |stats_visibility| + before(:each) { + user_groups_url = "/user_groups/#{classification_user_group.user_group_id}" + allow(panoptes_application_client).to receive_message_chain(:panoptes, :get).with(user_groups_url).and_return('user_groups' => [user_group(classification_user_group, stats_visibility)]) + } + end + + before(:each) { + allow(controller).to receive(:panoptes_application_client).and_return(panoptes_application_client) + } + + context 'individual_stats_breakdown is false/not a param' do + context 'public_show_all' do + include_context 'user group with stats_visibility', 'public_show_all' + context 'querying user is not a member' do + before(:each) { + authenticate_with_membership!(classification_user_group, []) + } + + it_behaves_like 'shows group aggregate stats' + + it 'does not compute project_contributions when params[:project_id] given' do + get :query, params: { id: classification_user_group.user_group_id.to_s, project_id: 2 } + expect(response.status).to eq(200) + response_body = JSON.parse(response.body) + expect(response_body).not_to have_key('project_contributions') + end + + it 'does not compute project_contributions when params[:workflow_id] given' do + get :query, params: { id: classification_user_group.user_group_id.to_s, workflow_id: 2 } + expect(response.status).to eq(200) + response_body = JSON.parse(response.body) + expect(response_body).not_to have_key('project_contributions') + end + end + + context 'querying user is not logged in' do + it_behaves_like 'shows group aggregate stats' + end + + context 'querying user is a group member' do + include_context 'user_group member' + it_behaves_like 'shows group aggregate stats' + end + + context 'querying user is a group admin' do + include_context 'user_group admin' + it_behaves_like 'shows group aggregate stats' + end + + context 'querying user is a zooniverse admin' do + include_context 'zooniverse admin' + it_behaves_like 'shows group aggregate stats' + end + end + + context 'public_agg_show_ind_if_member' do + include_context 'user group with stats_visibility', 'public_agg_show_ind_if_member' + + context 'querying user is not a member' do + before(:each) { + authenticate_with_membership!(classification_user_group, []) + } + + it_behaves_like 'shows group aggregate stats' + end + + context 'querying user is not logged in' do + it_behaves_like 'shows group aggregate stats' + end + + context 'querying user is a group member' do + include_context 'user_group member' + it_behaves_like 'shows group aggregate stats' + end + + context 'querying user is a group admin' do + include_context 'user_group admin' + it_behaves_like 'shows group aggregate stats' + end + + context 'querying user is a zooniverse admin' do + include_context 'zooniverse admin' + it_behaves_like 'shows group aggregate stats' + end + end + + context 'public_agg_only' do + include_context 'user group with stats_visibility', 'public_agg_only' + + context 'querying user is not a member' do + before(:each) { + authenticate_with_membership!(classification_user_group, []) + } + + it_behaves_like 'shows group aggregate stats' + end + + context 'querying user is not logged in' do + it_behaves_like 'shows group aggregate stats' + end + + context 'querying user is a group member' do + include_context 'user_group member' + it_behaves_like 'shows group aggregate stats' + end + + context 'querying user is a group admin' do + include_context 'user_group admin' + it_behaves_like 'shows group aggregate stats' + end + + context 'querying user is a zooniverse admin' do + include_context 'zooniverse admin' + it_behaves_like 'shows group aggregate stats' + end + end + + context 'private_show_agg_and_ind' do + include_context 'user group with stats_visibility', 'private_show_agg_and_ind' + + context 'querying user is not a member' do + before(:each) { + authenticate_with_membership!(classification_user_group, []) + } + + it 'does not show group aggregate stats' do + get :query, params: { id: classification_user_group.user_group_id.to_s } + expect(response.status).to eq(403) + end + end + + context 'querying user is not logged in' do + it_behaves_like 'returns 403 when authorization header is invalid' do + before(:each) { + get :query, params: { id: classification_user_group.user_group_id.to_s } + } + end + end + + context 'querying user is a group member' do + include_context 'user_group member' + it_behaves_like 'shows group aggregate stats' + end + + context 'querying user is a group admin' do + include_context 'user_group admin' + it_behaves_like 'shows group aggregate stats' + end + + context 'querying user is a zooniverse admin' do + include_context 'zooniverse admin' + it_behaves_like 'shows group aggregate stats' + end + end + + context 'private_agg_only' do + include_context 'user group with stats_visibility', 'private_agg_only' + + context 'querying user is not a member' do + before(:each) { + authenticate_with_membership!(classification_user_group, []) + } + + it 'does not show group aggregate stats' do + get :query, params: { id: classification_user_group.user_group_id.to_s } + expect(response.status).to eq(403) + end + end + + context 'querying user is not logged in' do + it_behaves_like 'returns 403 when authorization header is invalid' do + before(:each) { + get :query, params: { id: classification_user_group.user_group_id.to_s } + } + end + end + + context 'querying user is a group member' do + include_context 'user_group member' + it_behaves_like 'shows group aggregate stats' + end + + context 'querying user is a group admin' do + include_context 'user_group admin' + it_behaves_like 'shows group aggregate stats' + end + + context 'querying user is a zooniverse admin' do + include_context 'zooniverse admin' + it_behaves_like 'shows group aggregate stats' + end end end context 'individual_stats_breakdown is true' do - it 'returns group_member_stats_breakdown' do - get :query, params: { id: classification_user_group.user_group_id.to_s, individual_stats_breakdown: true } - expect(response.status).to eq(200) - response_body = JSON.parse(response.body) - expect(response_body).to have_key('group_member_stats_breakdown') + context 'public_show_all' do + include_context 'user group with stats_visibility', 'public_show_all' + + context 'querying user is not a member' do + before(:each) { + authenticate_with_membership!(classification_user_group, []) + } + it_behaves_like 'shows group individual stats breakdown' + end + + context 'querying user is not logged in' do + it_behaves_like 'shows group aggregate stats' + end + + context 'querying user is a group member' do + include_context 'user_group member' + it_behaves_like 'shows group individual stats breakdown' + end + + context 'querying user is a group admin' do + include_context 'user_group admin' + it_behaves_like 'shows group individual stats breakdown' + end + + context 'querying user is a zooniverse admin' do + include_context 'zooniverse admin' + it_behaves_like 'shows group individual stats breakdown' + end + end + + context 'public_agg_show_ind_if_member' do + include_context 'user group with stats_visibility', 'public_agg_show_ind_if_member' + + context 'querying user is not a member' do + before(:each) { + authenticate_with_membership!(classification_user_group, []) + } + + it 'does not show individual stats breakdown' do + get :query, params: { id: classification_user_group.user_group_id.to_s, individual_stats_breakdown: true } + expect(response.status).to eq(403) + end + end + + context 'querying user is not logged in' do + it_behaves_like 'returns 403 when authorization header is invalid' do + before(:each) { + get :query, params: { id: classification_user_group.user_group_id.to_s, individual_stats_breakdown: true } + expect(response.status).to eq(403) + } + end + end + + context 'querying user is a group member' do + include_context 'user_group member' + it_behaves_like 'shows group individual stats breakdown' + end + + context 'querying user is a group admin' do + include_context 'user_group admin' + it_behaves_like 'shows group individual stats breakdown' + end + + context 'querying user is a zooniverse admin' do + include_context 'zooniverse admin' + it_behaves_like 'shows group individual stats breakdown' + end + end + + context 'public_agg_only' do + include_context 'user group with stats_visibility', 'public_agg_only' + + context 'querying user is not a member' do + before(:each) { + authenticate_with_membership!(classification_user_group, []) + } + + it 'does not show individual stats breakdown' do + get :query, params: { id: classification_user_group.user_group_id.to_s, individual_stats_breakdown: true } + expect(response.status).to eq(403) + end + end + + context 'querying user is not logged in' do + it_behaves_like 'returns 403 when authorization header is invalid' do + before(:each) { + get :query, params: { id: classification_user_group.user_group_id.to_s, individual_stats_breakdown: true } + expect(response.status).to eq(403) + } + end + end + + context 'querying user is a group member' do + include_context 'user_group member' + it 'does not show individual stats breakdown' do + get :query, params: { id: classification_user_group.user_group_id.to_s, individual_stats_breakdown: true } + expect(response.status).to eq(403) + end + end + + context 'querying user is a group admin' do + include_context 'user_group admin' + it_behaves_like 'shows group individual stats breakdown' + end + + context 'querying user is a zooniverse admin' do + include_context 'zooniverse admin' + it_behaves_like 'shows group individual stats breakdown' + end + end + + context 'private_show_agg_and_ind' do + include_context 'user group with stats_visibility', 'private_show_agg_and_ind' + + context 'querying user is not a member' do + before(:each) { + authenticate_with_membership!(classification_user_group, []) + } + + it 'does not show individual stats breakdown' do + get :query, params: { id: classification_user_group.user_group_id.to_s, individual_stats_breakdown: true } + expect(response.status).to eq(403) + end + end + + context 'querying user is not logged in' do + it_behaves_like 'returns 403 when authorization header is invalid' do + before(:each) { + get :query, params: { id: classification_user_group.user_group_id.to_s, individual_stats_breakdown: true } + expect(response.status).to eq(403) + } + end + end + + context 'querying user is a group member' do + include_context 'user_group member' + it_behaves_like 'shows group individual stats breakdown' + end + + context 'querying user is a group admin' do + include_context 'user_group admin' + it_behaves_like 'shows group individual stats breakdown' + end + + context 'querying user is a zooniverse admin' do + include_context 'zooniverse admin' + it_behaves_like 'shows group individual stats breakdown' + end + end + + context 'private_agg_only' do + include_context 'user group with stats_visibility', 'private_agg_only' + + context 'querying user is not a member' do + before(:each) { + authenticate_with_membership!(classification_user_group, []) + } + + it 'does not show individual stats breakdown' do + get :query, params: { id: classification_user_group.user_group_id.to_s, individual_stats_breakdown: true } + expect(response.status).to eq(403) + end + end + + context 'querying user is not logged in' do + it_behaves_like 'returns 403 when authorization header is invalid' do + before(:each) { + get :query, params: { id: classification_user_group.user_group_id.to_s, individual_stats_breakdown: true } + expect(response.status).to eq(403) + } + end + end + + context 'querying user is a group member' do + include_context 'user_group member' + it 'does not show individual stats breakdown' do + get :query, params: { id: classification_user_group.user_group_id.to_s, individual_stats_breakdown: true } + expect(response.status).to eq(403) + end + end + + context 'querying user is a group admin' do + include_context 'user_group admin' + it_behaves_like 'shows group individual stats breakdown' + end + + context 'querying user is a zooniverse admin' do + include_context 'zooniverse admin' + it_behaves_like 'shows group individual stats breakdown' + end end end context 'param validations' do + include_context 'user group with stats_visibility', 'public_show_all' + it_behaves_like 'ensure valid query params', :query, id: 1 it 'ensures you cannot query individual_stats_breakdown and any non date range param' do @@ -66,4 +463,14 @@ end end end + + def membership(classification_user_group, role_type='group_member') + { 'id' => 123, + 'user_group_id' => classification_user_group.user_group_id, 'user_id' => classification_user_group.user_id, + 'roles' => [role_type] } + end + + def user_group(classification_user_group, stats_visibility) + { 'id' => classification_user_group.user_group_id, 'stats_visibility' => stats_visibility } + end end diff --git a/spec/policies/queried_user_group_context_policy_spec.rb b/spec/policies/queried_user_group_context_policy_spec.rb new file mode 100644 index 0000000..18b1cb3 --- /dev/null +++ b/spec/policies/queried_user_group_context_policy_spec.rb @@ -0,0 +1,160 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe QueriedUserGroupContextPolicy do + let(:querying_user) { + { + 'id' => '1234', + 'login' => 'login', + 'display_name' => 'display_name' + } + } + + permissions :show? do + it 'permits querying_user to query if panoptes admin' do + querying_user['admin'] = true + expect(described_class).to permit(querying_user) + end + + context 'individual stats breakdown requested' do + before(:each) do + querying_user['individual_stats_breakdown'] = true + end + + it 'permits if group_stats_visibility is public_show_all' do + querying_user['user_group_stats_visibility'] = 'public_show_all' + expect(described_class).to permit(querying_user) + end + + context 'public_agg_show_ind_if_member' do + before(:each) do + querying_user['user_group_stats_visibility'] = 'public_agg_show_ind_if_member' + end + it 'forbids if user is not a member' do + expect(described_class).not_to permit(querying_user) + end + + it 'permits if user is a member' do + querying_user['user_membership'] = membership + expect(described_class).to permit(querying_user) + end + end + + context 'private_show_agg_and_ind' do + before(:each) do + querying_user['user_group_stats_visibility'] = 'private_show_agg_and_ind' + end + it 'forbids if user is not a member' do + expect(described_class).not_to permit(querying_user) + end + + it 'permits if user is a member' do + querying_user['user_membership'] = membership + expect(described_class).to permit(querying_user) + end + end + + context 'public_agg_only' do + before(:each) do + querying_user['user_group_stats_visibility'] = 'public_agg_only' + end + it 'forbids if user is not a member' do + expect(described_class).not_to permit(querying_user) + end + + it 'forbids if user is a member' do + querying_user['user_membership'] = membership + expect(described_class).not_to permit(querying_user) + end + + it 'permits if user is an admin' do + querying_user['user_membership'] = membership('group_admin') + expect(described_class).to permit(querying_user) + end + end + + context 'private_agg_only' do + before(:each) do + querying_user['user_group_stats_visibility'] = 'private_agg_only' + end + it 'forbids if user is not a member' do + expect(described_class).not_to permit(querying_user) + end + + it 'forbids if user is a member' do + querying_user['user_membership'] = membership + expect(described_class).not_to permit(querying_user) + end + + it 'permits if user is an admin' do + querying_user['user_membership'] = membership('group_admin') + expect(described_class).to permit(querying_user) + end + end + + it 'forbids if group visibility is not of listed types' do + querying_user['user_group_stats_visibility'] = 'other' + expect(described_class).not_to permit(querying_user) + end + end + + context 'group aggregate stats' do + it 'permits if group_stats_visibility is public_show_all' do + querying_user['user_group_stats_visibility'] = 'public_show_all' + expect(described_class).to permit(querying_user) + end + + it 'permits if group_stats_visibility is public_agg_show_ind_if_member' do + querying_user['user_group_stats_visibility'] = 'public_agg_show_ind_if_member' + expect(described_class).to permit(querying_user) + end + + it 'permits if group_stats_visibility is public_agg_only' do + querying_user['user_group_stats_visibility'] = 'public_agg_only' + expect(described_class).to permit(querying_user) + end + + context 'private_show_agg_and_ind' do + before(:each) do + querying_user['user_group_stats_visibility'] = 'private_show_agg_and_ind' + end + + it 'forbids if user is not a member' do + expect(described_class).not_to permit(querying_user) + end + + it 'permits if user is a group member' do + querying_user['user_membership'] = membership + expect(described_class).to permit(querying_user) + end + end + + context 'private_agg_only' do + before(:each) do + querying_user['user_group_stats_visibility'] = 'private_agg_only' + end + + it 'forbids if user is not a group member' do + expect(described_class).not_to permit(querying_user) + end + + it 'permits if user is a group member' do + querying_user['user_membership'] = membership + expect(described_class).to permit(querying_user) + end + end + end + + it 'forbids unauthorized users' do + expect(described_class).not_to permit(querying_user) + end + end + + def membership(role_type='group_member') + { 'id' => 123, + 'user_group_id' => 123, + 'user_id' => 123, + 'roles' => [role_type] } + end +end diff --git a/spec/support/auth_header_validator.rb b/spec/support/auth_header_validator.rb new file mode 100644 index 0000000..f48bdb1 --- /dev/null +++ b/spec/support/auth_header_validator.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'returns 403 when authorization header is invalid' do + it 'returns a 403 missing authorization header' do + expected_response = { error: 'Missing Authorization header' } + expect(response.status).to eq(403) + expect(response.body).to eq(expected_response.to_json) + end + + it 'returns a 403 missing authorization header' do + request.headers['Authorization'] = 'asjdhaskdhsa' + expected_response = { error: 'Missing Authorization header' } + expect(response.status).to eq(403) + expect(response.body).to eq(expected_response.to_json) + end +end diff --git a/spec/support/authentication_helpers.rb b/spec/support/authentication_helpers.rb index c234de2..2b41097 100644 --- a/spec/support/authentication_helpers.rb +++ b/spec/support/authentication_helpers.rb @@ -5,6 +5,33 @@ def authenticate!(user_id='9999', is_panoptes_admin: false) allow(controller).to receive(:client).and_return(user_client(user_id, is_panoptes_admin)) end + def authenticate_with_membership!(classification_user_group, memberships, is_panoptes_admin: false) + allow(controller).to receive(:client).and_return(user_client_with_membership(classification_user_group, memberships, is_panoptes_admin)) + end + + def panoptes_application_client + @panoptes_application_client ||= instance_double(Panoptes::Client) + end + + def user_client_with_membership(classification_user_group, memberships, is_panoptes_admin) + return @user_client_with_membership if @user_client_with_membership + + me_hash = { + 'id' => classification_user_group.user_id, + 'login' => 'login', + 'display_name' => 'display_name', + 'admin' => is_panoptes_admin + } + memberships_url = "/memberships?user_id=#{classification_user_group.user_id}&user_group_id=#{classification_user_group.user_group_id}" + + @user_client_with_membership = double(Panoptes::Client, me: me_hash).tap do |client| + allow(client).to receive(:is_a?).with(Panoptes::Client).and_return(true) + allow(client).to receive_message_chain(:panoptes, :get).with(memberships_url).and_return('memberships' => memberships) + end + + @user_client_with_membership + end + def user_client(user_id, is_panoptes_admin) return @user_client if @user_client @@ -16,7 +43,6 @@ def user_client(user_id, is_panoptes_admin) } @user_client = double(Panoptes::Client, me: me_hash).tap do |client| - allow(client).to receive(:is_a?).and_return(false) allow(client).to receive(:is_a?).with(Panoptes::Client).and_return(true) end