diff --git a/app/components/profile/api_token_card_component.rb b/app/components/profile/api_token_card_component.rb new file mode 100644 index 000000000..840cc9bb5 --- /dev/null +++ b/app/components/profile/api_token_card_component.rb @@ -0,0 +1,22 @@ +class Profile::APITokenCardComponent < ApplicationComponent + def initialize(created_api_token: nil, created_packed_token: nil) + @created_api_token = created_api_token + @created_packed_token = created_packed_token + end + + private + + def render? + current_administrateur.present? + end + + def api_and_packed_tokens + current_administrateur.api_tokens.order(:created_at).map do |api_token| + if api_token == @created_api_token && @created_packed_token.present? + [api_token, @created_packed_token] + else + [api_token, nil] + end + end + end +end diff --git a/app/components/profile/api_token_card_component/api_token_card_component.html.haml b/app/components/profile/api_token_card_component/api_token_card_component.html.haml new file mode 100644 index 000000000..b820cf0ce --- /dev/null +++ b/app/components/profile/api_token_card_component/api_token_card_component.html.haml @@ -0,0 +1,12 @@ +.card.no-list{ 'data-turbo': 'true', id: dom_id(current_administrateur, :profil_api_token) } + .card-title Jetons d’identification de l’API (token) + %p Ces jetons sont nécessaire pour effectuer des appels vers l’API de #{APPLICATION_NAME}. + %p Si vous avez déjà des applications qui utilisent un jeton et vous le révoquez, l’accès à l’API sera bloqué pour ces applications. + + = render Dsfr::ListComponent.new do |list| + - api_and_packed_tokens.each do |(api_token, packed_token)| + - list.with_item do + = render Profile::APITokenComponent.new(api_token:, packed_token:) + + %br + = button_to "Créer et afficher un nouveau jeton", api_tokens_path, method: :post, class: "fr-btn fr-btn--secondary" diff --git a/app/components/profile/api_token_component.rb b/app/components/profile/api_token_component.rb new file mode 100644 index 000000000..a584281d9 --- /dev/null +++ b/app/components/profile/api_token_component.rb @@ -0,0 +1,6 @@ +class Profile::APITokenComponent < ApplicationComponent + def initialize(api_token:, packed_token: nil) + @api_token = api_token + @packed_token = packed_token + end +end diff --git a/app/components/profile/api_token_component/api_token_component.html.haml b/app/components/profile/api_token_component/api_token_component.html.haml new file mode 100644 index 000000000..bf2c51dfd --- /dev/null +++ b/app/components/profile/api_token_component/api_token_component.html.haml @@ -0,0 +1,15 @@ +%p + %b= "#{@api_token.name} " + %span.fr-text--sm= @api_token.prefix + +- if @packed_token.present? + .fr-text--sm{ style: "width: 80%; word-break: break-all;" } + - button = render Dsfr::CopyButtonComponent.new(text: @packed_token, title: "Copier le jeton dans le presse-papier", success: "Le jeton a été copié dans le presse-papier") + = "#{@packed_token} #{button}" + + %p Pour des raisons de sécurité, il ne sera plus ré-affiché, notez-le bien. + +- else + %p Pour des raisons de sécurité, nous ne pouvons vous l’afficher que lors de sa création. + += button_to "Révoquer le jeton", api_token_path(@api_token), method: :delete, class: "fr-btn fr-btn--secondary", data: { turbo_confirm: "Confirmez-vous la révocation de ce jeton ? Les applications qui l’utilisent actuellement seront bloquées." } diff --git a/app/controllers/api/v2/base_controller.rb b/app/controllers/api/v2/base_controller.rb index d993c787e..d8a172c3d 100644 --- a/app/controllers/api/v2/base_controller.rb +++ b/app/controllers/api/v2/base_controller.rb @@ -8,15 +8,15 @@ class API::V2::BaseController < ApplicationController private def context - # new token give administrateur_id - if api_token.administrateur? - { administrateur_id: api_token.administrateur_id, token: api_token.token } + # new token + if api_token.present? + { token: authorization_bearer_token, administrateur_id: api_token.administrateur.id } # web interface (/graphql) give current_administrateur elsif current_administrateur.present? { administrateur_id: current_administrateur.id } # old token else - { token: api_token.token } + { token: authorization_bearer_token } end end @@ -24,6 +24,19 @@ class API::V2::BaseController < ApplicationController authorization_bearer_token.present? end + def authenticate_administrateur_from_token + if api_token.present? + @current_user = api_token.administrateur.user + end + end + + def api_token + if @api_token.nil? + @api_token = APIToken.find_and_verify(authorization_bearer_token) || false + end + @api_token + end + def authorization_bearer_token @authorization_bearer_token ||= begin received_token = nil @@ -33,17 +46,4 @@ class API::V2::BaseController < ApplicationController received_token end end - - def authenticate_administrateur_from_token - if api_token.administrateur? - administrateur = Administrateur.includes(:user).find_by(id: api_token.administrateur_id) - if administrateur.valid_api_token?(api_token.token) - @current_user = administrateur.user - end - end - end - - def api_token - @api_token ||= APIToken.new(authorization_bearer_token) - end end diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index 4231b8c8d..52dacccb6 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -4,8 +4,9 @@ class APIController < ApplicationController protected def find_administrateur_for_token(procedure) - procedure.administrateurs.find do |administrateur| - administrateur.valid_api_token?(api_token.token) + api_token = APIToken.find_and_verify(authorization_bearer_token, procedure.administrateurs) + if api_token.present? && procedure.administrateurs.include?(api_token.administrateur) + api_token.administrateur end end @@ -15,10 +16,6 @@ class APIController < ApplicationController request.format = "json" if !request.params[:format] end - def api_token - @api_token ||= APIToken.new(authorization_bearer_token) - end - def authorization_bearer_token params_token.presence || header_token end diff --git a/app/controllers/api_tokens_controller.rb b/app/controllers/api_tokens_controller.rb new file mode 100644 index 000000000..6e859de4a --- /dev/null +++ b/app/controllers/api_tokens_controller.rb @@ -0,0 +1,38 @@ +class APITokensController < ApplicationController + before_action :authenticate_administrateur! + + def create + @api_token, @packed_token = APIToken.generate(current_administrateur) + + respond_to do |format| + format.turbo_stream { render :index } + format.html { redirect_back(fallback_location: profil_path) } + end + end + + def update + @api_token = current_administrateur.api_tokens.find(params[:id]) + @api_token.update!(api_token_params) + + respond_to do |format| + format.turbo_stream { render :index } + format.html { redirect_back(fallback_location: profil_path) } + end + end + + def destroy + @api_token = current_administrateur.api_tokens.find(params[:id]) + @api_token.destroy + + respond_to do |format| + format.turbo_stream { render :index } + format.html { redirect_back(fallback_location: profil_path) } + end + end + + private + + def api_token_params + params.require(:api_token).permit(:name) + end +end diff --git a/app/controllers/users/profil_controller.rb b/app/controllers/users/profil_controller.rb index ebd75c278..2e039d025 100644 --- a/app/controllers/users/profil_controller.rb +++ b/app/controllers/users/profil_controller.rb @@ -1,18 +1,12 @@ module Users class ProfilController < UserController before_action :ensure_update_email_is_authorized, only: :update_email - before_action :find_transfers, only: [:show, :renew_api_token] + before_action :find_transfers, only: [:show] def show @france_connect_informations = FranceConnectInformation.where(user: current_user) end - def renew_api_token - @token = current_administrateur.renew_api_token - flash.now.notice = 'Votre jeton a été regénéré.' - render :show - end - def update_email requested_user = User.find_by(email: requested_email) if requested_user.present? && current_user.ask_for_merge(requested_user) diff --git a/app/graphql/api/v2/context.rb b/app/graphql/api/v2/context.rb index 26151a80e..431b877e9 100644 --- a/app/graphql/api/v2/context.rb +++ b/app/graphql/api/v2/context.rb @@ -23,15 +23,11 @@ class API::V2::Context < GraphQL::Query::Context # We are caching authorization logic because it is called for each node # of the requested graph and can be expensive. Context is reset per request so it is safe. self[:authorized] ||= Hash.new do |hash, demarche_id| - # Compute the hash value dynamically when first requested - authorized_administrateur = demarche.administrateurs.find do |administrateur| - if self[:token] - administrateur.valid_api_token?(self[:token]) - else - administrateur.id == self[:administrateur_id] - end + hash[demarche_id] = if self[:token] + APIToken.find_and_verify(self[:token], demarche.administrateurs).present? + elsif self[:administrateur_id] + demarche.administrateurs.map(&:id).include?(self[:administrateur_id]) end - hash[demarche_id] = authorized_administrateur.present? end self[:authorized][demarche.id] diff --git a/app/models/administrateur.rb b/app/models/administrateur.rb index 27114acdb..63a4d369c 100644 --- a/app/models/administrateur.rb +++ b/app/models/administrateur.rb @@ -3,15 +3,12 @@ # Table name: administrateurs # # id :integer not null, primary key -# active :boolean default(FALSE) # encrypted_token :string # created_at :datetime # updated_at :datetime # user_id :bigint not null # class Administrateur < ApplicationRecord - include ActiveRecord::SecureToken - self.ignored_columns = [:active] UNUSED_ADMIN_THRESHOLD = 6.months @@ -20,6 +17,7 @@ class Administrateur < ApplicationRecord has_many :administrateurs_procedures has_many :procedures, through: :administrateurs_procedures has_many :services + has_many :api_tokens, inverse_of: :administrateur, dependent: :destroy belongs_to :user @@ -56,19 +54,6 @@ class Administrateur < ApplicationRecord self.inactive.find(id) end - def renew_api_token - api_token = Administrateur.generate_unique_secure_token - encrypted_token = BCrypt::Password.create(api_token) - update(encrypted_token: encrypted_token) - APIToken.signe(id, api_token) - end - - def valid_api_token?(api_token) - BCrypt::Password.new(encrypted_token) == api_token - rescue BCrypt::Errors::InvalidHash - false - end - def registration_state if user.active? 'Actif' diff --git a/app/models/api_token.rb b/app/models/api_token.rb index 5b2d01d3f..8ee587620 100644 --- a/app/models/api_token.rb +++ b/app/models/api_token.rb @@ -1,27 +1,85 @@ -class APIToken - attr_reader :administrateur_id, :token +# == Schema Information +# +# Table name: api_tokens +# +# id :uuid not null, primary key +# encrypted_token :string not null +# name :string not null +# version :integer default(3), not null +# created_at :datetime not null +# updated_at :datetime not null +# administrateur_id :bigint not null +# +class APIToken < ApplicationRecord + include ActiveRecord::SecureToken - def initialize(token) - @token = token - verify! + belongs_to :administrateur, inverse_of: :api_tokens + + # Prefix is made of the first 6 characters of the uuid base64 encoded + # it does not leak plain token + def prefix + Base64.urlsafe_encode64(id).slice(0, 5) end - def administrateur? - administrateur_id.present? - end + class << self + def generate(administrateur) + plain_token = generate_unique_secure_token + encrypted_token = BCrypt::Password.create(plain_token) + api_token = create!(administrateur:, encrypted_token:, name: Date.today.strftime('Jeton d’API généré le %d/%m/%Y')) + packed_token = Base64.urlsafe_encode64([api_token.id, plain_token].join(';')) + [api_token, packed_token] + end - def self.message_verifier - Rails.application.message_verifier('api_v2_token') - end + def find_and_verify(maybe_packed_token, administrateurs = []) + case unpack(maybe_packed_token) + in { plain_token:, id: } # token v3 + find_by(id:, version: 3)&.then(&ensure_valid_token(plain_token)) + in { plain_token:, administrateur_id: } # token v2 + # the migration to the APIToken model set `version: 1` for all the v1 and v2 token + # this is the only place where we can fix the version + where(administrateur_id:, version: 1).update_all(version: 2) # update to v2 + find_by(administrateur_id:, version: 2)&.then(&ensure_valid_token(plain_token)) || + find_with_administrateur_encrypted_token(plain_token, administrateurs) # before migration + in { plain_token: } # token v1 + where(administrateur: administrateurs, version: 1).find(&ensure_valid_token(plain_token)) || + find_with_administrateur_encrypted_token(plain_token, administrateurs) # before migration + end + end - def self.signe(administrateur_id, token) - message_verifier.generate([administrateur_id, token]) - end + private - private + # FIXME remove after migration + def find_with_administrateur_encrypted_token(plain_token, administrateurs) + administrateurs + .lazy + .filter { _1.encrypted_token.present? } + .map { APIToken.new(administrateur: _1, encrypted_token: _1.encrypted_token, version: 1) } + .find(&ensure_valid_token(plain_token)) + end - def verify! - @administrateur_id, @token = self.class.message_verifier.verified(@token) || [nil, @token] - rescue + UUID_SIZE = SecureRandom.uuid.size + def unpack(maybe_packed_token) + case message_verifier.verified(maybe_packed_token) + in [administrateur_id, plain_token] + { plain_token:, administrateur_id: } + else + case Base64.urlsafe_decode64(maybe_packed_token).split(';') + in [id, plain_token] if id.size == UUID_SIZE # valid format ";" + { plain_token:, id: } + else + { plain_token: maybe_packed_token } + end + end + rescue + { plain_token: maybe_packed_token } + end + + def message_verifier + Rails.application.message_verifier('api_v2_token') + end + + def ensure_valid_token(plain_token) + -> (api_token) { api_token if BCrypt::Password.new(api_token.encrypted_token) == plain_token } + end end end diff --git a/app/views/api_tokens/index.turbo_stream.haml b/app/views/api_tokens/index.turbo_stream.haml new file mode 100644 index 000000000..1ea8888ff --- /dev/null +++ b/app/views/api_tokens/index.turbo_stream.haml @@ -0,0 +1,2 @@ += turbo_stream.morph dom_id(current_administrateur, :profil_api_token) do + = render Profile::APITokenCardComponent.new created_api_token: @api_token, created_packed_token: @packed_token diff --git a/app/views/users/profil/show.html.haml b/app/views/users/profil/show.html.haml index 40503da58..0f41d01c5 100644 --- a/app/views/users/profil/show.html.haml +++ b/app/views/users/profil/show.html.haml @@ -59,25 +59,7 @@ - @waiting_transfers.each do |email, nb_dossier| %li= t('.one_waiting_transfer', email: email, count: nb_dossier) - - if current_administrateur.present? - .card - .card-title Jeton d’identification de l’API (token) - %p Ce jeton est nécessaire pour effectuer des appels vers l’API de #{APPLICATION_NAME}. - - - if defined?(@token) - %p Jeton : #{@token} - %p Pour des raisons de sécurité, ce jeton ne sera plus ré-affiché, notez-le bien. - - - else - %p Pour des raisons de sécurité, nous ne pouvons vous l’afficher que lors de sa génération. - %p Attention, si vous avez déjà des applications qui utilisent votre jeton, le regénérer bloquera leurs accès à l’API. - - = link_to "Regénérer et afficher mon jeton", - renew_api_token_path, - method: :post, - class: "fr-btn fr-btn--secondary", - data: { confirm: "Confirmez-vous la regénération de votre jeton ? Les applications qui l’utilisent actuellement seront bloquées.", - disable: true } + = render Profile::APITokenCardComponent.new - if @france_connect_informations.present? .card diff --git a/config/routes.rb b/config/routes.rb index d745204a6..7d0f3a709 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -173,6 +173,7 @@ Rails.application.routes.draw do resources :attachments, only: [:show, :destroy] resources :recherche, only: [:index] + resources :api_tokens, only: [:create, :update, :destroy] get "patron" => "root#patron" get "suivi" => "root#suivi" @@ -306,9 +307,6 @@ Rails.application.routes.draw do get 'demarches' => 'demarches#index' get 'profil' => 'profil#show' - post 'renew-api-token' => 'profil#renew_api_token' - # allow refresh 'renew api token' page - get 'renew-api-token' => redirect('/profil') patch 'update_email' => 'profil#update_email' post 'transfer_all_dossiers' => 'profil#transfer_all_dossiers' post 'accept_merge' => 'profil#accept_merge' diff --git a/db/migrate/20221129104327_create_api_tokens.rb b/db/migrate/20221129104327_create_api_tokens.rb new file mode 100644 index 000000000..836963063 --- /dev/null +++ b/db/migrate/20221129104327_create_api_tokens.rb @@ -0,0 +1,11 @@ +class CreateAPITokens < ActiveRecord::Migration[6.1] + def change + create_table :api_tokens, id: :uuid do |t| + t.references :administrateur, null: false, foreign_key: true + t.string :encrypted_token, null: false + t.string :name, null: false + t.integer :version, null: false, default: 3 + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index ef138ebf4..4234f4f8c 100644 --- a/db/schema.rb +++ b/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: 2022_12_01_091658) do +ActiveRecord::Schema.define(version: 2022_12_05_144624) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" @@ -86,6 +86,16 @@ ActiveRecord::Schema.define(version: 2022_12_01_091658) do t.index ["procedure_id"], name: "index_administrateurs_procedures_on_procedure_id" end + create_table "api_tokens", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.bigint "administrateur_id", null: false + t.datetime "created_at", precision: 6, null: false + t.string "encrypted_token", null: false + t.string "name", null: false + t.datetime "updated_at", precision: 6, null: false + t.integer "version", default: 3, null: false + t.index ["administrateur_id"], name: "index_api_tokens_on_administrateur_id" + end + create_table "archives", force: :cascade do |t| t.datetime "created_at", precision: 6, null: false t.string "job_status", null: false @@ -928,6 +938,7 @@ ActiveRecord::Schema.define(version: 2022_12_01_091658) do add_foreign_key "administrateurs_instructeurs", "instructeurs" add_foreign_key "administrateurs_procedures", "administrateurs" add_foreign_key "administrateurs_procedures", "procedures" + add_foreign_key "api_tokens", "administrateurs" add_foreign_key "archives_groupe_instructeurs", "archives" add_foreign_key "archives_groupe_instructeurs", "groupe_instructeurs" add_foreign_key "assign_tos", "groupe_instructeurs" diff --git a/lib/tasks/deployment/20221129162903_migrate_api_tokens.rake b/lib/tasks/deployment/20221129162903_migrate_api_tokens.rake new file mode 100644 index 000000000..d0bb72c1b --- /dev/null +++ b/lib/tasks/deployment/20221129162903_migrate_api_tokens.rake @@ -0,0 +1,30 @@ +namespace :after_party do + desc 'Deployment task: migrate_api_tokens' + task migrate_api_tokens: :environment do + puts "Running deploy task 'migrate_api_tokens'" + + administrateurs = Administrateur + .where.not(encrypted_token: nil) + .where.missing(:api_tokens) + + progress = ProgressReport.new(administrateurs.count) + + administrateurs.find_each do |administrateur| + administrateur.transaction do + administrateur + .api_tokens + .create!(name: administrateur.updated_at.strftime('Jeton d’API généré le %d/%m/%Y'), + encrypted_token: administrateur.encrypted_token, + version: 1) + administrateur.update_column(:encrypted_token, nil) + end + progress.inc + end + progress.finish + + # Update task as completed. If you remove the line below, the task will + # run with every deploy (or every time you call after_party:run). + AfterParty::TaskRecord + .create version: AfterParty::TaskRecorder.new(__FILE__).timestamp + end +end diff --git a/spec/controllers/api/v1/dossiers_controller_spec.rb b/spec/controllers/api/v1/dossiers_controller_spec.rb index 7f080e112..81f0f02b7 100644 --- a/spec/controllers/api/v1/dossiers_controller_spec.rb +++ b/spec/controllers/api/v1/dossiers_controller_spec.rb @@ -1,6 +1,6 @@ describe API::V1::DossiersController do let(:admin) { create(:administrateur) } - let(:token) { admin.renew_api_token } + let(:token) { APIToken.generate(admin)[1] } let(:procedure) { create(:procedure, :with_type_de_champ, :with_type_de_champ_private, administrateur: admin) } let(:wrong_procedure) { create(:procedure) } diff --git a/spec/controllers/api/v1/procedures_controller_spec.rb b/spec/controllers/api/v1/procedures_controller_spec.rb index 2f7e238da..88d8676ae 100644 --- a/spec/controllers/api/v1/procedures_controller_spec.rb +++ b/spec/controllers/api/v1/procedures_controller_spec.rb @@ -1,6 +1,6 @@ describe API::V1::ProceduresController, type: :controller do let!(:admin) { create(:administrateur, :with_api_token) } - let!(:token) { admin.renew_api_token } + let!(:token) { APIToken.generate(admin)[1] } it { expect(described_class).to be < APIController } diff --git a/spec/controllers/api/v2/graphql_controller_spec.rb b/spec/controllers/api/v2/graphql_controller_spec.rb index f7e21ec82..39c809b50 100644 --- a/spec/controllers/api/v2/graphql_controller_spec.rb +++ b/spec/controllers/api/v2/graphql_controller_spec.rb @@ -1,7 +1,7 @@ describe API::V2::GraphqlController do let(:admin) { create(:administrateur) } - let(:token) { admin.renew_api_token } - let(:legacy_token) { APIToken.new(token).token } + let(:token) { APIToken.generate(admin)[1] } + 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) } let(:dossier1) { create(:dossier, :en_construction, :with_individual, procedure: procedure, en_construction_at: 1.day.ago) } @@ -108,6 +108,7 @@ describe API::V2::GraphqlController do before do request.env['HTTP_AUTHORIZATION'] = authorization_header + admin.api_tokens.first.update(version: 1) end it "returns the demarche" do @@ -141,7 +142,7 @@ describe API::V2::GraphqlController do context "when the token is revoked" do before do - admin.update(encrypted_token: nil) + admin.api_tokens.destroy_all end it { 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 674670b0e..cff2fabab 100644 --- a/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb +++ b/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb @@ -1,7 +1,7 @@ describe API::V2::GraphqlController do let(:admin) { create(:administrateur) } - let(:token) { admin.renew_api_token } - let(:legacy_token) { APIToken.new(token).token } + let(:token) { APIToken.generate(admin)[1] } + 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) } let(:dossier1) { create(:dossier, :en_construction, :with_individual, procedure: procedure, en_construction_at: 1.day.ago) } diff --git a/spec/controllers/api_controller_spec.rb b/spec/controllers/api_controller_spec.rb index f83116629..fb839d925 100644 --- a/spec/controllers/api_controller_spec.rb +++ b/spec/controllers/api_controller_spec.rb @@ -12,7 +12,7 @@ describe APIController, type: :controller do end context 'when the admin has a token' do - let!(:token) { admin.renew_api_token } + let!(:token) { APIToken.generate(admin)[1] } context 'and the token is given by params' do before { controller.params[:token] = token } diff --git a/spec/controllers/users/profil_controller_spec.rb b/spec/controllers/users/profil_controller_spec.rb index 8a43e6389..ab39e8509 100644 --- a/spec/controllers/users/profil_controller_spec.rb +++ b/spec/controllers/users/profil_controller_spec.rb @@ -31,22 +31,6 @@ describe Users::ProfilController, type: :controller do end end - describe 'POST #renew_api_token' do - let(:administrateur) { create(:administrateur) } - - before { sign_in(administrateur.user) } - - before do - allow(administrateur).to receive(:renew_api_token) - allow(controller).to receive(:current_administrateur) { administrateur } - post :renew_api_token - end - - it { expect(administrateur).to have_received(:renew_api_token) } - it { expect(response.status).to render_template(:show) } - it { expect(flash.notice).to eq('Votre jeton a été regénéré.') } - end - describe 'PATCH #update_email' do context 'when email is same as user' do it 'fails' do diff --git a/spec/factories/administrateur.rb b/spec/factories/administrateur.rb index 479ee824c..f24db4c1b 100644 --- a/spec/factories/administrateur.rb +++ b/spec/factories/administrateur.rb @@ -18,7 +18,7 @@ FactoryBot.define do trait :with_api_token do after(:create) do |admin| - admin.renew_api_token + APIToken.generate(admin) end end diff --git a/spec/models/administrateur_spec.rb b/spec/models/administrateur_spec.rb index eab6cfe93..851d871a2 100644 --- a/spec/models/administrateur_spec.rb +++ b/spec/models/administrateur_spec.rb @@ -5,21 +5,6 @@ describe Administrateur, type: :model do it { is_expected.to have_and_belong_to_many(:instructeurs) } end - describe "#renew_api_token" do - let(:administrateur) { create(:administrateur) } - let!(:token) { administrateur.renew_api_token } - let(:encrypted_token) { BCrypt::Password.new(administrateur.encrypted_token) } - let(:base_token) { APIToken.new(token).token } - - it { expect(encrypted_token).to eq(base_token) } - - context 'when it s called twice' do - let!(:new_token) { administrateur.renew_api_token } - - it { expect(new_token).not_to eq(token) } - end - end - describe "#can_be_deleted?" do subject { administrateur.can_be_deleted? } diff --git a/spec/models/api_token_spec.rb b/spec/models/api_token_spec.rb new file mode 100644 index 000000000..8f96edc3f --- /dev/null +++ b/spec/models/api_token_spec.rb @@ -0,0 +1,178 @@ +describe APIToken, type: :model do + let(:administrateur) { create(:administrateur) } + let(:api_token_and_packed_token) { APIToken.generate(administrateur) } + let(:api_token) { api_token_and_packed_token.first } + let(:packed_token) { api_token_and_packed_token.second } + let(:plain_token) { APIToken.send(:unpack, packed_token)[:plain_token] } + let(:packed_token_v2) { APIToken.send(:message_verifier).generate([administrateur.id, plain_token]) } + + describe '#generate' do + it 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) + end + end + + describe '#find_and_verify' do + let(:result) { APIToken.find_and_verify(token, administrateurs) } + let(:token) { packed_token } + let(:administrateurs) { [administrateur] } + + context 'without administrateur' do + let(:administrateurs) { [] } + + context 'with packed token' do + it { expect(result).to be_truthy } + end + + context 'with packed token v2' do + before { api_token.update(version: 2) } + let(:token) { packed_token_v2 } + it { expect(result).to be_truthy } + end + + context 'with plain token' do + before { api_token.update(version: 1) } + let(:token) { plain_token } + it { expect(result).to be_falsey } + end + end + + context 'with destroyed token' do + before { api_token.destroy } + + context 'with packed token' do + it { expect(result).to be_falsey } + end + + context 'with packed token v2' do + let(:token) { packed_token_v2 } + it { expect(result).to be_falsey } + end + + context 'with plain token' do + let(:token) { plain_token } + it { expect(result).to be_falsey } + end + end + + context 'with destroyed administrateur' do + before { api_token.administrateur.destroy } + let(:administrateurs) { [] } + + context 'with packed token' do + it { expect(result).to be_falsey } + end + + context 'with packed token v2' do + let(:token) { packed_token_v2 } + it { expect(result).to be_falsey } + end + + context 'with plain token' do + let(:token) { plain_token } + it { expect(result).to be_falsey } + end + end + + context 'with other administrateur' do + let(:other_administrateur) { create(:administrateur, :with_api_token) } + let(:administrateurs) { [other_administrateur] } + + context 'with packed token' do + it { expect(result).to be_truthy } + end + + context 'with packed token v2' do + before { api_token.update(version: 2) } + + let(:token) { packed_token_v2 } + it { expect(result).to be_truthy } + end + + context 'with plain token' do + before { api_token.update(version: 1) } + + let(:token) { plain_token } + it { expect(result).to be_falsey } + end + end + + context 'with many administrateurs' do + let(:other_administrateur) { create(:administrateur, :with_api_token) } + let(:other_api_token_and_packed_token) { APIToken.generate(other_administrateur) } + let(:other_api_token) { other_api_token_and_packed_token.first } + let(:other_packed_token) { other_api_token_and_packed_token.second } + let(:other_plain_token) { APIToken.send(:unpack, other_packed_token)[:plain_token] } + let(:administrateurs) { [administrateur, other_administrateur] } + + context 'with plain token' do + before do + api_token.update(version: 1) + other_api_token.update(version: 1) + end + + let(:token) { plain_token } + it { expect(result).to be_truthy } + + context 'with other plain token' do + let(:token) { other_plain_token } + it { expect(result).to be_truthy } + end + end + + context 'with plain token (before migration)' do + before do + administrateur.update(encrypted_token: api_token.encrypted_token) + other_administrateur.update(encrypted_token: other_api_token.encrypted_token) + api_token.destroy + other_api_token.destroy + end + + let(:token) { plain_token } + it { expect(result).to be_truthy } + + context 'with other plain token' do + let(:token) { other_plain_token } + it { expect(result).to be_truthy } + end + end + end + + context 'with packed token' do + it { expect(result).to be_truthy } + end + + context 'with packed token v2' do + before { api_token.update(version: 2) } + + let(:token) { packed_token_v2 } + it { expect(result).to be_truthy } + end + + context 'with plain token' do + before { api_token.update(version: 1) } + + let(:token) { plain_token } + it { expect(result).to be_truthy } + end + + context 'with plain token (before migration)' do + before do + administrateur.update(encrypted_token: api_token.encrypted_token) + api_token.destroy + end + + let(:token) { plain_token } + it { expect(result).to be_truthy } + end + + context "with valid garbage base64" do + before { api_token.update(version: 1, encrypted_token: BCrypt::Password.create(token)) } + + let(:token) { "R5dAqE7nMxfMp93PcuuevDtn" } + it { expect(result).to be_truthy } + end + end +end