Skip to content

Commit

Permalink
Conditional access to System Info (#192)
Browse files Browse the repository at this point in the history
* handleSearchFunction

* WIP - auth /api

* WIP

* WIP - proxy config

* remove artifact from stash pop

* WIP - customAuth app.ts

* Revert "WIP - customAuth app.ts"

This reverts commit 3c850f3fbc5b68cc9738f962c0172a6e502da818.

* working auth

* Conditionally display system version info in About component

* Include version data in SystemInfo API response regardless of user authentication

* Refactor SystemInfo response to handle ExoSystemInfo types"

* add auth strategy

* fix mongodb typo, config param removal for CreateReadSystemInfo test.

* api version with dependency injection

* Add mockApiVersion to system info tests

* remove comment
  • Loading branch information
kimura-developer authored Jan 24, 2024
1 parent 955441f commit 017ae1e
Show file tree
Hide file tree
Showing 12 changed files with 256 additions and 93 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@

import express from 'express'
import passport from 'passport';
import { WebAppRequestFactory } from '../adapters.controllers.web'
import { SystemInfoAppLayer } from '../../app.api/systemInfo/app.api.systemInfo'
import { AppRequest, AppRequestContext } from '../../app.api/app.api.global'
import { UserWithRole } from '../../permissions/permissions.role-based.base'

type systemInfoRequestType = AppRequest<UserWithRole, AppRequestContext<UserWithRole>>;
type SystemInfoRequestType = AppRequest<UserWithRole, AppRequestContext<UserWithRole>>;


export function SystemInfoRoutes(appLayer: SystemInfoAppLayer, createAppRequest: WebAppRequestFactory): express.Router {
Expand All @@ -14,14 +14,50 @@ export function SystemInfoRoutes(appLayer: SystemInfoAppLayer, createAppRequest:

routes.route('/')
.get(async (req, res, next) => {
const appReq = createAppRequest<systemInfoRequestType>(req)
const appReq = createAppRequest<SystemInfoRequestType>(req); // Define appReq
// Check for Authorization header
if (req.headers.authorization) {
// Directly use passport.authenticate with a custom callback
passport.authenticate(
'bearer',
{ session: false },
(err: any, user: any | false) => {
if (err) {
return next(err);
}
if (!user) {
// Authentication failed - Send a JSON response
return res.status(401).json({ error: 'Unauthorized access' });
}
// Authentication successful, attach user to request
req.user = user;
handleRequest(appReq, res, next, appLayer);
}
)(req, res, next);
} else {
// Proceed without authentication if no Authorization header
handleRequest(appReq, res, next, appLayer);
}

const appRes = await appLayer.readSystemInfo(appReq)
if (appRes.success) {
return res.json(appRes.success)
}
return next(appRes.error)
})

return routes
}

async function handleRequest(
req: SystemInfoRequestType,
res: express.Response,
next: express.NextFunction,
appLayer: SystemInfoAppLayer,
) {
try {
const appRes = await appLayer.readSystemInfo(req);
if (appRes.success) {
res.json(appRes.success);
} else {
next(appRes.error);
}
} catch (err) {
next(err);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class EnvironmentServiceImpl implements EnvironmentService {
}
return {
nodeVersion: this.nodeVersion,
monogdbVersion: this.mongodbVersion!,
mongodbVersion: this.mongodbVersion!,
}
}

Expand Down
6 changes: 4 additions & 2 deletions service/src/app.api/systemInfo/app.api.systemInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ export interface ReadSystemInfoRequest extends AppRequest {
export interface ReadSystemInfoResponse extends AppResponse<ExoSystemInfo, InfrastructureError> {}

export interface ReadSystemInfo {
(req: ReadSystemInfoRequest): Promise<ReadSystemInfoResponse>
(req: ReadSystemInfoRequest): Promise<
ReadSystemInfoResponse
>;
}

export interface SystemInfoAppLayer {
Expand All @@ -23,5 +25,5 @@ export interface SystemInfoAppLayer {
}

export interface SystemInfoPermissionService {
ensureReadSystemInfoPermission(context: AppRequestContext<UserWithRole>): Promise<null | PermissionDeniedError>;
ensureReadSystemInfoPermission(context: AppRequestContext<UserWithRole | null>): Promise<null | PermissionDeniedError>;
}
55 changes: 31 additions & 24 deletions service/src/app.impl/systemInfo/app.impl.systemInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@ import { EnvironmentService } from '../../entities/systemInfo/entities.systemInf
import * as Settings from '../../models/setting';
import * as AuthenticationConfiguration from '../../models/authenticationconfiguration';
import AuthenticationConfigurationTransformer from '../../transformers/authenticationconfiguration';
import { ExoPrivilegedSystemInfo, ExoRedactedSystemInfo, ExoSystemInfo, SystemInfoPermissionService } from '../../app.api/systemInfo/app.api.systemInfo';

export interface ApiVersion {
major: number;
minor: number;
micro: number;
}

import { SystemInfoPermissionService } from '../../app.api/systemInfo/app.api.systemInfo';
/**
* This factory function creates the implementation of the {@link api.ReadSystemInfo}
* application layer interface.
*/
export function CreateReadSystemInfo(
environmentService: EnvironmentService,
config: any,
versionInfo: ApiVersion,
settingsModule: typeof Settings = Settings,
authConfigModule: typeof AuthenticationConfiguration = AuthenticationConfiguration,
authConfigTransformerModule: typeof AuthenticationConfigurationTransformer = AuthenticationConfigurationTransformer,
Expand Down Expand Up @@ -43,36 +49,37 @@ export function CreateReadSystemInfo(
);
return apiCopy;
}

return async function readSystemInfo(
req: api.ReadSystemInfoRequest
req: api.ReadSystemInfoRequest
): Promise<api.ReadSystemInfoResponse> {
const hasReadSystemInfoPermission =
(await permissions.ensureReadSystemInfoPermission(req.context)) === null;

let environment;
if (hasReadSystemInfoPermission) {
environment = await environmentService.readEnvironmentInfo();
}
const isAuthenticated = req.context.requestingPrincipal() != null;

const disclaimer = (await settingsModule.getSetting('disclaimer')) || {};
const contactInfo = (await settingsModule.getSetting('contactInfo')) || {};
// Initialize with base system info
let systemInfoResponse: ExoRedactedSystemInfo = {
version: versionInfo,
disclaimer: (await settingsModule.getSetting('disclaimer')) || {},
contactInfo: (await settingsModule.getSetting('contactInfo')) || {}
};

const apiConfig = Object.assign({}, config.api, {
environment: environment,
disclaimer: disclaimer,
contactInfo: contactInfo
});
// Add environment details for authenticated users with permission
if (isAuthenticated) {
const hasReadSystemInfoPermission =
(await permissions.ensureReadSystemInfoPermission(req.context)) === null;

// Ensure the environment is removed if the user doesn't have permission
if (!hasReadSystemInfoPermission) {
delete apiConfig.environment;
if (hasReadSystemInfoPermission) {
const environmentInfo = await environmentService.readEnvironmentInfo();
systemInfoResponse = {
...systemInfoResponse,
environment: environmentInfo
} as ExoPrivilegedSystemInfo;
}
}

const updatedApiConfig = await appendAuthenticationStrategies(apiConfig, {
whitelist: true
// Apply authentication strategies to the system info response
const updatedApiConfig = await appendAuthenticationStrategies(systemInfoResponse, {
whitelist: true
});

return AppResponse.success(updatedApiConfig as any);
return AppResponse.success(updatedApiConfig as ExoSystemInfo); // Cast to ExoSystemInfo
};
}
4 changes: 2 additions & 2 deletions service/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,10 +501,11 @@ function initFeedsAppLayer(repos: Repositories): AppLayer['feeds'] {

function initSystemInfoAppLayer(repos: Repositories): SystemInfoAppLayer {
const permissionsService = new RoleBasedSystemInfoPermissionService()
const versionInfo = apiConfig.api.version
return {
readSystemInfo: CreateReadSystemInfo(
repos.enviromentInfo,
apiConfig,
versionInfo,
Settings,
AuthenticationConfiguration,
AuthenticationConfigurationTransformer,
Expand Down Expand Up @@ -573,7 +574,6 @@ async function initWebLayer(repos: Repositories, app: AppLayer, webUIPlugins: st
])
const systemInfoRoutes = SystemInfoRoutes(app.systemInfo, appRequestFactory)
webController.use('/api', [
bearerAuth,
systemInfoRoutes
])
const observationRequestFactory: ObservationWebAppRequestFactory = <Params extends object | undefined>(req: express.Request, params: Params) => {
Expand Down
2 changes: 1 addition & 1 deletion service/src/entities/systemInfo/entities.systemInfo.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

export interface EnvironmentInfo {
nodeVersion: string
monogdbVersion: string
mongodbVersion: string
// TODO: maybe relavant environment variables? redact sensitive values
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describe('SystemInfo web controller', () => {
const expected: SystemInfo = {
environment: {
nodeVersion: 'test',
monogdbVersion: 'test'
mongodbVersion: 'test'
},
disclaimer: {},
contactInfo: {},
Expand Down
14 changes: 11 additions & 3 deletions service/test/app/systemInfo/app.systemInfo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
SystemInfo
} from '../../../lib/entities/systemInfo/entities.systemInfo';
import { Substitute, Arg } from '@fluffy-spoon/substitute';
import { CreateReadSystemInfo } from '../../../lib/app.impl/systemInfo/app.impl.systemInfo';
import { CreateReadSystemInfo, ApiVersion } from '../../../lib/app.impl/systemInfo/app.impl.systemInfo';
import * as Settings from '../../../lib/models/setting';
import * as AuthenticationConfiguration from '../../../lib/models/authenticationconfiguration';
import * as AuthenticationConfigurationTransformer from '../../../lib/transformers/authenticationconfiguration';
Expand All @@ -13,16 +13,23 @@ import { ReadSystemInfo, ReadSystemInfoRequest } from '../../../lib/app.api/syst
import { RoleBasedSystemInfoPermissionService } from '../../../lib/permissions/permissions.systemInfo';
import { SystemInfoPermission } from '../../../lib/entities/authorization/entities.permissions';

// Mocked environment info, disclaimer, contactInfo, and API version
const mockNodeVersion = '14.16.1';
const mockMongoDBVersion = '4.2.0';

const mockEnvironmentInfo: EnvironmentInfo = {
nodeVersion: mockNodeVersion,
monogdbVersion: mockMongoDBVersion
mongodbVersion: mockMongoDBVersion
};
const mockDisclaimer = {};
const mockContactInfo = {};
const mockVersionInfo: ApiVersion = {
major: 1,
minor: 2,
micro: 3
};

// Mocked user with role having READ_SYSTEM_INFO permission
const mockUserWithRole = ({
_id: 'mockObjectId',
id: 'testUserId',
Expand All @@ -41,6 +48,7 @@ const mockUserWithRole = ({
lastUpdated: new Date()
} as unknown) as UserWithRole;

// Mocked user without READ_SYSTEM_INFO permission
const mockUserWithoutPermission = ({
_id: 'mockObjectId2',
id: 'testUserId2',
Expand Down Expand Up @@ -126,7 +134,7 @@ describe('CreateReadSystemInfo', () => {
readEnvironmentInfo: () => Promise.resolve(mockEnvironmentInfo),
readDependencies: () => Promise.resolve({})
},
mockConfig,
mockVersionInfo,
mockedSettingsModule,
mockedAuthConfigModule,
mockedAuthConfigTransformerModule,
Expand Down
14 changes: 8 additions & 6 deletions web-app/src/ng1/about/about.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div class="content-flex about-content">
<h2>About MAGE Server {{$ctrl.serverVersion.major}} {{$ctrl.serverVersion.minor}} {{$ctrl.serverVersion.micro}}</h2>
<h2>About MAGE Server {{$ctrl.serverVersion.major}}.{{$ctrl.serverVersion.minor}}.{{$ctrl.serverVersion.micro}}</h2>
<p>
MAGE is a dynamic, secure, mobile situational awareness and field data collection platform that supports
low-bandwidth and disconnected users. MAGE can integrate with existing command centers and common operating
Expand All @@ -26,11 +26,13 @@ <h2 class="top-gap-l">Mobile Applications</h2>
<h2 class="top-gap-l">API</h2>
<p>Browse and try the MAGE API live with <a ui-sref="swagger">Swagger UI.</a></p>
<p><strong>Important:</strong> Swagger interactive documentation will modify MAGE data via API calls; please be careful with <strong>POST/PUT/DELETE</strong> operations.</p>
<h2>System</h2>
<ul>
<li>Node Version: {{$ctrl.nodeVersion}} </li>
<li>MongoDB Version: {{$ctrl.mongodbVersion}} </li>
</ul>
<div ng-if="$ctrl.nodeVersion && $ctrl.mongodbVersion">
<h2>System</h2>
<ul>
<li>Node Version: {{$ctrl.nodeVersion}}</li>
<li>MongoDB Version: {{$ctrl.mongodbVersion}}</li>
</ul>
</div>
<h2 class="top-gap-l">Acknowledgements</h2>
<div class="row bottom-gap-l">
<div class="col-md-6">
Expand Down
Loading

0 comments on commit 017ae1e

Please sign in to comment.