diff options
| author | 2017-05-08 02:06:38 +0000 | |
|---|---|---|
| committer | 2017-05-08 02:25:18 +0000 | |
| commit | 7342c7f0e19e15ab3c7ba2133a56393c15989f08 (patch) | |
| tree | 5e00a9c2a3164bd49d18ded9263f1ea7ae99d7f6 /lib/ASM | |
| parent | 62d8fcde1c6dc5be2f535f6fcffa1afd838b62bd (diff) | |
Move users to DB
Summary:
This will automatically take care of migrating the users from
users.json; you may delete that file.
Note that this removes htpasswd support. We now store (hashed) user
passwords in the database.
See T19 for rationale.
Test Plan: Run this on a testnet for a while, try to break it.
Reviewers: ilbelkyr, #antispammeta
Reviewed By: ilbelkyr, #antispammeta
Tags: #antispammeta, #database
Differential Revision: https://dev.antispammeta.net/D2
Diffstat (limited to 'lib/ASM')
| -rw-r--r-- | lib/ASM/Commander.pm | 338 | ||||
| -rw-r--r-- | lib/ASM/Config.pm | 7 | ||||
| -rw-r--r-- | lib/ASM/DB.pm | 2 | ||||
| -rw-r--r-- | lib/ASM/DB/Result/User.pm | 34 | ||||
| -rw-r--r-- | lib/ASM/DB/ResultSet/User.pm | 23 |
5 files changed, 238 insertions, 166 deletions
diff --git a/lib/ASM/Commander.pm b/lib/ASM/Commander.pm index edad532..6de19fd 100644 --- a/lib/ASM/Commander.pm +++ b/lib/ASM/Commander.pm @@ -1,6 +1,7 @@ package ASM::Commander; no autovivification; +use v5.10; use warnings; use strict; use IO::All; @@ -8,45 +9,60 @@ use POSIX qw(strftime); use Data::Dumper; use URI::Escape; use ASM::Shortener; +use Const::Fast; no if $] >= 5.017011, warnings => 'experimental::smartmatch'; +const my $secret => 'flag_secret'; +const my $hilights => 'flag_hilights'; +const my $admin => 'flag_admin'; +const my $plugin => 'flag_plugin'; +const my $debug => 'flag_debug'; + +const my %letter_to_flag => ( + s => $secret, + h => $hilights, + a => $admin, + p => $plugin, + d => $debug, +); + +const my %flag_to_letter => reverse(%letter_to_flag); + my $cmdtbl = { '^;wallop' => { - 'flag' => 'd', + 'flag' => $debug, 'cmd' => \&cmd_wallop }, '^;;addwebuser (?<pass>.{6,})' => { - 'flag' => 's', + 'flag' => $secret, + 'txn' => 1, 'cmd' => \&cmd_addwebuser }, - '^;delwebuser (?<user>\S+)' => { - 'flag' => 'a', - 'cmd' => \&cmd_delwebuser }, '^;teredo (?<ip>\S+)' => { 'cmd' => \&cmd_teredo }, '^;status$' => { 'cmd' => \&cmd_status }, '^;mship (?<nick>\S+)' => { - 'flag' => 's', + 'flag' => $secret, 'cmd' => \&cmd_mship }, '^;source$' => { 'cmd' => \&cmd_source }, '^;monitor (?<chan>\S+) *$' => { - 'flag' => 's', + 'flag' => $secret, 'cmd' => \&cmd_monitor }, '^;monitor (?<chan>\S+) ?(?<switch>yes|no)$' => { - 'flag' => 'a', + 'flag' => $admin, 'cmd' => \&cmd_monitor2 }, '^;suppress (?<chan>\S+) *$' => { - 'flag' => 's', + 'flag' => $secret, 'cmd' => \&cmd_suppress }, '^;unsuppress (?<chan>\S+) *$' => { - 'flag' => 's', + 'flag' => $secret, 'cmd' => \&cmd_unsuppress }, '^;silence (?<chan>\S+) *$' => { - 'flag' => 's', + 'flag' => $secret, 'cmd' => \&cmd_silence }, '^;silence (?<chan>\S+) (?<switch>yes|no) *$' => { - 'flag' => 'a', + 'flag' => $admin, 'cmd' => \&cmd_silence2 }, '^;help$' => { 'cmd' => \&cmd_help }, @@ -57,89 +73,88 @@ my $cmdtbl = { '^;query (\S+) ?(\S+)?$' => { 'cmd' => \&cmd_query }, '^;investigate (?<nick>\S+) *$' => { - 'flag' => 's', + 'flag' => $secret, 'cmd' => \&cmd_investigate }, '^;investigate2 (?<nick>\S+) ?(?<skip>\d*) *$' => { - 'flag' => 's', + 'flag' => $secret, 'cmd' => \&cmd_investigate2 }, - '^;userx? add (?<account>\S+) (?<flags>\S+)$' => { - 'flag' => 'a', - 'cmd' => \&cmd_user_add }, + '^;userx? (?:add|flags) (?<account>\S+) (?<flags>\S+)$' => { + 'flag' => $admin, + 'txn' => 1, + 'cmd' => \&cmd_user_set_flags }, '^;userx? flags (?<account>\S+) ?$' => { - 'cmd' => \&cmd_user_flags }, - '^;userx? flags (?<account>\S+) (?<flags>\S+)$' => { - 'flag' => 'a', - 'cmd' => \&cmd_user_flags2 }, + 'cmd' => \&cmd_user_get_flags }, '^;userx? del (?<account>\S+)$' => { - 'flag' => 'a', + 'flag' => $admin, + 'txn' => 1, 'cmd' => \&cmd_user_del }, '^;target (?<chan>\S+) (?<nickchan>\S+) ?(?<level>[a-z]*)$' => { - 'flag' => 'a', + 'flag' => $admin, 'cmd' => \&cmd_target }, '^;detarget (?<chan>\S+) (?<nickchan>\S+)' => { - 'flag' => 'a', + 'flag' => $admin, 'cmd' => \&cmd_detarget }, '^;showhilights (?<nick>\S+) *$' => { - 'flag' => 'h', + 'flag' => $hilights, 'cmd' => \&cmd_showhilights }, '^;hilight (?<chan>\S+) (?<nicks>\S+) ?(?<level>[a-z]*)$' => { - 'flag' => 'h', + 'flag' => $hilights, 'cmd' => \&cmd_hilight }, '^;dehilight (?<chan>\S+) (?<nicks>\S+)' => { - 'flag' => 'h', + 'flag' => $hilights, 'cmd' => \&cmd_dehilight }, '^;join (?<chan>\S+)' => { - 'flag' => 'a', + 'flag' => $admin, 'cmd' => \&cmd_join }, '^;part (?<chan>\S+)' => { - 'flag' => 'a', + 'flag' => $admin, 'cmd' => \&cmd_part }, '^;sl (?<string>.+)' => { - 'flag' => 'd', + 'flag' => $debug, 'cmd' => \&cmd_sl }, '^;quit ?(?<reason>.*)' => { - 'flag' => 'a', + 'flag' => $admin, 'cmd' => \&cmd_quit }, '^;ev (?<string>.*)' => { - 'flag' => 'd', + 'flag' => $debug, 'cmd' => \&cmd_ev }, '^;rehash$' => { - 'flag' => 'a', + 'flag' => $admin, 'cmd' => \&cmd_rehash }, '^;restrict (?<type>nick|account|host) (?<who>\S+) (?<mode>\+|-)(?<restriction>[a-z0-9_-]+)$' => { - 'flag' => 'a', + 'flag' => $admin, 'cmd' => \&cmd_restrict }, '^\s*\!ops ?(?<chan>#\S+)? ?(?<reason>.*)' => { 'nohush' => 'nohush', 'cmd' => \&cmd_ops }, '^;blacklist (?<string>.+)' => { - 'flag' => 's', + 'flag' => $secret, 'cmd' => \&cmd_blacklist }, '^;blacklistpcre (?<string>.+)' => { - 'flag' => 'a', + 'flag' => $admin, 'cmd' => \&cmd_blacklistpcre }, '^;unblacklist (?<id>[0-9a-f]+)$' => { - 'flag' => 's', + 'flag' => $secret, 'cmd' => \&cmd_unblacklist }, '^;plugin (?<chan>\S+) (?<risk>\S+) (?<reason>.*)' => { - 'flag' => 'p', + 'flag' => $plugin, 'cmd' => \&cmd_plugin }, '^;sync (?<chan>\S+)' => { - 'flag' => 'a', + 'flag' => $admin, 'cmd' => \&cmd_sync }, '^;ping\s*$' => { 'cmd' => \&cmd_ping }, '^;ping (?<string>\S.*)$' => { - 'flag' => 's', + 'flag' => $secret, 'cmd' => \&cmd_ping2 }, '^;blreason (?<id>[0-9a-f]+) (?<reason>.*)' => { - 'flag' => 's', + 'flag' => $secret, 'cmd' => \&cmd_blreason }, '^;bllookup (?<id>[0-9a-f]+)$' => { - 'flag' => 's', + 'flag' => $secret, 'cmd' => \&cmd_bllookup }, '^;falsematch\b' => { - 'flag' => 's', + 'flag' => $secret, 'cmd' => \&cmd_falsematch }, '^;version$' => { 'cmd' => \&cmd_version }, @@ -174,34 +189,50 @@ sub command { unless ( (ASM::Util->speak($event->{to}->[0])) ) { next unless (defined($self->{cmdtbl}->{$command}->{nohush})); } - if (defined($self->{cmdtbl}->{$command}->{flag})) { #If the command is restricted, - if (!defined($acct)) { - $fail = 1; - } - elsif (!defined($::users->{person}->{$acct})) { #make sure the requester has an account - $fail = 1; - } - elsif (!defined($::users->{person}->{$acct}->{flags})) { #make sure the requester has flags defined - $fail = 1; - } - elsif (!(grep {$_ eq $self->{cmdtbl}->{$command}->{flag}} split('', $::users->{person}->{$acct}->{flags}))) { #make sure the requester has the needed flags - $fail = 1; - } - } if ($cmd=~/$command/) { my $where = $event->{to}[0]; if (index($where, '#') == -1) { $where = 'PM'; } ASM::Util->dprint("$event->{from} told me in $where: $cmd", "commander"); + if (!ASM::Util->notRestricted($nick, "nocommands")) { $fail = 1; } - if ($fail == 1) { - $conn->privmsg($nick, "You don't have permission to use that command, or you're not signed into nickserv."); - } else { - &{$self->{cmdtbl}->{$command}->{cmd}}($conn, $event); + + my $check_and_run_command = sub { + # If the command is restricted, + if ( my $flag = $self->{cmdtbl}->{$command}->{flag} ) { + # require an account + if (!defined($acct)) { + $fail = 1; + } + else { + # and check for the flag + my $user = $::db->resultset('User')->by_name($acct); + if (!defined $user || !$user->$flag) { + $fail = 1; + } + } + } + + if ($fail == 1) { + $conn->privmsg($nick, "You don't have permission to use that command, or you're not signed into nickserv."); + } else { + &{$self->{cmdtbl}->{$command}->{cmd}}($conn, $event); + } + }; + + # Do we need to wrap the entire command - including the permission check - in a transaction? + # Be careful; due to re-establishing a DB connection, this requires the command's code to + # be idempotent. See the DBIx::Class::Storage documentation on the txn_do method for details. + if ($self->{cmdtbl}{$command}{txn}) { + $::db->txn_do($check_and_run_command); } + else { + $check_and_run_command->(); + } + last; } } @@ -234,30 +265,12 @@ sub cmd_addwebuser { $conn->privmsg($event->replyto, "This command must be used in PM. Try again WITH A DIFFERENT PASSWORD!"); return; } - use Apache::Htpasswd; use Apache::Htgroup; - my $o_Htpasswd = new Apache::Htpasswd({passwdFile => $::settings->{web}->{userfile}, UseMD5 => 1}); - my $o_Htgroup = new Apache::Htgroup($::settings->{web}->{groupfile}); - my $user = lc $::sn{lc $event->{nick}}->{account}; - $o_Htpasswd->htDelete($user); - $o_Htpasswd->htpasswd($user, $pass); - $o_Htpasswd->writeInfo($user, strftime("%F %T", gmtime)); - $o_Htgroup->adduser($user, 'actionlogs'); - $o_Htgroup->save(); - $conn->privmsg($event->replyto, "Added $user to the list of authorized web users.") -} - -sub cmd_delwebuser { - my ($conn, $event) = @_; + my $user = $::db->resultset('User')->by_name(lc $::sn{lc $event->{nick}}->{account}); + $user->passphrase($pass); + $user->update; - my $user = lc $+{user}; - use Apache::Htpasswd; - use Apache::Htgroup; - my $o_Htpasswd = new Apache::Htpasswd({passwdFile => $::settings->{web}->{userfile}, UseMD5 => 1}); - my $o_Htgroup = new Apache::Htgroup($::settings->{web}->{groupfile}); - $o_Htpasswd->htDelete($user); - $o_Htgroup->deleteuser($user, 'actionlogs'); - $o_Htgroup->save(); - $conn->privmsg($event->replyto, "Removed $user from the list of authorized web users.") + my $name = $user->name; + $conn->privmsg($event->replyto, "Added $name to the list of authorized web users.") } sub cmd_teredo { @@ -594,40 +607,89 @@ sub cmd_investigate2 { $conn->privmsg($event->nick, "Only 10 results are shown at a time. For more, do ;investigate2 $nick " . ($skip+1) . '.'); } -sub cmd_user_add { +sub get_user_flagstring { + my ($user) = @_; + + my $string = ''; + + for my $letter (sort keys %letter_to_flag) { + my $flag = $letter_to_flag{$letter}; + + $string .= $letter if $user->$flag; + } + + return $string; +} + +sub set_user_flagstring { + my ($user, $string) = @_; + + while (my ($letter, $flag) = each %letter_to_flag) { + if (index($string, $letter) != -1) { + $user->$flag(1); + } + else { + $user->$flag(0); + } + } +} + +sub is_flagstring_superset { + my ($super, $sub) = @_; + for my $letter (split //, $sub) { + return 0 if index($super, $letter) == -1; + } + return 1; +} + +sub cmd_user_set_flags { my ($conn, $event) = @_; my $nick = lc $+{account}; my $account; my $flags = $+{flags}; - my %hasflagshash = (); - foreach my $item (split(//, $::users->{person}->{lc $::sn{lc $event->{nick}}->{account}}->{flags})) { - $hasflagshash{$item} = 1; + + # we need to be idempotent if interrupted halfway. + # TODO: this is rather ugly / error-prone. + state $sent_message = 0; + + if ( (defined($::sn{$nick}->{account})) && ( ($account = lc $::sn{$nick}->{account}) ne $nick ) ) { + $conn->privmsg($event->replyto, "I'm assuming you mean " . $nick . "'s nickserv account, " . $account . '.') + if !($sent_message++); + $nick = $account; } - foreach my $flag (split(//, $flags)) { - if (!defined($hasflagshash{$flag})) { - $conn->privmsg($event->replyto, "You can't give a flag you don't already have."); - return; - } + + my $giver = $::db->resultset('User')->by_name( lc $::sn{lc $event->{nick}}{account} ); + + my $own_flags = get_user_flagstring($giver); + + if (!is_flagstring_superset($own_flags, $flags)) { + $conn->privmsg($event->replyto, "You can't give a flag you don't already have."); + $sent_message = 0; + return; } if ($flags =~ /d/) { - $conn->privmsg($event->replyto, "The d flag may not be assigned over IRC. Edit the configuration manually."); + $conn->privmsg($event->replyto, "The d flag may not be assigned over IRC. Edit the database manually."); + $sent_message = 0; return; } - if ( (defined($::sn{$nick}->{account})) && ( ($account = lc $::sn{$nick}->{account}) ne $nick ) ) { - $conn->privmsg($event->replyto, "I'm assuming you mean " . $nick . "'s nickserv account, " . $account . '.'); - $nick = $account; - } - if (defined($::users->{person}->{$nick})) { - $conn->privmsg($event->replyto, "The user $nick already exists. Use ;user flags $nick $flags to set their flags"); + + my $target = $::db->resultset('User')->by_name_or_new( $nick ); + + if ($target->flag_debug) { + $conn->privmsg($event->replyto, "Users with the 'd' flag are untouchable. Edit the database manually."); + $sent_message = 0; return; } - $::users->{person}->{$nick} = { 'flags' => $flags }; - ASM::Config->writeUsers(); + + set_user_flagstring($target, $flags); + $target->update_or_insert; + + $sent_message = 0; $conn->privmsg($event->replyto, "Flags for NickServ account $nick set to $flags"); } -sub cmd_user_flags { +sub cmd_user_get_flags { my ($conn, $event) = @_; my $nick = lc $+{account}; @@ -637,72 +699,32 @@ sub cmd_user_flags { $nick = $account; } my $sayNick = substr($nick, 0, 1) . "\x02\x02" . substr($nick, 1); - if (defined($::users->{person}->{$nick}->{flags})) { - $conn->privmsg($event->replyto, "Flags for $sayNick: $::users->{person}->{$nick}->{flags}"); - } else { - $conn->privmsg($event->replyto, "$sayNick has no flags"); - } -} -sub cmd_user_flags2 { - my ($conn, $event) = @_; + my $user = $::db->resultset('User')->by_name($nick); - my $nick = lc $+{account}; - my $flags = $+{flags}; - my $account; - my %hasflagshash = (); - foreach my $item (split(//, $::users->{person}->{lc $::sn{lc $event->{nick}}->{account}}->{flags})) { - $hasflagshash{$item} = 1; + if (defined $user and length( my $flags = get_user_flagstring($user) )) { + $conn->privmsg($event->replyto, "Flags for $sayNick: $flags"); } - foreach my $flag (split(//, $flags)) { - if (!defined($hasflagshash{$flag})) { - $conn->privmsg($event->replyto, "You can't give a flag you don't already have."); - return; - } - } - if ($flags =~ /d/) { - $conn->privmsg($event->replyto, "The d flag may not be assigned over IRC. Edit the configuration manually."); - return; - } - if ( (defined($::sn{$nick}->{account})) && ( ($account = lc $::sn{$nick}->{account}) ne $nick ) ) { - $conn->privmsg($event->replyto, "I'm assuming you mean " . $nick . "'s nickserv account, " . $account . '.'); - $nick = $account; - } - if (defined($::users->{person}->{$nick}) && - defined($::users->{person}->{$nick}->{flags}) && - ($::users->{person}->{$nick}->{flags} =~ /d/)) { - return $conn->privmsg($event->replyto, "Users with the 'd' flag are untouchable. Edit the config file manually."); - } - if ($flags !~ /s/) { - use Apache::Htpasswd; use Apache::Htgroup; - my $o_Htpasswd = new Apache::Htpasswd({passwdFile => $::settings->{web}->{userfile}, UseMD5 => 1}); - my $o_Htgroup = new Apache::Htgroup($::settings->{web}->{groupfile}); - $o_Htpasswd->htDelete($nick); - $o_Htgroup->deleteuser($nick, 'actionlogs'); - $o_Htgroup->save(); + else { + $conn->privmsg($event->replyto, "$sayNick has no flags"); } - $::users->{person}->{$nick}->{flags} = $flags; - ASM::Config->writeUsers(); - $conn->privmsg($event->replyto, "Flags for $nick set to $flags"); } sub cmd_user_del { my ($conn, $event) = @_; my $nick = lc $+{account}; - if (defined($::users->{person}->{$nick}) && - defined($::users->{person}->{$nick}->{flags}) && - ($::users->{person}->{$nick}->{flags} =~ /d/)) { - return $conn->privmsg($event->replyto, "Users with the 'd' flag are untouchable. Edit the config file manually."); + + my $target = $::db->resultset('User')->by_name($nick); + if (!defined $target) { + $conn->privmsg($event->replyto, "I know no user by that name. Make sure you specified the account name."); + return; + } + if ($target->flag_debug) { + $conn->privmsg($event->replyto, "Users with the 'd' flag are untouchable. Edit the database manually."); + return; } - delete($::users->{person}->{$nick}); - ASM::Config->writeUsers(); - use Apache::Htpasswd; use Apache::Htgroup; - my $o_Htpasswd = new Apache::Htpasswd({passwdFile => $::settings->{web}->{userfile}, UseMD5 => 1}); - my $o_Htgroup = new Apache::Htgroup($::settings->{web}->{groupfile}); - $o_Htpasswd->htDelete($nick); - $o_Htgroup->deleteuser($nick, 'actionlogs'); - $o_Htgroup->save(); + $target->delete; $conn->privmsg($event->replyto, "Removed $nick from authorized users." . " MAKE SURE YOU PROVIDED a nickserv account to this command, rather than an altnick of the accountholder"); } diff --git a/lib/ASM/Config.pm b/lib/ASM/Config.pm index ff6f745..8e336a3 100644 --- a/lib/ASM/Config.pm +++ b/lib/ASM/Config.pm @@ -19,7 +19,6 @@ sub deserialize { sub readConfig { $::settings = deserialize(io->file("$::cset/settings.json")->all); $::channels = deserialize(io->file("$::cset/channels.json")->all); - $::users = deserialize(io->file("$::cset/users.json")->all); $::mysql = deserialize(io->file("$::cset/mysql.json")->all); $::dnsbl = deserialize(io->file("$::cset/dnsbl.json")->all); $::rules = deserialize(io->file("$::cset/rules.json")->all); @@ -30,7 +29,6 @@ sub readConfig { sub writeConfig { writeMysql(); writeChannels(); - writeUsers(); writeSettings(); writeRestrictions(); writeBlacklist(); @@ -58,11 +56,6 @@ sub writeChannels { serialize($::channels) > io("$::cset/channels.json"); } -sub writeUsers { - $::settingschanged=1; - serialize($::users) > io("$::cset/users.json"); -} - sub writeSettings { $::settingschanged=1; serialize($::settings) > io("$::cset/settings.json"); diff --git a/lib/ASM/DB.pm b/lib/ASM/DB.pm index cc9906c..5e1d44d 100644 --- a/lib/ASM/DB.pm +++ b/lib/ASM/DB.pm @@ -1,5 +1,5 @@ use utf8; -package ASM::DB 3; +package ASM::DB 4; use strict; use warnings; diff --git a/lib/ASM/DB/Result/User.pm b/lib/ASM/DB/Result/User.pm index 5e5d17c..675ccba 100644 --- a/lib/ASM/DB/Result/User.pm +++ b/lib/ASM/DB/Result/User.pm @@ -6,6 +6,8 @@ use warnings; use parent 'DBIx::Class::Core'; +use Authen::Passphrase::RejectAll; + __PACKAGE__->load_components('InflateColumn::DateTime', 'PassphraseColumn'); __PACKAGE__->table('users'); __PACKAGE__->add_columns( @@ -30,9 +32,41 @@ __PACKAGE__->add_columns( passphrase_check_method => 'check_password', is_nullable => 0, }, + flag_secret => { + data_type => 'boolean', + is_nullable => 0, + default_value => 0, + }, + flag_hilights => { + data_type => 'boolean', + is_nullable => 0, + default_value => 0, + }, + flag_admin => { + data_type => 'boolean', + is_nullable => 0, + default_value => 0, + }, + flag_plugin => { + data_type => 'boolean', + is_nullable => 0, + default_value => 0, + }, + flag_debug => { + data_type => 'boolean', + is_nullable => 0, + default_value => 0, + }, ); __PACKAGE__->set_primary_key('id'); __PACKAGE__->add_unique_constraint(uniq_user_name => ['name']); +sub new { + my $self = shift; + $_[0]{passphrase} //= Authen::Passphrase::RejectAll->new; + + $self->SUPER::new(@_); +} + 1; diff --git a/lib/ASM/DB/ResultSet/User.pm b/lib/ASM/DB/ResultSet/User.pm new file mode 100644 index 0000000..68c0871 --- /dev/null +++ b/lib/ASM/DB/ResultSet/User.pm @@ -0,0 +1,23 @@ +use utf8; + +package ASM::DB::ResultSet::User; + +use strict; +use warnings; + +use parent 'DBIx::Class::ResultSet'; +use namespace::autoclean; + +sub by_name { + my ( $self, $name ) = @_; + + return $self->find( { name => $name }, { key => 'uniq_user_name' } ); +} + +sub by_name_or_new { + my ( $self, $name ) = @_; + + return $self->find_or_new( { name => $name }, { key => 'uniq_user_name' } ); +} + +1; |
