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
2 hours 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.
If my answer has helped with your question, please mark my answer as the accepted solution and give a thumbs up.
Best regards
Anders
Rising star 2024
MVP 2025
linkedIn: https://www.linkedin.com/in/andersskovbjerg/
