Skip to content

Commit

Permalink
Add new MAX_PROJECT_VISIBILITY config option (#2115)
Browse files Browse the repository at this point in the history
Some CDash administrators may want users to be able to create projects
on their own, but not public or protected projects. This PR adds a new
`MAX_PROJECT_VISIBILITY` environment variable which specifies the most
visible setting a project can be configured to use. Instance
administrators can override this setting, to allow some projects to be
made public or protected with administrator approval.
  • Loading branch information
williamjallen authored Apr 3, 2024
1 parent c946bb2 commit fd961b0
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 6 deletions.
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions app/Http/Controllers/ProjectController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
46 changes: 46 additions & 0 deletions app/Rules/ProjectVisibilityAllowed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace App\Rules;

use App\Models\Project;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Illuminate\Translation\PotentiallyTranslatedString;

class ProjectVisibilityAllowed implements ValidationRule
{
/**
* Verify that the current user is able to create/edit a project with the requested visibility.
*
* @param Closure(string): PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$value = (int) $value;

if ($value < 0 || $value > 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.');
}
}
}
}
10 changes: 10 additions & 0 deletions app/cdash/public/api/v1/project.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -256,6 +258,14 @@ function populate_project($Project)
$project_settings['CvsUrl'] = str_replace('&amp;', '&', $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;
}
Expand Down
2 changes: 2 additions & 0 deletions config/cdash.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
];
2 changes: 1 addition & 1 deletion graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
}


Expand Down
2 changes: 1 addition & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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

-
Expand Down
14 changes: 11 additions & 3 deletions resources/js/components/EditProject.vue
Original file line number Diff line number Diff line change
Expand Up @@ -371,13 +371,21 @@
@change="cdash.changesmade = true"
@focus="showHelp('public_help')"
>
<option value="0">
<option
value="0"
>
Private
</option>
<option value="1">
<option
value="1"
v-if="cdash.max_project_visibility === 'PUBLIC'"
>
Public
</option>
<option value="2">
<option
value="2"
v-if="cdash.max_project_visibility === 'PUBLIC' || cdash.max_project_visibility === 'PROTECTED'"
>
Protected
</option>
</select>
Expand Down
77 changes: 77 additions & 0 deletions tests/Feature/GraphQL/ProjectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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].');
}
}
}

0 comments on commit fd961b0

Please sign in to comment.