diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 26efb189a..0f4326c20 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -21,4 +21,13 @@ def emergency_message(emergency_message, user, lang) mail(from: user.university.mail_from[:full], to: user.email, subject: subject) end + def website_invalid_access_token(website, user) + @website = website + merge_with_university_infos(@website.university, {}) + @url = edit_admin_communication_website_url(@website) + I18n.locale = user.language.iso_code + subject = t('mailers.notifications.website_invalid_access_token.subject', website: website) + mail(from: user.university.mail_from[:full], to: user.email, subject: subject) + end + end diff --git a/app/models/communication/website.rb b/app/models/communication/website.rb index f9dc4898c..39f1f18be 100644 --- a/app/models/communication/website.rb +++ b/app/models/communication/website.rb @@ -49,6 +49,7 @@ class Communication::Website < ApplicationRecord include WithGitRepository include WithImport include WithLanguages + include WithManagers include WithProgramCategories include WithReferences include WithSpecialPages diff --git a/app/models/communication/website/with_git_repository.rb b/app/models/communication/website/with_git_repository.rb index 43a5d6c14..93abed016 100644 --- a/app/models/communication/website/with_git_repository.rb +++ b/app/models/communication/website/with_git_repository.rb @@ -38,6 +38,16 @@ def destroy_obsolete_git_files end handle_asynchronously :destroy_obsolete_git_files, queue: :default + def invalidate_access_token! + # Nullify the expired token + update_column :access_token, nil + # Notify admins and website managers managing this website. + users_to_notify = university.users.admin + university.users.website_manager.where(id: manager_ids) + users_to_notify.each do |user| + NotificationMailer.website_invalid_access_token(self, user).deliver_later + end + end + # Le website devient data/website.yml # Les configs héritent du modèle website et s'exportent en différents fichiers def exportable_to_git? diff --git a/app/models/communication/website/with_managers.rb b/app/models/communication/website/with_managers.rb new file mode 100644 index 000000000..c98cf2c21 --- /dev/null +++ b/app/models/communication/website/with_managers.rb @@ -0,0 +1,11 @@ +module Communication::Website::WithManagers + extend ActiveSupport::Concern + + included do + has_and_belongs_to_many :managers, + class_name: 'User', + join_table: :communication_websites_users, + foreign_key: :communication_website_id, + association_foreign_key: :user_id + end +end diff --git a/app/services/git/providers/abstract.rb b/app/services/git/providers/abstract.rb index ceb8a3423..9aa897ec7 100644 --- a/app/services/git/providers/abstract.rb +++ b/app/services/git/providers/abstract.rb @@ -1,11 +1,12 @@ class Git::Providers::Abstract - attr_reader :endpoint, :branch, :access_token, :repository - - def initialize(endpoint, branch, access_token, repository) - @endpoint = endpoint - @branch = branch - @access_token = access_token - @repository = repository + attr_reader :git_repository, :endpoint, :branch, :access_token, :repository + + def initialize(git_repository) + @git_repository = git_repository + @endpoint = git_repository.website.git_endpoint + @branch = git_repository.website.git_branch + @access_token = git_repository.website.access_token + @repository = git_repository.website.repository end def valid? diff --git a/app/services/git/providers/github.rb b/app/services/git/providers/github.rb index 58dae24af..882b936b0 100644 --- a/app/services/git/providers/github.rb +++ b/app/services/git/providers/github.rb @@ -86,6 +86,17 @@ def git_sha(path) nil end + def valid? + return false unless super + begin + client.repository(repository) + true + rescue Octokit::Unauthorized + git_repository.website.invalidate_access_token! + false + end + end + protected def client diff --git a/app/services/git/providers/gitlab.rb b/app/services/git/providers/gitlab.rb index 39c928078..44cdfe59c 100644 --- a/app/services/git/providers/gitlab.rb +++ b/app/services/git/providers/gitlab.rb @@ -63,6 +63,17 @@ def git_sha(path) sha end + def valid? + return false unless super + begin + client.project(repository) + true + rescue Gitlab::Error::Unauthorized + git_repository.website.invalidate_access_token! + false + end + end + def branch super.present? ? super : 'main' @@ -76,7 +87,7 @@ def endpoint end def client - @client ||= Gitlab.client( + @client ||= Gitlab.client( endpoint: endpoint, private_token: access_token ) diff --git a/app/services/git/repository.rb b/app/services/git/repository.rb index 7863c6de0..9ba136cf2 100644 --- a/app/services/git/repository.rb +++ b/app/services/git/repository.rb @@ -48,10 +48,7 @@ def valid? protected def provider - @provider ||= provider_class.new website&.git_endpoint, - website&.git_branch, - website&.access_token, - website&.repository + @provider ||= provider_class.new self end def provider_class diff --git a/app/views/mailers/notifications/website_invalid_access_token.html.erb b/app/views/mailers/notifications/website_invalid_access_token.html.erb new file mode 100644 index 000000000..dc65ffc75 --- /dev/null +++ b/app/views/mailers/notifications/website_invalid_access_token.html.erb @@ -0,0 +1,3 @@ +

<%= t('mailers.notifications.website_invalid_access_token.text_line_1_html', website: @website) %>

+

<%= t('mailers.notifications.website_invalid_access_token.text_line_2_html', url: @url) %>

+

<%= t('mailers.yours') %>

diff --git a/config/locales/en.yml b/config/locales/en.yml index 3131d72c2..71db7a90c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -141,7 +141,7 @@ en: successfully_quit_html: "%{model} successfully quit %{target}." successfully_removed_html: "%{model} was successfully removed." successfully_updated_html: "%{model} was successfully updated." - summary: + summary: label: Summary hint: A short text, like a heading for an article. Don't write all the content here, there are blocks for that. users_alerts: @@ -269,6 +269,10 @@ en: text_line_3_html: "Number of lines in the file: %{number}." text_error_msg: "Line %{line}: %{error}" text_errors_title: "Some errors have occured:" + website_invalid_access_token: + subject: "Expired access token for \"%{website}\"" + text_line_1_html: "The access token used for the website \"%{website}\" has expired and does not allow the website to be updated anymore." + text_line_2_html: "To solve this issue, please fill in a new access token by clicking here." yours: Yours. menu: admin: Admin @@ -307,7 +311,7 @@ en: delivered: Your message has been sent filters: Filters target: Target - users: + users: one: "%{count} user" other: "%{count} users" websites: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 22a120ba2..0f854167f 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -141,7 +141,7 @@ fr: successfully_quit_html: "%{model} a bien quitté %{target}." successfully_removed_html: "%{model} a bien été retiré(e)." successfully_updated_html: "%{model} a bien été mis(e) à jour." - summary: + summary: label: Résumé hint: Un texte court, comme un chapô pour un article. Ne mettez pas tout le contenu ici, pour ça, il y a les blocs ! users_alerts: @@ -269,6 +269,10 @@ fr: text_line_3_html: "Nombre de lignes traitées : %{number}." text_error_msg: "Ligne %{line} : %{error}" text_errors_title: "Des erreurs sont survenues :" + website_invalid_access_token: + subject: Jeton d'accès expiré pour « %{website} » + text_line_1_html: Le jeton d'accès utilisé pour le site « %{website} » a expiré et ne permet plus la mise à jour du site. + text_line_2_html: Pour résoudre ce problème, veuillez renseigner un nouveau jeton d'accès en cliquant ici. yours: Cordialement. menu: admin: Admin @@ -307,7 +311,7 @@ fr: delivered: Votre message a bien été envoyé filters: Filtres target: Cible - users: + users: one: "%{count} utilisateur" other: "%{count} utilisateurs" websites: diff --git a/test/cassettes/GitRepositoryTest_test_incorrect_credentials_for_github.yml b/test/cassettes/GitRepositoryTest_test_incorrect_credentials_for_github.yml index cd75fe76d..81d6f04ec 100644 --- a/test/cassettes/GitRepositoryTest_test_incorrect_credentials_for_github.yml +++ b/test/cassettes/GitRepositoryTest_test_incorrect_credentials_for_github.yml @@ -2,7 +2,7 @@ http_interactions: - request: method: get - uri: https://api.github.com/repos//branches/main + uri: https://api.github.com/repos/ body: encoding: US-ASCII string: '' @@ -10,7 +10,7 @@ http_interactions: Accept: - application/vnd.github.v3+json User-Agent: - - Octokit Ruby Gem 6.0.1 + - Octokit Ruby Gem 7.1.0 Content-Type: - application/json Authorization: @@ -25,7 +25,7 @@ http_interactions: Server: - GitHub.com Date: - - Thu, 22 Dec 2022 23:31:26 GMT + - Fri, 08 Sep 2023 10:07:50 GMT Content-Type: - application/json; charset=utf-8 Content-Length: @@ -35,11 +35,11 @@ http_interactions: X-Ratelimit-Limit: - '60' X-Ratelimit-Remaining: - - '58' + - '59' X-Ratelimit-Reset: - - '1671754710' + - '1694171270' X-Ratelimit-Used: - - '2' + - '1' X-Ratelimit-Resource: - core Access-Control-Expose-Headers: @@ -64,9 +64,9 @@ http_interactions: Vary: - Accept-Encoding, Accept, X-Requested-With X-Github-Request-Id: - - 7908:9325:B0926D7:B2D17C4:63A4E8CE + - DB6A:F7A3:CA66C42:CC466F2:64FAF276 body: encoding: UTF-8 string: '{"message":"Bad credentials","documentation_url":"https://docs.github.com/rest"}' - recorded_at: Thu, 22 Dec 2022 23:31:26 GMT -recorded_with: VCR 6.1.0 \ No newline at end of file + recorded_at: Fri, 08 Sep 2023 10:07:50 GMT +recorded_with: VCR 6.2.0 diff --git a/test/cassettes/GitRepositoryTest_test_incorrect_credentials_for_gitlab.yml b/test/cassettes/GitRepositoryTest_test_incorrect_credentials_for_gitlab.yml index 4b7b393cf..b3f1981a0 100644 --- a/test/cassettes/GitRepositoryTest_test_incorrect_credentials_for_gitlab.yml +++ b/test/cassettes/GitRepositoryTest_test_incorrect_credentials_for_gitlab.yml @@ -1,20 +1,20 @@ --- http_interactions: - request: - method: post - uri: https://gitlab.com/api/v4/projects//repository/commits + method: get + uri: https://gitlab.com/api/v4/projects/ body: - encoding: UTF-8 - string: branch=main&commit_message=this%20is%20a%20commit&actions%5B%5D%5Baction%5D=create&actions%5B%5D%5Bfile_path%5D=%2Fpath.txt&actions%5B%5D%5Bcontent%5D=content + encoding: US-ASCII + string: '' headers: Accept: - application/json Content-Type: - application/x-www-form-urlencoded User-Agent: - - Gitlab Ruby Gem 4.18.0 + - Gitlab Ruby Gem 4.19.0 Private-Token: - - wrong access_token + - wrong access token Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 response: @@ -23,7 +23,7 @@ http_interactions: message: Unauthorized headers: Date: - - Thu, 19 May 2022 12:21:26 GMT + - Fri, 08 Sep 2023 10:08:06 GMT Content-Type: - application/json Content-Length: @@ -32,16 +32,20 @@ http_interactions: - keep-alive Cache-Control: - no-cache + Content-Security-Policy: + - default-src 'none' Vary: - - Origin + - Origin, Accept-Encoding X-Content-Type-Options: - nosniff X-Frame-Options: - SAMEORIGIN + X-Gitlab-Meta: + - '{"correlation_id":"18f903f59db15ce5c95b2eba90052e47","version":"1"}' X-Request-Id: - - 01G3E480HMNA1WW243XJG7S0BR + - 18f903f59db15ce5c95b2eba90052e47 X-Runtime: - - '0.038668' + - '0.032215' Strict-Transport-Security: - max-age=31536000 Referrer-Policy: @@ -51,29 +55,30 @@ http_interactions: Ratelimit-Remaining: - '1999' Ratelimit-Reset: - - '1652962946' + - '1694167746' Ratelimit-Resettime: - - Thu, 19 May 2022 12:22:26 GMT + - Fri, 08 Sep 2023 10:09:06 GMT Ratelimit-Limit: - '2000' Gitlab-Lb: - - fe-24-lb-gprd + - fe-26-lb-gprd Gitlab-Sv: - localhost Cf-Cache-Status: - - DYNAMIC - Expect-Ct: - - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + - MISS Report-To: - - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=vfvVpSgDUjG6ip4GnUGUII9epvnb%2BRgsscvlm0HBfkDNDSIpQIzquThr43ZchKxPCtbsxa%2Fy83hk8o5G58K8mlFL0bCbjFxXDBqtyx%2BVjazYFLShNEn3qJjjml4%3D"}],"group":"cf-nel","max_age":604800}' + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=kTLmHFN91Gt%2FvsntOQEyNIJvzsnkFvJGIgp9u7lIMOZtfIJUZmbGcnlUkGG1bBpmZgO9Gk4LhDrGh8a8Fstjr9zB2P1F8g4O8UPiM%2FiVTV0dnPJp1GpKysN86Ik%3D"}],"group":"cf-nel","max_age":604800}' Nel: - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Set-Cookie: + - _cfuvid=zMZ6n19rblkxQOxRfOZt9E6LX21MnIsF4WIVAHj47WI-1694167686543-0-604800000; + path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None Server: - cloudflare Cf-Ray: - - 70dccad50d056958-FRA + - 80366365fb613cf2-CDG body: encoding: UTF-8 string: '{"message":"401 Unauthorized"}' - recorded_at: Thu, 19 May 2022 12:21:26 GMT -recorded_with: VCR 6.1.0 + recorded_at: Fri, 08 Sep 2023 10:08:06 GMT +recorded_with: VCR 6.2.0 diff --git a/test/fixtures/communication/websites.yml b/test/fixtures/communication/websites.yml index ed3627da6..2bc9f4d0c 100644 --- a/test/fixtures/communication/websites.yml +++ b/test/fixtures/communication/websites.yml @@ -41,8 +41,10 @@ website_with_github: university: default_university name: Site de test git_provider: github - access_token: confidentialdata - repository: noesya/bordeauxmontaigne-test + git_endpoint: <%= ENV['TEST_GITHUB_ENDPOINT'] %> + git_branch: <%= ENV['TEST_GITHUB_BRANCH'] %> + access_token: <%= ENV['TEST_GITHUB_TOKEN'] %> + repository: <%= ENV['TEST_GITHUB_REPOSITORY'] %> languages: [fr] default_language: fr @@ -50,7 +52,9 @@ website_with_gitlab: university: default_university name: Site with gitlab git_provider: gitlab - access_token: test - repository: test + git_endpoint: <%= ENV['TEST_GITLAB_ENDPOINT'] %> + git_branch: <%= ENV['TEST_GITLAB_BRANCH'] %> + access_token: <%= ENV['TEST_GITLAB_TOKEN'] %> + repository: <%= ENV['TEST_GITLAB_REPOSITORY'] %> languages: [fr, en] default_language: fr diff --git a/test/mailers/.keep b/test/mailers/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/mailers/previews/base_mailer_preview.rb b/test/mailers/previews/base_mailer_preview.rb new file mode 100644 index 000000000..e1e3e1f02 --- /dev/null +++ b/test/mailers/previews/base_mailer_preview.rb @@ -0,0 +1,42 @@ +class BaseMailerPreview < ActionMailer::Preview + + protected + + def university + @university ||= University.first + end + + def user + @user ||= university.users.first + end + + def website + @website ||= university.communication_websites.first + end + + def organizations_import + @organizations_import ||= Import.new( + id: SecureRandom.uuid, + university: university, + kind: :organizations, + number_of_lines: 42, + processing_errors: {}, + status: :finished, + user: user + ) + end + + def sample_emergency_message + @sample_emergency_message ||= EmergencyMessage.new( + id: SecureRandom.uuid, + university: university, + name: "Message d'urgence", + content_en: "This is an emergency message.", + content_fr: "Ceci est un message d'urgence.", + role: 'admin', + subject_en: "Warning", + subject_fr: "Attention" + ) + end + +end diff --git a/test/mailers/previews/notification_mailer_preview.rb b/test/mailers/previews/notification_mailer_preview.rb new file mode 100644 index 000000000..332563510 --- /dev/null +++ b/test/mailers/previews/notification_mailer_preview.rb @@ -0,0 +1,14 @@ +class NotificationMailerPreview < BaseMailerPreview + def import + NotificationMailer.import(organizations_import) + end + + def emergency_message + NotificationMailer.emergency_message(sample_emergency_message, user, 'fr') + end + + def website_invalid_access_token + NotificationMailer.website_invalid_access_token(website, user) + end + +end diff --git a/test/services/git_repository_test.rb b/test/services/git_repository_test.rb index a5f1d673d..fa046c510 100644 --- a/test/services/git_repository_test.rb +++ b/test/services/git_repository_test.rb @@ -1,27 +1,23 @@ require "test_helper" class GitRepositoryTest < ActiveSupport::TestCase + include ActionMailer::TestHelper + test "incorrect credentials for github" do + website_with_github.update(access_token: 'wrong access token') VCR.use_cassette(location) do - exception = assert_raises(Exception) do - provider = Git::Providers::Github.new ENV['TEST_GITHUB_ENDPOINT'], - ENV['TEST_GITHUB_BRANCH'], - 'wrong access token', - ENV['TEST_GITHUB_REPOSITORY'] + assert_enqueued_emails 1 do + provider = website_with_github.git_repository.send(:provider) provider.create_file '/path.txt', 'content' provider.push 'this is a commit' end - assert_equal Octokit::Unauthorized, exception.class end end test "file creation on github" do VCR.use_cassette(location) do assert_nothing_raised do - provider = Git::Providers::Github.new ENV['TEST_GITHUB_ENDPOINT'], - ENV['TEST_GITHUB_BRANCH'], - ENV['TEST_GITHUB_TOKEN'], - ENV['TEST_GITHUB_REPOSITORY'] + provider = website_with_github.git_repository.send(:provider) provider.create_file 'test.txt', 'content' result = provider.push 'Creating test.txt file' end @@ -31,10 +27,7 @@ class GitRepositoryTest < ActiveSupport::TestCase test "file update on github" do VCR.use_cassette(location) do assert_nothing_raised do - provider = Git::Providers::Github.new ENV['TEST_GITHUB_ENDPOINT'], - ENV['TEST_GITHUB_BRANCH'], - ENV['TEST_GITHUB_TOKEN'], - ENV['TEST_GITHUB_REPOSITORY'] + provider = website_with_github.git_repository.send(:provider) provider.update_file 'test.txt', 'test.txt', 'new content' result = provider.push 'Updating test.txt file' end @@ -44,10 +37,7 @@ class GitRepositoryTest < ActiveSupport::TestCase test "file move on github" do VCR.use_cassette(location) do assert_nothing_raised do - provider = Git::Providers::Github.new ENV['TEST_GITHUB_ENDPOINT'], - ENV['TEST_GITHUB_BRANCH'], - ENV['TEST_GITHUB_TOKEN'], - ENV['TEST_GITHUB_REPOSITORY'] + provider = website_with_github.git_repository.send(:provider) provider.update_file 'new_test.txt', 'test.txt', 'new content' result = provider.push 'Moving test.txt file' end @@ -57,10 +47,7 @@ class GitRepositoryTest < ActiveSupport::TestCase test "file destroy on github" do VCR.use_cassette(location) do assert_nothing_raised do - provider = Git::Providers::Github.new ENV['TEST_GITHUB_ENDPOINT'], - ENV['TEST_GITHUB_BRANCH'], - ENV['TEST_GITHUB_TOKEN'], - ENV['TEST_GITHUB_REPOSITORY'] + provider = website_with_github.git_repository.send(:provider) provider.destroy_file 'new_test.txt' result = provider.push 'Destroying new_test.txt file' end @@ -69,25 +56,19 @@ class GitRepositoryTest < ActiveSupport::TestCase test "incorrect credentials for gitlab" do VCR.use_cassette(location) do - exception = assert_raises(Exception) do - provider = Git::Providers::Gitlab.new ENV['TEST_GITLAB_ENDPOINT'], - ENV['TEST_GITLAB_BRANCH'], - 'wrong access_token', - ENV['TEST_GITLAB_REPOSITORY'] + assert_enqueued_emails 1 do + website_with_gitlab.update(access_token: 'wrong access token') + provider = website_with_gitlab.git_repository.send(:provider) provider.create_file '/path.txt', 'content' provider.push 'this is a commit' end - assert_equal exception.class, Gitlab::Error::Unauthorized end end test "file creation on gitlab" do VCR.use_cassette(location) do assert_nothing_raised do - provider = Git::Providers::Gitlab.new ENV['TEST_GITLAB_ENDPOINT'], - ENV['TEST_GITLAB_BRANCH'], - ENV['TEST_GITLAB_TOKEN'], - ENV['TEST_GITLAB_REPOSITORY'] + provider = website_with_gitlab.git_repository.send(:provider) provider.create_file 'test.txt', 'content' result = provider.push 'Creating test.txt file' end @@ -97,10 +78,7 @@ class GitRepositoryTest < ActiveSupport::TestCase test "file update on gitlab" do VCR.use_cassette(location) do assert_nothing_raised do - provider = Git::Providers::Gitlab.new ENV['TEST_GITLAB_ENDPOINT'], - ENV['TEST_GITLAB_BRANCH'], - ENV['TEST_GITLAB_TOKEN'], - ENV['TEST_GITLAB_REPOSITORY'] + provider = website_with_gitlab.git_repository.send(:provider) provider.update_file 'test.txt', 'test.txt', 'new content' result = provider.push 'Updating test.txt file' end @@ -110,10 +88,7 @@ class GitRepositoryTest < ActiveSupport::TestCase test "file move on gitlab" do VCR.use_cassette(location) do assert_nothing_raised do - provider = Git::Providers::Gitlab.new ENV['TEST_GITLAB_ENDPOINT'], - ENV['TEST_GITLAB_BRANCH'], - ENV['TEST_GITLAB_TOKEN'], - ENV['TEST_GITLAB_REPOSITORY'] + provider = website_with_gitlab.git_repository.send(:provider) provider.update_file 'new_test.txt', 'test.txt', 'new content' result = provider.push 'Moving test.txt file' end @@ -123,10 +98,7 @@ class GitRepositoryTest < ActiveSupport::TestCase test "file destroy on gitlab" do VCR.use_cassette(location) do assert_nothing_raised do - provider = Git::Providers::Gitlab.new ENV['TEST_GITLAB_ENDPOINT'], - ENV['TEST_GITLAB_BRANCH'], - ENV['TEST_GITLAB_TOKEN'], - ENV['TEST_GITLAB_REPOSITORY'] + provider = website_with_gitlab.git_repository.send(:provider) provider.destroy_file 'new_test.txt' result = provider.push 'Destroying new_test.txt file' end diff --git a/test/test_helper.rb b/test/test_helper.rb index 34de059ce..536f1beeb 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -30,6 +30,10 @@ def website_with_github @website_with_github ||= communication_websites(:website_with_github) end + def website_with_gitlab + @website_with_gitlab ||= communication_websites(:website_with_gitlab) + end + def default_school @default_school ||= education_schools(:default_school) end