Restrict attachment so it’s only available for specific assignment group.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
3 weeks ago
Restrict attachment so it’s only available for specific assignment group.
Have you ever considered the business requirement, that attachment in a specific catalogue item should only be available for a specified assignment group? I heard about this business requirement for some time back and created a simple but scalable solution. I’m not stating this is the best solution or it’s the only solution, but nothing the less, I will share it a maybe some will get inspired, utilize it or provide recommendations to it.
The artefacts developed:
- Custom table
- ACL
- Script includes
So, what did I do:
First, I created a table with a form as showing below:
Lookup item field: Is a reference field for the dictionary entry with script include reference qualifier that looks at the target table:
// Script Include: DocumentRestrictionRuleUtils
var DocumentRestrictionRuleUtils = Class.create();
DocumentRestrictionRuleUtils.prototype = {
initialize: function() {},
getFieldQualifier: function(current) {
var tableName = current.u_target_table.toString();
if (!tableName) return 'sys_id=NULL'; // no results until table is chosen
var tables = this._getTableHierarchy(tableName);
return 'nameIN' + tables.join(',');
},
_getTableHierarchy: function(tableName) {
var tables = [tableName];
var gr = new GlideRecord('sys_db_object');
if (gr.get('name', tableName)) {
var superClass = gr.getValue('super_class');
while (superClass) {
var parentGr = new GlideRecord('sys_db_object');
if (parentGr.get(superClass)) {
tables.push(parentGr.getValue('name'));
superClass = parentGr.getValue('super_class');
} else {
break;
}
}
}
return tables;
},
type: 'DocumentRestrictionRuleUtils'
};
Match catalogue item: Is a reference field looking directly for the catalogue item in scope
Lookup group field: is looking for which field is storing the group that should be allowed to see the attachment.
Target table: is the target table of the catalogue item
Restricted variable: If only 1 attachment and not all is restricted, then the variable attachment can be selected
Match groups: is the group that should be allowed to see the single / all attachments
Restriction type: all / single based on if it should be all attachment or single attachments
Secondly, I created a deny-unless ACL looking at the sys_attachment table calling the script include:
answer = new DocumentAccessManager().canReadAttachment(current.sys_id);
The script include is written as below:
var DocumentAccessManager = Class.create();
DocumentAccessManager.prototype = {
REQUESTER_FIELD_CANDIDATES: ['requested_for', 'opened_by', 'caller_id', 'u_end_user'],
initialize: function() {},
canReadAttachment: function(attachmentSysId) {
var attGr = new GlideRecord('sys_attachment');
if (!attGr.get(attachmentSysId))
return false; // no attachments
}
var tableName = attGr.getValue('table_name');
var tableSysId = attGr.getValue('table_sys_id');
// Handle ServiceNow's internal ZZ_YY collision-renaming
// (occurs when a table name collides with a renamed/legacy table)
if (tableName.indexOf('ZZ_YY') === 0) {
tableName = tableName.substring(5); // 'ZZ_YY' is 5 characters
}
var recGr = new GlideRecord(tableName);
if (!recGr.get(tableSysId)) {
return true; // all read
}
var rule = this._findMatchingRule(tableName, recGr);
if (!rule) {
return true; // no matching rule -> not restricted -> allow
}
if (rule.type == 'single') {
if (!this._isRestrictedAttachment(tableSysId, attachmentSysId, rule.variableSysId)) {
return true; // not the flagged attachment -> allow
}
}
// type == 'all' -> every attachment on this record is in scope, no further check
var userId = gs.getUserID();
if (this._isRequester(recGr, userId)) {
return true;
}
if (rule.group && this._isMemberOfGroup(userId, rule.group)) {
return true;
}
return false; // explicit deny
},
/**
* Finds a matching restriction rule for this table/record.
* - u_lookup_item_field: metadata -> which field on the record holds the catalog item
* - u_match_catalog_item: criteria -> the catalog item value this rule applies to
* (if blank, the rule applies to ALL records on this table)
* - u_lookup_group_field: metadata -> which field on the record holds the assignment group
* (read dynamically, so access follows live reassignment)
* - u_restriction_type: 'all' (every attachment restricted) or 'single' (only the
* flagged variable's attachment restricted)
*/
_findMatchingRule: function(tableName, recGr) {
var result = null;
var gr = new GlideRecord('u_pii_affected');
gr.addQuery('u_target_table', tableName);
gr.addQuery('u_active', true);
gr.query();
while (gr.next()) {
// Rule matching: based on catalog item field comparison
var ruleItemValue = gr.getValue('u_match_catalog_item');
if (ruleItemValue) {
var itemFieldName = gr.u_lookup_item_field.element ?
gr.u_lookup_item_field.element.toString() : '';
if (!itemFieldName || !recGr.isValidField(itemFieldName)) continue;
var recordItemValue = recGr.getValue(itemFieldName);
if (ruleItemValue != recordItemValue) continue; // catalog item doesn't match
}
// If no catalog item specified on the rule, it matches ALL records on this table
// The access group is defined on the restriction rule itself. This is the
// group a user must belong to in order to see the restricted attachment.
var configuredGroupValue = gr.getValue('u_match_groups');
if (!configuredGroupValue) {
var groupFieldName = gr.u_lookup_group_field.element ?
gr.u_lookup_group_field.element.toString() : '';
if (groupFieldName && recGr.isValidField(groupFieldName)) {
configuredGroupValue = recGr.getValue(groupFieldName);
}
}
var variableSysId = gr.getValue('u_restricted_variable');
var ruleType = (gr.getValue('u_restriction_type') || '').toLowerCase();
// "all" is the most restrictive -> return immediately
if (ruleType == 'all') {
return {
type: 'all',
variableSysId: '',
group: configuredGroupValue
};
}
// Keep the first "single" rule as a fallback in case a later
// row in this loop turns out to be type "all"
if (!result) {
result = {
type: ruleType,
variableSysId: variableSysId,
group: configuredGroupValue
};
}
}
return result;
},
_isRequester: function(recGr, userId) {
for (var i = 0; i < this.REQUESTER_FIELD_CANDIDATES.length; i++) {
var field = this.REQUESTER_FIELD_CANDIDATES[i];
if (recGr.isValidField(field) && recGr.getValue(field) == userId) {
return true;
}
}
return false; // no candidate field matched -> not the requester
},
_isRestrictedAttachment: function(recordSysId, attachmentSysId, variableSysId) {
if (!variableSysId) return false;
var gr = new GlideRecord('sc_item_option');
gr.addQuery('item_option_new', variableSysId);
gr.addQuery('value', attachmentSysId);
gr.query();
while (gr.next()) {
var moGr = new GlideRecord('sc_item_option_mtom');
moGr.addQuery('sc_item_option', gr.getUniqueValue());
moGr.addQuery('request_item', recordSysId);
moGr.query();
if (moGr.next()) return true;
}
return false;
},
_isMemberOfGroup: function(userId, groupSysId) {
var gr = new GlideRecord('sys_user_grmember');
gr.addQuery('user', userId);
gr.addQuery('group', groupSysId);
gr.query();
return gr.next();
},
type: 'DocumentAccessManager'
};
Solution outcome:
So as an admin i would be able to see below for record RITM0010017:
But as a user that is not part of the database team, the user would see no attachments:
Hope that somebody have stood in the same and can find the article relevant.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
3 weeks ago
Hi @AndersBGS ,
Can't we just use an ACL advanced condition and a Script Include on the sys_attachment table?
We can get the table sys_id, Glide RITM to get the catalog item, get logged in user grou, and if it doesn't match, don't allow read permission.
Am i missing something here 😁
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
3 weeks ago
Hi @Hemanth M
Thank you for comment. You’re absolutely right - you could do that. The reason for my solution was to make it scalable. Single / all attachment, multiple tables, multiple catalog item/record producers and at the same time only run the server side logic if rule exist to minimize query on the database.
as stated in my article, I will not state that this is the only and best solution… but purely inspiration to how it could be done.