Added impersonation. More data available via auth service about the user. Added admin auth guard.

This commit is contained in:
Tom 2024-10-31 05:33:11 +00:00
parent 65f4172bc2
commit 2bde8b850a
16 changed files with 218 additions and 47 deletions

View File

@ -1,4 +1,4 @@
<main class="main"> <main class="main">
<navigation class="navigation" /> <navigation class="navigation" />
<router-outlet class="content" /> <router-outlet class="content" />
</main> </main>

View File

@ -3,7 +3,7 @@ import { Component, OnInit, Inject, PLATFORM_ID, NgZone, OnDestroy } from '@angu
import { Router, RouterOutlet } from '@angular/router'; import { Router, RouterOutlet } from '@angular/router';
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { HermesClientService } from './hermes-client.service'; import { HermesClientService } from './hermes-client.service';
import { AuthGuard } from './shared/auth/auth.guard' import { AuthUserGuard } from './shared/auth/auth.user.guard'
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { PolicyComponent } from "./policy/policy.component"; import { PolicyComponent } from "./policy/policy.component";
import { NavigationComponent } from "./navigation/navigation.component"; import { NavigationComponent } from "./navigation/navigation.component";
@ -14,7 +14,7 @@ import { ApiAuthenticationService } from './shared/services/api/api-authenticati
selector: 'app-root', selector: 'app-root',
standalone: true, standalone: true,
imports: [RouterOutlet, CommonModule, FormsModule, PolicyComponent, NavigationComponent], imports: [RouterOutlet, CommonModule, FormsModule, PolicyComponent, NavigationComponent],
providers: [AuthGuard], providers: [AuthUserGuard],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.scss' styleUrl: './app.component.scss'
}) })

View File

@ -1,6 +1,6 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { PolicyComponent } from './policy/policy.component'; import { PolicyComponent } from './policy/policy.component';
import { AuthGuard } from './shared/auth/auth.guard'; import { AuthUserGuard } from './shared/auth/auth.user.guard';
import { LoginComponent } from './login/login.component'; import { LoginComponent } from './login/login.component';
import { TtsLoginComponent } from './tts-login/tts-login.component'; import { TtsLoginComponent } from './tts-login/tts-login.component';
import { TwitchAuthCallbackComponent } from './twitch-auth-callback/twitch-auth-callback.component'; import { TwitchAuthCallbackComponent } from './twitch-auth-callback/twitch-auth-callback.component';
@ -9,7 +9,7 @@ export const routes: Routes = [
{ {
path: 'policies', path: 'policies',
component: PolicyComponent, component: PolicyComponent,
canActivate: [AuthGuard], canActivate: [AuthUserGuard],
}, },
{ {
path: 'login', path: 'login',
@ -18,7 +18,7 @@ export const routes: Routes = [
{ {
path: 'tts-login', path: 'tts-login',
component: TtsLoginComponent, component: TtsLoginComponent,
canActivate: [AuthGuard], canActivate: [AuthUserGuard],
}, },
{ {
path: 'auth', path: 'auth',

View File

@ -42,8 +42,9 @@ export class HermesClientService {
if (!this.connected) if (!this.connected)
return; return;
this.socket.close();
this.connected = false; this.connected = false;
this.logged_in = false;
this.socket.close();
this.events.emit('tts_logoff', null); this.events.emit('tts_logoff', null);
} }
@ -58,6 +59,8 @@ export class HermesClientService {
} }
public login(api_key: string) { public login(api_key: string) {
if (!this.connected)
this.connect();
if (this.logged_in) if (this.logged_in)
return; return;

View File

@ -0,0 +1,19 @@
@if (isAdmin()) {
<mat-card appearance="outlined">
<mat-card-header>
<mat-card-title> Impersonation</mat-card-title>
<mat-card-subtitle>Impersonate as another user</mat-card-subtitle>
</mat-card-header>
<mat-card-actions>
<mat-form-field>
<mat-label>User to impersonate</mat-label>
<mat-select (selectionChange)="onChange($event)" [(value)]="impersonated">
<mat-option>{{getUsername()}}</mat-option>
@for (user of users; track user) {
<mat-option [value]="user.id">{{ user.name }}</mat-option>
}
</mat-select>
</mat-form-field>
</mat-card-actions>
</mat-card>
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ImpersonationComponent } from './impersonation.component';
describe('ImpersonationComponent', () => {
let component: ImpersonationComponent;
let fixture: ComponentFixture<ImpersonationComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ImpersonationComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ImpersonationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,82 @@
import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core';
import { ApiAuthenticationService } from '../shared/services/api/api-authentication.service';
import { MatCardModule } from '@angular/material/card';
import { MatSelectModule } from '@angular/material/select';
import { HttpClient } from '@angular/common/http';
import { isPlatformBrowser } from '@angular/common';
import { environment } from '../../environments/environment';
import EventService from '../shared/services/EventService';
import { HermesClientService } from '../hermes-client.service';
import { Router } from '@angular/router';
@Component({
selector: 'impersonation',
standalone: true,
imports: [MatCardModule, MatSelectModule],
templateUrl: './impersonation.component.html',
styleUrl: './impersonation.component.scss'
})
export class ImpersonationComponent implements OnInit {
impersonated: string | undefined;
users: { id: string, name: string }[];
constructor(private hermes: HermesClientService, private auth: ApiAuthenticationService, private router: Router, private events: EventService, private http: HttpClient, @Inject(PLATFORM_ID) private platformId: Object) {
this.users = []
}
ngOnInit(): void {
if (!isPlatformBrowser(this.platformId)) {
return;
}
this.http.get(environment.API_HOST + '/admin/users', {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
}
}).subscribe((data: any) => {
this.users = data.filter((d: any) => d.name != this.auth.getUsername());
const id = this.auth.getImpersonatedId();
if (this.users.find(u => u.id == id)) {
this.impersonated = id;
}
});
}
public isAdmin() {
return this.auth.isAdmin();
}
public getUsername() {
return this.auth.getUsername();
}
public onChange(e: any) {
console.log('impersonate befre', e.value);
if (!e.value) {
this.http.delete(environment.API_HOST + '/admin/impersonate', {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
},
body: {
impersonation: e.value
}
}).subscribe((data: any) => {
this.hermes.disconnect();
this.events.emit('impersonation', e.value);
this.router.navigate(['/tts-login']);
});
} else {
this.http.put(environment.API_HOST + '/admin/impersonate', {
impersonation: e.value
}, {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
}
}).subscribe((data: any) => {
this.hermes.disconnect();
this.events.emit('impersonation', e.value);
this.router.navigate(['/tts-login']);
});
}
}
}

View File

@ -1,10 +1,11 @@
<nav> <nav>
<impersonation />
<ul> <ul>
<li> <li>
<a <a
routerLink="/login" routerLink="/login"
routerLinkActive="active" routerLinkActive="active"
*ngIf="!twitch_logged_in"> *ngIf="!isLoggedIn()">
Login Login
</a> </a>
</li> </li>
@ -12,7 +13,7 @@
<a <a
routerLink="/tts-login" routerLink="/tts-login"
routerLinkActive="active" routerLinkActive="active"
*ngIf="twitch_logged_in && !tts_logged_in"> *ngIf="isLoggedIn() && !isTTSLoggedIn()">
TTS Login TTS Login
</a> </a>
</li> </li>
@ -20,7 +21,7 @@
<a <a
routerLink="/policies" routerLink="/policies"
routerLinkActive="active" routerLinkActive="active"
*ngIf="twitch_logged_in && tts_logged_in"> *ngIf="isLoggedIn() && isTTSLoggedIn()">
Policies Policies
</a> </a>
</li> </li>

View File

@ -3,22 +3,28 @@ import { RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { HermesClientService } from '../hermes-client.service'; import { HermesClientService } from '../hermes-client.service';
import { ApiAuthenticationService } from '../shared/services/api/api-authentication.service'; import { ApiAuthenticationService } from '../shared/services/api/api-authentication.service';
import { ImpersonationComponent } from '../impersonation/impersonation.component';
import { MatCardModule } from '@angular/material/card';
@Component({ @Component({
selector: 'navigation', selector: 'navigation',
standalone: true, standalone: true,
imports: [CommonModule, RouterModule], imports: [CommonModule, RouterModule, ImpersonationComponent, MatCardModule],
templateUrl: './navigation.component.html', templateUrl: './navigation.component.html',
styleUrl: './navigation.component.scss' styleUrl: './navigation.component.scss'
}) })
export class NavigationComponent { export class NavigationComponent {
constructor(private auth: ApiAuthenticationService, private hermes: HermesClientService) { } constructor(private auth: ApiAuthenticationService, private hermes: HermesClientService) { }
get twitch_logged_in() { isLoggedIn() {
return this.auth.isAuthenticated(); return this.auth.isAuthenticated();
} }
get tts_logged_in() { isAdmin() {
return this.isLoggedIn() && this.auth.isAdmin()
}
isTTSLoggedIn() {
return this.hermes?.logged_in ?? false; return this.hermes?.logged_in ?? false;
} }
} }

View File

@ -83,7 +83,6 @@ export class PolicyTableComponent implements OnInit, OnDestroy {
} }
} else if (response.request.type == "get_permissions") { } else if (response.request.type == "get_permissions") {
this.groups = Object.assign({}, ...response.data.groups.map((g: any) => ({ [g.id]: g }))); this.groups = Object.assign({}, ...response.data.groups.map((g: any) => ({ [g.id]: g })));
console.log(this.groups);
} }
}); });
this.hermes.fetchPolicies(); this.hermes.fetchPolicies();

View File

@ -5,17 +5,11 @@ import { ApiAuthenticationService } from '../services/api/api-authentication.ser
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class AuthGuard implements CanActivate { export class AuthAdminGuard implements CanActivate {
constructor(private auth: ApiAuthenticationService, private router: Router) {} constructor(private auth: ApiAuthenticationService, private router: Router) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> { async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
if (this.auth.isAuthenticated()) { return this.auth.isAuthenticated() && this.auth.isAdmin();
console.log('Valid OAuth');
return true;
}
console.log("Invalid OAuth");
return false;
} }
} }

View File

@ -0,0 +1,15 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { ApiAuthenticationService } from '../services/api/api-authentication.service';
@Injectable({
providedIn: 'root'
})
export class AuthUserGuard implements CanActivate {
constructor(private auth: ApiAuthenticationService, private router: Router) { }
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
return this.auth.isAuthenticated();
}
}

View File

@ -6,11 +6,13 @@ import EventService from '../EventService';
providedIn: 'root' providedIn: 'root'
}) })
export class ApiAuthenticationService { export class ApiAuthenticationService {
private authenticated: boolean; private authenticated: boolean;
private lastCheck: Date; private user: any;
private lastCheck: Date;
constructor(private http: HttpClient, private events: EventService) { constructor(private http: HttpClient, private events: EventService) {
this.authenticated = false; this.authenticated = false;
this.user = null;
this.lastCheck = new Date(); this.lastCheck = new Date();
} }
@ -18,35 +20,48 @@ export class ApiAuthenticationService {
return this.authenticated; return this.authenticated;
} }
isAdmin() {
return this.isAuthenticated() && this.user.role == 'ADMIN';
}
getImpersonatedId() {
return this.user.impersonation.id;
}
getUsername() {
return this.user.name;
}
update() { update() {
const jwt = localStorage.getItem('jwt'); const jwt = localStorage.getItem('jwt');
if (!jwt) { if (!jwt) {
this.updateAuthenticated(false); this.updateAuthenticated(false, null);
return; return;
} }
// /api/auth/jwt // /api/auth/validate
this.http.get('/api/auth/jwt', { this.http.get('/api/auth/validate', {
headers: { headers: {
'Authorization': 'Bearer ' + jwt 'Authorization': 'Bearer ' + jwt
} }
}).subscribe((data: any) => { }).subscribe((data: any) => {
console.log('jwt validation', data); console.log('jwt validation', data);
this.updateAuthenticated(data?.authenticated); this.updateAuthenticated(data?.authenticated, data?.user);
}); });
} }
private updateAuthenticated(value: boolean) { private updateAuthenticated(authenticated: boolean, user: any) {
const previous = this.authenticated; const previous = this.authenticated;
this.authenticated = value; this.authenticated = authenticated;
this.user = user;
this.lastCheck = new Date(); this.lastCheck = new Date();
if (previous != value) { if (previous != authenticated) {
if (value) { if (authenticated) {
this.events.emit('login', null); this.events.emit('login', null);
} else { } else {
this.events.emit('logoff', null); this.events.emit('logoff', null);
} }
} }
} }
} }

View File

@ -4,8 +4,8 @@
<mat-label>API Key</mat-label> <mat-label>API Key</mat-label>
<mat-select <mat-select
[(value)]="selected_api_key"> [(value)]="selected_api_key">
@for (key of api_keys; track key) { @for (key of api_keys; track key.id) {
<mat-option [value]="key">{{key}}</mat-option> <mat-option [value]="key.id">{{key.label}}</mat-option>
} }
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>

View File

@ -9,6 +9,7 @@ import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
import { HermesClientService } from '../hermes-client.service';
@Component({ @Component({
selector: 'tts-login', selector: 'tts-login',
@ -18,12 +19,12 @@ import { environment } from '../../environments/environment';
styleUrl: './tts-login.component.scss' styleUrl: './tts-login.component.scss'
}) })
export class TtsLoginComponent implements OnInit, OnDestroy { export class TtsLoginComponent implements OnInit, OnDestroy {
api_keys: string[]; api_keys: { id: string, label: string }[];
selected_api_key: string|undefined; selected_api_key: string|undefined;
private subscription: Subscription|undefined; private subscription: Subscription|undefined;
constructor(private events: EventService, private http: HttpClient, private router: Router) { constructor(private hermes: HermesClientService, private events: EventService, private http: HttpClient, private router: Router) {
this.api_keys = []; this.api_keys = [];
} }
@ -32,13 +33,25 @@ export class TtsLoginComponent implements OnInit, OnDestroy {
headers: { headers: {
'Authorization': 'Bearer ' + localStorage.getItem('jwt') 'Authorization': 'Bearer ' + localStorage.getItem('jwt')
} }
}).subscribe((data: any) => this.api_keys = data.map((d: any) => d.id)); }).subscribe((data: any) => this.api_keys = data);
this.subscription = this.events.listen('tts_login_ack', _ => { this.subscription = this.events.listen('tts_login_ack', _ => {
if (document.location.href.includes('/tts-login')) { if (document.location.href.includes('/tts-login')) {
this.router.navigate(['/policies']) this.router.navigate(['/policies'])
} }
}); });
this.events.listen('tts_logoff', _ => {
this.selected_api_key = undefined;
});
this.events.listen('impersonation', _ => {
this.selected_api_key = undefined;
this.http.get(environment.API_HOST + '/keys', {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
}
}).subscribe((data: any) => this.api_keys = data);
});
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -47,9 +60,10 @@ export class TtsLoginComponent implements OnInit, OnDestroy {
} }
login() { login() {
console.log('api key for login', this.selected_api_key)
if (!this.selected_api_key) if (!this.selected_api_key)
return; return;
this.events.emit('tts_login', this.selected_api_key); this.hermes.login(this.selected_api_key);
} }
} }