3
0
mirror of https://github.com/pragma-/pbot.git synced 2024-12-01 16:39:27 +01:00
pbot/lib/PBot/Core/Commands/Users.pm
Pragmatic Software 6722fd7f8d
Store user passwords as salted hash digests
This was way overdue. User passwords are no longer stored as cleartext.

When PBot is restarted after applying this commit, all stored passwords will
be converted to salted hash digests.

The `useradd`, `userset` and `my` commands will now hash passwords.

Why did it take me so long to finally get around to hashing passwords
properly, you might ask. The reason why this wasn't done sooner is because
all of my users used hostmask-based `autologin`. The passwords that PBot
randomly generated were ignored and never used.

I do regret that it took me so long to get around to this, for those of you
who might be using custom passwords instead of hostmask-based `autologin`.
2024-06-22 22:38:15 -07:00

430 lines
18 KiB
Perl

# File: Users.pm
#
# Purpose: Commands to manage list of bot users/admins and their metadata.
# SPDX-FileCopyrightText: 2010-2023 Pragmatic Software <pragma78@gmail.com>
# SPDX-License-Identifier: MIT
package PBot::Core::Commands::Users;
use PBot::Imports;
use parent 'PBot::Core::Class';
sub initialize($self, %conf) {
# register commands
$self->{pbot}->{commands}->register(sub { $self->cmd_login(@_) }, "login", 0);
$self->{pbot}->{commands}->register(sub { $self->cmd_logout(@_) }, "logout", 0);
$self->{pbot}->{commands}->register(sub { $self->cmd_useradd(@_) }, "useradd", 1);
$self->{pbot}->{commands}->register(sub { $self->cmd_userdel(@_) }, "userdel", 1);
$self->{pbot}->{commands}->register(sub { $self->cmd_usershow(@_) }, "usershow", 0);
$self->{pbot}->{commands}->register(sub { $self->cmd_userset(@_) }, "userset", 1);
$self->{pbot}->{commands}->register(sub { $self->cmd_userunset(@_) }, "userunset", 1);
$self->{pbot}->{commands}->register(sub { $self->cmd_users(@_) }, "users", 0);
$self->{pbot}->{commands}->register(sub { $self->cmd_my(@_) }, "my", 0);
$self->{pbot}->{commands}->register(sub { $self->cmd_id(@_) }, "id", 0);
# add capabilities to admin group
$self->{pbot}->{capabilities}->add('admin', 'can-useradd', 1);
$self->{pbot}->{capabilities}->add('admin', 'can-userdel', 1);
$self->{pbot}->{capabilities}->add('admin', 'can-userset', 1);
$self->{pbot}->{capabilities}->add('admin', 'can-userunset', 1);
# create capability (it will get added to botowner group when Core is done loading)
$self->{pbot}->{capabilities}->add('can-modify-admins', undef, 1);
}
sub cmd_login($self, $context) {
my $channel = $context->{from};
return "Usage: login [channel] password" if not $context->{arguments};
my $arguments = $context->{arguments};
if ($arguments =~ m/^([^ ]+)\s+(.+)/) {
$channel = $1;
$arguments = $2;
}
my ($user_channel, $user_hostmask) = $self->{pbot}->{users}->find_user_account($channel, $context->{hostmask});
return "/msg $context->{nick} You do not have a user account. You may use the `my` command to create a personal user account. See `help my`." if not defined $user_channel;
my $name = $self->{pbot}->{users}->{user_index}->{$user_channel}->{$user_hostmask};
my $u = $self->{pbot}->{users}->{storage}->get_data($name);
my $channel_text = $user_channel eq 'global' ? '' : " for $user_channel";
if ($u->{loggedin}) {
return "/msg $context->{nick} You are already logged into " . $self->{pbot}->{users}->{storage}->get_key_name($name) . " ($user_hostmask)$channel_text.";
}
my $result = $self->{pbot}->{users}->login($user_channel, $user_hostmask, $arguments);
return "/msg $context->{nick} $result";
}
sub cmd_logout($self, $context) {
$context->{from} = $context->{arguments} if length $context->{arguments};
my ($user_channel, $user_hostmask) = $self->{pbot}->{users}->find_user_account($context->{from}, $context->{hostmask});
return "/msg $context->{nick} You do not have a user account. You may use the `my` command to create a personal user account. See `help my`." if not defined $user_channel;
my $name = $self->{pbot}->{users}->{user_index}->{$user_channel}->{$user_hostmask};
my $u = $self->{pbot}->{users}->{storage}->get_data($name);
my $channel_text = $user_channel eq 'global' ? '' : " for $user_channel";
return "/msg $context->{nick} You are not logged into " . $self->{pbot}->{users}->{storage}->get_key_name($name) . " ($user_hostmask)$channel_text." if not $u->{loggedin};
$self->{pbot}->{users}->logout($user_channel, $user_hostmask);
return "/msg $context->{nick} Logged out of " . $self->{pbot}->{users}->{storage}->get_key_name($name) . " ($user_hostmask)$channel_text.";
}
sub cmd_users($self, $context) {
my $channel = $self->{pbot}->{interpreter}->shift_arg($context->{arglist});
my $include_global = '';
if (not defined $channel) {
$channel = $context->{from};
$include_global = 'global';
} else {
$channel = 'global' if $channel !~ /^#/;
}
my $text = "Users: ";
my $last_channel = "";
my $sep = "";
foreach my $chan (sort keys %{$self->{pbot}->{users}->{user_index}}) {
next if $context->{from} =~ m/^#/ and $chan ne $channel and $chan ne $include_global;
next if $context->{from} !~ m/^#/ and $channel =~ m/^#/ and $chan ne $channel;
if ($last_channel ne $chan) {
$text .= "$sep$chan: ";
$last_channel = $chan;
$sep = "";
}
my %seen_names;
foreach my $hostmask (
sort { $self->{pbot}->{users}->{user_index}->{$chan}->{$a} cmp $self->{pbot}->{users}->{user_index}->{$chan}->{$b} }
keys %{$self->{pbot}->{users}->{user_index}->{$chan}}
)
{
my $name = $self->{pbot}->{users}->{user_index}->{$chan}->{$hostmask};
next if $seen_names{$name};
$seen_names{$name} = 1;
$text .= $sep;
my $has_cap = 0;
foreach my $key ($self->{pbot}->{users}->{storage}->get_keys($name)) {
if ($self->{pbot}->{capabilities}->exists($key)) {
$has_cap = 1;
last;
}
}
$text .= '+' if $has_cap;
$text .= $self->{pbot}->{users}->{storage}->get_key_name($name);
$sep = " ";
}
$sep = "; ";
}
return $text;
}
sub cmd_useradd($self, $context) {
my ($name, $hostmasks, $channels, $capabilities, $password) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 5);
$capabilities //= 'none';
if (not defined $name or not defined $hostmasks) { return "Usage: useradd <username> <hostmasks> [channels [capabilities [password]]]"; }
$channels = 'global' if !$channels or $channels !~ /^#/;
my $u;
foreach my $channel (sort split /\s*,\s*/, lc $channels) {
$u = $self->{pbot}->{users}->find_user($channel, $context->{hostmask});
if (not defined $u) {
return "You do not have a user account for $channel; cannot add users to that channel.\n";
}
}
if ($capabilities ne 'none' and not $self->{pbot}->{capabilities}->userhas($u, 'can-modify-capabilities')) {
return "Your user account does not have the can-modify-capabilities capability. You cannot create user accounts with capabilities.";
}
foreach my $cap (split /\s*,\s*/, lc $capabilities) {
next if $cap eq 'none';
return "There is no such capability $cap." if not $self->{pbot}->{capabilities}->exists($cap);
if (not $self->{pbot}->{capabilities}->userhas($u, $cap)) { return "To set the $cap capability your user account must also have it."; }
if ($self->{pbot}->{capabilities}->has($cap, 'admin') and not $self->{pbot}->{capabilities}->userhas($u, 'can-modify-admins')) {
return "To set the $cap capability your user account must have the can-modify-admins capability.";
}
}
$self->{pbot}->{users}->add_user($name, $channels, $hostmasks, $capabilities, $password);
return "User added.";
}
sub cmd_userdel($self, $context) {
if (not length $context->{arguments}) { return "Usage: userdel <username>"; }
my $u = $self->{pbot}->{users}->find_user($context->{from}, $context->{hostmask});
my $t = $self->{pbot}->{users}->{storage}->get_data($context->{arguments});
if ($self->{pbot}->{capabilities}->userhas($t, 'botowner') and not $self->{pbot}->{capabilities}->userhas($u, 'botowner')) {
return "Only botowners may delete botowner user accounts.";
}
if ($self->{pbot}->{capabilities}->userhas($t, 'admin') and not $self->{pbot}->{capabilities}->userhas($u, 'can-modify-admins')) {
return "To delete admin user accounts your user account must have the can-modify-admins capability.";
}
return $self->{pbot}->{users}->remove_user($context->{arguments});
}
sub cmd_usershow($self, $context) {
my ($name, $key) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2);
if (not defined $name) { return "Usage: usershow <username> [key]"; }
my $channel = $context->{from};
my $target = $self->{pbot}->{users}->{storage}->get_data($name);
if (not $target) {
return "There is no user account $name.";
}
if (defined $key && lc $key eq 'password') {
return "I don't think so.";
}
my $result = $self->{pbot}->{users}->{storage}->set($name, $key, undef);
$result =~ s/^password: .*;?$/password: <private>;/m;
return $result;
}
sub cmd_userset($self, $context) {
my ($name, $key, $value) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 3);
if (not defined $name) { return "Usage: userset <username> [key [value]]"; }
my $channel = $context->{from};
my $u = $self->{pbot}->{users}->find_user($channel, $context->{hostmask}, 1);
my $target = $self->{pbot}->{users}->{storage}->get_data($name);
if (not $u) {
$channel = 'global' if $channel !~ /^#/;
return "You do not have a user account for $channel; cannot modify their users.";
}
if (not $target) {
return "There is no user account $name.";
}
$key = lc $key if defined $key;
if (defined $value and not $self->{pbot}->{capabilities}->userhas($u, 'can-modify-capabilities')) {
if ($key =~ m/^can-/i or $self->{pbot}->{capabilities}->exists($key)) {
return "The $key metadata requires the can-modify-capabilities capability, which your user account does not have.";
}
}
if (defined $value and $self->{pbot}->{capabilities}->userhas($target, 'admin') and not $self->{pbot}->{capabilities}->userhas($u, 'can-modify-admins')) {
return "To modify admin user accounts your user account must have the can-modify-admins capability.";
}
if (defined $key and $self->{pbot}->{capabilities}->exists($key) and not $self->{pbot}->{capabilities}->userhas($u, $key)) {
return "To set the $key capability your user account must also have it." unless $self->{pbot}->{capabilities}->userhas($u, 'botowner');
}
if ($key eq 'password' and defined $value) {
$value = $self->{pbot}->{users}->digest_password($value);
}
my $result = $self->{pbot}->{users}->{storage}->set($name, $key, $value);
$result =~ s/^password: .*;?$/password: <private>;/m;
if (defined $key and ($key eq 'channels' or $key eq 'hostmasks') and defined $value) {
$self->{pbot}->{users}->rebuild_user_index;
}
return $result;
}
sub cmd_userunset($self, $context) {
my ($name, $key) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2);
if (not defined $name or not defined $key) { return "Usage: userunset <username> <key>"; }
$key = lc $key;
my @disallowed = qw/channels hostmasks password/;
if (grep { $_ eq $key } @disallowed) {
return "The $key metadata cannot be unset. Use the `userset` command to modify it.";
}
my $channel = $context->{from};
my $u = $self->{pbot}->{users}->find_user($channel, $context->{hostmask}, 1);
my $target = $self->{pbot}->{users}->{storage}->get_data($name);
if (not $u) {
$channel = 'global' if $channel !~ /^#/;
return "You do not have a user account for $channel; cannot modify their users.";
}
if (not $target) {
return "There is no user account $name.";
}
if (not $self->{pbot}->{capabilities}->userhas($u, 'can-modify-capabilities')) {
if ($key =~ m/^can-/i or $self->{pbot}->{capabilities}->exists($key)) {
return "The $key metadata requires the can-modify-capabilities capability, which your user account does not have.";
}
}
if ($self->{pbot}->{capabilities}->userhas($target, 'admin') and not $self->{pbot}->{capabilities}->userhas($u, 'can-modify-admins')) {
return "To modify admin user accounts your user account must have the can-modify-admins capability.";
}
if ($self->{pbot}->{capabilities}->exists($key) and not $self->{pbot}->{capabilities}->userhas($u, $key)) {
return "To unset the $key capability your user account must also have it." unless $self->{pbot}->{capabilities}->userhas($u, 'botowner');
}
return $self->{pbot}->{users}->{storage}->unset($name, $key);
}
sub cmd_my($self, $context) {
my ($key, $value) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2);
if (defined $value) {
$value =~ s/^is\s+//;
$value = undef if not length $value;
}
my $channel = $context->{from};
my $hostmask = $context->{hostmask};
my ($u, $name) = $self->{pbot}->{users}->find_user($channel, $hostmask, 1);
if (not $u) {
$channel = 'global';
$hostmask = "$context->{nick}!$context->{user}\@" . $self->{pbot}->{antiflood}->address_to_mask($context->{host});
$name = $context->{nick};
$u = $self->{pbot}->{users}->{storage}->get_data($name);
if ($u) {
$self->{pbot}->{logger}->log("Adding additional hostmask $hostmask to user account $name\n");
$u->{hostmasks} .= ",$hostmask";
$self->{pbot}->{users}->rebuild_user_index;
} else {
$u = $self->{pbot}->{users}->add_user($name, $channel, $hostmask, undef, undef, 1);
$u->{loggedin} = 1;
$u->{stayloggedin} = 1;
$u->{autologin} = 1;
$self->{pbot}->{users}->save;
}
}
my $result = '';
if (defined $key) {
$key = lc $key;
if (defined $value) {
if (not $self->{pbot}->{capabilities}->userhas($u, 'can-modify-capabilities')) {
if ($key =~ m/^is-/ or $key =~ m/^can-/ or $self->{pbot}->{capabilities}->exists($key)) {
return "The $key metadata requires the can-modify-capabilities capability, which your user account does not have.";
}
}
if (not $self->{pbot}->{capabilities}->userhas($u, 'botowner')) {
my @disallowed = qw/can-modify-admins botowner can-modify-capabilities channels/;
if (grep { $_ eq $key } @disallowed) {
return "The $key metadata requires the botowner capability to set, which your user account does not have.";
}
}
if (not $self->{pbot}->{capabilities}->userhas($u, 'admin')) {
my @disallowed = qw/name autoop autovoice chanop admin hostmasks/;
if (grep { $_ eq $key } @disallowed) {
return "The $key metadata requires the admin capability to set, which your user account does not have.";
}
}
}
} else {
$result = "Usage: my <key> [value]; ";
}
if ($key eq 'password' and defined $value) {
$value = $self->{pbot}->{users}->digest_password($value);
}
$result .= $self->{pbot}->{users}->{storage}->set($name, $key, $value);
$result =~ s/^password: .*;?$/password: <private>;/m;
return $result;
}
sub cmd_id($self, $context) {
my $target = length $context->{arguments} ? $context->{arguments} : $context->{nick};
my ($message_account, $hostmask);
if ($target =~ m/^\d+$/) {
$hostmask = $self->{pbot}->{messagehistory}->{database}->find_message_account_by_id($target);
return "I don't know anybody with id $target." if not $hostmask;
$message_account = $target;
} elsif ($target =~ m/[!@]/) {
my @accounts = $self->{pbot}->{messagehistory}->{database}->find_message_accounts_by_mask($target, 20);
my %seen;
@accounts = grep !$seen{$_}++, @accounts;
if (not @accounts) {
return "I don't know anybody matching hostmask $target.";
} elsif (@accounts > 1) {
# found more than one account, list them
my @hostmasks;
foreach my $account (@accounts) {
my $hostmask = $self->{pbot}->{messagehistory}->{database}->find_most_recent_hostmask($account);
push @hostmasks, "$hostmask ($account)";
}
return "Found multiple accounts: " . (join ', ', sort @hostmasks);
} else {
# found just one account, we'll use it
$message_account = $accounts[0];
$hostmask = $self->{pbot}->{messagehistory}->{database}->find_most_recent_hostmask($accounts[0]);
}
} else {
($message_account, $hostmask) = $self->{pbot}->{messagehistory}->{database}->find_message_account_by_nick($target);
return "I don't know anybody named $target." if not $message_account;
}
my $ancestor_id = $self->{pbot}->{messagehistory}->{database}->get_ancestor_id($message_account);
my $nickserv = $self->{pbot}->{messagehistory}->{database}->get_current_nickserv_account($message_account);
my ($u, $name) = $self->{pbot}->{users}->find_user($context->{from}, $hostmask, 1);
my $result = "$target ($hostmask): user id: $message_account; ";
if ($message_account != $ancestor_id) {
my $ancestor_hostmask = $self->{pbot}->{messagehistory}->{database}->find_message_account_by_id($ancestor_id);
$ancestor_hostmask = 'undefined' if not $ancestor_hostmask;
$result .= "parent user id: $ancestor_id ($ancestor_hostmask); ";
}
if (defined $u) {
$result .= "user account: $name (";
$result .= ($u->{loggedin} ? "logged in" : "not logged in") . '); ';
}
if (defined $nickserv and length $nickserv) {
$result .= "NickServ: $nickserv";
}
return $result;
}
1;