MT#59449 add progressive user bans support

* users are now progressively banned.
* ban_min_time is used to ban a user for the first time.
* consecutive ban is ban_min_time + ban_increment * ban_increment_stage
* ban_max_time is the absolute maximum ban time that is not increased
  any further.
* a successful login resets the ban_increment_stage.

Change-Id: I4d7e1a93d7a21d21a0dcf69d856a872d2ed75ea0
mr13.0
Kirill Solomko 1 year ago
parent f383837ea9
commit 2972d3b62d

@ -526,14 +526,17 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') {
return;
}
my $banned = NGCP::Panel::Utils::Auth::user_is_banned($c, $user, $ngcp_realm);
my $log_user = $user || '%-jwt-%';
my $log_user_id = '';
my $banned = NGCP::Panel::Utils::Auth::user_is_banned($c, $log_user, $ngcp_realm);
if ($banned) {
my $ip = $c->request->address;
$c->response->status(HTTP_FORBIDDEN);
$c->response->body(encode_json({
code => HTTP_FORBIDDEN,
message => "Forbidden!" })."\n");
$c->log->debug("Banned user=$user realm=$ngcp_realm ip=$ip login attempt");
$c->log->debug("Banned user=$log_user realm=$ngcp_realm ip=$ip login attempt");
return;
}
@ -548,10 +551,10 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') {
code => HTTP_FORBIDDEN,
message => "Forbidden!" })."\n");
$c->log->info("Invalid JWT");
NGCP::Panel::Utils::Auth::log_failed_login_attempt($c, $user, $ngcp_realm);
NGCP::Panel::Utils::Auth::log_failed_login_attempt($c, $log_user, $ngcp_realm);
return;
}
$auth_user = $c->user;
$log_user = $auth_user = $c->user;
} elsif ($auth_token) {
my $redis = $c->redis_get_connection({database => $c->config->{'Plugin::Session'}->{redis_db}});
unless ($redis) {
@ -573,40 +576,30 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') {
code => HTTP_FORBIDDEN,
message => "Forbidden!" })."\n");
$c->log->info("Unknown auth_token");
NGCP::Panel::Utils::Auth::log_failed_login_attempt($c, $user, $ngcp_realm);
NGCP::Panel::Utils::Auth::log_failed_login_attempt($c, $log_user, $ngcp_realm);
return;
}
$redis->del("auth_token:$auth_token") if $type eq 'onetime';
if ($ngcp_realm eq 'admin') {
unless (grep {$role eq $_} qw/admin reseller ccare ccareadmin/) {
$c->response->status(HTTP_FORBIDDEN);
$c->response->body(encode_json({
code => HTTP_FORBIDDEN,
message => "Forbidden!" })."\n");
$c->log->info("Wrong auth_token role");
NGCP::Panel::Utils::Auth::log_failed_login_attempt($c, $user, $ngcp_realm);
return;
}
my $authrs = $c->model('DB')->resultset('admins')->search({
id => $user_id,
is_active => 1,
});
$auth_user = $authrs->first if ($authrs->first);
} else {
unless (grep {$role eq $_} qw/subscriber subscriberadmin/) {
$log_user = $auth_user = $authrs->first if ($authrs->first);
unless (grep {$role eq $_} qw/admin reseller ccare ccareadmin/) {
$c->response->status(HTTP_FORBIDDEN);
$c->response->body(encode_json({
code => HTTP_FORBIDDEN,
message => "Forbidden!" })."\n");
$c->log->info("Wrong auth_token role");
NGCP::Panel::Utils::Auth::log_failed_login_attempt($c, $user, $ngcp_realm);
NGCP::Panel::Utils::Auth::log_failed_login_attempt($c, $log_user, $ngcp_realm);
return;
}
} else {
my $authrs = $c->model('DB')->resultset('provisioning_voip_subscribers')->search({
'me.id' => $user_id,
'voip_subscriber.status' => 'active',
@ -615,7 +608,17 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') {
join => ['contract', 'voip_subscriber'],
});
$auth_user = $authrs->first if ($authrs->first);
$log_user = $auth_user = $authrs->first if ($authrs->first);
unless (grep {$role eq $_} qw/subscriber subscriberadmin/) {
$c->response->status(HTTP_FORBIDDEN);
$c->response->body(encode_json({
code => HTTP_FORBIDDEN,
message => "Forbidden!" })."\n");
$c->log->info("Wrong auth_token role");
NGCP::Panel::Utils::Auth::log_failed_login_attempt($c, $log_user, $ngcp_realm);
return;
}
}
} else {
unless ($user && $pass) {
@ -636,17 +639,7 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') {
return;
}
my ($u, $d, $t) = split(/\@/, $user, 3);
if(defined $t) {
# in case username is an email address
$u = $u . '@' . $d;
$d = $t;
}
unless(defined $d) {
$d = $c->req->uri->host;
}
my ($u, $d) = NGCP::Panel::Utils::Auth::get_user_domain($c, $user);
if ($ngcp_realm eq 'admin') {
my $authrs = $c->model('DB')->resultset('admins')->search({
@ -667,7 +660,7 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') {
code => HTTP_FORBIDDEN,
message => "User not found" })."\n");
$c->log->info("User not found");
NGCP::Panel::Utils::Auth::log_failed_login_attempt($c, $user, $ngcp_realm);
NGCP::Panel::Utils::Auth::log_failed_login_attempt($c, $log_user, $ngcp_realm, $d);
return;
}
} else {
@ -726,8 +719,6 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') {
my $result = {};
my $log_user = '';
my $log_user_id = '';
if ($ngcp_realm eq 'admin') {
if ($auth_user) {
my $jwt_data = {
@ -749,7 +740,7 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') {
code => HTTP_FORBIDDEN,
message => "User not found" })."\n");
$c->log->info("User not found");
NGCP::Panel::Utils::Auth::log_failed_login_attempt($c, $user, $ngcp_realm);
NGCP::Panel::Utils::Auth::log_failed_login_attempt($c, $log_user, $ngcp_realm);
return;
}
$log_user = $auth_user->login;
@ -775,13 +766,14 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') {
code => HTTP_FORBIDDEN,
message => "User not found" })."\n");
$c->log->info("User not found");
NGCP::Panel::Utils::Auth::log_failed_login_attempt($c, $user, $ngcp_realm);
NGCP::Panel::Utils::Auth::log_failed_login_attempt($c, $log_user, $ngcp_realm);
return;
}
$log_user = $auth_user->webusername;
$log_user_id = $auth_user->uuid;
}
NGCP::Panel::Utils::Auth::clear_failed_login_attempts($c, $user, $ngcp_realm);
NGCP::Panel::Utils::Auth::clear_failed_login_attempts($c, $log_user, $ngcp_realm);
NGCP::Panel::Utils::Auth::reset_ban_increment_stage($c, $log_user, $ngcp_realm);
$c->log->debug(sprintf '%s JWT token for user=%s id=%s realm=%s expires_in_secs=%d',
$jwt ? 'Re-issue' : 'Issue',

@ -113,8 +113,11 @@ sub perform_auth {
}
}
$res ? clear_failed_login_attempts($c, $user, $realm)
: log_failed_login_attempt($c, $user, $realm);
$res ? do {
clear_failed_login_attempts($c, $user, 'admin');
reset_ban_increment_stage($c, $user, 'admin');
}
: log_failed_login_attempt($c, $user, 'admin');
return $res;
}
@ -137,7 +140,7 @@ sub perform_subscriber_auth {
return $res;
}
my $userdom = "$user:$domain";
my $userdom = $user . '@' . $domain;
return $res if user_is_banned($c, $userdom, 'subscriber');
my $authrs = $c->model('DB')->resultset('provisioning_voip_subscribers')->search({
@ -220,8 +223,11 @@ sub perform_subscriber_auth {
}
}
$res ? clear_failed_login_attempts($c, $userdom, 'subscriber')
: log_failed_login_attempt($c, $userdom, 'subscriber');
$res ? do {
clear_failed_login_attempts($c, $userdom, 'subscriber');
reset_ban_increment_stage($c, $userdom, 'subscriber');
}
: log_failed_login_attempt($c, $userdom, 'subscriber');
return $res;
}
@ -490,13 +496,31 @@ sub generate_auth_token {
return $uuid_string;
}
sub get_user_domain {
my ($c, $user) = @_;
my ($p_user, $p_domain, $t) = split(/\@/, $user, 3);
if (defined $t) {
# in case username is an email address
$p_user = $p_user . '@' . $p_domain;
$p_domain = $t;
}
unless(defined $p_domain) {
$p_domain = $c->req->uri->host;
}
return ($p_user, $p_domain);
}
sub user_is_banned {
my ($c, $user, $realm) = @_;
my $ip = $c->request->address;
my $redis = $c->redis_get_connection({database => $c->config->{'Plugin::Session'}->{redis_db}});
my $key = "login:ban:$user:$realm:$ip";
my ($p_user, $p_domain) = get_user_domain($c, $user);
my $key = "login:ban:$p_user:$p_domain:$realm:$ip";
return $redis->exists($key) ? 1 : 0;
}
@ -505,13 +529,15 @@ sub log_failed_login_attempt {
my ($c, $user, $realm) = @_;
return unless $c->config->{security}{login}{ban_enable};
my $expire = $c->config->{security}{login}{ban_expire_time} // 0;
my $expire = $c->config->{security}{login}{ban_max_time} // 3600;
my $max_attempts = $c->config->{security}{login}{max_attempts} // return;
my $ip = $c->request->address;
my $redis = $c->redis_get_connection({database => $c->config->{'Plugin::Session'}->{redis_db}});
my $key = "login:fail:$user:$realm:$ip";
my ($p_user, $p_domain) = get_user_domain($c, $user);
my $key = "login:fail:$p_user:$p_domain:$realm:$ip";
my $attempted = ($redis->hget($key, 'attempts') // 0) + 1;
$attempted >= $max_attempts
? ban_user($c, $user, $realm)
@ -529,8 +555,10 @@ sub log_failed_login_attempt {
sub clear_failed_login_attempts {
my ($c, $user, $realm) = @_;
my ($p_user, $p_domain) = get_user_domain($c, $user);
my $ip = $c->request->address;
my $key = "login:fail:$user:$realm:$ip";
my $key = "login:fail:$p_user:$p_domain:$realm:$ip";
my $redis = $c->redis_get_connection({database => $c->config->{'Plugin::Session'}->{redis_db}});
@ -539,21 +567,86 @@ sub clear_failed_login_attempts {
return;
}
sub ban_user {
sub reset_ban_increment_stage {
my ($c, $user, $realm) = @_;
my ($p_user, $p_domain) = get_user_domain($c, $user);
my $usr_rs;
if ($realm eq 'admin') {
$usr_rs = $c->model('DB')->resultset('admins')->search({
login => $p_user,
})->first;
} elsif ($realm eq 'subscriber') {
$usr_rs = $c->model('DB')->resultset('provisioning_voip_subscribers')->search({
webusername => $p_user,
'domain.domain' => $p_domain,
}, {
join => 'domain',
})->first;
}
if ($usr_rs) {
my $ip = $c->request->address;
$c->log->debug("Reset ban increment for user=$p_user domain=$p_domain realm=$realm ip=$ip");
$usr_rs->update({ban_increment_stage => 0});
}
return;
}
sub ban_user {
my ($c, $user, $realm, $domain) = @_;
return unless $c->config->{security}{login}{ban_enable};
my $expire = $c->config->{security}{login}{ban_expire_time} // 0;
my $min_time = $c->config->{security}{login}{ban_min_time} // 300;
my $max_time = $c->config->{security}{login}{ban_max_time} // 3600;
my $increment = $c->config->{security}{login}{ban_increment} // 300;
my ($p_user, $p_domain) = get_user_domain($c, $user);
my $ip = $c->request->address;
my $key = "login:ban:$user:$realm:$ip";
my $key = "login:ban:$p_user:$p_domain:$realm:$ip";
my $increment_stage = -1;
my $expire = 3600;
my $usr_rs;
if ($realm eq 'admin') {
$usr_rs = $c->model('DB')->resultset('admins')->search({
login => $p_user,
})->first;
if ($usr_rs) {
$increment_stage = $usr_rs->ban_increment_stage;
}
} elsif ($realm eq 'subscriber') {
$usr_rs = $c->model('DB')->resultset('provisioning_voip_subscribers')->search({
webusername => $p_user,
'domain.domain' => $p_domain,
},{
join => 'domain',
})->first;
if ($usr_rs) {
$increment_stage = $usr_rs->ban_increment_stage;
}
}
$c->log->info("Ban user=$user realm=$realm ip=$ip for $expire seconds");
if ($increment_stage >= 0) {
$expire = $min_time + $increment*$increment_stage;
$expire = $max_time if $expire > $max_time;
$increment_stage++;
}
$c->log->info("Ban user=$p_user domain=$p_domain realm=$realm ip=$ip stage=$increment_stage for $expire seconds");
my $redis = $c->redis_get_connection({database => $c->config->{'Plugin::Session'}->{redis_db}});
$redis->hset($key, 'banned_at', time());
$redis->expire($key, $expire) if $expire;
if ($increment_stage > 0 && $usr_rs) {
$usr_rs->update({ban_increment_stage => $increment_stage});
}
clear_failed_login_attempts($c, $user, $realm);
return;

Loading…
Cancel
Save