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 restrict submissions #1817 #1914

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
2 changes: 1 addition & 1 deletion src/js/collections/maps/MapAssets.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ define(
model: CesiumVectorData
},
{
types: ['BingMapsImageryProvider', 'IonImageryProvider'],
types: ['BingMapsImageryProvider', 'IonImageryProvider', 'WebMapTileServiceImageryProvider'],
model: CesiumImagery
},
{
Expand Down
24 changes: 24 additions & 0 deletions src/js/models/AppModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,13 @@ define(['jquery', 'underscore', 'backbone'],
*/
nodeServiceUrl: null,
/**
* The URL for the DataONE listNodes() API. This URL is contructed dynamically when the
* AppModel is initialized. Only override this if you are an advanced user and have a reason to!
* (see https://releases.dataone.org/online/api-documentation-v2.0/apis/CN_APIs.html#CNCore.listNodes)
* @type {string}
*/
getCapabilitiesServiceUrl: null,
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this the same as the AppModel.nodeServiceUrl?

Copy link
Member

Choose a reason for hiding this comment

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

Ah, I see that the nodeServiceUrl is for the CN and getCapabilitiesServiceUrl is for the MN.

/**
* The URL for the DataONE View API. This URL is contructed dynamically when the
* AppModel is initialized. Only override this if you are an advanced user and have a reason to!
* (see https://releases.dataone.org/online/api-documentation-v2.0/apis/MN_APIs.html#module-MNView)
Expand Down Expand Up @@ -1841,6 +1848,22 @@ define(['jquery', 'underscore', 'backbone'],
*/
enableMeasurementTypeView: false,

/**
* This message will display when a user tries to submit a dataset and they are not in the allowed submitters list.
* The allowed submitters list is set in the Metacat configuration for a Member Node and inspected by Metacatui.
* @see https://knb.ecoinformatics.org/knb/docs/metacat-properties.html#authorization-and-authentication-properties
* @type {string}
*/
notAllowedSubmitterMessage: "You are not authorized to submit or edit datasets.",

/**
* Enable checking of the allowed submitters list is set in the Metacat configuration for a Member Node and inspected by Metacatui.
* @see https://knb.ecoinformatics.org/knb/docs/metacat-properties.html#authorization-and-authentication-properties.
* @type {boolean}
* @default false
*/
checkAllowedSubmitters: false,

/**
* The following configuration options are deprecated or experimental and should only be changed by advanced users
*/
Expand Down Expand Up @@ -2037,6 +2060,7 @@ define(['jquery', 'underscore', 'backbone'],
urls.queryServiceUrl = baseUrl + '/query/solr/?';
urls.metaServiceUrl = baseUrl + '/meta/';
urls.packageServiceUrl = baseUrl + '/packages/application%2Fbagit-097/';
urls.getCapabilitiesServiceUrl = baseUrl + '/';

if( d1Service.indexOf("mn") > 0 ){
urls.objectServiceUrl = baseUrl + '/object/';
Expand Down
92 changes: 81 additions & 11 deletions src/js/models/NodeModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ define(['jquery', 'underscore', 'backbone'],
hiddenMembers: [],
currentMemberNode: MetacatUI.appModel.get("nodeId") || null,
checked: false,
error: false
error: false,
allowedSubmitters: [],
nodeInfoFound: false /* the node might access the CN, but not be part of the network, so check if MN capabilites are included in the CN report */
},

initialize: function(){
Expand All @@ -23,7 +25,11 @@ define(['jquery', 'underscore', 'backbone'],
if(MetacatUI.appModel.get('nodeServiceUrl')){
//Get the node information from the CN
this.getNodeInfo();
}
} else if(MetacatUI.appModeel.get('getCapabilitiesServiceUrl')) {
// If the CN node service URL is not defined, see if we can get getCapabilities
// information from the node directly
this.getCapabilities();
}
},

getMember: function(memberInfo){
Expand Down Expand Up @@ -75,11 +81,16 @@ define(['jquery', 'underscore', 'backbone'],
url: MetacatUI.appModel.get('nodeServiceUrl'),
dataType: "text",
success: function(data, textStatus, xhr) {

var xmlResponse = $.parseXML(data) || null;
if(!xmlResponse) return;

thisModel.saveNodeInfo(xmlResponse);
// It is possible for MN to retrieve the CN node capabilties report, but not be registered with DataONE.
// If the CN node report was successfully retrieved, but the calling MN was not in the report, then we
// need to get the MN capabilities directly from the MN.
if(!thisModel.get("nodeInfoFound")) {
thisModel.getCapabilities();
}
Comment on lines +91 to +93
Copy link
Member

Choose a reason for hiding this comment

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

I notice that the app never sets this attribute (nodeInfoFound) so the member node doc will always be retrieved. When the CN node info doc is parsed, let's check if the current member node was found in the list and if not, set nodeInfoFound to false.

Even better, we could just use NodeModel.getMember() to see if the current member node is already set on the model. If getMember() doesn't return our node, then we know it wasn't found in the CN node list.

This does mean that the member node info doc needs to be synced with the CN for the changes in this PR to work. Right now I'm testing with dev.nceas and it hasn't synced with the CN, so everything has been working in my tests so far because the member node info doc is grabbed directly from dev.nceas.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea on using NodeModel.getMember(), I'll implement the use of that.

The MN config change (updating allowed.submitters is only changed when the metcat admin config for DataONE is updated (i.e. https://dev.nceas.ucsb.edu/knb/admin?configureType=dataone), even though the allow.submitters property is changed in the global properties admin menu, (https://dev.nceas.ucsb.edu/knb/admin?configureType=properties). Maybe this needs a revisit on design?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, denied.submitters should be implemented, but could that happen in a later release (ESS-DIVE hasn't requested this)?


thisModel.set("checked", true);
},
Expand All @@ -96,26 +107,66 @@ define(['jquery', 'underscore', 'backbone'],
});
}

//Trigger an error on this model
thisModel.set("error", true);
thisModel.trigger("error");

thisModel.set("checked", true);
// If the node capabilities service isn't available from the CN, try getting it from the current MN.
// This is also intended to be used by standalone repositories, i.e. those that are not part of the DataONE network.
thisModel.getCapabilities();
}
});
},

getCapabilities: function(){
Copy link
Member

Choose a reason for hiding this comment

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

I think this method name is misleading because more than the node capabilities are retrieved and set on the model. If we are only setting the node capabilities, then let's keep this name. But if we are setting all of the node properties (which it appears we are based on like 127 (thisModel.saveNodeInfo(xmlResponse);)), then let's name this method something more appropriate like getMemberNodeInfo()

Copy link
Member

Choose a reason for hiding this comment

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

Can you also please add a method description using JSDoc?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, I'll update the method call name and create the JSDoc entry.

var thisModel = this;

$.ajax({
url: MetacatUI.appModel.get('getCapabilitiesServiceUrl'),
dataType: "text",
success: function(data, textStatus, xhr) {

var xmlResponse = $.parseXML(data) || null;
if(!xmlResponse) return;
thisModel.saveNodeInfo(xmlResponse);
thisModel.set("checked", true);
},
error: function(xhr, textStatus, errorThrown){

//Log the error to the console
console.error("Couldn't get the DataONE node capabilities document: ", textStatus, errorThrown);

//Send this exception to Google Analytics
if(MetacatUI.appModel.get("googleAnalyticsKey") && (typeof ga !== "undefined")){
ga("send", "exception", {
"exDescription": "Couldn't get the DataONE node capabilties document: " + textStatus + ", " + errorThrown + " | v. " + MetacatUI.metacatUIVersion,
"exFatal": false
});
}

//Trigger an error on this model
thisModel.set("error", true);
thisModel.trigger("error");
thisModel.set("checked", true);
}
});
},

// end new

saveNodeInfo: function(xml){
var thisModel = this,
memberList = this.get('members'),
coordList = this.get('coordinators'),
children = xml.children || xml.childNodes;


//Traverse the XML response to get the MN info
_.each(children, function(d1NodeList){

var d1NodeListChildren = d1NodeList.children || d1NodeList.childNodes;
// If the capabbilities reeport was obtained directly from the MN
// then there will be a single <node> entry as the root elemeent. If it was obtained
// from the CN 'listNodes' then the root element will be <nodeList>
if (d1NodeList.localName == "node") {
var d1NodeListChildren = $(d1NodeList).toArray();
} else {
var d1NodeListChildren = d1NodeList.children || d1NodeList.childNodes;
}

//The first (and only) child should be the d1NodeList
_.each(d1NodeListChildren, function(thisNode){
Expand All @@ -124,7 +175,7 @@ define(['jquery', 'underscore', 'backbone'],
if(!thisNode.attributes) return;

//'node' will be a single node
var node = {},
var node = {} ,
nodeProperties = thisNode.children || thisNode.childNodes;

//Grab information about this node from XML nodes
Expand All @@ -135,11 +186,30 @@ define(['jquery', 'underscore', 'backbone'],
else
node[nodeProperty.nodeName] = nodeProperty.textContent;

var submitters = [];
//Check if this member node has v2 read capabilities - important for the Package service
if((nodeProperty.nodeName == "services") && nodeProperty.childNodes.length){
var v2 = $(nodeProperty).find("service[name='MNRead'][version='v2'][available='true']").length;
node["readv2"] = v2;
// Get the service restrictions for the current member node
if( MetacatUI.nodeModel.get("currentMemberNode") == $(thisNode).find("identifier").text()) {
var storageProperty = $(nodeProperty).find("service[name='MNStorage'][version='v2'][available='true']");
if (storageProperty.children().length) {
var restrictionProperty = $(storageProperty).find("restriction[methodName='create']");
if (restrictionProperty.length && restrictionProperty.children().length) {
_.each(restrictionProperty.children(), function(subject) {
if(subject.nodeName == "subject") {
submitters.push(subject.textContent);
}
});
}
}
}
}
if(submitters.length) {
var currentSubmitters = thisModel.get("allowedSubmitters");
thisModel.set("allowedSubmitters", submitters.concat(currentSubmitters));
}
});

//Grab information about this node from XLM attributes
Expand Down
59 changes: 58 additions & 1 deletion src/js/models/UserModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ define(['jquery', 'underscore', 'backbone', 'jws', 'models/Search', "collections
//When the user is logged in, see if they have a DataONE subscription
this.on("change:loggedIn", this.fetchSubscription);
}
this.checkAllowedSubmitters();
},

createSearchModel: function(){
Expand Down Expand Up @@ -1137,7 +1138,63 @@ define(['jquery', 'underscore', 'backbone', 'jws', 'models/Search', "collections
reset: function(){
var defaults = _.omit(this.defaults(), ["searchModel", "searchResults"]);
this.set(defaults);
}
},

/**
* Check if the currently logged in user is in the list of users that are allowed to
* create DataONE objects. Note that this list is set by the metacat configuration
* parameter 'auth.allowSubmitters'.
*/
checkAllowedSubmitters: function() {
var thisModel = this;

var checkAllowedSubmitters = MetacatUI.appModel.get("checkAllowedSubmitters");
// If 'checkAllowedSubmitters' is set to false, then set the current user
// to allowed, to allow access without checking the metacat config 'allow.submitters' list.
if (!checkAllowedSubmitters) {
this.set("isAllowedSubmitter", true);
return;
}

if( !this.get("loggedIn") ){
this.set("isAllowedSubmitter", false);
this.listenToOnce(this, "change:loggedIn", function(){
thisModel.checkAllowedSubmitters();
});
return;
}

// This is needed for `hasIdentityOverlap()``
if( !this.get("allIdentitiesAndGroups").length ){
this.listenToOnce(this, "change:allIdentitiesAndGroups", function(){
thisModel.checkAllowedSubmitters();
});
return;
}

// This is needed for 'allowedSubmitters'
if(!MetacatUI.nodeModel.get("checked")){
this.listenToOnce(MetacatUI.nodeModel, "change:checked", function(){
thisModel.checkAllowedSubmitters();
});
return;
}
// The allowed submitters list is obtained from the MN capabilities report
// provided by the CN 'listNodes' service
var allowedSubmitters = MetacatUI.nodeModel.get("allowedSubmitters");
// If allowedSubmitters is not set, then all users are allowed to submit
if(!allowedSubmitters.length) {
this.set("isAllowedSubmitter", true);
} else {
// This user (or equivalent identity) is in the allowed submitters list.
if(this.hasIdentityOverlap(allowedSubmitters)) {
this.set("isAllowedSubmitter", true);
} else {
this.set("isAllowedSubmitter", false);
}
}
},

});

return UserModel;
Expand Down
2 changes: 1 addition & 1 deletion src/js/models/maps/assets/CesiumImagery.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ define(
* @name CesiumImagery#defaults
* @extends MapAsset#defaults
* @type {Object}
* @property {'BingMapsImageryProvider'|'IonImageryProvider'} type A string
* @property {'BingMapsImageryProvider'|'IonImageryProvider'|'WebMapTileServiceImageryProvider'} type A string
* indicating a Cesium Imagery Provider type. See
* {@link https://cesium.com/learn/cesiumjs-learn/cesiumjs-imagery/#more-imagery-providers}
* @property {Cesium.ImageryLayer} cesiumModel A model created and used by Cesium
Expand Down
4 changes: 2 additions & 2 deletions src/js/models/maps/assets/MapAsset.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ define(
* Default attributes for MapAsset models
* @name MapAsset#defaults
* @type {Object}
* @property {('Cesium3DTileset'|'BingMapsImageryProvider'|'IonImageryProvider'|'CesiumTerrainProvider')} type
* @property {('Cesium3DTileset'|'BingMapsImageryProvider'|'IonImageryProvider'|'WebMapTileServiceImageryProvider'|'CesiumTerrainProvider')} type
* The format of the data. Must be one of the supported types.
* @property {string} label A user friendly name for this asset, to be displayed
* in a map.
Expand Down Expand Up @@ -118,7 +118,7 @@ define(
* description.
* @typedef {Object} MapAssetConfig
* @name MapConfig#MapAssetConfig
* @property {('Cesium3DTileset'|'BingMapsImageryProvider'|'IonImageryProvider'|'CesiumTerrainProvider'|'GeoJsonDataSource')} type -
* @property {('Cesium3DTileset'|'BingMapsImageryProvider'|'IonImageryProvider'|'WebMapTileServiceImageryProvider'|'CesiumTerrainProvider'|'GeoJsonDataSource')} type -
* A string indicating the format of the data. Some of these types correspond
* directly to Cesium classes.
* @property {(Cesium3DTileset#cesiumOptions|CesiumImagery#cesiumOptions|CesiumTerrain#cesiumOptions|CesiumVectorData#cesiumOptions)} [cesiumOptions] -
Expand Down
4 changes: 4 additions & 0 deletions src/js/templates/notAllowedSubmitter.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div class="alert.plain">
<p><%=messageText%></p>
<i class="icon-warning-sign">Editing is not allowed.></i>
</div>
5 changes: 3 additions & 2 deletions src/js/views/EditorView.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ define(['underscore',
'backbone',
"views/SignInView",
"text!templates/editorSubmitMessage.html"],

function(_, $, Backbone, SignInView, EditorSubmitMessageTemplate){

/**
Expand Down Expand Up @@ -700,8 +701,8 @@ function(_, $, Backbone, SignInView, EditorSubmitMessageTemplate){
this.stopListening();
this.undelegateEvents();

}

},
});

return EditorView;
Expand Down
13 changes: 11 additions & 2 deletions src/js/views/MetadataView.js
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,14 @@ define(['jquery',
authorization = [],
resourceMap = this.dataPackage ? this.dataPackage.packageModel : null,
modelsToCheck = [this.model, resourceMap];

// Check if a list of allowed submitters has been specified for this MN.
if (typeof MetacatUI.appUserModel.get("isAllowedSubmitter") === 'undefined') {
view.listenToOnce(MetacatUI.appUserModel, "change:isAllowedSubmitter", function () {
view.checkWritePermissions();
});
return;
}

modelsToCheck.forEach(function (model, index) {
// If there is no resource map or no EML,
Expand Down Expand Up @@ -1172,8 +1180,9 @@ define(['jquery',
} else {
return
}
// Only render the editor controls if we have completed the checks AND the user has full editor permissions
if (allTrue) {
// Only render the editor controls if user is in the 'allowed submitters' list (if it exists) and
// if we have completed the checks AND the user has full editor permissions
if (MetacatUI.appUserModel.get("isAllowedSubmitter") == true && allTrue) {
this.insertEditorControls();
}

Expand Down
2 changes: 1 addition & 1 deletion src/js/views/maps/CesiumWidgetView.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ define(
renderFunction: 'addVectorData'
},
{
types: ['BingMapsImageryProvider', 'IonImageryProvider'],
types: ['BingMapsImageryProvider', 'IonImageryProvider', 'WebMapTileServiceImageryProvider'],
renderFunction: 'addImagery'
},
{
Expand Down