diff options
Diffstat (limited to 'perllib')
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Auth/Social.pm | 34 | ||||
-rw-r--r-- | perllib/FixMyStreet/Cobrand/Default.pm | 3 | ||||
-rw-r--r-- | perllib/FixMyStreet/Cobrand/Hackney.pm | 187 | ||||
-rw-r--r-- | perllib/FixMyStreet/Cobrand/UKCouncils.pm | 2 | ||||
-rw-r--r-- | perllib/FixMyStreet/Cobrand/Zurich.pm | 2 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm | 2 | ||||
-rw-r--r-- | perllib/FixMyStreet/Email.pm | 4 | ||||
-rw-r--r-- | perllib/FixMyStreet/Geocode/OSM.pm | 1 | ||||
-rw-r--r-- | perllib/FixMyStreet/Queue/Item/Report.pm | 2 | ||||
-rw-r--r-- | perllib/FixMyStreet/Script/Alerts.pm | 2 | ||||
-rw-r--r-- | perllib/FixMyStreet/SendReport/Email.pm | 5 | ||||
-rw-r--r-- | perllib/OIDC/Lite/Client/WebServer/AuthCodeFlow.pm (renamed from perllib/OIDC/Lite/Client/WebServer/Azure.pm) | 13 | ||||
-rw-r--r-- | perllib/Open311/PopulateServiceList.pm | 8 | ||||
-rwxr-xr-x | perllib/Open311/PostServiceRequestUpdates.pm | 2 |
14 files changed, 244 insertions, 23 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Auth/Social.pm b/perllib/FixMyStreet/App/Controller/Auth/Social.pm index 06e67573f..ce94fe256 100644 --- a/perllib/FixMyStreet/App/Controller/Auth/Social.pm +++ b/perllib/FixMyStreet/App/Controller/Auth/Social.pm @@ -6,7 +6,7 @@ BEGIN { extends 'Catalyst::Controller'; } use Net::Facebook::Oauth2; use Net::Twitter::Lite::WithAPIv1_1; -use OIDC::Lite::Client::WebServer::Azure; +use OIDC::Lite::Client::WebServer::AuthCodeFlow; use URI::Escape; use mySociety::AuthToken; @@ -167,7 +167,7 @@ sub oidc : Private { my $config = $c->cobrand->feature('oidc_login'); - OIDC::Lite::Client::WebServer::Azure->new( + OIDC::Lite::Client::WebServer::AuthCodeFlow->new( id => $config->{client_id}, secret => $config->{secret}, authorize_uri => $config->{auth_uri}, @@ -179,7 +179,9 @@ sub oidc_sign_in : Private { my ( $self, $c ) = @_; $c->detach( '/page_error_403_access_denied', [] ) if FixMyStreet->config('SIGNUPS_DISABLED'); - $c->detach( '/page_error_400_bad_request', [] ) unless $c->cobrand->feature('oidc_login'); + + my $cfg = $c->cobrand->feature('oidc_login'); + $c->detach( '/page_error_400_bad_request', [] ) unless $cfg; my $oidc = $c->forward('oidc'); my $nonce = $self->generate_nonce(); @@ -190,6 +192,15 @@ sub oidc_sign_in : Private { extra => { response_mode => 'form_post', nonce => $nonce, + # auth_extra_params provides a way to pass custom parameters + # to the OIDC endpoint for the intial authentication request. + # This allows, for example, a custom scope to be used, + # or the `hd` parameter which customises the appearance of + # the login form. + # This is primarily useful for Google G Suite authentication - see + # available parameters here: + # https://developers.google.com/identity/protocols/oauth2/openid-connect#authenticationuriparameters + %{ $cfg->{auth_extra_params} || {} } , }, ); @@ -201,14 +212,14 @@ sub oidc_sign_in : Private { # The OIDC endpoint may require a specific URI to be called to log the user # out when they log out of FMS. - if ( my $redirect_uri = $c->cobrand->feature('oidc_login')->{logout_uri} ) { + if ( my $redirect_uri = $cfg->{logout_uri} ) { $redirect_uri .= "?post_logout_redirect_uri="; $redirect_uri .= URI::Escape::uri_escape( $c->uri_for('/auth/sign_out') ); $oauth{logout_redirect_uri} = $redirect_uri; } # The OIDC endpoint may provide a specific URI for changing the user's password. - if ( my $password_change_uri = $c->cobrand->feature('oidc_login')->{password_change_uri} ) { + if ( my $password_change_uri = $cfg->{password_change_uri} ) { $oauth{change_password_uri} = $oidc->uri_to_redirect( uri => $password_change_uri, redirect_uri => $c->uri_for('/auth/OIDC'), @@ -279,6 +290,7 @@ sub oidc_callback: Path('/auth/OIDC') : Args(0) { eval { $id_token = $oidc->get_access_token( code => $c->get_param('code'), + redirect_uri => $c->uri_for('/auth/OIDC') ); }; if ($@) { @@ -294,10 +306,18 @@ sub oidc_callback: Path('/auth/OIDC') : Args(0) { # check that the nonce matches what we set in the user session $c->detach('/page_error_500_internal_error', ['invalid id_token']) unless $id_token->payload->{nonce} eq $c->session->{oauth}{nonce}; + if (my $domains = $c->cobrand->feature('oidc_login')->{allowed_domains}) { + # Check that the hd payload is present in the token and matches the + # list of allowed domains from the config + my $hd = $id_token->payload->{hd}; + my %allowed_domains = map { $_ => 1} @$domains; + $c->detach('oauth_failure') unless $allowed_domains{$hd}; + } + # Some claims need parsing into a friendlier format - # XXX check how much of this is Westminster/Azure-specific - my $name = join(" ", $id_token->payload->{given_name}, $id_token->payload->{family_name}); + my $name = $id_token->payload->{name} || join(" ", $id_token->payload->{given_name}, $id_token->payload->{family_name}); my $email = $id_token->payload->{email}; + # WCC Azure provides a single email address as an array for some reason my $emails = $id_token->payload->{emails}; if ($emails && @$emails) { diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm index 73340338b..bd8b7e4b4 100644 --- a/perllib/FixMyStreet/Cobrand/Default.pm +++ b/perllib/FixMyStreet/Cobrand/Default.pm @@ -957,9 +957,10 @@ Get stats to display on the council reports page sub get_report_stats { return 0; } sub get_body_sender { - my ( $self, $body, $category ) = @_; + my ( $self, $body, $problem ) = @_; # look up via category + my $category = $problem->category; my $contact = $body->contacts->search( { category => $category } )->first; if ( $body->can_be_devolved && $contact && $contact->send_method ) { return { method => $contact->send_method, config => $contact, contact => $contact }; diff --git a/perllib/FixMyStreet/Cobrand/Hackney.pm b/perllib/FixMyStreet/Cobrand/Hackney.pm new file mode 100644 index 000000000..234573e3e --- /dev/null +++ b/perllib/FixMyStreet/Cobrand/Hackney.pm @@ -0,0 +1,187 @@ +package FixMyStreet::Cobrand::Hackney; +use parent 'FixMyStreet::Cobrand::Whitelabel'; + +use strict; +use warnings; +use mySociety::EmailUtil qw(is_valid_email); + +sub council_area_id { return 2508; } +sub council_area { return 'Hackney'; } +sub council_name { return 'Hackney Council'; } +sub council_url { return 'hackney'; } +sub send_questionnaires { 0 } + +sub disambiguate_location { + my $self = shift; + my $string = shift; + + my $town = 'Hackney'; + + # Teale Street is on the boundary with Tower Hamlets and + # shows the 'please use fixmystreet.com' message, but Hackney + # do provide services on that road. + ($string, $town) = ('E2 9AA', '') if $string =~ /^teale\s+st/i; + + return { + %{ $self->SUPER::disambiguate_location() }, + string => $string, + town => $town, + centre => '51.552267,-0.063316', + bounds => [ 51.519814, -0.104511, 51.577784, -0.016527 ], + }; +} + +sub do_not_reply_email { shift->feature('do_not_reply_email') } + +sub verp_email_domain { shift->feature('verp_email_domain') } + +sub get_geocoder { + return 'OSM'; # default of Bing gives poor results, let's try overriding. +} + +sub geocoder_munge_query_params { + my ($self, $params) = @_; + + $params->{addressdetails} = 1; +} + +sub geocoder_munge_results { + my ($self, $result) = @_; + if (my $a = $result->{address}) { + if ($a->{road} && $a->{suburb} && $a->{postcode}) { + $result->{display_name} = "$a->{road}, $a->{suburb}, $a->{postcode}"; + return; + } + } + $result->{display_name} = '' unless $result->{display_name} =~ /Hackney/; + $result->{display_name} =~ s/, United Kingdom$//; + $result->{display_name} =~ s/, London, Greater London, England//; + $result->{display_name} =~ s/, London Borough of Hackney//; +} + + +sub open311_config { + my ($self, $row, $h, $params) = @_; + + $params->{multi_photos} = 1; +} + +sub open311_extra_data { + my ($self, $row, $h, $extra, $contact) = @_; + + my $open311_only = [ + { name => 'report_url', + value => $h->{url} }, + { name => 'title', + value => $row->title }, + { name => 'description', + value => $row->detail }, + { name => 'category', + value => $row->category }, + ]; + + # Make sure contact 'email' set correctly for Open311 + if (my $sent_to = $row->get_extra_metadata('sent_to')) { + $row->unset_extra_metadata('sent_to'); + my $code = $sent_to->{$contact->email}; + $contact->email($code) if $code; + } + + return $open311_only; +} + +sub map_type { 'OSM' } + +sub default_map_zoom { 5 } + +sub admin_user_domain { 'hackney.gov.uk' } + +sub social_auth_enabled { + my $self = shift; + + return $self->feature('oidc_login') ? 1 : 0; +} + +sub anonymous_account { + my $self = shift; + return { + email => $self->feature('anonymous_account') . '@' . $self->admin_user_domain, + name => 'Anonymous user', + }; +} + +sub open311_skip_existing_contact { + my ($self, $contact) = @_; + + # For Hackney we want the 'protected' flag to prevent any changes to this + # contact at all. + return $contact->get_extra_metadata("open311_protect") ? 1 : 0; +} + +sub open311_filter_contacts_for_deletion { + my ($self, $contacts) = @_; + + # Don't delete open311 protected contacts when importing + return $contacts->search({ + extra => { -not_like => '%T15:open311_protect,I1:1%' }, + }); +} + +sub problem_is_within_area_type { + my ($self, $problem, $type) = @_; + my $layer_map = { + park => "greenspaces:hackney_park", + estate => "housing:lbh_estate", + }; + my $layer = $layer_map->{$type}; + return unless $layer; + + my ($x, $y) = $problem->local_coords; + + my $cfg = { + url => "https://map.hackney.gov.uk/geoserver/wfs", + srsname => "urn:ogc:def:crs:EPSG::27700", + typename => $layer, + outputformat => "json", + filter => "<Filter xmlns:gml=\"http://www.opengis.net/gml\"><Intersects><PropertyName>geom</PropertyName><gml:Point srsName=\"27700\"><gml:coordinates>$x,$y</gml:coordinates></gml:Point></Intersects></Filter>", + }; + + my $features = $self->_fetch_features($cfg, $x, $y) || []; + return scalar @$features ? 1 : 0; +} + +sub get_body_sender { + my ( $self, $body, $problem ) = @_; + + my $contact = $body->contacts->search( { category => $problem->category } )->first; + + my $parts = join '\s*', qw(^ park : (.*?) ; estate : (.*?) ; other : (.*?) $); + my $regex = qr/$parts/i; + if (my ($park, $estate, $other) = $contact->email =~ $regex) { + my $to = $other; + if ($self->problem_is_within_area_type($problem, 'park')) { + $to = $park; + } elsif ($self->problem_is_within_area_type($problem, 'estate')) { + $to = $estate; + } + $problem->set_extra_metadata(sent_to => { $contact->email => $to }); + if (is_valid_email($to)) { + return { method => 'Email', contact => $contact }; + } + } + return $self->SUPER::get_body_sender($body, $problem); +} + +# Translate email address to actual delivery address +sub munge_sendreport_params { + my ($self, $row, $h, $params) = @_; + + my $sent_to = $row->get_extra_metadata('sent_to') or return; + $row->unset_extra_metadata('sent_to'); + for my $recip (@{$params->{To}}) { + my ($email, $name) = @$recip; + $recip->[0] = $sent_to->{$email} if $sent_to->{$email}; + } +} + +1; diff --git a/perllib/FixMyStreet/Cobrand/UKCouncils.pm b/perllib/FixMyStreet/Cobrand/UKCouncils.pm index 21dd2d455..7456d9ddf 100644 --- a/perllib/FixMyStreet/Cobrand/UKCouncils.pm +++ b/perllib/FixMyStreet/Cobrand/UKCouncils.pm @@ -392,7 +392,7 @@ sub _fetch_features_url { SRSNAME => $cfg->{srsname}, TYPENAME => $cfg->{typename}, VERSION => "1.1.0", - outputformat => "geojson", + outputformat => $cfg->{outputformat} || "geojson", $cfg->{filter} ? ( Filter => $cfg->{filter} ) : ( BBOX => $cfg->{bbox} ), ); diff --git a/perllib/FixMyStreet/Cobrand/Zurich.pm b/perllib/FixMyStreet/Cobrand/Zurich.pm index 3cf678f9c..42932ec41 100644 --- a/perllib/FixMyStreet/Cobrand/Zurich.pm +++ b/perllib/FixMyStreet/Cobrand/Zurich.pm @@ -217,7 +217,7 @@ sub allow_photo_display { } sub get_body_sender { - my ( $self, $body, $category ) = @_; + my ( $self, $body, $problem ) = @_; return { method => 'Zurich' }; } diff --git a/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm b/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm index 1805e1fd2..6d14e6a9f 100644 --- a/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm +++ b/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm @@ -163,7 +163,7 @@ sub compare_extra { my $new = $other->get_extra_metadata; my $both = { %$old, %$new }; - my @all_keys = sort keys %$both; + my @all_keys = grep { $_ ne 'sent_to' } sort keys %$both; my @s; foreach (@all_keys) { if ($old->{$_} && $new->{$_}) { diff --git a/perllib/FixMyStreet/Email.pm b/perllib/FixMyStreet/Email.pm index 3d7b48539..0cc6a880c 100644 --- a/perllib/FixMyStreet/Email.pm +++ b/perllib/FixMyStreet/Email.pm @@ -79,7 +79,9 @@ sub _render_template { } sub unique_verp_id { - sprintf('fms-%s@%s', generate_verp_token(@_), FixMyStreet->config('EMAIL_DOMAIN')); + my $parts = shift; + my $domain = shift || FixMyStreet->config('EMAIL_DOMAIN'); + sprintf('fms-%s@%s', generate_verp_token(@$parts), $domain); } sub _unique_id { diff --git a/perllib/FixMyStreet/Geocode/OSM.pm b/perllib/FixMyStreet/Geocode/OSM.pm index 20e653cf6..06162d74c 100644 --- a/perllib/FixMyStreet/Geocode/OSM.pm +++ b/perllib/FixMyStreet/Geocode/OSM.pm @@ -45,6 +45,7 @@ sub string { if $params->{bounds}; $query_params{countrycodes} = $params->{country} if $params->{country}; + $c->cobrand->call_hook(geocoder_munge_query_params => \%query_params); $url .= join('&', map { "$_=$query_params{$_}" } sort keys %query_params); $c->stash->{geocoder_url} = $url; diff --git a/perllib/FixMyStreet/Queue/Item/Report.pm b/perllib/FixMyStreet/Queue/Item/Report.pm index e38987838..60e9ad3dc 100644 --- a/perllib/FixMyStreet/Queue/Item/Report.pm +++ b/perllib/FixMyStreet/Queue/Item/Report.pm @@ -172,7 +172,7 @@ sub _create_reporters { my @dear; my %reporters = (); while (my $body = $bodies->next) { - my $sender_info = $self->cobrand->get_body_sender( $body, $row->category ); + my $sender_info = $self->cobrand_handler->get_body_sender( $body, $row ); my $sender = "FixMyStreet::SendReport::" . $sender_info->{method}; if ( ! exists $self->senders->{ $sender } ) { diff --git a/perllib/FixMyStreet/Script/Alerts.pm b/perllib/FixMyStreet/Script/Alerts.pm index d07728092..03373a8cc 100644 --- a/perllib/FixMyStreet/Script/Alerts.pm +++ b/perllib/FixMyStreet/Script/Alerts.pm @@ -327,7 +327,7 @@ sub _send_aggregated_alert_email(%) { } ); $data{unsubscribe_url} = $cobrand->base_url( $data{cobrand_data} ) . '/A/' . $token->token; - my $sender = FixMyStreet::Email::unique_verp_id('alert', $data{alert_id}); + my $sender = FixMyStreet::Email::unique_verp_id([ 'alert', $data{alert_id} ], $cobrand->call_hook('verp_email_domain')); my $result = FixMyStreet::Email::send_cron( $data{schema}, "$data{template}.txt", diff --git a/perllib/FixMyStreet/SendReport/Email.pm b/perllib/FixMyStreet/SendReport/Email.pm index 81a25f896..2d5e85f3e 100644 --- a/perllib/FixMyStreet/SendReport/Email.pm +++ b/perllib/FixMyStreet/SendReport/Email.pm @@ -53,10 +53,11 @@ sub send_from { sub envelope_sender { my ($self, $row) = @_; + my $cobrand = $row->get_cobrand_logged; if ($row->user->email && $row->user->email_verified) { - return FixMyStreet::Email::unique_verp_id('report', $row->id); + return FixMyStreet::Email::unique_verp_id([ 'report', $row->id ], $cobrand->call_hook('verp_email_domain')); } - return $row->get_cobrand_logged->do_not_reply_email; + return $cobrand->do_not_reply_email; } sub send { diff --git a/perllib/OIDC/Lite/Client/WebServer/Azure.pm b/perllib/OIDC/Lite/Client/WebServer/AuthCodeFlow.pm index b19dce90e..33a9a788f 100644 --- a/perllib/OIDC/Lite/Client/WebServer/Azure.pm +++ b/perllib/OIDC/Lite/Client/WebServer/AuthCodeFlow.pm @@ -1,4 +1,4 @@ -package OIDC::Lite::Client::WebServer::Azure; +package OIDC::Lite::Client::WebServer::AuthCodeFlow; use strict; use warnings; @@ -8,12 +8,15 @@ use OIDC::Lite::Client::IDTokenResponseParser; =head1 NAME -OIDC::Lite::Client::WebServer::Azure - extension to auth against Azure AD B2C +OIDC::Lite::Client::WebServer::AuthCodeFlow - extension to auth against an +identity provider using the authorization code flow, such as Azure AD B2C or +Google OAuth 2.0. +More info: https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowSteps OIDC::Lite doesn't appear to support the authorisation code flow to get an -ID token - only an access token. Azure returns all its claims in the id_token -and doesn't support a UserInfo endpoint, so this extension adds support for -parsing the id_token when calling get_access_token. +ID token - only an access token. This flow returns all its claims in the id_token +(and may not support a UserInfo endpoint e.g. Azure AD B2C), so this extension +adds support for parsing the id_token when calling get_access_token. =cut diff --git a/perllib/Open311/PopulateServiceList.pm b/perllib/Open311/PopulateServiceList.pm index 20fca90b3..a3672770c 100644 --- a/perllib/Open311/PopulateServiceList.pm +++ b/perllib/Open311/PopulateServiceList.pm @@ -145,6 +145,8 @@ sub _handle_existing_contact { my $service_name = $self->_normalize_service_name; my $protected = $contact->get_extra_metadata("open311_protect"); + return if $self->_current_body_cobrand && $self->_current_body_cobrand->call_hook(open311_skip_existing_contact => $contact); + print $self->_current_body->id . " already has a contact for service code " . $self->_current_service->{service_code} . "\n" if $self->verbose >= 2; if ( $contact->state eq 'deleted' || $service_name ne $contact->category || $self->_current_service->{service_code} ne $contact->email ) { @@ -370,7 +372,11 @@ sub _delete_contacts_not_in_service_list { sub _delete_contacts_not_in_service_list_cobrand_overrides { my ( $self, $found_contacts ) = @_; - return $found_contacts; + if ($self->_current_body_cobrand && $self->_current_body_cobrand->can('open311_filter_contacts_for_deletion')) { + return $self->_current_body_cobrand->open311_filter_contacts_for_deletion($found_contacts); + } else { + return $found_contacts; + } } 1; diff --git a/perllib/Open311/PostServiceRequestUpdates.pm b/perllib/Open311/PostServiceRequestUpdates.pm index d7345ea4d..a31bca8f7 100755 --- a/perllib/Open311/PostServiceRequestUpdates.pm +++ b/perllib/Open311/PostServiceRequestUpdates.pm @@ -50,7 +50,7 @@ sub open311_params { my $conf = $body; if ($comment) { my $cobrand_logged = $comment->get_cobrand_logged; - my $sender = $cobrand_logged->get_body_sender($body, $comment->problem->category); + my $sender = $cobrand_logged->get_body_sender($body, $comment->problem); $conf = $sender->{config}; } |