feat(api_token): add allowed_procedure_ids and write_access

This commit is contained in:
Paul Chavard 2023-02-13 14:57:51 +01:00
parent 85d31632e3
commit 8ee13f1719
9 changed files with 173 additions and 15 deletions

View file

@ -10,13 +10,20 @@ class API::V2::BaseController < ApplicationController
def context def context
# new token # new token
if api_token.present? if api_token.present?
{ administrateur_id: api_token.administrateur.id } api_token.context
# web interface (/graphql) give current_administrateur # web interface (/graphql) give current_administrateur
elsif current_administrateur.present? 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 # old token
else else
{ token: authorization_bearer_token } {
token: authorization_bearer_token,
write_access: true
}
end end
end end

View file

@ -5,7 +5,7 @@ class APIController < ApplicationController
def find_administrateur_for_token(procedure) def find_administrateur_for_token(procedure)
api_token = APIToken.find_and_verify(authorization_bearer_token, procedure.administrateurs) 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 api_token.administrateur
end end
end end

View file

@ -49,10 +49,12 @@ class API::V2::Context < GraphQL::Query::Context
self[:authorized] ||= {} self[:authorized] ||= {}
if self[:authorized][demarche.id].nil? if self[:authorized][demarche.id].nil?
self[:authorized][demarche.id] = if self[:administrateur_id] self[:authorized][demarche.id] = if self[:procedure_ids].present?
demarche.administrateurs.map(&:id).include?(self[:administrateur_id]) self[:procedure_ids].include?(demarche.id)
elsif self[:token] elsif self[:token].present?
APIToken.find_and_verify(self[:token], demarche.administrateurs).present? APIToken.find_and_verify(self[:token], demarche.administrateurs).present?
else
false
end end
end end

View file

@ -16,6 +16,17 @@ class APIToken < ApplicationRecord
include ActiveRecord::SecureToken include ActiveRecord::SecureToken
belongs_to :administrateur, inverse_of: :api_tokens 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 # Prefix is made of the first 6 characters of the uuid base64 encoded
# it does not leak plain token # it does not leak plain token

View file

@ -1,6 +1,8 @@
describe API::V2::GraphqlController do describe API::V2::GraphqlController do
let(:admin) { create(:administrateur) } 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(:legacy_token) { APIToken.send(:unpack, token)[:plain_token] }
let(:procedure) { create(:procedure, :published, :for_individual, :with_service, administrateurs: [admin]) } let(:procedure) { create(:procedure, :published, :for_individual, :with_service, administrateurs: [admin]) }
let(:dossier) { create(:dossier, :en_construction, :with_individual, procedure: procedure) } 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") expect(gql_errors.first[:message]).to eq("An object of type Demarche was hidden due to permissions")
} }
end 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 end
describe "demarche" do describe "demarche" do

View file

@ -1,6 +1,8 @@
describe API::V2::GraphqlController do describe API::V2::GraphqlController do
let(:admin) { create(:administrateur) } 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(:legacy_token) { APIToken.send(:unpack, token)[:plain_token] }
let(:procedure) { create(:procedure, :published, :for_individual, :with_service, administrateurs: [admin], types_de_champ_public:) } let(:procedure) { create(:procedure, :published, :for_individual, :with_service, administrateurs: [admin], types_de_champ_public:) }
let(: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][:id]).to eq(dossier.to_typed_id)
expect(gql_data[:dossierArchiver][:dossier][:archived]).to be_truthy 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 end
context 'dossierPasserEnInstruction' do 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][:id]).to eq(dossier.to_typed_id)
expect(gql_data[:dossierAccepter][:dossier][:state]).to eq('accepte') 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 end
context 'dossierRefuser' do 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][:id]).to eq(dossier.to_typed_id)
expect(gql_data[:dossierRefuser][:dossier][:state]).to eq('refuse') 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 end
context 'dossierClasserSansSuite' do 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][:id]).to eq(dossier.to_typed_id)
expect(gql_data[:dossierClasserSansSuite][:dossier][:state]).to eq('sans_suite') 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 end
context 'groupeInstructeurModifier' do context 'groupeInstructeurModifier' do

View file

@ -5,7 +5,7 @@ RSpec.describe Mutations::DossierModifierAnnotation, type: :graphql do
let(:instructeur) { create(:instructeur, followed_dossiers: dossiers) } let(:instructeur) { create(:instructeur, followed_dossiers: dossiers) }
let(:query) { '' } let(:query) { '' }
let(:context) { { administrateur_id: admin.id } } let(:context) { { administrateur_id: admin.id, procedure_ids: admin.procedure_ids, write_access: true } }
let(:variables) { {} } let(:variables) { {} }
subject { API::V2::Schema.execute(query, variables: variables, context: context) } subject { API::V2::Schema.execute(query, variables: variables, context: context) }

View file

@ -1,6 +1,7 @@
RSpec.describe Types::DemarcheType, type: :graphql do RSpec.describe Types::DemarcheType, type: :graphql do
let(:admin) { create(:administrateur) }
let(:query) { '' } let(:query) { '' }
let(:context) { { internal_use: true } } let(:context) { { procedure_ids: admin.procedure_ids } }
let(:variables) { {} } let(:variables) { {} }
subject { API::V2::Schema.execute(query, variables: variables, context: context) } 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 describe 'context should correctly preserve demarche authorization state' do
let(:query) { DEMARCHE_QUERY } let(:query) { DEMARCHE_QUERY }
let(:admin) { create(:administrateur) }
let(:procedure) { create(:procedure, administrateurs: [admin]) } let(:procedure) { create(:procedure, administrateurs: [admin]) }
let(:other_admin_procedure) { create(:procedure) } let(:other_admin_procedure) { create(:procedure) }
let(:context) { { administrateur_id: admin.id } }
let(:variables) { { number: procedure.id } } let(:variables) { { number: procedure.id } }
it do it do
@ -27,8 +26,8 @@ RSpec.describe Types::DemarcheType, type: :graphql do
end end
describe 'demarche with clone' do describe 'demarche with clone' do
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :yes_no }]) } let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :yes_no }], administrateurs: [admin]) }
let(:procedure_clone) { procedure.clone(procedure.administrateurs.first, false) } let(:procedure_clone) { procedure.clone(admin, false) }
let(:query) { DEMARCHE_WITH_CHAMP_DESCRIPTORS_QUERY } let(:query) { DEMARCHE_WITH_CHAMP_DESCRIPTORS_QUERY }
let(:variables) { { number: procedure_clone.id } } let(:variables) { { number: procedure_clone.id } }
let(:champ_descriptor_id) { procedure.draft_revision.types_de_champ_public.first.to_typed_id } let(:champ_descriptor_id) { procedure.draft_revision.types_de_champ_public.first.to_typed_id }

View file

@ -11,6 +11,100 @@ describe APIToken, type: :model do
expect(api_token.administrateur).to eq(administrateur) expect(api_token.administrateur).to eq(administrateur)
expect(api_token.prefix).to eq(packed_token.slice(0, 5)) expect(api_token.prefix).to eq(packed_token.slice(0, 5))
expect(api_token.version).to eq(3) 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
end end