Skip to content
Snippets Groups Projects
WebRequest.pm 19.9 KiB
Newer Older
package IdPAccountManager::WebRequest;

use strict;
use warnings;

use English qw(-no_match_vars);
use IdPAccountManager::Logger;
## New web request
sub new {
    my ($pkg, %args) = @_;

    my $self = {
        format  => $args{format},
        actions => $args{actions},
    };
    $self->{logger} = IdPAccountManager::Logger->new(
        file      => $Conf::global{'log_file'},
        verbosity => $Conf::global{'log_level'}
    );
    $self->{logger}->log(
        level   => LOG_INFO,
        message => ''
    );
    my $http_query = CGI->new();

    ## Input parameters
    my %in_vars = $http_query->Vars;
    $self->{'param_in'} = \%in_vars;

    ## Check if admin acts as another user
    $self->{'cookies'} = CGI::Cookie->fetch;
#if (defined $self->{'cookies'}{'as_user'} && $request->{'is_admin'}) {
#    $self->{'utilisateur'} =  $request->{'as_user'} = $request->{'cookies'}{'as_user'}->value;
#    $self->{'is_admin'} = 0;
#}

    ## Usefull data for output (web pages or mail notices)
    $self->{'param_out'}{'url_cgi'} = $ENV{'SCRIPT_NAME'};
    $self->{'param_out'}{'env'}     = \%ENV;
    $self->{'param_out'}{'actions'} = $args{actions};
    $self->{'param_out'}{'conf'}    = \%Conf::global;
    foreach my $key (keys %{ $self->{'param_in'} }) {

        ## Removing all ^M (0D)
        $self->{'param_in'}{$key} =~ s/\r//g;
        $self->{'param_in'}{$key} =~ s/\s+$//;    ## Remove trailing spaces
        $self->{'param_in'}{$key} =~ s/^\s+//;    ## Remove leading spaces

        ## If action_xx param is set, then action=xx
        ## Usefull to have sementicless values in submit forms
        if ($key =~ /^action_(\w+)$/) {

            #$self->{logger}->log(level => LOG_TRACE, message => "ACTION $key");
            $self->{'param_in'}{'action'} = $1;
        }
    }

    ## Check the requested action
    if ($self->{'param_in'}{'action'}) {
        $self->{'action'} = $self->{'param_in'}{'action'};
    } else {
        ## Default action
        $self->{logger}->log(level => LOG_INFO, message => 'Default action');
        $self->{'action'} = 'home';
    bless $self, $pkg;
    return $self;
}

## Execute a web request
sub execute {
    my ($self) = @_;
    $self->{logger}->log(level => LOG_DEBUG, message => "");

    my $status;

    ## Check input parameters format
    foreach my $key (keys %{ $self->{'param_in'} }) {
        if (   $self->{'param_in'}{$key} !~ /^\s*$/
            && defined $self->{format}->{$key}
            && !ref($self->{format}->{$key}))
            unless ($self->{'param_in'}{$key} =~ /^$self->format->{$key}$/) {
                push @{ $self->{'param_out'}{'errors'} }, "format_$key";
                $self->{logger}->log(
                    level   => LOG_ERROR,
                    message => "Incorrect parameter format : $key"
                );
                return undef;
            }
        }
    }

    do {
        ## Actions can be chained
        $self->{'action'} = $self->{'next_action'} if ($self->{'next_action'});
        delete $self->{'next_action'};    ## Prevent loops

        if (defined $self->actions->{ $self->{'action'} }) {
            ## Execute the target subroutine named req_actionName
            no strict 'refs';
            my $sub = 'req_' . $self->{'action'};
            $status = &{$sub}($self);
        } else {
            ## Inknown action
            push @{ $self->{'param_out'}{'errors'} }, "unknown_action";
            $self->{logger}->log(
                level   => LOG_ERROR,
                message => "Unknown action '%s'",
                $self->{'action'}
            );

        }

    } while ($self->{'next_action'});

    return 1;
}

## Return HTML content
sub respond {
    my ($self) = @_;
    $self->{logger}->log(level => LOG_DEBUG, message => "");

    ## Automatic pass object entries to the output hash
    foreach my $key (keys %{$self}) {

        $self->{'param_out'}{$key} ||= $self->{$key}
          unless ($key eq 'param_out');
    }

    ## An action may redirect to an external URL
    if ($self->{'url_redirection'}) {

        printf "Location: %s\n\n", $self->{'url_redirection'};

    } else {

        ## Parse template
        my $tt2 = Template->new(
            {
                ENCODING => 'iso-8859-1',    ## le défaut apparemment
                FILTERS  => {
                    'encode_utf8', =>
                      [ \&IdPAccountManager::Tools::encode_utf8, 0 ],
                    'escape_quotes' =>
                      [ \&IdPAccountManager::Tools::escape_quotes, 0 ]
                },
                INCLUDE_PATH => $Conf::global{'root_manager_dir'} . ':'
                  . $Conf::global{'root_manager_dir'}
                  . '/templates/accountProfiles',

                #DEBUG => 'all',
                #DEBUG => 'caller',
                #DEBUG => 'parser'
            }
        );

        my $template;

        ## nobanner is used to do AJAX to get only pieces of HTML to load in the web client
        if ($self->{'param_in'}{'style'} eq 'nobanner') {
            $template = 'templates/web/index-nobanner.tt2.html';
        } else {
            $template = 'templates/web/index.tt2.html';
        }

        unless ($tt2->process($template, $self->{'param_out'}, \*STDOUT)) {
            printf "Content-type: text/plain\n\n Error: %s", $tt2->error();
            $self->{logger}->log(
                level   => LOG_ERROR,
                message => sprintf("Web parser error : %s", $tt2->error())
            );
        }
    }

    ## Ignore some type of errors
    my @errors_admin;
    foreach my $id_error (@{ $self->{'param_out'}{'errors'} }) {
        unless ($id_error =~ /^(error_x)$/) {
            push @errors_admin, $id_error;
        }
    }

    ## Mail notification of admins about the error
    if (@errors_admin) {
        $self->{'param_out'}{'subject'} = 'Error notification - web interface';
        IdPAccountManager::Tools::mail_notice(
            'template'    => 'templates/mail/notification_generic_error.tt2.eml',
            'data'        => $self->{'param_out'},
            'logger'      => $self->{'logger'},
            'conf'                => \%Conf::global,
            'admin_email'         => $Conf::global{'admin_email'},
            'dev_no_mail_outside' => $Conf::global{'dev_no_mail_outside'},
            'dev_sp_contact'      => $Conf::global{'dev_sp_contact'},
            'notice_from'         => $Conf::global{'notice_from'}
        );
    }
}

## Return the list of known SPs first
sub req_account_wizard {
    my ($self) = @_;
    $self->{logger}->log(level => LOG_INFO, message => "");

    my $federation_metadata = IdPAccountManager::SAMLMetadata->new(
        logger => $self->{logger}
    );

    unless (
        $federation_metadata->load(
            federation_metadata_file_path =>
              $Conf::global{'federation_metadata_file_path'}
        )
      )
    {
        push @{ $self->{'param_out'}{'errors'} }, "internal";
        $self->{logger}->log(
            level   => LOG_ERROR,
            message => "Failed to load federation metadata : $ERRNO"
        return undef;
    }

    unless ($federation_metadata->parse()) {
        push @{ $self->{'param_out'}{'errors'} }, "internal";
        $self->{logger}->log(
            level   => LOG_ERROR,
            message => "Failed to parse federation metadata : $ERRNO"
        return undef;
    }

    $self->{'param_out'}{'federation_metadata_as_hashref'} =
      $federation_metadata->{'federation_metadata_as_hashref'};

    return 1;
}

## Select a Service Provider and return metadata sctucture for the SP
## Sample URL : https://dev-edugain.renater.fr/accountmanager?action=select_sp&sp_entityid=http%3A%2F%2Fsp.lat.csc.fi
sub req_select_sp {
    my ($self) = @_;
    $self->{logger}->log(level => LOG_INFO, message => "");

    unless ($self->{'param_in'}{'sp_entityid'}) {
        push @{ $self->{'param_out'}{'errors'} }, "missing_sp_entityid";
        $self->{logger}
          ->log(level => LOG_ERROR, message => "Missing parameter sp_entityid");
    my $federation_metadata = IdPAccountManager::SAMLMetadata->new(
        logger => $self->{logger}
    );

    unless (
        $federation_metadata->load(
            federation_metadata_file_path =>
              $Conf::global{'federation_metadata_file_path'}
        )
      )
    {
        push @{ $self->{'param_out'}{'errors'} }, "internal";
        $self->{logger}->log(
            level   => LOG_ERROR,
            message => "Failed to load federation metadata : $ERRNO"
        return undef;
    }

    unless (
        $federation_metadata->parse(
            filter_entity_id => $self->{'param_in'}{'sp_entityid'}
        )
      )
    {
        push @{ $self->{'param_out'}{'errors'} }, "internal";
        $self->{logger}->log(
            level   => LOG_ERROR,
            message => "Failed to parse federation metadata : $ERRNO"
        return undef;
    }

    ## Create a serviceprovider object to store major parameters for this SP in DB
    my $service_provider = IdPAccountManager::ServiceProvider->new(
        entityid       => $self->{'param_in'}{'sp_entityid'},
        dev_sp_contact => $Conf::global{'dev_sp_contact'}
    );

    ## Prepare data
    my $sp_metadata_as_hashref =
      $federation_metadata->{'federation_metadata_as_hashref'}[0];
    my @contacts;
    if (defined $sp_metadata_as_hashref->{'contacts'}) {
        foreach my $contact (@{ $sp_metadata_as_hashref->{'contacts'} }) {
            my $email = $contact->{'EmailAddress'};
            $email =~ s/^(mailto:)//;    ## Remove 'mailto:' prefixes if any
            push @contacts, $email;
        }
    }
    my $display_name;
    if (defined $sp_metadata_as_hashref->{'display_name'}) {
        ## Use English version of displayName if available
        if ($sp_metadata_as_hashref->{'display_name'}{'en'}) {
            $display_name = $sp_metadata_as_hashref->{'display_name'}{'en'};
            ## Else any language
        } else {
            foreach
              my $lang (keys %{ $sp_metadata_as_hashref->{'display_name'} })
            {
                $display_name =
                  $sp_metadata_as_hashref->{'display_name'}{$lang};
                last;
            }
        }
    }

    ## Try loading DB object first
    if ($service_provider->load(speculative => 1)) {
        $service_provider->contacts(join(',', @contacts));
        $service_provider->displayname($display_name);

    } else {

        $service_provider = IdPAccountManager::ServiceProvider->new(
            entityid       => $self->{'param_in'}{'sp_entityid'},
            contacts       => join(',', @contacts),
            displayname    => $display_name,
            dev_sp_contact => $Conf::global{'dev_sp_contact'}
        );
        unless (defined $service_provider) {
            push @{ $self->{'param_out'}{'errors'} }, "internal";
            $self->{logger}->log(
                level   => LOG_ERROR,
                message => "Failed to create serviceprovider object"
            );
            return undef;
        }
    }

    unless ($service_provider->save()) {
        push @{ $self->{'param_out'}{'errors'} }, "internal";
        $self->{logger}->log(
            level   => LOG_ERROR,
            message => "Failed to save serviceprovider object"
        );
        return undef;
    }

    $self->{'param_out'}{'sp_metadata_as_hashref'} =
      $federation_metadata->{'federation_metadata_as_hashref'}[0];
    $self->{'param_out'}{'serviceprovider'} = $service_provider;

    return 1;
}

## Generate an authentication token to validate an email address
## Sample call : dev-edugain.renater.fr/accountmanager?action=generate_token&style=nobanner&sp_entityid=https%3A%2F%2Fsourcesup.cru.fr%2Fshibboleth&email_address=support%40renater.fr
sub req_generate_token {
    my ($self) = @_;
    $self->{logger}->log(level => LOG_INFO, message => "");

    unless ($self->{'param_in'}{'sp_entityid'}) {
        push @{ $self->{'param_out'}{'errors'} }, "missing_sp_entityid";
        $self->{logger}
          ->log(level => LOG_ERROR, message => "Missing parameter sp_entityid");
        return undef;
    }

    unless ($self->{'param_in'}{'email_address'}) {
        push @{ $self->{'param_out'}{'errors'} }, "email_address";
        $self->{logger}->log(
            level   => LOG_ERROR,
            message => "Missing parameter email_address"
        );
        return undef;
    }

    ## Create a serviceprovider object to load parameters for this SP from DB
    my $service_provider = IdPAccountManager::ServiceProvider->new(
        entityid       => $self->{'param_in'}{'sp_entityid'},
        dev_sp_contact => $Conf::global{'dev_sp_contact'}
    );

    # Try loading DB object first
    unless ($service_provider->load(speculative => 1)) {
        push @{ $self->{'param_out'}{'errors'} }, "internal";
        $self->{logger}->log(
            level   => LOG_ERROR,
            message => "Failed to load SP with entityid '%s'",
            $self->{'param_in'}{'sp_entityid'}
        );
        return undef;
    }

    ## Check that email_address is a known contact for this SP
    unless ($service_provider->is_contact($self->{'param_in'}{'email_address'}))
    {
        push @{ $self->{'param_out'}{'errors'} }, "internal";
        $self->{logger}->log(
            level => LOG_ERROR,
            message =>
              "Requested a token for %s for an unautorized address '%s'",
            $self->{'param_in'}{'sp_entityid'},
            $self->{'param_in'}{'email_address'}
        );
        return undef;
    }

    my $authentication_token = IdPAccountManager::AuthenticationToken->new(
        'email_address' => $self->{'param_in'}{'email_address'},
        'sp_entityid'   => $self->{'param_in'}{'sp_entityid'}
    );
    unless (defined $authentication_token) {
        push @{ $self->{'param_out'}{'errors'} }, "internal";
        $self->{logger}->log(
            level   => LOG_ERROR,
            message => "Failed to create authentication token"
        );
        return undef;
    }

    ## First remove token if one exist for this email+SP
    if ($authentication_token->load()) {
        unless ($authentication_token->delete()) {
            push @{ $self->{'param_out'}{'errors'} }, "internal";
            $self->{logger}->log(
                level   => LOG_ERROR,
                message => sprintf(
                    "Failed to delete previous authentication token with ID %s",
                    $authentication_token->get('id')
                )
        $authentication_token = IdPAccountManager::AuthenticationToken->new(
            'email_address' => $self->{'param_in'}{'email_address'},
            'sp_entityid'   => $self->{'param_in'}{'sp_entityid'}
        );
        unless (defined $authentication_token) {
            push @{ $self->{'param_out'}{'errors'} }, "internal";
            $self->{logger}->log(
                level   => LOG_ERROR,
                message => "Failed to create authentication token"
            );
            return undef;
        }
    }

    unless ($authentication_token->save()) {
        push @{ $self->{'param_out'}{'errors'} }, "internal";
        $self->{logger}->log(
            level   => LOG_ERROR,
            message => "Failed to save authentication token"
        );
        return undef;
    }

    $self->{'param_out'}{'authentication_token'} =
      $authentication_token->get('token');
    $self->{'param_out'}{'email_address'} =
      $self->{'param_in'}{'email_address'};
    $self->{'param_out'}{'sp_entityid'} = $self->{'param_in'}{'sp_entityid'};
    $self->{'param_out'}{'to'}          = $self->{'param_in'}{'email_address'};

    ## Send the challenge email with the token
    IdPAccountManager::Tools::mail_notice(
        'template' => 'templates/mail/send_authentication_token.tt2.eml',
        'to'       => $self->{'param_in'}{'email_address'},
        'data'     => $self->{'param_out'},
        'logger'   => $self->{'logger'}
    $self->{logger}->log(
        level   => LOG_INFO,
        message => "Token send to %s for sp_entityid=%s;token=%s",
        $self->{'param_in'}{'email_address'},
        $self->{'param_in'}{'sp_entityid'},
        $self->{'param_out'}{'authentication_token'}
    );

    return 1;
}

## Validate an authentication token
## Test accounts get created
## Sample call : dev-edugain.renater.fr/accountmanager?action=validate_token&style=nobanner&sp_entityid=https%3A%2F%2Fsourcesup.cru.fr%2Fshibboleth&authentication_token=c1cfecb51ea40d39a695
sub req_validate_token {
    my ($self) = @_;
    $self->{logger}->log(level => LOG_INFO, message => "");

    unless ($self->{'param_in'}{'sp_entityid'}) {
        push @{ $self->{'param_out'}{'errors'} }, "missing_sp_entityid";
        $self->{logger}
          ->log(level => LOG_ERROR, message => "Missing parameter sp_entityid");
        return undef;
    }

    unless ($self->{'param_in'}{'authentication_token'}) {
        push @{ $self->{'param_out'}{'errors'} },
          "missing_authentication_token";
        $self->{logger}->log(
            level   => LOG_ERROR,
            message => "Missing parameter authentication_token"
        );
    my $authentication_token = IdPAccountManager::AuthenticationToken->new(
        token => $self->{'param_in'}{'authentication_token'});

    unless ($authentication_token->load()) {
        push @{ $self->{'param_out'}{'errors'} }, "wrong_token";
        $self->{logger}->log(
            level => LOG_ERROR,
            message =>
              "Failed to validate authentication token %s for sp_entityid %s",
            $self->{'param_in'}{'authentication_token'},
            $self->{'param_in'}{'sp_entityid'}
        );
        return undef;
    }

    unless ($authentication_token->get('sp_entityid') eq
        $self->{'param_in'}{'sp_entityid'})
    {
        push @{ $self->{'param_out'}{'errors'} }, "wrong_token_for_sp";
        $self->{logger}->log(
            level => LOG_ERROR,
            message =>
              "Authentication token %s cannot be used for SP with entityid %s",
            $self->{'param_in'}{'authentication_token'},
            $self->{'param_in'}{'sp_entityid'}
        );
        return undef;
    }

    ## delete the token
    unless ($authentication_token->delete()) {
        $self->{logger}->log(
            level   => LOG_ERROR,
            message => "Failed to delete authentication token %s",
            $self->{'param_in'}{'authentication_token'}
        );
    }

    ## create test accounts
    my @test_accounts =
      IdPAccountManager::TestAccount::create_test_accounts_for_sp(
        sp_entityid      => $self->{'param_in'}{'sp_entityid'},
        account_profiles => $Conf::global{'account_profiles'}
    );

    unless (@test_accounts) {
        push @{ $self->{'param_out'}{'errors'} }, "accounts_creation_failed";
        $self->{logger}->log(
            level   => LOG_ERROR,
            message => "Failed to create test accounts for SP with entityid %s",
            $self->{'param_in'}{'sp_entityid'}
        );
        return undef;
    }

    ## Update simpleSAMLphp configuration to enable test accounts
    unless (IdPAccountManager::Tools::update_ssp_authsources(
            $Conf::global{'root_manager_dir'},
            \%Conf::global
        )) {
        push @{ $self->{'param_out'}{'errors'} }, "accounts_creation_failed";
        $self->{logger}->log(
            level   => LOG_ERROR,
            message => "Failed to create simpleSAMLphp configuration file"
        );
    $self->{logger}->log(
        level   => LOG_INFO,
        message => "Token validated for sp_entityid=%s;token=%s",
        $self->{'param_in'}{'sp_entityid'},
        $self->{'param_in'}{'authentication_token'}
    );

    $self->{'param_out'}{'sp_entityid'}   = $self->{'param_in'}{'sp_entityid'};
    $self->{'param_out'}{'test_accounts'} = \@test_accounts;

    return 1;
}

## Return the homepage of the service
sub req_home {
    my ($self) = @_;
    $self->{logger}->log(level => LOG_INFO, message => "");