Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions conf/defaults.config
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,18 @@ $authen{proctor_module} = "WeBWorK::Authen::Proctor";
# This is used instead of $authen{user_module} when logging into the admin course.
$authen{admin_module} = ['WeBWorK::Authen::Basic_TheLastOption'];

# If set to true, then a user who is not logged into course X, but whose browser still
# holds an active session cookie for the admin course, will be logged into course X
# automatically, provided that:
# - the site uses cookie session management (not key)
# - the admin course user holds the create_and_delete_courses permission there, and
# - course X has a user with the same user_id whose password hash matches the one in
# the admin course. Note that password hashes will match when a user is added to
# course X from the admin course at the time course X is initialized. However,
# password hashes will not match simply from setting the same password in course X
# directly.
$authen{admin_cross_course_login} = 0;

################################################################################
# Authorization system (Make local overrides in localOverrides.conf )
################################################################################
Expand Down
12 changes: 12 additions & 0 deletions conf/localOverrides.conf.dist
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,18 @@ $mail{feedbackRecipients} = [
# the templates for a new course can be copied.
#$modelCoursesForCopy = [ "modelCourse", "anotherModelCourse", "aThirdOne" ];

# If set to true, then a user who is not logged into course X, but whose browser still
# holds an active session cookie for the admin course, will be logged into course X
# automatically, provided that:
# - the site uses cookie session management (not key)
# - the admin course user holds the create_and_delete_courses permission there, and
# - course X has a user with the same user_id whose password hash matches the one in
# the admin course. Note that password hashes will match when a user is added to
# course X from the admin course at the time course X is initialized. However,
# password hashes will not match simply from setting the same password in course X
# directly.
#$authen{admin_cross_course_login} = 1;

################################################################################
# OpenProblemLibrary
################################################################################
Expand Down
123 changes: 121 additions & 2 deletions lib/WeBWorK/Authen.pm
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ use Scalar::Util qw(weaken);
use Mojo::Util qw(b64_encode b64_decode);
use Math::Random::Secure qw(irand);

use WeBWorK::CourseEnvironment;
use WeBWorK::DB;
use WeBWorK::Debug;
use WeBWorK::Utils qw(x runtime_use utf8Crypt cryptPassword);
use WeBWorK::Utils::Logs qw(writeCourseLog);
Expand Down Expand Up @@ -199,7 +201,9 @@ sub verify {
} else {
$self->write_log_entry("LOGIN FAILED $self->{log_error}") if defined $self->{log_error};
$self->maybe_kill_cookie;
$c->stash(authen_error => $self->{error}) if $self->{error} && $self->{error} =~ /\S/;
# A failed admin cross-course login attempt should not surface an error to the user.
$c->stash(authen_error => $self->{error})
if $self->{error} && $self->{error} =~ /\S/ && ($self->{credential_source} // '') ne 'admin_cross_course';
}

my $caliper_sensor = Caliper::Sensor->new($c->ce);
Expand Down Expand Up @@ -261,6 +265,8 @@ sub do_verify {

if (defined $self->{login_type} && $self->{login_type} eq 'guest') {
return $self->verify_practice_user;
} elsif (($self->{credential_source} // '') eq 'admin_cross_course') {
return $self->verify_admin_cross_course_user;
} else {
return $self->verify_normal_user;
}
Expand Down Expand Up @@ -378,7 +384,89 @@ sub get_credentials {
return 1;
}

return 0;
return $self->try_admin_cross_course_credentials;
}

=head2 try_admin_cross_course_credentials

This is a last resort used by C<get_credentials> when a request for this course has no cookie or
C<user> parameter. If C<< $ce->{authen}{admin_cross_course_login} >> is enabled, the admin course
uses C<session_cookie> session management, and the browser still holds an active session cookie
for the admin course, this decodes that cookie, confirms the session is still valid, and confirms
the admin course user has the C<create_and_delete_courses> permission. If all of that succeeds,
C<user_id> and C<credential_source> are set so that C<verify_admin_cross_course_user> can finish
authenticating the user into this course.

=cut

sub try_admin_cross_course_credentials {
my $self = shift;
my $c = $self->{c};
my $ce = $c->ce;

return 0
unless $ce->{authen}{admin_cross_course_login}
&& defined $ce->{admin_course_id}
&& $ce->{courseName} ne $ce->{admin_course_id};

my ($ce_admin, $db_admin);
eval {
$ce_admin = WeBWorK::CourseEnvironment->new({ courseName => $ce->{admin_course_id} });
$db_admin = WeBWorK::DB->new($ce_admin);
};
return 0 if $@ || !$db_admin || ($ce_admin->{session_management_via} // '') ne 'session_cookie';

# Decode the admin course's session cookie
my $sessions = $c->app->sessions;
my $adminCookieName = 'WeBWorKCourseSession.' . $ce->{admin_course_id};
my $cookieMethod = $sessions->encrypted ? 'encrypted_cookie' : 'signed_cookie';
my $rawValue = $c->$cookieMethod($adminCookieName);
return 0 unless $rawValue;
$rawValue =~ y/-/=/;
my $adminSession = eval { $sessions->deserialize->(b64_decode($rawValue)) };
return 0 unless $adminSession;

my $expiration = $adminSession->{expiration} // $sessions->default_expiration;
my $expires = delete $adminSession->{expires};
return 0 if !$expires && $expiration || defined $expires && $expires <= time;

my ($adminUserID, $adminKey, $adminTimestamp) = @{$adminSession}{qw(user_id key timestamp)};
return 0 unless $adminUserID && $adminKey;

# Confirm the admin session is still valid
my $AdminKey = $db_admin->getKey($adminUserID);
return 0
unless defined $AdminKey
&& $AdminKey->key eq $adminKey
&& time <= ($adminTimestamp // $AdminKey->timestamp) + $ce_admin->{sessionTimeout};

return 0 unless _admin_course_has_create_delete_permission($ce_admin, $db_admin, $adminUserID);

my $AdminPassword = $db_admin->getPassword($adminUserID);
return 0 unless defined $AdminPassword && $AdminPassword->password =~ /\S/;

$self->{user_id} = $adminUserID;
$self->{admin_cross_course_password} = $AdminPassword->password;
$self->{login_type} = 'normal';
$self->{credential_source} = 'admin_cross_course';
debug('credential source: "admin_cross_course", user: "', $self->{user_id}, '"');
return 1;
}

# Mirrors the relevant part of WeBWorK::Authz::hasPermissions for the admin course, since that method requires a
# full Mojolicious controller bound to the admin course, and here the controller is bound to the course being
# entered instead.
sub _admin_course_has_create_delete_permission {
my ($ce_admin, $db_admin, $user) = @_;

my $activity_role = $ce_admin->{permissionLevels}{create_and_delete_courses};
return 0 unless defined $activity_role && exists $ce_admin->{userRoles}{$activity_role};
my $role_permlevel = $ce_admin->{userRoles}{$activity_role};

my $PermissionLevel = $db_admin->getPermissionLevel($user);
return 0 unless defined $PermissionLevel && defined $PermissionLevel->permission;

return $PermissionLevel->permission >= $role_permlevel;
}

sub check_user {
Expand Down Expand Up @@ -594,6 +682,37 @@ sub verify_normal_user {
}
}

=head2 verify_admin_cross_course_user

Finishes authenticating a user identified by C<try_admin_cross_course_credentials>.
Compares this course's password hash for the user against the admin course's hash
captured by C<try_admin_cross_course_credentials>.

=cut

sub verify_admin_cross_course_user {
my $self = shift;
my $c = $self->{c};

my $user_id = $self->{user_id};

my $coursePassword = $c->db->getPassword($user_id);
unless (defined $coursePassword
&& $coursePassword->password =~ /\S/
&& $coursePassword->password eq $self->{admin_cross_course_password})
{
$self->{log_error} = 'admin cross-course login: no matching password for this user in this course';
$self->{error} = $c->maketext(GENERIC_ERROR_MESSAGE);
return 0;
}

return 0 unless $self->validate_user;

$self->{session_key} = $self->create_session($user_id);
$self->{initial_login} = 1;
return 1;
}

# Returns 1 if authentication succeeded, returns 0 if required data was present but authentication failed,
# and returns -1 if the password is missing.
sub authenticate {
Expand Down