diff --git a/app/controllers/api/v2/base_controller.rb b/app/controllers/api/v2/base_controller.rb index 90240acfd..e3e166e9a 100644 --- a/app/controllers/api/v2/base_controller.rb +++ b/app/controllers/api/v2/base_controller.rb @@ -10,13 +10,20 @@ class API::V2::BaseController < ApplicationController def context # new token if api_token.present? - { administrateur_id: api_token.administrateur.id } + api_token.context # web interface (/graphql) give current_administrateur elsif current_administrateur.present? - { administrateur_id: current_administrateur.id } + { + administrateur_id: current_administrateur.id, + procedure_ids: current_administrateur.procedure_ids, + write_access: true + } # old token else - { token: authorization_bearer_token } + { + token: authorization_bearer_token, + write_access: true + } end end diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index 52dacccb6..55f4600b1 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -5,7 +5,7 @@ class APIController < ApplicationController def find_administrateur_for_token(procedure) api_token = APIToken.find_and_verify(authorization_bearer_token, procedure.administrateurs) - if api_token.present? && procedure.administrateurs.include?(api_token.administrateur) + if api_token.present? && api_token.context.fetch(:procedure_ids).include?(procedure.id) api_token.administrateur end end diff --git a/app/graphql/api/v2/context.rb b/app/graphql/api/v2/context.rb index b664f821c..13d0982b6 100644 --- a/app/graphql/api/v2/context.rb +++ b/app/graphql/api/v2/context.rb @@ -49,10 +49,12 @@ class API::V2::Context < GraphQL::Query::Context self[:authorized] ||= {} if self[:authorized][demarche.id].nil? - self[:authorized][demarche.id] = if self[:administrateur_id] - demarche.administrateurs.map(&:id).include?(self[:administrateur_id]) - elsif self[:token] + self[:authorized][demarche.id] = if self[:procedure_ids].present? + self[:procedure_ids].include?(demarche.id) + elsif self[:token].present? APIToken.find_and_verify(self[:token], demarche.administrateurs).present? + else + false end end diff --git a/app/models/api_token.rb b/app/models/api_token.rb index da81e4a41..f40ccc19d 100644 --- a/app/models/api_token.rb +++ b/app/models/api_token.rb @@ -16,6 +16,17 @@ class APIToken < ApplicationRecord include ActiveRecord::SecureToken belongs_to :administrateur, inverse_of: :api_tokens + has_many :procedures, through: :administrateur + + def context + context = { administrateur_id: administrateur_id, write_access: write_access? } + + if full_access? + context.merge procedure_ids: + else + context.merge procedure_ids: procedure_ids & allowed_procedure_ids + end + end # Prefix is made of the first 6 characters of the uuid base64 encoded # it does not leak plain token diff --git a/spec/controllers/api/v2/graphql_controller_spec.rb b/spec/controllers/api/v2/graphql_controller_spec.rb index 6f70743cd..1697c05ce 100644 --- a/spec/controllers/api/v2/graphql_controller_spec.rb +++ b/spec/controllers/api/v2/graphql_controller_spec.rb @@ -1,6 +1,8 @@ describe API::V2::GraphqlController do let(:admin) { create(:administrateur) } - let(:token) { APIToken.generate(admin)[1] } + let(:generated_token) { APIToken.generate(admin) } + let(:api_token) { generated_token.first } + let(:token) { generated_token.second } let(:legacy_token) { APIToken.send(:unpack, token)[:plain_token] } let(:procedure) { create(:procedure, :published, :for_individual, :with_service, administrateurs: [admin]) } let(:dossier) { create(:dossier, :en_construction, :with_individual, procedure: procedure) } @@ -193,6 +195,15 @@ describe API::V2::GraphqlController do expect(gql_errors.first[:message]).to eq("An object of type Demarche was hidden due to permissions") } end + + context 'when procedure is not selected' do + let(:other_procedure) { create(:procedure, administrateurs: [admin]) } + before { api_token.update(allowed_procedure_ids: [other_procedure.id]) } + + it { + expect(gql_errors.first[:message]).to eq("An object of type Demarche was hidden due to permissions") + } + end end describe "demarche" do diff --git a/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb b/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb index ae6749376..2e8552ae8 100644 --- a/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb +++ b/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb @@ -1,6 +1,8 @@ describe API::V2::GraphqlController do let(:admin) { create(:administrateur) } - let(:token) { APIToken.generate(admin)[1] } + let(:generated_token) { APIToken.generate(admin) } + let(:api_token) { generated_token.first } + let(:token) { generated_token.second } let(:legacy_token) { APIToken.send(:unpack, token)[:plain_token] } let(:procedure) { create(:procedure, :published, :for_individual, :with_service, administrateurs: [admin], types_de_champ_public:) } let(:types_de_champ_public) { [] } @@ -210,6 +212,14 @@ describe API::V2::GraphqlController do expect(gql_data[:dossierArchiver][:dossier][:id]).to eq(dossier.to_typed_id) expect(gql_data[:dossierArchiver][:dossier][:archived]).to be_truthy } + + context 'read only token' do + before { api_token.update(write_access: false) } + + it { + expect(gql_data[:dossierArchiver][:errors].first[:message]).to eq('Le jeton utilisé est configuré seulement en lecture') + } + end end context 'dossierPasserEnInstruction' do @@ -262,6 +272,14 @@ describe API::V2::GraphqlController do expect(gql_data[:dossierAccepter][:dossier][:id]).to eq(dossier.to_typed_id) expect(gql_data[:dossierAccepter][:dossier][:state]).to eq('accepte') } + + context 'read only token' do + before { api_token.update(write_access: false) } + + it { + expect(gql_data[:dossierAccepter][:errors].first[:message]).to eq('Le jeton utilisé est configuré seulement en lecture') + } + end end context 'dossierRefuser' do @@ -275,6 +293,14 @@ describe API::V2::GraphqlController do expect(gql_data[:dossierRefuser][:dossier][:id]).to eq(dossier.to_typed_id) expect(gql_data[:dossierRefuser][:dossier][:state]).to eq('refuse') } + + context 'read only token' do + before { api_token.update(write_access: false) } + + it { + expect(gql_data[:dossierRefuser][:errors].first[:message]).to eq('Le jeton utilisé est configuré seulement en lecture') + } + end end context 'dossierClasserSansSuite' do @@ -288,6 +314,14 @@ describe API::V2::GraphqlController do expect(gql_data[:dossierClasserSansSuite][:dossier][:id]).to eq(dossier.to_typed_id) expect(gql_data[:dossierClasserSansSuite][:dossier][:state]).to eq('sans_suite') } + + context 'read only token' do + before { api_token.update(write_access: false) } + + it { + expect(gql_data[:dossierClasserSansSuite][:errors].first[:message]).to eq('Le jeton utilisé est configuré seulement en lecture') + } + end end context 'groupeInstructeurModifier' do diff --git a/spec/graphql/annotation_spec.rb b/spec/graphql/annotation_spec.rb index 17bffffc5..40156db66 100644 --- a/spec/graphql/annotation_spec.rb +++ b/spec/graphql/annotation_spec.rb @@ -5,7 +5,7 @@ RSpec.describe Mutations::DossierModifierAnnotation, type: :graphql do let(:instructeur) { create(:instructeur, followed_dossiers: dossiers) } let(:query) { '' } - let(:context) { { administrateur_id: admin.id } } + let(:context) { { administrateur_id: admin.id, procedure_ids: admin.procedure_ids, write_access: true } } let(:variables) { {} } subject { API::V2::Schema.execute(query, variables: variables, context: context) } diff --git a/spec/graphql/demarche_spec.rb b/spec/graphql/demarche_spec.rb index db60988d0..9c181c6ba 100644 --- a/spec/graphql/demarche_spec.rb +++ b/spec/graphql/demarche_spec.rb @@ -1,6 +1,7 @@ RSpec.describe Types::DemarcheType, type: :graphql do + let(:admin) { create(:administrateur) } let(:query) { '' } - let(:context) { { internal_use: true } } + let(:context) { { procedure_ids: admin.procedure_ids } } let(:variables) { {} } subject { API::V2::Schema.execute(query, variables: variables, context: context) } @@ -10,11 +11,9 @@ RSpec.describe Types::DemarcheType, type: :graphql do describe 'context should correctly preserve demarche authorization state' do let(:query) { DEMARCHE_QUERY } - let(:admin) { create(:administrateur) } let(:procedure) { create(:procedure, administrateurs: [admin]) } let(:other_admin_procedure) { create(:procedure) } - let(:context) { { administrateur_id: admin.id } } let(:variables) { { number: procedure.id } } it do @@ -27,8 +26,8 @@ RSpec.describe Types::DemarcheType, type: :graphql do end describe 'demarche with clone' do - let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :yes_no }]) } - let(:procedure_clone) { procedure.clone(procedure.administrateurs.first, false) } + let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :yes_no }], administrateurs: [admin]) } + let(:procedure_clone) { procedure.clone(admin, false) } let(:query) { DEMARCHE_WITH_CHAMP_DESCRIPTORS_QUERY } let(:variables) { { number: procedure_clone.id } } let(:champ_descriptor_id) { procedure.draft_revision.types_de_champ_public.first.to_typed_id } diff --git a/spec/models/api_token_spec.rb b/spec/models/api_token_spec.rb index 3a895ae8a..3cf2db399 100644 --- a/spec/models/api_token_spec.rb +++ b/spec/models/api_token_spec.rb @@ -11,6 +11,100 @@ describe APIToken, type: :model do expect(api_token.administrateur).to eq(administrateur) expect(api_token.prefix).to eq(packed_token.slice(0, 5)) expect(api_token.version).to eq(3) + expect(api_token.write_access?).to eq(true) + expect(api_token.procedure_ids).to eq([]) + expect(api_token.allowed_procedure_ids).to eq(nil) + expect(api_token.context).to eq(administrateur_id: administrateur.id, procedure_ids: [], write_access: true) + expect(api_token.full_access?).to be_truthy + end + + context 'with read_only' do + before { api_token.update(write_access: false) } + it do + expect(api_token.full_access?).to be_truthy + expect(api_token.context).to eq(administrateur_id: administrateur.id, procedure_ids: [], write_access: false) + end + end + + context 'with procedure' do + let(:procedure) { create(:procedure, administrateurs: [administrateur]) } + before { procedure } + + it do + expect(api_token.procedure_ids).to eq([procedure.id]) + expect(api_token.procedures_to_allow).to eq([procedure]) + expect(api_token.allowed_procedure_ids).to eq(nil) + expect(api_token.context).to eq(administrateur_id: administrateur.id, procedure_ids: [procedure.id], write_access: true) + end + + context 'update with procedure_id' do + let(:procedure) { create(:procedure, administrateurs: [administrateur]) } + let(:other_procedure) { create(:procedure, administrateurs: [administrateur]) } + before { api_token.update(allowed_procedure_ids: [procedure.id]); other_procedure } + + it do + expect(api_token.procedure_ids).to match_array([procedure.id, other_procedure.id]) + expect(api_token.procedures_to_allow).to eq([other_procedure]) + expect(api_token.allowed_procedure_ids).to eq([procedure.id]) + expect(api_token.context).to eq(administrateur_id: administrateur.id, procedure_ids: [procedure.id], write_access: true) + end + end + + context 'update with wrong procedure_id' do + let(:other_administrateur) { create(:administrateur) } + let(:procedure) { create(:procedure, administrateurs: [other_administrateur]) } + before { api_token.update(allowed_procedure_ids: [procedure.id]) } + + it do + expect(api_token.full_access?).to be_falsey + expect(api_token.procedure_ids).to eq([]) + expect(api_token.procedures_to_allow).to eq([]) + expect(api_token.allowed_procedure_ids).to eq([]) + expect(api_token.context).to eq(administrateur_id: administrateur.id, procedure_ids: [], write_access: true) + end + end + + context 'update with destroyed procedure_id' do + let(:procedure) { create(:procedure, administrateurs: [administrateur]) } + before { api_token.update(allowed_procedure_ids: [procedure.id]); procedure.destroy } + + it do + expect(api_token.full_access?).to be_falsey + expect(api_token.procedure_ids).to eq([]) + expect(api_token.procedures_to_allow).to eq([]) + expect(api_token.allowed_procedure_ids).to eq([procedure.id]) + expect(api_token.context).to eq(administrateur_id: administrateur.id, procedure_ids: [], write_access: true) + end + end + + context 'update with detached procedure_id' do + let(:other_procedure) { create(:procedure, administrateurs: [administrateur]) } + let(:procedure) { create(:procedure, administrateurs: [administrateur]) } + before { api_token.update(allowed_procedure_ids: [procedure.id]); other_procedure; administrateur.procedures.delete(procedure) } + + it do + expect(api_token.full_access?).to be_falsey + expect(api_token.procedure_ids).to eq([other_procedure.id]) + expect(api_token.allowed_procedure_ids).to eq([procedure.id]) + expect(api_token.context).to eq(administrateur_id: administrateur.id, procedure_ids: [], write_access: true) + end + end + end + + context 'with procedure and allowed_procedure_ids' do + let(:procedure) { create(:procedure, administrateurs: [administrateur]) } + let(:other_procedure) { create(:procedure, administrateurs: [administrateur]) } + + before do + api_token.update(allowed_procedure_ids: [procedure.id]) + other_procedure + end + + it do + expect(api_token.procedure_ids).to eq([procedure.id, other_procedure.id]) + expect(api_token.allowed_procedure_ids).to eq([procedure.id]) + expect(api_token.context).to eq(administrateur_id: administrateur.id, procedure_ids: [procedure.id], write_access: true) + end end end