Browse Source

Task/restrict health check of AI service to admin (#8326)

* Restrict health check of AI service to admin

* Update changelog
pull/8327/head^2
Thomas Kaul 8 hours ago
committed by GitHub
parent
commit
4fe5b8594f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 6
      apps/api/src/app/__snapshots__/app.module.spec.ts.snap
  3. 1
      apps/api/src/app/app.module.spec.ts
  4. 11
      apps/api/src/app/endpoints/health/health.controller.ts
  5. 17
      apps/client/src/app/pages/api/api-page.component.ts
  6. 38
      apps/client/src/app/pages/api/api-page.html

2
CHANGELOG.md

@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Restricted the health check endpoint of the AI service (`GET api/v1/health/ai`) to administrators
- Increased the timeout in the health check endpoint of the AI service to 30 seconds
- Deprecated the `api/v1/access` endpoints in favor of the `api/v1/accesses` endpoints - Deprecated the `api/v1/access` endpoints in favor of the `api/v1/accesses` endpoints
### Fixed ### Fixed

6
apps/api/src/app/__snapshots__/app.module.spec.ts.snap

@ -539,8 +539,12 @@ exports[`AppModule should keep the security of each route 1`] = `
"interceptors": [], "interceptors": [],
}, },
"GET /health/ai": { "GET /health/ai": {
"guards": [], "guards": [
"AuthGuard('jwt')",
"HasPermissionGuard",
],
"interceptors": [], "interceptors": [],
"permission": "accessAdminControl",
}, },
"GET /health/data-enhancer/:name": { "GET /health/data-enhancer/:name": {
"guards": [], "guards": [],

1
apps/api/src/app/app.module.spec.ts

@ -67,7 +67,6 @@ const PUBLIC_ROUTES = [
'GET /auth/oidc/callback', 'GET /auth/oidc/callback',
'GET /benchmarks', 'GET /benchmarks',
'GET /health', 'GET /health',
'GET /health/ai',
'GET /health/data-enhancer/:name', 'GET /health/data-enhancer/:name',
'GET /health/data-provider/:dataSource', 'GET /health/data-provider/:dataSource',
'GET /health/liveness', 'GET /health/liveness',

11
apps/api/src/app/endpoints/health/health.controller.ts

@ -1,4 +1,6 @@
import { AiService } from '@ghostfolio/api/app/endpoints/ai/ai.service'; import { AiService } from '@ghostfolio/api/app/endpoints/ai/ai.service';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { import {
AiServiceHealthResponse, AiServiceHealthResponse,
@ -6,6 +8,7 @@ import {
DataProviderHealthResponse, DataProviderHealthResponse,
LivenessHealthResponse LivenessHealthResponse
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import { import {
Controller, Controller,
@ -15,11 +18,14 @@ import {
Logger, Logger,
Param, Param,
Res, Res,
UseGuards,
UseInterceptors UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { Response } from 'express'; import { Response } from 'express';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import ms from 'ms';
import { HealthService } from './health.service'; import { HealthService } from './health.service';
@ -52,12 +58,15 @@ export class HealthController {
} }
@Get('ai') @Get('ai')
@HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getHealthOfAiService( public async getHealthOfAiService(
@Res() response: Response @Res() response: Response
): Promise<Response<AiServiceHealthResponse>> { ): Promise<Response<AiServiceHealthResponse>> {
try { try {
const { text } = await this.aiService.generateText({ const { text } = await this.aiService.generateText({
prompt: `Reply with the word "OK" and nothing else.` prompt: `Reply with the word "OK" and nothing else.`,
requestTimeout: ms('30 seconds')
}); });
if (text === 'OK') { if (text === 'OK') {

17
apps/client/src/app/pages/api/api-page.component.ts

@ -1,3 +1,4 @@
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { import {
HEADER_KEY_SKIP_INTERCEPTOR, HEADER_KEY_SKIP_INTERCEPTOR,
HEADER_KEY_TOKEN HEADER_KEY_TOKEN
@ -13,6 +14,7 @@ import {
MarketDataOfMarketsResponse, MarketDataOfMarketsResponse,
QuotesResponse QuotesResponse
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfFearAndGreedIndexComponent } from '@ghostfolio/ui/fear-and-greed-index'; import { GfFearAndGreedIndexComponent } from '@ghostfolio/ui/fear-and-greed-index';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@ -24,6 +26,7 @@ import {
} from '@angular/common/http'; } from '@angular/common/http';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef,
Component, Component,
DestroyRef, DestroyRef,
inject, inject,
@ -57,6 +60,7 @@ export class GfApiPageComponent implements OnInit {
FetchResult<DataProviderGhostfolioAssetProfileResponse> FetchResult<DataProviderGhostfolioAssetProfileResponse>
>; >;
protected dividends$: Observable<FetchResult<DividendsResponse['dividends']>>; protected dividends$: Observable<FetchResult<DividendsResponse['dividends']>>;
protected hasPermissionToAccessAdminControl: boolean;
protected historicalData$: Observable< protected historicalData$: Observable<
FetchResult<HistoricalResponse['historicalData']> FetchResult<HistoricalResponse['historicalData']>
>; >;
@ -72,13 +76,26 @@ export class GfApiPageComponent implements OnInit {
private apiKey: string; private apiKey: string;
private readonly changeDetectorRef = inject(ChangeDetectorRef);
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly http = inject(HttpClient); private readonly http = inject(HttpClient);
private readonly userService = inject(UserService);
public ngOnInit() { public ngOnInit() {
this.apiKey = this.apiKey =
prompt($localize`Please enter your Ghostfolio API key:`) ?? ''; prompt($localize`Please enter your Ghostfolio API key:`) ?? '';
this.userService.stateChanged
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => {
this.hasPermissionToAccessAdminControl = hasPermission(
state?.user?.permissions,
permissions.accessAdminControl
);
this.changeDetectorRef.markForCheck();
});
this.aiServiceHealth$ = this.fetchAiServiceHealth(); this.aiServiceHealth$ = this.fetchAiServiceHealth();
this.assetProfile$ = this.fetchAssetProfile({ symbol: 'AAPL' }); this.assetProfile$ = this.fetchAssetProfile({ symbol: 'AAPL' });
this.dividends$ = this.fetchDividends({ symbol: 'KO' }); this.dividends$ = this.fetchDividends({ symbol: 'KO' });

38
apps/client/src/app/pages/api/api-page.html

@ -1,24 +1,26 @@
<div class="container"> <div class="container">
<div class="mb-5 row"> <div class="mb-5 row">
<div class="col"> <div class="col">
<mat-card appearance="outlined" class="mb-3"> @if (hasPermissionToAccessAdminControl) {
<mat-card-header> <mat-card appearance="outlined" class="mb-3">
<mat-card-title>AI Service Health</mat-card-title> <mat-card-header>
</mat-card-header> <mat-card-title>AI Service Health</mat-card-title>
<mat-card-content> </mat-card-header>
@let aiServiceHealth = aiServiceHealth$ | async; <mat-card-content>
@if (isFetchFailure(aiServiceHealth)) { @let aiServiceHealth = aiServiceHealth$ | async;
🔴 {{ aiServiceHealth.fetchError }} @if (isFetchFailure(aiServiceHealth)) {
} @else if (aiServiceHealth) { 🔴 {{ aiServiceHealth.fetchError }}
🟢 {{ aiServiceHealth.status }} } @else if (aiServiceHealth) {
} @else { 🟢 {{ aiServiceHealth.status }}
<ngx-skeleton-loader } @else {
animation="pulse" <ngx-skeleton-loader
[theme]="{ height: '1.5rem', width: '100%' }" animation="pulse"
/> [theme]="{ height: '1.5rem', width: '100%' }"
} />
</mat-card-content> }
</mat-card> </mat-card-content>
</mat-card>
}
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-header> <mat-card-header>
<mat-card-title>Status</mat-card-title> <mat-card-title>Status</mat-card-title>

Loading…
Cancel
Save