Availability | Confluence 6.10 or later |
Read-Only Mode for Data Center customers helps admins perform routine maintenance, recover from unexpected problems, or prepare to migrate content to a new site. When a site is placed in read-only mode, users will be able to view pages and their history, but not create, edit, comment, copy or move content. Administration actions such as changing site configuration are not restricted.
To make your app (also known as an add-on or plugin) compatible with read-only mode, there are two main things you will need to determine:
Finally, you should also mark your app as compatible, so admins know it is safe to leave your app enabled while read-only mode is on.
This feature is only available for Confluence Data Center. This means your test site will require a Data Center license. See Starting a Confluence cluster on a single machine to find out how to set up a Data Center instance for testing. This page also has a 72 hour Data Center license that you can use.
Once you've got your Data Center test instance set up, turn on read-only mode:
As UI elements can be rendered in various ways, there are several ways to do this.
This is the recommended approach.
If your web-item is configured with the appropriate condition, it will be hidden automatically when read-only mode is enabled. For example:
1 2<web-item key="edit-page" name="Edit Tab" section="system.content.button" weight="10"> ... <condition class="com.atlassian.confluence.plugin.descriptor.web.conditions.PagePermissionCondition"> <param name="permission">edit</param> </condition> ... </web-item> <web-item key="remove-page" name="Remove Link" section="system.content.action/modify" weight="30"> ... <condition class="com.atlassian.confluence.plugin.descriptor.web.conditions.PagePermissionCondition"> <param name="permission">remove</param> </condition> ... </web-item>
If you've used a custom Condition class for the web-item, you'll need to update that Condition class to check for read-only mode appropriately or add the new ReadWriteAccessModeCondition
implemented as below:
1 2public class ReadWriteAccessModeCondition extends BaseConfluenceCondition { private AccessModeService accessModeService; @Override protected boolean shouldDisplay(WebInterfaceContext context) { return AccessMode.READ_WRITE.equals(accessModeService.getAccessMode()); } public void setAccessModeService(AccessModeService accessModeService) { this.accessModeService = accessModeService; } }
1 2<web-item key="remove-page" name="Remove Link" section="system.content.action/modify" weight="30"> ... <condition class="com.atlassian.confluence.plugin.descriptor.web.conditions.ReadWriteAccessModeCondition"/> ... </web-item>
If a web-item can't be used for any reason, upon loading the page, you can check one of the two following AJS meta tags in your Javascript to determine the read-only state:
"READ_ONLY"
or "READ_WRITE"
): this meta tag reflects the rendering state of the page at the time it is loaded. Its value will not be changed over time unless the user refreshes the page."READ_ONLY"
or "READ_WRITE"
): this meta tag reflects the state of the page at the time the JS code is run. Its value will change if the admin changes the access mode (within a 30-second window of the change being made).The JS code will need to check if read-only mode is enabled by checking the access-mode
AJS meta tag as follows:
1 2function isReadOnlyModeEnabled() { return Meta.get('access-mode') === 'READ_ONLY'; }
If you need to insert or remove a static element from the DOM at some point after the page is loaded, you'll need to call the new /accessmode
REST end-point to check whether the read-only mode is (still) enabled. This is because the read-only state may change while the user is viewing the page.
Confluence will throw a ReadOnlyException
if accessModeService.isReadOnlyAccessModeEnabled()
is true in the back end, as in the removeLike
method of the DefaultLikeManager
.
1 2public void removeLike(final ContentEntityObject contentEntity, final User user) { if (accessModeService.isReadOnlyAccessModeEnabled()) { throw new ReadOnlyException(i18NBeanFactory.getI18NBean().getText("read.only.mode.default.banner.message")); } }
First, make sure there is a RestExceptionMapper
class in the same package of the REST resource classes as follows:
1 2@Provider public class RestExceptionMapper extends ServiceExceptionMapper { @Override protected void _annotateThisClassWithProvider() { //NO-OP just gets the annotated class on the bundles class path https://jira.atlassian.com/browse/CRA-733 } }
Note: For RESTV2 plugins please use com.atlassian.confluence.rest.v2.api.exception.ServiceExceptionMapper
Then you can either throw a ReadOnlyException
in the REST method or the Service/Manager method that it calls (as in 2.1).
1 2public void removeLike(final ContentEntityObject contentEntity, final User user) { if (accessModeService.isReadOnlyAccessModeEnabled()) { throw new ReadOnlyException(i18NBeanFactory.getI18NBean().getText("read.only.mode.default.banner.message")); } }
And then just call the Service/Manager method in the REST method as usual.
1 2@DELETE @Produces({APPLICATION_JSON}) @Path("/{id}/likes") @Consumes(APPLICATION_JSON) public Response removeLike(@PathParam("id") final Long contentId) { // all other lines are removed for simplicity likeManager.removeLike(contentEntity, AuthenticatedUserThreadLocal.get()); }
Update your front-end Ajax request counterpart to handle the 405 status code and check if the reason equals to "READ_ONLY"
or use our new MessageController
module as follows:
1 2define('confluence-like/like', [ // ... 'confluence/message-controller' ], function( // ... MessageController ) { // ... $.ajax({ type: type === LIKE ? "POST" : "DELETE", url: getRestUrl(contentId), contentType: 'application/json', data: { "atlassian-token": Meta.get("atlassian-token") }, dataType: "json", timeout: 5000 }).fail(function (jqXhr) { MessageController.showError(MessageController.parseError(jqXhr), MessageController.Location.FLAG); }); }
or:
1 2$.ajax({ type: "DELETE", url: getRestUrl(contentId), contentType: 'application/json', data: { "atlassian-token": Meta.get("atlassian-token") }, dataType: "json", timeout: 5000 }).fail(function (xhr) { var data = $.parseJSON(xhr.responseText) || xhr.responseText; if (xhr.status === 405 && data.reason === "READ_ONLY") { new Flag($.extend({}, { body: data.message }, flagDefaults)); } else { // handle the other errors here } }
Here's the format of the response:
1 2{ "statusCode":405, "data":{ }, "message":"", "reason":"READ_ONLY" }
MessageController usage:
1 2var errorMessage = MessageController.parseError(jqXhr);
1 2MessageController.showError(MessageController.parseError(jqXhr), MessageController.Location.FLAG);
1 2var defaultErrorMessage = "Some default error message"; MessageController.showError(MessageController.parseError(jqXhr, defaultErrorMessage), MessageController.Location.FLAG)
If it's not appropriate to throw a ReadOnlyException
from the current REST API, the front-end code will need to check for read-only mode at runtime by sending an Ajax request to the end-point /rest/api/accessmode
before deciding to execute the next logic.
1 2$.ajax({ type: "GET", url: AJS.contextPath() + "/rest/api/accessmode", contentType: 'application/json', dataType: "json", timeout: 5000 }).done(function (xhr) { if (xhr.responseText === "READ_ONLY") { // show an error message } else { // continue } }
The REST end-point will return either a "READ_ONLY"
or "READ_WRITE"
response.
Any POST/PUT/DELETE REST requests will be effectively blocked by a request filter, with the option for allowing them to pass through via the @ReadOnlyAccessAllowed
annotation on the relevant REST resource’s method/class/package. We allow GET requests to pass through the request filter. In some special cases, where you also want to block GET requests, you can use the @ReadOnlyAccessBlocked
annotation on the relevant REST resource’s method/class/package.
Any POST/PUT/DELETE action requests will be effectively blocked by an interceptor, with the option for allowing them to pass through via the @ReadOnlyAccessAllowed
annotation on the relevant Action method/class/package. We allow GET requests to pass through the interceptor. In some special cases, where you also want to block GET requests, you can use the @ReadOnlyAccessBlocked
annotation on the relevant Struts action’s method/class/package.
Most apps will want to maintain backwards compatibility with versions prior to Confluence 6.10. We've provided a compatibility library to help with this.
To use the compatibility library in you app, add a new dependency with the compile scope to your plugin’s pom.xml
file as follows:
1 2<dependency> <groupId>com.atlassian.confluence.compat</groupId> <artifactId>confluence-compat-lib</artifactId> <version>1.2.1</version> </dependency>
Then, import the following OSGi package if you're using amps to build the plugin:
1 2<Import-Package> ... com.atlassian.confluence.api.service.accessmode;resolution:="optional", ... *;resolution:=optional </Import-Package>
Note: you must import it as an optional package with resolution:="optional"
, or the app won't work in older Confluence versions.
The library provides you the following classes/services to make your app work with read-only mode:
The ReadWriteAccessModeCondition
can be used to make a web-item or web-panel definition visible when the access mode is READ_WRITE
(when Confluence is not in read-only mode). For example:
1 2<web-item key="awesomeWebItem" name="AwesomeWebItem" section="system.content.button" weight="150"> <label key="i18n.label.key"/> <tooltip key="i18n.tooltip.key"/> <condition class="com.atlassian.confluence.plugin.descriptor.web.conditions.ReadWriteAccessModeCondition"/> <condition class="com.atlassian.confluence.plugin.descriptor.web.conditions.PagePermissionCondition"> <param name="permission">view</param> </condition> <styleClass>customCssStyle</styleClass> <param name="iconClass">customIconClass</param> </web-item>
In the example above, the awesomeWebItem
is only visible if the user has VIEW
permission on the page and Confluence is not in read-only mode.
The ReadWriteAccessModeUrlReadingCondition
can be used to make a web-resource definition available for download when the access mode is READ_WRITE
(when Confluence is not in read-only mode). For example:
1 2<web-resource key="awesome-resources" name="Awesome Resources"> <condition class="com.atlassian.confluence.plugin.descriptor.web.urlreadingconditions.ReadWriteAccessModeUrlReadingCondition"> </condition> <resource name="awesome-view.js" type="download" location="awesome-view.js"/> </web-resource>
In the example above, the awesome-resources
is only visible if Confluence is not in read-only mode.
If your app needs to check the access mode in its logic, you can declare a Spring bean and inject the AccessModeCompatService
component into a class (preferably in a Service component, Struts action or REST resource) as follows:
1 2<beans:bean id="accessModeCompatService" class="com.atlassian.confluence.compat.api.service.accessmode.impl.DefaultAccessModeCompatService"/>
Another option is to use the Atlassian Spring Scanner library to look up this class at compile time and inject it to the caller service with the @ClasspathComponent
annotation.
1 2@Component public class AwesomeContentService { final AccessModeCompatService accessModeCompatService; @Autowired public AwesomeContentService(final AccessModeCompatService accessModeCompatService) { // your awesome business logic } }
The @ReadOnlyAccessAllowed annotation is to bypass the read-only check when a request is served by a Struts action or a REST resource. You can add the annotation to a method, a class, or a package.
For example, an action executed by a sysadmin should be bypassed in read-only mode as follows:
1 2@ReadOnlyAccessAllowed @WebSudoRequired public class ReIndexAwesomeContentAction extends ConfluenceActionSupport { @Override public String execute () throws Exception { return SUCCESS; } }
WARNING: This annotation must be used for admin actions only or user usage tracking services, for example, recently viewed and analytics.
Normally, an action that serves a POST/PUT/DELETE request is blocked in read-only mode by default. However, it can be also blocked while serving a GET request if the action is annotated with @ReadOnlyAccessBlocked as follows:
1 2@ReadOnlyAccessBlocked public class ViewAwesomeContentAction extends ConfluenceActionSupport { @Override public String execute () throws Exception { return SUCCESS; } }
When enabled, read-only mode alters how content editing permissions are checked, and by default blocks all content editing permissions for all users regardless of whether they have these permissions normally. This can complicate permission checking, when you need to determine if a permission is blocked because of read-only mode or because the user is denied access normally.
To execute some code and have it behave as if read-only mode is disabled, use withReadOnlyAccessExemption()
as follows:
1 2public class AwesomeContentPermissionManager { @Autowired private AccessModeService accessModeService; public boolean hasAwesomeEditPermission() { try { return accessService.withReadOnlyAccessExemption( () -> spacePermissionManager.hasPermission(SpacePermission.CREATEEDIT_PAGE_PERMISSION, null, authorizationContext.getAuthenticatedUser())); } catch (ServiceException e) { return spacePermissionManager.hasPermission(SpacePermission.CREATEEDIT_PAGE_PERMISSION, null, authorizationContext.getAuthenticatedUser()); } } }
This method is also available in the AccessModeCompatService
.
NOTE: Only code that uses shouldEnforceReadOnlyAccess()
will execute as if read-only mode is disabled when run with this method. Code paths that use isReadOnlyAccessModeEnabled()
will not be affected.
To confirm that your app is compatible, and ensure it does not write content to the database when read-only mode is enabled, you should enable read-only mode and test what actions are possible with your app. Don't forget to consider users who may already be editing a page or in the midst of an operation at the point read-only mode is enabled.
Mark your app as compatible by setting the read-only-access-mode-compatible
parameter to true in the plugin-info tag, as shown in the example below:
1 2<plugin-info> <param name="atlassian-data-center-compatible">true</param> <param name="read-only-access-mode-compatible">true</param> <description>${project.description}</description> <version>${project.version}</version> <vendor name="${project.organization.name}" url="${project.organization.url}" /> </plugin-info>
Incompatible apps will be listed on the read-only mode admin screen.
Rate this page: