diff --git a/webtools/testopia/Bugzilla/Config/Testopia.pm b/webtools/testopia/Bugzilla/Config/Testopia.pm index 0eec405283a..4a88e0a3d0b 100644 --- a/webtools/testopia/Bugzilla/Config/Testopia.pm +++ b/webtools/testopia/Bugzilla/Config/Testopia.pm @@ -44,6 +44,17 @@ sub get_param_list { default => 0, }, + { + name => 'testopia-allow-group-member-deletes', + type => 'b', + default => 0, + }, + + { + name => 'testopia-default-plan-testers-regexp', + type => 't', + }, + # { # name => 'print-tag-in-case-log', # type => 'b', diff --git a/webtools/testopia/Bugzilla/Testopia/Search.pm b/webtools/testopia/Bugzilla/Testopia/Search.pm index d3bbbf81944..9a9f1bffe98 100644 --- a/webtools/testopia/Bugzilla/Testopia/Search.pm +++ b/webtools/testopia/Bugzilla/Testopia/Search.pm @@ -104,7 +104,7 @@ sub init { my @fields; my %specialorderjoin; my %chartfields; - + #my ($testergroup) = $dbh->selectrow_array("SELECT id FROM groups WHERE name = ?",undef, 'Testers'); # $chartid is the number of the current chart whose SQL we're constructing # $row is the current row of the current chart @@ -302,7 +302,30 @@ sub init { } } + # Set up tables for access control + unless (Bugzilla->user->in_group('Testers')){ + if ($obj eq 'case'){ + push(@supptables, "INNER JOIN test_case_plans AS case_plans " . + "ON test_cases.case_id = case_plans.case_id"); + push(@supptables, "INNER JOIN test_plans " . + "ON case_plans.plan_id = test_plans.plan_id"); } + elsif ($obj eq 'case_run'){ + push(@supptables, "INNER JOIN test_case_runs AS case_runs " . + "ON test_runs.run_id = case_runs.run_id"); + push(@supptables, "INNER JOIN test_cases " . + "ON case_runs.case_id = test_cases.case_id"); + } + elsif ($obj eq 'run'){ + push(@supptables, "INNER JOIN test_plans " . + "ON test_runs.plan_id = test_plans.plan_id"); + } + push @supptables, "INNER JOIN test_plan_permissions ON test_plans.plan_id = test_plan_permissions.plan_id"; + push @supptables, "INNER JOIN user_group_map AS map_testers ON map_testers.user_id = test_plan_permissions.userid"; + push @supptables, "INNER JOIN groups on map_testers.group_id = groups.id"; + push @wherepart, "((test_plan_permissions.permissions > 0 AND test_plan_permissions.userid = ". Bugzilla->user->id + .") OR (map_testers.user_id = ". Bugzilla->user->id ." AND groups.name = 'Testers'))"; + } # Set up tables for field sort order my $order = $cgi->param('order') || ''; if ($order eq 'author') { diff --git a/webtools/testopia/Bugzilla/Testopia/TestCase.pm b/webtools/testopia/Bugzilla/Testopia/TestCase.pm index 31adfa8b5de..8dd8e166ee4 100644 --- a/webtools/testopia/Bugzilla/Testopia/TestCase.pm +++ b/webtools/testopia/Bugzilla/Testopia/TestCase.pm @@ -740,18 +740,26 @@ sub unlink_plan { my $self = shift; my $dbh = Bugzilla->dbh; my ($plan_id) = @_; - + my $plan = Bugzilla::Testopia::TestPlan->new($plan_id); + + if (scalar @{$self->plans} == 1){ + $self->obliterate; + return 1; + } + $dbh->bz_lock_tables('test_case_plans WRITE', 'test_case_runs WRITE', 'test_runs READ', 'test_plans READ'); - if (!$self->can_unlink_plan($plan_id)){ - $dbh->bz_unlock_tables(); - return 0; + foreach my $run (@{$plan->test_runs}){ + $dbh->do("DELETE FROM test_case_runs + WHERE case_id = ? + AND run_id = ?", undef, $self->id, $run->id); } $dbh->do("DELETE FROM test_case_plans WHERE plan_id = ? - AND case_id = ?", undef, $plan_id, $self->{'case_id'}); + AND case_id = ?", + undef, $plan_id, $self->{'case_id'}); $dbh->bz_unlock_tables(); @@ -1262,20 +1270,12 @@ sub can_unlink_plan { my $self = shift; my ($plan_id) = @_; - return 0 if (!UserInGroup('managetestplans')); - - return 0 if scalar @{$self->plans} < 2; - - my $dbh = Bugzilla->dbh; - my ($res) = $dbh->selectrow_array( - "SELECT 1 - FROM test_case_runs - INNER JOIN test_runs ON test_case_runs.run_id = test_runs.run_id - WHERE test_runs.plan_id = ? - AND test_case_runs.case_id = ?", - undef, ($plan_id, $self->{'case_id'})); - - return !$res; + my $plan = Bugzilla::Testopia::TestPlan->new($plan_id); + return 1 if Bugzilla->user->in_group('admin'); + return 1 if Bugzilla->user->in_group('Testers') && Param("testopia-allow-group-member-deletes"); + return 1 if $plan->get_user_rights(Bugzilla->user->id, GRANT_REGEXP) & 4; + return 1 if $plan->get_user_rights(Bugzilla->user->id, GRANT_DIRECT) & 4; + return 0; } =head2 obliterate @@ -1315,9 +1315,10 @@ Returns true if the logged in user has rights to edit this test case. sub canedit { my $self = shift; - return $self->canview - && UserInGroup('managetestplans') - || UserInGroup('edittestcases'); + return 1 if Bugzilla->user->in_group('Testers'); + return 1 if $self->get_user_rights(Bugzilla->user->id, GRANT_REGEXP) & 2; + return 1 if $self->get_user_rights(Bugzilla->user->id, GRANT_DIRECT) & 2; + return 0; } =head2 canview @@ -1328,13 +1329,10 @@ Returns true if the logged in user has rights to view this test case. sub canview { my $self = shift; - return $self->{'canview'} if exists $self->{'canview'}; - my $ret = 1; - foreach my $p (@{$self->plans}){ - $ret = 0 unless Bugzilla::Testopia::Util::can_view_product($p->product_id); - } - $self->{'canview'} = $ret; - return $self->{'canview'}; + return 1 if Bugzilla->user->in_group('Testers'); + return 1 if $self->get_user_rights(Bugzilla->user->id, GRANT_REGEXP) > 0; + return 1 if $self->get_user_rights(Bugzilla->user->id, GRANT_DIRECT) > 0; + return 0; } =head2 candelete @@ -1345,26 +1343,36 @@ Returns true if the logged in user has rights to delete this test case. sub candelete { my $self = shift; - return 0 unless $self->canedit && Param("allow-test-deletion"); - return 1 if Bugzilla->user->in_group("admin"); - - # Allow plan author to delete if this case is linked only to plans she owns. + return 1 if Bugzilla->user->in_group('admin'); + return 0 unless Param("allow-test-deletion"); + return 1 if Bugzilla->user->in_group('Testers') && Param("testopia-allow-group-member-deletes"); + # Otherwise, check for delete rights on all the plans this is linked to my $own_all = 1; foreach my $plan (@{$self->plans}){ - if (Bugzilla->user->id != $plan->author->id) { + if (!($plan->get_user_rights(Bugzilla->user->id, GRANT_REGEXP) & 4) + || !($plan->get_user_rights(Bugzilla->user->id, GRANT_REGEXP) & 4)) { $own_all = 0; last; } } return 1 if $own_all; - - # Allow case author to delete if this case is not in any runs. - return 1 if Bugzilla->user->id == $self->author->id && - $self->get_caserun_count == 0; - return 0; } +sub get_user_rights { + my $self = shift; + my ($userid, $type) = @_; + + my $dbh = Bugzilla->dbh; + my ($perms) = $dbh->selectrow_array( + "SELECT MAX(permissions) FROM test_plan_permissions + LEFT JOIN test_case_plans ON test_plan_permissions.plan_id = test_case_plans.plan_id + INNER JOIN test_cases ON test_case_plans.case_id = test_cases.case_id + WHERE userid = ? AND test_plan_permissions.plan_id = ? AND grant_type = ?", + undef, ($userid, $self->id, $type)); + + return $perms; +} ############################### #### Accessors #### ############################### diff --git a/webtools/testopia/Bugzilla/Testopia/TestCaseRun.pm b/webtools/testopia/Bugzilla/Testopia/TestCaseRun.pm index 8508afbe005..380f81432eb 100644 --- a/webtools/testopia/Bugzilla/Testopia/TestCaseRun.pm +++ b/webtools/testopia/Bugzilla/Testopia/TestCaseRun.pm @@ -48,6 +48,7 @@ use Bugzilla::Util; use Bugzilla::Error; use Bugzilla::User; use Bugzilla::Config; +use Bugzilla::Constants; use Bugzilla::Testopia::Util; use Bugzilla::Testopia::Constants; use Bugzilla::Bug; @@ -976,66 +977,11 @@ Returns true if the logged in user has rights to view this case-run. =cut sub canview { - - my $self = shift; -# return $self->{'canview'} if exists $self->{'canview'}; -# my ($case_log_id, $run_id, $plan_id, $current_user_id) = @_; -# -# my $dbh = Bugzilla->dbh; -# my $canview = 0; -# my $current_user_id = Bugzilla->user->id; -# my ($plan_id) = $dbh->selectrow_array("SELECT plan_id FROM test_runs -# WHERE run_id=?", -# undef, $self->{'test_run_id'}); -# -# if (0 == &Bugzilla::Param('private-cases-log')) { -# $canview = 1; -# } else { -# -# if (0 == $self->{'isprivate'}) { -# # if !isprivate, then everybody can run it and should be able to see -# # the current status -# $canview = 1; -# } else { -# # check is the current user is a tester: -# if (defined $current_user_id) { -# -# SendSQL("select 1 from test_case_run_testers ". -# "where case_log_id=". $self->{'id'} ." and userid=$current_user_id"); -# -# if (FetchOneColumn()) { -# # current user is a tester -# $canview = 1; -# } else { -# # check editors -# SendSQL("select 1 from test_plans where plan_id=$plan_id and editor=$current_user_id"); -# -# if (FetchOneColumn()) { -# $canview = 1; -# } else { -# # check watchers -# SendSQL("select 1 from test_plans_watchers where plan_id=$plan_id and userid=$current_user_id"); -# if (FetchOneColumn()) { -# $canview = 1; -# } else { -# #check test run manager -# SendSQL("select 1 from test_runs where test_run_id=". $self->{'test_run_id'} ." and manager=$current_user_id"); -# $canview = FetchOneColumn()? 1 : 0; -# -# if (0 == $canview) { -# if (UserInGroup('admin')) { -# $canview = 1; -# } -# } -# } -# } -# } -# } -# } -# } - - $self->{'canview'} = 1; - return $self->{'canview'}; + my $self = shift; + return 1 if Bugzilla->user->in_group('Testers'); + return 1 if $self->run->plan->get_user_rights(Bugzilla->user->id, GRANT_REGEXP) > 0; + return 1 if $self->run->plan->get_user_rights(Bugzilla->user->id, GRANT_DIRECT) > 0; + return 0; } =head2 canedit @@ -1046,10 +992,10 @@ Returns true if the logged in user has rights to edit this case-run. sub canedit { my $self = shift; - return !$self->run->stop_date && $self->canview - && (UserInGroup('managetestplans') - || UserInGroup('edittestcases') - || UserInGroup('runtests')); + return 1 if Bugzilla->user->in_group('Testers'); + return 1 if $self->run->plan->get_user_rights(Bugzilla->user->id, GRANT_REGEXP) & 2; + return 1 if $self->run->plan->get_user_rights(Bugzilla->user->id, GRANT_DIRECT) & 2; + return 0; } =head2 candelete @@ -1060,9 +1006,11 @@ Returns true if the logged in user has rights to delete this case-run. sub candelete { my $self = shift; - return 0 unless $self->canedit && Param("allow-test-deletion"); - return 1 if Bugzilla->user->in_group("admin"); - return 1 if Bugzilla->user->id == $self->run->manager->id; + return 1 if Bugzilla->user->in_group('admin'); + return 0 unless Param("allow-test-deletion"); + return 1 if Bugzilla->user->in_group('Testers') && Param("testopia-allow-group-member-deletes"); + return 1 if $self->run->plan->get_user_rights(Bugzilla->user->id, GRANT_REGEXP) & 4; + return 1 if $self->run->plan->get_user_rights(Bugzilla->user->id, GRANT_DIRECT) & 4; return 0; } diff --git a/webtools/testopia/Bugzilla/Testopia/TestPlan.pm b/webtools/testopia/Bugzilla/Testopia/TestPlan.pm index 63472f72d92..2e5b026b11c 100644 --- a/webtools/testopia/Bugzilla/Testopia/TestPlan.pm +++ b/webtools/testopia/Bugzilla/Testopia/TestPlan.pm @@ -45,6 +45,7 @@ use Bugzilla::User; use Bugzilla::Util; use Bugzilla::Error; use Bugzilla::Config; +use Bugzilla::Constants; use Bugzilla::Testopia::Util; use Bugzilla::Testopia::TestRun; use Bugzilla::Testopia::TestCase; @@ -77,14 +78,14 @@ use base qw(Exporter); =cut use constant DB_COLUMNS => qw( - test_plans.plan_id - test_plans.product_id - test_plans.author_id - test_plans.type_id - test_plans.default_product_version - test_plans.name - test_plans.creation_date - test_plans.isactive + plan_id + product_id + author_id + type_id + default_product_version + name + creation_date + isactive ); use constant NAME_MAX_LENGTH => 255; @@ -183,6 +184,14 @@ sub store { my $key = $dbh->bz_last_key( 'test_plans', 'plan_id' ); $self->store_text($key, $self->{'author_id'}, $self->text, $timestamp); $self->{'plan_id'} = $key; + + # Add permissions for the plan + $self->add_tester($self->{'author_id'},15); + if (Param('testopia-default-plan-testers-regexp')) { + $self->set_tester_regexp( Param('testopia-default-plan-testers-regexp'), 3); + $self->derive_regexp_testers(Param('testopia-default-plan-testers-regexp')); + } + return $key; } @@ -595,6 +604,20 @@ sub check_plan_type { return $type; } +sub check_tester { + my $self = shift; + my $userid = shift; + my $dbh = Bugzilla->dbh; + + my ($exists) = $dbh->selectrow_array( + "SELECT 1 + FROM test_plan_permissions + WHERE userid = ? AND plan_id = ?", + undef, ($userid, $self->id)); + + return $exists; + +} =head2 update_plan_type Update the given type @@ -772,6 +795,30 @@ sub history { return $ref; } +sub copy_permissions { + my $self = shift; + my ($planid) = @_; + my $dbh = Bugzilla->dbh; + + my ($regexp, $perms) = $dbh->selectrow_array( + "SELECT user_regexp, permissions + FROM test_plan_permissions_regexp + WHERE plan_id = ?",undef, $self->id); + + $dbh->do("INSERT INTO test_plan_permissions_regexp (plan_id, user_regexp, permissions) + VALUES(?,?,?)", undef,($planid, $regexp, $perms)); + + my $ref = $dbh->selectall_arrayref( + "SELECT userid, permissions + FROM test_plan_permissions + WHERE plan_id = ? AND grant_type = ?", + {'Slice' =>{}}, ($self->id, GRANT_DIRECT)); + foreach my $row (@$ref){ + $dbh->do("INSERT INTO test_plan_permissions (userid, plan_id, permissions, grant_type) + VALUES(?,?,?,?)", undef, ($row->{'userid'}, $planid, $row->{'permissions'}, GRANT_DIRECT)); + } +} + =head2 lookup_type Takes an ID of the type field and returns the value @@ -846,7 +893,94 @@ sub lookup_product_by_name { return $value; } +sub set_tester_regexp { + my $self = shift; + my ($regexp, $permissions) = @_; + my $dbh = Bugzilla->dbh; + my ($is, $oldreg, $oldperms) = $dbh->selectrow_array( + "SELECT 1, user_regexp, permissions + FROM test_plan_permissions_regexp + WHERE plan_id = ?",undef, $self->id); + + return unless ($oldreg ne $regexp || $oldperms != $permissions); + if ($is){ + $dbh->do("UPDATE test_plan_permissions_regexp + SET user_regexp = ?, permissions = ? + WHERE plan_id = ?", undef, ($regexp, $permissions, $self->id)); + } + else { + $dbh->do("INSERT INTO test_plan_permissions_regexp(plan_id, user_regexp, permissions) + VALUES(?,?,?)", + undef, ($self->id, $regexp, $permissions)); + } + + $self->derive_regexp_testers($regexp); + +} +sub derive_regexp_testers { + my $self = shift; + my $regexp = shift; + my $dbh = Bugzilla->dbh; + # Get the permissions of the regexp testers so we can set it later. + my ($permissions) = $dbh->selectrow_array( + "SELECT permissions + FROM test_plan_permissions_regexp + WHERE plan_id = ?", undef, $self->id); + # If something has changed, it is easier to delete everyone and add tham back in + $dbh->do("DELETE FROM test_plan_permissions + WHERE plan_id = ? AND grant_type = ?", + undef, ($self->id, GRANT_REGEXP)); + + my $sth = $dbh->prepare("SELECT profiles.userid, profiles.login_name, plan_id + FROM profiles + LEFT JOIN test_plan_permissions + ON test_plan_permissions.userid = profiles.userid + AND test_plan_permissions.plan_id = ? + AND grant_type = ?"); + my $plan_add = $dbh->prepare("INSERT INTO test_plan_permissions + (userid, plan_id, permissions, grant_type) + VALUES (?,?,?,?)"); + my $plan_del = $dbh->prepare("DELETE FROM test_plan_permissions + WHERE user_id = ? AND plan_id = ? + AND grant_type = ?"); + $sth->execute($self->id, GRANT_REGEXP); + while (my ($userid, $login, $present) = $sth->fetchrow_array()) { + if (($regexp =~ /\S+/) && ($login =~ m/$regexp/i)){ + $plan_add->execute($userid, $self->id, $permissions, GRANT_REGEXP) unless $present; + } + } +} + +sub remove_tester { + my $self = shift; + my ($userid) = @_; + my $dbh = Bugzilla->dbh; + + $dbh->do("DELETE FROM test_plan_permissions + WHERE userid = ? AND plan_id = ? AND grant_type = ?", + undef, ($userid, $self->id, GRANT_DIRECT)); +} + +sub add_tester { + my $self = shift; + my ($userid, $perms) = @_; + my $dbh = Bugzilla->dbh; + + $dbh->do("INSERT INTO test_plan_permissions(userid, plan_id, permissions) + VALUES(?,?,?,?)", + undef, ($userid, $self->id, $perms, GRANT_DIRECT)); +} + +sub update_tester { + my $self = shift; + my ($userid, $perms) = @_; + my $dbh = Bugzilla->dbh; + + $dbh->do("UPDATE test_plan_permissions SET permissions = ? + WHERE userid = ? AND plan_id = ? AND grant_type = ?", + undef, ($perms, $userid, $self->id, GRANT_DIRECT)); +} =head2 obliterate @@ -903,7 +1037,11 @@ Returns true if the logged in user has rights to edit this plan sub canedit { my $self = shift; - return $self->canview && UserInGroup("managetestplans"); + return 1 if Bugzilla->user->in_group('Testers'); + return 1 if $self->get_user_rights(Bugzilla->user->id, GRANT_REGEXP) & 2; + return 1 if $self->get_user_rights(Bugzilla->user->id, GRANT_DIRECT) & 2; + return 0; + } =head2 canview @@ -914,8 +1052,10 @@ Returns true if the logged in user has rights to view this plan sub canview { my $self = shift; - return 1 if (Bugzilla->user->id == $self->author->id); - return Bugzilla::Testopia::Util::can_view_product($self->product_id); + return 1 if Bugzilla->user->in_group('Testers'); + return 1 if $self->get_user_rights(Bugzilla->user->id, GRANT_REGEXP) > 0; + return 1 if $self->get_user_rights(Bugzilla->user->id, GRANT_DIRECT) > 0; + return 0; } =head2 candelete @@ -926,12 +1066,34 @@ Returns true if the logged in user has rights to delete this plan sub candelete { my $self = shift; - return 0 unless $self->canedit && Param("allow-test-deletion"); + return 1 if Bugzilla->user->in_group('admin'); + return 0 unless Param("allow-test-deletion"); + return 1 if Bugzilla->user->in_group('Testers') && Param("testopia-allow-group-member-deletes"); + return 1 if $self->get_user_rights(Bugzilla->user->id, GRANT_REGEXP) & 4; + return 1 if $self->get_user_rights(Bugzilla->user->id, GRANT_DIRECT) & 4; + return 0; +} + +sub canadmin { + my $self = shift; return 1 if Bugzilla->user->in_group("admin"); - return 1 if Bugzilla->user->id == $self->author->id; + return 1 if ($self->get_user_rights(Bugzilla->user->id, GRANT_REGEXP) & 8); + return 1 if ($self->get_user_rights(Bugzilla->user->id, GRANT_DIRECT) & 8); return 0; } +sub get_user_rights { + my $self = shift; + my ($userid, $type) = @_; + + my $dbh = Bugzilla->dbh; + my ($perms) = $dbh->selectrow_array( + "SELECT permissions FROM test_plan_permissions + WHERE userid = ? AND plan_id = ? AND grant_type = ?", + undef, ($userid, $self->id, $type)); + + return $perms; +} ############################### #### Accessors #### @@ -970,6 +1132,12 @@ Returns the type id of this plan Returns true if this plan is not archived +=head2 use_product_rights + +If true, user access is granted based first on product groups and then on the +plan's access list otherwise, all right associated with this plan are +determined bey the ACL. + =cut sub id { return $_[0]->{'plan_id'}; } @@ -993,6 +1161,59 @@ sub type { return $self->{'type'}; } +sub tester_regexp { + my ($self) = @_; + my $dbh = Bugzilla->dbh; + + my ($regexp) = $dbh->selectrow_array( + "SELECT user_regexp + FROM test_plan_permissions_regexp + WHERE plan_id = ?", undef, $self->id); + + return $regexp; +} + +sub tester_regexp_permissions { + my ($self) = @_; + my $dbh = Bugzilla->dbh; + + my ($perms) = $dbh->selectrow_array( + "SELECT permissions + FROM test_plan_permissions_regexp + WHERE plan_id = ?", undef, $self->id); + my $p; + + $p->{'read'} = 1 & $perms; + $p->{'write'} = 2 & $perms; + $p->{'delete'} = 4 & $perms; + $p->{'admin'} = 8 & $perms; + + return $p; +} + +sub access_list { + my ($self) = @_; + my $dbh = Bugzilla->dbh; + + my $ref = $dbh->selectall_arrayref( + "SELECT tpt.userid, permissions + FROM test_plan_permissions AS tpt + JOIN profiles ON profiles.userid = tpt.userid + WHERE plan_id = ? AND grant_type = ? + ORDER BY profiles.realname", {'Slice' =>{}}, ($self->id, GRANT_DIRECT)); + my @rows; + foreach my $row (@$ref){ + push @rows, {'user' => Bugzilla::User->new($row->{'userid'}), + 'read' => 1 & $row->{'permissions'}, + 'write' => 2 & $row->{'permissions'}, + 'delete' => 4 & $row->{'permissions'}, + 'admin' => 8 & $row->{'permissions'}, + }; + } + $self->{'access_list'} = \@rows; + return $self->{'access_list'}; +} + =head2 attachments Returns a reference to a list of attachments on this plan diff --git a/webtools/testopia/Bugzilla/Testopia/TestRun.pm b/webtools/testopia/Bugzilla/Testopia/TestRun.pm index 024c5d9caaf..e9a61577033 100644 --- a/webtools/testopia/Bugzilla/Testopia/TestRun.pm +++ b/webtools/testopia/Bugzilla/Testopia/TestRun.pm @@ -738,10 +738,10 @@ Returns true if the logged in user has rights to edit this test run. sub canedit { my $self = shift; - return $self->canview - && (UserInGroup('managetestplans') - || UserInGroup('edittestcases') - || UserInGroup('runtests')); + return 1 if Bugzilla->user->in_group('Testers'); + return 1 if $self->plan->get_user_rights(Bugzilla->user->id, GRANT_REGEXP) & 2; + return 1 if $self->plan->get_user_rights(Bugzilla->user->id, GRANT_DIRECT) & 2; + return 0; } =head2 canview @@ -752,9 +752,11 @@ Returns true if the logged in user has rights to view this test run. sub canview { my $self = shift; - return $self->{'canview'} if exists $self->{'canview'}; - $self->{'canview'} = Bugzilla::Testopia::Util::can_view_product($self->plan->product_id); - return $self->{'canview'}; + return 1 if Bugzilla->user->in_group('Testers'); + return 1 if $self->plan->get_user_rights(Bugzilla->user->id, GRANT_REGEXP) > 0; + return 1 if $self->plan->get_user_rights(Bugzilla->user->id, GRANT_DIRECT) > 0; + return 0; + } =head2 candelete @@ -765,10 +767,11 @@ Returns true if the logged in user has rights to delete this test run. sub candelete { my $self = shift; - return 0 unless $self->canedit && Param("allow-test-deletion"); - return 1 if Bugzilla->user->in_group("admin"); - return 1 if Bugzilla->user->id == $self->manager->id; - return 1 if Bugzilla->user->id == $self->plan->author->id; + return 1 if Bugzilla->user->in_group('admin'); + return 0 unless Param("allow-test-deletion"); + return 1 if Bugzilla->user->in_group('Testers') && Param("testopia-allow-group-member-deletes"); + return 1 if $self->plan->get_user_rights(Bugzilla->user->id, GRANT_REGEXP) & 4; + return 1 if $self->plan->get_user_rights(Bugzilla->user->id, GRANT_DIRECT) & 4; return 0; } diff --git a/webtools/testopia/template/en/default/admin/params/testopia.html.tmpl b/webtools/testopia/template/en/default/admin/params/testopia.html.tmpl index 48f3bedc651..59fc8cb7e9f 100644 --- a/webtools/testopia/template/en/default/admin/params/testopia.html.tmpl +++ b/webtools/testopia/template/en/default/admin/params/testopia.html.tmpl @@ -29,6 +29,12 @@ "allow-test-deletion" => "If this option is on, users can delete objects including plans and cases", + "testopia-allow-group-member-deletes" => "If this option is on, members of the Testers group will be + allowed to delete test objects", + + "testopia-default-plan-testers-regexp" => "This is the default regular expression for granting + access to new test plans", + "print-tag-in-case-log" => 'If this option is on, the entire tag text is printed in a test case ' _ 'log entry. Otherwise, only an href to the tag is put there.', diff --git a/webtools/testopia/template/en/default/hook/global/user-error.html.tmpl/errors/tr-user-error.html.tmpl b/webtools/testopia/template/en/default/hook/global/user-error.html.tmpl/errors/tr-user-error.html.tmpl index ba3c35b2c2a..0388cb263a0 100644 --- a/webtools/testopia/template/en/default/hook/global/user-error.html.tmpl/errors/tr-user-error.html.tmpl +++ b/webtools/testopia/template/en/default/hook/global/user-error.html.tmpl/errors/tr-user-error.html.tmpl @@ -120,5 +120,11 @@ [% ELSIF error == "missing-plans-list" %] [% title = "No plans selected" %] You did not select any plans to copy this case to. + [% ELSIF error == "testopia-tester-already-on-list" %] + [% title = "Selected user is already on the list" %] + The user [% login FILTER html %] is already a member of the ACL for this plan. + [% ELSIF error == "testopia-plan-acl-denied" %] + [% title = "Plan Administrator Privileges Required" %] + You must be an administrator of this test plan to modify the access control list. [% END %] diff --git a/webtools/testopia/template/en/default/testopia/admin/access-list.html.tmpl b/webtools/testopia/template/en/default/testopia/admin/access-list.html.tmpl new file mode 100644 index 00000000000..52370bc4660 --- /dev/null +++ b/webtools/testopia/template/en/default/testopia/admin/access-list.html.tmpl @@ -0,0 +1,106 @@ +[%# 1.0@bugzilla.org %] +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Testopia System. + # + # The Initial Developer of the Original Code is Greg Hendricks. + # Portions created by Greg Hendricks are Copyright (C) 2006 + # Novell. All Rights Reserved. + # + # Contributor(s): Greg Hendricks + #%] + + +[% PROCESS global/header.html.tmpl %] + +

Access Control for Plan [% plan.id FILTER none %] - [% plan.name FILTER html %]

+

+This page allows plan managers to permit access to a test plan. By default, members of the +Testers group in Bugzilla will have read and write access. Beyond this, users login names +(email addresses) that match a given regular expression will be granted the level of permissions +specified. Lastly, individuals can be granted additional permissions by adding them +explicitly. +

+

+Delete and Admin rights are always handled by the access control list, unless the +testopia-allow-group-member-deletes parameter is set to on. Doing so will grant +delete rights to the Testers group. +

+ +[% PROCESS testopia/messages.html.tmpl %] +
+ +

Access Method

+ + + + + + + + + + + + + + + +
User Regular ExpressionReadWriteDeleteAdmin

+
+

Access Control List

+ + + + + + + + + [% FOREACH row = plan.access_list %] + + + + + + + + + [% END %] + + + + + + + + +
UserReadWriteDeleteAdmin
[%'*' IF row.user.id == plan.author.id %][% row.user.identity FILTER html %]Remove
+

+ +

+
+
Read
+
Allows viewing rights to the plan and all test cases, test runs, and test case-runs associated with it. + Test cases linked to more than one plan will not be visible unless the user has view rights on all plans linked
+
Write
+
Implies Read. Allows rights to modify the plan and associated cases, runs, and case-runs.
+
Delete
+
Implies Read and Write. Allows rights to delete the plan and associated cases, runs, and case-runs.
+
Admin
+
Implies Read, Write, and Delete. Allows rights to modify the plan's access controls (this page).
+
+* Plan author +

+Back to test plan +

+ +[% PROCESS global/footer.html.tmpl %] diff --git a/webtools/testopia/template/en/default/testopia/case/delete.html.tmpl b/webtools/testopia/template/en/default/testopia/case/delete.html.tmpl index a85cfce6972..04f0dd06cfe 100644 --- a/webtools/testopia/template/en/default/testopia/case/delete.html.tmpl +++ b/webtools/testopia/template/en/default/testopia/case/delete.html.tmpl @@ -34,7 +34,7 @@ [%############################################################################%] [% PROCESS global/header.html.tmpl - title = "Delete Test Case $case.summary" + title = "Delete Test Case: $case.id - $case.summary" %] [% IF NOT deleted %] diff --git a/webtools/testopia/template/en/default/testopia/case/unlink.html.tmpl b/webtools/testopia/template/en/default/testopia/case/unlink.html.tmpl new file mode 100644 index 00000000000..628d6f663c9 --- /dev/null +++ b/webtools/testopia/template/en/default/testopia/case/unlink.html.tmpl @@ -0,0 +1,48 @@ +[%# 1.0@bugzilla.org %] +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Testopia System. + # + # The Initial Developer of the Original Code is Greg Hendricks. + # Portions created by Greg Hendricks are Copyright (C) 2007 + # Novell. All Rights Reserved. + # + # Contributor(s): Greg Hendricks + #%] + +[%# INTERFACE: + # ... + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Unlink Test Case: $case.id - $case.summary" +%] + +You are about to permanently unlink this test case from test plan
+[% plan.id FILTER none %] - [% plan.name FILTER html %]. +

+This will remove all history of this case from this plan. +
+Warning: This action cannot be undone +
+
+ + + + + +

+ +Go back + +[% PROCESS global/footer.html.tmpl %] diff --git a/webtools/testopia/template/en/default/testopia/plan/clone.html.tmpl b/webtools/testopia/template/en/default/testopia/plan/clone.html.tmpl index 94f9745dabd..8956af60ba5 100644 --- a/webtools/testopia/template/en/default/testopia/plan/clone.html.tmpl +++ b/webtools/testopia/template/en/default/testopia/plan/clone.html.tmpl @@ -90,6 +90,12 @@ type="radio" name="copy_tags" value="0" />No + + Copy permissions: + Yes No + + Maintain Plan and Case Authors: Yes
Edit Plan Attributes + [% IF plan.canadmin %] +  | Edit Access Controls + [% END %] + diff --git a/webtools/testopia/testopia/css/default.css b/webtools/testopia/testopia/css/default.css index b50827d60f3..e69de29bb2d 100755 --- a/webtools/testopia/testopia/css/default.css +++ b/webtools/testopia/testopia/css/default.css @@ -1,177 +0,0 @@ -textarea { - background-color: #fff; - border: 1px solid #999; -} - -.resultsTable { - border-collapse: collapse; /*cellspacing*/ - border-spacing: 0px; -} -.resultsTable td { /*only immediate tds of mytable*/ - padding: 4px; /*cellpadding*/ -} - -.resultsTable td a:hover { - text-decoration:underline overline; -} - -.resultsTable th { - border-color:#FFF; - background-color:#C0FFC0; - - border-style:solid; - border-width: 2px; - padding: 4px; -} - -.dlgTable { - border-collapse: collapse; /*cellspacing*/ - border-spacing: 0px; -} - -.dlgTable td { /*only immediate tds of mytable*/ - padding: 4px; /*cellpadding*/ - background-color:#EEE; -} - -.spacerTable { - border-collapse: collapse; /*cellspacing*/ - border-spacing: 0px; -} -.spacerTable td { /*only immediate tds of mytable*/ - padding: 16px; /*cellpadding*/ - background-color:#FFF; -} - -.menuTable { - border-collapse: collapse; /*cellspacing*/ - border-spacing: 0px; -} -.menuTable td { /*only immediate tds of mytable*/ - padding: 4px; /*cellpadding*/ - background-color:#EEE; - - border-style:solid; - border-width: 3px; - border-color:#FFF; -} - -.evenRow_first { - background-color:#FFF; -} -.evenRow { - background-color:#FFF; -} -.evenRow td{ - background-color:#FFF;border-top-style:solid;border-top-width:1px;border-top-color:#CCC; -} - -.oddRow_first { - background-color:#EEE; -} -.oddRow { - background-color:#EEE; -} -.oddRow td { - background-color:#EEE;border-top-style:solid;border-top-width:1px;border-top-color:#CCC; -} - -.highlightedCell { - border-width:1px; - border-style:solid; - background-color:lightyellow; -} - -/* tr_showcaselog */ - -.short_body { - border-bottom:1px solid #000; - border-left:1px solid #000; - border-right:1px solid #000; -} - -.short_head { - border-top:1px solid #000; - border-left:1px solid #000; - border-right:1px solid #000; -} - -.ae_dv { - display:none; - margin-left:15px; - border-width:0px; -} - -.ae_tb { - width:100%; - border:0; - margin:0; - margin-right:30px; - margin-bottom:10px; - border-collapse:collapse; - border-spacing: 0px; -} - -.ae_s { - border-style:solid; - border-width:1px; - border-color:#000; - background-color:#FFFFE0; -} - -.cc_i { - float:left; - margin-left:10px; -} - -.cc_xx { - clear:both; - margin-right:30px; - padding-top:5px; -} - -.cc_trg { - padding-left:5px; - padding-right:5px; - vertical-align:baseline; -} - -/* TODO: refactor these two: */ - -#floatMsg { - text-align:right; - background-color:#FFDD66; - color:black; - padding:4px; - padding-left:20px; - padding-right:20px; - font-family:Arial; - font-weight:bold; - font-size:12px; - display:none; -} - -.floatMsg { - text-align:right; - background-color:#FFDD66; - color:black; - padding:4px; - padding-left:20px; - padding-right:20px; - font-family:Arial; - font-weight:bold; - font-size:12px; - display:none; -} - -.tr_button { - width:100px; -} - -a img { - border: none; -} - -h3 { - border-bottom: 1px dotted #000; -} \ No newline at end of file diff --git a/webtools/testopia/tr_install.pl b/webtools/testopia/tr_install.pl index 5472dbd89cb..8e956c86b5a 100644 --- a/webtools/testopia/tr_install.pl +++ b/webtools/testopia/tr_install.pl @@ -87,6 +87,9 @@ if (!GroupDoesExist($dbh, "runtests")) { 'Can add, delete and edit test runs.', $adminid); tr_AssignAdminGrants($dbh, $groupid, $adminid); } + +updateACLs($dbh); + print "Done.\n\n"; ############################################################################## print "Cleaning up Testopia cache ...\n"; @@ -295,9 +298,9 @@ sub UpdateDB { $dbh->bz_drop_table('test_case_group_map'); $dbh->bz_drop_table('test_category_templates'); $dbh->bz_drop_table('test_plan_testers'); - + $dbh->bz_drop_table('test_plan_group_map'); $dbh->bz_drop_column('test_plans', 'editor_id'); - + $dbh->bz_add_column('test_case_bugs', 'case_id', {TYPE => 'INT4', UNSIGNED => 1}); $dbh->bz_add_column('test_case_runs', 'environment_id', {TYPE => 'INT4', UNSIGNED => 1, NOTNULL => 1}, 0); $dbh->bz_add_column('test_case_tags', 'userid', {TYPE => 'INT3', NOTNULL => 1}, 0); @@ -380,7 +383,7 @@ sub UpdateDB { $dbh->bz_add_index('test_runs', 'test_run_plan_id_run_id_idx', [qw(plan_id run_id)]); $dbh->bz_add_index('test_runs', 'test_runs_summary_idx', {FIELDS => ['summary'], TYPE => 'FULLTEXT'}); - if ($dbh->bz_index_info('test_case_tags', 'case_tags_case_id_idx')->{TYPE} eq '') { + if ($dbh->bz_index_info('test_case_tags', 'case_tags_case_id_idx') && $dbh->bz_index_info('test_case_tags', 'case_tags_case_id_idx')->{TYPE} eq '') { $dbh->bz_drop_index('test_case_tags', 'case_tags_case_id_idx'); $dbh->bz_add_index('test_case_tags', 'case_tags_case_id_idx', {FIELDS => [qw(tag_id case_id)], TYPE => 'UNIQUE'}); } @@ -399,6 +402,21 @@ sub UpdateDB { migrateEnvData($dbh); } +sub updateACLs { + my $dbh = shift; + print "Checking plan ACLs \n"; + my $ref = $dbh->selectall_arrayref("SELECT plan_id, author_id FROM test_plans", {'Slice' =>{}}); + foreach my $plan (@$ref){ + my ($finished) = $dbh->selectrow_array( + "SELECT COUNT(*) FROM test_plan_permissions + WHERE plan_id = ? AND userid = ?", + undef, ($plan->{'plan_id'}, $plan->{'author_id'})); + next if ($finished); + $dbh->do("INSERT INTO test_plan_permissions(userid, plan_id, permissions) + VALUES(?,?,?)", + undef, ($plan->{'author_id'}, $plan->{'plan_id'}, 15)); + } +} sub populateMiscTables { my ($dbh) = (@_); diff --git a/webtools/testopia/tr_plan_access.cgi b/webtools/testopia/tr_plan_access.cgi new file mode 100644 index 00000000000..8bde3ce4e2a --- /dev/null +++ b/webtools/testopia/tr_plan_access.cgi @@ -0,0 +1,139 @@ +#!/usr/bin/perl -wT +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. +# +# The Original Code is the Bugzilla Testopia System. +# +# The Initial Developer of the Original Code is Greg Hendricks. +# Portions created by Greg Hendricks are Copyright (C) 2007 +# Novell. All Rights Reserved. +# +# Contributor(s): Greg Hendricks + +use strict; +use lib "."; + +use Bugzilla; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Util; +use Bugzilla::Testopia::Util; +use Bugzilla::Testopia::Product; + +use vars qw($vars); +use Data::Dumper; + +require 'globals.pl'; + +my $template = Bugzilla->template; +my $cgi = Bugzilla->cgi; +Bugzilla->login(); +print $cgi->header; + +my $plan_id = trim($cgi->param('plan_id') || ''); +my $action = $cgi->param('action') || ''; + +unless (detaint_natural($plan_id)){ + $vars->{'form_action'} = 'tr_plan_access.cgi'; + $template->process("testopia/plan/choose.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; +} + +my $plan = Bugzilla::Testopia::TestPlan->new($plan_id); + +unless ($plan->canadmin){ + ThrowUserError('testopia-plan-acl-denied', {plan_id => $plan->id}); +} + + +#print Dumper($plan->access_list); + +if ($action eq 'Apply Changes'){ + do_update(); + display(); +} +elsif ($action eq 'Add User'){ + do_update(); + + my $dbh = Bugzilla->dbh; + my $userid = DBNameToIdAndCheck(trim($cgi->param('adduser'))); + ThrowUserError('testopia-tester-already-on-list', {'login' => $cgi->param('adduser')}) + if ($plan->check_tester($userid)); + + my $perms = 0; + + # The order we check these is important since each permission + # implies the prior ones. + $perms = $cgi->param("nr") ? 1 : $perms; + $perms = $cgi->param("nw") ? 3 : $perms; + $perms = $cgi->param("nd") ? 7 : $perms; + $perms = $cgi->param("na") ? 15 : $perms; + + $plan->add_tester($userid, $perms); + + display(); +} +elsif ($action eq 'delete'){ + my $userid = $cgi->param('user'); + detaint_natural($userid); + my $user = Bugzilla::User->new($userid); + $plan->remove_tester($user->id); + $vars->{'tr_message'} = $user->login ." Removed from Plan"; + display(); +} + +else{ + display(); +} + +sub do_update { + # We need at least on admin + my $params = join(" ", $cgi->param()); + ThrowUserErorr('testopia-no-admins') unless $params =~ /a\d+/; + + my $tester_regexp = $cgi->param('userregexp'); + trick_taint($tester_regexp); + + my $regexp_perms = 0; + + # The order we check these is important since each permission + # implies the prior ones. + $regexp_perms = $cgi->param('pr') ? 1 : $regexp_perms; + $regexp_perms = $cgi->param('pw') ? 3 : $regexp_perms; + $regexp_perms = $cgi->param('pd') ? 7 : $regexp_perms; + $regexp_perms = $cgi->param('pa') ? 15 : $regexp_perms; + + $plan->set_tester_regexp($tester_regexp, $regexp_perms); + + my $dbh = Bugzilla->dbh; + foreach my $row (@{$plan->access_list}){ + my $perms = 0; + + # The order we check these is important since each permission + # implies the prior ones. + $perms = $cgi->param('r'.$row->{'user'}->id) ? 1 : $perms; + $perms = $cgi->param('w'.$row->{'user'}->id) ? 3 : $perms; + $perms = $cgi->param('d'.$row->{'user'}->id) ? 7 : $perms; + $perms = $cgi->param('a'.$row->{'user'}->id) ? 15 : $perms; + + $plan->update_tester($row->{'user'}->id, $perms); + } +} + +sub display { + $vars->{'plan'} = $plan; + $vars->{'user'} = Bugzilla->user; + $template->process("testopia/admin/access-list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + +} \ No newline at end of file diff --git a/webtools/testopia/tr_show_case.cgi b/webtools/testopia/tr_show_case.cgi index 3f4ed694ab3..860541aaf8f 100755 --- a/webtools/testopia/tr_show_case.cgi +++ b/webtools/testopia/tr_show_case.cgi @@ -222,12 +222,37 @@ elsif ($action eq 'unlink'){ my $plan_id = $cgi->param('plan_id'); validate_test_id($plan_id, 'plan'); my $case = Bugzilla::Testopia::TestCase->new($case_id); + ThrowUserError("testopia-read-only", {'object' => 'case'}) + unless ($case->can_unlink_plan($plan_id)); + + if (scalar @{$case->plans} == 1){ + $vars->{'case'} = $case; + $vars->{'runcount'} = scalar @{$case->runs}; + $vars->{'plancount'} = scalar @{$case->plans}; + $vars->{'bugcount'} = scalar @{$case->bugs}; + $template->process("testopia/case/delete.html.tmpl", $vars) || + ThrowTemplateError($template->error()); + } + else { + $vars->{'plan'} = Bugzilla::Testopia::TestPlan->new($plan_id); + $vars->{'case'} = $case; + $template->process("testopia/case/unlink.html.tmpl", $vars) || + ThrowTemplateError($template->error()); + } +} + +elsif ($action eq 'do_unlink'){ + Bugzilla->login(LOGIN_REQUIRED); + my $plan_id = $cgi->param('plan_id'); + validate_test_id($plan_id, 'plan'); + my $case = Bugzilla::Testopia::TestCase->new($case_id); + ThrowUserError("testopia-read-only", {'object' => 'case'}) + unless ($case->can_unlink_plan($plan_id)); + if ($case->unlink_plan($plan_id)){ $vars->{'tr_message'} = "Test plan successfully unlinked"; } - else { - $vars->{'tr_error'} = "Test plan could not be unlinked. It is used in test runs."; - } + $vars->{'backlink'} = $case; display($case); } diff --git a/webtools/testopia/tr_show_plan.cgi b/webtools/testopia/tr_show_plan.cgi index 724ef87006a..948b5cbe087 100755 --- a/webtools/testopia/tr_show_plan.cgi +++ b/webtools/testopia/tr_show_plan.cgi @@ -135,6 +135,17 @@ elsif ($action eq 'do_clone'){ $newplan->add_tag($newtagid); } } + if ($cgi->param('copy_perms')){ + $plan->copy_permissions($newplanid); + $newplan->derive_regexp_testers($plan->tester_regexp); + } + else { + # Give the author admin rights + $newplan->add_tester($author, 15); + $newplan->set_tester_regexp( Param('testopia-default-plan-testers-regexp'), 3) + if Param('testopia-default-plan-testers-regexp'); + $newplan->derive_regexp_testers(Param('testopia-default-plan-testers-regexp')) + } if ($cgi->param('copy_cases')){ my @catids; #TODO: Copy case to the new category @@ -478,13 +489,13 @@ sub display { my @time = localtime(time()); my $date = sprintf "%04d-%02d-%02d", 1900+$time[5],$time[4]+1,$time[3]; my $filename = "testcases-$date.$format->{extension}"; -# print $cgi->multipart_final if ($serverpush && $vars->{'case_table'}->list_count >= 1000); print $cgi->header(-type => $format->{'ctype'}, - -content_disposition => "$disp; filename=$filename"); + -content_disposition => "$disp; filename=$filename") + unless ($action eq 'do_clone'); - $vars->{'percentage'} = \&percentage; - $template->process($format->{'template'}, $vars) || - ThrowTemplateError($template->error()); + $vars->{'percentage'} = \&percentage; + $template->process($format->{'template'}, $vars) || + ThrowTemplateError($template->error()); }