Bivio::Util::RealmAdmin
# Copyright (c) 2002-2023 bivio Software, Inc. All Rights Reserved.
package Bivio::Util::RealmAdmin;
use strict;
use Bivio::Base 'Bivio.ShellUtil';
b_use('IO.ClassLoaderAUTOLOAD');
my($_C) = b_use('SQL.Connection');
my($_DT) = b_use('Type.DateTime');
my($_M) = b_use('Biz.Model');
my($_L) = b_use('Type.Location');
sub USAGE {
return <<'EOF';
usage: b-realm-admin [options] command [args...]
commands:
create_user email display_name password [user_name] -- creates a new user
delete_auth_realm -- deletes auth_realm
delete_auth_realm_and_users -- deletes realm and all of its users
delete_auth_user -- deletes auth_user
delete_email_verify -- delete email verification record for auth_realm
diff_users left_realm right_realm -- report differences between rosters
force_email_verify -- creates/updates email verification record for auth_realm
info -- dump info on a realm
invalidate_email -- invalidate a user's email
invalidate_password -- invalidates a user's password
join_user roles... -- adds specified user role to realm
leave_role -- remove one user role from a realm
leave_user -- removes all user roles from realm
reset_login_attempts -- reset consecutive failed login attempt count for user
reset_password password -- reset a user's password
scan_realm_id [realm_id] -- checks for auth_id in all table fields
subscribe_user_to_realm -- subscribe given user to given realm
to_id anything -- the id for the realm passed as an argument
unsafe_to_id anything -- to_id if it exists else undef
unsubscribe_user_from_realm -- unsubscribe given user from given realm
users [role] -- dump users in realm [with a specific role]
verify_realm_owners -- ensures RealmOwner has associated owner model
EOF
}
sub create_user {
my($self, $email, $display_name, $password, $user_name) = shift->name_args([
'Email',
[DisplayName => sub {
my(undef, $args) = @_;
return b_use('Type.Email')->get_local_part($args->{Email});
}],
[Password => sub {b_use('Biz.Random')->string}],
[RealmName => sub {
my(undef, $args) = @_;
return b_use('Type.RealmName')
->clean_and_trim($args->{DisplayName});
}],
], \@_);
$self->initialize_ui;
return $self->model(UserCreateForm => {
'Email.email' => $email,
'RealmOwner.display_name' => $display_name,
new_password => $password,
confirm_password => $password,
'RealmOwner.name' => $user_name,
})->get('User.user_id');
}
sub delete_auth_realm {
my($self) = @_;
$self->are_you_sure('Delete ' . $self->req('auth_realm')->as_string . '?');
my($id) = $self->req('auth_id');
$self->req->set_realm(undef);
$self->model('RealmOwner')->unauth_delete_realm({realm_id => $id});
return;
}
sub delete_auth_realm_and_users {
my($self) = @_;
my($req) = $self->req;
$self->usage_error($self->req('auth_realm'), ': cannot delete a default realm')
if $req->get('auth_realm')->is_default;
$self->are_you_sure('delete realm ' . $self->req(qw(auth_realm owner_name)));
foreach my $r (
@{$self->model('RealmUser')
->map_iterate(sub {shift->get('user_id')}, 'user_id')
},
) {
next
if $r eq $self->req(qw(auth_id));
$req->with_user(
$r,
sub {
$self->delete_auth_user;
return;
},
);
}
return $self->delete_auth_realm;
}
sub delete_auth_user {
my($self) = @_;
my($u) = $self->req('auth_user');
my($req) = $self->req;
$req->set_user(undef);
$req->set_realm(undef)
if Type_PrimaryId()->is_equal($u->get('realm_id'), $req->get('auth_id'));
$u->unauth_delete_realm;
return;
}
sub delete_email_verify {
my($req) = shift->req;
$_M->new($req, 'EmailVerify')->delete({location => $_L->get_default});
return;
}
sub delete_user {
Bivio::IO::Alert->warn_deprecated('use delete_auth_user');
return shift->delete_auth_user(@_);
}
sub delete_with_users {
Bivio::IO::Alert->warn_deprecated('use delete_auth_realm_and_users');
return shift->delete_auth_realm_and_users(@_);
}
sub diff_users {
sub DIFF_USERS {[[qw(left_realm RealmArg)], [qw(right_realm RealmArg)]]}
my($self) = shift;
$self->initialize_fully;
my(undef, $bp) = $self->parameters(\@_);
my($maps) = {};
foreach my $which (qw(left right)) {
my($u) = $self->req->with_realm(
$bp->{$which . '_realm'},
sub {$self->model('GroupUserList')->map_iterate},
);
foreach my $field (qw(RealmOwner.display_name RealmUser.realm_id)) {
($maps->{$field} ||= {})->{$which}
= {map(($_->{$field} => $_), @$u)};
}
}
foreach my $map (values(%$maps)) {
while (my($key, $row) = each(%{$map->{left}})) {
next
unless $row == ($map->{right}->{$key} || {});
foreach my $which (qw(left right)) {
delete($maps->{'RealmUser.realm_id'}
->{$which}->{$row->{'RealmUser.realm_id'}});
}
}
}
return [map([values(%{$maps->{'RealmUser.realm_id'}->{$_}})], qw(left right))];
}
sub force_email_verify {
my($req) = shift->req;
$_M->new_other('EmailVerify')->force_update($req->get('auth_id'));
return;
}
sub info {
my($self, $owner) = @_;
# Info on I<realm_owner> or auth_realm.
return _info(
$owner || $self->get_request->get_nested(qw(auth_realm owner))
) . "\n";
}
sub invalidate_email {
my($self) = @_;
# Invalidates the user's email address.
_validate_user($self, 'Invalidate Email')
->get_model('User')->invalidate_email;
return;
}
sub invalidate_password {
my($self) = @_;
# Invalidate the user's password.
_validate_user($self, 'Invalidate Password')->invalidate_password;
return;
}
sub is_realm_user {
my($self) = @_;
return $self->model('RealmUser')->unauth_rows_exist({
realm_id => $self->req('auth_id'),
user_id => $self->req('auth_user_id'),
});
}
sub join_user {
my($self, @roles) = shift->name_args([['Auth.Role']], \@_);
my($req) = $self->req;
foreach my $role (@roles) {
my($v) = {
realm_id => $req->get('auth_id'),
user_id => $req->get('auth_user_id'),
role => $role,
};
$self->model('RealmUser')->create($v)
unless $self->model('RealmUser')->unauth_load($v);
$self->model('RealmUserAddForm')->set_subscription(
$req->get('auth_user_id'), $req->get('auth_id'))
if $role->eq_mail_recipient;
}
return;
}
sub leave_role {
my($self, @roles) = shift->name_args([['Auth.Role']], \@_);
$self->assert_have_user;
foreach my $role (@roles) {
$self->model('RealmUser')->delete({
realm_id => $self->req('auth_id'),
user_id => $self->req('auth_user_id'),
role => $role,
});
}
return;
}
sub leave_user {
my($self) = @_;
# Drops I<user> from I<realm>.
my($req) = $self->get_request;
my($realm_user) = Bivio::Biz::Model->new($req, 'RealmUser');
$realm_user->unauth_iterate_start('realm_id', {
realm_id => $req->get('auth_id')
|| $self->usage_error('realm not set'),
user_id => $req->get('auth_user_id')
|| $self->usage_error('user not set'),
});
while ($realm_user->iterate_next_and_load) {
$realm_user->delete;
}
$realm_user->iterate_end;
return;
}
sub reset_login_attempts {
my($self) = @_;
$self->assert_not_general;
$self->model('LoginAttempt')->reset_failure_count($self->req('auth_id'));
return;
}
sub reset_password {
my($self, $password) = @_;
# Changes a user's password.
$self->usage_error("missing new password")
unless defined($password);
_validate_user($self, 'Reset Password')->update({
password => b_use('Type.Password')->encrypt($password),
});
return;
}
sub scan_realm_id {
my($self, $realm_id) = @_;
# Scans all bivio tables, looking for realm_id.
my($id) = $realm_id || $self->req('auth_id');
$self->usage_error('missing realm')
unless $id && $id > 1;
my($tables) = $_C->map_execute(
'SELECT relname FROM pg_class WHERE relname LIKE ? ORDER BY relname',
['%_t']);
foreach my $table (@$tables) {
next if $table eq 'task_log_t' || $table =~ /^pg_/;
my($count) = 0;
$_C->do_execute(sub {
my($row) = @_;
foreach my $v (grep($_, @$row)) {
$count++
if $v eq $id;
}
return 1;
}, "SELECT * FROM $table");
$self->print(join(',', $table, $count), "\n")
if $count;
}
return;
}
sub subscribe_user_to_realm {
_subscription(shift, 1);
return;
}
sub to_id {
return shift->unsafe_to_id(@_)
|| b_die(shift, ': not found');
}
sub unsafe_to_id {
sub UNSAFE_TO_ID {[[qw(anything Line)]]}
my($self, $bp) = shift->parameters(\@_);
my($r) = $self->model('RealmOwner');
return undef
unless $r->unauth_load_by_email_id_or_name($bp->{anything})
|| $r->unauth_load({display_name => $bp->{anything}});
return $r->get('realm_id');
}
sub unsubscribe_user_from_realm {
_subscription(shift, 0);
return;
}
sub users {
my($self, $role) = @_;
# Users for realm. Filter by role
$role &&= uc($role);
my($util) = $self->new_other('User');
my($roles) = {};
$self->model('RealmUser')->do_iterate(
sub {
my($ru) = @_;
push(@{$roles->{$ru->get('user_id')} ||= []}, [
$ru->get('role')->get_name,
$_DT->to_xml($ru->get('creation_date_time'))
. $util->subscribe_info($ru),
]);
return 1;
},
'role asc',
);
return join('',
map({
my($ro, $roles) = @$_;
join("\n ", _info($ro), map(join(' ', @$_), sort(@$roles))) . "\n";
} sort {
$a->[0]->get('name') cmp $b->[0]->get('name')
} map(
[$self->unauth_model(RealmOwner => {realm_id => $_}), $roles->{$_}],
!$role ? keys(%$roles)
: grep(grep($_->[0] eq $role, @{$roles->{$_}}), keys(%$roles)),
)),
);
}
sub verify_realm_owners {
my($self) = @_;
my($owner_models) = {};
b_use('Biz.PropertyModel')->do_iterate_model_subclasses(
sub {
my($proto) = @_;
if (UNIVERSAL::isa($proto, b_use('Model.RealmOwnerBase'))) {
my($m) = $proto->new($self->req);
my($key_names) = $m->get_info('primary_key_names');
b_die($proto)
if @$key_names != 1;
$owner_models->{$proto} = {
primary_id => $key_names->[0],
model => $m,
table => $m->get_info('table_name'),
};
}
return 1;
},
);
foreach my $info (values(%$owner_models)) {
my($table, $column) = ($info->{table}, $info->{primary_id});
b_use('SQL.Connection')->do_execute(
sub {
my($row) = @_;
$self->print($table, ' ', $column, ': ', $row->[0], "\n");
return 1;
},
<<"EOF",
SELECT $column FROM $table
WHERE NOT EXISTS (
SELECT ro.realm_id FROM realm_owner_t ro
WHERE ro.realm_id = $table.$column
)
EOF
);
}
$self->model('RealmOwner')->do_iterate(
sub {
my($ro) = @_;
return 0
if $ro->get('realm_id') < 100;
foreach my $info (values(%$owner_models)) {
next unless $info->{model}->unauth_load({
$info->{primary_id} => $ro->get('realm_id'),
});
return 1;
}
$self->print(
$ro->get('realm_type')->get_name,
' ',
$ro->get('realm_id'),
"\n");
return 1;
},
'unauth_iterate_start',
'realm_id DESC',
);
return;
}
sub _info {
my($user) = @_;
return join("\n ",
join(' ',
$user->get(qw(name realm_id password)),
$_DT->to_xml($user->get('creation_date_time')),
$user->get('display_name'),
),
@{$user->new_other('Email')->map_iterate(
sub {
my($l, $e) = shift->get(qw(location email));
return $l->get_name . ' ' . $e;
},
'unauth_iterate_start',
'location',
{realm_id => $user->get('realm_id')},
)},
$user->is_locked_out ? 'Is Locked Out' : (),
);
}
sub _subscription {
my($self, $subscribed) = @_;
$self->assert_not_general;
$self->assert_have_user;
$self->model('UserRealmSubscription')->create_or_update({
user_id => $self->req('auth_user_id'),
realm_id => $self->req('auth_id'),
is_subscribed => $subscribed,
});
return
unless $subscribed;
b_warn('user does not have MAIL_RECIPIENT role in this realm, will not receive mail')
unless $self->model('RealmUser')->unsafe_load({
user_id => $self->req('auth_user_id'),
role => Auth_Role('MAIL_RECIPIENT'),
});
return;
}
sub _validate_user {
my($self, $message) = @_;
# Ensures the user is present, displays the are_you_sure using the
# specified message.
# Returns the user's realm.
$self->assert_have_user;
my($req) = $self->get_request;
$self->are_you_sure($message . ' for '
. $req->get_nested(qw(auth_user display_name))
. ' of '
. $req->get_nested(qw(auth_realm owner display_name))
. '?');
return $req->get('auth_user');
}
1;