Advanced Permissions
You can create custom JavaScript expressions to apply permission rules and attribute filters. This is a powerful feature that lets you apply multiple permissions/filters to multiple users/groups at once.
You can construct an expression that accesses details about the data, the user, and/or the request, and then applies permissions accordingly. For example, you might want to configure an expression that applies permissions according to:
- The user making the request.
- The roles the user belongs to.
- The current request URL.
- The layer being accessed.
You will need a basic understanding of JavaScript.
An invalid expression results in the incoming request being denied.
Errors encountered when evaluating an expression are included in the Access Control logs that are saved on the machine where Access Control is installed. See Logging.
Order of Evaluation and Inheritance
While advanced permissions are powerful and offer many advantages, it is important to note how they differ from regular permissions.
-
Regular permissions:
-
Are evaluated first.
-
Are evaluated from the service level down.
-
Are subject to inheritance rules apply (see Inheritance).
-
-
Advanced permissions:
-
Are evaluated after all regular permissions have been evaluated.
-
Are evaluated in the opposite direction from regular permissions—that is, starting at the most granular and progressing up to the service level.
-
Are not subject to inheritance rules.
-
Because advanced permissions are evaluated in the opposite direction from regular permissions, any advanced permission configured at a higher level overrides more granular permissions. For example, an advanced permission configured at the service level will always override any advanced permission configured at the layer, table, and/or task level.
JavaScript Support
The Access Control Expression Editor supports all JavaScript keywords and functions. You will also find some items that are particularly useful or specific to Access Control.
For example:
- allowLayer, allowField, allowTask, allowLayerEdit
- denyLayer, denyField, denyTask
- user, isUserInRole
- layer
- request
The editor provides code completion suggestions as you type, but you can press Ctrl + Space to display code completion suggestions at any time.
Configure an Expression
-
In the VertiGIS Studio Access Control Designer: select the service, layer, or task on which you want to configure an advanced expression.
For details, refer to the appropriate section:
-
Add Service Permissions, steps 1 - 2
-
Add Layer Permissions, step 1
-
-
Click Advanced.
The Advanced Permissions panel for the selected element displays.
-
In the Advanced Permissions panel, click Add.
-
Select a user or group:
In the Select a User or Group dialog box, in the drop-down list on the right, choose whether to search by Group or by User.
Start typing the name of the group/user into the text box, and then select from the drop-down list that displays.
-
In the Advanced Permissions table, click Edit Expression.
The Expression Editor opens.
-
Type your expression into the editor. As you type, the editor provides JavaScript code completion suggestions.
Press Ctrl + Space to display code completion suggestions at any time.
-
Click OK.
-
Click Save.
Examples
The following sections show how advanced permissions can be used in some common use cases.
These examples are intended to illustrate the APIs and patterns of usage in advanced permissions. They do not represent complete solutions with proper handling of all errors and boundary conditions.
Grant access to specific layers based on user role
Use case: You have multiple layers that you want to allow multiple groups to access.
Using regular permissions, you would need to add one permission to each layer for each group you want to allow.
Using advanced permissions, you can achieve the same result by configuring a single permission at the service level.
Example expression:
if (layer.name === "Cities" || layer.name === "Countries") {
if (isUserInRole("Role 1", "Role 2")) {
allowLayer();
}
}
Notice that we can achieve the same result with a single permission and a small amount of code. Now consider scaling the problem up to 50 layers and 10 groups. The advanced permissions approach is much simpler and easier to maintain.
Deny access to an internal field
Use case: Your service has many layers, and many of them contain an internal field that you do not want your users to access.
You could use regular permissions to achieve this, but you would need to configure a deny permission on every layer containing the field.
Using advanced permissions, you only need to configure one permission at the service level.
Because it is applied at the service level, the following expression evaluates for every layer. If the field does not exist on some of the layers, it is simply ignored.
Example expression:
denyField("INTERNAL_ID");
This is a simple and efficient way to apply a single permission to multiple layers, using a small amount of code.
Deny access to a field based on URL of the request
Use case: You want to prevent users from accessing a particular field if the incoming request is based on a query.
You cannot use regular permissions to achieve this. However, you can configure one advanced permission that checks the URL path of the request. If the path ends with '/query', access to the field is denied.
Example expression:
if (request.path.endsWith("/query")) {
denyField("Field1");
}
You can use this approach to deny access to multiple fields, or to an entire layer, table, or task.
Apply attribute filter based on a user lookup
Use case: You want to restrict which features of a layer a user can access based on a "ZONE" attribute of the layer using a lookup of usernames to zones defined in code.
Example expression:
const lookup = {
bsmith: "Zone 1",
jdoe: "Zone 2",
user1: "Zone 1",
user2: "Zone 3",
};
const zone = lookup[user.name];
if (zone) {
setAttributeFilter(`ZONE = '${zone}'`);
} else {
setAttributeFilter("1 = 0");
}
You can use setAttributeFilter("1 = 0")
to deny access to all features.
Apply attribute filter based on groups
Use case: You want to restrict which features of a layer a user can access based on a "PROJECT" attribute of the layer and the user's membership in one or more groups.
Example expression:
if (isUserInRole("Admin Role")) {
// All projects
return;
}
const projects = [];
if (isUserInRole("Project Role 1")) {
projects.push(1);
}
if (isUserInRole("Project Role 2")) {
projects.push(2);
}
if (projects.length > 0) {
// Specific projects
setAttributeFilter(`PROJECT IN (${projects.join(", ")})`);
} else {
// No projects
setAttributeFilter("1 = 0");
}
Apply attribute filter based on a user lookup that uses a configuration file
Use case: You want to restrict which features of a layer a user can access based on a "ZONE" attribute of the layer using a lookup of usernames to zones defined in a JSON configuration file located at C:\ProgramData\Geocortex\Access Control\config\access-control\custom\folder\file1.json
.
Example expression:
const lookup = getConfig("folder1/file1.json");
const zone = lookup[user.name];
if (zone) {
setAttributeFilter(`ZONE = '${zone}'`);
} else {
setAttributeFilter("1 = 0");
}
The getConfig
function provides a way to centralize configuration that you want to share between multiple advanced permissions.
Apply attribute filter based on a user lookup that uses a HTTP request
Use case: You want to restrict which features of a layer a user can access based on a "ZONE" attribute of the layer using a lookup of usernames to zones provided by an external service over HTTP.
Example expression:
run(async () => {
const response = await fetch(`https://server/api/?user=${encodeURIComponent(user.name)}`);
const json = await response.json();
const zone = json.zone;
setAttributeFilter(`ZONE = '${zone}'`);
});
The run(async () => {...
pattern is required when you need to perform asynchronous operations like fetch
that use the await
keyword.
Apply attribute filter based on a user lookup that uses a HTTP request with caching
Use case: A cache optimized version of the previous example that caches the result of the HTTP lookup for 30 seconds.
Example expression:
run(async () => {
const cacheKey = `zone-cache-${user.name}`;
let zone = getCachedData(cacheKey);
if (!zone) {
const response = await fetch(`https://server/api/?user=${encodeURIComponent(user.name)}`);
const json = await response.json();
zone = json.zone;
setCachedData(cacheKey, zone, 30000);
}
setAttributeFilter(`ZONE = '${zone}'`);
});
Apply attribute filter based on a user lookup that uses a HTTP request to an ArcGIS Server service
Use case: You want to restrict which features of a layer a user can access based on a "ZONE" attribute of the layer using a lookup of usernames to zones provided by an ArcGIS Server service.
Example expression:
run(async () => {
const where = encodeURIComponent(`USER = '${user.name}'`);
const response = await fetch(`https://server/arcgis/rest/services/MyService/FeatureServer/0/query?f=json&where=${where}&outFields=ZONE`);
const json = await response.json();
const zone = json.features[0].attributes.ZONE;
setAttributeFilter(`ZONE = '${zone}'`);
});
Apply geometry filter based on a user lookup that uses a HTTP request to an ArcGIS Server service
Use case: You want to restrict which features of a layer a user can access based on a geometry using a lookup of usernames to geometries provided by an ArcGIS Server service.
Example expression:
run(async () => {
const where = encodeURIComponent(`USER = '${user.name}'`);
const response = await fetch(`https://server/arcgis/rest/services/MyService/FeatureServer/0/query?f=json&where=${where}&returnGeometry=true`);
const json = await response.json();
const geometry = json.features[0].geometry;
geometry.spatialReference = json.spatialReference;
setGeometryFilter(geometry);
});
The geometry used in a geometry filter must always specify a spatial reference.
The geometry provided by an ArcGIS Server query operation does not include a spatial reference.
The geometry.spatialReference = json.spatialReference
assignment copies the spatial reference from the root of the response onto the geometry.
Apply attribute filter to multiple layers at once
Use case: You want to apply a single attribute filter to multiple layers that have common fields. For example, your service contains multiple layers with a "ZONE" field and you only want the user to see features in zone "2". You could apply a single attribute filter to the All Layers and Tables of the service.
Example expression:
const fieldName = "ZONE";
if (layer && layer.fields && layer.fields.some(x => x.name === fieldName)) {
setAttributeFilter(`${fieldName} = 2`);
}
The layer && layer.fields
null checks are required because the layer
object will not always be defined and have fields when advanced permissions are evaluated at the All Layers and Tables level.
The layer.fields.some(x => x.name === fieldName)
test ensures that the attribute filter is only applied to layers that contain the field in question.
The x.name === fieldName
test is case sensitive. You could use x.name.toUpperCase() === fieldName
to do a case-insensitive match.
Apply geometry filter to multiple layers at once
Use case: You want to restrict which features (across multiple layers) that a user can access based on a geometry filter.
Example expression:
if (layer) {
setGeometryFilter("name-of-your-geofence-geometry");
}
The layer
null check is required because the layer
object will not always be defined when advanced permissions are evaluated at the All Layers and Tables level and there is no need to assign a geometry filter when a layer is not present.
This example applies the geometry filter to all layers in the service. Use caution with this approach because geometry filters can impact performance. You should only apply geometry filters to the specific set of layers that need them.
Error handling
Use case: You want to ensure that if there is an error during the execution of your advanced permission JavaScript that you can catch the error and apply appropriate fallback permissions.
Example expression:
try {
// Do something that could throw an error
} catch (e) {
// Log the failure
console.log("An error occurred", e);
// Apply fallback permissions
setAttributeFilter("1 = 0");
}
Example asynchronous expression:
run(async () => {
try {
const response = await fetch(`https://server/api/?user=${encodeURIComponent(user.name)}`);
const json = await response.json();
const zone = json.zone;
setAttributeFilter(`ZONE = '${zone}'`);
} catch (e) {
// Log the failure
console.log("An error occurred", e);
// Apply fallback permissions
setAttributeFilter("1 = 0");
}
});
When an advanced permission throws an error, that does not get caught by your JavaScript code, the request will be denied.