From 21a232b78413718d8a68867ba7eb4f52a287f9b6 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Thu, 7 Jun 2012 16:58:33 -0700 Subject: [PATCH] rt 4.0.6 --- rt/etc/upgrade/4.0.6/content | 17 +++ rt/etc/upgrade/4.0.6/schema.mysql | 1 + rt/share/html/Articles/Elements/ShowTopicLink | 27 ++++ rt/share/html/Elements/CSRF | 74 ++++++++++ rt/share/html/l_unsafe | 52 +++++++ rt/t/api/report_tickets.t | 15 ++ rt/t/mail/dashboard-chart-with-utf8.t | 82 +++++++++++ rt/t/mail/rfc2231-attachment.t | 28 ++++ rt/t/mail/specials-in-encodedwords.t | 40 ++++++ rt/t/web/command_line_link_to_articles.t | 48 +++++++ rt/t/web/csrf-rest.t | 77 +++++++++++ rt/t/web/csrf.t | 181 ++++++++++++++++++++++++ rt/t/web/installer.t | 95 +++++++++++++ rt/t/web/owner_disabled_group_19221.t | 190 ++++++++++++++++++++++++++ rt/t/web/query_builder_queue_limits.t | 180 ++++++++++++++++++++++++ rt/t/web/rest_cfs_with_same_name.t | 88 ++++++++++++ 16 files changed, 1195 insertions(+) create mode 100644 rt/etc/upgrade/4.0.6/content create mode 100644 rt/etc/upgrade/4.0.6/schema.mysql create mode 100644 rt/share/html/Articles/Elements/ShowTopicLink create mode 100644 rt/share/html/Elements/CSRF create mode 100644 rt/share/html/l_unsafe create mode 100644 rt/t/api/report_tickets.t create mode 100644 rt/t/mail/dashboard-chart-with-utf8.t create mode 100644 rt/t/mail/rfc2231-attachment.t create mode 100644 rt/t/mail/specials-in-encodedwords.t create mode 100644 rt/t/web/command_line_link_to_articles.t create mode 100644 rt/t/web/csrf-rest.t create mode 100644 rt/t/web/csrf.t create mode 100644 rt/t/web/installer.t create mode 100644 rt/t/web/owner_disabled_group_19221.t create mode 100644 rt/t/web/query_builder_queue_limits.t create mode 100644 rt/t/web/rest_cfs_with_same_name.t diff --git a/rt/etc/upgrade/4.0.6/content b/rt/etc/upgrade/4.0.6/content new file mode 100644 index 000000000..dc1a00951 --- /dev/null +++ b/rt/etc/upgrade/4.0.6/content @@ -0,0 +1,17 @@ +@Initial = ( + sub { + my $txns = RT::Transactions->new( $RT::SystemUser ); + $txns->Limit( + FIELD => "ObjectType", + VALUE => "RT::User", + ); + $txns->Limit( + FIELD => "Field", + VALUE => "Password", + ); + while (my $txn = $txns->Next) { + $txn->__Set( Field => $_, Value => '********' ) + for qw/OldValue NewValue/; + } + }, +); diff --git a/rt/etc/upgrade/4.0.6/schema.mysql b/rt/etc/upgrade/4.0.6/schema.mysql new file mode 100644 index 000000000..ab32007ae --- /dev/null +++ b/rt/etc/upgrade/4.0.6/schema.mysql @@ -0,0 +1 @@ +ALTER TABLE Attributes MODIFY Content LONGBLOB; diff --git a/rt/share/html/Articles/Elements/ShowTopicLink b/rt/share/html/Articles/Elements/ShowTopicLink new file mode 100644 index 000000000..7b6d550be --- /dev/null +++ b/rt/share/html/Articles/Elements/ShowTopicLink @@ -0,0 +1,27 @@ +<%args> +$Topic +$Class => 0 + +% if ($Link) { +\ +% } +<% $Topic->Name() || loc("(no name)") %>\ +% if ($Topic->Description) { +: <% $Topic->Description %> +% } + +% if ( $Articles->Count ) { + (<&|/l, $Articles->Count &>[quant,_1,article]) +% } + +% if ($Link) { + +% } + +<%init> +my $Articles = RT::ObjectTopics->new( $session{'CurrentUser'} ); +$Articles->Limit( FIELD => 'ObjectType', VALUE => 'RT::Article' ); +$Articles->Limit( FIELD => 'Topic', VALUE => $Topic->Id ); + +my $Link = $Topic->Children->Count || $Articles->Count; + diff --git a/rt/share/html/Elements/CSRF b/rt/share/html/Elements/CSRF new file mode 100644 index 000000000..4893c1216 --- /dev/null +++ b/rt/share/html/Elements/CSRF @@ -0,0 +1,74 @@ +%# BEGIN BPS TAGGED BLOCK {{{ +%# +%# COPYRIGHT: +%# +%# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +%# +%# +%# (Except where explicitly superseded by other copyright notices) +%# +%# +%# LICENSE: +%# +%# This work is made available to you under the terms of Version 2 of +%# the GNU General Public License. A copy of that license should have +%# been provided with this software, but in any event can be snarfed +%# from www.gnu.org. +%# +%# This work is distributed in the hope that it will be useful, but +%# WITHOUT ANY WARRANTY; without even the implied warranty of +%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%# General Public License for more details. +%# +%# You should have received a copy of the GNU General Public License +%# along with this program; if not, write to the Free Software +%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +%# 02110-1301 or visit their web page on the internet at +%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +%# +%# +%# CONTRIBUTION SUBMISSION POLICY: +%# +%# (The following paragraph is not intended to limit the rights granted +%# to you to modify and distribute this software under the terms of +%# the GNU General Public License and is only of importance to you if +%# you choose to contribute your changes and enhancements to the +%# community by submitting them to Best Practical Solutions, LLC.) +%# +%# By intentionally submitting any modifications, corrections or +%# derivatives to this work, or any other work intended for use with +%# Request Tracker, to Best Practical Solutions, LLC, you confirm that +%# you are the copyright holder for those contributions and you grant +%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +%# royalty-free, perpetual, license to use, copy, create derivative +%# works based on those contributions, and sublicense and distribute +%# those contributions and any derivatives thereof. +%# +%# END BPS TAGGED BLOCK }}} +<& /Elements/Header, Title => loc('Possible cross-site request forgery') &> +<& /Elements/Tabs &> + +

<&|/l&>Possible cross-site request forgery

+ +% my $strong_start = ""; +% my $strong_end = ""; +

<&|/l_unsafe, $strong_start, $strong_end, $Reason &>RT has detected a possible [_1]cross-site request forgery[_2] for this request, because [_3]. This is possibly caused by a malicious attacker trying to perform actions against RT on your behalf. If you did not initiate this request, then you should alert your security team.

+ +% my $start = qq||; +% my $end = qq||; +

<&|/l_unsafe, $escaped_path, $start, $end &>If you really intended to visit [_1], then [_2]click here to resume your request[_3].

+ +<& /Elements/Footer, %ARGS &> +% $m->abort; +<%ARGS> +$OriginalURL => '' +$Reason => '' +$Token => '' + +<%INIT> +my $escaped_path = $m->interp->apply_escapes($OriginalURL, 'h'); +$escaped_path = "$escaped_path"; + +my $url_with_token = URI->new($OriginalURL); +$url_with_token->query_form([CSRF_Token => $Token]); + diff --git a/rt/share/html/l_unsafe b/rt/share/html/l_unsafe new file mode 100644 index 000000000..6396bc640 --- /dev/null +++ b/rt/share/html/l_unsafe @@ -0,0 +1,52 @@ +%# BEGIN BPS TAGGED BLOCK {{{ +%# +%# COPYRIGHT: +%# +%# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +%# +%# +%# (Except where explicitly superseded by other copyright notices) +%# +%# +%# LICENSE: +%# +%# This work is made available to you under the terms of Version 2 of +%# the GNU General Public License. A copy of that license should have +%# been provided with this software, but in any event can be snarfed +%# from www.gnu.org. +%# +%# This work is distributed in the hope that it will be useful, but +%# WITHOUT ANY WARRANTY; without even the implied warranty of +%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%# General Public License for more details. +%# +%# You should have received a copy of the GNU General Public License +%# along with this program; if not, write to the Free Software +%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +%# 02110-1301 or visit their web page on the internet at +%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +%# +%# +%# CONTRIBUTION SUBMISSION POLICY: +%# +%# (The following paragraph is not intended to limit the rights granted +%# to you to modify and distribute this software under the terms of +%# the GNU General Public License and is only of importance to you if +%# you choose to contribute your changes and enhancements to the +%# community by submitting them to Best Practical Solutions, LLC.) +%# +%# By intentionally submitting any modifications, corrections or +%# derivatives to this work, or any other work intended for use with +%# Request Tracker, to Best Practical Solutions, LLC, you confirm that +%# you are the copyright holder for those contributions and you grant +%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +%# royalty-free, perpetual, license to use, copy, create derivative +%# works based on those contributions, and sublicense and distribute +%# those contributions and any derivatives thereof. +%# +%# END BPS TAGGED BLOCK }}} +<%init> + my $hand = ($session{'CurrentUser'} ||= RT::CurrentUser->new)->LanguageHandle; + $m->print($hand->maketext($m->content,@_)); + return(1); + diff --git a/rt/t/api/report_tickets.t b/rt/t/api/report_tickets.t new file mode 100644 index 000000000..4144c6046 --- /dev/null +++ b/rt/t/api/report_tickets.t @@ -0,0 +1,15 @@ +use strict; +use warnings; +use RT::Test tests => 5; + +use RT::Report::Tickets; + +my $ticket = RT::Test->create_ticket( Queue => 'General', Subject => 'test' ); + +my $tickets = RT::Report::Tickets->new( RT->SystemUser ); +$tickets->FromSQL('Updated <= "tomorrow"'); +is( $tickets->Count, 1, "search with transaction join and positive results" ); + +$tickets->FromSQL('Updated < "yesterday"'); +is( $tickets->Count, 0, "search with transaction join and 0 results" ); + diff --git a/rt/t/mail/dashboard-chart-with-utf8.t b/rt/t/mail/dashboard-chart-with-utf8.t new file mode 100644 index 000000000..6d07b963b --- /dev/null +++ b/rt/t/mail/dashboard-chart-with-utf8.t @@ -0,0 +1,82 @@ +use strict; +use warnings; + +use RT::Test tests => 15; +use utf8; + +my $root = RT::Test->load_or_create_user( Name => 'root' ); + +my ( $baseurl, $m ) = RT::Test->started_ok; +ok( $m->login, 'logged in' ); +my $ticket = RT::Ticket->new( $RT::SystemUser ); +$ticket->Create( + Queue => 'General', + Subject => 'test äöü', +); +ok( $ticket->id, 'created ticket' ); + +$m->get_ok(q{/Search/Chart.html?Query=Subject LIKE 'test äöü'}); +$m->submit_form( + form_name => 'SaveSearch', + fields => { + SavedSearchDescription => 'chart foo', + SavedSearchOwner => 'RT::User-' . $root->id, + }, + button => 'SavedSearchSave', +); + +# first, create and populate a dashboard +$m->get_ok('/Dashboards/Modify.html?Create=1'); +$m->form_name('ModifyDashboard'); +$m->field( 'Name' => 'dashboard foo' ); +$m->click_button( value => 'Create' ); + +$m->follow_link_ok( { text => 'Content' } ); +my $form = $m->form_name('Dashboard-Searches-body'); +my @input = $form->find_input('Searches-body-Available'); +my ($dashboards_component) = + map { ( $_->possible_values )[1] } + grep { ( $_->value_names )[1] =~ /^Chart/ } @input; +$form->value( 'Searches-body-Available' => $dashboards_component ); +$m->click_button( name => 'add' ); +$m->content_contains('Dashboard updated'); + +$m->follow_link_ok( { text => 'Subscription' } ); +$m->form_name('SubscribeDashboard'); +$m->field( 'Frequency' => 'daily' ); +$m->field( 'Hour' => '06:00' ); +$m->click_button( name => 'Save' ); +$m->content_contains('Subscribed to dashboard dashboard foo'); + +my $c = $m->get(q{/Search/Chart?Query=Subject LIKE 'test äöü'}); +my $image = $c->content; +RT::Test->run_and_capture( + command => $RT::SbinPath . '/rt-email-dashboards', all => 1 +); + +my @mails = RT::Test->fetch_caught_mails; +is @mails, 1, "got a dashboard mail"; + +# can't use parse_mail here is because it deletes all attachments +# before we can call bodyhandle :/ +use RT::EmailParser; +my $parser = RT::EmailParser->new; +my $mail = $parser->ParseMIMEEntityFromScalar( $mails[0] ); +like( + $mail->head->get('Subject'), + qr/Daily Dashboard: dashboard foo/, + 'mail subject' +); + +my ($mail_image) = grep { $_->mime_type eq 'image/png' } $mail->parts; +ok( $mail_image, 'mail contains image attachment' ); + +my $handle = $mail_image->bodyhandle; + +my $mail_image_data = ''; +if ( my $io = $handle->open('r') ) { + while ( defined( $_ = $io->getline ) ) { $mail_image_data .= $_ } + $io->close; +} +is( $mail_image_data, $image, 'image in mail is the same one in web' ); + diff --git a/rt/t/mail/rfc2231-attachment.t b/rt/t/mail/rfc2231-attachment.t new file mode 100644 index 000000000..fc74c4720 --- /dev/null +++ b/rt/t/mail/rfc2231-attachment.t @@ -0,0 +1,28 @@ +use strict; +use warnings; + +use utf8; +use RT::Test tests => undef; +my ($baseurl, $m) = RT::Test->started_ok; +ok $m->login, 'logged in as root'; + +diag "encoded attachment filename with parameter continuations"; +{ + my $mail = RT::Test->file_content( + RT::Test::get_relocatable_file( + 'rfc2231-attachment-filename-continuations', + (File::Spec->updir(), 'data', 'emails') + ) + ); + + my ( $status, $id ) = RT::Test->send_via_mailgate($mail); + is( $status >> 8, 0, "The mail gateway exited normally" ); + ok( $id, "Created ticket" ); + + $m->get_ok("/Ticket/Display.html?id=$id"); + $m->content_contains("新しいテキスト ドキュメント.txt", "found full filename"); +} + +undef $m; +done_testing; + diff --git a/rt/t/mail/specials-in-encodedwords.t b/rt/t/mail/specials-in-encodedwords.t new file mode 100644 index 000000000..f9da9c6e9 --- /dev/null +++ b/rt/t/mail/specials-in-encodedwords.t @@ -0,0 +1,40 @@ +use strict; +use warnings; + +use RT::Test tests => undef; + +diag "specials (, and ;) in MIME encoded-words aren't treated as specials"; +{ + # RT decodes too early in the game (i.e. before parsing), so it needs to + # ensure special characters in encoded words are properly escaped/quoted + # after decoding + + RT->Config->Set( ParseNewMessageForTicketCcs => 1 ); + my $mail = <<'.'; +From: root@localhost +Subject: testing mime encoded specials +Cc: a@example.com, =?utf8?q?d=40example.com=2ce=40example.com=3b?= + ; c@example.com +Content-Type: text/plain; charset=utf8 + +here's some content +. + + my ( $status, $id ) = RT::Test->send_via_mailgate($mail); + is( $status >> 8, 0, "The mail gateway exited normally" ); + ok( $id, "Created ticket" ); + + my $ticket = RT::Ticket->new( RT->SystemUser ); + $ticket->Load($id); + ok $ticket->id, 'loaded ticket'; + + my @cc = @{$ticket->Cc->UserMembersObj->ItemsArrayRef}; + is scalar @cc, 3, "three ccs"; + for my $addr (qw(a b c)) { + ok( (scalar grep { $_->EmailAddress eq "$addr\@example.com" } @cc), + "found $addr" ); + } +} + +done_testing; + diff --git a/rt/t/web/command_line_link_to_articles.t b/rt/t/web/command_line_link_to_articles.t new file mode 100644 index 000000000..9a49145fd --- /dev/null +++ b/rt/t/web/command_line_link_to_articles.t @@ -0,0 +1,48 @@ +use strict; +use warnings; +use Test::Expect; +use RT::Test tests => 12, actual_server => 1; + +my $class = RT::Class->new( RT->SystemUser ); +my ( $class_id, $msg ) = $class->Create( Name => 'foo' ); +ok( $class_id, $msg ); + +my $article = RT::Article->new( RT->SystemUser ); +( my $article_id, $msg ) = + $article->Create( Class => 'foo', Summary => 'article summary' ); +ok( $article_id, $msg ); + +my ( $baseurl, $m ) = RT::Test->started_ok; +my $rt_tool_path = "$RT::BinPath/rt"; + +$ENV{'RTUSER'} = 'root'; +$ENV{'RTPASSWD'} = 'password'; +$RT::Logger->debug( + "Connecting to server at " . RT->Config->Get('WebBaseURL') ); +$ENV{'RTSERVER'} = RT->Config->Get('WebBaseURL'); +$ENV{'RTDEBUG'} = '1'; +$ENV{'RTCONFIG'} = '/dev/null'; + +expect_run( + command => "$rt_tool_path shell", + prompt => 'rt> ', + quit => 'quit', +); +expect_send( q{create -t ticket set subject='new ticket'}, + "creating a ticket..." ); + +expect_like( qr/Ticket \d+ created/, "created the ticket" ); +expect_handle->before() =~ /Ticket (\d+) created/; +my $ticket_id = $1; +expect_send( + "link $ticket_id RefersTo a:$article_id", + "link $ticket_id RefersTo a:$article_id" +); +expect_like( qr/Created link $ticket_id RefersTo a:$article_id/, + 'created link' ); +expect_send( "show -s ticket/$ticket_id/links", "show ticket links" ); +expect_like( qr|RefersTo: fsck\.com-article://example\.com/article/$article_id|, + "found new created link" ); + +expect_quit(); + diff --git a/rt/t/web/csrf-rest.t b/rt/t/web/csrf-rest.t new file mode 100644 index 000000000..5bb908165 --- /dev/null +++ b/rt/t/web/csrf-rest.t @@ -0,0 +1,77 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use RT::Test tests => undef; + +my ($baseurl, $m) = RT::Test->started_ok; + +# Get a non-REST session +diag "Standard web session"; +ok $m->login, 'logged in'; +$m->content_contains("RT at a glance", "Get full UI content"); + +# Requesting a REST page should be fine, as we have a Referer +$m->post("$baseurl/REST/1.0/ticket/new", [ + format => 'l', +]); +$m->content_like(qr{^id: ticket/new}m, "REST request with referrer"); + +# Removing the Referer header gets us an interstitial +$m->add_header(Referer => undef); +$m->post("$baseurl/REST/1.0/ticket/new", [ + format => 'l', + foo => 'bar', +]); +$m->content_contains("Possible cross-site request forgery", + "REST request without referrer is blocked"); + +# But passing username and password lets us though +$m->post("$baseurl/REST/1.0/ticket/new", [ + user => 'root', + pass => 'password', + format => 'l', +]); +$m->content_like(qr{^id: ticket/new}m, "REST request without referrer, but username/password supplied, is OK"); + +# And we can still access non-REST urls +$m->get("$baseurl"); +$m->content_contains("RT at a glance", "Full UI is still available"); + + +# Now go get a REST session +diag "REST session"; +$m = RT::Test::Web->new; +$m->post("$baseurl/REST/1.0/ticket/new", [ + user => 'root', + pass => 'password', + format => 'l', +]); +$m->content_like(qr{^id: ticket/new}m, "REST request to log in"); + +# Requesting that page again, with a username/password but no referrer, +# is fine +$m->add_header(Referer => undef); +$m->post("$baseurl/REST/1.0/ticket/new", [ + user => 'root', + pass => 'password', + format => 'l', +]); +$m->content_like(qr{^id: ticket/new}m, "REST request with no referrer, but username/pass"); + +# And it's still fine without both referer and username and password, +# because REST is special-cased +$m->post("$baseurl/REST/1.0/ticket/new", [ + format => 'l', +]); +$m->content_like(qr{^id: ticket/new}m, "REST request with no referrer or username/pass is special-cased for REST sessions"); + +# But the REST page can't request normal pages +$m->get("$baseurl"); +$m->content_lacks("RT at a glance", "Full UI is denied for REST sessions"); +$m->content_contains("This login session belongs to a REST client", "Tells you why"); +$m->warning_like(qr/This login session belongs to a REST client/, "Logs a warning"); + +undef $m; +done_testing; + diff --git a/rt/t/web/csrf.t b/rt/t/web/csrf.t new file mode 100644 index 000000000..d99b4ce22 --- /dev/null +++ b/rt/t/web/csrf.t @@ -0,0 +1,181 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use RT::Test tests => undef; + +my $ticket = RT::Ticket->new(RT::CurrentUser->new('root')); +my ($ok, $msg) = $ticket->Create(Queue => 1, Owner => 'nobody', Subject => 'bad music'); +ok($ok); +my $other = RT::Test->load_or_create_queue(Name => "Other queue", Disabled => 0); +my $other_queue_id = $other->id; + +my ($baseurl, $m) = RT::Test->started_ok; + +my $test_page = "/Ticket/Create.html?Queue=1"; +my $test_path = "/Ticket/Create.html"; + +ok $m->login, 'logged in'; + +# valid referer +$m->add_header(Referer => $baseurl); +$m->get_ok($test_page); +$m->content_lacks("Possible cross-site request forgery"); +$m->title_is('Create a new ticket'); + +# off-site referer BUT provides auth +$m->add_header(Referer => 'http://example.net'); +$m->get_ok("$test_page&user=root&pass=password"); +$m->content_lacks("Possible cross-site request forgery"); +$m->title_is('Create a new ticket'); + +# explicitly no referer BUT provides auth +$m->add_header(Referer => undef); +$m->get_ok("$test_page&user=root&pass=password"); +$m->content_lacks("Possible cross-site request forgery"); +$m->title_is('Create a new ticket'); + +# now send a referer from an attacker +$m->add_header(Referer => 'http://example.net'); +$m->get_ok($test_page); +$m->content_contains("Possible cross-site request forgery"); +$m->content_contains("If you really intended to visit /Ticket/Create.html"); +$m->content_contains("the Referrer header supplied by your browser (example.net:80) is not allowed"); +$m->title_is('Possible cross-site request forgery'); + +# reinstate mech's usual header policy +$m->delete_header('Referer'); + +# clicking the resume request button gets us to the test page +$m->follow_link(text_regex => qr{resume your request}); +$m->content_lacks("Possible cross-site request forgery"); +like($m->response->request->uri, qr{^http://[^/]+\Q$test_path\E\?CSRF_Token=\w+$}); +$m->title_is('Create a new ticket'); + +# try a whitelisted argument from an attacker +$m->add_header(Referer => 'http://example.net'); +$m->get_ok("/Ticket/Display.html?id=1"); +$m->content_lacks("Possible cross-site request forgery"); +$m->title_is('#1: bad music'); + +# now a non-whitelisted argument +$m->get_ok("/Ticket/Display.html?id=1&Action=Take"); +$m->content_contains("Possible cross-site request forgery"); +$m->content_contains("If you really intended to visit /Ticket/Display.html"); +$m->content_contains("the Referrer header supplied by your browser (example.net:80) is not allowed"); +$m->title_is('Possible cross-site request forgery'); + +$m->delete_header('Referer'); +$m->follow_link(text_regex => qr{resume your request}); +$m->content_lacks("Possible cross-site request forgery"); +like($m->response->request->uri, qr{^http://[^/]+\Q/Ticket/Display.html}); +$m->title_is('#1: bad music'); +$m->content_contains('Owner changed from Nobody to root'); + +# force mech to never set referer +$m->add_header(Referer => undef); +$m->get_ok($test_page); +$m->content_contains("Possible cross-site request forgery"); +$m->content_contains("If you really intended to visit /Ticket/Create.html"); +$m->content_contains("your browser did not supply a Referrer header"); +$m->title_is('Possible cross-site request forgery'); + +$m->follow_link(text_regex => qr{resume your request}); +$m->content_lacks("Possible cross-site request forgery"); +is($m->response->redirects, 0, "no redirection"); +like($m->response->request->uri, qr{^http://[^/]+\Q$test_path\E\?CSRF_Token=\w+$}); +$m->title_is('Create a new ticket'); + +# try sending the wrong csrf token, then the right one +$m->add_header(Referer => undef); +$m->get_ok($test_page); +$m->content_contains("Possible cross-site request forgery"); +$m->content_contains("If you really intended to visit /Ticket/Create.html"); +$m->content_contains("your browser did not supply a Referrer header"); +$m->title_is('Possible cross-site request forgery'); + +# Sending a wrong CSRF is just a normal request. We'll make a request +# with just an invalid token, which means no Queue=, which means +# Create.html errors out. +my $link = $m->find_link(text_regex => qr{resume your request}); +(my $broken_url = $link->url) =~ s/(CSRF_Token)=\w+/$1=crud/; +$m->get_ok($broken_url); +$m->content_contains("Queue could not be loaded"); +$m->title_is('RT Error'); +$m->warning_like(qr/Queue could not be loaded/); + +# The token doesn't work for other pages, or other arguments to the same page. +$m->add_header(Referer => undef); +$m->get_ok($test_page); +$m->content_contains("Possible cross-site request forgery"); +my ($token) = $m->content =~ m{CSRF_Token=(\w+)}; + +$m->add_header(Referer => undef); +$m->get_ok("/Admin/Queues/Modify.html?id=new&Name=test&CSRF_Token=$token"); +$m->content_contains("Possible cross-site request forgery"); +$m->content_contains("If you really intended to visit /Admin/Queues/Modify.html"); +$m->content_contains("your browser did not supply a Referrer header"); +$m->title_is('Possible cross-site request forgery'); + +$m->follow_link(text_regex => qr{resume your request}); +$m->content_lacks("Possible cross-site request forgery"); +$m->title_is('Configuration for queue test'); + +# Try the same page, but different query parameters, which are blatted by the token +$m->get_ok("/Ticket/Create.html?Queue=$other_queue_id&CSRF_Token=$token"); +$m->content_lacks("Possible cross-site request forgery"); +$m->title_is('Create a new ticket'); +$m->text_unlike(qr/Queue:\s*Other queue/); +$m->text_like(qr/Queue:\s*General/); + +# Ensure that file uploads work across the interstitial +$m->delete_header('Referer'); +$m->get_ok($test_page); +$m->content_contains("Create a new ticket", 'ticket create page'); +$m->form_name('TicketCreate'); +$m->field('Subject', 'Attachments test'); + +my $logofile = "$RT::MasonComponentRoot/NoAuth/images/bpslogo.png"; +open LOGO, "<", $logofile or die "Can't open logo file: $!"; +binmode LOGO; +my $logo_contents = do {local $/; }; +close LOGO; +$m->field('Attach', $logofile); + +# Lose the referer before the POST +$m->add_header(Referer => undef); +$m->submit; +$m->content_contains("Possible cross-site request forgery"); +$m->content_contains("If you really intended to visit /Ticket/Create.html"); +$m->follow_link(text_regex => qr{resume your request}); +$m->content_contains('Download bpslogo.png', 'page has file name'); +$m->follow_link_ok({text => "Download bpslogo.png"}); +is($m->content, $logo_contents, "Binary content matches"); + + +# now try self-service with CSRF +my $user = RT::User->new(RT->SystemUser); +$user->Create(Name => "SelfService", Password => "chops", Privileged => 0); + +$m = RT::Test::Web->new; +$m->get_ok("$baseurl/index.html?user=SelfService&pass=chops"); +$m->title_is("Open tickets", "got self-service interface"); +$m->content_contains("My open tickets", "got self-service interface"); + +# post without referer +$m->add_header(Referer => undef); +$m->get_ok("/SelfService/Create.html?Queue=1"); +$m->content_contains("Possible cross-site request forgery"); +$m->content_contains("If you really intended to visit /SelfService/Create.html"); +$m->content_contains("your browser did not supply a Referrer header"); +$m->title_is('Possible cross-site request forgery'); + +$m->follow_link(text_regex => qr{resume your request}); +$m->content_lacks("Possible cross-site request forgery"); +is($m->response->redirects, 0, "no redirection"); +like($m->response->request->uri, qr{^http://[^/]+\Q/SelfService/Create.html\E\?CSRF_Token=\w+$}); +$m->title_is('Create a ticket'); +$m->content_contains('Describe the issue below:'); + +undef $m; +done_testing; diff --git a/rt/t/web/installer.t b/rt/t/web/installer.t new file mode 100644 index 000000000..4dc82df47 --- /dev/null +++ b/rt/t/web/installer.t @@ -0,0 +1,95 @@ +#!/usr/bin/perl +use strict; +use warnings; + +$ENV{RT_TEST_WEB_HANDLER} = 'plack+rt-server'; +use RT::Test + tests => undef, + nodb => 1, + server_ok => 1; + +my ($base, $m) = RT::Test->started_ok; + +$m->warning_like(qr/If this is a new installation of RT/, + "Got startup warning"); + +$m->get_ok($base); +like $m->uri, qr/Install/, 'at installer'; + +diag "Testing language change"; +{ + $m->submit_form_ok( + { + with_fields => { + Lang => 'fr', + }, + button => 'ChangeLang', + }, + 'change language to french' + ); + $m->content_like(qr/RT\s+pour\s+example\.com/i); + $m->submit_form_ok( + { + with_fields => { + Lang => 'en', + }, + button => 'ChangeLang', + }, + 'change language to english' + ); + $m->content_like(qr/RT\s+for\s+example\.com/i); +} + +diag "Walking through install screens setting defaults"; +{ + $m->click_ok('Run'); + + # Database type + $m->content_contains('DatabaseType'); + $m->content_contains($_, "found database $_") + for qw(MySQL PostgreSQL Oracle SQLite); + $m->submit(); + + # Database details + $m->content_contains('DatabaseName'); + $m->submit(); + $m->content_contains('Connection succeeded'); + $m->submit_form_ok({ button => 'Next' }); + + # Basic options + $m->submit_form_ok({ + with_fields => { + Password => 'password', + } + }, 'set root password'); + + # Mail options + $m->submit_form_ok({ + with_fields => { + OwnerEmail => 'admin@example.com', + }, + }, 'set admin email'); + + # Mail addresses + $m->submit_form_ok({ + with_fields => { + CorrespondAddress => 'rt@example.com', + CommentAddress => 'rt-comment@example.com', + }, + }, 'set addresses'); + + # Initialize database + $m->content_contains('database'); + $m->submit(); + + # Finish + $m->content_contains('/RT_SiteConfig.pm'); + $m->content_contains('Finish'); + $m->submit(); + + $m->content_contains('Login'); + ok $m->login(), 'logged in'; +} + +undef $m; +done_testing; diff --git a/rt/t/web/owner_disabled_group_19221.t b/rt/t/web/owner_disabled_group_19221.t new file mode 100644 index 000000000..2664c5bc2 --- /dev/null +++ b/rt/t/web/owner_disabled_group_19221.t @@ -0,0 +1,190 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use RT::Test tests => undef; + +my $queue = RT::Test->load_or_create_queue( Name => 'Test' ); +ok $queue && $queue->id, 'loaded or created queue'; + +my $user = RT::Test->load_or_create_user( + Name => 'ausername', + Privileged => 1, +); +ok $user && $user->id, 'loaded or created user'; + +my $group = RT::Group->new(RT->SystemUser); +my ($ok, $msg) = $group->CreateUserDefinedGroup(Name => 'Disabled Group'); +ok($ok, $msg); + +($ok, $msg) = $group->AddMember( $user->PrincipalId ); +ok($ok, $msg); + +ok( RT::Test->set_rights({ + Principal => $group, + Object => $queue, + Right => [qw(OwnTicket)] +}), 'set rights'); + +RT->Config->Set( AutocompleteOwners => 0 ); +my ($base, $m) = RT::Test->started_ok; +ok $m->login, 'logged in'; + +diag "user from group shows up in create form"; +{ + $m->get_ok('/', 'open home page'); + $m->form_name('CreateTicketInQueue'); + $m->select( 'Queue', $queue->id ); + $m->submit; + + $m->content_contains('Create a new ticket', 'opened create ticket page'); + my $form = $m->form_name('TicketCreate'); + my $input = $form->find_input('Owner'); + is $input->value, RT->Nobody->Id, 'correct owner selected'; + ok((scalar grep { $_ == $user->Id } $input->possible_values), 'user from group is in dropdown'); +} + +diag "user from disabled group DOESN'T shows up in create form"; +{ + ($ok, $msg) = $group->SetDisabled(1); + ok($ok, $msg); + + $m->get_ok('/', 'open home page'); + $m->form_name('CreateTicketInQueue'); + $m->select( 'Queue', $queue->id ); + $m->submit; + + $m->content_contains('Create a new ticket', 'opened create ticket page'); + my $form = $m->form_name('TicketCreate'); + my $input = $form->find_input('Owner'); + is $input->value, RT->Nobody->Id, 'correct owner selected'; + ok((not scalar grep { $_ == $user->Id } $input->possible_values), 'user from disabled group is NOT in dropdown'); + ($ok, $msg) = $group->SetDisabled(0); + ok($ok, $msg); +} + + + +diag "Put us in a nested group"; +my $super = RT::Group->new(RT->SystemUser); +($ok, $msg) = $super->CreateUserDefinedGroup(Name => 'Supergroup'); +ok($ok, $msg); + +($ok, $msg) = $super->AddMember( $group->PrincipalId ); +ok($ok, $msg); + +ok( RT::Test->set_rights({ + Principal => $super, + Object => $queue, + Right => [qw(OwnTicket)] +}), 'set rights'); + + +diag "Disable the middle group"; +{ + ($ok, $msg) = $group->SetDisabled(1); + ok($ok, "Disabled group: $msg"); + + $m->get_ok('/', 'open home page'); + $m->form_name('CreateTicketInQueue'); + $m->select( 'Queue', $queue->id ); + $m->submit; + + $m->content_contains('Create a new ticket', 'opened create ticket page'); + my $form = $m->form_name('TicketCreate'); + my $input = $form->find_input('Owner'); + is $input->value, RT->Nobody->Id, 'correct owner selected'; + ok((not scalar grep { $_ == $user->Id } $input->possible_values), 'user from disabled group is NOT in dropdown'); + ($ok, $msg) = $group->SetDisabled(0); + ok($ok, "Re-enabled group: $msg"); +} + +diag "Disable the top group"; +{ + ($ok, $msg) = $super->SetDisabled(1); + ok($ok, "Disabled supergroup: $msg"); + + $m->get_ok('/', 'open home page'); + $m->form_name('CreateTicketInQueue'); + $m->select( 'Queue', $queue->id ); + $m->submit; + + $m->content_contains('Create a new ticket', 'opened create ticket page'); + my $form = $m->form_name('TicketCreate'); + my $input = $form->find_input('Owner'); + is $input->value, RT->Nobody->Id, 'correct owner selected'; + ok((not scalar grep { $_ == $user->Id } $input->possible_values), 'user from disabled group is NOT in dropdown'); + ($ok, $msg) = $super->SetDisabled(0); + ok($ok, "Re-enabled supergroup: $msg"); +} + + +diag "Check WithMember and WithoutMember recursively"; +{ + my $with = RT::Groups->new( RT->SystemUser ); + $with->WithMember( PrincipalId => $user->PrincipalObj->Id, Recursively => 1 ); + $with->Limit( FIELD => 'domain', OPERATOR => '=', VALUE => 'UserDefined' ); + is_deeply( + [map {$_->Name} @{$with->ItemsArrayRef}], + ['Disabled Group','Supergroup'], + "Get expected recursive memberships", + ); + + my $without = RT::Groups->new( RT->SystemUser ); + $without->WithoutMember( PrincipalId => $user->PrincipalObj->Id, Recursively => 1 ); + $without->Limit( FIELD => 'domain', OPERATOR => '=', VALUE => 'UserDefined' ); + is_deeply( + [map {$_->Name} @{$without->ItemsArrayRef}], + [], + "And not a member of no groups", + ); + + ($ok, $msg) = $super->SetDisabled(1); + ok($ok, "Disabled supergroup: $msg"); + $with->RedoSearch; + $without->RedoSearch; + is_deeply( + [map {$_->Name} @{$with->ItemsArrayRef}], + ['Disabled Group'], + "Recursive check only contains subgroup", + ); + is_deeply( + [map {$_->Name} @{$without->ItemsArrayRef}], + [], + "Doesn't find the currently disabled group", + ); + ($ok, $msg) = $super->SetDisabled(0); + ok($ok, "Re-enabled supergroup: $msg"); + + ($ok, $msg) = $group->SetDisabled(1); + ok($ok, "Disabled intermediate group: $msg"); + $with->RedoSearch; + $without->RedoSearch; + is_deeply( + [map {$_->Name} @{$with->ItemsArrayRef}], + [], + "Recursive check finds no groups", + ); + is_deeply( + [map {$_->Name} @{$without->ItemsArrayRef}], + ['Supergroup'], + "Now not a member of the supergroup", + ); + ($ok, $msg) = $group->SetDisabled(0); + ok($ok, "Re-enabled intermediate group: $msg"); +} + +diag "Check MemberOfGroup"; +{ + ($ok, $msg) = $group->SetDisabled(1); + ok($ok, "Disabled intermediate group: $msg"); + my $users = RT::Users->new(RT->SystemUser); + $users->MemberOfGroup($super->PrincipalObj->id); + is($users->Count, 0, "Supergroup claims no members"); + ($ok, $msg) = $group->SetDisabled(0); + ok($ok, "Re-enabled intermediate group: $msg"); +} + + +undef $m; +done_testing; diff --git a/rt/t/web/query_builder_queue_limits.t b/rt/t/web/query_builder_queue_limits.t new file mode 100644 index 000000000..a3b976524 --- /dev/null +++ b/rt/t/web/query_builder_queue_limits.t @@ -0,0 +1,180 @@ +use strict; +use warnings; + +use RT::Test tests => 34; + +my $lifecycles = RT->Config->Get('Lifecycles'); +$lifecycles->{foo} = { + initial => ['initial'], + active => ['open'], + inactive => ['resolved'], + +}; + +RT::Lifecycle->FillCache(); + +my $general = RT::Test->load_or_create_queue( Name => 'General' ); +my $foo = RT::Test->load_or_create_queue( Name => 'foo', Lifecycle => 'foo' ); + +my $global_cf = RT::Test->load_or_create_custom_field( + Name => 'global_cf', + Queue => 0, + Type => 'FreeformSingle', +); + +my $general_cf = RT::Test->load_or_create_custom_field( + Name => 'general_cf', + Queue => 'General', + Type => 'FreeformSingle', +); + +my $foo_cf = RT::Test->load_or_create_custom_field( + Name => 'foo_cf', + Queue => 'foo', + Type => 'FreeformSingle' +); + +my $root = RT::Test->load_or_create_user( Name => 'root', ); +my $user_a = RT::Test->load_or_create_user( + Name => 'user_a', + Password => 'password', +); +my $user_b = RT::Test->load_or_create_user( + Name => 'user_b', + Password => 'password', +); + +ok( + RT::Test->set_rights( + { + Principal => $user_a, + Object => $general, + Right => ['OwnTicket'], + }, + { + Principal => $user_b, + Object => $foo, + Right => ['OwnTicket'], + }, + ), + 'granted OwnTicket right for user_a and user_b' +); + +my ( $url, $m ) = RT::Test->started_ok; +ok( $m->login, 'logged in' ); + +$m->get_ok( $url . '/Search/Build.html' ); + +diag "check default statuses, cf and owners"; +my $form = $m->form_name('BuildQuery'); +ok( $form, 'found BuildQuery form' ); +ok( $form->find_input("ValueOf'CF.{global_cf}'"), 'found global_cf by default' ); +ok( !$form->find_input("ValueOf'CF.{general_cf}'"), 'no general_cf by default' ); +ok( !$form->find_input("ValueOf'CF.{foo_cf}'"), 'no foo_cf by default' ); + +my $status_input = $form->find_input('ValueOfStatus'); +my @statuses = sort $status_input->possible_values; +is_deeply( + \@statuses, [ '', qw/initial new open rejected resolved stalled/], 'found all statuses' +); + +my $owner_input = $form->find_input('ValueOfActor'); +my @owners = sort $owner_input->possible_values; +is_deeply( + \@owners, [ '', qw/Nobody root user_a user_b/], 'found all users' +); + +diag "limit queue to foo"; +$m->submit_form( + fields => { ValueOfQueue => 'foo' }, + button => 'AddClause', +); + +$form = $m->form_name('BuildQuery'); +ok( $form->find_input("ValueOf'CF.{foo_cf}'"), 'found foo_cf' ); +ok( $form->find_input("ValueOf'CF.{global_cf}'"), 'found global_cf' ); +ok( !$form->find_input("ValueOf'CF.{general_cf}'"), 'still no general_cf' ); +$status_input = $form->find_input('ValueOfStatus'); +@statuses = sort $status_input->possible_values; +is_deeply( + \@statuses, + [ '', qw/initial open resolved/ ], + 'found statuses from foo only' +); + +$owner_input = $form->find_input('ValueOfActor'); +@owners = sort $owner_input->possible_values; +is_deeply( + \@owners, [ '', qw/Nobody root user_b/], 'no user_a' +); + +diag "limit queue to general too"; + +$m->submit_form( + fields => { ValueOfQueue => 'General' }, + button => 'AddClause', +); + +$form = $m->form_name('BuildQuery'); +ok( $form->find_input("ValueOf'CF.{general_cf}'"), 'found general_cf' ); +ok( $form->find_input("ValueOf'CF.{foo_cf}'"), 'found foo_cf' ); +ok( $form->find_input("ValueOf'CF.{global_cf}'"), 'found global_cf' ); +$status_input = $form->find_input('ValueOfStatus'); +@statuses = sort $status_input->possible_values; +is_deeply( + \@statuses, + [ '', qw/initial new open rejected resolved stalled/ ], + 'found all statuses again' +); +$owner_input = $form->find_input('ValueOfActor'); +@owners = sort $owner_input->possible_values; +is_deeply( + \@owners, [ '', qw/Nobody root user_a user_b/], 'found all users again' +); + +diag "limit queue to != foo"; +$m->get_ok( $url . '/Search/Build.html?NewQuery=1' ); +$m->submit_form( + form_name => 'BuildQuery', + fields => { ValueOfQueue => 'foo', QueueOp => '!=' }, + button => 'AddClause', +); + +$form = $m->form_name('BuildQuery'); +ok( $form->find_input("ValueOf'CF.{global_cf}'"), 'found global_cf' ); +ok( !$form->find_input("ValueOf'CF.{foo_cf}'"), 'no foo_cf' ); +ok( !$form->find_input("ValueOf'CF.{general_cf}'"), 'no general_cf' ); +$status_input = $form->find_input('ValueOfStatus'); +@statuses = sort $status_input->possible_values; +is_deeply( + \@statuses, [ '', qw/initial new open rejected resolved stalled/], + 'found all statuses' +); +$owner_input = $form->find_input('ValueOfActor'); +@owners = sort $owner_input->possible_values; +is_deeply( + \@owners, [ '', qw/Nobody root user_a user_b/], 'found all users' +); + +diag "limit queue to General OR foo"; +$m->get_ok( $url . '/Search/Edit.html' ); +$m->submit_form( + form_name => 'BuildQueryAdvanced', + fields => { Query => q{Queue = 'General' OR Queue = 'foo'} }, +); +$form = $m->form_name('BuildQuery'); +ok( $form->find_input("ValueOf'CF.{general_cf}'"), 'found general_cf' ); +ok( $form->find_input("ValueOf'CF.{foo_cf}'"), 'found foo_cf' ); +ok( $form->find_input("ValueOf'CF.{global_cf}'"), 'found global_cf' ); +$status_input = $form->find_input('ValueOfStatus'); +@statuses = sort $status_input->possible_values; +is_deeply( + \@statuses, + [ '', qw/initial new open rejected resolved stalled/ ], + 'found all statuses' +); +$owner_input = $form->find_input('ValueOfActor'); +@owners = sort $owner_input->possible_values; +is_deeply( + \@owners, [ '', qw/Nobody root user_a user_b/], 'found all users' +); diff --git a/rt/t/web/rest_cfs_with_same_name.t b/rt/t/web/rest_cfs_with_same_name.t new file mode 100644 index 000000000..958f67177 --- /dev/null +++ b/rt/t/web/rest_cfs_with_same_name.t @@ -0,0 +1,88 @@ +use strict; +use warnings; +use RT::Interface::REST; + +use RT::Test tests => 25; + +my ( $baseurl, $m ) = RT::Test->started_ok; +for my $queue_name (qw/foo bar/) { + + my $queue = RT::Test->load_or_create_queue( Name => $queue_name ); + ok( $queue, "created queue $queue_name" ); + my $cf = RT::Test->load_or_create_custom_field( + Name => 'test', + Type => 'Freeform', + Queue => $queue_name, + ); + ok( $cf->id, "created cf test for queue $queue_name " . $cf->id ); + + $m->post( + "$baseurl/REST/1.0/ticket/new", + [ + user => 'root', + pass => 'password', + format => 'l', + ] + ); + + my $text = $m->content; + my @lines = $text =~ m{.*}g; + shift @lines; # header + + # cfs aren't in the default ticket form + push @lines, "CF.{test}: baz"; + + $text = join "\n", @lines; + + ok( $text =~ s/Subject:\s*$/Subject: test cf/m, + "successfully replaced subject" ); + ok( $text =~ s/Queue: General\s*$/Queue: $queue_name/m, + "successfully replaced Queue" ); + + $m->post( + "$baseurl/REST/1.0/ticket/edit", + [ + user => 'root', + pass => 'password', + content => $text, + ], + Content_Type => 'form-data' + ); + + my ($id) = $m->content =~ /Ticket (\d+) created/; + ok( $id, "got ticket #$id" ); + + my $ticket = RT::Ticket->new( RT->SystemUser ); + $ticket->Load($id); + is( $ticket->id, $id, "loaded the REST-created ticket" ); + is( $ticket->Subject, "test cf", "subject successfully set" ); + is( $ticket->Queue, $queue->id, "queue successfully set" ); + is( $ticket->FirstCustomFieldValue("test"), "baz", "cf successfully set" ); + + $m->post( + "$baseurl/REST/1.0/ticket/show", + [ + user => 'root', + pass => 'password', + format => 'l', + id => "ticket/$id", + ] + ); + $text = $m->content; + like( $text, qr/^CF\.{test}: baz\s*$/m, 'cf value in rest show' ); + + $text =~ s{.*}{}; # remove header + $text =~ s!CF\.{test}: baz!CF.{test}: newbaz!; + $m->post( + "$baseurl/REST/1.0/ticket/edit", + [ + user => 'root', + pass => 'password', + content => $text, + ], + Content_Type => 'form-data' + ); + $m->content =~ /Ticket ($id) updated/; + is( $ticket->FirstCustomFieldValue("test"), "newbaz", "cf successfully updated" ); +} + -- 2.11.0