Stand with Ukraine 🇺🇦

Page Restrictions — Confluence Cloud MacroNew

confluence-content

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.

Try for free

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