diff --git a/app/assets/javascripts/vue.js b/app/assets/javascripts/vue.js index 1394abd16d1c22a72ff984c8475972930003e9c2..f812291c676f39a69a6905da16ff171b60e1cf5c 100644 --- a/app/assets/javascripts/vue.js +++ b/app/assets/javascripts/vue.js @@ -1,2 +1,2 @@ -//= require vue/dist/vue.global.js +//= require vue/dist/vue.global.prod.js //= require vue-draggable-next/dist/vue-draggable-next.global.js diff --git a/app/assets/stylesheets/commons/_block.sass b/app/assets/stylesheets/commons/_block.sass index 573a390e9e1a82760d4a4ab3f93e03cfec8a4c83..4acd08cac2f9e8b41426a464b9c202efc055910c 100644 --- a/app/assets/stylesheets/commons/_block.sass +++ b/app/assets/stylesheets/commons/_block.sass @@ -1,5 +1,5 @@ [v-cloak] - form + form, .app-form display: none [data-v-app] .spinner-border diff --git a/app/controllers/server/universities_controller.rb b/app/controllers/server/universities_controller.rb index db42c8fb802eb74ae03e190555a1b4bc74d9c2df..7993a87cfea8b6c12957c0e7ad198585dba0c135 100644 --- a/app/controllers/server/universities_controller.rb +++ b/app/controllers/server/universities_controller.rb @@ -62,7 +62,7 @@ class Server::UniversitiesController < Server::ApplicationController params.require(:university).permit(:name, :address, :zipcode, :city, :country, :private, :identifier, :logo, :logo_delete, :sms_sender_name, - :has_sso, :sso_target_url, :sso_cert, :sso_name_identifier_format, + :has_sso, :sso_target_url, :sso_cert, :sso_name_identifier_format, :sso_mapping, :invoice_date, :invoice_amount) end end diff --git a/app/models/user/with_omniauth.rb b/app/models/user/with_omniauth.rb index f0de9a849352717c3274fbb4eaea58713ed5bfeb..24d8361368f2e7c1cdceb2bdd808f89df7b45fa1 100644 --- a/app/models/user/with_omniauth.rb +++ b/app/models/user/with_omniauth.rb @@ -5,10 +5,9 @@ module User::WithOmniauth def self.from_omniauth(university, attributes) mapping = university.sso_mapping - email = 'pierreandre.boissinot@noesya.coop' - # email_sso_key = mapping.select { |elmt| elmt['internal_key'] == 'email' }&.first&.dig('sso_key') - email_sso_key = 'email' + # first step: we find the email (we are supposed to have an email mapping) + email_sso_key = mapping.select { |elmt| elmt['internal_key'] == 'email' }&.first&.dig('sso_key') email = attributes.dig(email_sso_key) return unless email email = email.first if email.is_a?(Array) @@ -17,6 +16,47 @@ module User::WithOmniauth user = User.where(university: university, email: email).first_or_create do |u| u.password = "#{Devise.friendly_token[0,20]}!" # meets password complexity requirements end + + # update user data according to mapping & infos provided by SSO + mapping.select { |elmt| elmt['internal_key'] != 'email' }.each do |mapping_element| + user = self.update_data_for_mapping_element(user, mapping_element, attributes) + end + + user.save + user + end + + protected + + def self.update_data_for_mapping_element(user, mapping_element, attributes) + sso_key = mapping_element['sso_key'] + return user if attributes[sso_key].nil? # if not provided by sso, just return + internal_key = mapping_element['internal_key'] + user = self.update_data_for_mapping_element_standard(user, mapping_element, self.get_provided_answer(attributes[sso_key])) + user + end + + def self.update_data_for_mapping_element_standard(user, mapping_element, sso_value) + case mapping_element['internal_key'] + when 'language' + user = self.set_best_id_for(user, mapping_element['type'], sso_value.first) + when 'role' + value = mapping_element['roles'].select { |key, val| val == sso_value.first }.first&.first + user['role'] = value if value + else + user[mapping_element['internal_key']] = sso_value.first + end + user + end + + def self.get_provided_answer(value) + # SAML send an array (even for a single value) where OAuth2 send a string for single values. We harmonize to always get an array + value.is_a?(Array) ? value : [value] + end + + def self.set_best_id_for(user, type, iso) + element_id = eval(type.classify).find_by(iso_code: iso)&.id + user["#{type}_id"] = element_id unless element_id.nil? user end diff --git a/app/views/admin/communication/blocks/edit.html.erb b/app/views/admin/communication/blocks/edit.html.erb index ea57a1e441a8671e0d736fecd7e9064d68ff2385..e126220a12c759bcd08d3d5ee3c88aa013bcc9f3 100644 --- a/app/views/admin/communication/blocks/edit.html.erb +++ b/app/views/admin/communication/blocks/edit.html.erb @@ -6,7 +6,7 @@ %> <div id="app" v-cloak> <div class="spinner-border text-primary" role="status"> - <span class="sr-only">Loading...</span> + <span class="sr-only"><%= t 'loading' %></span> </div> <%= simple_form_for [:admin, @block] do |f| %> <div class="row"> diff --git a/app/views/server/universities/_form.html.erb b/app/views/server/universities/_form.html.erb index c3e2a4743b9860e325cb2746712564c7c1375b42..5ef1508748f0d071e6782559b6453803da92fbbb 100644 --- a/app/views/server/universities/_form.html.erb +++ b/app/views/server/universities/_form.html.erb @@ -28,16 +28,19 @@ </div> </div> + <h3 class="mt-5"><%= t('university.sso') %></h3> <div class="row"> <div class="col-md-6"> - <h3 class="mt-5"><%= t('university.sso') %></h3> <%= f.input :has_sso %> <div id="sso-inputs"> <%= f.input :sso_target_url, required: true %> <%= f.input :sso_cert, required: true %> <%= f.input :sso_name_identifier_format, required: true %> - - <%#= render 'sso_mapping', brand: brand %> + </div> + </div> + <div class="col-md-6"> + <h4 class="mb-4"><%= University.human_attribute_name('sso_mapping') %></h4> + <%= render 'sso_mapping', university: university %> </div> </div> diff --git a/app/views/server/universities/_sso_mapping.html.erb b/app/views/server/universities/_sso_mapping.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..a1fc7362a035003a0e4e5cc843a494d284017d58 --- /dev/null +++ b/app/views/server/universities/_sso_mapping.html.erb @@ -0,0 +1,82 @@ +<% mapping_keys = ['email', 'first_name', 'last_name', 'role', 'mobile_phone', 'language', 'picture_url'] %> + +<%# Include vue.js before call Vue.createApp %> +<%= javascript_include_tag 'vue' %> + +<div id="app" v-cloak> + <div class="spinner-border text-primary" role="status"> + <span class="sr-only"><%= t('loading') %></span> + </div> + + <div class="app-form"> + <draggable :list="fields"> + <div v-for="(field, index) in fields"> + <div class="card"> + <div class="card-header d-flex justify-content-between"> + <a data-bs-toggle="collapse" :href="'#sso_mapping_collapse_' + index "> + {{index + 1}}. {{ field.sso_key }} -> {{ keys[field.internal_key]}} + </a> + <a + v-on:click="fields.splice(fields.indexOf(field), 1)" + title="Remove field"> + <i class="far fa-trash-alt"></i> + </a> + </div> + <div class="card-body collapse pt-0" :id="'sso_mapping_collapse_' + index "> + <hr class="mt-0"> + <div class="form-group"> + <label for="" class="form-control-label"><%= t('university.sso_key') %> <abbr title="required">*</abbr></label> + <input + v-model="field.sso_key" + type="text" class="form-control"> + </div> + <div class="form-group"> + <label for="" class="form-control-label"><%= t('university.internal_key') %> <abbr title="required">*</abbr></label> + <select v-model="field.internal_key" id="" class="form-select" required> + <option v-for="(label, key) in keys" :value="key">{{ label }}</option> + </select> + </div> + <div v-if="field.internal_key === 'role'"> + <hr class="mt-4"> + <% User.roles.keys.each do |role| %> + <div class="form-group"> + <label for="" class="form-label"><%= t("activerecord.attributes.user.roles.#{role}") %></label> + <input v-model="field.roles.<%= role %>" type="text" class="form-control"> + </div> + <% end %> + </div> + </div> + </div> + </div> + </draggable> + + <a v-on:click="fields.push({sso_key: 'key', internal_key: 'email', roles: {}})" class="btn btn-primary btn-sm"> + <%= t('add_field') %> + </a> + </div> + + <textarea name="university[sso_mapping]" id="university_sso_mapping" rows="20" cols="200" class="d-none"> + {{ JSON.stringify(fields) }} + </textarea> + +</div> + +<script> + var app = Vue.createApp({ + components: { + draggable: VueDraggableNext.VueDraggableNext, + }, + data() { + return { + fields: <%= university.sso_mapping.blank? ? '[]' : university.sso_mapping.to_json.html_safe %>, + keys: <%= mapping_keys.map { |key| [key, User.human_attribute_name(key)] }.to_h.to_json.html_safe %> + } + } + }); + + window.addEventListener('load', function(){ + setTimeout(function() { + app.mount('#app'); + }, 1000); + }); +</script> diff --git a/config/locales/en.yml b/config/locales/en.yml index aeaae6cf586c5b579138ff01856c8448b6fc191c..49071105af0eb9cde4fe42461b5a176d54940a54 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -38,6 +38,7 @@ en: one: User other: Users add: Add + add_field: Add field admin: attachment_not_available: Attachment not available dashboard: Dashboard diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 98572ab5182adbebcfee951ea6724522753a471a..ab257fa73827bc04141c59c0edb70995dd5fcf76 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -38,6 +38,7 @@ fr: one: Utilisateur·rice other: Utilisateur·rice·s add: Ajouter + add_field: Ajouter un champ admin: attachment_not_available: Impossible d'accéder à l'élément dashboard: Tableau de bord diff --git a/config/locales/university/en.yml b/config/locales/university/en.yml index 63a83a4e13d8231a460c218b51e5f82d13f3b9b9..8b3633d360145d4a543319a7e49e7a7415508b2a 100644 --- a/config/locales/university/en.yml +++ b/config/locales/university/en.yml @@ -16,6 +16,7 @@ en: public_or_private: Public/private sms_sender_name: SMS sender name sso_cert: Certificate + sso_mapping: Mapping sso_name_identifier_format: Name Identifier Format sso_target_url: Target URL url: URL @@ -125,8 +126,10 @@ en: non_profit: Association government: Government university: + internal_key: Internal Key invoice_informations: Invoice informations person: administrator_roles: Administrator roles taught_programs: Taught programs sso: SSO + sso_key: SSO Key diff --git a/config/locales/university/fr.yml b/config/locales/university/fr.yml index f263aead1db38c3fbd9063474e72394808a2a9e9..049859fb4d3a08af294c74cf74428c0d05e2e548 100644 --- a/config/locales/university/fr.yml +++ b/config/locales/university/fr.yml @@ -16,6 +16,7 @@ fr: public_or_private: Public/privé sms_sender_name: Nom de l'expéditeur SMS sso_cert: Certificat + sso_mapping: Mapping sso_name_identifier_format: Name Identifier Format sso_target_url: URL cible url: 'URL' @@ -125,8 +126,10 @@ fr: non_profit: Association government: Structure gouvernementale university: + internal_key: Clé interne invoice_informations: Données de facturation person: administrator_roles: Rôles administratifs taught_programs: Formations enseignées sso: SSO + sso_key: Clé sur le SSO