Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions site/app/clients/abstract_data_pass_api_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
require 'faraday'

class AbstractDataPassAPIClient
protected

def http_connection(&block)
Faraday.new do |conn|
conn.request :retry, max: 5
conn.response :raise_error
conn.response :json
conn.options.timeout = 2
yield(conn) if block
end
end

def client_id
AdminApientreprise.credentials[:datapass_client_id]
end

def client_secret
AdminApientreprise.credentials[:datapass_client_secret]
end
end
21 changes: 21 additions & 0 deletions site/app/clients/data_pass_api_authentication.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# :nocov:
class DataPassAPIAuthentication < AbstractDataPassAPIClient
def access_token
http_connection.post(
auth_url,
URI.encode_www_form(
grant_type: 'client_credentials',
client_id:,
client_secret:,
scope: 'read_authorizations'
),
'Content-Type' => 'application/x-www-form-urlencoded'
).body['access_token']
end

private

def auth_url
"#{DataPass::BASE_URL}/api/oauth/token"
end
end
14 changes: 14 additions & 0 deletions site/app/clients/data_pass_api_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# :nocov:
class DataPassAPIClient < AbstractDataPassAPIClient
def definitions(api)
http_connection.get("#{DataPass::BASE_URL}/api/v1/definitions/#{api}").body
end

protected

def http_connection
super do |conn|
conn.request :authorization, 'Bearer', -> { DataPassAPIAuthentication.new.access_token }
end
end
end
16 changes: 15 additions & 1 deletion site/app/helpers/scope_helper.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
module ScopeHelper
UNKNOWN_SCOPE_GROUP = 'Autres'.freeze

def build_scopes(scopes, api)
scopes_tree = {}
scopes.each do |scope|
Expand All @@ -9,11 +11,23 @@ def build_scopes(scopes, api)
end

def humanize_scope(scope, api)
I18n.t("api_#{api}.tokens.token.scope.#{scope}.label", default: scope.humanize)
entry = ScopeCatalog.for(api).lookup(scope)
scope_display_parts(api, entry, scope).join(' || ')
end

private

def scope_display_parts(api, entry, scope)
name = entry&.dig(:name).presence || scope.humanize
provider = entry&.dig(:provider).presence || UNKNOWN_SCOPE_GROUP
if api == 'api_particulier'
group = entry&.dig(:group).presence || UNKNOWN_SCOPE_GROUP
[provider, group, name]
else
[provider, name]
end
end

def build_scopes_parts(scopes_tree, splitted_scope) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
if splitted_scope.size > 2
scopes_tree[splitted_scope[0]] ||= {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ class APIEntreprise::AuthorizationRequestMailer < APIEntrepriseMailer

include ExternalUrlHelper

helper :scope

%w[
embarquement_demande_refusee
update_embarquement_demande_refusee
Expand All @@ -20,9 +22,8 @@ class APIEntreprise::AuthorizationRequestMailer < APIEntrepriseMailer
update_demande_recue
].each do |method|
send('define_method', method) do |args|
@all_scopes = I18n.t('api_entreprise.tokens.token.scope')
@authorization_request = args[:authorization_request]
@authorization_request_scopes = @authorization_request.scopes.map(&:to_sym).presence
@authorization_request_scopes = @authorization_request.scopes.presence
@authorization_request_datapass_url = datapass_authorization_request_url(@authorization_request)

@full_name_demandeur = @authorization_request.demandeur.full_name
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
class APIParticulier::AuthorizationRequestMailer < APIParticulierMailer
include ExternalUrlHelper

helper :scope

%w[
demande_recue
update_demande_recue
Expand All @@ -16,9 +18,8 @@ class APIParticulier::AuthorizationRequestMailer < APIParticulierMailer
update_embarquement_valide_to_demandeur
].each do |method|
send('define_method', method) do |args|
@all_scopes = I18n.t('api_particulier.tokens.token.scope')
@authorization_request = args[:authorization_request]
@authorization_request_scopes = @authorization_request.scopes.map(&:to_sym).presence
@authorization_request_scopes = @authorization_request.scopes.presence
@authorization_request_datapass_url = datapass_authorization_request_url(@authorization_request)

@full_name_demandeur = @authorization_request.demandeur.full_name
Expand Down
42 changes: 42 additions & 0 deletions site/app/services/scope_catalog.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
class ScopeCatalog
CACHE_TTL = ENV.fetch('DATAPASS_SCOPE_CATALOG_CACHE_TTL_MINUTES', '360').to_i.minutes

def self.for(api)
new(api)
end

def initialize(api)
@api = api
end

def lookup(scope_value)
scopes[scope_value]
end

private

attr_reader :api

def scopes
Rails.cache.fetch(cache_key, expires_in: CACHE_TTL) do
fetch_scopes.tap { |data| Rails.cache.write(stale_cache_key, data, expires_in: nil) }
end
rescue Faraday::Error, TypeError, NoMethodError => e
Sentry.capture_exception(e)
Rails.cache.read(stale_cache_key) || {}
end

def fetch_scopes
DataPassAPIClient.new.definitions(api)['scopes'].to_h do |scope|
[scope['value'], { provider: scope['provider'], group: scope['group'], name: scope['name'] }]
end
end

def cache_key
"data_pass_scope_catalog/#{api}"
end

def stale_cache_key
"#{cache_key}/stale"
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@
🔐 Cette habilitation donne accès aux API suivantes&nbsp;:
</h2>
<ul>
<% @all_scopes.each do |scope_key, scope_data| %>
<% if (@authorization_request_scopes || []).include?(scope_key) %>
<li>
<%= scope_data[:label] %>
</li>
<% end %>
<% @authorization_request_scopes.each do |scope| %>
<li>
<%= humanize_scope(scope, 'api_entreprise') %>
</li>
<% end %>
</ul>
</mj-text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
🔐 Cette habilitation donne accès aux API suivantes&nbsp;:
</h2>
<ul>
<% @all_scopes.each do |scope_key, scope_label| %>
<% if (@authorization_request_scopes || []).include?(scope_key) %>
<li>
<%= scope_label %>
</li>
<% end %>
<% (@authorization_request_scopes || []).each do |scope| %>
<li>
<%= humanize_scope(scope, 'api_particulier') %>
</li>
<% end %>
</ul>
</mj-text>
2 changes: 1 addition & 1 deletion site/app/views/api_particulier/endpoints/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@
<ul class="fr-mb-3w">
<% @endpoint.scopes.each do |scope_name| %>
<li>
<strong><%= humanize_scope(scope_name, 'particulier').split('||').last.strip %></strong>
<strong><%= humanize_scope(scope_name, 'api_particulier').split('||').last.strip %></strong>
<code>(<%= scope_name %>)</code>
</li>
<% end %>
Expand Down
2 changes: 1 addition & 1 deletion site/app/views/shared/authorization_requests/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<div class="fr-mb-2v">
<div class="fr-container fr-p-4v" style="background-color: var(--custom-lightest-blue-light)">
<div class="fr-grid-row fr-grid-row--gutters fr-mb-3w">
<% build_scopes(@authorization_request.scopes, namespace == 'api_entreprise' ? 'entreprise' : 'particulier').each_pair do |key, values| %>
<% build_scopes(@authorization_request.scopes, namespace).each_pair do |key, values| %>
<div class="fr-col">
<div class="fr-card">
<div class="fr-card__body">
Expand Down
1 change: 0 additions & 1 deletion site/config/i18n-tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ ignore_unused:
- '*.sessions.after_logout.*'
- 'api_{entreprise,particulier}.fiches_pratiques_entries.*'
- 'api_{entreprise,particulier}.public_token_magic_links.show.*'
- 'api_{entreprise,particulier}.tokens.token.scope.*'
- 'api_{entreprise,particulier}.documentation_pages.*.{title,sections}'
- 'api_{entreprise,particulier}.token_mailer.*'
- 'api_{entreprise,particulier}.authorization_request_mailer.*'
Expand Down
10 changes: 10 additions & 0 deletions site/config/initializers/api_base_urls.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,13 @@ module APIParticulier
'https://particulier.api.gouv.fr'
end
end

module DataPass
BASE_URL = if Rails.env.sandbox?
'https://sandbox.datapass.api.gouv.fr'
elsif Rails.env.staging? || Rails.env.development?
'https://staging.datapass.api.gouv.fr'
else
'https://datapass.api.gouv.fr'
end
end
29 changes: 29 additions & 0 deletions site/spec/clients/data_pass_api_authentication_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
RSpec.describe DataPassAPIAuthentication do
describe '#access_token' do
subject(:access_token) { described_class.new.access_token }

let(:auth_url) { "#{DataPass::BASE_URL}/api/oauth/token" }

before do
allow(AdminApientreprise).to receive(:credentials).and_return(
datapass_client_id: 'test_client_id',
datapass_client_secret: 'test_client_secret'
)

stub_request(:post, auth_url)
.with(
body: 'grant_type=client_credentials&client_id=test_client_id&client_secret=test_client_secret&scope=read_authorizations',
headers: { 'Content-Type' => 'application/x-www-form-urlencoded' }
)
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: { access_token: 'data_pass_access_token' }.to_json
)
end

it 'requests a token with the read_authorizations scope' do
expect(access_token).to eq('data_pass_access_token')
end
end
end
49 changes: 49 additions & 0 deletions site/spec/clients/data_pass_api_client_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
RSpec.describe DataPassAPIClient do
let(:data_pass_api_authentication) { instance_double(DataPassAPIAuthentication, access_token: 'access_token') }

before do
allow(DataPassAPIAuthentication).to receive(:new).and_return(data_pass_api_authentication)
end

describe '#definitions' do
subject(:definitions) { described_class.new.definitions('api_entreprise') }

let(:url) { "#{DataPass::BASE_URL}/api/v1/definitions/api_entreprise" }

context 'when the API returns a 200' do
let(:payload) do
{
'id' => 'api_entreprise',
'name' => 'API Entreprise',
'scopes' => [
{ 'name' => 'Data', 'value' => 'a_scope', 'group' => 'Group', 'provider' => 'INSEE', 'link' => nil }
]
}
end

before do
stub_request(:get, url)
.with(headers: { 'Authorization' => 'Bearer access_token' })
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: payload.to_json
)
end

it 'returns the parsed definition payload' do
expect(definitions).to eq(payload)
end
end

context 'when the API returns an error status' do
before do
stub_request(:get, url).to_return(status: 500)
end

it 'raises a Faraday error' do
expect { definitions }.to raise_error(Faraday::Error)
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,55 @@
expect(page).to have_text(distance_of_time_in_words(Time.zone.now, banned_token.blacklisted_at))
end

describe 'when DataPass provides scope definitions' do
let!(:scopes) { ['cnaf_quotient_familial'] }

before do
Rails.cache.clear

stub_request(:get, %r{#{Regexp.escape(DataPass::BASE_URL)}/api/v1/definitions/})
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: {
'scopes' => [
{
'value' => 'cnaf_quotient_familial',
'name' => 'Quotient familial CAF & MSA',
'group' => 'API Quotient familial',
'provider' => 'CNAF & MSA'
}
]
}.to_json
)

visit api_particulier_authorization_request_path(id: authorization_request.id)
end

it 'displays the provider and label fetched from DataPass' do
expect(page).to have_text('CNAF & MSA')
expect(page).to have_text('Quotient familial CAF & MSA')
end
end

describe 'when DataPass is unreachable' do
let!(:scopes) { ['cnaf_quotient_familial'] }

before do
Rails.cache.clear

stub_request(:get, %r{#{Regexp.escape(DataPass::BASE_URL)}/api/v1/definitions/})
.to_timeout

visit api_particulier_authorization_request_path(id: authorization_request.id)
end

it 'still renders the page, falling back to a humanized version of the scope code' do
expect(page).to have_current_path(api_particulier_authorization_request_path(id: authorization_request.id), ignore_query: true)
expect(page).to have_text('Cnaf quotient familial')
end
end

describe 'when the user is demandeur' do
describe 'when the token has less than 90 days left' do
it 'displays the button to prolong the token' do
Expand Down
Loading
Loading