2022-11-30 10:06:33 +01:00
|
|
|
|
# == Schema Information
|
|
|
|
|
#
|
|
|
|
|
# Table name: api_tokens
|
|
|
|
|
#
|
2023-02-13 14:22:21 +01:00
|
|
|
|
# id :uuid not null, primary key
|
|
|
|
|
# allowed_procedure_ids :bigint is an Array
|
|
|
|
|
# encrypted_token :string not null
|
|
|
|
|
# name :string not null
|
|
|
|
|
# write_access :boolean default(TRUE), not null
|
|
|
|
|
# version :integer default(3), not null
|
|
|
|
|
# created_at :datetime not null
|
|
|
|
|
# updated_at :datetime not null
|
|
|
|
|
# administrateur_id :bigint not null
|
2022-11-30 10:06:33 +01:00
|
|
|
|
#
|
|
|
|
|
class APIToken < ApplicationRecord
|
|
|
|
|
include ActiveRecord::SecureToken
|
2022-09-28 12:40:44 +02:00
|
|
|
|
|
2022-11-30 10:06:33 +01:00
|
|
|
|
belongs_to :administrateur, inverse_of: :api_tokens
|
2023-02-13 14:57:51 +01:00
|
|
|
|
has_many :procedures, through: :administrateur
|
|
|
|
|
|
2023-02-28 16:33:22 +01:00
|
|
|
|
before_save :check_allowed_procedure_ids_ownership
|
|
|
|
|
|
2023-02-13 14:57:51 +01:00
|
|
|
|
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
|
2022-09-28 12:40:44 +02:00
|
|
|
|
|
2023-02-28 16:33:22 +01:00
|
|
|
|
def full_access?
|
|
|
|
|
allowed_procedure_ids.nil?
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def procedures_to_allow
|
|
|
|
|
procedures.select(:id, :libelle, :path).where.not(id: allowed_procedure_ids || []).order(:libelle)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def allowed_procedures
|
|
|
|
|
if allowed_procedure_ids.present?
|
|
|
|
|
procedures.select(:id, :libelle, :path).where(id: allowed_procedure_ids).order(:libelle)
|
|
|
|
|
else
|
|
|
|
|
[]
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def disallow_procedure(procedure_id)
|
|
|
|
|
allowed_procedure_ids = allowed_procedures.map(&:id) - [procedure_id]
|
|
|
|
|
if allowed_procedure_ids.empty?
|
|
|
|
|
allowed_procedure_ids = nil
|
|
|
|
|
end
|
|
|
|
|
update!(allowed_procedure_ids:)
|
|
|
|
|
end
|
|
|
|
|
|
2022-11-30 10:06:33 +01:00
|
|
|
|
# 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)
|
2022-09-28 12:40:44 +02:00
|
|
|
|
end
|
|
|
|
|
|
2022-11-30 10:06:33 +01:00
|
|
|
|
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
|
2022-09-28 12:40:44 +02:00
|
|
|
|
|
2022-11-30 10:06:33 +01:00
|
|
|
|
def find_and_verify(maybe_packed_token, administrateurs = [])
|
2023-07-10 15:25:02 +02:00
|
|
|
|
token = case unpack(maybe_packed_token)
|
2022-11-30 10:06:33 +01:00
|
|
|
|
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
|
2022-12-07 18:56:06 +01:00
|
|
|
|
find_by(administrateur_id:, version: 2)&.then(&ensure_valid_token(plain_token))
|
2022-11-30 10:06:33 +01:00
|
|
|
|
in { plain_token: } # token v1
|
2022-12-07 18:56:06 +01:00
|
|
|
|
where(administrateur: administrateurs, version: 1).find(&ensure_valid_token(plain_token))
|
2022-11-30 10:06:33 +01:00
|
|
|
|
end
|
2023-07-10 15:25:02 +02:00
|
|
|
|
|
|
|
|
|
# TODO:
|
|
|
|
|
# remove all the not v3 version code
|
|
|
|
|
# when everyone has migrated
|
|
|
|
|
# it should also be a good place in case we need to feature flag old token use
|
|
|
|
|
if token&.version == 3 || Rails.env.test?
|
|
|
|
|
token
|
|
|
|
|
else
|
|
|
|
|
nil
|
|
|
|
|
end
|
2022-11-30 10:06:33 +01:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
|
|
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 "<uuid>;<random token>"
|
|
|
|
|
{ plain_token:, id: }
|
|
|
|
|
else
|
|
|
|
|
{ plain_token: maybe_packed_token }
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
rescue
|
|
|
|
|
{ plain_token: maybe_packed_token }
|
|
|
|
|
end
|
2022-09-28 12:40:44 +02:00
|
|
|
|
|
2022-11-30 10:06:33 +01:00
|
|
|
|
def message_verifier
|
|
|
|
|
Rails.application.message_verifier('api_v2_token')
|
|
|
|
|
end
|
2022-09-28 12:40:44 +02:00
|
|
|
|
|
2022-11-30 10:06:33 +01:00
|
|
|
|
def ensure_valid_token(plain_token)
|
|
|
|
|
-> (api_token) { api_token if BCrypt::Password.new(api_token.encrypted_token) == plain_token }
|
|
|
|
|
end
|
2022-09-28 12:40:44 +02:00
|
|
|
|
end
|
2023-02-28 16:33:22 +01:00
|
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
|
|
def check_allowed_procedure_ids_ownership
|
|
|
|
|
if allowed_procedure_ids.present?
|
|
|
|
|
self.allowed_procedure_ids = allowed_procedures.map(&:id)
|
|
|
|
|
end
|
|
|
|
|
end
|
2022-09-28 12:40:44 +02:00
|
|
|
|
end
|