├── .agignore ├── .dockerignore ├── .github ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── actionlint.yml │ ├── ci.yml │ ├── copy-pr-template-to-dependabot-prs.yml │ ├── deploy.yml │ ├── pact-verify.yml │ └── release.yml ├── .gitignore ├── .govuk_dependabot_merger.yml ├── .node-version ├── .rubocop.yml ├── .ruby-version ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── LICENCE ├── Procfile ├── Procfile.dev ├── README.md ├── Rakefile ├── app ├── assets │ ├── builds │ │ └── .keep │ ├── config │ │ └── manifest.js │ ├── images │ │ └── qr.png │ ├── javascripts │ │ ├── application.js │ │ ├── components │ │ │ └── table.js │ │ ├── es6-components.js │ │ └── modules │ │ │ ├── accessible-autocomplete.js │ │ │ ├── auto-submit-form.js │ │ │ ├── clear-selected-permissions.js │ │ │ └── password-strength-indicator.js │ └── stylesheets │ │ ├── _user_research_recruitment_banner.scss │ │ ├── application.scss │ │ └── components │ │ ├── _accessible-autocomplete.scss │ │ ├── _contact-details.scss │ │ └── _table.scss ├── controllers │ ├── account │ │ ├── activities_controller.rb │ │ ├── applications_controller.rb │ │ ├── emails_controller.rb │ │ ├── organisations_controller.rb │ │ ├── passwords_controller.rb │ │ ├── permissions_controller.rb │ │ ├── roles_controller.rb │ │ └── signin_permissions_controller.rb │ ├── accounts_controller.rb │ ├── api │ │ └── users_controller.rb │ ├── api_users │ │ ├── applications_controller.rb │ │ └── permissions_controller.rb │ ├── api_users_controller.rb │ ├── application_controller.rb │ ├── authorisations_controller.rb │ ├── batch_invitation_permissions_controller.rb │ ├── batch_invitations_controller.rb │ ├── confirmations_controller.rb │ ├── devise │ │ ├── two_step_verification_controller.rb │ │ └── two_step_verification_session_controller.rb │ ├── doorkeeper_applications_controller.rb │ ├── healthcheck_controller.rb │ ├── invitations_controller.rb │ ├── oauth_users_controller.rb │ ├── organisations_controller.rb │ ├── passwords_controller.rb │ ├── root_controller.rb │ ├── sessions_controller.rb │ ├── signin_required_authorizations_controller.rb │ ├── supported_permissions_controller.rb │ ├── suspensions_controller.rb │ ├── two_step_verification_exemptions_controller.rb │ ├── user_research_recruitment_controller.rb │ ├── users │ │ ├── applications_controller.rb │ │ ├── emails_controller.rb │ │ ├── invitation_resends_controller.rb │ │ ├── names_controller.rb │ │ ├── organisations_controller.rb │ │ ├── permissions_controller.rb │ │ ├── roles_controller.rb │ │ ├── signin_permissions_controller.rb │ │ ├── two_step_verification_mandations_controller.rb │ │ ├── two_step_verification_resets_controller.rb │ │ └── unlockings_controller.rb │ └── users_controller.rb ├── helpers │ ├── account_helper.rb │ ├── api_users_helper.rb │ ├── application_access_helper.rb │ ├── application_helper.rb │ ├── application_permissions_helper.rb │ ├── application_table_helper.rb │ ├── batch_invitations_helper.rb │ ├── components │ │ └── table_helper.rb │ ├── event_log_helper.rb │ ├── navigation_items_helper.rb │ ├── organisation_helper.rb │ ├── root_helper.rb │ ├── two_step_verification_helper.rb │ ├── users_helper.rb │ └── users_with_access_helper.rb ├── jobs │ ├── application_job.rb │ ├── batch_invitation_job.rb │ ├── permission_updater.rb │ ├── push_user_updates_job.rb │ └── reauth_enforcer.rb ├── mailers │ ├── .gitkeep │ ├── application_mailer.rb │ ├── mailer_helper.rb │ ├── noisy_batch_invitation.rb │ └── user_mailer.rb ├── models │ ├── .gitkeep │ ├── api_user.rb │ ├── application_record.rb │ ├── batch_invitation.rb │ ├── batch_invitation_application_permission.rb │ ├── batch_invitation_user.rb │ ├── doorkeeper │ │ ├── access_grant.rb │ │ ├── access_token.rb │ │ ├── after_successful_authorization_processor.rb │ │ └── application.rb │ ├── event_log.rb │ ├── govuk_environment.rb │ ├── legacy_users_filter.rb │ ├── log_entry.rb │ ├── old_password.rb │ ├── organisation.rb │ ├── reject_non_governmental_email_addresses_validator.rb │ ├── supported_permission.rb │ ├── suspension.rb │ ├── two_step_verification_exemption.rb │ ├── user.rb │ ├── user_agent.rb │ ├── user_application_permission.rb │ ├── user_update_permission_builder.rb │ └── users_filter.rb ├── policies │ ├── account │ │ ├── activities_policy.rb │ │ ├── application_policy.rb │ │ ├── emails_policy.rb │ │ ├── organisations_policy.rb │ │ ├── passwords_policy.rb │ │ └── roles_policy.rb │ ├── account_page_policy.rb │ ├── api_user_policy.rb │ ├── application_policy.rb │ ├── authorisation_policy.rb │ ├── base_policy.rb │ ├── batch_invitation_policy.rb │ ├── organisation_policy.rb │ ├── supported_permission_policy.rb │ ├── user_policy.rb │ └── users │ │ └── application_policy.rb ├── presenters │ ├── api │ │ └── user_presenter.rb │ ├── user_export_presenter.rb │ └── user_o_auth_presenter.rb ├── queries │ └── users_with_access.rb ├── services │ └── user_update.rb └── views │ ├── account │ ├── activities │ │ └── show.html.erb │ ├── applications │ │ └── index.html.erb │ ├── emails │ │ └── edit.html.erb │ ├── organisations │ │ └── edit.html.erb │ ├── passwords │ │ └── edit.html.erb │ ├── permissions │ │ ├── edit.html.erb │ │ └── show.html.erb │ ├── roles │ │ └── edit.html.erb │ └── signin_permissions │ │ └── delete.html.erb │ ├── accounts │ └── show.html.erb │ ├── api_users │ ├── applications │ │ └── index.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── manage_tokens.html.erb │ ├── new.html.erb │ └── permissions │ │ └── edit.html.erb │ ├── authorisations │ ├── edit.html.erb │ └── new.html.erb │ ├── batch_invitation_permissions │ └── new.html.erb │ ├── batch_invitations │ ├── new.html.erb │ └── show.html.erb │ ├── components │ ├── _contact_details.html.erb │ ├── _table.html.erb │ └── docs │ │ ├── contact_details.yml │ │ └── table.yml │ ├── devise │ ├── _links.erb │ ├── confirmations │ │ └── show.html.erb │ ├── invitations │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── passwords │ │ ├── _change_password_panel.html.erb │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ └── reset_error.html.erb │ ├── sessions │ │ └── new.html.erb │ ├── two_step_verification │ │ ├── _make_your_account_more_secure.html.erb │ │ ├── prompt.html.erb │ │ └── show.html.erb │ └── two_step_verification_session │ │ ├── max_2sv_login_attempts_reached.html.erb │ │ └── new.html.erb │ ├── doorkeeper_applications │ ├── _application_list.html.erb │ ├── access_logs.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── monthly_access_stats.html.erb │ └── users_with_access.html.erb │ ├── kaminari │ └── gds │ │ ├── _gap.html.erb │ │ ├── _next_page.html.erb │ │ ├── _page.html.erb │ │ ├── _paginator.html.erb │ │ └── _prev_page.html.erb │ ├── layouts │ ├── _google_tag_manager.html.erb │ ├── application.html.erb │ ├── user_mailer.html.erb │ └── user_mailer.text.erb │ ├── noisy_batch_invitation │ └── make_noise.text.erb │ ├── organisations │ ├── _organisation_table.html.erb │ ├── edit.html.erb │ └── index.html.erb │ ├── root │ ├── accessibility_statement.html.erb │ ├── index.html.erb │ ├── privacy_notice.html.erb │ └── signin_required.html.erb │ ├── shared │ ├── _add_permissions_with_autocomplete_form.html.erb │ ├── _edit_permissions_form.html.erb │ ├── _event_logs_table.html.erb │ ├── _permissions_forms.html.erb │ └── address_blacklisted.html.erb │ ├── signin_required_authorizations │ └── error.html.erb │ ├── supported_permissions │ ├── _form.html.erb │ ├── confirm_destroy.erb │ ├── edit.html.erb │ ├── index.html.erb │ └── new.html.erb │ ├── suspensions │ └── edit.html.erb │ ├── two_step_verification_exemptions │ └── edit.html.erb │ ├── user_mailer │ ├── confirmation_instructions.text.erb │ ├── email_changed_by_admin_notification.text.erb │ ├── email_changed_notification.text.erb │ ├── invitation_instructions.text.erb │ ├── notify_reset_password_disallowed_due_to_suspension.text.erb │ ├── notify_reset_password_disallowed_due_to_unaccepted_invitation.text.erb │ ├── reset_password_instructions.text.erb │ ├── suspension_notification.text.erb │ ├── suspension_reminder.text.erb │ ├── two_step_changed.text.erb │ ├── two_step_enabled.text.erb │ ├── two_step_mandated.text.erb │ ├── two_step_reset.text.erb │ └── unlock_instructions.text.erb │ └── users │ ├── _users_filter.html.erb │ ├── applications │ └── index.html.erb │ ├── edit.html.erb │ ├── emails │ └── edit.html.erb │ ├── event_logs.html.erb │ ├── index.html.erb │ ├── invitation_resends │ └── edit.html.erb │ ├── names │ └── edit.html.erb │ ├── organisations │ └── edit.html.erb │ ├── permissions │ ├── edit.html.erb │ └── show.html.erb │ ├── require_2sv.html.erb │ ├── roles │ └── edit.html.erb │ ├── signin_permissions │ └── delete.html.erb │ ├── two_step_verification_mandations │ └── edit.html.erb │ ├── two_step_verification_resets │ └── edit.html.erb │ └── unlockings │ └── edit.html.erb ├── bin ├── brakeman ├── dev ├── rails ├── rake ├── rubocop ├── setup └── yarn ├── config.ru ├── config ├── application.rb ├── boot.rb ├── brakeman.ignore ├── database.yml ├── devise.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── content_security_policy.rb │ ├── cookies_serializer.rb │ ├── dartsass.rb │ ├── date_formats.rb │ ├── devise.rb │ ├── doorkeeper.rb │ ├── doorkeeper_scopes.rb │ ├── filter_parameter_logging.rb │ ├── govuk_error.rb │ ├── i18n.rb │ ├── inflections.rb │ ├── load_model_enhancements.rb │ ├── mime_types.rb │ ├── permissions_policy.rb │ ├── prometheus.rb │ ├── rack_attack.rb │ ├── session_store.rb │ └── wrap_parameters.rb ├── locales │ ├── department_specific.en.yml │ ├── devise.en.yml │ ├── devise.security_extension.en.yml │ ├── devise_invitable.en.yml │ ├── doorkeeper.en.yml │ └── en.yml ├── puma.rb ├── routes.rb ├── schedule.rb ├── secrets.yml ├── sidekiq.yml └── unicorn.rb ├── db ├── migrate │ ├── 20240228080000_reencrypt_user_otp_keys.rb │ ├── 20240423105028_remove_user_application_permissions_with_deleted_supported_permission.rb │ ├── 20240429130154_remove_user_permissions_with_deleted_permission.rb │ ├── 20240809112119_index_event_logs_columns.rb │ └── 20241001140623_rename_supported_permission_delegatable_column.rb ├── schema.rb └── seeds.rb ├── docs ├── access_and_permissions.md ├── api.md ├── diagrams │ └── signon-sign-in-from-whitehall-publisher.svg ├── environment-variables.md ├── mass_password_reset.md ├── oauth.md ├── troubleshooting.md └── usage.md ├── lib ├── abilities.rb ├── assets │ └── .gitkeep ├── code_verifier.rb ├── collectors │ └── global_prometheus_collector.rb ├── data_hygiene │ └── bulk_organisation_updater.rb ├── devise │ ├── hooks │ │ └── two_step_verification.rb │ └── models │ │ ├── password_archivable.rb │ │ └── suspendable.rb ├── exception_handler.rb ├── expired_not_signed_in_user_deleter.rb ├── expired_oauth_access_records_deleter.rb ├── healthcheck │ └── api_tokens.rb ├── inactive_users_suspender.rb ├── inactive_users_suspension_reminder.rb ├── inactive_users_suspension_reminder_mailing_list.rb ├── kubernetes │ └── client.rb ├── organisation_mappings │ └── zendesk_to_signon.rb ├── organisations_fetcher.rb ├── roles.rb ├── roles │ ├── admin.rb │ ├── base.rb │ ├── normal.rb │ ├── organisation_admin.rb │ ├── super_organisation_admin.rb │ └── superadmin.rb ├── signin_permission_granter.rb ├── sso_push_client.rb ├── sso_push_credential.rb ├── sso_push_error.rb ├── supported_permission_parameter_filter.rb ├── tasks │ ├── .gitkeep │ ├── applications.rake │ ├── assets.rake │ ├── data_hygiene.rake │ ├── event_log.rake │ ├── jasmine.rake │ ├── lint.rake │ ├── oauth_access_records.rake │ ├── organisation_mappings.rake │ ├── organisations.rake │ ├── permissions.rake │ ├── sync_kubernetes_secrets.rake │ └── users.rake ├── unsupported_permission_error.rb ├── user_parameter_sanitiser.rb ├── user_permission_migrator.rb └── volatile_lock.rb ├── log └── .gitkeep ├── package.json ├── public ├── 400.html ├── 404.html ├── 406-unsupported-browser.html ├── 422.html ├── 500.html ├── favicon.ico ├── icon.png ├── icon.svg └── robots.txt ├── script ├── import_user_organisation_membership_from_whitehall ├── make_oauth_work_in_dev └── minitest-ci ├── spec ├── javascripts │ ├── helpers │ │ └── .gitkeep │ └── modules │ │ ├── accessible-autocomplete-spec.js │ │ └── clear-selected-permissions-spec.js └── support │ └── jasmine-browser.json ├── test ├── controllers │ ├── .gitkeep │ ├── account │ │ ├── applications_controller_test.rb │ │ ├── emails_controller_test.rb │ │ ├── organisations_controller_test.rb │ │ ├── passwords_controller_test.rb │ │ ├── permissions_controller_test.rb │ │ ├── roles_controller_test.rb │ │ └── signin_permissions_controller_test.rb │ ├── api │ │ └── users_controller_test.rb │ ├── api_users │ │ ├── applications_controller_test.rb │ │ └── permissions_controller_test.rb │ ├── api_users_controller_test.rb │ ├── authorisations_controller_test.rb │ ├── batch_invitation_permissions_controller_test.rb │ ├── batch_invitations_controller_test.rb │ ├── confirmations_controller_test.rb │ ├── doorkeeper │ │ └── tokens_controller_test.rb │ ├── doorkeeper_applications_controller_test.rb │ ├── fixtures │ │ ├── empty_users.csv │ │ ├── invalid_users.csv │ │ ├── no_headers_users.csv │ │ ├── partial_users.csv │ │ ├── reversed_users.csv │ │ ├── users.csv │ │ ├── users_with_non_valid_emails.csv │ │ └── users_with_orgs.csv │ ├── invitations_controller_test.rb │ ├── oauth_users_controller_test.rb │ ├── organisations_controller_test.rb │ ├── passwords_controller_test.rb │ ├── root_controller_test.rb │ ├── sessions_controller_test.rb │ ├── signin_required_authorizations_controller_test.rb │ ├── supported_permissions_controller_test.rb │ ├── suspensions_controller_test.rb │ ├── two_step_verification_controller_test.rb │ ├── two_step_verification_exemptions_controller_test.rb │ ├── user_research_recruitment_controller_test.rb │ ├── users │ │ ├── applications_controller_test.rb │ │ ├── emails_controller_test.rb │ │ ├── invitation_resends_controller_test.rb │ │ ├── names_controller_test.rb │ │ ├── organisations_controller_test.rb │ │ ├── permissions_controller_test.rb │ │ ├── roles_controller_test.rb │ │ ├── signin_permissions_controller_test.rb │ │ ├── two_step_verification_mandations_controller_test.rb │ │ ├── two_step_verification_resets_controller_test.rb │ │ └── unlockings_controller_test.rb │ └── users_controller_test.rb ├── factories │ ├── batch_invitation.rb │ ├── batch_invitation_application_permission.rb │ ├── batch_invitation_user.rb │ ├── event_log.rb │ ├── oauth_access_grants.rb │ ├── oauth_access_tokens.rb │ ├── oauth_applications.rb │ ├── organisation.rb │ ├── supported_permission.rb │ ├── two_step_verification_exemption.rb │ ├── user_application_permission.rb │ └── users.rb ├── fixtures │ ├── users.csv │ └── users_with_orgs.csv ├── helpers │ ├── account_helper_test.rb │ ├── application_access_helper_test.rb │ ├── application_helper_test.rb │ ├── application_permissions_helper_test.rb │ ├── application_table_helper_test.rb │ ├── batch_invitations_helper_test.rb │ ├── organisation_helper_test.rb │ ├── root_helper_test.rb │ ├── two_step_verification_helper_test.rb │ ├── users_helper_test.rb │ └── users_with_access_helper_test.rb ├── integration │ ├── account │ │ ├── account_test.rb │ │ ├── activities_test.rb │ │ ├── granting_access_test.rb │ │ ├── password_change_test.rb │ │ ├── removing_access_test.rb │ │ ├── roles_test.rb │ │ ├── updating_organisation_test.rb │ │ └── updating_permissions_test.rb │ ├── api_authentication_test.rb │ ├── api_users_test.rb │ ├── applications │ │ ├── access_logs_test.rb │ │ ├── edit_test.rb │ │ ├── monthly_access_stats_test.rb │ │ └── users_with_access_test.rb │ ├── authorise_application_test.rb │ ├── batch_inviting_users_test.rb │ ├── cookies_security_test.rb │ ├── dashboard_test.rb │ ├── doorkeeper_integration_test.rb │ ├── email_change_test.rb │ ├── event_log_creation_test.rb │ ├── inviting_users_test.rb │ ├── mandate_2sv_for_organisation_test.rb │ ├── password_reset_test.rb │ ├── session_timeout_test.rb │ ├── sign_in_test.rb │ ├── sign_out_test.rb │ ├── two_step_verification_prompt_test.rb │ ├── two_step_verification_test.rb │ ├── updating_permissions_for_apps_with_many_permissions_test.rb │ ├── user_agent_test.rb │ ├── user_locking_test.rb │ ├── user_research_recruitment_banner_test.rb │ ├── user_suspension_test.rb │ ├── users │ │ ├── account_access_logs_test.rb │ │ ├── granting_access_test.rb │ │ ├── name_change_test.rb │ │ ├── removing_access_test.rb │ │ ├── roles_test.rb │ │ ├── status_test.rb │ │ ├── two_step_verification_test.rb │ │ ├── updating_organisation_test.rb │ │ ├── updating_permissions_test.rb │ │ └── users_test.rb │ └── volatile_lock_test.rb ├── jobs │ ├── permission_updater_test.rb │ ├── push_user_updates_job_test.rb │ └── reauth_enforcer_test.rb ├── lib │ ├── code_verifier_test.rb │ ├── collectors │ │ └── global_prometheus_collector_test.rb │ ├── data_hygiene │ │ └── bulk_organisation_updater_test.rb │ ├── expired_not_signed_in_user_deleter_test.rb │ ├── expired_oauth_access_records_deleter_test.rb │ ├── healthcheck │ │ └── api_tokens_test.rb │ ├── inactive_users_suspender_test.rb │ ├── inactive_users_suspension_reminder_mailing_list_test.rb │ ├── inactive_users_suspension_reminder_test.rb │ ├── kubernetes │ │ └── client_test.rb │ ├── organisation_mappings │ │ └── zendesk_to_signon_test.rb │ ├── organisations_fetcher_test.rb │ ├── roles_test.rb │ ├── signin_permission_granter_test.rb │ ├── sso_push_client_test.rb │ ├── sso_push_credential_test.rb │ ├── sso_push_error_test.rb │ ├── tasks │ │ ├── data_hygiene_test.rb │ │ ├── permissions_test.rb │ │ ├── sync_kubernetes_secrets_test.rb │ │ └── users_test.rb │ ├── user_parameter_sanitiser_test.rb │ └── user_permission_migrator_test.rb ├── models │ ├── .gitkeep │ ├── api_user_test.rb │ ├── batch_invitation_test.rb │ ├── batch_invitation_user_test.rb │ ├── doorkeeper │ │ ├── access_grant_test.rb │ │ ├── access_token_test.rb │ │ ├── after_successful_authorization_processor_test.rb │ │ └── application_test.rb │ ├── event_log_test.rb │ ├── govuk_environment_test.rb │ ├── legacy_users_filter_test.rb │ ├── noisy_batch_invitation_test.rb │ ├── organisation_test.rb │ ├── supported_permission_test.rb │ ├── suspension_test.rb │ ├── two_step_verification_exemption_test.rb │ ├── user_agent_test.rb │ ├── user_application_permission_test.rb │ ├── user_export_presenter_test.rb │ ├── user_mailer_content_test.rb │ ├── user_mailer_test.rb │ ├── user_o_auth_presenter_test.rb │ ├── user_test.rb │ ├── user_update_permission_builder_test.rb │ └── users_filter_test.rb ├── policies │ ├── account │ │ ├── activities_policy_test.rb │ │ ├── application_policy_test.rb │ │ ├── emails_policy_test.rb │ │ ├── organisations_policy_test.rb │ │ ├── passwords_policy_test.rb │ │ └── roles_policy_test.rb │ ├── account_page_policy_test.rb │ ├── api_user_policy_test.rb │ ├── application_policy_test.rb │ ├── authorisation_policy_test.rb │ ├── batch_invitation_policy_test.rb │ ├── organisation_policy_scope_test.rb │ ├── organisation_policy_test.rb │ ├── supported_permission_policy_scope_test.rb │ ├── user_policy_scope_test.rb │ ├── user_policy_test.rb │ └── users │ │ └── application_policy_test.rb ├── presenters │ └── api │ │ └── user_presenter_test.rb ├── service_consumers │ └── pact_helper.rb ├── services │ └── user_update_test.rb ├── support │ ├── analytics_helpers.rb │ ├── autocomplete_helper.rb │ ├── confirmation_token_helpers.rb │ ├── editing_users_helpers.rb │ ├── email_helpers.rb │ ├── flash_helpers.rb │ ├── granting_access_helpers.rb │ ├── managing_two_sv_helpers.rb │ ├── password_helpers.rb │ ├── policy_helpers.rb │ ├── pundit_helpers.rb │ ├── removing_access_helpers.rb │ ├── token_auth_helpers.rb │ ├── updating_permissions_helpers.rb │ ├── user_account_helpers.rb │ └── user_helpers.rb └── test_helper.rb ├── vendor ├── assets │ └── javascripts │ │ └── zxcvbn.js └── plugins │ └── .gitkeep └── yarn.lock /.agignore: -------------------------------------------------------------------------------- 1 | vendor/assets/javascripts/zxcvbn.js 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .git 3 | .github 4 | .gitignore 5 | Dockerfile 6 | Procfile 7 | README.md 8 | coverage 9 | docs 10 | features 11 | log 12 | node_modules 13 | script 14 | spec 15 | test 16 | tmp 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: / 5 | schedule: 6 | interval: daily 7 | allow: 8 | - dependency-type: "all" 9 | open-pull-requests-limit: 20 10 | 11 | - package-ecosystem: npm 12 | directory: / 13 | schedule: 14 | interval: daily 15 | 16 | - package-ecosystem: github-actions 17 | directory: / 18 | schedule: 19 | interval: daily 20 | 21 | # Ruby needs to be upgraded manually in multiple places, so cannot 22 | # be upgraded by Dependabot. That effectively makes the below 23 | # config redundant, as ruby is the only updatable thing in the 24 | # Dockerfile, although this may change in the future. We hope this 25 | # config will save a dev from trying to upgrade ruby via Dependabot. 26 | - package-ecosystem: docker 27 | ignore: 28 | - dependency-name: ruby 29 | directory: / 30 | schedule: 31 | interval: weekly 32 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | This application is owned by the publishing platform team. Please let us know in #govuk-publishing-platform when you raise any PRs. 2 | 3 | ⚠️ This repo is Continuously Deployed: make sure you [follow the guidance](https://docs.publishing.service.gov.uk/manual/development-pipeline.html#merge-your-own-pull-request) ⚠️ 4 | 5 | Follow [these steps](https://guides.rubyonrails.org/upgrading_ruby_on_rails.html) if you are doing a Rails upgrade. 6 | -------------------------------------------------------------------------------- /.github/workflows/actionlint.yml: -------------------------------------------------------------------------------- 1 | name: Lint GitHub Actions 2 | on: 3 | push: 4 | paths: ['.github/**'] 5 | jobs: 6 | actionlint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | with: 11 | show-progress: false 12 | - uses: alphagov/govuk-infrastructure/.github/actions/actionlint@main 13 | -------------------------------------------------------------------------------- /.github/workflows/copy-pr-template-to-dependabot-prs.yml: -------------------------------------------------------------------------------- 1 | name: Copy PR template to Dependabot PRs 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened] 6 | 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | 11 | jobs: 12 | copy_pr_template: 13 | name: Copy PR template to Dependabot PR 14 | runs-on: ubuntu-latest 15 | if: github.actor == 'dependabot[bot]' 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Post PR template as a comment 20 | uses: actions/github-script@v7 21 | with: 22 | script: | 23 | const fs = require('fs') 24 | 25 | const body = [ 26 | "pull_request_template.md", 27 | ".github/pull_request_template.md", 28 | "docs/pull_request_template.md", 29 | ]. 30 | filter(path => fs.existsSync(path)). 31 | map(path => fs.readFileSync(path)). 32 | join("\n") 33 | 34 | if (body !== "") { 35 | github.rest.issues.createComment({ 36 | issue_number: context.issue.number, 37 | owner: context.repo.owner, 38 | repo: context.repo.repo, 39 | body 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | workflow_run: 6 | workflows: [CI] 7 | types: [completed] 8 | branches: [main] 9 | 10 | jobs: 11 | release: 12 | if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' 13 | name: Release 14 | uses: alphagov/govuk-infrastructure/.github/workflows/release.yml@main 15 | secrets: 16 | GH_TOKEN: ${{ secrets.GOVUK_CI_GITHUB_API_TOKEN }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile ~/.gitignore_global 6 | 7 | .DS_Store 8 | 9 | # Ignore bundler config 10 | /.bundle 11 | .DS_Store 12 | 13 | # Ignore the default SQLite database. 14 | /db/*.sqlite3 15 | /db/*.sqlite3-journal 16 | 17 | # Ignore all logfiles and tempfiles. 18 | /log/*.log 19 | /tmp 20 | 21 | .*.swp 22 | 23 | # Simplecov 24 | coverage/* 25 | test/reports/* 26 | 27 | # yarn 28 | node_modules 29 | yarn-error.log 30 | 31 | # Ignore compiled Rails assets 32 | /public/assets 33 | 34 | # For dartsass-rails 35 | /app/assets/builds/* 36 | !/app/assets/builds/.keep 37 | -------------------------------------------------------------------------------- /.govuk_dependabot_merger.yml: -------------------------------------------------------------------------------- 1 | api_version: 2 2 | defaults: 3 | allowed_semver_bumps: 4 | - patch 5 | - minor 6 | auto_merge: true 7 | update_external_dependencies: true 8 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20.11.0 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | rubocop-govuk: 3 | - config/default.yml 4 | - config/rails.yml 5 | 6 | inherit_mode: 7 | merge: 8 | - Exclude 9 | 10 | # ************************************************************** 11 | # TRY NOT TO ADD OVERRIDES IN THIS FILE 12 | # 13 | # This repo is configured to follow the RuboCop GOV.UK styleguide. 14 | # Any rules you override here will cause this repo to diverge from 15 | # the way we write code in all other GOV.UK repos. 16 | # 17 | # See https://github.com/alphagov/rubocop-govuk/blob/main/CONTRIBUTING.md 18 | # ************************************************************** 19 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.7 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ruby_version=3.3 2 | ARG base_image=ghcr.io/alphagov/govuk-ruby-base:$ruby_version 3 | ARG builder_image=ghcr.io/alphagov/govuk-ruby-builder:$ruby_version 4 | 5 | 6 | FROM --platform=$TARGETPLATFORM $builder_image AS builder 7 | 8 | ENV DEVISE_PEPPER=unused \ 9 | DEVISE_SECRET_KEY=unused \ 10 | GOVUK_ENVIRONMENT=unused 11 | 12 | WORKDIR $APP_HOME 13 | COPY Gemfile* .ruby-version ./ 14 | RUN bundle install 15 | COPY package.json yarn.lock ./ 16 | RUN yarn install 17 | COPY . . 18 | RUN bootsnap precompile --gemfile . 19 | RUN rails assets:precompile && rm -fr log 20 | 21 | 22 | FROM --platform=$TARGETPLATFORM $base_image 23 | 24 | ENV GOVUK_APP_NAME=signon 25 | 26 | WORKDIR $APP_HOME 27 | COPY --from=builder $BUNDLE_PATH $BUNDLE_PATH 28 | COPY --from=builder $BOOTSNAP_CACHE_DIR $BOOTSNAP_CACHE_DIR 29 | COPY --from=builder $APP_HOME . 30 | 31 | USER app 32 | CMD ["puma"] 33 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2015 Crown copyright (Government Digital Service) 4 | 5 | Portions Copyright (C) 2012 Dmitrii Golub, 2011 Marco Scholl 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | this software and associated documentation files (the "Software"), to deal in 9 | the Software without restriction, including without limitation the rights to 10 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 11 | of the Software, and to permit persons to whom the Software is furnished to do 12 | so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec unicorn -c ./config/unicorn.rb -p ${PORT:-3016} 2 | worker: bundle exec sidekiq -C ./config/sidekiq.yml 3 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: bin/rails server -p 3000 2 | css: bin/rails dartsass:watch 3 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path("config/application", __dir__) 5 | 6 | Signon::Application.load_tasks 7 | 8 | begin 9 | require "pact/tasks" 10 | rescue LoadError 11 | # Pact isn't available in all environments 12 | end 13 | 14 | Rake::Task[:default].clear_prerequisites 15 | task default: %i[lint jasmine test] 16 | -------------------------------------------------------------------------------- /app/assets/builds/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphagov/signon/f1093373f0cbde6947ecee93030bac5bbcf2f4e1/app/assets/builds/.keep -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_tree ../builds 3 | //= link application.js 4 | //= link es6-components.js 5 | -------------------------------------------------------------------------------- /app/assets/images/qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphagov/signon/f1093373f0cbde6947ecee93030bac5bbcf2f4e1/app/assets/images/qr.png -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | //= require govuk_publishing_components/dependencies 2 | //= require govuk_publishing_components/components/copy-to-clipboard 3 | //= require govuk_publishing_components/components/option-select 4 | //= require govuk_publishing_components/components/password-input 5 | //= require govuk_publishing_components/components/table 6 | 7 | //= require_tree ./modules 8 | //= require_tree ./components 9 | //= require rails-ujs 10 | -------------------------------------------------------------------------------- /app/assets/javascripts/es6-components.js: -------------------------------------------------------------------------------- 1 | // These modules from govuk_publishing_components 2 | // depend on govuk-frontend modules. govuk-frontend 3 | // now targets browsers that support `type="module"`. 4 | // 5 | // To gracefully prevent execution of these scripts 6 | // on browsers that don't support ES6, this script 7 | // should be included in a `type="module"` script tag 8 | // which will ensure they are never loaded. 9 | 10 | //= require govuk_publishing_components/components/button 11 | //= require govuk_publishing_components/components/checkboxes 12 | //= require govuk_publishing_components/components/error-summary 13 | //= require govuk_publishing_components/components/layout-header 14 | //= require govuk_publishing_components/components/skip-link 15 | //= require govuk_publishing_components/components/tabs 16 | -------------------------------------------------------------------------------- /app/assets/javascripts/modules/auto-submit-form.js: -------------------------------------------------------------------------------- 1 | window.GOVUK = window.GOVUK || {} 2 | window.GOVUK.Modules = window.GOVUK.Modules || {}; 3 | 4 | (function (Modules) { 5 | 'use strict' 6 | 7 | function AutoSubmitForm (module) { 8 | this.module = module 9 | this.module.ignore = this.module.getAttribute('data-auto-submit-ignore').split(',') 10 | } 11 | 12 | AutoSubmitForm.prototype.init = function () { 13 | this.module.addEventListener('change', function (e) { 14 | if (!this.module.ignore.includes(e.target.getAttribute('name'))) { 15 | this.module.submit() 16 | } 17 | }.bind(this)) 18 | } 19 | 20 | Modules.AutoSubmitForm = AutoSubmitForm 21 | })(window.GOVUK.Modules) 22 | -------------------------------------------------------------------------------- /app/assets/javascripts/modules/clear-selected-permissions.js: -------------------------------------------------------------------------------- 1 | window.GOVUK = window.GOVUK || {} 2 | window.GOVUK.Modules = window.GOVUK.Modules || {}; 3 | 4 | (function (Modules) { 5 | 'use strict' 6 | 7 | function ClearSelectedPermissions (module) { 8 | this.module = module 9 | this.clearButton = this.module.querySelector('button[data-action="clear"]') 10 | this.checkboxes = this.module.querySelectorAll('.govuk-checkboxes__input') 11 | this.checkboxLists = this.module.querySelectorAll('.gem-c-checkboxes__list') 12 | } 13 | 14 | ClearSelectedPermissions.prototype.init = function () { 15 | this.clearButton.addEventListener('click', this.clear.bind(this)) 16 | } 17 | 18 | ClearSelectedPermissions.prototype.clear = function (event) { 19 | event.preventDefault() 20 | this.uncheckBoxes() 21 | this.updateSelectCounts() 22 | } 23 | 24 | ClearSelectedPermissions.prototype.uncheckBoxes = function () { 25 | this.checkboxes.forEach(c => { c.checked = false }) 26 | } 27 | 28 | ClearSelectedPermissions.prototype.updateSelectCounts = function () { 29 | const event = new Event('change') 30 | this.checkboxLists.forEach(c => { c.dispatchEvent(event) }) 31 | } 32 | 33 | Modules.ClearSelectedPermissions = ClearSelectedPermissions 34 | })(window.GOVUK.Modules) 35 | -------------------------------------------------------------------------------- /app/assets/stylesheets/_user_research_recruitment_banner.scss: -------------------------------------------------------------------------------- 1 | .user-research-recruitment-banner { 2 | background-color: $govuk-brand-colour; 3 | @include govuk-responsive-padding(8, "top"); 4 | } 5 | 6 | .user-research-recruitment-banner__divider { 7 | border-bottom: 1px solid govuk-colour("white"); 8 | } 9 | 10 | .user-research-recruitment-banner__title { 11 | color: govuk-colour("white"); 12 | @include govuk-responsive-margin(5, "bottom"); 13 | } 14 | 15 | .user-research-recruitment-banner__intro { 16 | color: govuk-colour("white"); 17 | } 18 | 19 | .user-research-recruitment-banner__buttons { 20 | align-items: center; 21 | @include govuk-responsive-padding(6, "bottom"); 22 | } 23 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/_contact-details.scss: -------------------------------------------------------------------------------- 1 | .app-c-contact-details { 2 | padding-left: govuk-spacing(3); 3 | clear: both; 4 | border-left: 1px solid $govuk-border-colour; 5 | 6 | @include govuk-font($size: 19); 7 | @include govuk-text-colour; 8 | // Margin top intended to collapse 9 | // This adds an additional 10px to the paragraph above 10 | @include govuk-responsive-margin(6, "top"); 11 | @include govuk-responsive-margin(6, "bottom"); 12 | } 13 | -------------------------------------------------------------------------------- /app/controllers/account/activities_controller.rb: -------------------------------------------------------------------------------- 1 | class Account::ActivitiesController < ApplicationController 2 | before_action :authenticate_user! 3 | before_action :authorise_user 4 | 5 | def show 6 | @logs = current_user.event_logs.page(params[:page]).per(100) 7 | end 8 | 9 | private 10 | 11 | def authorise_user 12 | authorize %i[account activities] 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/controllers/account/applications_controller.rb: -------------------------------------------------------------------------------- 1 | class Account::ApplicationsController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | def show 5 | authorize [:account, Doorkeeper::Application] 6 | 7 | redirect_to account_applications_path 8 | end 9 | 10 | def index 11 | authorize [:account, Doorkeeper::Application] 12 | 13 | @applications_with_signin = Doorkeeper::Application.not_api_only.can_signin(current_user) 14 | @applications_without_signin = Doorkeeper::Application.not_api_only.without_signin_permission_for(current_user) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/controllers/account/organisations_controller.rb: -------------------------------------------------------------------------------- 1 | class Account::OrganisationsController < ApplicationController 2 | before_action :authenticate_user! 3 | before_action :authorise_user 4 | 5 | def edit; end 6 | 7 | def update 8 | organisation_id = params[:user][:organisation_id] 9 | organisation = Organisation.find(organisation_id) 10 | 11 | if UserUpdate.new(current_user, { organisation_id: }, current_user, user_ip_address).call 12 | redirect_to account_path, notice: "Your organisation is now #{organisation.name}" 13 | else 14 | flash[:alert] = "There was a problem changing your organisation." 15 | render :edit 16 | end 17 | end 18 | 19 | private 20 | 21 | def authorise_user 22 | authorize %i[account organisations] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/controllers/account/passwords_controller.rb: -------------------------------------------------------------------------------- 1 | class Account::PasswordsController < ApplicationController 2 | before_action :authenticate_user! 3 | before_action :authorise_user 4 | 5 | def edit; end 6 | 7 | def update 8 | if current_user.update_with_password(password_params) 9 | EventLog.record_event(current_user, EventLog::SUCCESSFUL_PASSWORD_CHANGE, ip_address: user_ip_address) 10 | flash[:notice] = t(:updated, scope: "devise.passwords") 11 | bypass_sign_in(current_user) 12 | redirect_to account_path 13 | else 14 | EventLog.record_event(current_user, EventLog::UNSUCCESSFUL_PASSWORD_CHANGE, ip_address: user_ip_address) 15 | render :edit 16 | end 17 | end 18 | 19 | private 20 | 21 | def authorise_user 22 | authorize %i[account passwords] 23 | end 24 | 25 | def password_params 26 | params.require(:user).permit( 27 | :current_password, 28 | :password, 29 | :password_confirmation, 30 | ) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/controllers/account/roles_controller.rb: -------------------------------------------------------------------------------- 1 | class Account::RolesController < ApplicationController 2 | before_action :authenticate_user! 3 | before_action :authorise_user 4 | 5 | def edit; end 6 | 7 | def update 8 | role = params[:user][:role] 9 | 10 | if UserUpdate.new(current_user, { role: }, current_user, user_ip_address).call 11 | redirect_to account_path, notice: "Your role is now #{Roles.find(role).display_name}" 12 | else 13 | flash[:alert] = "There was a problem changing your role." 14 | render :edit 15 | end 16 | end 17 | 18 | private 19 | 20 | def authorise_user 21 | authorize %i[account roles] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/controllers/accounts_controller.rb: -------------------------------------------------------------------------------- 1 | class AccountsController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | def show 5 | authorize :account_page 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::UsersController < ApplicationController 2 | before_action :authorize_api_access, unless: -> { Rails.env.development? } 3 | skip_after_action :verify_authorized 4 | 5 | def index 6 | users = User.where(uid: params[:uuids]) 7 | render json: Api::UserPresenter.present_many(users) 8 | end 9 | 10 | private 11 | 12 | def authorize_api_access 13 | doorkeeper_authorize! && check_signon_permissions 14 | end 15 | 16 | def check_signon_permissions 17 | head :unauthorized unless doorkeeper_token&.application&.signon? 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/controllers/api_users/applications_controller.rb: -------------------------------------------------------------------------------- 1 | class ApiUsers::ApplicationsController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | def index 5 | @api_user = ApiUser.find(params[:api_user_id]) 6 | 7 | authorize @api_user 8 | 9 | @applications = @api_user.authorised_applications.merge(Doorkeeper::AccessToken.not_revoked) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/batch_invitation_permissions_controller.rb: -------------------------------------------------------------------------------- 1 | class BatchInvitationPermissionsController < ApplicationController 2 | before_action :authenticate_user! 3 | before_action :load_batch_invitation 4 | before_action :authorise_to_manage_permissions 5 | before_action :prevent_updating 6 | 7 | def new; end 8 | 9 | def create 10 | @batch_invitation.supported_permission_ids = params[:user][:supported_permission_ids] if params[:user] 11 | 12 | @batch_invitation.save! 13 | 14 | @batch_invitation.enqueue 15 | flash[:notice] = "Scheduled invitation of #{@batch_invitation.batch_invitation_users.count} users" 16 | redirect_to batch_invitation_path(@batch_invitation) 17 | end 18 | 19 | private 20 | 21 | def load_batch_invitation 22 | @batch_invitation = current_user.batch_invitations.find(params[:batch_invitation_id]) 23 | end 24 | 25 | def authorise_to_manage_permissions 26 | authorize @batch_invitation, :manage_permissions? 27 | end 28 | 29 | def prevent_updating 30 | if @batch_invitation.has_permissions? 31 | flash[:alert] = "Permissions have already been set for this batch of users" 32 | redirect_to batch_invitation_path(@batch_invitation) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/controllers/healthcheck_controller.rb: -------------------------------------------------------------------------------- 1 | class HealthcheckController < ApplicationController 2 | skip_after_action :verify_authorized 3 | 4 | def api_tokens 5 | render json: Healthcheck::ApiTokens.new.to_hash 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/oauth_users_controller.rb: -------------------------------------------------------------------------------- 1 | class OauthUsersController < ApplicationController 2 | before_action :doorkeeper_authorize! 3 | before_action :validate_token_matches_client_id 4 | skip_after_action :verify_authorized 5 | 6 | def show 7 | current_resource_owner.permissions_synced!(application_making_request) 8 | respond_to do |format| 9 | format.json do 10 | presenter = UserOAuthPresenter.new(current_resource_owner, application_making_request) 11 | render json: presenter.as_hash.to_json 12 | end 13 | end 14 | end 15 | 16 | private 17 | 18 | def validate_token_matches_client_id 19 | # FIXME: Once gds-sso is updated everywhere, this should always validate 20 | # the client_id param. It should 401 if no client_id is given. 21 | if params[:client_id].present? && (params[:client_id] != doorkeeper_token.application.uid) 22 | head :unauthorized 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/controllers/organisations_controller.rb: -------------------------------------------------------------------------------- 1 | class OrganisationsController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | respond_to :html 5 | 6 | def index 7 | authorize Organisation 8 | @organisations = policy_scope(Organisation) 9 | end 10 | 11 | def update 12 | authorize Organisation, :edit? 13 | @organisation = Organisation.find(params[:id]) 14 | if params[:organisation] && params[:organisation][:require_2sv] == "1" 15 | @organisation.update!(require_2sv: true) 16 | else 17 | @organisation.update!(require_2sv: false) 18 | end 19 | redirect_to organisations_path 20 | end 21 | 22 | def edit 23 | authorize Organisation 24 | @organisation = Organisation.find(params[:id]) 25 | render :edit 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/controllers/root_controller.rb: -------------------------------------------------------------------------------- 1 | class RootController < ApplicationController 2 | before_action :authenticate_user!, except: %i[privacy_notice accessibility_statement] 3 | skip_after_action :verify_authorized 4 | 5 | def index 6 | @applications = Doorkeeper::Application.not_api_only.with_home_uri.can_signin(current_user) 7 | end 8 | 9 | def signin_required 10 | @application = Doorkeeper::Application.find_by(id: session.delete(:signin_missing_for_application)) 11 | end 12 | 13 | def privacy_notice; end 14 | 15 | def accessibility_statement; end 16 | 17 | private 18 | 19 | def show_user_research_recruitment_banner? 20 | Rails.application.config.show_user_research_recruitment_banner && 21 | !cookies[:dismiss_user_research_recruitment_banner] && 22 | !current_user.user_research_recruitment_banner_hidden? 23 | end 24 | helper_method :show_user_research_recruitment_banner? 25 | end 26 | -------------------------------------------------------------------------------- /app/controllers/suspensions_controller.rb: -------------------------------------------------------------------------------- 1 | class SuspensionsController < ApplicationController 2 | before_action :authenticate_user!, :load_and_authorize_user 3 | respond_to :html 4 | 5 | def edit 6 | @suspension = Suspension.new(suspend: @user.suspended?, 7 | reason_for_suspension: @user.reason_for_suspension) 8 | end 9 | 10 | def update 11 | @suspension = Suspension.new(suspend: params[:user][:suspended] == "1", 12 | reason_for_suspension: params[:user][:reason_for_suspension], 13 | user: @user, 14 | initiator: current_user, 15 | ip_address: user_ip_address) 16 | 17 | if @suspension.save 18 | flash[:notice] = "#{@user.email} is now #{@user.suspended? ? 'suspended' : 'active'}." 19 | 20 | redirect_to @user.api_user? ? edit_api_user_path(@user) : edit_user_path(@user) 21 | else 22 | render :edit 23 | end 24 | end 25 | 26 | private 27 | 28 | def load_and_authorize_user 29 | @user = ApiUser.find_by(id: params[:id]) || User.find_by(id: params[:id]) 30 | raise ActiveRecord::RecordNotFound if @user.blank? 31 | 32 | authorize @user, :suspension? 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/controllers/two_step_verification_exemptions_controller.rb: -------------------------------------------------------------------------------- 1 | class TwoStepVerificationExemptionsController < ApplicationController 2 | before_action :authenticate_user!, :load_and_authorize_user 3 | 4 | def edit 5 | @exemption = TwoStepVerificationExemption.from_user(@user) 6 | end 7 | 8 | def update 9 | @exemption = TwoStepVerificationExemption.from_params(exemption_params) 10 | if @exemption.valid? 11 | @user.exempt_from_2sv(@exemption.reason, current_user, @exemption.expiry_date) 12 | flash[:notice] = "User exempted from 2-step verification" 13 | redirect_to edit_user_path(@user) 14 | else 15 | render "edit" 16 | end 17 | end 18 | 19 | private 20 | 21 | def exemption_params 22 | params.require(:exemption).permit(:reason, expiry_date: %i[day month year]) 23 | end 24 | 25 | def load_and_authorize_user 26 | @user = User.find(params[:id]) 27 | authorize @user, :exempt_from_two_step_verification? 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/controllers/user_research_recruitment_controller.rb: -------------------------------------------------------------------------------- 1 | class UserResearchRecruitmentController < ApplicationController 2 | USER_RESEARCH_RECRUITMENT_FORM_URL = "https://docs.google.com/forms/d/1Bdu_GqOrSR4j6mbuzXkFTQg6FRktRMQc8Y-q879Mny8/viewform".freeze 3 | 4 | before_action :authenticate_user! 5 | skip_after_action :verify_authorized 6 | 7 | def update 8 | case params[:choice] 9 | when "participate" 10 | current_user.update!(user_research_recruitment_banner_hidden: true) 11 | redirect_to USER_RESEARCH_RECRUITMENT_FORM_URL, allow_other_host: true 12 | when "dismiss-banner" 13 | cookies[:dismiss_user_research_recruitment_banner] = true 14 | redirect_to root_path 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/users/applications_controller.rb: -------------------------------------------------------------------------------- 1 | class Users::ApplicationsController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | def show 5 | user = User.find(params[:user_id]) 6 | authorize [{ user: }], policy_class: Users::ApplicationPolicy 7 | 8 | redirect_to user_applications_path(user) 9 | end 10 | 11 | def index 12 | @user = User.find(params[:user_id]) 13 | authorize [{ user: @user }], policy_class: Users::ApplicationPolicy 14 | 15 | @applications_with_signin = Doorkeeper::Application.not_api_only.can_signin(@user) 16 | @applications_without_signin = Doorkeeper::Application.not_api_only.without_signin_permission_for(@user) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/controllers/users/invitation_resends_controller.rb: -------------------------------------------------------------------------------- 1 | class Users::InvitationResendsController < ApplicationController 2 | before_action :authenticate_user! 3 | before_action :load_user 4 | before_action :authorize_user 5 | before_action :redirect_if_invitation_already_accepted 6 | 7 | def edit; end 8 | 9 | def update 10 | @user.invite!(current_user) 11 | EventLog.record_account_invitation(@user, current_user) 12 | flash[:notice] = "Resent account invitation email to #{@user.email}" 13 | redirect_to edit_user_path(@user) 14 | end 15 | 16 | private 17 | 18 | def load_user 19 | @user = User.find(params[:user_id]) 20 | end 21 | 22 | def authorize_user 23 | authorize(@user, :resend_invitation?) 24 | end 25 | 26 | def redirect_if_invitation_already_accepted 27 | unless @user.invited_but_not_yet_accepted? 28 | flash[:notice] = "Invitation for #{@user.email} has already been accepted" 29 | redirect_to edit_user_path(@user) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/controllers/users/organisations_controller.rb: -------------------------------------------------------------------------------- 1 | class Users::OrganisationsController < ApplicationController 2 | before_action :authenticate_user! 3 | before_action :load_user 4 | before_action :authorize_user 5 | before_action :redirect_to_account_page_if_acting_on_own_user, only: %i[edit] 6 | 7 | def edit; end 8 | 9 | def update 10 | updater = UserUpdate.new(@user, user_params, current_user, user_ip_address) 11 | if updater.call 12 | redirect_to edit_user_path(@user), notice: "Updated user #{@user.email} successfully" 13 | else 14 | render :edit 15 | end 16 | end 17 | 18 | private 19 | 20 | def load_user 21 | @user = User.find(params[:user_id]) 22 | end 23 | 24 | def authorize_user 25 | authorize(@user, :assign_organisation?) 26 | end 27 | 28 | def user_params 29 | params.require(:user).permit(:organisation_id) 30 | end 31 | 32 | def redirect_to_account_page_if_acting_on_own_user 33 | redirect_to edit_account_organisation_path if current_user == @user 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/controllers/users/roles_controller.rb: -------------------------------------------------------------------------------- 1 | class Users::RolesController < ApplicationController 2 | before_action :authenticate_user! 3 | before_action :load_user 4 | before_action :authorize_user 5 | before_action :redirect_to_account_page_if_acting_on_own_user, only: %i[edit] 6 | 7 | def edit; end 8 | 9 | def update 10 | updater = UserUpdate.new(@user, user_params, current_user, user_ip_address) 11 | if updater.call 12 | redirect_to edit_user_path(@user), notice: "Updated user #{@user.email} successfully" 13 | else 14 | render :edit 15 | end 16 | end 17 | 18 | private 19 | 20 | def load_user 21 | @user = User.find(params[:user_id]) 22 | end 23 | 24 | def authorize_user 25 | authorize(@user, :assign_role?) 26 | end 27 | 28 | def user_params 29 | params.require(:user).permit(:role) 30 | end 31 | 32 | def redirect_to_account_page_if_acting_on_own_user 33 | redirect_to edit_account_role_path if current_user == @user 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/controllers/users/two_step_verification_mandations_controller.rb: -------------------------------------------------------------------------------- 1 | class Users::TwoStepVerificationMandationsController < ApplicationController 2 | before_action :authenticate_user! 3 | before_action :load_user 4 | before_action :authorize_user 5 | 6 | def edit; end 7 | 8 | def update 9 | user_params = { require_2sv: true } 10 | updater = UserUpdate.new(@user, user_params, current_user, user_ip_address) 11 | if updater.call 12 | redirect_to edit_user_path(@user), notice: "Updated user #{@user.email} successfully" 13 | else 14 | render :edit 15 | end 16 | end 17 | 18 | private 19 | 20 | def load_user 21 | @user = User.find(params[:user_id]) 22 | end 23 | 24 | def authorize_user 25 | authorize(@user, :mandate_2sv?) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/controllers/users/two_step_verification_resets_controller.rb: -------------------------------------------------------------------------------- 1 | class Users::TwoStepVerificationResetsController < ApplicationController 2 | before_action :authenticate_user! 3 | before_action :load_user 4 | before_action :authorize_user 5 | before_action :redirect_to_account_page_if_acting_on_own_user, only: %i[edit] 6 | 7 | def edit; end 8 | 9 | def update 10 | @user.reset_2sv!(current_user) 11 | UserMailer.two_step_reset(@user).deliver_later 12 | 13 | redirect_to edit_user_path(@user), notice: "Reset 2-step verification for #{@user.email}" 14 | end 15 | 16 | private 17 | 18 | def load_user 19 | @user = User.find(params[:user_id]) 20 | end 21 | 22 | def authorize_user 23 | authorize(@user, :reset_2sv?) 24 | end 25 | 26 | def redirect_to_account_page_if_acting_on_own_user 27 | redirect_to two_step_verification_path if current_user == @user 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/controllers/users/unlockings_controller.rb: -------------------------------------------------------------------------------- 1 | class Users::UnlockingsController < ApplicationController 2 | before_action :authenticate_user! 3 | before_action :load_user 4 | before_action :authorize_user 5 | before_action :redirect_if_already_unlocked 6 | 7 | def edit; end 8 | 9 | def update 10 | @user.unlock_access! 11 | EventLog.record_event(@user, EventLog::MANUAL_ACCOUNT_UNLOCK, initiator: current_user, ip_address: user_ip_address) 12 | flash[:notice] = "Unlocked #{@user.email}" 13 | redirect_to edit_user_path(@user) 14 | end 15 | 16 | private 17 | 18 | def load_user 19 | @user = User.find(params[:user_id]) 20 | end 21 | 22 | def authorize_user 23 | authorize(@user, :unlock?) 24 | end 25 | 26 | def redirect_if_already_unlocked 27 | unless @user.access_locked? 28 | flash[:notice] = "#{@user.email} is already unlocked" 29 | redirect_to edit_user_path(@user) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/helpers/account_helper.rb: -------------------------------------------------------------------------------- 1 | module AccountHelper 2 | def two_step_verification_page_title 3 | if current_user.has_2sv? 4 | "Change your 2-step verification phone" 5 | else 6 | "Set up 2-step verification" 7 | end 8 | end 9 | 10 | def role_page_title 11 | if policy(%i[account roles]).update? 12 | "Change your role" 13 | else 14 | "View your role" 15 | end 16 | end 17 | 18 | def organisation_page_title 19 | if policy(%i[account organisations]).update? 20 | "Change your organisation" 21 | else 22 | "View your organisation" 23 | end 24 | end 25 | 26 | def current_user_organisation_name 27 | current_user.organisation&.name_with_abbreviation || "No organisation" 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/helpers/api_users_helper.rb: -------------------------------------------------------------------------------- 1 | module ApiUsersHelper 2 | def truncate_access_token(token) 3 | raw "#{token[0..7]}#{'•' * 24}#{token[-8..]}" 4 | end 5 | 6 | def api_user_name(user) 7 | link_to(user.name, edit_api_user_path(user), class: "govuk-link") 8 | end 9 | 10 | def application_list(user) 11 | content_tag(:ul, class: "govuk-list") do 12 | safe_join( 13 | visible_applications(user).map do |application| 14 | next unless user.permissions_for(application).any? 15 | 16 | content_tag(:li, application.name) 17 | end, 18 | ) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/helpers/application_access_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationAccessHelper 2 | def access_granted_description(application_id, user = current_user) 3 | application = Doorkeeper::Application.find_by(id: application_id) 4 | return nil unless application 5 | 6 | return "You have been granted access to #{application.name}." if user == current_user 7 | 8 | "#{user.name} has been granted access to #{application.name}." 9 | end 10 | 11 | def access_removed_description(application_id, user = current_user) 12 | application = Doorkeeper::Application.find_by(id: application_id) 13 | return nil unless application 14 | 15 | return "Your access to #{application.name} has been removed." if user == current_user 16 | 17 | "#{user.name}'s access to #{application.name} has been removed." 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | require "addressable/uri" 2 | 3 | module ApplicationHelper 4 | def nav_link(text, link) 5 | recognized = Rails.application.routes.recognize_path(link) 6 | if recognized[:controller] == params[:controller] && 7 | recognized[:action] == params[:action] 8 | tag.li(class: "active") do 9 | link_to(text, link) 10 | end 11 | else 12 | tag.li do 13 | link_to(text, link) 14 | end 15 | end 16 | end 17 | 18 | SENSITIVE_QUERY_PARAMETERS = %w[reset_password_token invitation_token].freeze 19 | 20 | def sensitive_query_parameters? 21 | (request.query_parameters.keys & SENSITIVE_QUERY_PARAMETERS).any? 22 | end 23 | 24 | def sanitised_fullpath 25 | uri = Addressable::URI.parse(request.fullpath) 26 | uri.query_values = uri.query_values.reject { |key, _value| SENSITIVE_QUERY_PARAMETERS.include?(key) } 27 | uri.to_s 28 | end 29 | 30 | def with_checked_options_at_top(options) 31 | options.sort_by { |o| o[:checked] ? 0 : 1 } 32 | end 33 | 34 | def govuk_tag(text, classes = nil) 35 | css_classes = ["govuk-tag", classes].compact.join(" ") 36 | tag.strong(text, class: css_classes) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/helpers/batch_invitations_helper.rb: -------------------------------------------------------------------------------- 1 | module BatchInvitationsHelper 2 | def batch_invite_status_message(batch_invitation) 3 | if batch_invitation.in_progress? 4 | "In progress. " \ 5 | "#{batch_invitation.batch_invitation_users.processed.count} of " \ 6 | "#{batch_invitation.batch_invitation_users.count} " \ 7 | "users processed." 8 | elsif batch_invitation.all_successful? 9 | "#{batch_invitation.batch_invitation_users.count} users processed." 10 | elsif !batch_invitation.has_permissions? 11 | "Batch invitation doesn't have any permissions yet." 12 | else 13 | "#{pluralize(batch_invitation.batch_invitation_users.failed.count, 'error')} out of " \ 14 | "#{batch_invitation.batch_invitation_users.count} " \ 15 | "users processed." 16 | end 17 | end 18 | 19 | def batch_invite_organisation_for_user(batch_invitation_user) 20 | Organisation.find(batch_invitation_user.organisation_id).name 21 | rescue BatchInvitationUser::InvalidOrganisationSlug, ActiveRecord::RecordNotFound 22 | "" 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/helpers/organisation_helper.rb: -------------------------------------------------------------------------------- 1 | module OrganisationHelper 2 | def options_for_organisation_select(selected_id: nil) 3 | [{ text: Organisation::NONE, value: nil }] + policy_scope(Organisation).not_closed.map do |organisation| 4 | { text: organisation.name_with_abbreviation, value: organisation.id }.tap do |option| 5 | option[:selected] = true if option[:value] == selected_id 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/helpers/root_helper.rb: -------------------------------------------------------------------------------- 1 | module RootHelper 2 | def gds_only_application_and_non_gds_user?(application) 3 | application.present? && application.gds_only? && !current_user.belongs_to_gds? 4 | end 5 | 6 | def signin_required_title(application) 7 | if application.blank? || gds_only_application_and_non_gds_user?(application) 8 | "You don’t have permission to use this app." 9 | else 10 | "You don’t have permission to sign in to #{application.name}." 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/helpers/users_with_access_helper.rb: -------------------------------------------------------------------------------- 1 | module UsersWithAccessHelper 2 | def formatted_user_name(user) 3 | status = if user.invited_but_not_yet_accepted? 4 | "(invited)" 5 | elsif user.suspended? 6 | "(suspended)" 7 | elsif user.access_locked? 8 | "(access locked)" 9 | end 10 | 11 | link = link_to(user.name, edit_user_path(user), class: "govuk-link") 12 | 13 | [link, status].compact.join(" ").html_safe 14 | end 15 | 16 | def user_name_format(user) 17 | if user.unusable_account? 18 | "line-through" 19 | end 20 | end 21 | 22 | def formatted_last_sign_in(user) 23 | if user.current_sign_in_at 24 | "#{time_ago_in_words(user.current_sign_in_at)} ago" 25 | else 26 | "never signed in" 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/jobs/batch_invitation_job.rb: -------------------------------------------------------------------------------- 1 | class BatchInvitationJob < ApplicationJob 2 | def perform(id, options = {}) 3 | BatchInvitation.find(id).perform(options) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/jobs/permission_updater.rb: -------------------------------------------------------------------------------- 1 | require "sso_push_client" 2 | 3 | class PermissionUpdater < PushUserUpdatesJob 4 | def perform(uid, application_id) 5 | user = User.find_by(uid:) 6 | application = Doorkeeper::Application.find_by(id: application_id) 7 | # It's possible they've been deleted between when the job was scheduled and run. 8 | return if user.nil? || application.nil? 9 | return unless application.supports_push_updates? 10 | 11 | api = SSOPushClient.new(application) 12 | presenter = UserOAuthPresenter.new(user, application) 13 | api.update_user(user.uid, presenter.as_hash) 14 | 15 | user.permissions_synced!(application) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/jobs/push_user_updates_job.rb: -------------------------------------------------------------------------------- 1 | class PushUserUpdatesJob < ApplicationJob 2 | include ActiveJob::Retry.new(strategy: :exponential, limit: 6) 3 | 4 | def perform(*_args) 5 | raise NotImplementedError, "PushUserUpdatesJob must be subclassed" 6 | end 7 | 8 | class << self 9 | def perform_on(user) 10 | user.authorised_applications 11 | .each { |application| perform_later(user.uid, application.id) } 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/jobs/reauth_enforcer.rb: -------------------------------------------------------------------------------- 1 | require "sso_push_client" 2 | 3 | class ReauthEnforcer < PushUserUpdatesJob 4 | def perform(uid, application_id) 5 | application = Doorkeeper::Application.find_by(id: application_id) 6 | # It's possible the application has been deleted between when the job was scheduled and run. 7 | return if application.nil? 8 | return unless application.supports_push_updates? 9 | 10 | api = SSOPushClient.new(application) 11 | api.reauth_user(uid) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/mailers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphagov/signon/f1093373f0cbde6947ecee93030bac5bbcf2f4e1/app/mailers/.gitkeep -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < Mail::Notify::Mailer 2 | def template_id 3 | @template_id ||= ENV.fetch("GOVUK_NOTIFY_TEMPLATE_ID", "fake-test-template-id") 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/mailers/mailer_helper.rb: -------------------------------------------------------------------------------- 1 | module MailerHelper 2 | def email_from 3 | "#{app_name} <#{email_from_address}>" 4 | end 5 | 6 | def app_name 7 | I18n.t("mailer.app_name.instance", instance_name: GovukEnvironment.name) 8 | end 9 | 10 | def email_from_address 11 | if GovukEnvironment.production? 12 | I18n.t("mailer.email_from_address.no_instance") 13 | else 14 | I18n.t("mailer.email_from_address.instance", instance_name: GovukEnvironment.name.parameterize) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/mailers/noisy_batch_invitation.rb: -------------------------------------------------------------------------------- 1 | class NoisyBatchInvitation < ApplicationMailer 2 | include MailerHelper 3 | 4 | default from: proc { email_from } 5 | default to: I18n.t("noisy_batch_invitation_mailer.to") 6 | 7 | def make_noise(batch_invitation) 8 | @user = batch_invitation.user 9 | @batch_invitation = batch_invitation 10 | 11 | user_count = batch_invitation.batch_invitation_users.count 12 | subject = "[SIGNON] #{@user.name} created a batch of #{user_count} users" 13 | subject << " in #{GovukEnvironment.name}" unless GovukEnvironment.production? 14 | view_mail(template_id, to: I18n.t("noisy_batch_invitation_mailer.to"), subject:) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphagov/signon/f1093373f0cbde6947ecee93030bac5bbcf2f4e1/app/models/.gitkeep -------------------------------------------------------------------------------- /app/models/api_user.rb: -------------------------------------------------------------------------------- 1 | class ApiUser < User 2 | default_scope { where(api_user: true).order(:name) } 3 | 4 | validate :reason_for_2sv_exemption_blank 5 | validate :require_2sv_is_false 6 | 7 | DEFAULT_TOKEN_LIFE = 2.years.to_i 8 | 9 | def self.build(attributes = {}) 10 | password = SecureRandom.urlsafe_base64 11 | new(attributes.merge(password:, password_confirmation: password)).tap do |u| 12 | u.skip_confirmation! 13 | u.api_user = true 14 | end 15 | end 16 | 17 | private 18 | 19 | def require_2sv_is_false 20 | errors.add(:require_2sv, "can't be true for api user") if require_2sv 21 | end 22 | 23 | def reason_for_2sv_exemption_blank 24 | errors.add(:reason_for_2sv_exemption, "can't be present for api user") if exempt_from_2sv? 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/models/batch_invitation_application_permission.rb: -------------------------------------------------------------------------------- 1 | class BatchInvitationApplicationPermission < ApplicationRecord 2 | belongs_to :batch_invitation, inverse_of: :batch_invitation_application_permissions 3 | belongs_to :supported_permission 4 | 5 | validates :batch_invitation, :supported_permission, presence: true 6 | validates :supported_permission_id, uniqueness: { scope: :batch_invitation_id, case_sensitive: true } 7 | end 8 | -------------------------------------------------------------------------------- /app/models/doorkeeper/access_grant.rb: -------------------------------------------------------------------------------- 1 | module Doorkeeper 2 | class AccessGrant < ::ActiveRecord::Base # rubocop:disable Rails/ApplicationRecord 3 | include Models::ExpirationTimeSqlMath 4 | 5 | scope :expired, -> { where.not(expires_in: nil).where("#{sanitize_sql(expiration_time_sql)} < ?", Time.current) } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/doorkeeper/access_token.rb: -------------------------------------------------------------------------------- 1 | module Doorkeeper 2 | class AccessToken < ::ActiveRecord::Base # rubocop:disable Rails/ApplicationRecord 3 | scope :not_revoked, -> { where(revoked_at: nil) } 4 | scope :expires_after, ->(time) { where.not(expires_in: nil).where("#{sanitize_sql(expiration_time_sql)} > ?", time) } 5 | scope :expired, -> { where.not(expires_in: nil).where("#{sanitize_sql(expiration_time_sql)} < ?", Time.current) } 6 | scope :ordered_by_expires_at, -> { order(expiration_time_sql) } 7 | scope :ordered_by_application_name, -> { includes(:application).merge(Doorkeeper::Application.ordered_by_name) } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/models/doorkeeper/after_successful_authorization_processor.rb: -------------------------------------------------------------------------------- 1 | module Doorkeeper 2 | class AfterSuccessfulAuthorizationProcessor 3 | def initialize(controller, context) 4 | @controller = controller 5 | @context = context 6 | end 7 | 8 | def process 9 | return unless @controller.instance_of?(Doorkeeper::TokensController) 10 | return unless application && user 11 | 12 | EventLog.record_event(user, EventLog::SUCCESSFUL_USER_APPLICATION_AUTHORIZATION, application:) 13 | end 14 | 15 | private 16 | 17 | def token 18 | @context.auth.token 19 | end 20 | 21 | def application 22 | Doorkeeper::Application.find_by(id: token.application_id) 23 | end 24 | 25 | def user 26 | User.find_by(id: token.resource_owner_id) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/models/govuk_environment.rb: -------------------------------------------------------------------------------- 1 | class GovukEnvironment 2 | def self.name 3 | if Rails.env.development? || Rails.env.test? 4 | "development" 5 | else 6 | ENV.fetch("GOVUK_ENVIRONMENT") 7 | end 8 | end 9 | 10 | def self.production? 11 | name == "production" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/legacy_users_filter.rb: -------------------------------------------------------------------------------- 1 | class LegacyUsersFilter 2 | PARAM_KEYS = %i[status two_step_status role permission organisation].freeze 3 | LEGACY_TWO_STEP_STATUS_VS_TWO_STEP_STATUS = { 4 | "true" => User::TWO_STEP_STATUS_ENABLED, 5 | "false" => User::TWO_STEP_STATUS_NOT_SET_UP, 6 | "exempt" => User::TWO_STEP_STATUS_EXEMPTED, 7 | }.freeze 8 | 9 | def initialize(options = {}) 10 | @options = options 11 | end 12 | 13 | def redirect? 14 | !@options.slice(*PARAM_KEYS).empty? 15 | end 16 | 17 | def options 18 | @options.except(*PARAM_KEYS).tap do |o| 19 | o[:statuses] = [@options[:status]] if @options[:status].present? 20 | o[:two_step_statuses] = [two_step_status_from(@options[:two_step_status])] if @options[:two_step_status].present? 21 | o[:roles] = [@options[:role]] if @options[:role].present? 22 | o[:organisations] = [@options[:organisation]] if @options[:organisation].present? 23 | o[:permissions] = [@options[:permission]] if @options[:permission].present? 24 | end 25 | end 26 | 27 | private 28 | 29 | def two_step_status_from(legacy_two_step_status) 30 | LEGACY_TWO_STEP_STATUS_VS_TWO_STEP_STATUS[legacy_two_step_status] 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/models/log_entry.rb: -------------------------------------------------------------------------------- 1 | class LogEntry 2 | attr_reader :id, :description 3 | 4 | def initialize(id:, description:, require_uid: false, require_initiator: false, require_application: false, access_limited: false) 5 | @id = id 6 | @description = description 7 | @require_uid = require_uid 8 | @require_initiator = require_initiator 9 | @require_application = require_application 10 | @access_limited = access_limited 11 | end 12 | 13 | def require_uid? 14 | @require_uid 15 | end 16 | 17 | def require_initiator? 18 | @require_initiator 19 | end 20 | 21 | def require_application? 22 | @require_application 23 | end 24 | 25 | def access_limited? 26 | @access_limited 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/models/old_password.rb: -------------------------------------------------------------------------------- 1 | class OldPassword < ApplicationRecord 2 | belongs_to :password_archivable, polymorphic: true 3 | end 4 | -------------------------------------------------------------------------------- /app/models/organisation.rb: -------------------------------------------------------------------------------- 1 | class Organisation < ApplicationRecord 2 | GDS_ORG_CONTENT_ID = "af07d5a5-df63-4ddc-9383-6a666845ebe9".freeze 3 | NONE = "None".freeze 4 | 5 | has_ancestry 6 | 7 | has_many :users 8 | 9 | validates :slug, presence: true, uniqueness: { case_sensitive: true } 10 | validates :content_id, presence: true 11 | validates :name, presence: true 12 | validates :organisation_type, presence: true 13 | 14 | before_save :strip_whitespace_from_name 15 | 16 | scope :closed, -> { where(closed: true) } 17 | scope :not_closed, -> { where(closed: false) } 18 | 19 | default_scope { order(arel_table[:closed].asc, name: :asc) } 20 | 21 | def name_with_abbreviation(indicate_closed: true) 22 | return_value = if abbreviation.present? && abbreviation != name 23 | "#{name} - #{abbreviation}" 24 | else 25 | name 26 | end 27 | 28 | return_value += " (closed)" if indicate_closed && closed? 29 | 30 | return_value 31 | end 32 | 33 | private 34 | 35 | def strip_whitespace_from_name 36 | name.strip! 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/models/reject_non_governmental_email_addresses_validator.rb: -------------------------------------------------------------------------------- 1 | class RejectNonGovernmentalEmailAddressesValidator < ActiveModel::EachValidator 2 | NON_GOVERNMENTAL_EMAIL_DOMAIN_KEYWORDS = %w[ 3 | aol btinternet gmail hotmail outlook yahoo 4 | ].freeze 5 | 6 | MESSAGE = "not accepted. Please enter a workplace email to continue.".freeze 7 | 8 | def validate_each(record, attribute, value) 9 | return if value.blank? 10 | 11 | domain_part = value.split("@").last 12 | 13 | return if domain_part.blank? 14 | 15 | if keyword_matchers.any? { |keyword| keyword.match?(domain_part) } 16 | record.errors.add(attribute, options[:message] || MESSAGE) 17 | end 18 | end 19 | 20 | private 21 | 22 | def keyword_matchers 23 | NON_GOVERNMENTAL_EMAIL_DOMAIN_KEYWORDS.map { |keyword| /\b#{keyword}\b/ } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/models/suspension.rb: -------------------------------------------------------------------------------- 1 | class Suspension 2 | include ActiveModel::Validations 3 | validates :reason_for_suspension, presence: true, if: :suspend 4 | 5 | attr_reader :suspend, :reason_for_suspension, :user, :initiator, :ip_address 6 | 7 | def initialize(suspend: nil, reason_for_suspension: nil, user: nil, initiator: nil, ip_address: nil) 8 | @suspend = suspend 9 | @reason_for_suspension = reason_for_suspension 10 | @user = user 11 | @initiator = initiator 12 | @ip_address = ip_address 13 | end 14 | 15 | def save 16 | return false unless valid? 17 | 18 | if suspend 19 | user.suspend(reason_for_suspension) 20 | else 21 | user.unsuspend 22 | end 23 | 24 | EventLog.record_event(user, action, initiator:, ip_address:) 25 | PermissionUpdater.perform_on(user) 26 | ReauthEnforcer.perform_on(user) 27 | end 28 | alias_method :save!, :save 29 | 30 | def suspended? 31 | suspend 32 | end 33 | 34 | private 35 | 36 | def action 37 | if suspend 38 | EventLog::ACCOUNT_SUSPENDED 39 | else 40 | EventLog::ACCOUNT_UNSUSPENDED 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/models/user_agent.rb: -------------------------------------------------------------------------------- 1 | class UserAgent < ApplicationRecord 2 | validates :user_agent_string, presence: true 3 | end 4 | -------------------------------------------------------------------------------- /app/models/user_application_permission.rb: -------------------------------------------------------------------------------- 1 | class UserApplicationPermission < ApplicationRecord 2 | belongs_to :user 3 | belongs_to :application, class_name: "Doorkeeper::Application" 4 | belongs_to :supported_permission 5 | 6 | validates :user, :supported_permission, :application, presence: true 7 | validates :supported_permission_id, uniqueness: { scope: %i[user_id application_id], case_sensitive: true } 8 | 9 | before_validation :assign_application_id 10 | 11 | private 12 | 13 | # application_id is duplicated across supported_permissions and user_application_permissions 14 | # to efficiently address common queries for a user's permissions for a particular application 15 | def assign_application_id 16 | self.application_id = supported_permission.application_id if supported_permission.present? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/models/user_update_permission_builder.rb: -------------------------------------------------------------------------------- 1 | class UserUpdatePermissionBuilder 2 | def initialize(user:, updatable_permission_ids:, selected_permission_ids:) 3 | @user = user 4 | @updatable_permission_ids = updatable_permission_ids 5 | @selected_permission_ids = selected_permission_ids 6 | end 7 | 8 | def build 9 | permissions_user_has = @user.supported_permissions.pluck(:id) 10 | permissions_to_add = @updatable_permission_ids.intersection(@selected_permission_ids) 11 | permissions_to_remove = @updatable_permission_ids.difference(@selected_permission_ids) 12 | 13 | (permissions_user_has + permissions_to_add - permissions_to_remove).sort 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/policies/account/activities_policy.rb: -------------------------------------------------------------------------------- 1 | class Account::ActivitiesPolicy < BasePolicy 2 | def show? 3 | current_user.present? 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/policies/account/application_policy.rb: -------------------------------------------------------------------------------- 1 | class Account::ApplicationPolicy < BasePolicy 2 | def show? 3 | current_user.govuk_admin? || current_user.publishing_manager? 4 | end 5 | 6 | alias_method :index?, :show? 7 | alias_method :view_permissions?, :index? 8 | 9 | def grant_signin_permission? 10 | current_user.govuk_admin? 11 | end 12 | 13 | def remove_signin_permission? 14 | current_user.has_access_to?(record) && 15 | ( 16 | current_user.govuk_admin? || 17 | current_user.publishing_manager? && record.signin_permission.delegated? 18 | ) 19 | end 20 | 21 | def edit_permissions? 22 | current_user.has_access_to?(record) && (current_user.govuk_admin? || current_user.publishing_manager?) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/policies/account/emails_policy.rb: -------------------------------------------------------------------------------- 1 | class Account::EmailsPolicy < BasePolicy 2 | def edit? 3 | current_user.present? 4 | end 5 | alias_method :update?, :edit? 6 | alias_method :resend_email_change?, :edit? 7 | alias_method :cancel_email_change?, :edit? 8 | end 9 | -------------------------------------------------------------------------------- /app/policies/account/organisations_policy.rb: -------------------------------------------------------------------------------- 1 | class Account::OrganisationsPolicy < BasePolicy 2 | def edit? 3 | current_user.present? 4 | end 5 | 6 | def update? 7 | current_user.govuk_admin? 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/policies/account/passwords_policy.rb: -------------------------------------------------------------------------------- 1 | class Account::PasswordsPolicy < BasePolicy 2 | def edit? 3 | current_user.present? 4 | end 5 | alias_method :update?, :edit? 6 | end 7 | -------------------------------------------------------------------------------- /app/policies/account/roles_policy.rb: -------------------------------------------------------------------------------- 1 | class Account::RolesPolicy < BasePolicy 2 | def edit? 3 | current_user.present? 4 | end 5 | 6 | def update? 7 | current_user.superadmin? 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/policies/account_page_policy.rb: -------------------------------------------------------------------------------- 1 | class AccountPagePolicy < BasePolicy 2 | def show? 3 | current_user.present? 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/policies/api_user_policy.rb: -------------------------------------------------------------------------------- 1 | class ApiUserPolicy < BasePolicy 2 | def new? 3 | current_user.superadmin? 4 | end 5 | alias_method :create?, :new? 6 | alias_method :index?, :new? 7 | alias_method :edit?, :new? 8 | alias_method :update?, :new? 9 | alias_method :revoke?, :new? 10 | alias_method :manage_tokens?, :new? 11 | alias_method :suspension?, :new? 12 | 13 | def resend_email_change? = false 14 | def cancel_email_change? = false 15 | end 16 | -------------------------------------------------------------------------------- /app/policies/application_policy.rb: -------------------------------------------------------------------------------- 1 | class ApplicationPolicy < BasePolicy 2 | def index? 3 | current_user.superadmin? 4 | end 5 | alias_method :edit?, :index? 6 | alias_method :update?, :index? 7 | alias_method :manage_supported_permissions?, :index? 8 | alias_method :users_with_access?, :index? 9 | alias_method :access_logs?, :index? 10 | alias_method :monthly_access_stats?, :index? 11 | end 12 | -------------------------------------------------------------------------------- /app/policies/authorisation_policy.rb: -------------------------------------------------------------------------------- 1 | class AuthorisationPolicy < BasePolicy 2 | def new? 3 | current_user.superadmin? 4 | end 5 | alias_method :create?, :new? 6 | alias_method :edit?, :new? 7 | alias_method :revoke?, :new? 8 | end 9 | -------------------------------------------------------------------------------- /app/policies/base_policy.rb: -------------------------------------------------------------------------------- 1 | class BasePolicy 2 | attr_reader :current_user, :record 3 | 4 | def initialize(current_user, record) 5 | @current_user = current_user 6 | @record = record 7 | end 8 | 9 | def scope 10 | Pundit.policy_scope!(current_user, record.class) 11 | end 12 | 13 | protected 14 | 15 | def record_in_own_organisation? 16 | record.organisation && (record.organisation_id == current_user.organisation_id) 17 | end 18 | 19 | def record_in_child_organisation? 20 | current_user.organisation.subtree.include?(record.organisation) 21 | end 22 | 23 | class Scope 24 | attr_reader :current_user, :scope 25 | 26 | def initialize(current_user, scope) 27 | @current_user = current_user 28 | @scope = scope 29 | end 30 | 31 | def resolve 32 | scope 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/policies/batch_invitation_policy.rb: -------------------------------------------------------------------------------- 1 | class BatchInvitationPolicy < BasePolicy 2 | def new? 3 | return true if current_user.govuk_admin? 4 | 5 | false 6 | end 7 | alias_method :create?, :new? 8 | alias_method :show?, :new? 9 | alias_method :manage_permissions?, :new? 10 | end 11 | -------------------------------------------------------------------------------- /app/policies/organisation_policy.rb: -------------------------------------------------------------------------------- 1 | class OrganisationPolicy < BasePolicy 2 | def index? 3 | current_user.govuk_admin? 4 | end 5 | 6 | def can_assign? 7 | return true if current_user.govuk_admin? 8 | 9 | false 10 | end 11 | 12 | def edit? 13 | current_user.superadmin? 14 | end 15 | 16 | class Scope < ::BasePolicy::Scope 17 | def resolve 18 | if current_user.govuk_admin? 19 | scope.all 20 | else 21 | scope.none 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/policies/supported_permission_policy.rb: -------------------------------------------------------------------------------- 1 | class SupportedPermissionPolicy < BasePolicy 2 | class Scope < ::BasePolicy::Scope 3 | def resolve 4 | if current_user.govuk_admin? 5 | scope.all 6 | elsif current_user.publishing_manager? 7 | scope 8 | .delegated 9 | .joins(:application) 10 | .where(oauth_applications: { id: publishing_manager_manageable_application_ids }) 11 | else 12 | scope.none 13 | end 14 | end 15 | 16 | private 17 | 18 | def publishing_manager_manageable_application_ids 19 | Doorkeeper::Application 20 | .not_api_only 21 | .includes(:supported_permissions) 22 | .can_signin(current_user) 23 | .pluck(:id) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/policies/users/application_policy.rb: -------------------------------------------------------------------------------- 1 | class Users::ApplicationPolicy < BasePolicy 2 | attr_reader :user, :application 3 | 4 | def initialize(current_user, record) 5 | super 6 | 7 | @user = record[:user] 8 | @application = record[:application] 9 | end 10 | 11 | def show? 12 | Pundit.policy(current_user, user).edit? 13 | end 14 | 15 | alias_method :index?, :show? 16 | alias_method :view_permissions?, :show? 17 | 18 | def grant_signin_permission? 19 | return false unless Pundit.policy(current_user, user).edit? 20 | return true if current_user.govuk_admin? 21 | 22 | current_user.publishing_manager? && current_user.has_access_to?(application) && application.signin_permission.delegated? 23 | end 24 | 25 | alias_method :remove_signin_permission?, :grant_signin_permission? 26 | 27 | def edit_permissions? 28 | return false unless Pundit.policy(current_user, user).edit? 29 | return true if current_user.govuk_admin? 30 | 31 | current_user.publishing_manager? && current_user.has_access_to?(application) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/presenters/api/user_presenter.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class UserPresenter 3 | def self.present_many(users) 4 | users.map { |user| present(user) } 5 | end 6 | 7 | def self.present(user) 8 | new(user).present 9 | end 10 | 11 | def present 12 | { 13 | uid: @user.uid, 14 | name: @user.name, 15 | email: @user.email, 16 | organisation: organisation, 17 | } 18 | end 19 | 20 | private 21 | 22 | def initialize(user) 23 | @user = user 24 | end 25 | 26 | def organisation 27 | organisation = @user.organisation 28 | 29 | if organisation 30 | { 31 | content_id: organisation.content_id, 32 | name: organisation.name, 33 | slug: organisation.slug, 34 | } 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/presenters/user_o_auth_presenter.rb: -------------------------------------------------------------------------------- 1 | # Generates a hash suitable for exposing to an application integrating with 2 | # signon for SSO over OAuth. Also used when pushing user updates, which isn't 3 | # part of OAuth. 4 | UserOAuthPresenter = Struct.new(:user, :application) do 5 | def as_hash 6 | { 7 | user: { 8 | uid: user.uid, 9 | name: user.name, 10 | email: user.email, 11 | permissions:, 12 | organisation_slug:, 13 | organisation_content_id:, 14 | disabled: user.suspended?, 15 | }, 16 | } 17 | end 18 | 19 | def permissions 20 | user.suspended? ? [] : user.permissions_for(application) 21 | end 22 | 23 | def organisation_slug 24 | organisation = user.organisation 25 | organisation.nil? ? nil : organisation.slug 26 | end 27 | 28 | def organisation_content_id 29 | organisation = user.organisation 30 | organisation.nil? ? nil : organisation.content_id 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/queries/users_with_access.rb: -------------------------------------------------------------------------------- 1 | class UsersWithAccess 2 | attr_reader :scope, :application 3 | 4 | def initialize(scope, application) 5 | @scope = scope 6 | @application = application 7 | end 8 | 9 | def users 10 | scope 11 | .where(id: authorized_users_user_ids) 12 | .includes(:organisation, application_permissions: :supported_permission) 13 | .order("current_sign_in_at DESC") 14 | end 15 | 16 | private 17 | 18 | def authorized_users_user_ids 19 | UserApplicationPermission.where( 20 | supported_permission: application.signin_permission, 21 | application:, 22 | ).select(:user_id) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/views/account/activities/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "Account access log" %> 2 | 3 | <% content_for :breadcrumbs, 4 | render("govuk_publishing_components/components/breadcrumbs", { 5 | collapse_on_mobile: true, 6 | breadcrumbs: [ 7 | { 8 | title: "Dashboard", 9 | url: root_path, 10 | }, 11 | { 12 | title: "Settings", 13 | url: account_path, 14 | } 15 | ] 16 | }) 17 | %> 18 | 19 | <%= render "shared/event_logs_table", logs: @logs %> 20 | -------------------------------------------------------------------------------- /app/views/account/passwords/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "Change your password" %> 2 | 3 | <% content_for :breadcrumbs, 4 | render("govuk_publishing_components/components/breadcrumbs", { 5 | collapse_on_mobile: true, 6 | breadcrumbs: [ 7 | { 8 | title: "Dashboard", 9 | url: root_path, 10 | }, 11 | { 12 | title: "Settings", 13 | url: account_path, 14 | } 15 | ] 16 | }) 17 | %> 18 | 19 | <% if current_user.errors.count > 0 %> 20 | <% content_for :error_summary do %> 21 | <%= render "govuk_publishing_components/components/error_summary", { 22 | title: "There is a problem", 23 | items: current_user.errors.map do |error| 24 | { 25 | text: error.full_message, 26 | href: "#user_#{error.attribute}", 27 | } 28 | end, 29 | } %> 30 | <% end %> 31 | <% end %> 32 | 33 |
Are you sure you want to remove access to <%= @application.name %>?
20 | 21 | <%= form_with url: account_application_signin_permission_path(@application), method: :delete do |form| %> 22 | 29 | <% end %> 30 | -------------------------------------------------------------------------------- /app/views/api_users/index.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "API users" %> 2 | 3 |6 | Please enter your password to confirm the change of email address from 7 | <%= resource.email %> to <%= resource.unconfirmed_email %>. 8 |
9 | 10 | <%= form_for resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :put, class: 'well'} do |f| %> 11 | <%= hidden_field_tag :confirmation_token, params[:confirmation_token] %> 12 | 13 | <%= render "govuk_publishing_components/components/input", { 14 | label: { 15 | text: "Your password" 16 | }, 17 | name: "user[password]", 18 | type: "password", 19 | autocomplete: "current-password" 20 | } %> 21 | 22 | <%= render "govuk_publishing_components/components/button", { 23 | text: "Confirm change" 24 | } %> 25 | <% end %> 26 |7 | We'll send you an email to create a new password. 8 |
9 | 10 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> 11 | <%= render "govuk_publishing_components/components/input", { 12 | label: { 13 | text: "Email address" 14 | }, 15 | name: "user[email]", 16 | type: "email", 17 | autocomplete: "email" 18 | } %> 19 | 20 | <%= render "govuk_publishing_components/components/button", { 21 | text: "Send email" 22 | } %> 23 | <% end %> 24 |7 | This link to set a new password doesn't work. It may be that the link you 8 | were using has expired or was invalid. 9 |
10 | 11 | <%= render "govuk_publishing_components/components/button", { 12 | text: "Try again", 13 | href: new_user_password_path 14 | } %> 15 |27 | <%= link_to t('devise.links.forgot_password'), new_password_path(resource_name), class: "govuk-link" %> 28 |
29 |Make your account more secure by setting up 2‑step verification. You’ll need to install an app on your phone which will generate a verification code to enter when you sign in.
3 |Set up takes about 5 minutes.
4 |If you’re having problems with 2-step verification, ask an admin or managing editor in your organisation to reset your 2-step verification. If you have any other problems, they can also raise a support request for you.
20 | <% end %> 21 | -------------------------------------------------------------------------------- /app/views/doorkeeper_applications/_application_list.html.erb: -------------------------------------------------------------------------------- 1 | <%= GovukPublishingComponents::AppHelpers::TableHelper.helper(self, "Applications", { caption_classes: "govuk-visually-hidden" }) do |t| %> 2 | <%= t.head do %> 3 | <%= t.header "Name" %> 4 | <%= t.header "Description" %> 5 | <% end %> 6 | 7 | <%= t.body do %> 8 | <% applications.each do |application| %> 9 | <%= t.row do %> 10 | <%= t.cell link_to application.name, edit_doorkeeper_application_path(application), class: "govuk-link" %> 11 | <%= t.cell application.description %> 12 | <% end %> 13 | <% end %> 14 | <% end %> 15 | <% end %> 16 | -------------------------------------------------------------------------------- /app/views/doorkeeper_applications/index.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "Applications" %> 2 | 3 | <%= render "govuk_publishing_components/components/tabs", { 4 | tabs: [ 5 | { 6 | id: "active", 7 | label: "Active applications", 8 | content: render("application_list", applications: @applications.not_api_only) 9 | }, 10 | { 11 | id: "api-only", 12 | label: "API-only applications", 13 | content: render("application_list", applications: @applications.api_only) 14 | }, 15 | { 16 | id: "retired", 17 | label: "Retired applications", 18 | content: render("application_list", applications: @applications.unscoped.retired.ordered_by_name) 19 | } 20 | ] 21 | } %> 22 | -------------------------------------------------------------------------------- /app/views/kaminari/gds/_gap.html.erb: -------------------------------------------------------------------------------- 1 |Hello <%= @user.name %>,
2 | 3 | <%= yield %> 4 |All the best,
5 | 6 |<%= link_to t('department.name'), t('department.url') %> team
7 |<%= t('department.full_name') %>
8 | -------------------------------------------------------------------------------- /app/views/layouts/user_mailer.text.erb: -------------------------------------------------------------------------------- 1 | Hello <%= @user.name %>, 2 | 3 | <%= yield %> 4 | All the best, 5 | 6 | <%= t('department.name') %> team 7 | <%= t('department.full_name') %> 8 | -------------------------------------------------------------------------------- /app/views/noisy_batch_invitation/make_noise.text.erb: -------------------------------------------------------------------------------- 1 | Hello, 2 | 3 | This is to let you know that <%= @user.name %> <%= @user.email %> has uploaded a batch of <%= @batch_invitation.batch_invitation_users.count %> users that will be automatically created. 4 | 5 | If you are an admin (or a superadmin), you can review what's in the batch with this link: 6 | <%= batch_invitation_url(@batch_invitation) %> 7 | 8 | View the user who created the batch here: 9 | <%= edit_user_url(@user) %> 10 | 11 | Kind regards, 12 | <%= t('department.name') %> Signon 13 | -------------------------------------------------------------------------------- /app/views/organisations/_organisation_table.html.erb: -------------------------------------------------------------------------------- 1 | <%= GovukPublishingComponents::AppHelpers::TableHelper.helper(self, "Organisations", { caption_classes: "govuk-visually-hidden" }) do |t| %> 2 | <%= t.head do %> 3 | <%= t.header "Name" %> 4 | <%= t.header "Organisation Type" %> 5 | <%= t.header "Slug" %> 6 | <%= t.header "Parent Organisation" %> 7 | <%= t.header "2-step verification mandated?"%> 8 | <% end %> 9 | 10 | <%= t.body do %> 11 | <% organisations.each do |organisation| %> 12 | <%= t.row do %> 13 | <%= t.cell "#{organisation.name_with_abbreviation(indicate_closed: false)}" %> 14 | <%= t.cell organisation.organisation_type %> 15 | <%= t.cell organisation.slug %> 16 | <% if organisation.parent %> 17 | <%= t.cell organisation.parent.name %> 18 | <% else %> 19 | <%= t.cell "No parent" %> 20 | <% end %> 21 |7 | <% if current_user.belongs_to_gds? %> 8 | Ask your delivery manager if you need access. 9 | <% else %> 10 | Ask your organisation’s main GOV.UK contact if you need access. 11 | <% end %> 12 |
13 |14 | If you think something is wrong, try <%= link_to "signing out", destroy_user_session_path, class: "govuk-link" %> and then 15 | <%- if @application.present? -%> 16 | <%= link_to "back in", @application.home_uri, class: "govuk-link" -%> 17 | <%- else -%> 18 | back in 19 | <%- end %>. 20 |
21 | <% end %> 22 |That email address has been blacklisted by our mail server. This probably means that the email address does not exist, or it has a problem such as the inbox being full.
10 |This technical information might be useful:
11 |
12 | <%= @exception.message %>
13 |
You could navigate back and try again, or follow one of these links:
17 | <% if policy(User).index? %> 18 |<%= link_to "Administer users", users_path %>
19 | <% end %> 20 |<%= link_to "Home", root_path %>
21 |Are you sure you want to remove <%= @user.name %>'s access to <%= @application.name %>?
29 | 30 | <%= form_with url: user_application_signin_permission_path(@user, @application), method: :delete do |form| %> 31 | 38 | <% end %> 39 | -------------------------------------------------------------------------------- /bin/brakeman: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | ARGV.unshift("--ensure-latest") 6 | 7 | load Gem.bin_path("brakeman", "brakeman") 8 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | if ! gem list foreman -i --silent; then 3 | echo "Installing foreman..." 4 | gem install foreman 5 | fi 6 | exec foreman start -f Procfile.dev "$@" 7 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | # explicit rubocop config increases performance slightly while avoiding config confusion. 6 | ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) 7 | 8 | load Gem.bin_path("rubocop", "rubocop") 9 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | APP_ROOT = File.expand_path("..", __dir__) 5 | 6 | def system!(*args) 7 | system(*args, exception: true) 8 | end 9 | 10 | FileUtils.chdir APP_ROOT do 11 | # This script is a way to set up or update your development environment automatically. 12 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 13 | # Add necessary setup steps to this file. 14 | 15 | puts "== Installing dependencies ==" 16 | system("bundle check") || system!("bundle install") 17 | 18 | # puts "\n== Copying sample files ==" 19 | # unless File.exist?("config/database.yml") 20 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 21 | # end 22 | 23 | puts "\n== Preparing database ==" 24 | system! "bin/rails db:prepare" 25 | 26 | puts "\n== Removing old logs and tempfiles ==" 27 | system! "bin/rails log:clear tmp:clear" 28 | 29 | unless ARGV.include?("--skip-server") 30 | puts "\n== Starting development server ==" 31 | STDOUT.flush # flush the output before exec(2) so that it displays 32 | exec "bin/dev" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | yarn = ENV["PATH"].split(File::PATH_SEPARATOR). 5 | select { |dir| File.expand_path(dir) != __dir__ }. 6 | product(["yarn", "yarn.cmd", "yarn.ps1"]). 7 | map { |dir, file| File.expand_path(file, dir) }. 8 | find { |file| File.executable?(file) } 9 | 10 | if yarn 11 | exec yarn, *ARGV 12 | else 13 | $stderr.puts "Yarn executable was not detected in the system." 14 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 15 | exit 1 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Signon::Application 6 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /config/brakeman.ignore: -------------------------------------------------------------------------------- 1 | { 2 | "ignored_warnings": [ 3 | { 4 | "warning_type": "Mass Assignment", 5 | "warning_code": 105, 6 | "fingerprint": "767cb68f550b9a322a8dc311cf19b77e058eac5a546698967b4245c64cb8c32f", 7 | "check_name": "PermitAttributes", 8 | "message": "Potentially dangerous key allowed for mass assignment", 9 | "file": "app/controllers/users/roles_controller.rb", 10 | "line": 31, 11 | "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/", 12 | "code": "params.require(:user).permit(:role)", 13 | "render_path": null, 14 | "location": { 15 | "type": "method", 16 | "class": "Users::RolesController", 17 | "method": "user_params" 18 | }, 19 | "user_input": ":role", 20 | "confidence": "Medium", 21 | "cwe_id": [ 22 | 915 23 | ], 24 | "note": "Changing the user's role is the whole point of this controller" 25 | } 26 | ], 27 | "updated": "2023-12-13 15:23:04 +0000", 28 | "brakeman_version": "6.1.0" 29 | } 30 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: mysql2 3 | encoding: utf8 4 | reconnect: true 5 | username: signonotron2 6 | password: signonotron2 7 | pool: 5 8 | 9 | development: 10 | <<: *default 11 | database: signonotron2_development 12 | url: <%= ENV["DATABASE_URL"] %> 13 | 14 | test: &test 15 | <<: *default 16 | database: signonotron2_test 17 | url: <%= ENV["TEST_DATABASE_URL"] %> 18 | 19 | production: 20 | <<: *default 21 | url: <%= ENV["DATABASE_URL"] %> 22 | -------------------------------------------------------------------------------- /config/devise.yml: -------------------------------------------------------------------------------- 1 | development: 2 | pepper: 'fake-pepper' 3 | secret_key: 'fake-secret-key' 4 | 5 | test: 6 | pepper: 'fake-pepper' 7 | secret_key: 'fake-secret-key' 8 | stretches: 1 9 | 10 | production: 11 | pepper: <%= ENV['DEVISE_PEPPER'] %> 12 | secret_key: <%= ENV['DEVISE_SECRET_KEY'] %> 13 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = "1.0" 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in the app/assets 11 | # folder are already added. 12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 13 | 14 | Rails.application.config.assets.paths << Rails.root.join("node_modules") 15 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code 7 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". 8 | Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] 9 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | GovukContentSecurityPolicy.configure do |policy| 2 | # Ensures the ability to use inline JavaScript without protections. This is 3 | # required for compatibility with govuk_admin_template which both uses script 4 | # tags without nonces and uses jQuery 1.x which requires unsafe-inline in 5 | # some browsers (Firefox is one) 6 | script_policy_with_unsafe_inline = (policy.script_src + ["'unsafe-inline'"]).uniq 7 | policy.script_src(*script_policy_with_unsafe_inline) 8 | end 9 | 10 | # Disable any configured nonce generators so that unsafe-inline directives 11 | # can be used 12 | Rails.application.config.content_security_policy_nonce_generator = nil 13 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /config/initializers/dartsass.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.dartsass.build_options << " --quiet-deps" 2 | -------------------------------------------------------------------------------- /config/initializers/date_formats.rb: -------------------------------------------------------------------------------- 1 | Date::DATE_FORMATS[:short_ordinal] = "%d %B %Y" 2 | Date::DATE_FORMATS[:govuk_date] = "%-e %B %Y" 3 | Date::DATE_FORMATS[:govuk_date_short] = "%-e %b %Y" 4 | 5 | Time::DATE_FORMATS[:short_ordinal] = "%d %B %Y" 6 | Time::DATE_FORMATS[:govuk_date] = "%-I:%M%P, %-e %B %Y" 7 | Time::DATE_FORMATS[:govuk_date_short] = "%-I:%M%P, %-e %b %Y" 8 | Time::DATE_FORMATS[:govuk_time] = "%-I:%M%P" 9 | -------------------------------------------------------------------------------- /config/initializers/doorkeeper_scopes.rb: -------------------------------------------------------------------------------- 1 | Doorkeeper::OAuth::PreAuthorization.class_eval do 2 | alias_method :old_validate_scopes, :validate_scopes 3 | 4 | def scope 5 | @scope.presence || build_scopes 6 | end 7 | 8 | def validate_scopes 9 | return true if scope.blank? 10 | 11 | old_validate_scopes 12 | end 13 | 14 | def validate_params 15 | response_type.blank? ? :response_type : true 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. 4 | # Use this to limit dissemination of sensitive information. 5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. 6 | Rails.application.config.filter_parameters += %i[ 7 | passw secret token _key crypt salt certificate otp ssn cvv cvc 8 | ] 9 | -------------------------------------------------------------------------------- /config/initializers/govuk_error.rb: -------------------------------------------------------------------------------- 1 | GovukError.configure do |config| 2 | config.data_sync_excluded_exceptions << "Mysql2::Error" 3 | end 4 | -------------------------------------------------------------------------------- /config/initializers/i18n.rb: -------------------------------------------------------------------------------- 1 | # this is required to catch errors thrown from calls to I18n directly - primarily mailers 2 | 3 | if Rails.env.test? 4 | module I18n 5 | class AlwaysRaiseExceptionHandler < ExceptionHandler 6 | def call(exception, locale, key, options) 7 | if exception.is_a?(MissingTranslation) 8 | raise exception.to_exception 9 | else 10 | super 11 | end 12 | end 13 | end 14 | end 15 | 16 | I18n.exception_handler = I18n::AlwaysRaiseExceptionHandler.new 17 | end 18 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | ActiveSupport::Inflector.inflections(:en) do |inflect| 14 | inflect.acronym "SSO" 15 | end 16 | -------------------------------------------------------------------------------- /config/initializers/load_model_enhancements.rb: -------------------------------------------------------------------------------- 1 | # We conditionally apply the model enhancements (at the time of writing, only one exists) since the model change 2 | # was breaking a migration when spinning up a clean install. 3 | # 4 | # == SwapMagicEverythingPermissionsForRealOnes: migrating ====================== 5 | # rake aborted! 6 | # An error has occurred, all later migrations canceled: 7 | # 8 | # Mysql2::Error: Table 'signonotron2_development.supported_permissions' doesn't exist: SHOW FIELDS FROM `supported_permissions` 9 | # 10 | unless File.basename($PROGRAM_NAME) == "rake" && ARGV.include?("db:migrate") 11 | Dir[Rails.root.join("app/models/doorkeeper/*.rb")].each do |path| 12 | name = File.basename(path, ".rb") 13 | require "doorkeeper/#{name}" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide HTTP permissions policy. For further 4 | # information see: https://developers.google.com/web/updates/2018/06/feature-policy 5 | 6 | # Rails.application.config.permissions_policy do |policy| 7 | # policy.camera :none 8 | # policy.gyroscope :none 9 | # policy.microphone :none 10 | # policy.usb :none 11 | # policy.fullscreen :self 12 | # policy.payment :self, "https://secure.example.com" 13 | # end 14 | -------------------------------------------------------------------------------- /config/initializers/prometheus.rb: -------------------------------------------------------------------------------- 1 | require "govuk_app_config/govuk_prometheus_exporter" 2 | require "collectors/global_prometheus_collector" 3 | 4 | GovukPrometheusExporter.configure(collectors: [Collectors::GlobalPrometheusCollector]) 5 | -------------------------------------------------------------------------------- /config/initializers/rack_attack.rb: -------------------------------------------------------------------------------- 1 | Rack::Attack.throttle("limit 'POST /users/password' attempts per IP", limit: 20, period: 1.hour) do |request| 2 | if request.path == "/users/password" && request.post? 3 | request.env["action_dispatch.remote_ip"].to_s 4 | end 5 | end 6 | 7 | Rack::Attack.throttled_responder = lambda do |_request| 8 | [429, { "Content-Type" => "text/plain" }, ["Too many requests."]] 9 | end 10 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Signon::Application.config.session_store :cookie_store, 4 | key: "_signonotron2_session", 5 | secure: Rails.env.production?, 6 | httponly: true 7 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/locales/department_specific.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | department: 3 | name: 'GOV.UK' 4 | url: 'https://www.gov.uk/' 5 | full_name: 'Government Digital Service' 6 | devise: 7 | issuer: "GOV.UK Signon" 8 | mailer: 9 | app_name: 10 | instance: "GOV.UK Signon %{instance_name}" 11 | no_instance: "GOV.UK Signon" 12 | email_from_address: 13 | instance: "noreply-signon-%{instance_name}@digital.cabinet-office.gov.uk" 14 | no_instance: "noreply-signon@digital.cabinet-office.gov.uk" 15 | 16 | noisy_batch_invitation_mailer: 17 | to: "signon-alerts@digital.cabinet-office.gov.uk" 18 | 19 | support: 20 | name: 'GOV.UK Support' 21 | url: 'https://support.publishing.service.gov.uk' 22 | -------------------------------------------------------------------------------- /config/locales/devise.security_extension.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | errors: 3 | messages: 4 | taken_in_past: "was used previously. Please choose a different one." 5 | equal_to_current_password: "must be different to the current password!" 6 | devise: 7 | invalid_captcha: "The captcha input is not valid!" 8 | failure: 9 | session_limited: 'Your login credentials were used in another browser. Please sign in again to continue in this browser.' 10 | expired: 'Your account has expired due to inactivity. Please contact the site administrator.' 11 | -------------------------------------------------------------------------------- /config/locales/devise_invitable.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | devise: 3 | invitations: 4 | send_instructions: 'An invitation email has been sent to %{email}.' 5 | invitation_token_invalid: 'The invitation token provided is not valid!' 6 | updated: 'Your password was set successfully. You are now signed in.' 7 | no_invitations_remaining: "No invitations remaining" 8 | mailer: 9 | invitation_instructions: 10 | subject: 'Set up your GOV.UK publishing account' 11 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | require "govuk_app_config/govuk_puma" 2 | GovukPuma.configure_rails(self) 3 | -------------------------------------------------------------------------------- /config/schedule.rb: -------------------------------------------------------------------------------- 1 | # File required to ensure cronjobs are removed 2 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | development: 2 | active_record_encryption: 3 | primary_key: <%= ENV.fetch("ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY", "5iYPUMb6NWxlsyiRGyBrVgfY6ZPhm5ZA") %> 4 | key_derivation_salt: <%= ENV.fetch("ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT", "q8sHPMCYthvtuM5N7mv2438my5v4DPaP") %> 5 | secret_key_base: <%= ENV.fetch("SECRET_KEY_BASE", "101615e3369d108c13f7182caf9bb988fc8f2d8d309ebd16d39b34bece4b4bd0944df576bfa2ff35984e7c447658cc25810540b50759c15c2b94f8ef26867a8a") %> 6 | test: 7 | active_record_encryption: 8 | primary_key: 5iYPUMb6NWxlsyiRGyBrVgfY6ZPhm5ZA 9 | key_derivation_salt: q8sHPMCYthvtuM5N7mv2438my5v4DPaP 10 | secret_key_base: 3fb6b8dc769442c5f268a1fc6d1238e80bd6cf07b240d02ae622bd635a79f19342363291f053c80ac5f0132fe773f73e561022a46324a1310d9f0d368414d369 11 | production: 12 | active_record_encryption: 13 | primary_key: <%= ENV["ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY"] %> 14 | key_derivation_salt: <%= ENV["ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT"] %> 15 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 16 | notify_api_key: <%= ENV["GOVUK_NOTIFY_API_KEY"] %> 17 | -------------------------------------------------------------------------------- /config/sidekiq.yml: -------------------------------------------------------------------------------- 1 | # Configuration file for Sidekiq. 2 | # Options here can still be overridden by cmd line args. 3 | --- 4 | :verbose: true 5 | :concurrency: 2 6 | :queues: 7 | - default 8 | - mailers 9 | - logstream 10 | -------------------------------------------------------------------------------- /config/unicorn.rb: -------------------------------------------------------------------------------- 1 | require "govuk_app_config/govuk_unicorn" 2 | GovukUnicorn.configure(self) 3 | -------------------------------------------------------------------------------- /db/migrate/20240228080000_reencrypt_user_otp_keys.rb: -------------------------------------------------------------------------------- 1 | class ReencryptUserOtpKeys < ActiveRecord::Migration[7.1] 2 | def change 3 | User.find_each(&:encrypt) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20240423105028_remove_user_application_permissions_with_deleted_supported_permission.rb: -------------------------------------------------------------------------------- 1 | class RemoveUserApplicationPermissionsWithDeletedSupportedPermission < ActiveRecord::Migration[7.1] 2 | def change 3 | return unless SupportedPermission.find_by(id: 2316).nil? 4 | 5 | user_application_permissions_to_destroy = UserApplicationPermission.where(supported_permission_id: 2316) 6 | initiator = User.find_by(email: "ynda.jas@digital.cabinet-office.gov.uk") 7 | 8 | ActiveRecord::Base.transaction do 9 | user_application_permissions_to_destroy.each do |user_application_permission| 10 | user = user_application_permission.user 11 | application_id = user_application_permission.application_id 12 | 13 | raise "Could not destroy UserApplicationPermission with ID #{user_application_permission.id}" unless user_application_permission.destroy 14 | 15 | next unless user 16 | 17 | EventLog.record_event( 18 | user, 19 | EventLog::PERMISSIONS_REMOVED, 20 | initiator:, 21 | application_id:, 22 | trailing_message: "(previously deleted permission with ID 2316 removed via migration)", 23 | ) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /db/migrate/20240429130154_remove_user_permissions_with_deleted_permission.rb: -------------------------------------------------------------------------------- 1 | class RemoveUserPermissionsWithDeletedPermission < ActiveRecord::Migration[7.1] 2 | def change 3 | undeleted_permission = 1152 4 | return unless SupportedPermission.find_by(id: undeleted_permission).nil? 5 | 6 | user_application_permissions_to_destroy = UserApplicationPermission.where(supported_permission_id: undeleted_permission) 7 | initiator = User.find_by(email: "callum.knights@digital.cabinet-office.gov.uk") 8 | 9 | ActiveRecord::Base.transaction do 10 | user_application_permissions_to_destroy.each do |user_application_permission| 11 | user = user_application_permission.user 12 | application_id = user_application_permission.application_id 13 | 14 | raise "Could not destroy UserApplicationPermission with ID #{user_application_permission.id}" unless user_application_permission.destroy 15 | 16 | next unless user 17 | 18 | EventLog.record_event( 19 | user, 20 | EventLog::PERMISSIONS_REMOVED, 21 | initiator:, 22 | application_id:, 23 | trailing_message: "(previously deleted permission with ID #{undeleted_permission} removed via migration)", 24 | ) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /db/migrate/20240809112119_index_event_logs_columns.rb: -------------------------------------------------------------------------------- 1 | class IndexEventLogsColumns < ActiveRecord::Migration[7.1] 2 | def change 3 | change_table :event_logs, bulk: true do 4 | add_index :event_logs, :application_id 5 | add_index :event_logs, :event_id 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20241001140623_rename_supported_permission_delegatable_column.rb: -------------------------------------------------------------------------------- 1 | class RenameSupportedPermissionDelegatableColumn < ActiveRecord::Migration[7.2] 2 | def change 3 | rename_column :supported_permissions, :delegatable, :delegated 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | There is a lightweight API endpoint for signon, which returns information about users with 4 | given UUIDs. 5 | 6 | ## Accessing 7 | 8 | To access the API, you must have an API-only Application set up in Signon called "Signon API". 9 | This can be set up like so: 10 | 11 | ```shell 12 | rake applications:create name="Signon API" description="API endpoints for user management in Signon" \ 13 | home_uri="https://signon.integration.publishing.service.gov.uk" \ 14 | redirect_uri="https://signon.integration.publishing.service.gov.uk" \ 15 | api_only="true" 16 | ``` 17 | 18 | You can then create an API user in the Signon UI, grant them access to the Signon API, and 19 | access with the Bearer token like so: 20 | 21 | ```shell 22 | curl --location --globoff 'https://SIGNON_DOMAIN/api/users?uuids[]=c514c7e0-a049-013d-c537-3209197caa3b' \ 23 | --header 'Authorization: Bearer YOUR_BEARER_TOKEN' 24 | ``` 25 | 26 | ## Endpoints 27 | 28 | * [`GET /api/users`](#get-apiusers) 29 | 30 | ### `GET /api/users` 31 | 32 | Lists users for the given `uuids` 33 | 34 | #### Query string parameters 35 | 36 | * `uuids` (required) 37 | * An array of UUIDs to query for 38 | 39 | 40 | -------------------------------------------------------------------------------- /docs/environment-variables.md: -------------------------------------------------------------------------------- 1 | # Environment variables 2 | 3 | ## ActiveRecord Encryption 4 | 5 | Used by ActiveRecord to encrypt values in the database e.g. `User#otp_secret_key`. 6 | 7 | * `ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT` 8 | * `ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY` 9 | 10 | ## Devise 11 | 12 | Used by Devise and/or its extensions to encrypt values in the database e.g. `User#password`. 13 | 14 | * `DEVISE_PEPPER` 15 | * `DEVISE_SECRET_KEY` 16 | 17 | ## GOV.UK Notify 18 | 19 | Used to configure Mail::Notify for use by ActionMailer in sending emails. 20 | 21 | * `GOVUK_NOTIFY_API_KEY` 22 | * `GOVUK_NOTIFY_TEMPLATE_ID` 23 | 24 | ## Rewrite URIs for OAuth Applications 25 | 26 | Used by `Doorkeeper::Application#substituted_uri`. 27 | 28 | * `SIGNON_APPS_URI_SUB_PATTERN` 29 | * `SIGNON_APPS_URI_SUB_REPLACEMENT` 30 | 31 | ## GOV.UK app domain 32 | 33 | Used to configure Google Analytics in the new `app/views/layouts/admin_layout.html.erb`. 34 | 35 | * `GOVUK_APP_DOMAIN` 36 | 37 | ## GOV.UK environment names 38 | 39 | Used to configure `GovukAdminTemplate` and in `Healthcheck::ApiTokens#expiring_tokens`. 40 | 41 | * `GOVUK_ENVIRONMENT` 42 | -------------------------------------------------------------------------------- /lib/abilities.rb: -------------------------------------------------------------------------------- 1 | module Abilities 2 | Dir["#{File.dirname(__FILE__)}/abilities/*.rb"].sort.each { |file| require file } 3 | end 4 | -------------------------------------------------------------------------------- /lib/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphagov/signon/f1093373f0cbde6947ecee93030bac5bbcf2f4e1/lib/assets/.gitkeep -------------------------------------------------------------------------------- /lib/code_verifier.rb: -------------------------------------------------------------------------------- 1 | class CodeVerifier 2 | MAX_2SV_DRIFT_SECONDS = 30 3 | 4 | attr_reader :code, :otp_secret_key 5 | 6 | def initialize(code, otp_secret_key) 7 | @code = code 8 | @otp_secret_key = otp_secret_key 9 | end 10 | 11 | def verify 12 | totp = ROTP::TOTP.new(otp_secret_key) 13 | 14 | totp.verify(clean_code, drift_behind: MAX_2SV_DRIFT_SECONDS) 15 | end 16 | 17 | private 18 | 19 | def clean_code 20 | code.gsub(/[ -]/, "") 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/devise/hooks/two_step_verification.rb: -------------------------------------------------------------------------------- 1 | module Devise::Hooks::TwoStepVerification 2 | Warden::Manager.after_authentication do |user, auth, _options| 3 | if user.need_two_step_verification? 4 | cookie = auth.env["action_dispatch.cookies"].signed["remember_2sv_session"] 5 | valid = cookie && 6 | cookie["user_id"] == user.id && 7 | cookie["valid_until"] > Time.current && 8 | cookie["secret_hash"] == Digest::SHA256.hexdigest(user.otp_secret_key) 9 | unless valid 10 | auth.session(:user)["need_two_step_verification"] = user.need_two_step_verification? 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/exception_handler.rb: -------------------------------------------------------------------------------- 1 | require "sso_push_error" 2 | 3 | module ExceptionHandler 4 | def with_exception_handling 5 | yield 6 | rescue URI::InvalidURIError 7 | raise SSOPushError.new(@application, message: "Invalid URL for application.") 8 | rescue GdsApi::EndpointNotFound, SocketError 9 | raise SSOPushError.new(@application, message: "Couldn't find the application. Maybe the application is down?") 10 | rescue Errno::ETIMEDOUT, Timeout::Error, GdsApi::TimedOutException 11 | raise SSOPushError.new(@application, message: "Timeout connecting to application.") 12 | rescue GdsApi::HTTPErrorResponse => e 13 | raise SSOPushError.new(@application, response_code: e.code) 14 | rescue *network_errors, StandardError => e 15 | raise SSOPushError.new(@application, message: e.message) 16 | end 17 | 18 | def network_errors 19 | [SocketError, Errno::ECONNREFUSED, Errno::EHOSTDOWN, Errno::EHOSTUNREACH] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/expired_not_signed_in_user_deleter.rb: -------------------------------------------------------------------------------- 1 | class ExpiredNotSignedInUserDeleter 2 | def delete 3 | User.expired_never_signed_in.find_each do |user| 4 | EventLog.record_event( 5 | user, 6 | EventLog::ACCOUNT_DELETED, 7 | trailing_message: log_message(user), 8 | ) 9 | 10 | user.destroy! 11 | end 12 | end 13 | 14 | private 15 | 16 | def log_message(user) 17 | "#{user.email} was invited on " \ 18 | "#{user.invitation_sent_at.to_fs(:short_ordinal)} and never signed in" 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/expired_oauth_access_records_deleter.rb: -------------------------------------------------------------------------------- 1 | class ExpiredOauthAccessRecordsDeleter 2 | CLASSES = { 3 | access_grant: Doorkeeper::AccessGrant, 4 | access_token: Doorkeeper::AccessToken, 5 | }.freeze 6 | EVENTS = { 7 | access_grant: EventLog::ACCESS_GRANTS_DELETED, 8 | access_token: EventLog::ACCESS_TOKENS_DELETED, 9 | }.freeze 10 | 11 | def initialize(record_type:) 12 | @record_class = CLASSES.fetch(record_type) 13 | @event = EVENTS.fetch(record_type) 14 | @total_deleted = 0 15 | end 16 | 17 | attr_reader :record_class, :total_deleted 18 | 19 | def delete_expired 20 | @record_class.expired.in_batches do |relation| 21 | records_by_user_id = Doorkeeper::Application.unscoped { relation.includes(:application).group_by(&:resource_owner_id) } 22 | all_users = User.where(id: records_by_user_id.keys) 23 | 24 | all_users.each do |user| 25 | application_names = records_by_user_id[user.id].map { |record| record.application.name } 26 | 27 | EventLog.record_event( 28 | user, 29 | @event, 30 | trailing_message: "for #{application_names.to_sentence}", 31 | ) 32 | end 33 | 34 | @total_deleted += relation.delete_all 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/inactive_users_suspender.rb: -------------------------------------------------------------------------------- 1 | class InactiveUsersSuspender 2 | def suspend 3 | inactive_users = User.not_recently_unsuspended.last_signed_in_before(User::SUSPENSION_THRESHOLD_PERIOD.ago).each do |user| 4 | user.suspended_at = Time.current 5 | user.reason_for_suspension = reason 6 | user.save!(validate: false) 7 | 8 | user.revoke_all_authorisations 9 | 10 | PermissionUpdater.perform_on(user) 11 | ReauthEnforcer.perform_on(user) 12 | 13 | EventLog.record_event(user, EventLog::ACCOUNT_AUTOSUSPENDED) 14 | UserMailer.suspension_notification(user).deliver_now 15 | end 16 | 17 | inactive_users.count 18 | end 19 | 20 | private 21 | 22 | def reason 23 | "User has not logged in for #{User::SUSPENSION_THRESHOLD_PERIOD.inspect} since" \ 24 | " #{(User::SUSPENSION_THRESHOLD_PERIOD + 1.day).ago.strftime('%d %B %Y')}" 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/inactive_users_suspension_reminder_mailing_list.rb: -------------------------------------------------------------------------------- 1 | class InactiveUsersSuspensionReminderMailingList 2 | DAYS_TO_SUSPENSION = [14, 7, 3, 1].freeze 3 | 4 | def initialize(suspension_threshold_period) 5 | @suspension_threshold_period = suspension_threshold_period 6 | end 7 | 8 | def generate 9 | suspension_threshold_exceeded = @suspension_threshold_period + 1.day 10 | 11 | suspension_reminder_mailing_list = DAYS_TO_SUSPENSION.index_with do |days_to_suspension| 12 | User.last_signed_in_on((suspension_threshold_exceeded - days_to_suspension.days).ago) 13 | end 14 | suspension_reminder_mailing_list[1] += User.not_recently_unsuspended.last_signed_in_before(suspension_threshold_exceeded.ago).to_a 15 | suspension_reminder_mailing_list 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/roles.rb: -------------------------------------------------------------------------------- 1 | module Roles 2 | def self.all 3 | [ 4 | Roles::Admin, 5 | Roles::Normal, 6 | Roles::OrganisationAdmin, 7 | Roles::Superadmin, 8 | Roles::SuperOrganisationAdmin, 9 | ].sort_by(&:level) 10 | end 11 | 12 | def self.find(role_name) 13 | all.find { |role| role.name == role_name } 14 | end 15 | 16 | all.each do |klass| 17 | define_method("#{klass.name}?") do 18 | role&.name == klass.name 19 | end 20 | end 21 | 22 | def govuk_admin? 23 | superadmin? || admin? 24 | end 25 | 26 | def publishing_manager? 27 | super_organisation_admin? || organisation_admin? 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/roles/admin.rb: -------------------------------------------------------------------------------- 1 | module Roles 2 | class Admin < Base 3 | def self.permitted_user_params 4 | [ 5 | :uid, 6 | :name, 7 | :email, 8 | :password, 9 | :password_confirmation, 10 | :organisation_id, 11 | :unconfirmed_email, 12 | :confirmation_token, 13 | :require_2sv, 14 | { supported_permission_ids: [] }, 15 | ] 16 | end 17 | 18 | def self.name 19 | "admin" 20 | end 21 | 22 | def self.level 23 | 1 24 | end 25 | 26 | def self.manageable_organisations_for(_) 27 | Organisation.all 28 | end 29 | 30 | def self.require_2sv? 31 | true 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/roles/base.rb: -------------------------------------------------------------------------------- 1 | module Roles 2 | class Base 3 | def self.manageable_roles 4 | Roles.all.select { |role_class| role_class.level >= level }.reverse 5 | end 6 | 7 | def self.can_manage?(role_class) 8 | manageable_roles.include?(role_class) 9 | end 10 | 11 | def self.display_name 12 | name.humanize 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/roles/normal.rb: -------------------------------------------------------------------------------- 1 | module Roles 2 | class Normal < Base 3 | def self.permitted_user_params 4 | %i[uid name email password password_confirmation] 5 | end 6 | 7 | def self.name 8 | "normal" 9 | end 10 | 11 | def self.level 12 | 4 13 | end 14 | 15 | def self.manageable_roles 16 | [] 17 | end 18 | 19 | def self.manageable_organisations_for(_) 20 | Organisation.where("false") 21 | end 22 | 23 | def self.require_2sv? 24 | false 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/roles/organisation_admin.rb: -------------------------------------------------------------------------------- 1 | module Roles 2 | class OrganisationAdmin < Base 3 | def self.permitted_user_params 4 | [ 5 | :uid, 6 | :name, 7 | :email, 8 | :password, 9 | :password_confirmation, 10 | :unconfirmed_email, 11 | :confirmation_token, 12 | :require_2sv, 13 | { supported_permission_ids: [] }, 14 | ] 15 | end 16 | 17 | def self.name 18 | "organisation_admin" 19 | end 20 | 21 | def self.level 22 | 3 23 | end 24 | 25 | def self.manageable_organisations_for(user) 26 | Organisation.where(id: user.organisation) 27 | end 28 | 29 | def self.require_2sv? 30 | true 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/roles/super_organisation_admin.rb: -------------------------------------------------------------------------------- 1 | module Roles 2 | class SuperOrganisationAdmin < Base 3 | def self.permitted_user_params 4 | [ 5 | :uid, 6 | :name, 7 | :email, 8 | :password, 9 | :password_confirmation, 10 | :unconfirmed_email, 11 | :confirmation_token, 12 | :require_2sv, 13 | { supported_permission_ids: [] }, 14 | ] 15 | end 16 | 17 | def self.name 18 | "super_organisation_admin" 19 | end 20 | 21 | def self.level 22 | 2 23 | end 24 | 25 | def self.manageable_organisations_for(user) 26 | Organisation.where(id: user.organisation.subtree) 27 | end 28 | 29 | def self.require_2sv? 30 | true 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/roles/superadmin.rb: -------------------------------------------------------------------------------- 1 | module Roles 2 | class Superadmin < Base 3 | def self.permitted_user_params 4 | [ 5 | :uid, 6 | :name, 7 | :email, 8 | :password, 9 | :password_confirmation, 10 | :organisation_id, 11 | :unconfirmed_email, 12 | :confirmation_token, 13 | :role, 14 | :require_2sv, 15 | { supported_permission_ids: [] }, 16 | ] 17 | end 18 | 19 | def self.name 20 | "superadmin" 21 | end 22 | 23 | def self.level 24 | 0 25 | end 26 | 27 | def self.manageable_organisations_for(_) 28 | Organisation.all 29 | end 30 | 31 | def self.require_2sv? 32 | true 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/signin_permission_granter.rb: -------------------------------------------------------------------------------- 1 | class SigninPermissionGranter 2 | def self.call(users:, application:) 3 | users.each do |user| 4 | puts "Checking user ##{user.id}: #{user.name}" 5 | next if user.has_access_to?(application) 6 | 7 | puts "-- Adding signin permission for #{application.name}" 8 | user.grant_application_signin_permission(application) 9 | 10 | if application.supports_push_updates? 11 | PermissionUpdater.perform_later(user.uid, application.id) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/sso_push_client.rb: -------------------------------------------------------------------------------- 1 | require "gds_api/base" 2 | require "exception_handler" 3 | 4 | class SSOPushClient < GdsApi::Base 5 | include ExceptionHandler 6 | 7 | def initialize(application) 8 | @application = application 9 | super(application.url_without_path, bearer_token: SSOPushCredential.credentials(application)) 10 | end 11 | 12 | def update_user(uid, user) 13 | with_exception_handling do 14 | put_json("#{base_url}/users/#{CGI.escape(uid)}", user) 15 | end 16 | end 17 | 18 | def reauth_user(uid) 19 | with_exception_handling do 20 | post_json("#{base_url}/users/#{CGI.escape(uid)}/reauth", {}) 21 | end 22 | end 23 | 24 | private 25 | 26 | def base_url 27 | "#{@endpoint}/auth/gds/api" 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/sso_push_credential.rb: -------------------------------------------------------------------------------- 1 | class SSOPushCredential 2 | PERMISSIONS = %w[user_update_permission].freeze 3 | USER_NAME = "Signon API Client (permission and suspension updater)".freeze 4 | USER_EMAIL = "signon+permissions@alphagov.co.uk".freeze 5 | 6 | class << self 7 | def credentials(application) 8 | return if application.retired? 9 | 10 | user.grant_application_signin_permission(application) 11 | user.grant_application_permissions(application, PERMISSIONS) 12 | 13 | user.authorisations 14 | .not_expired 15 | .expires_after(4.weeks.from_now) 16 | .create_with(expires_in: 10.years) 17 | .find_or_create_by!(application_id: application.id).token 18 | end 19 | 20 | def user 21 | User.find_by(email: USER_EMAIL) || create_user! 22 | end 23 | 24 | private 25 | 26 | def create_user! 27 | ApiUser.build(name: USER_NAME, email: USER_EMAIL).tap(&:save!) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/sso_push_error.rb: -------------------------------------------------------------------------------- 1 | class SSOPushError < StandardError 2 | def initialize(application, details = {}) 3 | message = "Error pushing to #{application.name}" 4 | message += ", got response #{details[:response_code]}" if details[:response_code] 5 | message += ". #{details[:message]}" if details[:message] 6 | 7 | super(message) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/tasks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphagov/signon/f1093373f0cbde6947ecee93030bac5bbcf2f4e1/lib/tasks/.gitkeep -------------------------------------------------------------------------------- /lib/tasks/assets.rake: -------------------------------------------------------------------------------- 1 | require "sprockets/rails/task" 2 | Sprockets::Rails::Task.new(Rails.application) do |t| 3 | t.log_level = Logger::WARN 4 | end 5 | 6 | Rake::Task["assets:precompile"].enhance(["dartsass:build"]) 7 | -------------------------------------------------------------------------------- /lib/tasks/data_hygiene.rake: -------------------------------------------------------------------------------- 1 | namespace :data_hygiene do 2 | desc "Bulk update the organisations associated with users." 3 | task :bulk_update_organisation, %i[csv_filename] => :environment do |_, args| 4 | unless DataHygiene::BulkOrganisationUpdater.call(args[:csv_filename]) 5 | abort "bulk updating organisations encountered errors" 6 | end 7 | end 8 | 9 | desc "Mark an organisation as closed" 10 | task :close_organisation, %i[content_id] => :environment do |_, args| 11 | organisation = Organisation.find_by(content_id: args[:content_id]) 12 | organisation.update!(closed: true) 13 | puts "Marked organisation #{organisation.slug} as closed" 14 | end 15 | 16 | desc "Move all users from one organisation to another" 17 | task :bulk_update_user_organisation, %i[old_content_id new_content_id] => :environment do |_, args| 18 | old_organisation = Organisation.find_by(content_id: args[:old_content_id]) 19 | new_organisation = Organisation.find_by(content_id: args[:new_content_id]) 20 | 21 | users = User.where(organisation: old_organisation) 22 | users.update_all(organisation_id: new_organisation.id) 23 | 24 | puts "Moved #{users.count} users from #{old_organisation.slug} to #{new_organisation.slug}" 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/tasks/event_log.rake: -------------------------------------------------------------------------------- 1 | require "csv" 2 | 3 | namespace :event_log do 4 | desc "Delete all events in the event log older than 2 years" 5 | task delete_logs_older_than_two_years: :environment do 6 | delete_count = EventLog.where("created_at < ?", 2.years.ago).delete_all 7 | puts "#{delete_count} event log entries deleted" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/tasks/jasmine.rake: -------------------------------------------------------------------------------- 1 | desc "Run Jasmine tests" 2 | task jasmine: :environment do 3 | sh "yarn run jasmine:ci" 4 | end 5 | -------------------------------------------------------------------------------- /lib/tasks/lint.rake: -------------------------------------------------------------------------------- 1 | desc "Run all linters" 2 | task lint: :environment do 3 | sh "yarn run lint" 4 | sh "bundle exec brakeman . --except CheckRenderInline --quiet" 5 | sh "bundle exec rubocop" 6 | end 7 | -------------------------------------------------------------------------------- /lib/tasks/oauth_access_records.rake: -------------------------------------------------------------------------------- 1 | namespace :oauth_access_grants do 2 | desc "Delete expired OAuth access grants" 3 | task delete_expired: "oauth_access_records:delete_expired" 4 | end 5 | 6 | namespace :oauth_access_records do 7 | desc "Delete expired OAuth access grants and tokens" 8 | task delete_expired: :environment do 9 | %i[access_grant access_token].each do |record_type| 10 | deleter = ExpiredOauthAccessRecordsDeleter.new(record_type:) 11 | 12 | deleter.delete_expired 13 | 14 | puts "Deleted #{deleter.total_deleted} expired #{deleter.record_class} records" 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/tasks/organisation_mappings.rake: -------------------------------------------------------------------------------- 1 | namespace :organisation_mappings do 2 | desc "Apply organisation mappings from Zendesk to signon users" 3 | task zendesk_to_signon: :environment do 4 | users_without_organisations = User.where(organisation_id: nil).count 5 | 6 | OrganisationMappings::ZendeskToSignon.apply 7 | 8 | puts "#{users_without_organisations - User.where(organisation_id: nil).count} users were assigned to organisations." 9 | puts "#{User.where(organisation_id: nil).count} users still do not belong to any organisation." 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/tasks/organisations.rake: -------------------------------------------------------------------------------- 1 | namespace :organisations do 2 | desc "Fetch organisations" 3 | task fetch: :environment do 4 | include VolatileLock::DSL 5 | 6 | with_lock("signon:organisations:fetch") do 7 | OrganisationsFetcher.new.call 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/tasks/sync_kubernetes_secrets.rake: -------------------------------------------------------------------------------- 1 | namespace :kubernetes do 2 | desc "Synchronise OAuth Token secrets in Kubernetes" 3 | task sync_token_secrets: :environment do 4 | client = Kubernetes::Client.new 5 | 6 | api_users = ApiUser.where(suspended_at: nil) 7 | 8 | api_users.each do |api_user| 9 | api_user.authorisations.not_revoked.each do |token| 10 | name = "signon-token-#{api_user.name}-#{token.application.name}".parameterize 11 | data = { bearer_token: token.token } 12 | 13 | Rails.logger.info(name) 14 | client.apply_secret(name, data) 15 | end 16 | end 17 | end 18 | 19 | desc "Synchronise OAuth App secrets in Kubernetes" 20 | task sync_app_secrets: :environment do 21 | client = Kubernetes::Client.new 22 | 23 | apps = Doorkeeper::Application.where(retired: false) 24 | 25 | apps.each do |app| 26 | name = "signon-app-#{app.name}".parameterize 27 | data = { oauth_id: app.uid, oauth_secret: app.secret } 28 | 29 | Rails.logger.info(name) 30 | client.apply_secret(name, data) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/unsupported_permission_error.rb: -------------------------------------------------------------------------------- 1 | class UnsupportedPermissionError < StandardError 2 | end 3 | -------------------------------------------------------------------------------- /lib/user_parameter_sanitiser.rb: -------------------------------------------------------------------------------- 1 | class UserParameterSanitiser 2 | def initialize(user_params:, current_user_role:, permitted_params_by_role: default_permitted_params_by_role) 3 | @user_params = user_params 4 | @current_user_role = current_user_role 5 | @permitted_params_by_role = permitted_params_by_role 6 | end 7 | 8 | def sanitise 9 | sanitised_params 10 | end 11 | 12 | private 13 | 14 | attr_reader :user_params, :current_user_role, :permitted_params_by_role 15 | 16 | def sanitised_params 17 | ActionController::Parameters.new(user_params).permit(*permitted_params) 18 | end 19 | 20 | def permitted_params 21 | permitted_params_by_role.fetch(current_user_role, empty_whitelist) 22 | end 23 | 24 | def empty_whitelist 25 | [] 26 | end 27 | 28 | def default_permitted_params_by_role 29 | { 30 | normal: Roles::Normal.permitted_user_params, 31 | organisation_admin: Roles::OrganisationAdmin.permitted_user_params, 32 | super_organisation_admin: Roles::SuperOrganisationAdmin.permitted_user_params, 33 | admin: Roles::Admin.permitted_user_params, 34 | superadmin: Roles::Superadmin.permitted_user_params, 35 | } 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/user_permission_migrator.rb: -------------------------------------------------------------------------------- 1 | class UserPermissionMigrator 2 | def self.migrate(source:, target:) 3 | source_app = Doorkeeper::Application.find_by!(name: source) 4 | target_app = Doorkeeper::Application.find_by!(name: target) 5 | source_app_supported_permissions = source_app.supported_permissions 6 | target_app_supported_permissions = target_app.supported_permissions 7 | 8 | permissions = source_app_supported_permissions.map do |permission| 9 | [permission.name, target_app_supported_permissions.find_by!(name: permission.name)] 10 | end 11 | 12 | permission_mappings = permissions.to_h 13 | 14 | User.all.find_each do |user| 15 | next unless user.has_access_to?(source_app) 16 | 17 | permissions = user.permissions_for(source_app) 18 | permissions.each do |permission| 19 | UserApplicationPermission.create user:, application: target_app, supported_permission: permission_mappings[permission] 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/volatile_lock.rb: -------------------------------------------------------------------------------- 1 | class VolatileLock 2 | class FailedToSetExpiration < StandardError; end 3 | 4 | module DSL 5 | def with_lock(key) 6 | # lock for next 10.minutes 7 | if VolatileLock.new(key).obtained? 8 | yield 9 | else 10 | puts "Skipping task on #{Socket.gethostname}, couldn't obtain lock: #{key}" 11 | end 12 | end 13 | end 14 | 15 | # expiration_time takes care of time-drifts on our 16 | # servers. defaults to 10.minutes assuming our servers 17 | # won't realistically have a greater time-drift. 18 | def initialize(key, expiration_time = 10.minutes) 19 | @key = key 20 | @expiration_time = expiration_time 21 | end 22 | 23 | def obtained? 24 | delete_possibly_stale_keys 25 | 26 | result = redis.setnx(@key, hostname) 27 | result = expire if result 28 | result 29 | end 30 | 31 | private 32 | 33 | def expire 34 | result = redis.expire(@key, @expiration_time) 35 | return true if result 36 | 37 | redis.del(@key) 38 | raise FailedToSetExpiration 39 | end 40 | 41 | def delete_possibly_stale_keys 42 | redis.del(@key) if redis.get(@key) == hostname 43 | end 44 | 45 | def redis 46 | Redis.new 47 | end 48 | 49 | def hostname 50 | Socket.gethostname 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphagov/signon/f1093373f0cbde6947ecee93030bac5bbcf2f4e1/log/.gitkeep -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphagov/signon/f1093373f0cbde6947ecee93030bac5bbcf2f4e1/public/favicon.ico -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphagov/signon/f1093373f0cbde6947ecee93030bac5bbcf2f4e1/public/icon.png -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /script/minitest-ci: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This script is used during the GitHub Actions workflow 4 | # defined in .github/workflows/ci.yml. 5 | # It splits the MiniTest suite into randomly-allocated groups 6 | # which are executed across multiple GitHub Actions 'matrix' nodes. 7 | 8 | tests = Dir["test/**/*_test.rb"] 9 | .sort 10 | .shuffle(random: Random.new(ENV["GITHUB_SHA"].to_i(16))) 11 | .select 12 | .with_index do |_el, i| 13 | i % ENV["CI_NODE_TOTAL"].to_i == ENV["CI_NODE_INDEX"].to_i 14 | end 15 | 16 | exec "bundle exec rails test #{tests.join(' ')}" 17 | -------------------------------------------------------------------------------- /spec/javascripts/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphagov/signon/f1093373f0cbde6947ecee93030bac5bbcf2f4e1/spec/javascripts/helpers/.gitkeep -------------------------------------------------------------------------------- /spec/support/jasmine-browser.json: -------------------------------------------------------------------------------- 1 | { 2 | "srcDir": "public/assets/signon", 3 | "srcFiles": [ 4 | "application-*.js", 5 | "govuk-admin-template-*.js" 6 | ], 7 | "cssFiles": [ 8 | "application-*.css" 9 | ], 10 | "specDir": "spec/javascripts", 11 | "specFiles": [ 12 | "**/*[sS]pec.js" 13 | ], 14 | "helpers": [ 15 | "helpers/*.js" 16 | ], 17 | "browser": "headlessChrome" 18 | } 19 | -------------------------------------------------------------------------------- /test/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphagov/signon/f1093373f0cbde6947ecee93030bac5bbcf2f4e1/test/controllers/.gitkeep -------------------------------------------------------------------------------- /test/controllers/account/organisations_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Account::OrganisationsControllerTest < ActionController::TestCase 4 | setup do 5 | @organisation = create(:organisation) 6 | create(:organisation) 7 | @superadmin_user = create(:superadmin_user) 8 | sign_in @superadmin_user 9 | end 10 | 11 | context "GET edit" do 12 | should "display form with current organisation" do 13 | get :edit 14 | 15 | assert_select "form[action='#{account_organisation_path}']" do 16 | assert_select "select[name='user[organisation_id]']", value: @superadmin_user.organisation_id 17 | end 18 | end 19 | end 20 | 21 | context "PUT update" do 22 | should "display error when validation fails" do 23 | UserUpdate.stubs(:new).returns(stub("UserUpdate", call: false)) 24 | 25 | put :update, params: { user: { organisation_id: @organisation } } 26 | 27 | assert_template :edit 28 | assert_select "*[role='alert']", text: "There was a problem changing your organisation." 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/controllers/account/roles_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Account::RolesControllerTest < ActionController::TestCase 4 | setup do 5 | @superadmin_user = create(:superadmin_user) 6 | sign_in @superadmin_user 7 | end 8 | 9 | context "GET edit" do 10 | should "display form with current role" do 11 | get :edit 12 | 13 | assert_select "form[action='#{account_role_path}']" do 14 | assert_select "select[name='user[role]']", value: @superadmin_user.role 15 | end 16 | end 17 | end 18 | 19 | context "PUT update" do 20 | should "display error when validation fails" do 21 | UserUpdate.stubs(:new).returns(stub("UserUpdate", call: false)) 22 | 23 | put :update, params: { user: { role: Roles::Normal.name } } 24 | 25 | assert_template :edit 26 | assert_select "*[role='alert']", text: "There was a problem changing your role." 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/controllers/doorkeeper/tokens_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Doorkeeper::TokensControllerTest < ActionController::TestCase 4 | should "handle OAuth v1 requests to create a token" do 5 | assert_recognizes({ controller: "doorkeeper/tokens", action: "create" }, { path: "oauth/access_token", method: :post }) 6 | end 7 | 8 | should "handle OAuth v2 requests to create a token" do 9 | assert_recognizes({ controller: "doorkeeper/tokens", action: "create" }, { path: "oauth/token", method: :post }) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/controllers/fixtures/empty_users.csv: -------------------------------------------------------------------------------- 1 | Name,Email 2 | -------------------------------------------------------------------------------- /test/controllers/fixtures/invalid_users.csv: -------------------------------------------------------------------------------- 1 | Name,Email 2 | '10",a@mail.com 3 | -------------------------------------------------------------------------------- /test/controllers/fixtures/no_headers_users.csv: -------------------------------------------------------------------------------- 1 | Arthur Dent,a@hhg.com 2 | Tricia McMillan,t@hhg.com 3 | -------------------------------------------------------------------------------- /test/controllers/fixtures/partial_users.csv: -------------------------------------------------------------------------------- 1 | Name,Email 2 | ,a@hhg.com 3 | Tricia McMillan,t@hhg.com 4 | -------------------------------------------------------------------------------- /test/controllers/fixtures/reversed_users.csv: -------------------------------------------------------------------------------- 1 | Email,Name 2 | a@hhg.com,Arthur Dent 3 | t@hhg.com,Tricia McMillan 4 | -------------------------------------------------------------------------------- /test/controllers/fixtures/users.csv: -------------------------------------------------------------------------------- 1 | Name,Email 2 | Arthur Dent,a@hhg.com 3 | Tricia McMillan,t@hhg.com 4 | -------------------------------------------------------------------------------- /test/controllers/fixtures/users_with_non_valid_emails.csv: -------------------------------------------------------------------------------- 1 | Name,Email 2 | Raphael Gupta,raphael@example.gov.uk 3 | Bolormaa Śniegowski,@bolo 4 | Flora Gao,f.gao@example.gov.uk 5 | Aureliusz Clemente,aureliusz@examplegovuk 6 | -------------------------------------------------------------------------------- /test/controllers/fixtures/users_with_orgs.csv: -------------------------------------------------------------------------------- 1 | Name,Email,Organisation 2 | Arthur Dent,a@hhg.com,department-of-hats 3 | Tricia McMillan,t@hhg.com, 4 | Zaphod Beeblebrox,z@hhg.com,cabinet-office 5 | -------------------------------------------------------------------------------- /test/controllers/sessions_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SessionsControllerTest < ActionController::TestCase 4 | setup do 5 | @request.env["devise.mapping"] = Devise.mappings[:user] 6 | @user = create(:user) 7 | end 8 | 9 | should "sign in user if credentials are valid" do 10 | post :create, params: { user: { email: @user.email, password: @user.password } } 11 | 12 | assert @controller.signed_in? 13 | end 14 | 15 | should "not sign in user if credentials are not valid" do 16 | post :create, params: { user: { email: @user.email, password: "incorrect-password" } } 17 | 18 | assert_not @controller.signed_in? 19 | end 20 | 21 | should "not raise exception if email param is a Hash" do 22 | post :create, params: { user: { email: { foo: "bar" }, password: @user.password } } 23 | 24 | assert_not @controller.signed_in? 25 | end 26 | 27 | should "not raise exception if user param is not present" do 28 | post :create, params: {} 29 | 30 | assert_not @controller.signed_in? 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/controllers/two_step_verification_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class TwoStepVerificationControllerTest < ActionController::TestCase 4 | setup do 5 | request.env["devise.mapping"] = Devise.mappings[:user] 6 | @controller = Devise::TwoStepVerificationController.new 7 | 8 | @user = create(:user) 9 | sign_in @user 10 | end 11 | 12 | context "when user is not logged in" do 13 | setup { sign_out @user } 14 | 15 | should "redirect to login upon attempted prompt" do 16 | get :prompt 17 | 18 | assert_not_authenticated 19 | end 20 | end 21 | 22 | context "when MFA code is required by login journey" do 23 | setup do 24 | sign_out @user 25 | sign_in @user, passed_mfa: false 26 | end 27 | 28 | should "redirect to login upon attempted prompt" do 29 | get :prompt 30 | 31 | assert_redirected_to new_two_step_verification_session_path 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/factories/batch_invitation.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :batch_invitation do 3 | association :user, factory: :admin_user 4 | trait :with_organisation do 5 | association :organisation, factory: :organisation 6 | end 7 | 8 | trait :in_progress do 9 | outcome { nil } 10 | 11 | has_permissions 12 | end 13 | 14 | trait :has_permissions do 15 | after(:create) do |batch_invitation| 16 | unless batch_invitation.has_permissions? 17 | batch_invitation.supported_permissions << create(:supported_permission) 18 | batch_invitation.save! 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/factories/batch_invitation_application_permission.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :batch_invitation_application_permission do 3 | association :batch_invitation, factory: :batch_invitation 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/factories/batch_invitation_user.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :batch_invitation_user do 3 | name { "Mark France" } 4 | sequence(:email) { |n| "user#{n}@example.com" } 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/factories/event_log.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :event_log do 3 | event_id { EventLog::NO_SUCH_ACCOUNT_LOGIN.id } 4 | uid { create(:user).uid } 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/factories/oauth_access_grants.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :access_grant, class: Doorkeeper::AccessGrant do 3 | sequence(:resource_owner_id) { |n| n } 4 | application 5 | expires_in { 2.hours } 6 | redirect_uri { "https://app.com/callback" } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/factories/oauth_access_tokens.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :access_token, class: Doorkeeper::AccessToken do 3 | sequence(:resource_owner_id) { |n| n } 4 | application 5 | expires_in { 2.hours } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/factories/organisation.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :organisation do 3 | sequence(:slug) { |n| "ministry-of-funk-#{n}000" } 4 | sequence(:name) { |n| "Ministry of Funk #{n}000" } 5 | content_id { SecureRandom.uuid } 6 | organisation_type { "Ministerial Department" } 7 | end 8 | 9 | factory :gds_organisation, parent: :organisation do 10 | content_id { Organisation::GDS_ORG_CONTENT_ID } 11 | name { "Government Digital Service" } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/factories/supported_permission.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :supported_permission, aliases: [:non_delegated_supported_permission] do 3 | sequence(:name) { |n| "Permission ##{n}" } 4 | association :application, factory: :application 5 | end 6 | 7 | factory :delegated_supported_permission, parent: :supported_permission do 8 | delegated { true } 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/factories/two_step_verification_exemption.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :two_step_verification_exemption do 3 | reason { "a very good reason" } 4 | expiry_day { Time.zone.tomorrow.day } 5 | expiry_month { Time.zone.tomorrow.month } 6 | expiry_year { Time.zone.tomorrow.year } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/factories/user_application_permission.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :user_application_permission do 3 | user 4 | application 5 | supported_permission { application.signin_permission } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/fixtures/users.csv: -------------------------------------------------------------------------------- 1 | Name,Email 2 | Fred,fred@example.com 3 | -------------------------------------------------------------------------------- /test/fixtures/users_with_orgs.csv: -------------------------------------------------------------------------------- 1 | Name,Email,Organisation 2 | Fred,fred@example.com,department-of-hats 3 | Lara,lara@example.com, 4 | Emma,emma@example.com,a-missing-org 5 | -------------------------------------------------------------------------------- /test/integration/account/activities_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Account::ActivitiesTest < ActionDispatch::IntegrationTest 4 | context "#show" do 5 | should "list user's EventLogs in table" do 6 | user = create(:user) 7 | 8 | visit new_user_session_path 9 | signin_with user 10 | 11 | visit account_activity_path 12 | 13 | assert page.has_selector? "td", text: "Successful login" 14 | end 15 | should "not show technical events to normal users" do 16 | user = create(:user) 17 | EventLog.record_event(user, EventLog::ACCESS_GRANTS_DELETED) 18 | 19 | visit new_user_session_path 20 | signin_with user 21 | 22 | visit account_activity_path 23 | 24 | assert page.has_selector? "td", text: "Successful login" 25 | assert_no_text "Access grants deleted" 26 | end 27 | 28 | should "show technical events to admin/superadmin users" do 29 | user = create(:admin_user) 30 | EventLog.record_event(user, EventLog::ACCESS_GRANTS_DELETED) 31 | 32 | visit new_user_session_path 33 | signin_with user 34 | 35 | visit account_activity_path 36 | 37 | assert page.has_selector? "td", text: "Successful login" 38 | assert page.has_selector? "td", text: "Access grants deleted" 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/integration/account/granting_access_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Account::GrantingAccessTest < ActionDispatch::IntegrationTest 4 | setup { @application = create(:application) } 5 | 6 | %w[superadmin admin].each do |admin_role| 7 | context "as a #{admin_role}" do 8 | setup do 9 | @user = create(:"#{admin_role}_user") 10 | visit new_user_session_path 11 | signin_with @user 12 | end 13 | 14 | should("be able to grant access") { assert_grant_access_to_self(@application, @user) } 15 | end 16 | end 17 | 18 | %w[super_organisation_admin organisation_admin].each do |publishing_manager_role| 19 | context "as a #{publishing_manager_role}" do 20 | setup do 21 | @user = create(:"#{publishing_manager_role}_user") 22 | visit new_user_session_path 23 | signin_with @user 24 | end 25 | 26 | should("not be able to grant access") { refute_grant_access_to_self(@application) } 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/integration/account/roles_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Account::RolesTest < ActionDispatch::IntegrationTest 4 | context "#show" do 5 | should "display read-only values for users who aren't GOVUK Admins" do 6 | non_govuk_admin_user = create(:super_organisation_admin_user) 7 | 8 | visit new_user_session_path 9 | signin_with non_govuk_admin_user 10 | 11 | visit edit_account_role_path 12 | 13 | assert has_text? "Super organisation admin" 14 | end 15 | 16 | should "allow Superadmin users to change their role" do 17 | user = FactoryBot.create(:superadmin_user) 18 | 19 | visit new_user_session_path 20 | signin_with user 21 | 22 | visit edit_account_role_path 23 | 24 | assert has_select? "Role", selected: "Superadmin" 25 | select "Normal", from: "Role" 26 | click_button "Change role" 27 | 28 | assert_current_url account_path 29 | assert page.has_text? "Your role is now Normal" 30 | 31 | visit edit_account_role_path 32 | 33 | assert has_text? "Normal" 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/integration/applications/users_with_access_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApplicationsUsersWithAccessTest < ActionDispatch::IntegrationTest 4 | include ActiveJob::TestHelper 5 | 6 | context "logged in as an superadmin" do 7 | setup do 8 | @application = create(:application) 9 | 10 | visit new_user_session_path 11 | signin_with(create(:superadmin_user)) 12 | end 13 | 14 | should "see all the users with access" do 15 | click_link "Apps" 16 | 17 | # Create a user that's authorized to use our app 18 | user = create(:user, name: "My Test User") 19 | user.grant_application_signin_permission(@application) 20 | 21 | click_link @application.name 22 | 23 | click_link "Users with access" 24 | 25 | assert page.has_content?(user.name) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/integration/cookies_security_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class CookiesSecurityTest < ActionDispatch::IntegrationTest 4 | should "set the right cookies when signing in" do 5 | user = FactoryBot.create(:two_step_enabled_user) 6 | sign_up_with user.email, user.password 7 | visit new_user_session_path 8 | response_cookies = Capybara.current_session.driver.response.headers["Set-Cookie"] 9 | assert_match "httponly", response_cookies 10 | assert_match "samesite=lax", response_cookies 11 | end 12 | 13 | def sign_up_with(email, password) 14 | visit new_user_session_path 15 | fill_in "Email", with: email 16 | fill_in "Password", with: password 17 | click_button "Sign in" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/integration/session_timeout_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SessionTimeoutTest < ActionDispatch::IntegrationTest 4 | setup do 5 | @user_email = "email@example.com" 6 | @user_password = "some password with various $ymb0l$" 7 | @user = create(:user, email: @user_email, password: @user_password) 8 | end 9 | 10 | should "not extend an expired session by viewing the login form" do 11 | Timecop.freeze((User.timeout_in + 5.minutes).ago) do 12 | visit root_path 13 | signin_with(email: @user_email, password: @user_password) 14 | end 15 | 16 | visit "/users/sign_in" 17 | 18 | assert_response_contains "Sign in" 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/integration/sign_out_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SignOutTest < ActionDispatch::IntegrationTest 4 | setup do 5 | @user = create(:user_in_organisation, email: "email@example.com", password: "some password with various $ymb0l$") 6 | visit root_path 7 | end 8 | 9 | should "perform reauth on downstream apps" do 10 | signin_with(@user) 11 | ReauthEnforcer.expects(:perform_on).with(@user) 12 | 13 | click_link "Sign out" 14 | 15 | assert_response_contains("Sign in to GOV.UK") 16 | end 17 | 18 | should "not blow up if not already signed in" do 19 | signout 20 | assert_response_contains("Sign in") 21 | end 22 | 23 | should "stop sending the user org slug to GA once signed out" do 24 | use_javascript_driver 25 | visit root_path 26 | signin_with(@user) 27 | assert_dimension_is_set(8) 28 | 29 | signout 30 | refute_dimension_is_set(8) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/integration/two_step_verification_prompt_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class TwoStepVerificationPromptTest < ActionDispatch::IntegrationTest 4 | context "when the user has had 2-step verification mandated" do 5 | setup do 6 | @user = create(:two_step_mandated_superadmin_user) 7 | visit users_path 8 | signin_with(@user, set_up_2sv: false) 9 | end 10 | 11 | should "prompt the user to complete verification" do 12 | assert page.has_text?("Start set up") 13 | end 14 | 15 | context "when they try to access something else" do 16 | should "ensure the prompt is still displayed" do 17 | visit users_path 18 | 19 | assert page.has_text?("Start set up") 20 | end 21 | end 22 | 23 | context "they choose to setup 2-step verification" do 24 | should "direct them to setup" do 25 | secret = ROTP::Base32.random_base32 26 | ROTP::Base32.stubs(random_base32: secret) 27 | 28 | click_link "Start set up" 29 | 30 | assert page.has_text?("Set up 2-step verification") 31 | 32 | enter_2sv_code(secret) 33 | click_button "Finish set up" 34 | 35 | assert_equal users_path, current_path 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/integration/user_agent_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "support/password_helpers" 3 | 4 | class UserAgentIntegrationTest < ActionDispatch::IntegrationTest 5 | include PasswordHelpers 6 | 7 | setup do 8 | @user = create(:user, name: "Normal User") 9 | end 10 | 11 | test "record user's user-agent string on login" do 12 | user_agent_test = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36" 13 | page.driver.header("User-agent", user_agent_test) 14 | visit root_path 15 | signin_with(@user) 16 | 17 | assert_equal user_agent_test, UserAgent.last.user_agent_string 18 | user_agent = @user.event_logs.first.user_agent_as_string 19 | assert_equal user_agent_test, user_agent 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/integration/user_suspension_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class UserSuspensionTest < ActionDispatch::IntegrationTest 4 | setup do 5 | @user = create(:user) 6 | @user.suspend("gross misconduct") 7 | end 8 | 9 | should "prevent users from signing in" do 10 | visit new_user_session_path 11 | signin_with(@user) 12 | 13 | assert_response_contains("account has been suspended") 14 | end 15 | 16 | should "show the suspension reason to admins" do 17 | admin = create(:admin_user) 18 | visit new_user_session_path 19 | signin_with(admin) 20 | 21 | visit edit_user_path(@user) 22 | assert_response_contains(/Status\s+Suspended/) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/integration/users/name_change_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Users::NameChangeTest < ActionDispatch::IntegrationTest 4 | context "when signed in as any kind of admin" do 5 | setup do 6 | @superadmin = create(:superadmin_user) 7 | @user = create(:user, name: "user-name") 8 | end 9 | 10 | should "be able to change a normal user's name" do 11 | visit root_path 12 | signin_with(@superadmin) 13 | visit edit_user_path(@user) 14 | click_link "Change Name" 15 | fill_in "Name", with: "new-user-name" 16 | click_button "Change name" 17 | assert_equal "new-user-name", @user.reload.name 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/integration/users/status_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "support/password_helpers" 3 | 4 | class Users::StatusTest < ActionDispatch::IntegrationTest 5 | include PasswordHelpers 6 | 7 | setup do 8 | @admin = create(:admin_user) 9 | @user = create(:user, suspended_at: 1.day.ago, reason_for_suspension: "Inactivity") 10 | end 11 | 12 | test "User status appears on the edit user page" do 13 | visit root_path 14 | signin_with(@admin) 15 | visit user_path(@user) 16 | 17 | assert page.has_content?("Suspended") 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/jobs/push_user_updates_job_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class PushUserUpdatesJobTest < ActiveSupport::TestCase 4 | include ActiveJob::TestHelper 5 | 6 | class TestJob < PushUserUpdatesJob 7 | end 8 | 9 | context "perform_on" do 10 | should "perform_async updates on user's used applications" do 11 | user = create(:user) 12 | foo_app, _bar_app = *create_list(:application, 2) 13 | 14 | create(:access_token, resource_owner_id: user.id, application: foo_app) 15 | 16 | assert_enqueued_with(job: TestJob, args: [user.uid, foo_app.id]) do 17 | TestJob.perform_on(user) 18 | end 19 | end 20 | 21 | should "not perform_async updates on user's retired applications" do 22 | user = create(:user) 23 | retired_app = create(:application, retired: true) 24 | 25 | create(:access_token, resource_owner_id: user.id, application: retired_app) 26 | 27 | assert_no_enqueued_jobs do 28 | TestJob.perform_on(user) 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/lib/code_verifier_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class CodeVerifierTest < ActiveSupport::TestCase 4 | attr_reader :secret 5 | 6 | setup do 7 | @secret = "topsecret" 8 | end 9 | 10 | test "#verify" do 11 | verifier = CodeVerifier.new(valid_code, secret) 12 | 13 | assert verifier.verify 14 | end 15 | 16 | test "#verify when the code contains additional spaces" do 17 | valid_code_with_spaces = " #{valid_code[0..2]} #{valid_code[3..5]} " 18 | 19 | verifier = CodeVerifier.new(valid_code_with_spaces, secret) 20 | 21 | assert verifier.verify 22 | end 23 | 24 | test "#verify when the code contains dashes" do 25 | valid_code_with_dashes = "#{valid_code[0..2]}-#{valid_code[3..5]}" 26 | 27 | verifier = CodeVerifier.new(valid_code_with_dashes, secret) 28 | 29 | assert verifier.verify 30 | end 31 | 32 | private 33 | 34 | def valid_code 35 | totp = ROTP::TOTP.new(secret) 36 | totp.now 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/lib/collectors/global_prometheus_collector_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class GlobalPrometheusCollectorTest < ActiveSupport::TestCase 4 | def setup 5 | @collector = Collectors::GlobalPrometheusCollector.new 6 | @api_user = api_user_with_token("user1", token_count: 4) 7 | end 8 | 9 | context "#metrics" do 10 | should "list all non-revoked token expiry timestamps for non-retired aps" do 11 | @api_user.authorisations[2].revoke 12 | @api_user.authorisations[3].application.update!(retired: true) 13 | 14 | metrics = @collector.metrics 15 | 16 | assert_equal metrics.first.data, { 17 | { 18 | api_user: @api_user.email, 19 | application: @api_user.authorisations.first.application.name.parameterize, 20 | } => @api_user.authorisations.first.expires_at.to_i, 21 | { 22 | api_user: @api_user.email, 23 | application: @api_user.authorisations.second.application.name.parameterize, 24 | } => @api_user.authorisations.second.expires_at.to_i, 25 | } 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphagov/signon/f1093373f0cbde6947ecee93030bac5bbcf2f4e1/test/models/.gitkeep -------------------------------------------------------------------------------- /test/models/api_user_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | class ApiUserTest < ActiveSupport::TestCase 3 | should "not be valid if require 2sv is set to true" do 4 | user = build(:api_user, require_2sv: true) 5 | assert_not user.valid? 6 | end 7 | 8 | should "be valid if require 2sv is set to false" do 9 | user = build(:api_user, require_2sv: false) 10 | assert user.valid? 11 | end 12 | 13 | should "not be valid if a reason for 2sv exemption exists" do 14 | user = build(:api_user, reason_for_2sv_exemption: "some reason") 15 | assert_not user.valid? 16 | end 17 | 18 | should "be valid if reason for 2sv exemption is nil" do 19 | user = build(:api_user, reason_for_2sv_exemption: nil) 20 | assert user.valid? 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/models/doorkeeper/access_grant_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Doorkeeper::AccessGrantTest < ActiveSupport::TestCase 4 | context ".expired" do 5 | should "return grants that have expired" do 6 | grant_expiring_1_day_ago = create(:access_grant, expires_in: -1.day) 7 | grant_expiring_in_1_day = create(:access_grant, expires_in: 1.day) 8 | 9 | grants = Doorkeeper::AccessGrant.expired 10 | 11 | assert_includes grants, grant_expiring_1_day_ago 12 | assert_not_includes grants, grant_expiring_in_1_day 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/models/user_agent_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class UserAgentTest < ActiveSupport::TestCase 4 | setup do 5 | @user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36" 6 | end 7 | 8 | test "can create a valid record" do 9 | assert UserAgent.new(user_agent_string: @user_agent).valid? 10 | end 11 | 12 | test "requires an user agent" do 13 | assert_not UserAgent.new.valid? 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/models/user_application_permission_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class UserApplicationPermissionTest < ActiveSupport::TestCase 4 | context "validations" do 5 | setup do 6 | @user = create(:user) 7 | @application = create(:application) 8 | @supported_permission = @application.signin_permission 9 | end 10 | 11 | should "be invalid without user_id" do 12 | assert UserApplicationPermission.new(user: nil, supported_permission: @supported_permission).invalid? 13 | end 14 | 15 | should "be invalid without supported_permission_id" do 16 | assert UserApplicationPermission.new(supported_permission: nil, user: @user).invalid? 17 | end 18 | 19 | should "ensure unique user application permissions" do 20 | application_permission_attributes = { supported_permission: @supported_permission } 21 | @user.application_permissions.create!(application_permission_attributes) 22 | 23 | assert @user.application_permissions.build(application_permission_attributes).invalid? 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/policies/account/activities_policy_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "support/policy_helpers" 3 | 4 | class Account::ActivitiesPolicyTest < ActiveSupport::TestCase 5 | include PolicyHelpers 6 | 7 | should "allow logged in users to see show irrespective of their role" do 8 | assert permit?(build(:user), nil, :show) 9 | end 10 | 11 | should "not allow anonymous visitors to see show" do 12 | assert forbid?(nil, nil, :show) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/policies/account/emails_policy_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "support/policy_helpers" 3 | 4 | class Account::EmailsPolicyTest < ActiveSupport::TestCase 5 | include PolicyHelpers 6 | 7 | should "allow logged in users to see show irrespective of their role" do 8 | assert permit?(build(:user), nil, :edit) 9 | end 10 | 11 | should "not allow anonymous visitors to see edit" do 12 | assert forbid?(nil, nil, :edit) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/policies/account/organisations_policy_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "support/policy_helpers" 3 | 4 | class Account::OrganisationsPolicyTest < ActiveSupport::TestCase 5 | include PolicyHelpers 6 | 7 | context "edit?" do 8 | should "allow logged in users to see edit irrespective of their role" do 9 | assert permit?(build(:user), nil, :edit) 10 | end 11 | 12 | should "not allow anonymous visitors to see edit" do 13 | assert forbid?(nil, nil, :edit) 14 | end 15 | end 16 | 17 | context "update?" do 18 | %i[superadmin admin].each do |user_role| 19 | should "be permitted for #{user_role} users" do 20 | user = FactoryBot.build(:"#{user_role}_user") 21 | 22 | assert permit?(user, nil, :update) 23 | end 24 | end 25 | 26 | %i[super_organisation_admin organisation_admin normal].each do |user_role| 27 | should "be forbidden for #{user_role} users" do 28 | user = FactoryBot.build(:"#{user_role}_user") 29 | 30 | assert forbid?(user, nil, :update) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/policies/account/passwords_policy_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "support/policy_helpers" 3 | 4 | class Account::PasswordsPolicyTest < ActiveSupport::TestCase 5 | include PolicyHelpers 6 | 7 | should "allow logged in users to see edit irrespective of their role" do 8 | assert permit?(build(:user), nil, :edit) 9 | end 10 | 11 | should "not allow anonymous visitors to see edit" do 12 | assert forbid?(nil, nil, :edit) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/policies/account/roles_policy_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "support/policy_helpers" 3 | 4 | class Account::RolesPolicyTest < ActiveSupport::TestCase 5 | include PolicyHelpers 6 | 7 | context "edit?" do 8 | should "allow logged in users to see edit irrespective of their role" do 9 | assert permit?(build(:user), nil, :edit) 10 | end 11 | 12 | should "not allow anonymous visitors to see edit" do 13 | assert forbid?(nil, nil, :edit) 14 | end 15 | end 16 | 17 | context "update?" do 18 | %i[superadmin].each do |user_role| 19 | should "be permitted for #{user_role} users" do 20 | user = FactoryBot.build(:"#{user_role}_user") 21 | 22 | assert permit?(user, nil, :update) 23 | end 24 | end 25 | 26 | %i[admin super_organisation_admin organisation_admin normal].each do |user_role| 27 | should "be forbidden for #{user_role} users" do 28 | user = FactoryBot.build(:"#{user_role}_user") 29 | 30 | assert forbid?(user, nil, :update) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/policies/account_page_policy_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "support/policy_helpers" 3 | 4 | class AccountPagePolicyTest < ActiveSupport::TestCase 5 | include PolicyHelpers 6 | 7 | should "allow logged in users to see show irrespective of their role" do 8 | assert permit?(build(:user), nil, :show) 9 | end 10 | 11 | should "not allow anonymous visitors to see show" do 12 | assert forbid?(nil, nil, :show) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/policies/application_policy_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "support/policy_helpers" 3 | 4 | class ApplicationPolicyTest < ActiveSupport::TestCase 5 | include PolicyHelpers 6 | 7 | %i[index edit update manage_supported_permissions users_with_access].each do |permission_name| 8 | context permission_name do 9 | should "allow only for superadmins" do 10 | assert permit?(create(:superadmin_user), User, permission_name) 11 | 12 | assert forbid?(create(:admin_user), User, permission_name) 13 | assert forbid?(create(:super_organisation_admin_user), User, permission_name) 14 | assert forbid?(create(:organisation_admin_user), User, permission_name) 15 | assert forbid?(create(:user), User, permission_name) 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/policies/authorisation_policy_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "support/policy_helpers" 3 | 4 | class AuthorisationPolicyTest < ActiveSupport::TestCase 5 | include PolicyHelpers 6 | 7 | %i[new create edit revoke].each do |permission_name| 8 | context permission_name do 9 | should "allow only for superadmins" do 10 | assert permit?(create(:superadmin_user), User, permission_name) 11 | 12 | assert forbid?(create(:admin_user), User, permission_name) 13 | assert forbid?(create(:super_organisation_admin_user), User, permission_name) 14 | assert forbid?(create(:organisation_admin_user), User, permission_name) 15 | assert forbid?(create(:user), User, permission_name) 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/policies/batch_invitation_policy_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "support/policy_helpers" 3 | 4 | class BatchInvitationPolicyTest < ActiveSupport::TestCase 5 | include PolicyHelpers 6 | 7 | context "new" do 8 | should "allow superadmins and admins to create new batch uploads" do 9 | assert permit?(create(:superadmin_user), BatchInvitation.new, :new) 10 | assert permit?(create(:admin_user), BatchInvitation.new, :new) 11 | end 12 | 13 | should "forbid organisation admins to create new batch uploads even within their organisation subtree" do 14 | organisation_admin = create(:organisation_admin_user) 15 | 16 | assert forbid?(organisation_admin, BatchInvitation.new, :new) 17 | assert forbid?(organisation_admin, BatchInvitation.new(organisation_id: create(:organisation).id), :new) 18 | assert forbid?(organisation_admin, BatchInvitation.new(organisation_id: organisation_admin.organisation_id), :new) 19 | end 20 | 21 | should "forbid for normal users" do 22 | assert forbid?(create(:user), BatchInvitation.new, :new) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/support/analytics_helpers.rb: -------------------------------------------------------------------------------- 1 | module AnalyticsHelpers 2 | def refute_dimension_is_set(dimension) 3 | js_code = dimension_set_js_code(dimension) 4 | assert_not page.has_text?(:all, js_code) 5 | end 6 | 7 | def assert_dimension_is_set(dimension, with_value: nil) 8 | js_code = dimension_set_js_code(dimension, with_value:) 9 | assert page.has_text?(:all, js_code) 10 | end 11 | 12 | private 13 | 14 | def dimension_set_js_code(dimension, with_value: nil) 15 | code = "ga('set', 'dimension#{dimension}" 16 | with_value.present? ? code + "', \"#{with_value}\")" : code 17 | end 18 | 19 | def google_analytics_page_view_path 20 | case page.body 21 | when Regexp.new("ga\\('send', 'pageview', { page: '(?