Forums

Articles
Create
cancel
Showing results for 
Search instead for 
Did you mean: 

Inherit Edit Restrictions from Parent with Automation

[Edited with optimized variable creation for {{viewers}} and {{editors}}]

giphy

So when people start messing with Restrictions in Confluence, they often ask why Edit Restrictions are not inherited by child pages.

And as I was explaining this limitation today to one of my users, I thought, ok in some cases, it might be helpful if say, for a certain space, or certain pages, all child pages would inherit Edit restrictions via an Automation rule copying them from the parent.

So, I spent a little time and figured out how to do it. It's entirely possible I've missed something, so I welcome your feedback!

Prerequisites

Rule

image.png

Details

Trigger

In testing, my rule is triggered when a page is moved. As tested it runs on every page in a single space. More on this at the end under "Building on this"

Remove all Restrictions

We're going to clear all restrictions since we will be copying everything from the parent. So I'm setting Restrictions to "Anyone in the space can view and edit"

Get Parent Restrictions

We using the Get restrictions API call to get all of the Restrictions of the Parent Page. That call is:

https://boomaster.atlassian.net/wiki/rest/api/content/{{page.parent.id}}/restriction

Create Viewers

From the response to the API call, we're extracting all of the Users and Groups with read permission, and formatting them into a JSON excerpt that we are storing in a variable named {{viewers}}.

This "code" is a bit complex and was what took the longest time:

{{#webResponse.body.results}}{{#if(equals(operation, "read"))}}"user": {{restrictions.user.results.asJsonObjectArray("accountId")}}

,"group": {{restrictions.group.results.asJsonObjectArray("id")}}

{{/}}{{/}}

Create Editors

From the response to the API call, we're extracting all of the Users and Groups with update permission, and formatting them into a JSON excerpt that we are storing in a variable named {{editors}}. It's the same as above, but is looking for the "update" operation instead of "read":

{{#webResponse.body.results}}{{#if(equals(operation, "update"))}}"user": {{restrictions.user.results.asJsonObjectArray("accountId")}}

,"group": {{restrictions.group.results.asJsonObjectArray("id")}}

{{/}}{{/}}

Add Restrictions

We use the Add restrictions API call to apply the parent permissions that we've extracted above.

image.png

Custom data:

{ "results": [
  {
    "operation": "read",
    "restrictions": {
{{viewers}}
}
},
{
"operation": "update",
"restrictions": {
{{editors}}
}
}
]
}

And welp, that's basically it.

Building on this

If you don't want this behavior on every page in your space, you could add conditions to restrict it to specific Parent pages by doing a smart values condition to see if {{page.parent.id}} matches regular expression "^(1234|456|789)$".

Also, this rule only triggers on page moves. You could also implement it when a page is published, adding a condition to check if there is a parent page.

References

As I said, people have been asking about this forever.

From 2020's Is there a way to inherit editing permissions? I learned about Edit Permission Inheritance which is a paid app that adds a feature to toggle Inheritance of Edit Restrictions on specific pages. All descendants of those pages will have the same Edit Restrictions.

But then in Nov 2022 @Alexandra Astor said that:

We tried Edit Permission Inheritance by Purde Software, however, once the edit permissions are inherited it is not possible to turn them off.

And @Nic Brough -Adaptavist- responded:

Inheriting edit restrictions does not make a lot of sense, it's incredibly limiting.

However, setting them down through a tree can be useful.  Not inheriting, the way view restrictions do it automatically, but a one-off "parse the tree, set edit restriction on all pages automatically".  This would leave people with the flexibility to change the restrictions later.

Welp, here you go, Nic. :-} That's kind of what my Automation does, when you Move a page under page with restrictions.

You could take the core logic of the rule and create a manually triggered Rule that would copy the current page's restrictions to all descendants. (I believe this CQL would work: ancestor = {{page.id}})

4 comments

Bill Sheboy
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
May 24, 2025

Hi @Darryl Lee 

Once again: well done!  And, I wonder...

For the example rule you show, it initially sets restrictions to "all can view and edit".  Given the potential for update latency in Confluence-related stuff, I wonder if the rule could instead:

  • remove that reset to "all"
  • replace the Add Restrictions endpoint with the Update Restrictions one as that does a replace of the current restrictions with the new ones sent

Please let me know if I am misunderstanding that first rule action.  Thanks!

 

Kind regards,
Bill

Darryl Lee
Community Champion
May 24, 2025

Thanks, and nice catch @Bill Sheboy !

Yes, I did think about the small possibility that the page may be momentarily visible/editable to people who should not have access due to the latency.

I was lazily clearing all Restrictions because I erroneously thought that the alternative was having to make two additional calls: 1) to get the current restrictions and 2) to remove them.

But now that I've actually read the docs for the Update restrictions API endpoint (imagine that) it looks like implementing what you suggest would be as easy as changing my POST to a PUT!

Just tested it and yup, it works!

So here's the updated rule:

image.png

I love it - even shorter!

BTW, I included the warning which is only there because Atlassian's JSON validator doesn't recognize it's own Smart Values. :-}

Sidenote

Bill, have you or anyone else ever had success with asJsonObjectArray? I swear I've tried using it before with Web Responses and EVERY TIME I'm frustrated that I cannot just do something like this:

{{restrictions.user.results.accountId.asJsonObjectArray("accountId")}}

Instead of this mess:

[{{#restrictions.user.results.accountId}}{"accountId" : "{{.}}"}{{^last}},{{/}}{{/}}]

Or maybe the problem is that I'm using it within the context of another list iteration, or inside of an if, but I swear, if I just output {{restrictions.user.results.accountId}} I get what looks like an array, so I just don't get it.

Maybe I'll start breaking it down into parts and see if I can't get it to work.

Like Bill Sheboy likes this
Bill Sheboy
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
May 24, 2025

Regarding asJsonObjectArray() I have encountered two things...

First (as often is the case), latency / order of parsing challenges. In my experience, many of the actions which can take a complicated smart value expression, particularly Send Web Request, may not fully parse things in the order we expect before usage. 

The workaround I use is add a Create Variable action with the entire JSON expression.  That will fully evaluate it before needed and helps with debugging as it can more easily be logged.  This technique is even required for some cases with the just-in-time lookup of some smart values.

 

Next, that function takes a keyName as a parameter...which may be different for the source and target JSON.  My workaround for that is adding a replace() after the function:

{{myListFieldWithAttributes.asJsonObjectArray("accountId").replace("accountId", "id")}}

 

One more thing...If there are nested lists, you may want to add flatten after the accountId in your expression.

Darryl Lee
Community Champion
May 24, 2025

Ugh, yes, I think once again I've forgotten that asJsonObjectArray only works if it's ALREADY a list of pairs. You reminded me that keyName isn't a label for the OUTPUT, but a parameter for the INPUT.

(For some reason I always think this function lets me give it a simple array of values and it'll convert that to a JsonObjectArray with the keys I want.)

So ooof, what I really want for {{viewers}} is:

{{#webResponse.body.results}}{{#if(equals(operation, "read"))}}"user": {{restrictions.user.results.asJsonObjectArray("accountId")}}

,"group": {{restrictions.group.results.asJsonObjectArray("id")}}

{{/}}{{/}}

And for {{editors}}:

{{#webResponse.body.results}}{{#if(equals(operation, "update"))}}"user": {{restrictions.user.results.asJsonObjectArray("accountId")}}

,"group": {{restrictions.group.results.asJsonObjectArray("id")}}

{{/}}{{/}}

Woot! That works!

Like Bill Sheboy likes this

Comment

Log in or Sign up to comment
TAGS
AUG Leaders

Atlassian Community Events