From 2c8e020628b68960d7af934a35e9e4df0cbd21fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olimpia=20Kwiecie=C5=84?= Date: Mon, 14 Feb 2022 19:42:11 +0100 Subject: [PATCH] feat(access-control): Add Access Control functionalities --- .../account-app/account-app.component.html | 10 +- src/app/account-app/account-app.module.ts | 2 +- .../account-welcome.component.html | 10 +- .../account.security.component.scss | 32 ++ .../account.security.module.ts | 4 + .../last-logins.component.html | 462 +++++++++++++----- .../account-security/last-logins.component.ts | 63 ++- src/app/rmm/account-security-acl.ts | 161 ++---- 8 files changed, 480 insertions(+), 264 deletions(-) diff --git a/src/app/account-app/account-app.component.html b/src/app/account-app/account-app.component.html index 1ddd54235..fac5c3a5a 100644 --- a/src/app/account-app/account-app.component.html +++ b/src/app/account-app/account-app.component.html @@ -148,11 +148,11 @@

Settings Menu

Manage Services

- + +

+ Access Control +

+

Sessions diff --git a/src/app/account-app/account-app.module.ts b/src/app/account-app/account-app.module.ts index b438b4874..386d7b751 100644 --- a/src/app/account-app/account-app.module.ts +++ b/src/app/account-app/account-app.module.ts @@ -250,7 +250,7 @@ import { DomainRegisterComponent } from '../domainregister/domainregister.compon component: ManageServicesComponent, }, { - path: 'last_logins', + path: 'access_control', component: LastLoginsComponent, }, { diff --git a/src/app/account-app/account-welcome.component.html b/src/app/account-app/account-welcome.component.html index 39080736e..5214e67c5 100644 --- a/src/app/account-app/account-welcome.component.html +++ b/src/app/account-app/account-welcome.component.html @@ -189,14 +189,14 @@

Enable or disable the email services on your account, such as IMAP, POP, and SMTP.

- +

diff --git a/src/app/account-security/account.security.component.scss b/src/app/account-security/account.security.component.scss index a25000b73..756fdc96d 100644 --- a/src/app/account-security/account.security.component.scss +++ b/src/app/account-security/account.security.component.scss @@ -215,3 +215,35 @@ h2.runbox-section-header { width: calc(100% - 20px); } } + +table { + width: 100%; + @media only screen and (max-width: 767px) { + th, + td { + padding-left: 0; + padding-right: 0; + } + } +} + +table .cell-center-content { + text-align: center; +} + +.container { + max-width: 1024px; + padding-top: 10px; + padding-bottom: 30px; +} + +.textLink { + color: #01579b; + cursor: pointer; +} + +.flex-vertical { + display: flex; + flex-direction: column; + align-items: flex-start; +} diff --git a/src/app/account-security/account.security.module.ts b/src/app/account-security/account.security.module.ts index 2e5d8681c..92ae4bcb9 100644 --- a/src/app/account-security/account.security.module.ts +++ b/src/app/account-security/account.security.module.ts @@ -52,6 +52,8 @@ import { AppPasswordsComponent } from './app-passwords.component'; import { LastLoginsComponent } from './last-logins.component'; import { SessionsComponent } from './sessions.component'; import { AccountPasswordComponent } from './account-password.component'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatPaginatorModule } from '@angular/material/paginator'; @NgModule({ declarations: [ @@ -89,6 +91,8 @@ import { AccountPasswordComponent } from './account-password.component'; MenuModule, QRCodeModule, RunboxComponentModule, + ReactiveFormsModule, + MatPaginatorModule, ], entryComponents: [ ModalPasswordComponent, diff --git a/src/app/account-security/last-logins.component.html b/src/app/account-security/last-logins.component.html index 99aa51d25..39c9ba3e6 100644 --- a/src/app/account-security/last-logins.component.html +++ b/src/app/account-security/last-logins.component.html @@ -1,139 +1,337 @@
- -
-

Last Logins

-
-
-
- - Service - - Any - - {{ rmm.account_security.service.services_translation[service.key].name }} - - - - - Status - - Any - Success - Failure - - - - Period - - 1 hour - 1 day - 1 week - 1 month - - - - Filter IPs - - None - Exclude IPs - Include IPs - - - - - -
- - -
+
+ +
+

Access Control

- -
- - -

Service

-
- -

Login

-
- -

IP

-
- -

Hostname

-
- -

Time

-
- -

Status

-
-
-
-
- -
-
- - - {{ rmm.account_security.service.services_translation[result.service].name }} - - - {{ result.login }} - - - {{ result.ip }} - - - {{ result.hostname || '-' }} - - - {{ result.created }} - - - {{ result.success === 1 ? 'success' : 'fail' }} - - - -
-
+
+

In this section you can review your last logins and access control lists, and manage the IP addresses + that should have access to your account.

+ We recommend that you read the instructions below before making any changes.

+ -
-
-
- Service: - {{ rmm.account_security.service.services_translation[result.service].name }} -
-
- Login: - {{ result.login }} -
-
- IP: - {{ result.ip }} -
-
- Hostname: - {{ result.hostname || '-' }} -
-
- Time: - {{ result.created }} -
-
- Status: - {{ result.success === 1 ? 'success' : 'fail' }} + +
+

Last Logins

+
+
+
+ + Service + + Any + + {{ rmm.account_security.service.services_translation[service.key].name }} + + + + + Status + + Any + Success + Failure + + + + Period + + 1 hour + 1 day + 1 week + 1 month + + + + Filter IPs + + None + Exclude IPs + Include IPs + + + + + +
+ +
-
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Service {{ login.service }} Login {{ login.login }} IP {{ login.ip }} Hostname {{ login.hostname || '-' }} Time {{ login.created }} Status {{ login.success === 1 ? 'success' : 'failed' }} Block
+ No results for this filter +
+
+ +
+ +
+ + +
+
+ + +
+

Access Control List

+
+
+

The IPs and IP ranges below indicate one or more IP addresses allowed or denied access to your + account. + Allowed IPs have higher priority than Denied IPs. Therefore you can allow an IP from within a range + that + you have otherwise denied.

+ + +

Rules are valid for accounts: {{(rmmapi.me | async)?.user_address}}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rule {{ rule.rule }} IP {{ rule.ip }} Label {{ rule.label }} Remove
+ No IPs allowed/denied +
+
+
+ + +
+

Manage IPs

+
+
+

Enter one specific IP (e.g. 1.2.3.4) or an IP range (e.g. 1-24.2.3.4-55 or 200.*.*.*), then choose + Allow + or Deny. Currently supports IPv4 only.

+ +

One common type of rule is to deny *.*.*.* (all IPs) and only allow for example 12.34.56.78, which + could + be your own IP or the IP of an Internet connection you want access from. Be careful so that you + don't + lock yourself out.

+ +

The rules below affect the following accounts/sub-accounts: {{(rmmapi.me | + async)?.user_address}}

+ +
+ + Rule + + Allow + Deny + + + + IP + + + + Label + + + +
+
+
+ + +
+

Blocked IPs

+
+
+

The list below shows currently blocked IP addresses for your account.

+ +

The authentication service determines if a particular IP address should be allowed to access the + service or whether it should be blocked from further attempts by using a set of internal rules. It + also determines which IP addresses may be treated with less caution as they are your legitimate IP + addresses. +

+ +

If you change any of your account passwords, remember to update all the devices that are connecting + to your account as failed logins may lead to blocked IPs.

+ +

Unblock: Unblocks an IP address.

+ +

Block: When this option is selected any further attempts to log in from this IP + address will be rejected.

+ +

Allow: This option will not prevent the IP address from being blocked in the future. + Wrong passwords will fail but will not increase the failed login attempts count that leads to a + blocked IP address.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Service {{ blocked.service }} IP {{ blocked.ip }} Username {{ blocked.login }} Failed attempts {{ blocked.failed_attempts }} Unblock | Block | Allow +
+ No IPs blocked +
-
- -
+ + + \ No newline at end of file diff --git a/src/app/account-security/last-logins.component.ts b/src/app/account-security/last-logins.component.ts index bff1d1957..6513e3c01 100644 --- a/src/app/account-security/last-logins.component.ts +++ b/src/app/account-security/last-logins.component.ts @@ -17,21 +17,23 @@ // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- import { Component, Output, EventEmitter, ViewChild, OnInit } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; import { MatPaginator } from '@angular/material/paginator'; import { MatSnackBar } from '@angular/material/snack-bar'; import { MatDialog } from '@angular/material/dialog'; import { MobileQueryService } from '../mobile-query.service'; import { RMM } from '../rmm'; import { ModalPasswordComponent } from './account.security.component'; +import { RunboxWebmailAPI } from '../rmmapi/rbwebmail'; @Component({ selector: 'app-last-logins', styleUrls: ['account.security.component.scss'], templateUrl: 'last-logins.component.html', }) -export class LastLoginsComponent implements OnInit { +export class LastLoginsComponent implements OnInit { panelOpenState = false; - @ViewChild(MatPaginator, { static: false }) paginator: MatPaginator; + @ViewChild(MatPaginator) paginator: MatPaginator; @Output() Close: EventEmitter = new EventEmitter(); dialog_ref: any; acl_service = ''; @@ -41,13 +43,26 @@ export class LastLoginsComponent implements OnInit { acl_ip = ''; acl_overwrite_subaccount_rules = '0'; is_acl_clear_enabled = false; - acl_manage_ip_rule = 'deny'; + acl_manage_ip_rule = ''; acl_manage_ip_range = ''; acl_manage_ip_label = ''; is_busy_list_logins = false; + is_overwrite_subaccount_ip_rules = ''; modal_password_ref; - constructor(public snackBar: MatSnackBar, public dialog: MatDialog, public mobileQuery: MobileQueryService, public rmm: RMM) { + rulesColumns: string[] = ['rule', 'ip', 'label', 'action']; + blockedIpsColumns: string[] = ['service', 'ip', 'username', 'failed', 'action']; + lastLoginsColumns: string[] = ['service', 'login', 'ip', 'hostname', 'time', 'status', 'actions']; + + addRuleForm; + + constructor( + public snackBar: MatSnackBar, + public dialog: MatDialog, + public mobileQuery: MobileQueryService, + public rmm: RMM, + public rmmapi: RunboxWebmailAPI + ) { this.rmm.me.load(); } @@ -57,6 +72,15 @@ export class LastLoginsComponent implements OnInit { this.rmm.account_security.acl.blocked_list(); this.rmm.account_security.acl.accounts_affected(); this.rmm.account_security.acl.list({}); + this.rmm.account_security.tfa.get().subscribe(() => { + this.is_overwrite_subaccount_ip_rules = this.rmm.account_security.tfa.settings.is_overwrite_subaccount_ip_rules; + }); + + this.addRuleForm = new FormGroup({ + rule: new FormControl(''), + ip: new FormControl(''), + label: new FormControl('') + }); if (!this.rmm.account_security.user_password) { this.show_modal_password(); @@ -142,6 +166,8 @@ export class LastLoginsComponent implements OnInit { .subscribe((res) => { if (res.status === 'success') { this.rmm.account_security.acl.list({}); + this.show_error('Rule added', 'Dismiss'); + this.addRuleForm.reset(); } }); } @@ -172,6 +198,7 @@ export class LastLoginsComponent implements OnInit { if (res.status === 'error') { this.show_error(res.error || res.errors.join(''), 'Dismiss'); } + this.rmm.account_security.acl.blocked_list(); }); } @@ -191,11 +218,27 @@ export class LastLoginsComponent implements OnInit { if (res.status === 'error') { this.show_error(res.error || res.errors.join(''), 'Dismiss'); } - this.rmm.account_security.acl.logins_list({}); + if (res.status === 'success') { + this.show_error('IP ' + result.ip + ' blocked', 'Dismiss'); + } this.rmm.account_security.acl.blocked_list(); + this.rmm.account_security.acl.list({}); }); } + update_overwride_subaccount_rules() { + if (!this.rmm.account_security.user_password) { + this.show_modal_password(); + return; + } + const data = { + action: 'update_status', + is_overwrite_subaccount_ip_rules: this.is_overwrite_subaccount_ip_rules, + password: this.rmm.account_security.user_password, + }; + this.rmm.account_security.tfa.update(data); + } + ip_never_block(result) { if (!this.rmm.account_security.user_password) { this.show_modal_password(); @@ -204,6 +247,7 @@ export class LastLoginsComponent implements OnInit { this.rmm.account_security.acl .update({ ip: result.ip, + label: 'Never Block', rule: 'allow', password: this.rmm.account_security.user_password, }) @@ -211,9 +255,18 @@ export class LastLoginsComponent implements OnInit { if (res.status === 'error') { this.show_error(res.error || res.errors.join(''), 'Dismiss'); } + this.rmm.account_security.acl.blocked_list(); + this.rmm.account_security.acl.list({}); }); } + submit_rule_form(data) { + this.acl_manage_ip_rule = data.rule; + this.acl_manage_ip_range = data.ip; + this.acl_manage_ip_label = data.label; + this.acl_create_rule(); + } + show_modal_password() { this.modal_password_ref = this.dialog.open(ModalPasswordComponent, { width: '600px', diff --git a/src/app/rmm/account-security-acl.ts b/src/app/rmm/account-security-acl.ts index 71cfb6c3a..df78a7c5b 100644 --- a/src/app/rmm/account-security-acl.ts +++ b/src/app/rmm/account-security-acl.ts @@ -1,18 +1,18 @@ // --------- BEGIN RUNBOX LICENSE --------- // Copyright (C) 2016-2018 Runbox Solutions AS (runbox.com). -// +// // This file is part of Runbox 7. -// +// // Runbox 7 is free software: You can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the // Free Software Foundation, either version 3 of the License, or (at your // option) any later version. -// +// // Runbox 7 is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- @@ -28,119 +28,77 @@ export class AccountSecurityACL { results_blocked: any; results_accounts_affected: any; - constructor( - public app: RMM, - ) { - } + constructor(public app: RMM) {} accounts_affected() { this.is_busy = true; const data = {}; const req = this.app.ua.http.get('/rest/v1/acl/accounts_affected/', data).pipe(timeout(60000), share()); - req.subscribe( - reply => { + req.subscribe((reply) => { this.is_busy = false; - if ( reply['status'] === 'error' ) { - this.app.show_error( reply['error'].join( '' ), 'Dismiss' ); - return; + if (reply['status'] === 'error') { + return this.app.show_error('Could not load accounts related to this setting.', 'Dismiss'); } this.results_accounts_affected = reply['result'].usernames || []; return; - }, - error => { - this.is_busy = false; - return this.app.show_error('Could not load accounts related to this setting.', 'Dismiss'); - } - ); + }); return req; } update_sub_account(data): Observable { this.is_busy = true; - data = data || { - }; + data = data || {}; const req = this.app.ua.http.put('/rest/v1/acl/subaccount_rules/', data).pipe(timeout(60000), share()); - req.subscribe( - reply => { + req.subscribe((reply) => { this.is_busy = false; - if ( reply['status'] === 'error' ) { - this.app.show_error( reply['error'].join( '' ), 'Dismiss' ); - return; + if (reply['status'] === 'error') { + return this.app.show_error('Could not update subaccount rules.', 'Dismiss'); } return; - }, - error => { - this.is_busy = false; - return this.app.show_error('Could not update subaccount rules.', 'Dismiss'); - } - ); + }); return req; } blocked_list(): Observable { this.is_busy = true; const req = this.app.ua.http.get('/rest/v1/acl/blocked', {}).pipe(timeout(60000), share()); - req.subscribe( - reply => { + req.subscribe((reply) => { this.is_busy = false; - if ( reply['status'] === 'error' ) { - this.app.show_error( reply['error'].join( '' ), 'Dismiss' ); - return; + if (reply['status'] === 'error') { + return this.app.show_error('Could not list blocked IPs.', 'Dismiss'); } this.results_blocked = reply['blocked_ips']; - console.log('BLOCKED IPS', reply); return; - }, - error => { - this.is_busy = false; - return this.app.show_error('Could not list blocked ips.', 'Dismiss'); - } - ); + }); return req; } unblock(data): Observable { this.is_busy = true; - data = data || { - }; + data = data || {}; const req = this.app.ua.http.put('/rest/v1/acl/unblock', data).pipe(timeout(60000), share()); - req.subscribe( - reply => { + req.subscribe((reply) => { this.is_busy = false; - if ( reply['status'] === 'error' ) { - this.app.show_error( reply['error'].join( '' ), 'Dismiss' ); - return; + if (reply['status'] === 'error') { + return this.app.show_error('Could not unblock IP.', 'Dismiss'); } this.blocked_list(); return; - }, - error => { - this.is_busy = false; - return this.app.show_error('Could not unblock.', 'Dismiss'); - } - ); + }); return req; } update(data): Observable { this.is_busy = true; - data = data || { - }; + data = data || {}; const req = this.app.ua.http.post('/rest/v1/acl/rule', data).pipe(timeout(60000), share()); - req.subscribe( - reply => { + req.subscribe((reply) => { this.is_busy = false; - if ( reply['status'] === 'error' ) { - this.app.show_error( reply['error'].join( '' ), 'Dismiss' ); - return; + if (reply['status'] === 'error') { + return this.app.show_error('Could not update rules.', 'Dismiss'); } return; - }, - error => { - this.is_busy = false; - return this.app.show_error('Could not update rules.', 'Dismiss'); - } - ); + }); return req; } @@ -148,63 +106,41 @@ export class AccountSecurityACL { this.is_busy = true; data = data || {}; const req = this.app.ua.http.get('/rest/v1/acl/rules', data).pipe(timeout(60000), share()); - req.subscribe( - reply => { + req.subscribe((reply) => { this.is_busy = false; - if ( reply['status'] === 'error' ) { - this.app.show_error( reply['error'].join( '' ), 'Dismiss' ); - return; + if (reply['status'] === 'error') { + return this.app.show_error('Could not list rules.', 'Dismiss'); } this.results_rules = reply['rules']; return; - }, - error => { - this.is_busy = false; - return this.app.show_error('Could not list rules.', 'Dismiss'); - } - ); + }); return req; } remove_rule(data): Observable { this.is_busy = true; const req = this.app.ua.http.put('/rest/v1/acl/remove_rule/' + data.id, data).pipe(timeout(60000), share()); - req.subscribe( - reply => { + req.subscribe((reply) => { this.is_busy = false; - if ( reply['status'] === 'error' ) { - this.app.show_error( reply['error'].join( '' ), 'Dismiss' ); - return; + if (reply['status'] === 'error') { + return this.app.show_error('Could not delete rule', 'Dismiss'); } return; - }, - error => { - this.is_busy = false; - return this.app.show_error('Could not delete rule.', 'Dismiss'); - } - ); + }); return req; } create_rule(data): Observable { this.is_busy = true; - data = data || { - }; + data = data || {}; const req = this.app.ua.http.post('/rest/v1/acl/rule', data).pipe(timeout(60000), share()); - req.subscribe( - reply => { + req.subscribe((reply) => { this.is_busy = false; - if ( reply['status'] === 'error' ) { - this.app.show_error( reply['error'].join( '' ), 'Dismiss' ); - return; + if (reply['status'] === 'error') { + return this.app.show_error('Could not create rule', 'Dismiss'); } return; - }, - error => { - this.is_busy = false; - return this.app.show_error('Could not update rule.', 'Dismiss'); - } - ); + }); return req; } @@ -216,21 +152,14 @@ export class AccountSecurityACL { url.searchParams.set(key, encodeURI(data[key])); }); const req = this.app.ua.http.get([url.pathname, url.searchParams.toString()].join('?'), {}).pipe(timeout(60000), share()); - req.subscribe( - reply => { + req.subscribe((reply) => { this.is_busy = false; - if ( reply['status'] === 'error' ) { - this.app.show_error( reply['error'].join( '' ), 'Dismiss' ); - return; + if (reply['status'] === 'error') { + return this.app.show_error('Could not list last logins.', 'Dismiss'); } this.results_logins_list = reply['last_logins']; return; - }, - error => { - this.is_busy = false; - return this.app.show_error('Could not list last logins.', 'Dismiss'); - } - ); + }); return req; } }