Skip to content

Commit 4cc7fed

Browse files
committed
Merge branch 'develop' of github.com:ngageoint/mage-server into develop
2 parents 0f6e8a8 + ccfefbd commit 4cc7fed

25 files changed

+628
-102
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,15 @@ MAGE adheres to [Semantic Versioning](http://semver.org/).
55
---
66
## Pending on [`develop`](https://github.com/ngageoint/mage-server/tree/develop)
77

8+
##### Features
9+
10+
##### Bug Fixes
11+
12+
## [5.1.4](https://github.com/ngageoint/mage-server/releases/tag/5.1.4) (08-06-2018)
13+
814
##### Features
915
* Added GeoPackage layer support. Added server side XYZ urls to retrieve imagery from GeoPackages. The web client will use these URLs to display imagery tiles from a GeoPackage. Added server side url to retrieve vector tiles from feature GeoPackages. The web client will use these to display vector tiles (with the aid of a leaflet plugin).
16+
* User account lock settings. Admins can now configure account lock/disable settings for local accounts.
1017
* Replace local [environment](environment) NPM packages with a single Node module
1118
** No more manually deleting the local module from the `node_modules` directory for script changes
1219
* Load values from [environment variables](README.md#mage-environment-settings) instead of only from the script

api/user.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ var UserModel = require('../models/user')
22
, log = require('winston')
33
, TokenModel = require('../models/token')
44
, LoginModel = require('../models/login')
5-
, EventModel = require('../models/event')
65
, DeviceModel = require('../models/device')
76
, path = require('path')
87
, fs = require('fs-extra')

authentication/local.js

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module.exports = function(app, passport, provisioning) {
22

33
var log = require('winston')
4+
, moment = require('moment')
45
, LocalStrategy = require('passport-local').Strategy
56
, User = require('../models/user')
67
, api = require('../api')
@@ -17,40 +18,69 @@ module.exports = function(app, passport, provisioning) {
1718
next();
1819
}
1920

21+
function authenticate(req, res, next) {
22+
passport.authenticate('local', function(err, user, info) {
23+
if (err) return next(err);
24+
25+
info = info || {};
26+
if (!user) {
27+
return res.status(401).send(info.message);
28+
}
29+
30+
req.user = user;
31+
next();
32+
})(req, res, next);
33+
}
34+
2035
passport.use(new LocalStrategy(
2136
function(username, password, done) {
2237
User.getUserByUsername(username, function(err, user) {
2338
if (err) { return done(err); }
2439

2540
if (!user) {
2641
log.warn('Failed login attempt: User with username ' + username + ' not found');
27-
return done(null, false, { message: "User with username '" + username + "' not found" });
42+
return done(null, false);
2843
}
2944

3045
if (!user.active) {
3146
log.warn('Failed user login attempt: User ' + user.username + ' is not active');
32-
return done(null, false, { message: "User with username '" + username + "' not active" });
47+
return done(null, false);
48+
}
49+
50+
if (!user.enabled) {
51+
log.warn('Failed user login attempt: User ' + user.username + ' account is disabled.');
52+
return done(null, false, { message: 'Your account has been disabled, please contact a MAGE administrator for assistance.' });
53+
}
54+
55+
let security = user.authentication.security;
56+
if (security.locked && moment().isBefore(moment(security.lockedUntil))) {
57+
log.warn('Failed user login attempt: User ' + user.username + ' account is locked until ' + security.lockedUntil);
58+
return done(null, false, { message: 'Your account has been temporarily locked, please try again later or contact a MAGE administrator for assistance.' });
3359
}
3460

3561
user.validPassword(password, function(err, isValid) {
3662
if (err) {
3763
return done(err);
3864
}
3965

40-
if (!isValid) {
66+
if (isValid) {
67+
User.validLogin(user)
68+
.then(() => done(null, user))
69+
.catch(err => done(err));
70+
} else {
4171
log.warn('Failed login attempt: User with username ' + username + ' provided an invalid password');
42-
return done(null, false);
72+
User.invalidLogin(user)
73+
.then(() => done(null, false, {message: 'Please check your username, UID, and password and try again.'}))
74+
.catch(err => done(err));
4375
}
44-
45-
return done(null, user);
4676
});
4777
});
4878
}
4979
));
5080

5181
app.post(
5282
'/api/login',
53-
passport.authenticate('local'),
83+
authenticate,
5484
provisioning.provision.check(provisioning.strategy),
5585
parseLoginMetadata,
5686
function(req, res) {

models/setting.js

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
var mongoose = require('mongoose');
1+
const mongoose = require('mongoose');
22

33
// Creates a new Mongoose Schema object
4-
var Schema = mongoose.Schema;
4+
const Schema = mongoose.Schema;
55

6-
var SettingSchema = new Schema({
6+
const SettingSchema = new Schema({
77
type: { type: String, required: true, unique: true },
88
settings: Schema.Types.Mixed
99
},{
@@ -22,28 +22,20 @@ SettingSchema.set("toJSON", {
2222
});
2323

2424
// Creates the Model for the Setting Schema
25-
var Setting = mongoose.model('Setting', SettingSchema);
25+
const Setting = mongoose.model('Setting', SettingSchema);
2626

27-
exports.getSettings = function(callback) {
28-
Setting.find({}, function(err, settings) {
29-
callback(err, settings);
30-
});
27+
exports.getSettings = function() {
28+
return Setting.find({}).exec();
3129
};
3230

33-
exports.getSetting = function(type, callback) {
34-
Setting.findOne({type: type}, function(err, setting) {
35-
callback(err, setting);
36-
});
31+
exports.getSetting = function(type) {
32+
return Setting.findOne({type: type}).exec();
3733
};
3834

39-
exports.getSettingByType = function(type, callback) {
40-
Setting.findOne({type: type}, function(err, setting) {
41-
callback(err, setting);
42-
});
35+
exports.getSettingByType = function(type) {
36+
return Setting.findOne({type: type}).exec();
4337
};
4438

45-
exports.updateSettingByType = function(type, update, callback) {
46-
Setting.findOneAndUpdate({type: type}, update, {new: true, upsert: true}, function(err, setting) {
47-
callback(err, setting);
48-
});
39+
exports.updateSettingByType = function(type, update) {
40+
return Setting.findOneAndUpdate({type: type}, update, {new: true, upsert: true}).exec();
4941
};

models/user.js

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
var mongoose = require('mongoose')
22
, async = require("async")
33
, hasher = require('../utilities/pbkdf2')()
4-
, Token = require('../models/token')
5-
, Login = require('../models/login')
6-
, Event = require('../models/event')
7-
, Team = require('../models/team')
8-
, Observation = require('../models/observation')
9-
, Location = require('../models/location')
10-
, CappedLocation = require('../models/cappedLocation');
4+
, moment = require('moment')
5+
, Setting = require('./setting')
6+
, Token = require('./token')
7+
, Login = require('./login')
8+
, Event = require('./event')
9+
, Team = require('./team')
10+
, Observation = require('./observation')
11+
, Location = require('./location')
12+
, CappedLocation = require('./cappedLocation');
1113

1214
// Creates a new Mongoose Schema object
1315
var Schema = mongoose.Schema;
@@ -40,13 +42,20 @@ var UserSchema = new Schema({
4042
relativePath: { type: String, required: false }
4143
},
4244
active: { type: Boolean, required: true },
45+
enabled: { type: Boolean, default: true, required: true },
4346
roleId: { type: Schema.Types.ObjectId, ref: 'Role', required: true },
4447
status: { type: String, required: false, index: 'sparse' },
4548
recentEventIds: [{type: Number, ref: 'Event'}],
4649
authentication: {
4750
type: { type: String, required: false },
4851
id: { type: String, required: false },
49-
password: { type: String, required: false }
52+
password: { type: String, required: false },
53+
security: {
54+
locked: { type: Boolean },
55+
lockedUntil: { type: Date },
56+
invalidLoginAttempts: { type: Number, default: 0 },
57+
numberOfTimesLocked: { type: Number, default: 0 }
58+
}
5059
}
5160
},{
5261
versionKey: false,
@@ -183,7 +192,6 @@ UserSchema.pre('remove', function(next) {
183192
},
184193
eventAcl: function(done) {
185194
Event.removeUserFromAllAcls(user, function(err) {
186-
console.log('event acl remove', err);
187195
done(err);
188196
});
189197
},
@@ -192,7 +200,6 @@ UserSchema.pre('remove', function(next) {
192200
}
193201
},
194202
function(err) {
195-
console.log('remove all user stuff with err', err);
196203
next(err);
197204
});
198205
});
@@ -332,14 +339,46 @@ exports.updateUser = function(user, callback) {
332339
};
333340

334341
exports.deleteUser = function(user, callback) {
335-
console.log('delete user', user._id);
336342
user.remove(function(err, removedUser) {
337-
// console.log('deleted user', err);
338-
339343
callback(err, removedUser);
340344
});
341345
};
342346

347+
exports.invalidLogin = function(user) {
348+
return Setting.getSetting('security')
349+
.then(securitySettings => {
350+
let {enabled, max, interval, threshold} = securitySettings.settings.accountLock;
351+
if (!enabled) return Promise.resolve(user);
352+
353+
let security = user.authentication.security;
354+
const invalidLoginAttempts = security.invalidLoginAttempts + 1;
355+
if (invalidLoginAttempts >= threshold) {
356+
const numberOfTimesLocked = security.numberOfTimesLocked + 1;
357+
if (numberOfTimesLocked >= max) {
358+
user.enabled = false;
359+
user.authentication.security = {};
360+
} else {
361+
user.authentication.security = {
362+
locked: true,
363+
numberOfTimesLocked: numberOfTimesLocked,
364+
lockedUntil: moment().add(interval, 'seconds').toDate()
365+
};
366+
}
367+
} else {
368+
security.invalidLoginAttempts = invalidLoginAttempts;
369+
security.locked = undefined;
370+
security.lockedUntil = undefined;
371+
}
372+
373+
return user.save();
374+
});
375+
};
376+
377+
exports.validLogin = function(user) {
378+
user.authentication.security = {};
379+
return user.save();
380+
};
381+
343382
exports.setStatusForUser = function(user, status, callback) {
344383
var update = { status: status };
345384
User.findByIdAndUpdate(user._id, update, {new: true}, function(err, user) {

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mage-server",
3-
"version": "5.1.3",
3+
"version": "5.1.4",
44
"displayName": "MAGE Server",
55
"description": "Geospatial situation awareness application.",
66
"keywords": [
@@ -77,7 +77,7 @@
7777
"proxyquire": "~2.0.1",
7878
"sinon": "~4.5.0",
7979
"sinon-chai": "~3.0.0",
80-
"sinon-mongoose": "2.1.1",
80+
"sinon-mongoose": "2.2.1",
8181
"superagent": "1.4.0",
8282
"supertest": "1.1.0"
8383
},

public/app/admin/settings/settings.controller.js

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,33 @@ var _ = require('underscore');
22

33
module.exports = AdminSettingsController;
44

5-
AdminSettingsController.$inject = ['$scope', 'Settings', 'LocalStorageService'];
5+
AdminSettingsController.$inject = ['$scope', 'Api', 'Settings', 'LocalStorageService'];
66

7-
function AdminSettingsController($scope, Settings, LocalStorageService) {
7+
function AdminSettingsController($scope, Api, Settings, LocalStorageService) {
88
$scope.token = LocalStorageService.getToken();
9-
$scope.pill = 'banner';
9+
$scope.pill = 'security';
10+
11+
$scope.accountLock = {};
12+
$scope.accountLockChoices = [{
13+
title: 'Off',
14+
description: 'Do not lock MAGE user accounts.',
15+
value: false
16+
},{
17+
title: 'On',
18+
description: 'Lock MAGE user accounts for defined time \n after defined number of invalid login attempts.',
19+
value: true
20+
}];
21+
22+
$scope.maxLock = {};
23+
$scope.maxLockChoices = [{
24+
title: 'Off',
25+
description: 'Do not disable MAGE user accounts.',
26+
value: false
27+
},{
28+
title: 'On',
29+
description: 'Disable MAGE user accounts after account has been locked defined number of times.',
30+
value: true
31+
}];
1032

1133
$scope.minicolorSettings = {
1234
position: 'bottom right',
@@ -22,11 +44,26 @@ function AdminSettingsController($scope, Settings, LocalStorageService) {
2244
footerBackgroundColor: 'FFFFFF'
2345
};
2446

47+
Api.get(function(api) {
48+
var authenticationStrategies = api.authenticationStrategies || {};
49+
$scope.local = authenticationStrategies.local;
50+
$scope.pill = authenticationStrategies.local ? 'security' : 'banner';
51+
});
52+
2553
Settings.query(function(settings) {
2654
$scope.settings = _.indexBy(settings, 'type');
2755

2856
$scope.banner = $scope.settings.banner ? $scope.settings.banner.settings : {};
2957
$scope.disclaimer = $scope.settings.disclaimer ? $scope.settings.disclaimer.settings : {};
58+
$scope.security = $scope.settings.security ? $scope.settings.security.settings : {};
59+
60+
if (!$scope.security.accountLock) {
61+
$scope.security.accountLock = {
62+
enabled: false
63+
};
64+
}
65+
66+
$scope.maxLock.enabled = $scope.security.accountLock && $scope.security.accountLock.max !== undefined;
3067
});
3168

3269
$scope.saveBanner = function() {
@@ -53,6 +90,22 @@ function AdminSettingsController($scope, Settings, LocalStorageService) {
5390
});
5491
};
5592

93+
$scope.saveSecurity = function() {
94+
if (!$scope.maxLock.enabled) {
95+
delete $scope.security.accountLock.max;
96+
}
97+
98+
Settings.update({type: 'security'}, $scope.security, function() {
99+
$scope.saved = true;
100+
$scope.saving = false;
101+
$scope.saveStatus = 'Security settings successfully saved.';
102+
debounceHideSave();
103+
}, function() {
104+
$scope.saving = false;
105+
$scope.saveStatus = 'Failed to save security settings.';
106+
});
107+
};
108+
56109
var debounceHideSave = _.debounce(function() {
57110
$scope.$apply(function() {
58111
$scope.saved = false;

0 commit comments

Comments
 (0)