diff --git a/src/js/collections/maps/MapAssets.js b/src/js/collections/maps/MapAssets.js index 58dfe8a7f..13fa76ecd 100644 --- a/src/js/collections/maps/MapAssets.js +++ b/src/js/collections/maps/MapAssets.js @@ -60,7 +60,7 @@ define( model: CesiumVectorData }, { - types: ['BingMapsImageryProvider', 'IonImageryProvider'], + types: ['BingMapsImageryProvider', 'IonImageryProvider', 'WebMapTileServiceImageryProvider'], model: CesiumImagery }, { diff --git a/src/js/models/AppModel.js b/src/js/models/AppModel.js index 3d244770c..cc2e8bdb2 100644 --- a/src/js/models/AppModel.js +++ b/src/js/models/AppModel.js @@ -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, + /** * 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) @@ -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 */ @@ -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/'; diff --git a/src/js/models/NodeModel.js b/src/js/models/NodeModel.js index 2eeaf082f..e7f025c19 100644 --- a/src/js/models/NodeModel.js +++ b/src/js/models/NodeModel.js @@ -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(){ @@ -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){ @@ -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(); + } thisModel.set("checked", true); }, @@ -96,14 +107,48 @@ 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(){ + 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, @@ -111,11 +156,17 @@ define(['jquery', 'underscore', 'backbone'], 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 entry as the root elemeent. If it was obtained + // from the CN 'listNodes' then the root element will be + 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){ @@ -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 @@ -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 diff --git a/src/js/models/UserModel.js b/src/js/models/UserModel.js index 3acc36250..6202730a6 100644 --- a/src/js/models/UserModel.js +++ b/src/js/models/UserModel.js @@ -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(){ @@ -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; diff --git a/src/js/models/maps/assets/CesiumImagery.js b/src/js/models/maps/assets/CesiumImagery.js index d4c092f0d..288e74c6a 100644 --- a/src/js/models/maps/assets/CesiumImagery.js +++ b/src/js/models/maps/assets/CesiumImagery.js @@ -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 diff --git a/src/js/models/maps/assets/MapAsset.js b/src/js/models/maps/assets/MapAsset.js index b0fc8a4ab..cfa904f8c 100644 --- a/src/js/models/maps/assets/MapAsset.js +++ b/src/js/models/maps/assets/MapAsset.js @@ -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. @@ -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] - diff --git a/src/js/templates/notAllowedSubmitter.html b/src/js/templates/notAllowedSubmitter.html new file mode 100644 index 000000000..70d18ab4d --- /dev/null +++ b/src/js/templates/notAllowedSubmitter.html @@ -0,0 +1,4 @@ +
+

<%=messageText%>

+ Editing is not allowed.> +
diff --git a/src/js/views/EditorView.js b/src/js/views/EditorView.js index 1a107210a..3380f7c75 100644 --- a/src/js/views/EditorView.js +++ b/src/js/views/EditorView.js @@ -3,6 +3,7 @@ define(['underscore', 'backbone', "views/SignInView", "text!templates/editorSubmitMessage.html"], + function(_, $, Backbone, SignInView, EditorSubmitMessageTemplate){ /** @@ -700,8 +701,8 @@ function(_, $, Backbone, SignInView, EditorSubmitMessageTemplate){ this.stopListening(); this.undelegateEvents(); - } - + }, + }); return EditorView; diff --git a/src/js/views/MetadataView.js b/src/js/views/MetadataView.js index 3b1930667..bc680ea92 100644 --- a/src/js/views/MetadataView.js +++ b/src/js/views/MetadataView.js @@ -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, @@ -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(); } diff --git a/src/js/views/maps/CesiumWidgetView.js b/src/js/views/maps/CesiumWidgetView.js index a7cf93c46..3e53bd237 100644 --- a/src/js/views/maps/CesiumWidgetView.js +++ b/src/js/views/maps/CesiumWidgetView.js @@ -84,7 +84,7 @@ define( renderFunction: 'addVectorData' }, { - types: ['BingMapsImageryProvider', 'IonImageryProvider'], + types: ['BingMapsImageryProvider', 'IonImageryProvider', 'WebMapTileServiceImageryProvider'], renderFunction: 'addImagery' }, { diff --git a/src/js/views/metadata/EML211EditorView.js b/src/js/views/metadata/EML211EditorView.js index f9b969be7..1671ef0ba 100644 --- a/src/js/views/metadata/EML211EditorView.js +++ b/src/js/views/metadata/EML211EditorView.js @@ -243,6 +243,25 @@ define(['underscore', if (!MetacatUI.rootDataPackage.packageModel) { return } + + /** + * Adds messaging to this view to tell an unauthorized user that they cannot create or update datasets. + * of this object(s). + */ + if (typeof MetacatUI.appUserModel.get("isAllowedSubmitter") === 'undefined') { + this.listenToOnce(MetacatUI.appUserModel, "change:isAllowedSubmitter", function () { + this.renderEditorComponents(); + }); + return; + } else { + // If the user is not an authorized submitter (see metacat parmeter auth.submitters), then + // stop rendering of the editor. + if(!MetacatUI.appUserModel.get("isAllowedSubmitter") == true) { + this.displayNotAllowedSubmitter(); + return; + } + } + var resMapPermission = MetacatUI.rootDataPackage.packageModel.get("isAuthorized_write"), metadataPermission = this.model.get("isAuthorized_write"); @@ -1392,6 +1411,31 @@ define(['underscore', return requiredFields; + }, + + /** + * Adds messaging to this view to tell an unauthorized user that they cannot create or update datasets. + * of this object(s). + */ + displayNotAllowedSubmitter: function(){ + //var message = this.notAllowedSubmitterTemplate({ + // messageText: "Your DataONE user is not included in the list of users that are allowed to submit datasets to this repository." + // }); + + var msg; + if (typeof MetacatUI.appModel.get("notAllowedSubmitterMessage") === 'undefined') { + msg = "You are not authorized to submit or edit datasets"; + } else { + msg = MetacatUI.appModel.get("notAllowedSubmitterMessage"); + } + + this.$("#editor-body").empty(); + //Show the not found message + MetacatUI.appView.showAlert(msg, "alert-error", this.$("#editor-body"), null, {remove: true}); + this.$("#editor-view-not-found-pid").text(this.pid); + //Stop listening to any further events + this.stopListening(); + this.model.off(); } }); return EML211EditorView; diff --git a/src/js/views/portals/PortalListView.js b/src/js/views/portals/PortalListView.js index 5cd7b5768..55b1cbdc3 100644 --- a/src/js/views/portals/PortalListView.js +++ b/src/js/views/portals/PortalListView.js @@ -227,7 +227,17 @@ define(["jquery", renderList: function(){ try{ - + /** + * Adds messaging to this view to tell an unauthorized user that they cannot create or update datasets. + * of this object(s). + */ + if (typeof MetacatUI.appUserModel.get("isAllowedSubmitter") === 'undefined') { + this.listenToOnce(MetacatUI.appUserModel, "change:isAllowedSubmitter", function () { + this.renderList(); + }); + return; + } + //Get the list container element var listContainer = this.$(this.listContainer); @@ -389,7 +399,8 @@ define(["jquery", }); //Render an Edit button - if ( MetacatUI.appUserModel.hasIdentityOverlap(owners) ){ + + if ( MetacatUI.appUserModel.hasIdentityOverlap(owners) && MetacatUI.appUserModel.get("isAllowedSubmitter") == true){ //Create an Edit buttton var editButton = $(document.createElement("a")).attr("href", MetacatUI.root + "/edit/"+ MetacatUI.appModel.get("portalTermPlural") +"/" + encodeURIComponent((searchResult.get("label") || searchResult.get("seriesId") || searchResult.get("id"))) ) diff --git a/src/js/views/portals/PortalView.js b/src/js/views/portals/PortalView.js index b355eafe4..0520f8061 100644 --- a/src/js/views/portals/PortalView.js +++ b/src/js/views/portals/PortalView.js @@ -568,6 +568,15 @@ define(["jquery", // Insert the button into the navbar var container = $(this.editButtonContainer); + + if (typeof MetacatUI.appUserModel.get("isAllowedSubmitter") === 'undefined') { + this.listenToOnce(MetacatUI.appUserModel, "change:isAllowedSubmitter", function () { + this.insertOwnerControls(); + }); + return; + } else if (!MetacatUI.appUserModel.get("isAllowedSubmitter") == true) { + return; + } var model = this.model;