Page Restrictions — Confluence Cloud MacroNew
This Confluence user macro uses the Confluence Cloud REST API to fetch and display page restrictions for edit (update) and view (read) operations.
The macro retrieves direct and inherited restrictions from the current page and its ancestors, merges users and groups, removes duplicate view permissions when edit access exists, and renders the results as interactive AUI cards with avatar groups, expandable group members, and live search filtering.
Template
<style>
.wrapper {
scrollbar-width: none;
-ms-overflow-style: none;
}
.wrapper::-webkit-scrollbar {
display: none;
}
.permissions-container {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.permission-card {
flex: 1;
min-width: 260px;
border: 1px solid #dfe1e5;
border-radius: 6px;
padding: 14px;
background: #fff;
}
.permission-card h2 {
margin: 0 0 10px 0;
font-size: 16px;
color: #172b4d;
}
.permission-card h2 .aui-icon {
margin-left: 6px;
vertical-align: middle;
}
.permission-card strong {
display: block;
margin: 12px 0 8px 0;
font-size: 13px;
color: #5e6c84;
}
.group-list {
list-style: none;
margin: 0;
padding: 0;
}
.group-list li {
margin: 6px 0;
}
.group-name-toggle {
cursor: pointer;
color: #0052cc;
font-weight: 500;
}
.group-members {
display: none;
margin-top: 8px;
padding: 10px;
background: #f4f5f7;
border-radius: 4px;
}
.group-members.open {
display: block;
}
.empty-members {
color: #6b778c;
font-style: italic;
font-size: 13px;
}
.search-toolbar {
display: none;
gap: 8px;
align-items: center;
margin-bottom: 16px;
}
.search-toolbar.visible {
display: flex;
}
.search-toolbar input[type="search"] {
flex: 1;
min-width: 200px;
padding: 8px 10px;
border: 1px solid #dfe1e5;
border-radius: 4px;
}
.no-results {
color: #5e6c84;
font-style: italic;
display: none;
}
.hidden {
display: none;
}
aui-avatar[data-href] {
cursor: pointer;
}
.avatar-search-results {
display: none;
flex-wrap: wrap;
gap: 4px;
align-items: center;
}
.avatar-search-results.visible {
display: flex;
}
</style>
#set ($baseUrl = $StringUtils.replace($baseUrl,"/wiki",""))
#set ($contentId = $page.id)
#set ($showEditOnly = $parameters["showEditOnly"] == "true")
## Fetch all restrictions
#set ($allRestrictions = $ConfluenceManager.get("/wiki/rest/api/content/${contentId}/restriction"))
#set ($updateRestriction = "")
#set ($readRestriction = "")
#if ($allRestrictions && $allRestrictions.results)
#foreach ($r in $allRestrictions.results)
#if ($r.operation == "update")
#set ($updateRestriction = $r)
#end
#if (!$showEditOnly && $r.operation == "read")
#set ($readRestriction = $r)
#end
#end
#end
## Fetch ancestors
#set ($ancestorsResp = $ConfluenceManager.get("/wiki/api/v2/pages/${contentId}/ancestors"))
#set ($inheritedUpdateUserMap = {})
#set ($inheritedUpdateGroupMap = {})
#set ($inheritedReadUserMap = {})
#set ($inheritedReadGroupMap = {})
#if ($ancestorsResp && $ancestorsResp.results)
#foreach ($ancestor in $ancestorsResp.results)
#set ($ancestorRestrictions = $ConfluenceManager.get("/wiki/rest/api/content/${ancestor.id}/restriction"))
#foreach ($ar in $ancestorRestrictions.results)
#if ($ar.operation == "update")
#foreach ($u in $ar.restrictions.user.results)
#set ($void = $inheritedUpdateUserMap.put($u.accountId, $u))
#end
#foreach ($g in $ar.restrictions.group.results)
#set ($void = $inheritedUpdateGroupMap.put($g.id, $g))
#end
#end
#if (!$showEditOnly && $ar.operation == "read")
#foreach ($u in $ar.restrictions.user.results)
#set ($void = $inheritedReadUserMap.put($u.accountId, $u))
#end
#foreach ($g in $ar.restrictions.group.results)
#set ($void = $inheritedReadGroupMap.put($g.id, $g))
#end
#end
#end
#end
#end
## Merge update users
#set ($updateUsers = {})
#if ($updateRestriction && $updateRestriction.restrictions.user.results)
#foreach ($u in $updateRestriction.restrictions.user.results)
#set ($void = $updateUsers.put($u.accountId, {
"user": $u,
"direct": true,
"inherited": false
}))
#end
#end
#foreach ($accountId in $inheritedUpdateUserMap.keySet())
#if ($updateUsers.containsKey($accountId))
#set ($entry = $updateUsers.get($accountId))
#set ($void = $entry.put("inherited", true))
#else
#set ($u = $inheritedUpdateUserMap.get($accountId))
#set ($void = $updateUsers.put($accountId, {
"user": $u,
"direct": false,
"inherited": true
}))
#end
#end
## Merge update groups
#set ($updateGroups = {})
#if ($updateRestriction && $updateRestriction.restrictions.group.results)
#foreach ($g in $updateRestriction.restrictions.group.results)
#set ($void = $updateGroups.put($g.id, {
"group": $g,
"direct": true,
"inherited": false
}))
#end
#end
#foreach ($groupId in $inheritedUpdateGroupMap.keySet())
#if ($updateGroups.containsKey($groupId))
#set ($entry = $updateGroups.get($groupId))
#set ($void = $entry.put("inherited", true))
#else
#set ($g = $inheritedUpdateGroupMap.get($groupId))
#set ($void = $updateGroups.put($groupId, {
"group": $g,
"direct": false,
"inherited": true
}))
#end
#end
## Merge read users
#set ($readUsers = {})
#set ($readGroups = {})
#if (!$showEditOnly)
#if ($readRestriction && $readRestriction.restrictions.user.results)
#foreach ($u in $readRestriction.restrictions.user.results)
#set ($void = $readUsers.put($u.accountId, {
"user": $u,
"direct": true,
"inherited": false
}))
#end
#end
#foreach ($accountId in $inheritedReadUserMap.keySet())
#if ($readUsers.containsKey($accountId))
#set ($entry = $readUsers.get($accountId))
#set ($void = $entry.put("inherited", true))
#else
#set ($u = $inheritedReadUserMap.get($accountId))
#set ($void = $readUsers.put($accountId, {
"user": $u,
"direct": false,
"inherited": true
}))
#end
#end
## Filter out View users who already have Edit permission
#foreach ($accountId in $updateUsers.keySet())
#if ($readUsers.containsKey($accountId))
#set ($void = $readUsers.remove($accountId))
#end
#end
## Merge read groups
#if ($readRestriction && $readRestriction.restrictions.group.results)
#foreach ($g in $readRestriction.restrictions.group.results)
#set ($void = $readGroups.put($g.id, {
"group": $g,
"direct": true,
"inherited": false
}))
#end
#end
#foreach ($groupId in $inheritedReadGroupMap.keySet())
#if ($readGroups.containsKey($groupId))
#set ($entry = $readGroups.get($groupId))
#set ($void = $entry.put("inherited", true))
#else
#set ($g = $inheritedReadGroupMap.get($groupId))
#set ($void = $readGroups.put($groupId, {
"group": $g,
"direct": false,
"inherited": true
}))
#end
#end
#end
## Resulting UI
<div class="wrapper">
<div class="search-toolbar#if($parameters["showPanel"]) visible#end">
<input type="search" id="permissions-search" placeholder="🔍 Search..." />
</div>
<div class="permissions-container">
## Edit card
<div class="permission-card">
<h2><span class="aui-icon aui-icon-small aui-iconfont-new-edit" role="img" aria-label="Edit restrictions" style="vertical-align: baseline; margin-right: 0.150rem;"></span> Edit Restrictions</h2>
<strong>Users</strong>
#if ($updateUsers.size() > 0)
<aui-avatar-group size="medium" id="update-user-avatar-group">
#foreach ($entry in $updateUsers.values())
#set ($u = $entry.user)
#set ($tooltip = "$u.displayName / Direct")
#if ($entry.direct && $entry.inherited)
#set ($tooltip = "$u.displayName / Direct & Inherited")
#elseif ($entry.inherited)
#set ($tooltip = "$u.displayName / Inherited")
#end
<aui-avatar
src="${baseUrl}/$u.profilePicture.path"
alt="$u.displayName"
data-tooltip="$tooltip"
data-name="$u.displayName.toLowerCase()"
data-href="${baseUrl}/wiki/people/$u.accountId"
></aui-avatar>
#end
</aui-avatar-group>
<div id="update-user-search-results" class="avatar-search-results"></div>
<p class="no-results" id="update-no-results">No matching users found.</p>
#else
<p>No user restrictions.</p>
#end
<strong>Groups</strong>
#if ($updateGroups.size() > 0)
<ul class="group-list" id="update-main-group-list">
#set ($idx = 0)
#foreach ($gEntry in $updateGroups.values())
#set ($g = $gEntry.group)
#set ($idx = $idx + 1)
#set ($groupSource = "Direct")
#if ($gEntry.direct && $gEntry.inherited)
#set ($groupSource = "Direct & Inherited")
#elseif ($gEntry.inherited)
#set ($groupSource = "Inherited")
#end
#if ($idx == 4)
</ul>
#set ($extraUpdateGroupsCount = $updateGroups.size() - 3)
<button class="aui-button aui-button-link"
id="update-toggleGroups"
aria-pressed="false"
data-extra="$extraUpdateGroupsCount">
Show more <aui-badge>$extraUpdateGroupsCount</aui-badge>
</button>
<ul class="group-list hidden" id="update-extraGroupsList">
#end
<li>
<span class="group-name-toggle">$g.name</span>
#set ($members = $ConfluenceManager.get("/wiki/rest/api/group/${g.id}/membersByGroupId"))
<div class="group-members">
#if ($members && $members.results && $members.results.size() > 0)
<aui-avatar-group size="medium">
#foreach ($u in $members.results)
#set ($memberTooltip = "$u.displayName / $groupSource")
<aui-avatar
src="${baseUrl}/$u.profilePicture.path"
alt="$u.displayName"
data-tooltip="$memberTooltip"
data-name="$u.displayName.toLowerCase()"
data-href="${baseUrl}/wiki/people/$u.accountId"
></aui-avatar>
#end
</aui-avatar-group>
#else
<p class="empty-members">No members in this group.</p>
#end
</div>
</li>
#end
</ul>
<ul class="group-list" id="update-group-search-results" style="display:none"></ul>
<p class="no-results" id="update-no-group-results">No matching group members found.</p>
#else
<p>No group restrictions.</p>
#end
</div>
## View card
#if (!$showEditOnly)
<div class="permission-card">
<h2><span class="aui-icon aui-icon-small aui-iconfont-new-watch" role="img" aria-label="View restrictions" style="vertical-align: text-top; margin-top: 0.1rem;"></span> View Restrictions</h2>
<strong>Users</strong>
#if ($readUsers.size() > 0)
<aui-avatar-group size="medium" id="read-user-avatar-group">
#foreach ($entry in $readUsers.values())
#set ($u = $entry.user)
#set ($tooltip = "$u.displayName / Direct")
#if ($entry.direct && $entry.inherited)
#set ($tooltip = "$u.displayName / Direct & Inherited")
#elseif ($entry.inherited)
#set ($tooltip = "$u.displayName / Inherited")
#end
<aui-avatar
src="${baseUrl}/$u.profilePicture.path"
alt="$u.displayName"
data-tooltip="$tooltip"
data-name="$u.displayName.toLowerCase()"
data-href="${baseUrl}/wiki/people/$u.accountId"
></aui-avatar>
#end
</aui-avatar-group>
<div id="read-user-search-results" class="avatar-search-results"></div>
<p class="no-results" id="read-no-results">No matching users found.</p>
#else
<p>No user restrictions.</p>
#end
<strong>Groups</strong>
#if ($readGroups.size() > 0)
<ul class="group-list" id="read-main-group-list">
#set ($idx = 0)
#foreach ($gEntry in $readGroups.values())
#set ($g = $gEntry.group)
#set ($idx = $idx + 1)
#set ($groupSource = "Direct")
#if ($gEntry.direct && $gEntry.inherited)
#set ($groupSource = "Direct & Inherited")
#elseif ($gEntry.inherited)
#set ($groupSource = "Inherited")
#end
#if ($idx == 4)
</ul>
#set ($extraReadGroupsCount = $readGroups.size() - 3)
<button class="aui-button aui-button-link"
id="read-toggleGroups"
aria-pressed="false"
data-extra="$extraReadGroupsCount">
Show more <aui-badge>$extraReadGroupsCount</aui-badge>
</button>
<ul class="group-list hidden" id="read-extraGroupsList">
#end
<li>
<span class="group-name-toggle">$g.name</span>
#set ($members = $ConfluenceManager.get("/wiki/rest/api/group/${g.id}/membersByGroupId"))
<div class="group-members">
#if ($members && $members.results && $members.results.size() > 0)
<aui-avatar-group size="medium">
#foreach ($u in $members.results)
#set ($memberTooltip = "$u.displayName / $groupSource")
<aui-avatar
src="${baseUrl}/$u.profilePicture.path"
alt="$u.displayName"
data-tooltip="$memberTooltip"
data-name="$u.displayName.toLowerCase()"
data-href="${baseUrl}/wiki/people/$u.accountId"
></aui-avatar>
#end
</aui-avatar-group>
#else
<p class="empty-members">No members in this group.</p>
#end
</div>
</li>
#end
</ul>
<ul class="group-list" id="read-group-search-results" style="display:none"></ul>
<p class="no-results" id="read-no-group-results">No matching group members found.</p>
#else
<p>No group restrictions.</p>
#end
</div>
#end
</div>
</div>
<script>
AJS.toInit(() => {
const qs = s => document.querySelector(s);
const qsa = s => [...document.querySelectorAll(s)];
/* Resize iframe after any content change */
function resizeIframe() {
setTimeout(function () {
AP.resize('100%', (document.body.scrollHeight + 20) + 'px');
}, 50);
}
function bindAvatars(root) {
root.querySelectorAll('aui-avatar[data-href]').forEach(av =>
av.addEventListener('click', () => window.open(av.dataset.href, '_blank'))
);
AJS.$(root).find('aui-avatar[data-tooltip]').tooltip({
gravity: 's',
title: function () { return AJS.$(this).attr('data-tooltip'); }
});
}
bindAvatars(document);
function setupCard(prefix) {
const userGroup = qs('#' + prefix + '-user-avatar-group');
const userResults = qs('#' + prefix + '-user-search-results');
const noResults = qs('#' + prefix + '-no-results');
const noGroupResults = qs('#' + prefix + '-no-group-results');
const mainGL = qs('#' + prefix + '-main-group-list');
const toggleBtn = qs('#' + prefix + '-toggleGroups');
const extraGL = qs('#' + prefix + '-extraGroupsList');
const groupResults = qs('#' + prefix + '-group-search-results');
if (!userGroup && !mainGL) return null;
const allUserData = userGroup
? [...userGroup.querySelectorAll('aui-avatar')].map(av => ({
name: av.dataset.name || '',
tooltip: av.dataset.tooltip || '',
href: av.dataset.href || '',
src: av.getAttribute('src') || ''
}))
: [];
let groupLiSelector = [];
if (mainGL) groupLiSelector.push('#' + prefix + '-main-group-list li');
if (extraGL) groupLiSelector.push('#' + prefix + '-extraGroupsList li');
const allGroupData = groupLiSelector.length > 0
? qsa(groupLiSelector.join(', ')).map(li => ({
groupName: li.querySelector('.group-name-toggle')?.textContent?.trim() || '',
members: [...li.querySelectorAll('.group-members aui-avatar')].map(av => ({
name: av.dataset.name || '',
tooltip: av.dataset.tooltip || '',
href: av.dataset.href || '',
src: av.getAttribute('src') || ''
}))
}))
: [];
if (toggleBtn && extraGL) {
toggleBtn.addEventListener('click', function () {
const pressed = this.getAttribute('aria-pressed') === 'true';
if (pressed) {
extraGL.classList.add('hidden');
this.setAttribute('aria-pressed', 'false');
this.innerHTML = 'Show more <aui-badge>' + this.dataset.extra + '</aui-badge>';
} else {
extraGL.classList.remove('hidden');
this.setAttribute('aria-pressed', 'true');
this.innerHTML = 'Show less';
}
resizeIframe();
});
}
if (groupLiSelector.length > 0) {
const toggleSel = groupLiSelector.map(s => s.replace(' li', ' .group-name-toggle'));
qsa(toggleSel.join(', ')).forEach(t =>
t.addEventListener('click', () => {
t.nextElementSibling.classList.toggle('open');
resizeIframe();
})
);
}
return function (q) {
if (!q) {
if (userGroup) userGroup.style.display = '';
if (userResults) { userResults.classList.remove('visible'); userResults.innerHTML = ''; }
if (noResults) noResults.style.display = 'none';
if (noGroupResults) noGroupResults.style.display = 'none';
if (mainGL) mainGL.style.display = '';
if (toggleBtn) toggleBtn.style.display = '';
if (extraGL) {
extraGL.style.display = '';
if (toggleBtn && toggleBtn.getAttribute('aria-pressed') === 'true') {
extraGL.classList.remove('hidden');
} else {
extraGL.classList.add('hidden');
}
}
if (groupResults) { groupResults.style.display = 'none'; groupResults.innerHTML = ''; }
resizeIframe();
return;
}
/* Users */
if (userGroup) userGroup.style.display = 'none';
let uMatches = allUserData.filter(d => d.name.includes(q));
if (userResults) {
if (uMatches.length > 0) {
userResults.innerHTML = uMatches.map(d =>
'<aui-avatar src="' + d.src + '" alt="' + d.name
+ '" data-tooltip="' + d.tooltip
+ '" data-href="' + d.href + '"></aui-avatar>'
).join('');
userResults.classList.add('visible');
bindAvatars(userResults);
} else {
userResults.classList.remove('visible');
userResults.innerHTML = '';
}
}
if (noResults) noResults.style.display = uMatches.length === 0 ? 'block' : 'none';
/* Groups */
if (mainGL) mainGL.style.display = 'none';
if (toggleBtn) toggleBtn.style.display = 'none';
if (extraGL) extraGL.style.display = 'none';
if (groupResults) {
let html = '';
allGroupData.forEach(g => {
let mMatches = g.members.filter(m => m.name.includes(q));
if (mMatches.length > 0) {
html += '<li><span class="group-name-toggle">' + g.groupName + '</span>';
html += '<div class="group-members open"><div class="avatar-search-results visible">';
mMatches.forEach(m => {
html += '<aui-avatar src="' + m.src + '" alt="' + m.name
+ '" data-tooltip="' + m.tooltip
+ '" data-href="' + m.href + '"></aui-avatar>';
});
html += '</div></div></li>';
}
});
if (html) {
groupResults.innerHTML = html;
groupResults.style.display = '';
bindAvatars(groupResults);
groupResults.querySelectorAll('.group-name-toggle').forEach(t =>
t.addEventListener('click', () => {
t.nextElementSibling.classList.toggle('open');
resizeIframe();
})
);
if (noGroupResults) noGroupResults.style.display = 'none';
} else {
groupResults.style.display = 'none';
groupResults.innerHTML = '';
if (noGroupResults) noGroupResults.style.display = 'block';
}
}
resizeIframe();
};
}
const searchUpdate = setupCard('update');
const searchRead = setupCard('read');
qs('#permissions-search')?.addEventListener('input', e => {
const q = e.target.value.toLowerCase().trim();
if (searchUpdate) searchUpdate(q);
if (searchRead) searchRead(q);
});
resizeIframe();
});
</script>User Parameters
Show Panel
Check the box to show a search panel
Show Edit Only
Check the box to display 'edit' restrictions only
You May Also Like
Space Information macro by space Id
Shows page creation date
This macro allows user to retrieve all pages from current space despite limitation of 250 pages per request
Macro for generating ID in base32 format
Find image within page attachments. Handy for reuse files and update them all at once
Show an expandable page tree for a selected parent page (defaults to the current page)
Read and display the fixed version of Confluence pages by checking page or ancestor labels
Display a custom list of recently updated content
An overview of all pages within one space which contains the title, the version, and the last updated date
Perform customized searches based on labels, content types, and other parameters.