Merge pull request #451 from allerta-vvf/master

Migrate to new JWT format
This commit is contained in:
Matteo Gheza 2022-02-13 01:34:46 +01:00 committed by GitHub
commit 4c10f8cd1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 217 additions and 119 deletions

View File

@ -94,9 +94,9 @@ function apiRouter (FastRoute\RouteCollector $r) {
'/list',
function ($vars) {
global $db, $users;
requireLogin() || accessDenied();
requireLogin();
$users->online_time_update();
if($users->hasRole(Role::FULL_VIEWER)) {
if($users->hasRole(Role::SUPER_EDITOR)) {
$response = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `hidden` = 0 ORDER BY available DESC, chief DESC, services ASC, trainings DESC, availability_minutes ASC, name ASC");
} else {
$response = $db->select("SELECT `id`, `chief`, `online_time`, `available`, `availability_minutes`, `name`, `driver`, `services` FROM `".DB_PREFIX."_profiles` WHERE `hidden` = 0 ORDER BY available DESC, chief DESC, services ASC, trainings DESC, availability_minutes ASC, name ASC");
@ -112,7 +112,7 @@ function apiRouter (FastRoute\RouteCollector $r) {
'/logs',
function ($vars) {
global $db, $users;
requireLogin() || accessDenied();
requireLogin();
$users->online_time_update();
$response = $db->select("SELECT * FROM `".DB_PREFIX."_log` ORDER BY `timestamp` DESC");
if(!is_null($response)) {
@ -132,7 +132,7 @@ function apiRouter (FastRoute\RouteCollector $r) {
'/services',
function ($vars) {
global $services, $users;
requireLogin() || accessDenied();
requireLogin();
$users->online_time_update();
apiResponse($services->list());
}
@ -142,7 +142,7 @@ function apiRouter (FastRoute\RouteCollector $r) {
'/services',
function ($vars) {
global $services, $users;
requireLogin() || accessDenied();
requireLogin();
$users->online_time_update();
apiResponse(["response" => $services->add($_POST["start"], $_POST["end"], $_POST["code"], $_POST["chief"], $_POST["drivers"], $_POST["crew"], $_POST["place"], $_POST["notes"], $_POST["type"], $users->auth->getUserId())]);
}
@ -153,7 +153,7 @@ function apiRouter (FastRoute\RouteCollector $r) {
'/services/{id}',
function ($vars) {
global $services, $users;
requireLogin() || accessDenied();
requireLogin();
$users->online_time_update();
apiResponse($services->get($vars['id']));
}
@ -163,7 +163,7 @@ function apiRouter (FastRoute\RouteCollector $r) {
'/services/{id}',
function ($vars) {
global $services, $users;
requireLogin() || accessDenied();
requireLogin();
$users->online_time_update();
apiResponse(["response" => $services->delete($vars["id"])]);
}
@ -174,7 +174,7 @@ function apiRouter (FastRoute\RouteCollector $r) {
'/place_details',
function ($vars) {
global $db, $users;
requireLogin() || accessDenied();
requireLogin();
$users->online_time_update();
$response = $db->selectRow("SELECT * FROM `".DB_PREFIX."_places_info` WHERE `lat` = ? and `lng` = ? LIMIT 0,1;", [$_GET["lat"], $_GET["lng"]]);
apiResponse(!is_null($response) ? $response : []);
@ -186,7 +186,7 @@ function apiRouter (FastRoute\RouteCollector $r) {
'/trainings',
function ($vars) {
global $db, $users;
requireLogin() || accessDenied();
requireLogin();
$users->online_time_update();
$response = $db->select("SELECT * FROM `".DB_PREFIX."_trainings` ORDER BY date DESC, beginning desc");
apiResponse(
@ -200,7 +200,7 @@ function apiRouter (FastRoute\RouteCollector $r) {
'/users',
function ($vars) {
global $users, $users;
requireLogin() || accessDenied();
requireLogin();
$users->online_time_update();
apiResponse($users->get_users());
}
@ -210,8 +210,8 @@ function apiRouter (FastRoute\RouteCollector $r) {
'/users',
function ($vars) {
global $users;
requireLogin() || accessDenied();
if(!$users->hasRole(Role::FULL_VIEWER) && $_POST["id"] !== $users->auth->getUserId()){
requireLogin();
if(!$users->hasRole(Role::SUPER_EDITOR) && $_POST["id"] !== $users->auth->getUserId()){
exit;
}
apiResponse(["userId" => $users->add_user($_POST["email"], $_POST["name"], $_POST["username"], $_POST["password"], $_POST["phone_number"], $_POST["birthday"], $_POST["chief"], $_POST["driver"], $_POST["hidden"], $_POST["disabled"], "unknown")]);
@ -222,8 +222,8 @@ function apiRouter (FastRoute\RouteCollector $r) {
'/users/{userId}',
function ($vars) {
global $users;
requireLogin() || accessDenied();
if(!$users->hasRole(Role::FULL_VIEWER) && $_POST["id"] !== $users->auth->getUserId()){
requireLogin();
if(!$users->hasRole(Role::SUPER_EDITOR) && $_POST["id"] !== $users->auth->getUserId()){
exit;
}
apiResponse($users->get_user($vars["userId"]));
@ -234,8 +234,8 @@ function apiRouter (FastRoute\RouteCollector $r) {
'/users/{userId}',
function ($vars) {
global $users;
requireLogin() || accessDenied();
if(!$users->hasRole(Role::FULL_VIEWER) && $_POST["id"] !== $users->auth->getUserId()){
requireLogin();
if(!$users->hasRole(Role::SUPER_EDITOR) && $_POST["id"] !== $users->auth->getUserId()){
exit;
}
$users->remove_user($vars["userId"], "unknown");
@ -248,7 +248,7 @@ function apiRouter (FastRoute\RouteCollector $r) {
'/availability',
function ($vars) {
global $users, $db;
requireLogin() || accessDenied();
requireLogin();
$users->online_time_update();
$row = $db->selectRow(
"SELECT `available`, `manual_mode` FROM `".DB_PREFIX."_profiles` WHERE `id` = ?",
@ -265,9 +265,9 @@ function apiRouter (FastRoute\RouteCollector $r) {
'/availability',
function ($vars) {
global $users, $availability;
requireLogin() || accessDenied();
requireLogin();
$users->online_time_update();
if(!$users->hasRole(Role::FULL_VIEWER) && $_POST["id"] !== $users->auth->getUserId()){
if(!$users->hasRole(Role::SUPER_EDITOR) && $_POST["id"] !== $users->auth->getUserId()){
exit;
}
$user_id = is_numeric($_POST["id"]) ? $_POST["id"] : $users->auth->getUserId();
@ -283,7 +283,7 @@ function apiRouter (FastRoute\RouteCollector $r) {
"/manual_mode",
function ($vars) {
global $users, $availability;
requireLogin() || accessDenied();
requireLogin();
$users->online_time_update();
$availability->change_manual_mode($_POST["manual_mode"]);
apiResponse(["status" => "success"]);
@ -295,7 +295,7 @@ function apiRouter (FastRoute\RouteCollector $r) {
'/schedules',
function ($vars) {
global $users, $schedules;
requireLogin() || accessDenied();
requireLogin();
$users->online_time_update();
apiResponse($schedules->get());
}
@ -305,7 +305,7 @@ function apiRouter (FastRoute\RouteCollector $r) {
'/schedules',
function ($vars) {
global $users, $schedules;
requireLogin() || accessDenied();
requireLogin();
$users->online_time_update();
$new_schedules = !is_string($_POST["schedules"]) ? json_encode($_POST["schedules"]) : $_POST["schedules"];
apiResponse([
@ -319,7 +319,7 @@ function apiRouter (FastRoute\RouteCollector $r) {
'/service_types',
function ($vars) {
global $users, $db;
requireLogin() || accessDenied();
requireLogin();
$users->online_time_update();
$response = $db->select("SELECT * FROM `".DB_PREFIX."_type`");
apiResponse(is_null($response) ? [] : $response);
@ -330,7 +330,7 @@ function apiRouter (FastRoute\RouteCollector $r) {
'/service_types',
function ($vars) {
global $users, $db;
requireLogin() || accessDenied();
requireLogin();
$users->online_time_update();
$response = $db->insert(DB_PREFIX."_type", ["name" => $_POST["name"]]);
apiResponse($response);
@ -342,7 +342,7 @@ function apiRouter (FastRoute\RouteCollector $r) {
'/places/search',
function ($vars) {
global $places;
requireLogin() || accessDenied();
requireLogin();
apiResponse($places->search($_GET["q"]));
}
);
@ -352,7 +352,7 @@ function apiRouter (FastRoute\RouteCollector $r) {
'/telegram_login_token',
function ($vars) {
global $users, $db;
requireLogin() || accessDenied();
requireLogin();
$users->online_time_update();
$token = bin2hex(random_bytes(16));
apiResponse([
@ -405,6 +405,18 @@ function apiRouter (FastRoute\RouteCollector $r) {
}
}
);
$r->addRoute(
['GET', 'POST'],
'/refreshToken',
function ($vars) {
global $users;
requireLogin(false);
apiResponse([
"token" => $users->generateToken()
]);
}
);
$r->addRoute(
['GET', 'POST'],
'/validateToken',

View File

@ -130,12 +130,33 @@ function getBearerToken() {
return null;
}
function requireLogin()
function requireLogin($validate_token_version=true)
{
global $users;
$token = getBearerToken();
if($users->auth->isTokenValid($token)) {
$users->auth->authenticateWithToken($token);
if($users->auth->hasRole(\Delight\Auth\Role::CONSULTANT)) {
//Migrate to new user roles
$users->auth->admin()->removeRoleForUserById($users->auth->getUserId(), \Delight\Auth\Role::CONSULTANT);
$users->auth->admin()->addRoleForUserById($users->auth->getUserId(), Role::SUPER_EDITOR);
$users->auth->authenticateWithToken($token);
}
if($validate_token_version) {
if(!isset($users->auth->user_info["v"])) {
statusCode(400);
apiResponse(["status" => "error", "message" => "JWT client version is not supported", "type" => "jwt_update_required"]);
exit();
}
if((int) $users->auth->user_info["v"] !== 2) {
statusCode(400);
apiResponse(["status" => "error", "message" => "JWT client version ".$users->auth->user_info["v"]." is not supported", "type" => "jwt_update_required"]);
exit();
}
}
if(defined('SENTRY_LOADED')) {
\Sentry\configureScope(function (\Sentry\State\Scope $scope) use ($users): void {
$scope->setUser([
@ -147,15 +168,11 @@ function requireLogin()
]);
});
}
return true;
}
return false;
}
function accessDenied()
{
return;
}
statusCode(401);
apiResponse(["error" => "Access denied"]);
apiResponse(["status" => "error", "message" => "Access denied"]);
exit();
}

View File

@ -40,6 +40,8 @@ function getUserIdByMessage(Message $message)
function requireBotLogin(Message $message)
{
global $users;
$userId = getUserIdByMessage($message);
if ($userId === null) {
$message->reply(
@ -47,6 +49,12 @@ function requireBotLogin(Message $message)
"\nPer farlo, premere su <strong>\"Collega l'account al bot Telegram\"</strong>."
);
exit();
} else {
if($users->auth->hasRole(\Delight\Auth\Role::CONSULTANT)) {
//Migrate to new user roles
$users->auth->admin()->removeRoleForUserById($users->auth->getUserId(), \Delight\Auth\Role::CONSULTANT);
$users->auth->admin()->addRoleForUserById($users->auth->getUserId(), Role::SUPER_EDITOR);
}
}
}

View File

@ -74,15 +74,14 @@ $auth = new \Delight\Auth\Auth($db, $JWTconfig, get_ip(), DB_PREFIX."_");
final class Role
{
//https://github.com/delight-im/PHP-Auth/blob/master/src/Role.php
const GUEST = \Delight\Auth\Role::AUTHOR;
const BASIC_VIEWER = \Delight\Auth\Role::COLLABORATOR;
const FULL_VIEWER = \Delight\Auth\Role::CONSULTANT;
const EDITOR = \Delight\Auth\Role::CONSUMER;
const SUPER_EDITOR = \Delight\Auth\Role::CONTRIBUTOR;
const EDITOR = \Delight\Auth\Role::EDITOR;
const SUPER_EDITOR = \Delight\Auth\Role::SUPER_EDITOR;
const DEVELOPER = \Delight\Auth\Role::DEVELOPER;
const TESTER = \Delight\Auth\Role::CREATOR;
const GUEST = \Delight\Auth\Role::SUBSCRIBER;
const EXTERNAL_VIEWER = \Delight\Auth\Role::REVIEWER;
const ADMIN = \Delight\Auth\Role::ADMIN;
const SUPER_ADMIN = \Delight\Auth\Role::SUPER_ADMIN;
@ -191,7 +190,7 @@ class Users
["hidden" => $hidden, "disabled" => $disabled, "name" => $name, "phone_number" => $phone_number, "chief" => $chief, "driver" => $driver]
);
if($chief == 1) {
$this->auth->admin()->addRoleForUserById($userId, Role::FULL_VIEWER);
$this->auth->admin()->addRoleForUserById($userId, Role::SUPER_EDITOR);
}
logger("User added", $userId, $inserted_by);
return $userId;
@ -233,14 +232,29 @@ class Users
);
}
public function generateToken()
{
$token = $this->auth->generateJWTtoken([
"roles" => $this->auth->getRoles(),
"name" => $this->getName(),
"v" => 2
]);
return $token;
}
public function loginAndReturnToken($username, $password)
{
$this->auth->loginWithUsername($username, $password);
$token = $this->auth->generateJWTtoken([
"full_viewer" => $this->hasRole(Role::FULL_VIEWER),
"name" => $this->getName(),
]);
return $token;
if($this->auth->hasRole(\Delight\Auth\Role::CONSULTANT)) {
//Migrate to new user roles
$this->auth->admin()->removeRoleForUserById($this->auth->getUserId(), \Delight\Auth\Role::CONSULTANT);
$this->auth->admin()->addRoleForUserById($this->auth->getUserId(), Role::SUPER_EDITOR);
$this->auth->loginWithUsername($username, $password);
}
return $this->generateToken();
}
public function isHidden($id=null)

View File

@ -17752,9 +17752,7 @@
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
},
"karma": {
"version": "6.3.9",
"resolved": "https://registry.npmjs.org/karma/-/karma-6.3.9.tgz",
"integrity": "sha512-E/MqdLM9uVIhfuyVnrhlGBu4miafBdXEAEqCmwdEMh3n17C7UWC/8Kvm3AYKr91gc7scutekZ0xv6rxRaUCtnw==",
"version": "6.3.14",
"dev": true,
"requires": {
"body-parser": "^1.19.0",
@ -22000,4 +21998,4 @@
}
}
}
}
}

View File

@ -5,7 +5,7 @@
<th>Nome</th>
<th>Disponibile</th>
<th>Autista</th>
<ng-container *ngIf="auth.profile.full_viewer">
<ng-container *ngIf="auth.profile.hasRole('SUPER_EDITOR')">
<th>Chiama</th>
</ng-container>
<th>Interventi</th>
@ -27,7 +27,7 @@
<td>
<img alt="driver" src="./assets/icons/wheel.png" width="20px" *ngIf="row.driver">
</td>
<td *ngIf="auth.profile.full_viewer">
<td *ngIf="auth.profile.hasRole('SUPER_EDITOR')">
<a href="tel:{{row.phone_number}}"><i class="fa fa-phone"></i></a>
</td>
<td>{{ row.services }}</td>

View File

@ -61,7 +61,7 @@ export class TableComponent implements OnInit, OnDestroy {
}
onChangeAvailability(user: number, newState: 0|1) {
if(this.auth.profile.full_viewer) {
if(this.auth.profile.hasRole('SUPER_EDITOR')) {
this.changeAvailability.emit({user, newState});
}
}

View File

@ -0,0 +1,72 @@
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpHandler, HttpRequest, HttpEvent, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { AuthService } from '../_services/auth.service';
import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { catchError, filter, switchMap, take } from 'rxjs/operators';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject<string|undefined> = new BehaviorSubject<string|undefined>(undefined);
constructor(private auth: AuthService) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<Object>> {
const token = this.auth.getToken();
let authReq = this.addHeaders(req, token);
return next.handle(authReq).pipe(catchError(error => {
if (error instanceof HttpErrorResponse && !authReq.url.includes('login')) {
if(error.status === 400) {
return this.handle400Error(authReq, next);
} else if (error.status === 401) {
this.auth.logout();
}
}
return throwError(() => new Error(error));
}));
}
private handle400Error(request: HttpRequest<any>, next: HttpHandler) {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(undefined);
return this.auth.refreshToken().pipe(
switchMap((token: string) => {
this.isRefreshing = false;
this.refreshTokenSubject.next(token);
return next.handle(this.addHeaders(request, token));
}),
catchError((err) => {
this.isRefreshing = false;
this.auth.logout();
return throwError(() => new Error(err));
})
);
}
return this.refreshTokenSubject.pipe(
filter(token => token !== undefined),
take(1),
switchMap((token) => {
return next.handle(this.addHeaders(request, token));
})
);
}
private addHeaders(request: HttpRequest<any>, token: string|undefined) {
if (typeof token === 'string' && token.length > 10) {
const headers = new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Bearer ${token}`
});
return request.clone({ headers });
} else {
const headers = new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded'
});
return request.clone({ headers });
}
}
}

View File

@ -1,23 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
import { Observable, tap } from 'rxjs';
import { AuthService } from '../_services/auth.service';
@Injectable()
export class UnauthorizedInterceptor implements HttpInterceptor {
constructor(private auth: AuthService) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe( tap({
next: () => {},
error: (err: any) => {
if (err instanceof HttpErrorResponse) {
if (err.status !== 401 || request.url.includes('/login')) {
return;
}
console.log("Login required");
this.auth.logout();
}
}}));
}
}

View File

@ -6,24 +6,8 @@ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
})
export class ApiClientService {
private apiRoot = 'api/';
public requestOptions = {};
constructor(private http: HttpClient) {
this.requestOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded'
})
}
}
public setToken(token: string) {
this.requestOptions = {
headers: new HttpHeaders({
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/x-www-form-urlencoded'
})
}
}
constructor(private http: HttpClient) { }
public apiEndpoint(endpoint: string): string {
if(endpoint.startsWith('https')) {
@ -45,42 +29,37 @@ export class ApiClientService {
public get(endpoint: string, data: any = {}) {
return new Promise<any>((resolve, reject) => {
this.http.get(this.apiEndpoint(endpoint), {
...this.requestOptions,
params: new HttpParams({ fromObject: data })
}).subscribe((data: any) => {
resolve(data);
}, (err) => {
reject(err);
}).subscribe({
next: (v) => resolve(v),
error: (e) => reject(e)
});
});
}
public post(endpoint: string, data: any) {
public post(endpoint: string, data: any = {}) {
return new Promise<any>((resolve, reject) => {
this.http.post(this.apiEndpoint(endpoint), this.dataToParams(data), this.requestOptions).subscribe((data: any) => {
resolve(data);
}, (err) => {
reject(err);
this.http.post(this.apiEndpoint(endpoint), this.dataToParams(data)).subscribe({
next: (v) => resolve(v),
error: (e) => reject(e)
});
});
}
public put(endpoint: string, data: any) {
public put(endpoint: string, data: any = {}) {
return new Promise<any>((resolve, reject) => {
this.http.put(this.apiEndpoint(endpoint), this.dataToParams(data), this.requestOptions).subscribe((data: any) => {
resolve(data);
}, (err) => {
reject(err);
this.http.put(this.apiEndpoint(endpoint), this.dataToParams(data)).subscribe({
next: (v) => resolve(v),
error: (e) => reject(e)
});
});
}
public delete(endpoint: string) {
return new Promise<any>((resolve, reject) => {
this.http.delete(this.apiEndpoint(endpoint), this.requestOptions).subscribe((data: any) => {
resolve(data);
}, (err) => {
reject(err);
this.http.delete(this.apiEndpoint(endpoint)).subscribe({
next: (v) => resolve(v),
error: (e) => reject(e)
});
});
}

View File

@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { ApiClientService } from './api-client.service';
import { Observable } from "rxjs";
import jwt_decode from 'jwt-decode';
export interface LoginResponse {
@ -13,13 +14,14 @@ export interface LoginResponse {
})
export class AuthService {
public profile: any = undefined;
private access_token = '';
private access_token: string | undefined = undefined;
public loadProfile() {
try{
console.log("Loading profile", this.access_token);
let now = Date.now().valueOf() / 1000;
(window as any).jwt_decode = jwt_decode;
if(typeof(this.access_token) !== "string") return;
let decoded: any = jwt_decode(this.access_token);
if (typeof decoded.exp !== 'undefined' && decoded.exp < now) {
return false;
@ -29,6 +31,10 @@ export class AuthService {
}
this.profile = decoded.user_info;
this.profile.hasRole = (role: string) => {
return Object.keys(this.profile.roles).includes(role);
}
console.log(this.profile);
return true;
} catch(e) {
@ -42,19 +48,22 @@ export class AuthService {
constructor(private api: ApiClientService, private router: Router) {
if(localStorage.getItem("access_token") !== null) {
this.access_token = localStorage.getItem("access_token") as string;
this.api.setToken(this.access_token);
this.loadProfile();
}
}
private setToken(value: string) {
public setToken(value: string) {
localStorage.setItem("access_token", value);
this.access_token = value;
this.api.setToken(this.access_token);
this.loadProfile();
}
public getToken(): string | undefined {
return this.access_token;
}
private removeToken() {
this.access_token = '';
localStorage.removeItem("access_token");
}
@ -105,4 +114,16 @@ export class AuthService {
}
this.router.navigate(routerDestination);
}
public refreshToken() {
return new Observable<string>((observer) => {
this.api.post("refreshToken").then((data: any) => {
this.setToken(data.token);
observer.next(data.token);
observer.complete();
}).catch((err) => {
observer.error(err);
});
});
}
}

View File

@ -23,7 +23,7 @@ import { LogsComponent } from './_components/logs/logs.component';
import { ServicesComponent } from './_components/services/services.component';
import { TrainingsComponent } from './_components/trainings/trainings.component';
import { UnauthorizedInterceptor } from './_providers/unauthorized-interceptor.provider';
import { AuthInterceptor } from './_providers/auth-interceptor.provider';
@NgModule({
declarations: [
@ -63,7 +63,7 @@ import { UnauthorizedInterceptor } from './_providers/unauthorized-interceptor.p
],
providers: [{
provide: HTTP_INTERCEPTORS,
useClass: UnauthorizedInterceptor,
useClass: AuthInterceptor,
multi: true
}],
bootstrap: [AppComponent]