Skip to content

Commit

Permalink
Merge pull request #178 from ngageoint/auth-sysinfo
Browse files Browse the repository at this point in the history
Redact sensitive system information for non-admin users
  • Loading branch information
restjohn authored Sep 5, 2023
2 parents f3dd813 + 09255fc commit 5d8a5a5
Show file tree
Hide file tree
Showing 13 changed files with 669 additions and 324 deletions.
702 changes: 412 additions & 290 deletions CHANGELOG.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
import express from 'express'
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>>;


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

routes.route('/')
.get(async (req, res, next) => {
const appReq = createAppRequest(req)
const appReq = createAppRequest<systemInfoRequestType>(req)

const appRes = await appLayer.readSystemInfo(appReq)
if (appRes.success) {
return res.json(appRes.success)
Expand Down
16 changes: 12 additions & 4 deletions service/src/app.api/systemInfo/app.api.systemInfo.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import { SystemInfo } from '../../entities/systemInfo/entities.systemInfo'
import { InfrastructureError } from '../app.api.errors'
import { AppRequest, AppResponse } from '../app.api.global'
import { UserWithRole } from '../../permissions/permissions.role-based.base'
import { InfrastructureError, PermissionDeniedError } from '../app.api.errors'
import { AppRequest, AppRequestContext, AppResponse } from '../app.api.global'


export type ExoPrivilegedSystemInfo = SystemInfo
export type ExoRedactedSystemInfo = Omit<SystemInfo, 'environment'>
export type ExoSystemInfo = ExoPrivilegedSystemInfo | ExoRedactedSystemInfo

export interface ReadSystemInfoRequest extends AppRequest {}
export interface ReadSystemInfoRequest extends AppRequest {
context: AppRequestContext<UserWithRole>;
}
export interface ReadSystemInfoResponse extends AppResponse<ExoSystemInfo, InfrastructureError> {}

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

export interface SystemInfoAppLayer {
readSystemInfo: ReadSystemInfo
readSystemInfo: ReadSystemInfo;
permissionsService: SystemInfoPermissionService;
}

export interface SystemInfoPermissionService {
ensureReadSystemInfoPermission(context: AppRequestContext<UserWithRole>): Promise<null | PermissionDeniedError>;
}
24 changes: 18 additions & 6 deletions service/src/app.impl/systemInfo/app.impl.systemInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ 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 { SystemInfoPermissionService } from '../../app.api/systemInfo/app.api.systemInfo';
/**
* This factory function creates the implementation of the {@link api.ReadSystemInfo}
* application layer interface.
Expand All @@ -13,9 +15,9 @@ export function CreateReadSystemInfo(
config: any,
settingsModule: typeof Settings = Settings,
authConfigModule: typeof AuthenticationConfiguration = AuthenticationConfiguration,
authConfigTransformerModule: typeof AuthenticationConfigurationTransformer = AuthenticationConfigurationTransformer
authConfigTransformerModule: typeof AuthenticationConfigurationTransformer = AuthenticationConfigurationTransformer,
permissions: SystemInfoPermissionService
): api.ReadSystemInfo {

// appending the authentication strategies to the api
async function appendAuthenticationStrategies(
api: any,
Expand Down Expand Up @@ -45,9 +47,14 @@ export function CreateReadSystemInfo(
return async function readSystemInfo(
req: api.ReadSystemInfoRequest
): Promise<api.ReadSystemInfoResponse> {
// TODO: will need a permission check to determine what level of system
// information the requesting principal is allowed to see
const environment = await environmentService.readEnvironmentInfo();
const hasReadSystemInfoPermission =
(await permissions.ensureReadSystemInfoPermission(req.context)) === null;

let environment;
if (hasReadSystemInfoPermission) {
environment = await environmentService.readEnvironmentInfo();
}

const disclaimer = (await settingsModule.getSetting('disclaimer')) || {};
const contactInfo = (await settingsModule.getSetting('contactInfo')) || {};

Expand All @@ -57,10 +64,15 @@ export function CreateReadSystemInfo(
contactInfo: contactInfo
});

// Ensure the environment is removed if the user doesn't have permission
if (!hasReadSystemInfoPermission) {
delete apiConfig.environment;
}

const updatedApiConfig = await appendAuthenticationStrategies(apiConfig, {
whitelist: true
});

return AppResponse.success(updatedApiConfig as any);
};
}
}
11 changes: 8 additions & 3 deletions service/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import { CreateReadSystemInfo } from './app.impl/systemInfo/app.impl.systemInfo'
import AuthenticationConfiguration from "./models/authenticationconfiguration";
import AuthenticationConfigurationTransformer from "./transformers/authenticationconfiguration";
import { SystemInfoRoutes } from './adapters/systemInfo/adapters.systemInfo.controllers.web'
import { RoleBasedSystemInfoPermissionService } from './permissions/permissions.systemInfo'


export interface MageService {
Expand Down Expand Up @@ -478,15 +479,18 @@ function initFeedsAppLayer(repos: Repositories): AppLayer['feeds'] {
}

function initSystemInfoAppLayer(repos: Repositories): SystemInfoAppLayer {
const permissionsService = new RoleBasedSystemInfoPermissionService();
return {
readSystemInfo: CreateReadSystemInfo(
repos.enviromentInfo,
apiConfig,
Settings,
AuthenticationConfiguration,
AuthenticationConfigurationTransformer
)
}
AuthenticationConfigurationTransformer,
permissionsService
),
permissionsService
};
}

interface MageEventRequestContext extends AppRequestContext<UserDocument> {
Expand Down Expand Up @@ -531,6 +535,7 @@ 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
11 changes: 9 additions & 2 deletions service/src/entities/authorization/entities.permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ export enum SettingPermission {
UPDATE_SETTINGS = 'UPDATE_SETTINGS'
}


export enum SystemInfoPermission {
READ_SYSTEM_INFO = 'READ_SYSTEM_INFO',
}

export enum FeedsPermission {
FEEDS_LIST_SERVICE_TYPES = 'FEEDS_LIST_SERVICE_TYPES',
FEEDS_CREATE_SERVICE = 'FEEDS_CREATE_SERVICE',
Expand All @@ -100,8 +105,9 @@ export const allPermissions = Object.freeze({
...TeamPermission,
...SettingPermission,
...FeedsPermission,
...StaticIconPermission
})
...StaticIconPermission,
...SystemInfoPermission
});

export type AnyPermission =
| DevicePermission
Expand All @@ -115,6 +121,7 @@ export type AnyPermission =
| SettingPermission
| FeedsPermission
| StaticIconPermission
| SystemInfoPermission

const allPermissionsList = Object.freeze(Object.values(allPermissions))

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
Expand Up @@ -25,4 +25,4 @@ export interface SystemInfo {
environment: EnvironmentInfo
disclaimer: any // mongoose Document type
contactInfo: any
}
}
29 changes: 29 additions & 0 deletions service/src/migrations/030-add-read-system-info-permissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const mongoose = require('mongoose');
const RoleModel = mongoose.model('Role');

exports.id = 'add-read-system-info-permission';

exports.up = function(done) {
this.log('adding READ_SYSTEM_INFO permission to ADMIN_ROLE ...');

// Use $addToSet to ensure the permission is only added if it doesn't exist
RoleModel.updateOne(
{ name: 'ADMIN_ROLE' },
{ $addToSet: { permissions: 'READ_SYSTEM_INFO' } },
function(err) {
done(err);
}
);
};

exports.down = function(done) {
this.log('removing READ_SYSTEM_INFO permission from ADMIN_ROLE ...');

RoleModel.updateOne(
{ name: 'ADMIN_ROLE' },
{ $pull: { permissions: 'READ_SYSTEM_INFO' } },
function(err) {
done(err);
}
);
};
2 changes: 1 addition & 1 deletion service/src/permissions/permissions.icons.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { permissionDenied, PermissionDeniedError } from '../app.api/app.api.errors'
import { PermissionDeniedError } from '../app.api/app.api.errors'
import { AppRequestContext } from '../app.api/app.api.global'
import { StaticIconPermissionService } from '../app.api/icons/app.api.icons'
import { StaticIconPermission } from '../entities/authorization/entities.permissions'
Expand Down
22 changes: 22 additions & 0 deletions service/src/permissions/permissions.systemInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {
permissionDenied,
PermissionDeniedError
} from '../app.api/app.api.errors';
import { AppRequestContext } from '../app.api/app.api.global';
import { SystemInfoPermissionService } from '../app.api/systemInfo/app.api.systemInfo';
import { SystemInfoPermission } from '../entities/authorization/entities.permissions';
import {
UserWithRole,
ensureContextUserHasPermission
} from './permissions.role-based.base';

export class RoleBasedSystemInfoPermissionService implements SystemInfoPermissionService {
async ensureReadSystemInfoPermission(ctx: AppRequestContext<UserWithRole>): Promise<null | PermissionDeniedError> {
return ensureContextUserHasPermission(
ctx,
SystemInfoPermission.READ_SYSTEM_INFO
);
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { WebAppRequestFactory } from '../../../lib/adapters/adapters.controllers
import { Substitute as Sub, SubstituteOf, Arg } from '@fluffy-spoon/substitute';
import supertest from 'supertest';
import { SystemInfo } from '../../../lib/entities/systemInfo/entities.systemInfo';
import { SystemInfoAppLayer } from '../../../lib/app.api/systemInfo/app.api.systemInfo';
import {
SystemInfoAppLayer,
SystemInfoPermissionService
} from '../../../lib/app.api/systemInfo/app.api.systemInfo';
import { SystemInfoRoutes } from '../../../lib/adapters/systemInfo/adapters.systemInfo.controllers.web';

describe('SystemInfo web controller', () => {
Expand All @@ -17,12 +20,25 @@ describe('SystemInfo web controller', () => {
};

let client: supertest.SuperTest<supertest.Test>;
let appLayer: SubstituteOf<SystemInfoAppLayer>;

// Define and set up the mockPermissionsService
const mockPermissionsService = Sub.for<SystemInfoPermissionService>();
mockPermissionsService
.ensureReadSystemInfoPermission(Arg.any())
.returns(Promise.resolve(null));

let appLayer: SubstituteOf<SystemInfoAppLayer> = Sub.for<
SystemInfoAppLayer
>();


let appReqFactory: SubstituteOf<AppRequestFactoryHandle>;

beforeEach(function() {
// Reset appLayer for every test.
appLayer = Sub.for<SystemInfoAppLayer>();
appReqFactory = Sub.for<AppRequestFactoryHandle>();

const endpoint = express();
endpoint.use(function lookupUser(
req: express.Request,
Expand Down
Loading

0 comments on commit 5d8a5a5

Please sign in to comment.