Skip to content
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

Feature/chat locker 6 #9

Merged
merged 10 commits into from
Jul 17, 2015
Merged
1 change: 1 addition & 0 deletions .meteor/packages
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jquery
less
meteor-platform
reactive-var
reactive-dict
service-configuration

chrismbeckett:toastr
Expand Down
17 changes: 15 additions & 2 deletions client/stylesheets/base.less
Original file line number Diff line number Diff line change
Expand Up @@ -1748,11 +1748,24 @@ a.github-fork {
color: @primary-font-color;
}
}
.security-banner.U { color: white; background-color: green }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can create variables for these in _variables.less and reference them here. Then we can reuse them in other places.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean variables for the colors? Like, say, "@unclassified-color"?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

.security-banner.C { color: white; background-color: blue }
.security-banner.S { color: white; background-color: red }
.security-banner.TS { color: white; background-color: orange }
.security-banner {
padding: 5px 0px 5px 0px;
text-align: center;
/* default */
color:white;
background-color: green;
}
.wrapper {
position: absolute;
width: 100%;
height: 100%;
top: 0;
/* height: 100%; */
/* top: 0; */
/* make height 100%, minus the height of security banner */
height: calc(~'100% - 26px');
left: 0;
overflow-y: auto;
overflow-x: hidden;
Expand Down
36 changes: 36 additions & 0 deletions client/views/app/room.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,28 @@ Template.room.helpers
noRtcLayout: ->
return (!Session.get('rtcLayoutmode') || (Session.get('rtcLayoutmode') == 0) ? true: false);

bannerData: ->
# The data context only contains the room id. one way to get the banner data is to just pass
# this id to a server-side method and let it look up the room details (such as permissions)
# and then return the banner info.
#
# HOWEVER, doing it this way does not allow the banner to be reactive in case the underlying
# room data changes (eg, if someone edits Mongo manually). This is because the template has
# no way of knowing if anything changed, so the method never gets called again. One way around
# this is to make "bannerData" itself reactive by having it depend directly on the room data.
# Then, since that data gets synchronized with the server, the template will be reprocessed
# when the data changes.
accessPermissions = ChatRoom.findOne(this._id)?.accessPermissions || []
Template.instance().updateBannerData(accessPermissions)
return Template.instance().bannerData

# For helpers "classificationId" and "securityBannerText", "this" refers to what is returned
# from "bannerData"
classificationId: ->
return this.get 'classificationId'

securityBannerText: ->
return this.get 'text'

Template.room.events

Expand Down Expand Up @@ -528,10 +550,24 @@ Template.room.events

Template.room.onCreated ->
console.log 'room.onCreated' if window.rocketDebug
self = this
# this.scrollOnBottom = true
this.showUsersOffline = new ReactiveVar false
this.atBottom = true

this.bannerData = new ReactiveDict
this.bannerData.set 'text', 'Unknown'
this.bannerData.set 'classificationId', 'U'

this.updateBannerData = (accessPermissions) ->
Meteor.call 'getSecurityBanner', accessPermissions, (error, result) ->
if error
toastr.error error.reason
else
self.bannerData.set 'text', result.text
self.bannerData.set 'classificationId', result.classificationId


Template.room.onRendered ->
console.log 'room.onRendered' if window.rocketDebug
FlexTab.check()
Expand Down
5 changes: 5 additions & 0 deletions client/views/app/room.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ <h2>
</h2>
</header>
<div class="messages-box">
{{#with bannerData}}
<div class="security-banner {{classificationId}}">
{{securityBannerText}}
</div>
{{/with}}
<div class="wrapper">
<ul>
{{#if hasMore}}
Expand Down
158 changes: 158 additions & 0 deletions server/lib/accessPermission.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
Jedis = this.Jedis || {};
// Class for managing user/resource permissions.
// Structure: Hash of arrays of AccessPermission objects (see Schemas.AccessPermission), keyed by "type", which will be
// one of the following:
// classification
// SAP
// SCI
// Release Caveat
// Example:
// {
// "classification": ["TS", "S", "C", "U"],
// "SAP: [
// { "_id" : "107", "trigraph" : "QUE", "label" : "Quesadilla", "type" : "SAP" },
// { "_id" : "108", "trigraph" : "HAB", "label" : "Habanero", "type" : "SAP" }
// ]
// }
//

// Construct an AccessPermission object from a list of access ids.
Jedis.AccessPermission = function(ids) {
if (!(this instanceof arguments.callee)) {
// We were called without `new' operator.
return new arguments.callee(arguments);
}
// Allow any of the supported input types to be passed as scalar.
if (!_.isArray(ids)) {
ids = [ids];
}
// Now we have array, but of what? String id or Schema.AccessPermission objects?
// Assumption: Whichever it is, list should be homogeneous.
if (!ids.length) {
// Empty object
return this;
}
// Ensure we have a list of objects for grouping stage.
var perms = typeof ids[0] === 'object'
? ids
: AccessPermissions.find({_id: {$in : ids}}).fetch();

// Group by types.
perms.reduce(function(o, perm) {
var type = perm.type;
o[type] = o[type] || [];
o[type].push(perm);
return o;
}, this);
};
//
Jedis.AccessPermission.prototype.resourceClassifications = function() {
var classInfo = { selected:null, higher:[], lower:[] };
var classifications = Jedis.accessManager.getClassifications();
var classificationIds = _.pluck(classifications, '_id');
// Get the classification from the access permissions
var resourceClassificationIds = _.filter(_.pluck(this.classification,'_id'), function(id) {
return _.contains(classificationIds ,id);
});
if (resourceClassificationIds.length > 1) {
console.warn('Resource permissions has more then one classifications' + resourceClassificationIds.length )
}
var resourceClassificationId = resourceClassificationIds[0];
_.each(classifications, function(element, index, list) {
var cid = element._id;
if (cid === resourceClassificationId) {
classInfo.selected = cid;
} else if ( ! classInfo.selected) {
classInfo.higher.push(cid);
} else {
classInfo.lower.push(cid);
}
});
return classInfo;
};
// Return a flat list of ids whose types are in the provided list (default all)
// Design Intent: The object instance maintains the full access permission object, but in some scenarios (e.g., calls to
// external validation service), we may need only the ids, possibly only the ids for specific types.
Jedis.AccessPermission.prototype.getPermissionIds = function(types) {
var self = this;
if (typeof types === 'string') {
types = [types];
}
// Default (no types specified) means all types defined for this instance.
types = types || _.keys(self);
return types.reduce(function(acc, type) {
return self[type] ? acc.concat(_.pluck(self[type], '_id')) : acc;
}, []);
};

// Return true iff invocant has sufficient permissions to access input resource.
// TODO - Consider pros/cons with strategy vs instance method approach. First let's implement it all within the class.
Jedis.AccessPermission.prototype.canAccessResource = function(resPerms) {
var andTypes = ['SCI', 'SAP', 'classification'],
orTypes = ['Release Caveat'];
//console.log("canAccessResource: user perms: ", this.toString());
//console.log("canAccessResource: resource perms: ", resPerms.toString());

var userIds = this.getPermissionIds(andTypes);
// Note: The following will short-circuit on failure.
var fail =
// AND logic
resPerms.getPermissionIds(andTypes).some(function(resId) {
return userIds.indexOf(resId) === -1;
});
if (!fail) {
// OR logic
var resIds = resPerms.getPermissionIds(orTypes);
userIds = this.getPermissionIds(orTypes);
if (resIds.length) {
fail = resIds.every(function(resId) {
return userIds.indexOf(resId) === -1;
});
}
}
//console.log("canAccessResource says: ", fail ? "fail" : "pass");
return !fail;
};

// Convert hash of lists keyed by type to flat list.
Jedis.AccessPermission.prototype.toArray = function() {
return _.values(this).reduce(function(acc, perms) {
return acc.concat(perms);
}, []);
};

// --- Debug/Test methods ---
// Add permission object(s) represented by input id(s).
Jedis.AccessPermission.prototype.addAccessIds = function(ids) {
ids = _.isArray(ids) ? ids : [ids];
// Lookup input ids and add corresponding objects under applicable keys (if not already there).
_.pairs(
_.groupBy(
AccessPermissions.find({_id: {$in: ids}}).fetch(),
function(perm) { return perm.type }))
// Iterate [type, perm_ary] pairs.
.forEach(function(pair) {
var type = pair[0], perms = pair[1];
this[type] = this[type] || [];
// Merge the (unique) new access objects.
this[type] = _.uniq(this[type].concat(perms),
function(perm) { return perm._id })
}, this);
};

// Remove permission object(s) represented by input id(s).
Jedis.AccessPermission.prototype.removeAccessIds = function(ids) {
ids = _.isArray(ids) ? ids : [ids];
// Remove access objects (from under their respective keys) whose id is found in input list.
// Iterate access types (object's own enumerable properties)
// Idiosyncrasy: Underscore docs say mapObject, but map is actually overloaded.
_.map(this, function(perms, type) {
this[type] = perms.filter(function(perm) { return ids.indexOf(perm._id) === -1 });
}, this);
};

Jedis.AccessPermission.prototype.toString = function() {
return JSON.stringify(this, undefined, 4);
};

// vim:ts=4:sw=4:tw=120
65 changes: 65 additions & 0 deletions server/methods/getSecurityBanner.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
Meteor.methods
getSecurityBanner: (permissionIds) ->
if not Meteor.userId()
throw new Meteor.Error('invalid-user', "[methods] getSecurityBanner -> Invalid user")

banner = {}

perms = new Jedis.AccessPermission permissionIds
.toArray();

systemCountryCode = Jedis.accessManager.getPermissions(Jedis.settings.get('public').system.countryCode)

if systemCountryCode.length is 0
console.log 'System country not found. Defaulting to USA'
systemCountryCode = _id: '300', trigraph: 'USA', label: 'United States', type: 'Release Caveat'
else
systemCountryCode = systemCountryCode[0];



# Obtain classification, add to banner. If none, default: 'UNCLASSIFIED'
classification = _.chain perms
.filter (perm) -> return perm.type is 'classification'
# there should only be a single classification label
.first()
# if no classification then default to unclassified
.value() || _id : 'U', label : 'UNCLASSIFIED'


# get all sci and sap labels, sort separately by trigraph
# join trigraphs separated by ' / ''
sciLabels = _.chain perms
.filter (perm) -> return perm.type is 'SCI'
.pluck 'trigraph'
.sort()
.value()
sapLabels = _.chain perms
.filter (perm) -> return perm.type is 'SAP'
.pluck 'trigraph'
.sort()
.value()
sciSapLabels = _.flatten [sciLabels, sapLabels]
.join ' / '


# get all rel-to countries, add to banner with ', ' separator
# if none specified (or only 'USA'), default to 'NOFORN'
reltoLabels = _.chain perms
.filter (perm) -> return perm.type is 'Release Caveat'
# exclude system country code because we prepend later as first country
.reject (perm) -> return perm._id is systemCountryCode._id
.pluck 'trigraph'
.sort()
.value()
# if still contains entries, hard-code system country code at front else 'NOFORN'
reltoLabels.splice(0, 0, ( if reltoLabels.length > 0 then 'REL TO ' + systemCountryCode.trigraph else 'NOFORN'))
reltoLabels = reltoLabels.join ', '


# stitch everything together
banner.classificationId = classification._id
banner.text = _.compact [classification.label.toUpperCase(), sciSapLabels, reltoLabels]
.join ' // '

return banner
1 change: 1 addition & 0 deletions server/publications/room.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ Meteor.publish 'room', (rid) ->
cl: 1
u: 1
usernames: 1
accessPermissions: 1