Skip to content

fix: prevent auth disable #147

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Nov 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ MAGE adheres to [Semantic Versioning](http://semver.org/).

##### Bug fixes
* Fix single observation download
* Protect against disabling all authentications

## [6.2.0](https://github.com/ngageoint/mage-server/releases/tag/6.2.0)
##### Breaking Changes
Expand Down
36 changes: 33 additions & 3 deletions service/src/routes/authenticationconfigurations.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,34 @@ module.exports = function (app, security) {
const passport = security.authentication.passport;
const blacklist = AuthenticationConfiguration.blacklist;

async function isAllowed(req, res, next) {
if (req.method === 'DELETE' || req.body && req.body.enabled === 'false') {

let configToModifyId;
let method = 'disable';
if (req.method === 'DELETE') {
method = 'delete';
configToModifyId = req.params.id.toString();
} else {
configToModifyId = req.body._id.toString();
}

const configs = await AuthenticationConfiguration.getAllConfigurations();
let atLeastOneConfigEnabled = false;
configs.forEach(config => {
if (config.enabled && config._id.toString() !== configToModifyId) {
atLeastOneConfigEnabled = true;
}
});

if (!atLeastOneConfigEnabled) {
log.error('Cannot ' + method + ' ' + configToModifyId + ', since this would leave no enabled authentications');
return res.status(403).send('At least 1 authentication must be enabled');
}
}
return next();
}

app.get(
'/api/authentication/configuration/',
passport.authenticate('bearer'),
Expand All @@ -34,7 +62,7 @@ module.exports = function (app, security) {
});

const promises = [];

filtered.forEach(config => {
promises.push(SecurePropertyAppender.appendToConfig(config));
});
Expand Down Expand Up @@ -67,6 +95,7 @@ module.exports = function (app, security) {
'/api/authentication/configuration/:id',
passport.authenticate('bearer'),
access.authorize('UPDATE_AUTH_CONFIG'),
isAllowed,
function (req, res, next) {
const updatedConfig = {
_id: req.body._id,
Expand All @@ -86,7 +115,7 @@ module.exports = function (app, security) {

Object.keys(settings).forEach(key => {
if (blacklist && blacklist.indexOf(key.toLowerCase()) != -1) {
if(AuthenticationConfiguration.secureMask !== settings[key]) {
if (AuthenticationConfiguration.secureMask !== settings[key]) {
securityData[key] = settings[key];
}
} else {
Expand Down Expand Up @@ -157,7 +186,7 @@ module.exports = function (app, security) {

Object.keys(settings).forEach(key => {
if (blacklist && blacklist.indexOf(key.toLowerCase()) != -1) {
if(AuthenticationConfiguration.secureMask !== settings[key]) {
if (AuthenticationConfiguration.secureMask !== settings[key]) {
securityData[key] = settings[key];
}
} else {
Expand Down Expand Up @@ -218,6 +247,7 @@ module.exports = function (app, security) {
'/api/authentication/configuration/:id',
passport.authenticate('bearer'),
access.authorize('UPDATE_AUTH_CONFIG'),
isAllowed,
function (req, res, next) {

Authentication.getAuthenticationsByAuthConfigId(req.params.id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class AuthenticationDeleteComponent implements AfterViewInit {
this.dialogRef.close('delete');
}).catch((err: any) => {
console.error(err);
this.dialogRef.close('cancel');
this.dialogRef.close('error');
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,132 +7,137 @@ import { StateService } from '@uirouter/angular';
import { AuthenticationDeleteComponent } from './admin-authentication-delete/admin-authentication-delete.component';

@Component({
selector: 'admin-authentication',
templateUrl: 'admin-authentication.component.html',
styleUrls: ['./admin-authentication.component.scss']
selector: 'admin-authentication',
templateUrl: 'admin-authentication.component.html',
styleUrls: ['./admin-authentication.component.scss']
})
export class AdminAuthenticationComponent implements OnInit, OnChanges {
@Output() saveComplete = new EventEmitter<boolean>();
@Output() onDirty = new EventEmitter<boolean>();
@Input() beginSave: any;

teams: any[] = [];
events: any[] = [];

strategies: Strategy[] = [];

hasAuthConfigEditPermission: boolean;

constructor(
private dialog: MatDialog,
private stateService: StateService,
@Inject(Team)
public team: any,
@Inject(Event)
public event: any,
@Inject(LocalStorageService)
public localStorageService: any,
@Inject(AuthenticationConfigurationService)
private authenticationConfigurationService: any,
@Inject(UserService)
private userService: { myself: { role: { permissions: Array<string> } } }) {
this.hasAuthConfigEditPermission = _.contains(this.userService.myself.role.permissions, 'UPDATE_AUTH_CONFIG');
}

ngOnInit(): void {
const configsPromise = this.authenticationConfigurationService.getAllConfigurations({ includeDisabled: true });
const teamsPromise = this.team.query({ state: 'all', populate: false }).$promise;
const eventsPromise = this.event.query({ state: 'all', populate: false }).$promise;

Promise.all([configsPromise, teamsPromise, eventsPromise]).then(result => {
//Remove event teams
this.teams = result[1].filter(function (team: any): boolean {
return team.teamEventId === undefined;
});
this.events = result[2];

const unsortedStrategies = result[0] ? result[0].data : [];
this.processUnsortedStrategies(unsortedStrategies);
}).catch(err => {
console.log(err);
});
}

private processUnsortedStrategies(unsortedStrategies: Strategy[]): void {
this.strategies = _.sortBy(unsortedStrategies, 'title');

this.strategies.forEach(strategy => {
if (strategy.settings.newUserEvents) {
strategy.settings.newUserEvents = strategy.settings.newUserEvents.filter(id => {
return this.events.some(event => event.id === id)
});
}
if (strategy.settings.newUserTeams) {
// Remove any teams and events that no longer exist
strategy.settings.newUserTeams = strategy.settings.newUserTeams.filter(id => {
return this.teams.some(team => team.id === id)
});
}
if (strategy.icon) {
strategy.icon = "data:image/png;base64," + strategy.icon;
}
});
}

ngOnChanges(changes: SimpleChanges): void {
if (changes.beginSave && !changes.beginSave.firstChange) {
this.save();
}
}

private save(): void {
const promises = [];
this.strategies.forEach(strategy => {
if (strategy.isDirty) {
promises.push(this.authenticationConfigurationService.updateConfiguration(strategy));
}
@Output() saveComplete = new EventEmitter<boolean>();
@Output() deleteComplete = new EventEmitter<boolean>();
@Output() onDirty = new EventEmitter<boolean>();
@Input() beginSave: any;

teams: any[] = [];
events: any[] = [];

strategies: Strategy[] = [];

readonly hasAuthConfigEditPermission: boolean;

constructor(
private dialog: MatDialog,
private stateService: StateService,
@Inject(Team)
public team: any,
@Inject(Event)
public event: any,
@Inject(LocalStorageService)
public localStorageService: any,
@Inject(AuthenticationConfigurationService)
private authenticationConfigurationService: any,
@Inject(UserService)
private userService: { myself: { role: { permissions: Array<string> } } }) {
this.hasAuthConfigEditPermission = _.contains(this.userService.myself.role.permissions, 'UPDATE_AUTH_CONFIG');
}

ngOnInit(): void {
const configsPromise = this.authenticationConfigurationService.getAllConfigurations({ includeDisabled: true });
const teamsPromise = this.team.query({ state: 'all', populate: false }).$promise;
const eventsPromise = this.event.query({ state: 'all', populate: false }).$promise;

Promise.all([configsPromise, teamsPromise, eventsPromise]).then(result => {
// Remove event teams
this.teams = result[1].filter(function (team: any): boolean {
return team.teamEventId === undefined;
});
this.events = result[2];

const unsortedStrategies = result[0] ? result[0].data : [];
this.processUnsortedStrategies(unsortedStrategies);
}).catch(err => {
console.log(err);
});
}

private processUnsortedStrategies(unsortedStrategies: Strategy[]): void {
this.strategies = _.sortBy(unsortedStrategies, 'title');

this.strategies.forEach(strategy => {
if (strategy.settings.newUserEvents) {
strategy.settings.newUserEvents = strategy.settings.newUserEvents.filter(id => {
return this.events.some(event => event.id === id)
});

if (promises.length > 0) {
Promise.all(promises).then(() => {
return this.authenticationConfigurationService.getAllConfigurations({ includeDisabled: true });
}).then(strategies => {
this.processUnsortedStrategies(strategies.data);
this.saveComplete.emit(true);
}).catch(err => {
console.log(err);
this.saveComplete.emit(false);
});
}
this.onStrategyDirty(false);
}

deleteStrategy(strategy: Strategy): void {
this.dialog.open(AuthenticationDeleteComponent, {
width: '500px',
data: strategy,
autoFocus: false
}).afterClosed().subscribe(result => {
if (result === 'delete') {
this.authenticationConfigurationService.getAllConfigurations().then(configs => {
this.processUnsortedStrategies(configs.data);
}).catch(err => {
console.error(err);
})
}
}
if (strategy.settings.newUserTeams) {
// Remove any teams and events that no longer exist
strategy.settings.newUserTeams = strategy.settings.newUserTeams.filter(id => {
return this.teams.some(team => team.id === id)
});
}
if (strategy.icon) {
strategy.icon = 'data:image/png;base64,' + strategy.icon;
}
});
}

ngOnChanges(changes: SimpleChanges): void {
if (changes.beginSave && !changes.beginSave.firstChange) {
this.save();
}

createAuthentication(): void {
this.stateService.go('admin.authenticationCreate')
}

onStrategyDirty(isDirty: boolean): void {
this.onDirty.emit(isDirty);
}

onAuthenticationToggled(strategy: Strategy): void {
strategy.isDirty = true;
this.onStrategyDirty(true)
}

private save(): void {
const promises = [];
this.strategies.forEach(strategy => {
if (strategy.isDirty) {
promises.push(this.authenticationConfigurationService.updateConfiguration(strategy));
}
});

if (promises.length > 0) {
Promise.all(promises).then(() => {
return this.authenticationConfigurationService.getAllConfigurations({ includeDisabled: true });
}).then(strategies => {
this.processUnsortedStrategies(strategies.data);
this.saveComplete.emit(true);
}).catch(err => {
console.log(err);
this.saveComplete.emit(false);
});
}
}
this.onStrategyDirty(false);
}

deleteStrategy(strategy: Strategy): void {
this.dialog.open(AuthenticationDeleteComponent, {
width: '500px',
data: strategy,
autoFocus: false
}).afterClosed().subscribe(result => {
if (result === 'delete') {
this.authenticationConfigurationService.getAllConfigurations().then(configs => {
this.processUnsortedStrategies(configs.data);
this.deleteComplete.emit(true);
}).catch((err: any) => {
console.error(err);
this.deleteComplete.emit(false);
})
} else if (result === 'error') {
this.deleteComplete.emit(false);
}
});
}

createAuthentication(): void {
this.stateService.go('admin.authenticationCreate')
}

onStrategyDirty(isDirty: boolean): void {
this.onDirty.emit(isDirty);
}

onAuthenticationToggled(strategy: Strategy): void {
strategy.isDirty = true;
this.onStrategyDirty(true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<mat-tab-group color="primary" mat-stretch-tabs>
<mat-tab label="Authentication">
<admin-authentication (saveComplete)="onAuthenticationSaved($event)"
(onDirty)="onAuthenticationDirty($event)" [beginSave]="(onSave)">
(onDirty)="onAuthenticationDirty($event)" (deleteComplete)="onAuthenticationDeleted($event)" [beginSave]="(onSave)">
</admin-authentication>
</mat-tab>
<mat-tab label="Banner">
Expand Down
Loading