diff --git a/doc/manual/src/projects.md b/doc/manual/src/projects.md index f7c4975f..49e1fde1 100644 --- a/doc/manual/src/projects.md +++ b/doc/manual/src/projects.md @@ -312,6 +312,21 @@ Declarative Projects see this [chapter](./plugins/declarative-projects.md) +Private Projects +---------------- + +By checking the `Private` checkbox in the project creation form, a project +and everything related to it (jobsets, evals, builds, etc.) can only be accessed +if a user is authenticated. Otherwise, a 404 will be returned by the API and Web +UI. This is the main difference to "hidden" projects where everything can +be obtained if the URLs are known. + +Please note that the store paths that are realized in evaluations that belong to +private projects aren't protected! It is assumed that the hashes are unknown +and thus inaccessible. For a real protection of the binary cache it's recommended +to either use `nix.sshServe` instead or to protect the routes `/nar/*` and `*.narinfo` +with a reverse proxy. + Email Notifications ------------------- diff --git a/hydra-api.yaml b/hydra-api.yaml index 623c9082..f33057d2 100644 --- a/hydra-api.yaml +++ b/hydra-api.yaml @@ -184,6 +184,9 @@ paths: visible: description: when set to true the project is displayed in the web interface type: boolean + private: + description: when set to true the project and all related objects are only accessible to authenticated users + type: boolean declarative: description: declarative input configured for this project type: object @@ -625,6 +628,9 @@ components: hidden: description: when set to true the project is not displayed in the web interface type: boolean + private: + description: when set to true the project and all related objects are only accessible to authenticated users + type: boolean enabled: description: when set to true the project gets scheduled for evaluation type: boolean diff --git a/package.nix b/package.nix index 8789e058..ad3ac8c8 100644 --- a/package.nix +++ b/package.nix @@ -98,6 +98,7 @@ let FileLibMagic FileSlurper FileWhich + HTMLTreeBuilderXPath IOCompress IPCRun IPCRun3 diff --git a/src/Makefile.PL b/src/Makefile.PL deleted file mode 100644 index 9b04da7f..00000000 --- a/src/Makefile.PL +++ /dev/null @@ -1,21 +0,0 @@ -# IMPORTANT: if you delete this file your app will not work as -# expected. you have been warned -use strict; -use warnings; -use inc::Module::Install; - -name 'Hydra'; -all_from 'lib/Hydra.pm'; - -requires 'Catalyst::Runtime' => '5.7015'; -requires 'Catalyst::Plugin::ConfigLoader'; -requires 'Catalyst::Plugin::Static::Simple'; -requires 'Catalyst::Action::RenderView'; -requires 'parent'; -requires 'Config::General'; # This should reflect the config file format you've chosen - # See Catalyst::Plugin::ConfigLoader for supported formats -catalyst; - -install_script glob('script/*.pl'); -auto_install; -WriteAll; diff --git a/src/lib/Hydra/Base/Controller/ListBuilds.pm b/src/lib/Hydra/Base/Controller/ListBuilds.pm index 87ff0ee9..3712651b 100644 --- a/src/lib/Hydra/Base/Controller/ListBuilds.pm +++ b/src/lib/Hydra/Base/Controller/ListBuilds.pm @@ -23,23 +23,37 @@ sub all : Chained('get_builds') PathPart { $c->stash->{total} = $c->stash->{allBuilds}->search({finished => 1})->count unless defined $c->stash->{total}; - $c->stash->{builds} = [ $c->stash->{allBuilds}->search( - { finished => 1 }, - { order_by => "stoptime DESC" + my $extra = { + order_by => "stoptime DESC" , columns => [@buildListColumns] , rows => $resultsPerPage - , page => $page }) ]; + , page => $page }; + + my $criteria = { finished => 1 }; + + unless ($c->user_exists) { + $extra->{join} = {"jobset" => "project"}; + $criteria->{"project.private"} = 0; + } + + $c->stash->{builds} = [ $c->stash->{allBuilds}->search( + $criteria, + $extra + ) ]; } sub nix : Chained('get_builds') PathPart('channel/latest') CaptureArgs(0) { my ($self, $c) = @_; + my $private = $c->user_exists ? [1,0] : [0]; + $c->stash->{channelName} = $c->stash->{channelBaseName} . "-latest"; $c->stash->{channelBuilds} = $c->stash->{latestSucceeded} ->search_literal("exists (select 1 from buildproducts where build = me.id and type = 'nix-build')") - ->search({}, { columns => [@buildListColumns, 'drvpath', 'description', 'homepage'] - , join => ["buildoutputs"] + ->search({"project.private" => {-in => $private}}, + { columns => [@buildListColumns, 'drvpath', 'description', 'homepage'] + , join => ["buildoutputs", {"jobset" => "project"}] , order_by => ["me.id", "buildoutputs.name"] , '+select' => ['buildoutputs.path', 'buildoutputs.name'], '+as' => ['outpath', 'outname'] }); } diff --git a/src/lib/Hydra/Controller/API.pm b/src/lib/Hydra/Controller/API.pm index 1281bd89..e299e26f 100644 --- a/src/lib/Hydra/Controller/API.pm +++ b/src/lib/Hydra/Controller/API.pm @@ -49,6 +49,8 @@ sub latestbuilds : Chained('api') PathPart('latestbuilds') Args(0) { error($c, "Parameter not defined!") if !defined $nr; my $project = $c->request->params->{project}; + checkProjectVisibleForGuest($c, $c->stash->{project}); + my $jobset = $c->request->params->{jobset}; my $job = $c->request->params->{job}; my $system = $c->request->params->{system}; @@ -106,6 +108,8 @@ sub jobsets : Chained('api') PathPart('jobsets') Args(0) { my $project = $c->model('DB::Projects')->find($projectName) or notFound($c, "Project $projectName doesn't exist."); + checkProjectVisibleForGuest($c, $project); + my @jobsets = jobsetOverview($c, $project); my @list; @@ -124,7 +128,17 @@ sub queue : Chained('api') PathPart('queue') Args(0) { my $nr = $c->request->params->{nr}; error($c, "Parameter not defined!") if !defined $nr; - my @builds = $c->model('DB::Builds')->search({finished => 0}, {rows => $nr, order_by => ["priority DESC", "id"]}); + my $criteria = {finished => 0}; + my $extra = { + rows => $nr, + order_by => ["priority DESC", "id"] + }; + unless ($c->user_exists) { + $criteria->{"project.private"} = 0; + $extra->{join} = {"jobset" => "project"}; + } + + my @builds = $c->model('DB::Builds')->search($criteria, $extra); my @list; push @list, buildToHash($_) foreach @builds; @@ -198,6 +212,16 @@ sub scmdiff : Path('/api/scmdiff') Args(0) { my $rev1 = $c->request->params->{rev1}; my $rev2 = $c->request->params->{rev2}; + unless ($c->user_exists) { + my $search = $c->model('DB::JobsetEvalInputs')->search( + { "project.private" => 0, "me.uri" => $uri }, + { join => { "eval" => { jobset => "project" } } } + ); + if ($search == 0) { + die("invalid revisions: [$rev1] [$rev2]") + } + } + die("invalid revisions: [$rev1] [$rev2]") if $rev1 !~ m/^[a-zA-Z0-9_.]+$/ || $rev2 !~ m/^[a-zA-Z0-9_.]+$/; # FIXME: injection danger. diff --git a/src/lib/Hydra/Controller/Build.pm b/src/lib/Hydra/Controller/Build.pm index f2288a29..61fb5c19 100644 --- a/src/lib/Hydra/Controller/Build.pm +++ b/src/lib/Hydra/Controller/Build.pm @@ -39,6 +39,9 @@ sub buildChain :Chained('/') :PathPart('build') :CaptureArgs(1) { $c->stash->{project} = $c->stash->{build}->project; $c->stash->{jobset} = $c->stash->{build}->jobset; $c->stash->{job} = $c->stash->{build}->job; + + checkProjectVisibleForGuest($c, $c->stash->{project}); + $c->stash->{runcommandlogs} = [$c->stash->{build}->runcommandlogs->search({}, {order_by => ["id DESC"]})]; $c->stash->{runcommandlogProblem} = undef; diff --git a/src/lib/Hydra/Controller/Channel.pm b/src/lib/Hydra/Controller/Channel.pm index 67de61ae..9e8844b6 100644 --- a/src/lib/Hydra/Controller/Channel.pm +++ b/src/lib/Hydra/Controller/Channel.pm @@ -3,6 +3,7 @@ package Hydra::Controller::Channel; use strict; use warnings; use base 'Hydra::Base::Controller::REST'; +use Hydra::Helper::CatalystUtils; sub channel : Chained('/') PathPart('channel/custom') CaptureArgs(3) { @@ -10,6 +11,8 @@ sub channel : Chained('/') PathPart('channel/custom') CaptureArgs(3) { $c->stash->{project} = $c->model('DB::Projects')->find($projectName); + checkProjectVisibleForGuest($c, $c->stash->{project}); + notFound($c, "Project $projectName doesn't exist.") if !$c->stash->{project}; diff --git a/src/lib/Hydra/Controller/Job.pm b/src/lib/Hydra/Controller/Job.pm index b392e8e1..11ee8b10 100644 --- a/src/lib/Hydra/Controller/Job.pm +++ b/src/lib/Hydra/Controller/Job.pm @@ -27,6 +27,8 @@ sub job : Chained('/') PathPart('job') CaptureArgs(3) { $c->stash->{job} = $jobName; $c->stash->{project} = $c->stash->{jobset}->project; + + checkProjectVisibleForGuest($c, $c->stash->{project}); } sub shield :Chained('job') PathPart('shield') Args(0) { diff --git a/src/lib/Hydra/Controller/Jobset.pm b/src/lib/Hydra/Controller/Jobset.pm index bc7d7444..b04a6d7e 100644 --- a/src/lib/Hydra/Controller/Jobset.pm +++ b/src/lib/Hydra/Controller/Jobset.pm @@ -17,6 +17,8 @@ sub jobsetChain :Chained('/') :PathPart('jobset') :CaptureArgs(2) { $c->stash->{project} = $project; + checkProjectVisibleForGuest($c, $c->stash->{project}); + $c->stash->{jobset} = $project->jobsets->find({ name => $jobsetName }); if (!$c->stash->{jobset} && !($c->action->name eq "jobset" and $c->request->method eq "PUT")) { diff --git a/src/lib/Hydra/Controller/JobsetEval.pm b/src/lib/Hydra/Controller/JobsetEval.pm index 77c01a84..ff88ba4a 100644 --- a/src/lib/Hydra/Controller/JobsetEval.pm +++ b/src/lib/Hydra/Controller/JobsetEval.pm @@ -19,6 +19,8 @@ sub evalChain : Chained('/') PathPart('eval') CaptureArgs(1) { $c->stash->{eval} = $eval; $c->stash->{jobset} = $eval->jobset; $c->stash->{project} = $eval->jobset->project; + + checkProjectVisibleForGuest($c, $c->stash->{project}); } diff --git a/src/lib/Hydra/Controller/Project.pm b/src/lib/Hydra/Controller/Project.pm index 1141de4a..c8f81dfa 100644 --- a/src/lib/Hydra/Controller/Project.pm +++ b/src/lib/Hydra/Controller/Project.pm @@ -16,6 +16,8 @@ sub projectChain :Chained('/') :PathPart('project') :CaptureArgs(1) { $c->stash->{project} = $c->model('DB::Projects')->find($projectName); + checkProjectVisibleForGuest($c, $c->stash->{project}); + $c->stash->{isProjectOwner} = !$isCreate && isProjectOwner($c, $c->stash->{project}); notFound($c, "Project ‘$projectName’ doesn't exist.") @@ -161,6 +163,7 @@ sub updateProject { , homepage => trim($c->stash->{params}->{homepage}) , enabled => defined $c->stash->{params}->{enabled} ? 1 : 0 , hidden => defined $c->stash->{params}->{visible} ? 0 : 1 + , private => defined $c->stash->{params}->{private} ? 1 : 0 , owner => $owner , enable_dynamic_run_command => $enable_dynamic_run_command , declfile => trim($c->stash->{params}->{declarative}->{file}) diff --git a/src/lib/Hydra/Controller/Root.pm b/src/lib/Hydra/Controller/Root.pm index 79cb44b3..da436e3a 100644 --- a/src/lib/Hydra/Controller/Root.pm +++ b/src/lib/Hydra/Controller/Root.pm @@ -110,7 +110,13 @@ sub deserialize :ActionClass('Deserialize') { } sub index :Path :Args(0) { my ($self, $c) = @_; $c->stash->{template} = 'overview.tt'; - $c->stash->{projects} = [$c->model('DB::Projects')->search({}, {order_by => ['enabled DESC', 'name']})]; + + my $includePrivate = $c->user_exists ? [1,0] : [0]; + + $c->stash->{projects} = [$c->model('DB::Projects')->search( + {private => {-in => $includePrivate}}, + {order_by => ['enabled DESC', 'name']} + )]; $c->stash->{newsItems} = [$c->model('DB::NewsItems')->search({}, { order_by => ['createtime DESC'], rows => 5 })]; $self->status_ok($c, entity => $c->stash->{projects} @@ -122,15 +128,23 @@ sub queue :Local :Args(0) :ActionClass('REST') { } sub queue_GET { my ($self, $c) = @_; + my $criteria = {finished => 0}; + my $extra = { + columns => [@buildListColumns], + order_by => ["priority DESC", "id"] + }; + unless ($c->user_exists) { + $criteria->{"project.private"} = 0; + $extra->{join} = {"jobset" => "project"}; + } $c->stash->{template} = 'queue.tt'; $c->stash->{flashMsg} //= $c->flash->{buildMsg}; $self->status_ok( $c, entity => [$c->model('DB::Builds')->search( - { finished => 0 }, - { order_by => ["globalpriority desc", "id"], - , columns => [@buildListColumns] - })] + $criteria, + $extra + )] ); } @@ -139,10 +153,15 @@ sub queue_summary :Local :Path('queue-summary') :Args(0) { my ($self, $c) = @_; $c->stash->{template} = 'queue-summary.tt'; + my $extra = " where "; + unless ($c->user_exists) { + $extra = "inner join Projects p on p.name = project where p.private = 0 and "; + } + $c->stash->{queued} = dbh($c)->selectall_arrayref( "select jobsets.project as project, jobsets.name as jobset, count(*) as queued, min(timestamp) as oldest, max(timestamp) as newest from Builds " . "join Jobsets jobsets on jobsets.id = builds.jobset_id " . - "where finished = 0 group by jobsets.project, jobsets.name order by queued desc", + "$extra finished = 0 group by jobsets.project, jobsets.name order by queued desc", { Slice => {} }); $c->stash->{systems} = dbh($c)->selectall_arrayref( @@ -155,12 +174,19 @@ sub status :Local :Args(0) :ActionClass('REST') { } sub status_GET { my ($self, $c) = @_; + my $criteria = { "buildsteps.busy" => { '!=', 0 } }; + my $join = ["buildsteps"]; + unless ($c->user_exists) { + $criteria->{"project.private"} = 0; + push @{$join}, {"jobset" => "project"}; + } + $self->status_ok( $c, entity => [$c->model('DB::Builds')->search( - { "buildsteps.busy" => { '!=', 0 } }, + $criteria, { order_by => ["globalpriority DESC", "id"], - join => "buildsteps", + join => $join, columns => [@buildListColumns, 'buildsteps.drvpath', 'buildsteps.type'] })] ); @@ -202,13 +228,18 @@ sub machines :Local Args(0) { } } + my $extra = "where"; + unless ($c->user_exists) { + $extra = "inner join Projects p on p.name = jobsets.project where p.private = 0 and "; + } + $c->stash->{machines} = $machines; $c->stash->{steps} = dbh($c)->selectall_arrayref( "select build, stepnr, s.system as system, s.drvpath as drvpath, machine, s.starttime as starttime, jobsets.project as project, jobsets.name as jobset, job, s.busy as busy " . "from BuildSteps s " . "join Builds b on s.build = b.id " . "join Jobsets jobsets on jobsets.id = b.jobset_id " . - "where busy != 0 order by machine, stepnr", + "$extra busy != 0 order by machine, stepnr", { Slice => {} }); $c->stash->{template} = 'machine-status.tt'; $self->status_ok($c, entity => $c->stash->{machines}); @@ -450,16 +481,28 @@ sub steps :Local Args(0) { my $resultsPerPage = 20; + my $criteria = { + "me.starttime" => { '!=', undef }, + "me.stoptime" => { '!=', undef } + }; + + my $extra = { + order_by => [ "me.stoptime desc" ], + rows => $resultsPerPage, + offset => ($page - 1) * $resultsPerPage, + }; + + unless ($c->user_exists) { + $criteria->{"project.private"} = 0; + $extra->{join} = [{"build" => {"jobset" => "project"}}]; + } + $c->stash->{page} = $page; $c->stash->{resultsPerPage} = $resultsPerPage; $c->stash->{steps} = [ $c->model('DB::BuildSteps')->search( - { starttime => { '!=', undef }, - stoptime => { '!=', undef } - }, - { order_by => [ "stoptime desc" ], - rows => $resultsPerPage, - offset => ($page - 1) * $resultsPerPage - }) ]; + $criteria, + $extra + ) ]; $c->stash->{total} = approxTableSize($c, "IndexBuildStepsOnStopTime"); } @@ -480,28 +523,58 @@ sub search :Local Args(0) { $c->model('DB')->schema->txn_do(sub { $c->model('DB')->schema->storage->dbh->do("SET LOCAL statement_timeout = 20000"); - $c->stash->{projects} = [ $c->model('DB::Projects')->search( - { -and => + + my $projectCriteria = { + -and => [ { -or => [ name => { ilike => "%$query%" }, displayName => { ilike => "%$query%" }, description => { ilike => "%$query%" } ] } , { hidden => 0 } ] - }, - { order_by => ["name"] } ) ]; + }; - $c->stash->{jobsets} = [ $c->model('DB::Jobsets')->search( - { -and => + my $jobsetCriteria = { + -and => [ { -or => [ "me.name" => { ilike => "%$query%" }, "me.description" => { ilike => "%$query%" } ] } , { "project.hidden" => 0, "me.hidden" => 0 } ] - }, - { order_by => ["project", "name"], join => ["project"] } ) ]; + }; - $c->stash->{jobs} = [ $c->model('DB::Builds')->search( - { "job" => { ilike => "%$query%" } + my $buildCriteria = { + "job" => { ilike => "%$query%" } , "project.hidden" => 0 , "jobset.hidden" => 0 , iscurrent => 1 - }, + }; + + my $buildSearchExtra = { + order_by => ["id desc"] + , rows => $c->stash->{limit}, join => [] + }; + + my $outCriteria = { + "buildoutputs.path" => { ilike => "%$query%" } + }; + + my $drvCriteria = { "drvpath" => { ilike => "%$query%" } }; + + unless ($c->user_exists) { + $projectCriteria->{private} = 0; + $jobsetCriteria->{"project.private"} = 0; + $buildCriteria->{"project.private"} = 0; + push @{$buildSearchExtra->{join}}, {"jobset" => "project"}; + $outCriteria->{"project.private"} = 0; + $drvCriteria->{"project.private"} = 0; + } + + $c->stash->{projects} = [ $c->model('DB::Projects')->search( + $projectCriteria, + { order_by => ["name"] } ) ]; + + $c->stash->{jobsets} = [ $c->model('DB::Jobsets')->search( + $jobsetCriteria, + { order_by => ["project", "name"], join => ["project"] } ) ]; + + $c->stash->{jobs} = [ $c->model('DB::Builds')->search( + $buildCriteria, { order_by => ["jobset.project", "jobset.name", "job"], join => { "jobset" => "project" }, @@ -510,17 +583,16 @@ sub search :Local Args(0) { ]; # Perform build search in separate queries to prevent seq scan on buildoutputs table. + my $outExtra = $buildSearchExtra; + push @{$outExtra->{join}}, "buildoutputs"; $c->stash->{builds} = [ $c->model('DB::Builds')->search( - { "buildoutputs.path" => { ilike => "%$query%" } }, - { order_by => ["id desc"], join => ["buildoutputs"] - , rows => $c->stash->{limit} - } ) ]; + $outCriteria, + $outExtra + ) ]; $c->stash->{buildsdrv} = [ $c->model('DB::Builds')->search( - { "drvpath" => { ilike => "%$query%" } }, - { order_by => ["id desc"] - , rows => $c->stash->{limit} - } ) ]; + $drvCriteria, + $buildSearchExtra ) ]; $c->stash->{resource} = { projects => $c->stash->{projects}, jobsets => $c->stash->{jobsets}, diff --git a/src/lib/Hydra/Helper/CatalystUtils.pm b/src/lib/Hydra/Helper/CatalystUtils.pm index 2a2ad86f..96e6eda1 100644 --- a/src/lib/Hydra/Helper/CatalystUtils.pm +++ b/src/lib/Hydra/Helper/CatalystUtils.pm @@ -29,6 +29,7 @@ our @EXPORT = qw( approxTableSize requireLocalStore dbh + checkProjectVisibleForGuest ); @@ -256,6 +257,14 @@ sub requireProjectOwner { unless isProjectOwner($c, $project); } +sub checkProjectVisibleForGuest { + my ($c, $project) = @_; + if (defined $project && $project->private == 1 && !$c->user_exists) { + my $projectName = $project->name; + notFound($c, "Project ‘$projectName’ not found!"); + } +} + sub isAdmin { my ($c) = @_; diff --git a/src/lib/Hydra/Helper/Nix.pm b/src/lib/Hydra/Helper/Nix.pm index 2a479ddb..78ea0a35 100644 --- a/src/lib/Hydra/Helper/Nix.pm +++ b/src/lib/Hydra/Helper/Nix.pm @@ -182,17 +182,34 @@ sub findLog { my ($c, $drvPath, @outPaths) = @_; if (defined $drvPath) { + unless ($c->user_exists) { + my $existsForGuest = $c->model('DB::BuildSteps')->search( + {"me.drvpath" => $drvPath, "project.private" => 0}, + {join => {build => {"jobset" => "project"}}} + ); + if ($existsForGuest == 0) { + notFound($c, "Resource not found"); + } + } + my $logPath = getDrvLogPath($drvPath); return $logPath if defined $logPath; } return undef if scalar @outPaths == 0; + my $join = ["buildstepoutputs"]; + my $criteria = { path => { -in => [@outPaths] } }; + unless ($c->user_exists) { + push @{$join}, {"build" => {jobset => "project"}}; + $criteria->{"project.private"} = 0; + } + my @steps = $c->model('DB::BuildSteps')->search( - { path => { -in => [@outPaths] } }, + $criteria, { select => ["drvpath"] , distinct => 1 - , join => "buildstepoutputs" + , join => $join }); foreach my $step (@steps) { @@ -285,9 +302,19 @@ sub getEvals { my $me = $evals_result_set->current_source_alias; - my @evals = $evals_result_set->search( - { hasnewbuilds => 1 }, - { order_by => "$me.id DESC", rows => $rows, offset => $offset }); + my $criteria = { hasnewbuilds => 1 }; + my $extra = { + order_by => "$me.id DESC", + rows => $rows, + offset => $offset + }; + unless ($c->user_exists) { + $extra->{join} = {"jobset" => "project"}; + $criteria->{"project.private"} = 0; + } + + my @evals = $evals_result_set->search($criteria, $extra); + my @res = (); my $cache = {}; diff --git a/src/lib/Hydra/Schema/Result/Projects.pm b/src/lib/Hydra/Schema/Result/Projects.pm index d6e66bf7..edb4c4d8 100644 --- a/src/lib/Hydra/Schema/Result/Projects.pm +++ b/src/lib/Hydra/Schema/Result/Projects.pm @@ -62,6 +62,12 @@ __PACKAGE__->table("projects"); default_value: 0 is_nullable: 0 +=head2 private + + data_type: 'integer' + default_value: 0 + is_nullable: 0 + =head2 owner data_type: 'text' @@ -107,6 +113,8 @@ __PACKAGE__->add_columns( { data_type => "integer", default_value => 1, is_nullable => 0 }, "hidden", { data_type => "integer", default_value => 0, is_nullable => 0 }, + "private", + { data_type => "integer", default_value => 0, is_nullable => 0 }, "owner", { data_type => "text", is_foreign_key => 1, is_nullable => 0 }, "homepage", @@ -236,8 +244,8 @@ Composing rels: L -> username __PACKAGE__->many_to_many("usernames", "projectmembers", "username"); -# Created by DBIx::Class::Schema::Loader v0.07049 @ 2022-01-24 14:20:32 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:PtXDyT8Pc7LYhhdEG39EKQ +# Created by DBIx::Class::Schema::Loader v0.07049 @ 2022-11-22 12:51:02 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ppyLpFU2fZASFANhD7vUgg use JSON::MaybeXS; @@ -267,6 +275,7 @@ sub as_json { "enabled" => $self->get_column("enabled") ? JSON::MaybeXS::true : JSON::MaybeXS::false, "enable_dynamic_run_command" => $self->get_column("enable_dynamic_run_command") ? JSON::MaybeXS::true : JSON::MaybeXS::false, "hidden" => $self->get_column("hidden") ? JSON::MaybeXS::true : JSON::MaybeXS::false, + "private" => $self->get_column("private") ? JSON::MaybeXS::true : JSON::MaybeXS::false, "jobsets" => [ map { $_->name } $self->jobsets ] ); diff --git a/src/root/edit-project.tt b/src/root/edit-project.tt index bb850e5c..8836733b 100644 --- a/src/root/edit-project.tt +++ b/src/root/edit-project.tt @@ -17,6 +17,13 @@ +
+ +
+ +
+
+
diff --git a/src/root/overview.tt b/src/root/overview.tt index 6042c7bf..dcdc3fc0 100644 --- a/src/root/overview.tt +++ b/src/root/overview.tt @@ -54,7 +54,7 @@ [% FOREACH p IN projects %] - [% INCLUDE renderProjectName project=p.name inRow=1 %] + [% IF p.private %]🔒[% END %] [% INCLUDE renderProjectName project=p.name inRow=1 %] [% HTML.escape(p.displayname) %] [% WRAPPER maybeLink uri=p.homepage %][% HTML.escape(p.description) %][% END %] diff --git a/src/root/project.tt b/src/root/project.tt index 5e8ec0c8..21b993be 100644 --- a/src/root/project.tt +++ b/src/root/project.tt @@ -1,4 +1,9 @@ -[% WRAPPER layout.tt title="Project $project.name" %] +[% IF project.private %] +[% lock = ' 🔒' %] +[% ELSE %] +[% lock = '' %] +[% END %] +[% WRAPPER layout.tt titleHTML="Project $project.name$lock" title="Project $project.name" %] [% PROCESS common.tt %]