CVE-2023-49280 (published 2023-12-04T23:15Z)

XWiki Change Request is an XWiki application allowing to request changes on a wiki without publishing directly the changes. Change request allows to edit any page by default, and the changes are then exported in an XML file that anyone can download. So it's possible for an attacker to obtain password hash of users by performing an edit on the user profiles and then downloading the XML file that has been created. This is also true for any document that might contain password field and that a user can view.\nThis vulnerability impacts all version of Change Request, but the impact depends on the rights that has been set on the wiki since it requires for the user to have the Change request right (allowed by default) and view rights on the page to target. This issue cannot be easily exploited in an automated way. The patch consists in denying to users the right of editing pages that contains a password field with change request. It means that already existing change request for those pages won't be removed by the patch, administrators needs to take care of it. The patch is provided in Change Request 1.10, administrators should upgrade immediately. It's possible to workaround the vulnerability by denying manually the Change request right on some spaces, such as XWiki space which will include any user profile by default.

CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-522
cwe_name: Insufficiently Protected Credentials
description: The product transmits or stores authentication credentials, but it uses an insecure method that is susceptible to unauthorized interception and/or retrieval.
extended_description: 
            
            
            
         
         

Fix information:
repo_url: https://github.com/xwiki-contrib/application-changerequest
commit_details: repo_url: https://github.com/xwiki-contrib/application-changerequest
author: Simon Urli
author_date: 2023-08-29 18:36:59+02:00
msg: CRAPP-302: Edit button is not displayed for guest user

  * Change the way to deal with change request edition right
repository: repo_url: https://github.com/xwiki-contrib/application-changerequest
repo_name: xwiki-contrib/application-changerequest
description: New contribution workflow allowing supervised contributions.
repo_language: Java
owner: xwiki-contrib
file_changes: filename: ChangeRequestRightsManager.java
change_type: ModificationType.MODIFY
diff: @@ -201,4 +201,33 @@ public interface ChangeRequestRightsManager
     {
         return false;
     }
+
+    /**
+     * Check if the given user is allowed to use change request to edit the given document reference.
+     * @param userReference the user for whom to perform the check
+     * @param documentReference the reference for which to check authorization
+     * @return {@code true} if the given user is allowed to perform the edition with change request
+     * @throws ChangeRequestException in case of problem when performing the check
+     * @since 1.10
+     */
+    default boolean isEditWithChangeRequestAllowed(UserReference userReference, DocumentReference documentReference)
+        throws ChangeRequestException
+    {
+        return false;
+    }
+
+    /**
+     * Check if the given user is allowed to use change request to create a document having the given reference for
+     * parent.
+     * @param userReference the user for whom to perform the check
+     * @param parentSpaceReference the parent space where the new document would be created
+     * @return {@code true} if the given user is allowed to create a document with change request at the given place
+     * @throws ChangeRequestException in case of problem when performing the check
+     * @since 1.10
+     */
+    default boolean isCreateWithChangeRequestAllowed(UserReference userReference,
+        DocumentReference parentSpaceReference) throws ChangeRequestException
+    {
+        return false;
+    }
 }

programming_language: Java
filename: checkstyle-suppressions.xml
change_type: ModificationType.MODIFY
diff: @@ -53,4 +53,5 @@
   <suppress checks="ClassFanOutComplexity" files="ChangeRequestScriptService.java"/>
   <suppress checks="ClassFanOutComplexity" files="ChangeRequestResourceHandler.java"/>
   <suppress checks="CyclomaticComplexity" files="RightsUpdatedListener.java"/>
+  <suppress checks="CyclomaticComplexity" files="AbstractChangeRequestActionHandler.java"/>
 </suppressions>

programming_language: Ruby
filename: DefaultChangeRequestRightsManager.java
change_type: ModificationType.MODIFY
diff: @@ -20,14 +20,18 @@
 package org.xwiki.contrib.changerequest.internal;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 
 import javax.inject.Inject;
+import javax.inject.Provider;
 import javax.inject.Singleton;
 
 import org.xwiki.component.annotation.Component;
@@ -40,6 +44,7 @@ import org.xwiki.contrib.changerequest.ChangeRequestStatus;
 import org.xwiki.contrib.changerequest.DelegateApproverManager;
 import org.xwiki.contrib.changerequest.FileChange;
 import org.xwiki.contrib.changerequest.rights.ChangeRequestApproveRight;
+import org.xwiki.contrib.changerequest.rights.ChangeRequestRight;
 import org.xwiki.contrib.rights.RightsReader;
 import org.xwiki.contrib.rights.RightsWriter;
 import org.xwiki.contrib.rights.SecurityRuleAbacus;
@@ -59,7 +64,12 @@ import org.xwiki.security.authorization.RuleState;
 import org.xwiki.user.GuestUserReference;
 import org.xwiki.user.UserReference;
 
+import com.xpn.xwiki.XWikiContext;
 import com.xpn.xwiki.XWikiException;
+import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.objects.BaseObject;
+import com.xpn.xwiki.objects.classes.PasswordClass;
+import com.xpn.xwiki.objects.classes.PropertyClass;
 
 /**
  * Component in charge of performing right synchronization operations.
@@ -98,6 +108,9 @@ public class DefaultChangeRequestRightsManager implements ChangeRequestRightsMan
     @Inject
     private ChangeRequestConfiguration configuration;
 
+    @Inject
+    private Provider<XWikiContext> contextProvider;
+
     @Override
     public void copyAllButViewRights(ChangeRequest originalChangeRequest, ChangeRequest targetChangeRequest)
         throws ChangeRequestException
@@ -618,4 +631,64 @@ public class DefaultChangeRequestRightsManager implements ChangeRequestRightsMan
         }
         return result;
     }
+
+    @Override
+    public boolean isEditWithChangeRequestAllowed(UserReference userReference, DocumentReference documentReference)
+        throws ChangeRequestException
+    {
+        DocumentReference userDocReference = this.userReferenceConverter.convert(userReference);
+        boolean hasCREdit = this.authorizationManager.hasAccess(ChangeRequestRight.getRight(), userDocReference,
+            documentReference);
+        boolean result = false;
+        // if the user doesn't have CR edit right, then it's already solved.
+        if (hasCREdit) {
+            XWikiContext context = this.contextProvider.get();
+            try {
+                XWikiDocument document = context.getWiki().getDocument(documentReference, context);
+                // if it's a new document, we don't care
+                if (document.isNew()) {
+                    result = true;
+                } else {
+                    Map<DocumentReference, List<BaseObject>> objects = document.getXObjects();
+                    boolean foundOneObjectWithPassword = false;
+                    for (Map.Entry<DocumentReference, List<BaseObject>> objectsEntry : objects.entrySet()) {
+                        Optional<BaseObject> baseObjectOpt =
+                            objectsEntry.getValue().stream().filter(Objects::nonNull).findFirst();
+                        if (baseObjectOpt.isPresent() && this.isObjectContainingPassword(baseObjectOpt.get())) {
+                            foundOneObjectWithPassword = true;
+                            break;
+                        }
+                    }
+                    if (!foundOneObjectWithPassword) {
+                        result = true;
+                    }
+                }
+            } catch (XWikiException e) {
+                throw new ChangeRequestException(String.format("Cannot load document [%s] to check if CR right should "
+                    + "be granted for user [%s]", documentReference, userDocReference), e);
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public boolean isCreateWithChangeRequestAllowed(UserReference userReference, DocumentReference parentSpaceReference)
+        throws ChangeRequestException
+    {
+        DocumentReference userDocReference = this.userReferenceConverter.convert(userReference);
+        return this.authorizationManager.hasAccess(ChangeRequestRight.getRight(), userDocReference,
+            parentSpaceReference);
+    }
+
+    private boolean isObjectContainingPassword(BaseObject baseObject)
+    {
+        XWikiContext context = contextProvider.get();
+        Collection<PropertyClass> propertyClassCollection = baseObject.getXClass(context).getFieldList();
+        for (PropertyClass propertyClass : propertyClassCollection) {
+            if (propertyClass instanceof PasswordClass) {
+                return true;
+            }
+        }
+        return false;
+    }
 }

programming_language: Java
filename: AbstractChangeRequestActionHandler.java
change_type: ModificationType.MODIFY
diff: @@ -202,6 +202,12 @@ public abstract class AbstractChangeRequestActionHandler implements ChangeReques
         XWikiContext context = this.contextProvider.get();
         String serializedDocReference = request.getParameter("docReference");
         DocumentReference documentReference = this.documentReferenceResolver.resolve(serializedDocReference);
+        UserReference currentUserReference = this.currentUserReferenceResolver.resolve(CurrentUserReference.INSTANCE);
+        if (!this.changeRequestRightsManager.isEditWithChangeRequestAllowed(currentUserReference, documentReference)) {
+            throw new ChangeRequestException(
+                String.format("User [%s] is not allowed to edit the document [%s] through a change request.",
+                    currentUserReference, documentReference));
+        }
 
         XWikiDocument modifiedDocument = null;
         try {

programming_language: Java
filename: ChangeRequestAuthorizationScriptService.java
change_type: ModificationType.MODIFY
diff: @@ -242,4 +242,33 @@ public class ChangeRequestAuthorizationScriptService implements ScriptService
         UserReference currentUserReference = this.currentUserReferenceResolver.resolve(CurrentUserReference.INSTANCE);
         return this.changeRequestRightsManager.isAuthorizedToSplit(currentUserReference, changeRequest);
     }
+
+    /**
+     * Check if the current user is allowed to use change request to edit the given document reference.
+     * @param documentReference the reference for which to check authorization
+     * @return {@code true} if current user is allowed to perform the edition with change request
+     * @throws ChangeRequestException in case of problem when performing the checks
+     * @since 1.10
+     */
+    public boolean isEditWithChangeRequestAllowed(DocumentReference documentReference) throws ChangeRequestException
+    {
+        UserReference currentUserReference = this.currentUserReferenceResolver.resolve(CurrentUserReference.INSTANCE);
+        return this.changeRequestRightsManager.isEditWithChangeRequestAllowed(currentUserReference, documentReference);
+    }
+
+    /**
+     * Check if the current user is allowed to use change request to create a document having the given reference for
+     * parent.
+     * @param parentSpaceReference the parent space where the new document would be created
+     * @return {@code true} if current user is allowed to create a document with change request at the given place
+     * @throws ChangeRequestException in case of problem when performing the checks
+     * @since 1.10
+     */
+    public boolean isCreateWithChangeRequestAllowed(DocumentReference parentSpaceReference)
+        throws ChangeRequestException
+    {
+        UserReference currentUserReference = this.currentUserReferenceResolver.resolve(CurrentUserReference.INSTANCE);
+        return this.changeRequestRightsManager.isCreateWithChangeRequestAllowed(currentUserReference,
+            parentSpaceReference);
+    }
 }

programming_language: Java
filename: DefaultChangeRequestRightsManagerTest.java
change_type: ModificationType.MODIFY
diff: @@ -23,12 +23,15 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import javax.inject.Inject;
+import javax.inject.Provider;
 
 import org.junit.jupiter.api.Test;
 import org.xwiki.contrib.changerequest.ApproversManager;
@@ -39,6 +42,7 @@ import org.xwiki.contrib.changerequest.ChangeRequestStatus;
 import org.xwiki.contrib.changerequest.DelegateApproverManager;
 import org.xwiki.contrib.changerequest.FileChange;
 import org.xwiki.contrib.changerequest.internal.approvers.ChangeRequestApproversManager;
+import org.xwiki.contrib.changerequest.rights.ChangeRequestRight;
 import org.xwiki.contrib.rights.RightsReader;
 import org.xwiki.contrib.rights.RightsWriter;
 import org.xwiki.contrib.rights.SecurityRuleAbacus;
@@ -61,7 +65,14 @@ import org.xwiki.test.junit5.mockito.MockComponent;
 import org.xwiki.user.GuestUserReference;
 import org.xwiki.user.UserReference;
 
+import com.xpn.xwiki.XWiki;
+import com.xpn.xwiki.XWikiContext;
 import com.xpn.xwiki.XWikiException;
+import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.objects.BaseObject;
+import com.xpn.xwiki.objects.classes.BaseClass;
+import com.xpn.xwiki.objects.classes.PasswordClass;
+import com.xpn.xwiki.objects.classes.PropertyClass;
 
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -70,6 +81,7 @@ import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
@@ -113,6 +125,9 @@ class DefaultChangeRequestRightsManagerTest
     @MockComponent
     private DelegateApproverManager<ChangeRequest> changeRequestDelegateApproverManager;
 
+    @MockComponent
+    private Provider<XWikiContext> contextProvider;
+
     @Test
     void copyAllButViewRights() throws AuthorizationException, ChangeRequestException, XWikiException
     {
@@ -867,4 +882,92 @@ class DefaultChangeRequestRightsManagerTest
         when(this.changeRequestApproversManager.isApprover(userReference, changeRequest, false)).thenReturn(false);
         assertFalse(this.rightsManager.isAuthorizedToComment(userReference, changeRequest));
     }
+
+    @Test
+    void isCreateWithChangeRequestAllowed() throws ChangeRequestException
+    {
+        Right crRight = ChangeRequestRight.getRight();
+        UserReference userReference = mock(UserReference.class);
+        DocumentReference userDocReference = mock(DocumentReference.class);
+        DocumentReference parentSpaceReference = mock(DocumentReference.class);
+
+        when(this.userReferenceConverter.convert(userReference)).thenReturn(userDocReference);
+        when(this.authorizationManager.hasAccess(crRight, userDocReference, parentSpaceReference)).thenReturn(true);
+        assertTrue(this.rightsManager.isCreateWithChangeRequestAllowed(userReference, parentSpaceReference));
+
+        verify(this.authorizationManager).hasAccess(crRight, userDocReference, parentSpaceReference);
+    }
+
+    @Test
+    void isEditWithChangeRequestAllowed() throws ChangeRequestException, XWikiException
+    {
+        Right crRight = ChangeRequestRight.getRight();
+        UserReference userReference = mock(UserReference.class);
+        DocumentReference userDocReference = mock(DocumentReference.class);
+        DocumentReference documentReference = mock(DocumentReference.class);
+        XWikiDocument document = mock(XWikiDocument.class);
+        XWiki wiki = mock(XWiki.class);
+        XWikiContext context = mock(XWikiContext.class);
+
+        when(this.userReferenceConverter.convert(userReference)).thenReturn(userDocReference);
+        when(this.contextProvider.get()).thenReturn(context);
+        when(context.getWiki()).thenReturn(wiki);
+        when(wiki.getDocument(documentReference, context)).thenReturn(document);
+        when(this.authorizationManager.hasAccess(crRight, userDocReference, documentReference)).thenReturn(false);
+
+        assertFalse(this.rightsManager.isEditWithChangeRequestAllowed(userReference, documentReference));
+
+        verify(this.authorizationManager).hasAccess(crRight, userDocReference, documentReference);
+        verifyNoInteractions(document);
+
+        when(this.authorizationManager.hasAccess(crRight, userDocReference, documentReference)).thenReturn(true);
+        when(document.isNew()).thenReturn(true);
+        assertTrue(this.rightsManager.isEditWithChangeRequestAllowed(userReference, documentReference));
+
+        verify(this.authorizationManager, times(2)).hasAccess(crRight, userDocReference, documentReference);
+
+        when(document.isNew()).thenReturn(false);
+
+        DocumentReference objReference1 = mock(DocumentReference.class);
+        DocumentReference objReference2 = mock(DocumentReference.class);
+
+        BaseObject baseObject1 = mock(BaseObject.class);
+        BaseObject baseObject2 = mock(BaseObject.class);
+        BaseObject baseObject3 = mock(BaseObject.class);
+
+        List<BaseObject> baseObjectList1 = Arrays.asList(null, baseObject1);
+        List<BaseObject> baseObjectList2 = Arrays.asList(null, baseObject2, baseObject3);
+
+        Map<DocumentReference, List<BaseObject>> objectMap = new LinkedHashMap<>();
+        objectMap.put(objReference1, baseObjectList1);
+        objectMap.put(objReference2, baseObjectList2);
+
+        when(document.getXObjects()).thenReturn(objectMap);
+        BaseClass class1 = mock(BaseClass.class);
+        List<PropertyClass> propertyClasses = List.of(mock(PropertyClass.class), mock(PropertyClass.class),
+            mock(PasswordClass.class));
+        when(class1.getFieldList()).thenReturn(propertyClasses);
+        when(baseObject1.getXClass(context)).thenReturn(class1);
+
+        assertFalse(this.rightsManager.isEditWithChangeRequestAllowed(userReference, documentReference));
+        verify(baseObject1).getXClass(context);
+        verifyNoInteractions(baseObject2);
+        verifyNoInteractions(baseObject3);
+
+        when(class1.getFieldList()).thenReturn(List.of(mock(PropertyClass.class)));
+        BaseClass class2 = mock(BaseClass.class);
+        when(class2.getFieldList()).thenReturn(List.of(mock(PasswordClass.class)));
+        when(baseObject2.getXClass(context)).thenReturn(class2);
+
+        assertFalse(this.rightsManager.isEditWithChangeRequestAllowed(userReference, documentReference));
+        verify(baseObject1, times(2)).getXClass(context);
+        verify(baseObject2).getXClass(context);
+        verifyNoInteractions(baseObject3);
+
+        when(class2.getFieldList()).thenReturn(List.of());
+        assertTrue(this.rightsManager.isEditWithChangeRequestAllowed(userReference, documentReference));
+        verify(baseObject1, times(3)).getXClass(context);
+        verify(baseObject2, times(2)).getXClass(context);
+        verifyNoInteractions(baseObject3);
+    }
 }

programming_language: Java
filename: AddChangesChangeRequestHandlerTest.java
change_type: ModificationType.MODIFY
diff: @@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test;
 import org.suigeneris.jrcs.rcs.Version;
 import org.xwiki.contrib.changerequest.ApproversManager;
 import org.xwiki.contrib.changerequest.ChangeRequest;
+import org.xwiki.contrib.changerequest.ChangeRequestException;
 import org.xwiki.contrib.changerequest.ChangeRequestManager;
 import org.xwiki.contrib.changerequest.ChangeRequestMergeManager;
 import org.xwiki.contrib.changerequest.ChangeRequestReference;
@@ -59,9 +60,12 @@ import com.xpn.xwiki.web.EditForm;
 import com.xpn.xwiki.web.XWikiRequest;
 import com.xpn.xwiki.web.XWikiResponse;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -185,6 +189,8 @@ class AddChangesChangeRequestHandlerTest
         String url = "some url";
         when(wiki.getURL(changeRequestDocReference, "view", context)).thenReturn(url);
 
+        when(this.changeRequestRightsManager.isEditWithChangeRequestAllowed(userReference, documentReference))
+            .thenReturn(true);
         this.handler.handle(changeRequestReference);
         verify(this.requestParameterConverter).convert(request, response);
         verify(document).clone();
@@ -210,7 +216,7 @@ class AddChangesChangeRequestHandlerTest
         when(this.requestParameterConverter.convert(request, response)).thenReturn(Optional.of(request));
         String docReference = "XWiki.Doc.Reference";
         when(request.getParameter("docReference")).thenReturn(docReference);
-        DocumentReference documentReference = mock(DocumentReference.class);
+        DocumentReference documentReference = mock(DocumentReference.class, "editedDoc");
         when(this.documentReferenceResolver.resolve(docReference)).thenReturn(documentReference);
         XWikiDocument document = mock(XWikiDocument.class);
         when(wiki.getDocument(documentReference, context)).thenReturn(document);
@@ -222,7 +228,7 @@ class AddChangesChangeRequestHandlerTest
         ChangeRequest changeRequest = mock(ChangeRequest.class);
         when(this.storageManager.load(changeRequestId)).thenReturn(Optional.of(changeRequest));
         when(changeRequest.getId()).thenReturn(changeRequestId);
-        UserReference userReference = mock(UserReference.class);
+        UserReference userReference = mock(UserReference.class, "currentUser");
         when(this.userReferenceResolver.resolve(CurrentUserReference.INSTANCE)).thenReturn(userReference);
         when(request.getParameter(AddChangesChangeRequestHandler.PREVIOUS_VERSION_PARAMETER)).thenReturn("2.1");
 
@@ -261,9 +267,18 @@ class AddChangesChangeRequestHandlerTest
         String url = "some url";
         when(wiki.getURL(changeRequestDocReference, "view", context)).thenReturn(url);
 
+        when(this.changeRequestRightsManager.isEditWithChangeRequestAllowed(userReference, documentReference))
+            .thenReturn(false);
+        ChangeRequestException changeRequestException =
+            assertThrows(ChangeRequestException.class, () -> this.handler.handle(changeRequestReference));
+        assertEquals("User [currentUser] is not allowed to edit the document [editedDoc] through a change request.",
+            changeRequestException.getMessage());
+
+        when(this.changeRequestRightsManager.isEditWithChangeRequestAllowed(userReference, documentReference))
+            .thenReturn(true);
         this.handler.handle(changeRequestReference);
 
-        verify(this.requestParameterConverter).convert(request, response);
+        verify(this.requestParameterConverter, times(2)).convert(request, response);
         verify(document).clone();
         verify(document).readFromForm(any(EditForm.class), eq(context));
         verify(changeRequest).addFileChange(expectedFileChange);

programming_language: Java
filename: CreateChangeRequestHandlerTest.java
change_type: ModificationType.MODIFY
diff: @@ -30,6 +30,8 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.suigeneris.jrcs.rcs.Version;
 import org.xwiki.contrib.changerequest.ChangeRequest;
+import org.xwiki.contrib.changerequest.ChangeRequestException;
+import org.xwiki.contrib.changerequest.ChangeRequestRightsManager;
 import org.xwiki.contrib.changerequest.ChangeRequestStatus;
 import org.xwiki.contrib.changerequest.FileChange;
 import org.xwiki.contrib.changerequest.events.ChangeRequestCreatedEvent;
@@ -54,6 +56,7 @@ import com.xpn.xwiki.web.XWikiRequest;
 import com.xpn.xwiki.web.XWikiResponse;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
@@ -95,6 +98,9 @@ class CreateChangeRequestHandlerTest
     @MockComponent
     private UserReferenceResolver<CurrentUserReference> userReferenceResolver;
 
+    @MockComponent
+    private ChangeRequestRightsManager changeRequestRightsManager;
+
     private XWikiContext context;
     private XWikiRequest httpServletRequest;
     private XWikiResponse httpServletResponse;
@@ -126,7 +132,7 @@ class CreateChangeRequestHandlerTest
             .thenReturn(Optional.of(this.httpServletRequest));
         String serializedReference = "XWiki.SomeReference";
         when(this.httpServletRequest.getParameter("docReference")).thenReturn(serializedReference);
-        DocumentReference documentReference = mock(DocumentReference.class);
+        DocumentReference documentReference = mock(DocumentReference.class, "editedDoc");
         when(this.documentReferenceResolver.resolve(serializedReference)).thenReturn(documentReference);
         XWikiDocument modifiedDocument = mock(XWikiDocument.class);
         when(this.xWiki.getDocument(documentReference, this.context)).thenReturn(modifiedDocument);
@@ -139,7 +145,7 @@ class CreateChangeRequestHandlerTest
         when(this.httpServletRequest.getParameter("crTitle")).thenReturn(title);
         when(this.httpServletRequest.getParameter("crDescription")).thenReturn(description);
 
-        UserReference userReference = mock(UserReference.class);
+        UserReference userReference = mock(UserReference.class, "currentUser");
         when(this.userReferenceResolver.resolve(CurrentUserReference.INSTANCE)).thenReturn(userReference);
         String previousVersion = "3.2";
         when(this.httpServletRequest.getParameter("previousVersion")).thenReturn(previousVersion);
@@ -184,7 +190,19 @@ class CreateChangeRequestHandlerTest
         String expectedURL = "/mycr";
         when(this.xWiki.getURL(crDocReference, "view", this.context)).thenReturn(expectedURL);
 
+        when(this.changeRequestRightsManager.isEditWithChangeRequestAllowed(userReference, documentReference))
+            .thenReturn(false);
+        when(this.changeRequestRightsManager.isEditWithChangeRequestAllowed(userReference, documentReference))
+            .thenReturn(false);
+        ChangeRequestException changeRequestException =
+            assertThrows(ChangeRequestException.class, () -> this.handler.handle(null));
+        assertEquals("User [currentUser] is not allowed to edit the document [editedDoc] through a change request.",
+            changeRequestException.getMessage());
+
+        when(this.changeRequestRightsManager.isEditWithChangeRequestAllowed(userReference, documentReference))
+            .thenReturn(true);
         this.handler.handle(null);
+
         verify(this.storageManager).save(expectedChangeRequest);
         verify(this.observationManager)
             .notify(any(ChangeRequestCreatedEvent.class), eq(crId), eq(expectedChangeRequest));

programming_language: Java
filename: EditPageUIX.xml
change_type: ModificationType.MODIFY
diff: @@ -293,7 +293,8 @@
 {{html clean='false'}}
 #template("xwikivars.vm")
 #set ($crEditName = "changerequest")
-#set ($hasCREdit = $services.security.authorization.isRightRegistered($crEditName) &amp;&amp; $services.security.authorization.hasAccess($crEditName, $tdoc))
+#set ($hasCREdit = $services.changerequest.authorization.isEditWithChangeRequestAllowed($doc.getDocumentReference()))
+#set ($hasCRCreate = $services.changerequest.authorization.isCreateWithChangeRequestAllowed($doc.getDocumentReference()))
 #set ($hasEdit = $services.security.authorization.hasAccess('edit', $tdoc))
 #set ($hasCreateButton = ($hasCreateSpace || $hasCreatePage))
 #set ($editURL = $doc.getURL('editcr'))
@@ -306,6 +307,8 @@
   &lt;/a&gt;
 &lt;/div&gt;
 #end
+#end
+#if (!$isReadOnly &amp;&amp; $hasCRCreate)
 #if (!$hasCreateButton)
 &lt;div class="btn-group" id="crCreate"&gt;
   &lt;a href="$createURL" role="button" title="$services.localization.render('changerequest.create.button.title')" class="btn btn-default"&gt;

programming_language: TypeScript
filename: SaveButtonsUIX.xml
change_type: ModificationType.MODIFY
diff: @@ -370,7 +370,7 @@
     <property>
       <content>{{velocity}}
 #set ($crEditName = "changerequest")
-#set ($hasCREdit = $services.security.authorization.isRightRegistered($crEditName) &amp;&amp; $services.security.authorization.hasAccess($crEditName, $tdoc))
+#set ($hasCREdit = $services.changerequest.authorization.isEditWithChangeRequestAllowed($doc.getDocumentReference()))
 ## we need to also inject this in view action, because of the inplace editing mode.
 #set ($isEditOrViewAction = ($xcontext.action == 'edit' || $xcontext.action == 'view'))
 #if ($hasCREdit &amp;&amp; $isEditOrViewAction)

programming_language: TypeScript