diff --git a/.env.example b/.env.example index ab43017eba..f7dfc780cd 100755 --- a/.env.example +++ b/.env.example @@ -237,6 +237,13 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" # Whether or not a Project administrator can register a user # PROJECT_ADMIN_REGISTRATION_FORM_ENABLED=true +# The minimum permission level able to register a user. +# Leaving this value blank will disable registration forms, unless a value can be +# generated from USER_REGISTRATION_FORM_ENABLED and PROJECT_ADMIN_REGISTRATION_FORM_ENABLED + +# Options: PUBLIC, PROJECT_ADMIN, ADMIN, DISABLED +# USER_REGISTRATION_ACCESS_LEVEL_REQUIRED=PUBLIC + # Require all new projects to use authenticated submissions. # Instance administrators can override this, and this setting has no effect on # existing projects. diff --git a/app/Enums/RegistrationPermissionsLevel.php b/app/Enums/RegistrationPermissionsLevel.php new file mode 100644 index 0000000000..0a2ef605ef --- /dev/null +++ b/app/Enums/RegistrationPermissionsLevel.php @@ -0,0 +1,11 @@ +admin) { + if ($current_user->can('create', [User::class, EloquentProject::find($projectid)])) { $xml .= add_XML_value('canRegister', '1'); } $xml .= ''; @@ -426,7 +428,7 @@ private static function find_site_maintainers(int $projectid): array private function register_user($projectid, $email, $firstName, $lastName, $repositoryCredential) { - if (config('auth.project_admin_registration_form_enabled') === false) { + if (Gate::authorize('create', [User::class, EloquentProject::findOrFail($projectid)])->denied()) { return 'Users cannot be registered via this form at the current time.'; } diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php new file mode 100644 index 0000000000..41171a5079 --- /dev/null +++ b/app/Policies/UserPolicy.php @@ -0,0 +1,35 @@ +value : ((bool) env('PROJECT_ADMIN_REGISTRATION_FORM_ENABLED', true) ? RegistrationPermissionsLevel::PROJECT_ADMIN->value : RegistrationPermissionsLevel::ADMIN->value); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + $user_permission_level = Project::whereRelation('administrators', 'users.id', request()->user()?->id)->exists() ? 1 : 0; + $user_permission_level = $user->admin ? 2 : $user_permission_level; + $registration_permission_level_required = match (Str::upper(config('auth.user_registration_access_level_required'))) { + 'PUBLIC' => RegistrationPermissionsLevel::PUBLIC->value, + 'PROJECT_ADMIN' => RegistrationPermissionsLevel::PROJECT_ADMIN->value, + 'ADMIN' => RegistrationPermissionsLevel::ADMIN->value, + 'DISABLED' => RegistrationPermissionsLevel::DISABLED->value, + default => $this->AttemptValueBuild(), + }; + + // Fail if the caller is requesting a value that the setting disallows + return $user_permission_level >= $registration_permission_level_required; + } +} diff --git a/config/auth.php b/config/auth.php index 17ebbf1a5a..0d636e0157 100755 --- a/config/auth.php +++ b/config/auth.php @@ -6,10 +6,9 @@ return [ // Whether or not "normal" username+password authentication is enabled 'username_password_authentication_enabled' => env('USERNAME_PASSWORD_AUTHENTICATION_ENABLED', true), - // Whether or not "normal" username+password authentication is enabled - 'user_registration_form_enabled' => env('USER_REGISTRATION_FORM_ENABLED', true), - // Whether or not a Project administrator can register a user - 'project_admin_registration_form_enabled' => env('PROJECT_ADMIN_REGISTRATION_FORM_ENABLED', true), + // Which level of permissions can register a user + // supported: PUBLIC,PROJECT_ADMIN,ADMIN,"" + 'user_registration_access_level_required' => env('USER_REGISTRATION_ACCESS_LEVEL_REQUIRED', (bool) env('USER_REGISTRATION_FORM_ENABLED', true) ? 'PUBLIC' : ((bool) env('PROJECT_ADMIN_REGISTRATION_FORM_ENABLED', true) ? 'PROJECT_ADMIN' : 'ADMIN')), /* |-------------------------------------------------------------------------- diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 581e368616..516a5b5b97 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3719,6 +3719,16 @@ parameters: count: 2 path: app/Policies/ProjectPolicy.php + - + message: "#^Dynamic call to static method Illuminate\\\\Database\\\\Eloquent\\\\Builder\\\\:\\:exists\\(\\)\\.$#" + count: 1 + path: app/Policies/UserPolicy.php + + - + message: "#^Parameter \\#1 \\$value of static method Illuminate\\\\Support\\\\Str\\:\\:upper\\(\\) expects string, mixed given\\.$#" + count: 1 + path: app/Policies/UserPolicy.php + - message: "#^Dynamic call to static method Illuminate\\\\Foundation\\\\Application\\:\\:isProduction\\(\\)\\.$#" count: 1 diff --git a/resources/views/admin/manage-users.blade.php b/resources/views/admin/manage-users.blade.php index 5e4bee0a6c..2ea77c84f4 100644 --- a/resources/views/admin/manage-users.blade.php +++ b/resources/views/admin/manage-users.blade.php @@ -40,93 +40,95 @@ > - - - - - - - - - - Add new user - - - - - First Name: - - - - - - - - Last Name: - - - - - - - - Email: - - - - - - - - Password: - - - - - - - - - - Confirm Password: - - - - - - - - Institution: - - - - - - - - - - (password will be displayed in clear text upon addition) - - + @can("create", App\Models\User::class) + + + + + + + + + + Add new user + + + + + First Name: + + + + + + + + Last Name: + + + + + + + + Email: + + + + + + + + Password: + + + + + + + + + + Confirm Password: + + + + + + + + Institution: + + + + + + + + + + (password will be displayed in clear text upon addition) + + + @endcan diff --git a/resources/views/components/header.blade.php b/resources/views/components/header.blade.php index 6e1ba51993..725028e3ae 100755 --- a/resources/views/components/header.blade.php +++ b/resources/views/components/header.blade.php @@ -2,9 +2,10 @@ if (isset($project)) { $logoid = $project->ImageId; } -$hideRegistration = config('auth.user_registration_form_enabled') === false; -@endphp + use Illuminate\Support\Str; + $canRegister = Str::upper(config('auth.user_registration_access_level_required')) === "PUBLIC"; +@endphp @@ -18,7 +19,7 @@ Logout @else Login - @if(!$hideRegistration) + @if($canRegister) {{ __('Register') }} @endif @endif diff --git a/tests/Environments/.env.ADMIN b/tests/Environments/.env.ADMIN new file mode 100644 index 0000000000..ac24112e0c --- /dev/null +++ b/tests/Environments/.env.ADMIN @@ -0,0 +1,21 @@ +APP_NAME=CDash +APP_ENV=testing +APP_KEY=DV5SQLqXCbpnme4z2pKNujd6gFW9hrj5 +APP_DEBUG=true +APP_URL=http://localhost:8080 + +LOG_CHANNEL=single + +DB_DATABASE=cdash4simpletest +DB_PASSWORD=cdash4simpletest + +REGISTRATION_EMAIL_VERIFY=false + +QUEUE_CONNECTION=sync +USER_REGISTRATION_ACCESS_LEVEL_REQUIRED=ADMIN +# Disable the slow page warning messages when testing +SLOW_PAGE_TIME=1000000 + +MAIL_MAILER=smtp +MAIL_HOST=mailpit +MAIL_PORT=1025 diff --git a/tests/Environments/.env.DISABLED b/tests/Environments/.env.DISABLED new file mode 100644 index 0000000000..8a01ebce50 --- /dev/null +++ b/tests/Environments/.env.DISABLED @@ -0,0 +1,21 @@ +APP_NAME=CDash +APP_ENV=testing +APP_KEY=DV5SQLqXCbpnme4z2pKNujd6gFW9hrj5 +APP_DEBUG=true +APP_URL=http://localhost:8080 + +LOG_CHANNEL=single + +DB_DATABASE=cdash4simpletest +DB_PASSWORD=cdash4simpletest + +REGISTRATION_EMAIL_VERIFY=false + +QUEUE_CONNECTION=sync +USER_REGISTRATION_ACCESS_LEVEL_REQUIRED=DISABLED +# Disable the slow page warning messages when testing +SLOW_PAGE_TIME=1000000 + +MAIL_MAILER=smtp +MAIL_HOST=mailpit +MAIL_PORT=1025 diff --git a/tests/Environments/.env.PROJECT_ADMIN b/tests/Environments/.env.PROJECT_ADMIN new file mode 100644 index 0000000000..40597132ba --- /dev/null +++ b/tests/Environments/.env.PROJECT_ADMIN @@ -0,0 +1,21 @@ +APP_NAME=CDash +APP_ENV=testing +APP_KEY=DV5SQLqXCbpnme4z2pKNujd6gFW9hrj5 +APP_DEBUG=true +APP_URL=http://localhost:8080 + +LOG_CHANNEL=single + +DB_DATABASE=cdash4simpletest +DB_PASSWORD=cdash4simpletest + +REGISTRATION_EMAIL_VERIFY=false + +QUEUE_CONNECTION=sync +USER_REGISTRATION_ACCESS_LEVEL_REQUIRED=PROJECT_ADMIN +# Disable the slow page warning messages when testing +SLOW_PAGE_TIME=1000000 + +MAIL_MAILER=smtp +MAIL_HOST=mailpit +MAIL_PORT=1025 diff --git a/tests/Environments/.env.PUBLIC b/tests/Environments/.env.PUBLIC new file mode 100644 index 0000000000..888534ead5 --- /dev/null +++ b/tests/Environments/.env.PUBLIC @@ -0,0 +1,21 @@ +APP_NAME=CDash +APP_ENV=testing +APP_KEY=DV5SQLqXCbpnme4z2pKNujd6gFW9hrj5 +APP_DEBUG=true +APP_URL=http://localhost:8080 + +LOG_CHANNEL=single + +DB_DATABASE=cdash4simpletest +DB_PASSWORD=cdash4simpletest + +REGISTRATION_EMAIL_VERIFY=false + +QUEUE_CONNECTION=sync +USER_REGISTRATION_ACCESS_LEVEL_REQUIRED=PUBLIC +# Disable the slow page warning messages when testing +SLOW_PAGE_TIME=1000000 + +MAIL_MAILER=smtp +MAIL_HOST=mailpit +MAIL_PORT=1025 diff --git a/tests/cypress/e2e/CMakeLists.txt b/tests/cypress/e2e/CMakeLists.txt index be818423fd..b6e070cdfa 100644 --- a/tests/cypress/e2e/CMakeLists.txt +++ b/tests/cypress/e2e/CMakeLists.txt @@ -72,3 +72,6 @@ set_tests_properties(cypress/e2e/build-configure PROPERTIES DEPENDS cypress/e2e/ add_cypress_e2e_test(all-projects) set_tests_properties(cypress/e2e/all-projects PROPERTIES DEPENDS cypress/e2e/build-configure) + +add_cypress_e2e_test(registration-permission) +set_tests_properties(cypress/e2e/registration-permission PROPERTIES DEPENDS cypress/e2e/all-projects) \ No newline at end of file diff --git a/tests/cypress/e2e/registration-permission.cy.js b/tests/cypress/e2e/registration-permission.cy.js new file mode 100644 index 0000000000..b40a872ee0 --- /dev/null +++ b/tests/cypress/e2e/registration-permission.cy.js @@ -0,0 +1,94 @@ +describe('registration-permission', () => { + + function loadEnvFile(cy, envName) { + cy.exec('cp .env .env.backup'); + cy.exec(`cp tests/Environments/.env.${envName} .env`); + } + + function resetEnvFile(cy) { + cy.exec('cp .env.backup .env'); + } + + describe('Test the login page to verify that the Register link respects the permission level', () => { + function visitPublicRegistration(permissionRequirement, containString) { + loadEnvFile(cy, permissionRequirement); + cy.visit('/login'); + + // assert expected text in the page headers + cy.get('#topmenu').should(containString, 'Register'); + + resetEnvFile(cy); + }; + + it('does not show the registration button when registration is restricted to project and site admins', () => { + visitPublicRegistration('PROJECT_ADMIN', 'not.contain'); + }); + + it('shows the registration button when registration is unrestricted', () => { + visitPublicRegistration('PUBLIC', 'contain'); + }); + + it('does not show the registration button when registration is restricted to site admins', () => { + visitPublicRegistration('ADMIN', 'not.contain'); + }); + + it('does not show the registration button when registration is disabled', () => { + visitPublicRegistration('DISABLED', 'not.contain'); + }); + + }); + + describe('Test the Administrator ManageUsers page to verify that the registration form respects the permission level', () => { + function visitManageUsers(permissionRequirement, containString) { + loadEnvFile(cy, permissionRequirement); + cy.login(); + cy.visit('/manageUsers.php'); + // assert expected text in the page headers + cy.get('tbody').should(containString, 'Add new user'); + + resetEnvFile(cy); + }; + + it('does show the registration form when registration is unrestricted', () => { + visitManageUsers('PUBLIC', 'contain'); + }); + it('does show the registration form when registration is restricted to project and site admins', () => { + visitManageUsers('PROJECT_ADMIN', 'contain'); + }); + + it('does show the registration form when registration is restricted to site admins', () => { + visitManageUsers('ADMIN', 'contain'); + }); + + it('does not show the registration button when registration is disabled', () => { + visitManageUsers('DISABLED', 'not.contain'); + }); + }); + + + describe('Test the manageProjectRoles page to verify that the registration form respects the permission level', () => { + function visitManageProjectRoles(permissionRequirement, existString) { + loadEnvFile(cy, permissionRequirement); + cy.login(); + cy.visit('/manageProjectRoles.php?projectid=15'); + // assert expected text in the page headers + cy.get('#fragment-3').should(existString); + + resetEnvFile(cy); + }; + it('does show the registration form when registration is unrestricted', () => { + visitManageProjectRoles('PUBLIC', 'exist'); + }); + it('does show the registration form when registration is restricted to project and site admins', () => { + visitManageProjectRoles('PROJECT_ADMIN', 'exist'); + }); + + it('does show the registration form when registration is restricted to site admins', () => { + visitManageProjectRoles('ADMIN', 'not.exist'); + }); + + it('does not show the registration button when registration is disabled', () => { + visitManageProjectRoles('DISABLED', 'not.exist'); + }); + }); +});