diff --git a/.env.example b/.env.example
index d0d4b37d55..61d3f0d35b 100755
--- a/.env.example
+++ b/.env.example
@@ -258,5 +258,11 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
# Whether or not to automatically register new users upon first login
#SAML2_AUTO_REGISTER_NEW_USERS=false
-# Should normal user allowed to create projects
+# Should normal users be allowed to create projects
# USER_CREATE_PROJECTS = false
+
+# The maximum visibility level for user-created projects on this instance.
+# Instance admins are able to override this setting and set project visibility
+# to anything. Thus, this setting is only meaningful if USER_CREATE_PROJECTS=true.
+# Options: PUBLIC, PROTECTED, PRIVATE
+# MAX_PROJECT_VISIBILITY=PUBLIC
diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php
index 02cf14fc8d..59fa9cec0c 100644
--- a/app/Http/Controllers/ProjectController.php
+++ b/app/Http/Controllers/ProjectController.php
@@ -142,6 +142,8 @@ public function apiCreateProject(): JsonResponse
$response['vcsviewers'] = array_map($callback, array_keys($viewers));
+ $response['max_project_visibility'] = $User->admin ? 'PUBLIC' : config('cdash.max_project_visibility');
+
$pageTimer->end($response);
return response()->json(cast_data_for_JSON($response));
}
diff --git a/app/Rules/ProjectVisibilityAllowed.php b/app/Rules/ProjectVisibilityAllowed.php
new file mode 100644
index 0000000000..caea44137f
--- /dev/null
+++ b/app/Rules/ProjectVisibilityAllowed.php
@@ -0,0 +1,46 @@
+ 2) {
+ $fail('Invalid project visibility setting');
+ }
+
+ $user = Auth::user();
+ // Admins can always set the project to whatever visibility they want
+ if ($user === null || !$user->admin) {
+ $max_visibility = match (Str::upper(config('cdash.max_project_visibility'))) {
+ 'PUBLIC' => Project::ACCESS_PUBLIC,
+ 'PROTECTED' => Project::ACCESS_PROTECTED,
+ 'PRIVATE' => Project::ACCESS_PRIVATE,
+ default => $fail('This instance contains an improper MAX_PROJECT_VISIBILITY configuration.'),
+ };
+
+ // This horrible logic is an unfortunate byproduct of the decision to misorder the access numbering scheme,
+ // which prevents range-based logic...
+ if ($max_visibility === Project::ACCESS_PROTECTED && $value === Project::ACCESS_PUBLIC) {
+ $fail('This instance is only configured to contain protected or private projects.');
+ } elseif ($max_visibility === Project::ACCESS_PRIVATE && ($value === Project::ACCESS_PUBLIC || $value === Project::ACCESS_PROTECTED)) {
+ $fail('This instance is only configured to contain private projects.');
+ }
+ }
+ }
+}
diff --git a/app/cdash/public/api/v1/project.php b/app/cdash/public/api/v1/project.php
index f9913c0451..9c537dfacc 100644
--- a/app/cdash/public/api/v1/project.php
+++ b/app/cdash/public/api/v1/project.php
@@ -19,11 +19,13 @@
require_once 'include/api_common.php';
+use App\Rules\ProjectVisibilityAllowed;
use App\Utils\RepositoryUtils;
use CDash\Model\Project;
use CDash\Model\UserProject;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
+use Illuminate\Support\Facades\Validator;
// Read input parameters (if any).
$rest_input = file_get_contents('php://input');
@@ -256,6 +258,14 @@ function populate_project($Project)
$project_settings['CvsUrl'] = str_replace('&', '&', $cvsurl);
}
+ if (Validator::make([
+ 'visibility' => $project_settings['Public'],
+ ], [
+ 'visibility' => new ProjectVisibilityAllowed(),
+ ])->fails()) {
+ abort(403, "Project visibility {$project_settings['Public']} prohibited for this instance.");
+ }
+
foreach ($project_settings as $k => $v) {
$Project->{$k} = $v;
}
diff --git a/config/cdash.php b/config/cdash.php
index 3e0732fa41..4cbdc376a3 100755
--- a/config/cdash.php
+++ b/config/cdash.php
@@ -81,5 +81,7 @@
'unlimited_projects' => $unlimited_projects,
'use_compression' => env('USE_COMPRESSION', true),
'user_create_projects' => env('USER_CREATE_PROJECTS', false),
+ // Defaults to public. Only meaningful if USER_CREATE_PROJECT=true.
+ 'max_project_visibility' => env('MAX_PROJECT_VISIBILITY', 'PUBLIC'),
'use_vcs_api' => env('USE_VCS_API', true),
];
diff --git a/graphql/schema.graphql b/graphql/schema.graphql
index e763b14ee5..c942757292 100644
--- a/graphql/schema.graphql
+++ b/graphql/schema.graphql
@@ -111,7 +111,7 @@ input CreateProjectInput {
homeurl: String!
"Visibility."
- visibility: ProjectVisibility! @rename(attribute: "public")
+ visibility: ProjectVisibility! @rename(attribute: "public") @rules(attribute: "public", apply: ["App\\Rules\\ProjectVisibilityAllowed"])
}
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 592b2b193d..b3ee6cb3dd 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -29726,7 +29726,7 @@ parameters:
-
message: "#^Dynamic call to static method Illuminate\\\\Testing\\\\TestResponse\\:\\:assertGraphQLErrorMessage\\(\\)\\.$#"
- count: 3
+ count: 4
path: tests/Feature/GraphQL/ProjectTest.php
-
diff --git a/resources/js/components/EditProject.vue b/resources/js/components/EditProject.vue
index e9411fee61..e775d47e6b 100644
--- a/resources/js/components/EditProject.vue
+++ b/resources/js/components/EditProject.vue
@@ -371,13 +371,21 @@
@change="cdash.changesmade = true"
@focus="showHelp('public_help')"
>
-
-
-
diff --git a/tests/Feature/GraphQL/ProjectTest.php b/tests/Feature/GraphQL/ProjectTest.php
index a98adf692e..defa4c4e19 100644
--- a/tests/Feature/GraphQL/ProjectTest.php
+++ b/tests/Feature/GraphQL/ProjectTest.php
@@ -635,4 +635,81 @@ public function testProjectVisibilityValue(): void
],
], true);
}
+
+ /**
+ * @return array{
+ * array{
+ * string,
+ * string,
+ * string,
+ * bool
+ * }
+ * }
+ */
+ public function createProjectVisibilityRules(): array
+ {
+ return [
+ ['normal', 'PUBLIC', 'PUBLIC', true],
+ ['normal', 'PROTECTED', 'PUBLIC', true],
+ ['normal', 'PRIVATE', 'PUBLIC', true],
+ ['normal', 'PUBLIC', 'PROTECTED', false],
+ ['normal', 'PROTECTED', 'PROTECTED', true],
+ ['normal', 'PRIVATE', 'PROTECTED', true],
+ ['normal', 'PUBLIC', 'PRIVATE', false],
+ ['normal', 'PROTECTED', 'PRIVATE', false],
+ ['normal', 'PRIVATE', 'PRIVATE', true],
+ ['admin', 'PUBLIC', 'PUBLIC', true],
+ ['admin', 'PROTECTED', 'PUBLIC', true],
+ ['admin', 'PRIVATE', 'PUBLIC', true],
+ ['admin', 'PUBLIC', 'PROTECTED', true],
+ ['admin', 'PROTECTED', 'PROTECTED', true],
+ ['admin', 'PRIVATE', 'PROTECTED', true],
+ ['admin', 'PUBLIC', 'PRIVATE', true],
+ ['admin', 'PROTECTED', 'PRIVATE', true],
+ ['admin', 'PRIVATE', 'PRIVATE', true],
+ ];
+ }
+
+ /**
+ * @dataProvider createProjectVisibilityRules
+ */
+ public function testCreateProjectMaxVisibility(string $user, string $visibility, string $max_visibility, bool $can_create): void
+ {
+ Config::set('cdash.user_create_projects', true);
+ Config::set('cdash.max_project_visibility', $max_visibility);
+
+ $name = 'test-project' . Str::uuid();
+ $response = $this->actingAs($this->users[$user])->graphQL('
+ mutation CreateProject($input: CreateProjectInput!) {
+ createProject(input: $input) {
+ visibility
+ }
+ }
+ ', [
+ 'input' => [
+ 'name' => $name,
+ 'description' => 'test',
+ 'homeurl' => 'https://cdash.org',
+ 'visibility' => $visibility,
+ ],
+ ]);
+
+ if ($can_create) {
+ $project = Project::where('name', $name)->firstOrFail();
+ $response->assertJson([
+ 'data' => [
+ 'createProject' => [
+ 'visibility' => $visibility,
+ ],
+ ],
+ ], true);
+ $project->delete();
+ } else {
+ // A final check to ensure this project wasn't created anyway
+ $this->assertDatabaseMissing(Project::class, [
+ 'name' => $name,
+ ]);
+ $response->assertGraphQLErrorMessage('Validation failed for the field [createProject].');
+ }
+ }
}