diff --git a/documents-storage-jcr/src/main/java/org/exoplatform/documents/storage/jcr/webdav/plugin/PathCommandHandler.java b/documents-storage-jcr/src/main/java/org/exoplatform/documents/storage/jcr/webdav/plugin/PathCommandHandler.java index 9c0f36d72f..31b7cebcc7 100644 --- a/documents-storage-jcr/src/main/java/org/exoplatform/documents/storage/jcr/webdav/plugin/PathCommandHandler.java +++ b/documents-storage-jcr/src/main/java/org/exoplatform/documents/storage/jcr/webdav/plugin/PathCommandHandler.java @@ -23,8 +23,8 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; -import java.util.Objects; import java.util.Optional; +import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.jcr.*; @@ -95,6 +95,15 @@ public class PathCommandHandler { public static final String PATHS_CONCAT_FORMAT = "%s/%s"; + public static final String SEGMENT_CHAR_REPLACEMENT = "_"; + + /** + * Characters a name cannot carry into a WebDAV path segment: the two path + * separators, and the characters whose percent-encoded form is in + * {@code StrictHttpFirewall}'s blocklist. + */ + private static final Pattern UNSAFE_SEGMENT_CHARS = Pattern.compile("[/\\\\%;\\p{Cntrl}]"); + protected static final Log LOG = ExoLogger.getLogger(PathCommandHandler.class); private static final String WEBDAV_IDENTITY_JCR_PATH_CACHE_NAME = "webdav.identityJcrBasePath"; @@ -146,6 +155,23 @@ public void init() { addMappingEventListener(); } + /** + * Last-resort guard keeping a drive name inside a single WebDAV path segment. + * The names fed to it — a Space pretty name, a username — are already URL-safe + * by construction (see {@link #getIdentitySegmentName(Identity)}); this only + * makes sure an identity store that yields something unexpected cannot emit a + * '%2F', which is rejected before the request reaches any handler and which, + * once decoded, would split the drive into two segments so that the identity + * id can no longer be read back from the path. + * + * @param segmentName drive name, may be null + * @return the name with every character unusable in a path segment replaced + * by {@link #SEGMENT_CHAR_REPLACEMENT} + */ + public static String toWebDavSegment(String segmentName) { + return UNSAFE_SEGMENT_CHARS.matcher(StringUtils.defaultString(segmentName)).replaceAll(SEGMENT_CHAR_REPLACEMENT); + } + @SneakyThrows @Cacheable(WEBDAV_IDENTITY_JCR_PATH_CACHE_NAME) public String getIdentityBaseJcrPath(String webDavPath) { @@ -264,11 +290,13 @@ public String resolveToJcrPath(Session session, String webDavPath) throws WebDav if (session.itemExists(legacyChildJcrPath)) { Item existingItem = session.getItem(legacyChildJcrPath); if (existingItem instanceof Node existingNode) { - String existingWebDavPath = getOrCreateWebDavPath(existingNode); - String decodedExistingWebDavPath = decodeUrlString(existingWebDavPath); - String decodedExistingWebDavPathPrefix = StringUtils.removeEnd(decodedExistingWebDavPath, "/") + "/"; // NOSONAR - if (webDavPath.startsWith(decodedExistingWebDavPathPrefix) - || webDavPath.equals(decodedExistingWebDavPath)) { + // Compare the identity-relative parts only: the drive segment is + // addressed by its id, so a client may legitimately hold an older + // spelling of the drive name in the path it sends + String existingRelativePath = getIdentityRelativeDecodedWebDavPath(getOrCreateWebDavPath(existingNode)); + if (StringUtils.isBlank(existingRelativePath) + || StringUtils.equals(identityRelativeWebDavPath, existingRelativePath) + || StringUtils.startsWith(identityRelativeWebDavPath, existingRelativePath + "/")) { currentParentJcrPath = legacyChildJcrPath; continue; } @@ -307,7 +335,7 @@ public String getOrCreateWebDavPath(Node node) { return null; } String identityBaseJcrPath = getIdentityBaseJcrPath(identityId); - String identityRootWebDavPath = getIdentityRootWebDavPath(identityId, getIdentityDisplayName(identity)); + String identityRootWebDavPath = getIdentityRootWebDavPath(identityId, getIdentitySegmentName(identity)); return getOrCreateWebDavPath(String.valueOf(identityId), identityBaseJcrPath, identityRootWebDavPath, @@ -486,6 +514,19 @@ private String getIdentityRelativeWebDavPath(String webDavPath) { .collect(Collectors.joining("/")); } + /** + * @param encodedWebDavPath a WebDAV path as stored/emitted, percent-encoded + * @return the same path without its drive segment, each remaining segment + * decoded — comparable with the decoded path a client sends + */ + private String getIdentityRelativeDecodedWebDavPath(String encodedWebDavPath) { + return Arrays.stream(StringUtils.defaultString(encodedWebDavPath).split("/")) + .filter(StringUtils::isNotBlank) + .skip(1) + .map(this::decodeUrlString) + .collect(Collectors.joining("/")); + } + private List splitDecodedSegments(String relativeWebDavPath) { return Arrays.stream(relativeWebDavPath.split("/")) .filter(StringUtils::isNotBlank) @@ -734,30 +775,42 @@ private Long getIdentityIdFromJcrPath(String jcrPath) { } private String getIdentityRootWebDavPath(long identityId) { - return getIdentityRootWebDavPath(identityId, getIdentityDisplayName(identityId)); + return getIdentityRootWebDavPath(identityId, getIdentitySegmentName(identityManager.getIdentity(identityId))); } - private String getIdentityRootWebDavPath(long identityId, String displayName) { + private String getIdentityRootWebDavPath(long identityId, String segmentName) { return String.format("/%s%s%s%s", - encodeUrlString(displayName), + encodeUrlString(toWebDavSegment(segmentName)), IDENTITY_ID_PREFIX, identityId, IDENTITY_ID_SUFFIX); } - private String getIdentityDisplayName(long identityId) { - Identity identity = identityManager.getIdentity(identityId); - return Objects.requireNonNullElseGet(getIdentityDisplayName(identity), () -> String.valueOf(identityId)); - } - - private String getIdentityDisplayName(Identity identity) { - if (identity != null && identity.isSpace()) { - Space space = spaceService.getSpaceByPrettyName(identity.getRemoteId()); - if (space != null) { - return space.getDisplayName(); - } + /** + * Returns the name an identity contributes to its WebDAV drive segment: + * + * + * @param identity {@link Identity} of the drive owner, may be null + * @return the name the drive is addressed by, never null + */ + public static String getIdentitySegmentName(Identity identity) { + if (identity == null) { + return ""; + } + if (identity.isSpace()) { + return StringUtils.defaultIfBlank(identity.getRemoteId(), identity.getId()); } - return identity == null ? null : identity.getProfile().getFullName(); + String fullName = identity.getProfile() == null ? null : identity.getProfile().getFullName(); + return StringUtils.firstNonBlank(fullName, identity.getRemoteId(), identity.getId(), ""); } @SneakyThrows diff --git a/documents-storage-jcr/src/main/java/org/exoplatform/documents/storage/jcr/webdav/plugin/WebdavReadCommandHandler.java b/documents-storage-jcr/src/main/java/org/exoplatform/documents/storage/jcr/webdav/plugin/WebdavReadCommandHandler.java index 6ce7e22363..c79653efc5 100644 --- a/documents-storage-jcr/src/main/java/org/exoplatform/documents/storage/jcr/webdav/plugin/WebdavReadCommandHandler.java +++ b/documents-storage-jcr/src/main/java/org/exoplatform/documents/storage/jcr/webdav/plugin/WebdavReadCommandHandler.java @@ -38,6 +38,8 @@ import static org.exoplatform.documents.storage.jcr.webdav.plugin.PathCommandHandler.IDENTITY_PATHS_FORMAT; import static org.exoplatform.documents.storage.jcr.webdav.plugin.PathCommandHandler.LOG; import static org.exoplatform.documents.storage.jcr.webdav.plugin.PathCommandHandler.PROPERTY_NAMES; +import static org.exoplatform.documents.storage.jcr.webdav.plugin.PathCommandHandler.getIdentitySegmentName; +import static org.exoplatform.documents.storage.jcr.webdav.plugin.PathCommandHandler.toWebDavSegment; import static org.exoplatform.documents.webdav.model.constant.PropertyConstants.CHECKEDIN; import static org.exoplatform.documents.webdav.model.constant.PropertyConstants.CHECKEDOUT; import static org.exoplatform.documents.webdav.model.constant.PropertyConstants.CHILDCOUNT; @@ -190,7 +192,7 @@ public WebDavItem get(Session session, // NOSONAR return get(getNode(session, pathCommandHandler.resolveToJcrPath(session, webDavPath)), pathCommandHandler.getIdentityBaseJcrPath(webDavPath), identity.getIdentityId(), - identity.getProfile().getFullName(), + getIdentitySegmentName(identity), requestedPropertyNames, requestPropertyNamesOnly, depth, @@ -261,7 +263,7 @@ public List getVersions(Session session, String webDavPath, Set get(getVersionNode(version), identityBaseJcrPath, identity.getIdentityId(), - identity.getProfile().getFullName(), + getIdentitySegmentName(identity), requestedPropertyNames, false, 0, @@ -365,7 +367,7 @@ private WebDavItem get(Node node, return get(node, pathCommandHandler.getIdentityBaseJcrPath(identityId), identity.getIdentityId(), - identity.getProfile().getFullName(), + getIdentitySegmentName(identity), requestedPropertyNames, false, 0, @@ -377,7 +379,7 @@ private WebDavItem get(Node node, private WebDavItem get(Node node, // NOSONAR String identityBaseJcrPath, long identityId, - String displayName, + String segmentName, Set requestedPropertyNames, boolean requestPropertyNamesOnly, int depth, @@ -387,8 +389,8 @@ private WebDavItem get(Node node, // NOSONAR } WebDavItem result = new WebDavItem(); result.setFile(isFile(node)); - String webDavPath = getMappedWebDavPath(node, identityBaseJcrPath, identityId, displayName); - String identityRootWebDavPath = getIdentityRootWebDavPath(identityId, displayName); + String webDavPath = getMappedWebDavPath(node, identityBaseJcrPath, identityId, segmentName); + String identityRootWebDavPath = getIdentityRootWebDavPath(identityId, segmentName); String identifier = identityBaseUri; if (!StringUtils.equals(webDavPath, identityRootWebDavPath)) { identifier = identityBaseUri + webDavPath.substring(identityRootWebDavPath.length()); @@ -400,7 +402,7 @@ private WebDavItem get(Node node, // NOSONAR node, identityBaseJcrPath, identityId, - displayName, + segmentName, requestedPropertyNames, requestPropertyNamesOnly, depth, @@ -418,7 +420,7 @@ private void addChildren(WebDavItem webDavItem, // NOSONAR Node node, String identityBaseJcrPath, long identityId, - String displayName, + String segmentName, Set requestedPropertyNames, boolean requestPropertyNamesOnly, int depth, @@ -431,7 +433,7 @@ private void addChildren(WebDavItem webDavItem, // NOSONAR .map(childNode -> get(childNode, identityBaseJcrPath, identityId, - displayName, + segmentName, requestedPropertyNames, requestPropertyNamesOnly, depth - 1, @@ -706,9 +708,12 @@ private WebDavItem getWebDavIdentityItem(Session session, // NOSONAR displayName = getDisplayName(identityId); } WebDavItem identityWebDavItem = new WebDavItem(); + // The drive is addressed by its segment name (Space pretty name / username) + // and only presented under its display name + String segmentName = getIdentitySegmentName(identityManager.getIdentity(identityId)); String identityBaseUri = String.format(IDENTITY_PATHS_FORMAT, baseUri, - encodeUrlString(displayName), + encodeUrlString(toWebDavSegment(segmentName)), IDENTITY_ID_PREFIX, identityId, IDENTITY_ID_SUFFIX); @@ -723,7 +728,7 @@ private WebDavItem getWebDavIdentityItem(Session session, // NOSONAR identityWebDavItem.setWebDavPath(getMappedWebDavPath(identityParentNode, identityBaseJcrPath, identityId, - displayName)); + segmentName)); addProperties(identityWebDavItem, identityParentNode, requestedPropertyNames, @@ -733,7 +738,7 @@ private WebDavItem getWebDavIdentityItem(Session session, // NOSONAR identityParentNode, identityBaseJcrPath, identityId, - displayName, + segmentName, requestedPropertyNames, requestPropertyNamesOnly, depth, @@ -765,9 +770,9 @@ private void addProperties(WebDavItem result, } } - private String getIdentityRootWebDavPath(long identityId, String displayName) { + private String getIdentityRootWebDavPath(long identityId, String segmentName) { return String.format("/%s%s%s%s", - encodeUrlString(displayName), + encodeUrlString(toWebDavSegment(segmentName)), IDENTITY_ID_PREFIX, identityId, IDENTITY_ID_SUFFIX); @@ -777,8 +782,8 @@ private String getIdentityRootWebDavPath(long identityId, String displayName) { private String getMappedWebDavPath(Node node, String identityBaseJcrPath, long identityId, - String displayName) { - String identityRootWebDavPath = getIdentityRootWebDavPath(identityId, displayName); + String segmentName) { + String identityRootWebDavPath = getIdentityRootWebDavPath(identityId, segmentName); if (StringUtils.equals(node.getPath(), identityBaseJcrPath)) { return identityRootWebDavPath; } else { @@ -804,7 +809,7 @@ private String getIdentityBaseUri(String baseUri, long identityId) { private String getIdentityBaseUri(String baseUri, Identity identity) { return String.format(IDENTITY_PATHS_FORMAT, baseUri, - encodeUrlString(identity.getProfile().getFullName()), + encodeUrlString(toWebDavSegment(getIdentitySegmentName(identity))), IDENTITY_ID_PREFIX, identity.getId(), IDENTITY_ID_SUFFIX); diff --git a/documents-storage-jcr/src/test/java/org/exoplatform/documents/storage/jcr/webdav/plugin/PathCommandHandlerTest.java b/documents-storage-jcr/src/test/java/org/exoplatform/documents/storage/jcr/webdav/plugin/PathCommandHandlerTest.java index 1bd81dbb8a..e2bdddbeec 100644 --- a/documents-storage-jcr/src/test/java/org/exoplatform/documents/storage/jcr/webdav/plugin/PathCommandHandlerTest.java +++ b/documents-storage-jcr/src/test/java/org/exoplatform/documents/storage/jcr/webdav/plugin/PathCommandHandlerTest.java @@ -493,7 +493,7 @@ public void testRefreshMappingOrDeleteRefreshesExistingMappingAfterCrossIdentity String newIdentityBaseJcrPath = "/groups/spaces/marketing/Documents"; // NOSONAR String newParentJcrPath = newIdentityBaseJcrPath + "/Folder"; String newJcrPath = newParentJcrPath + "/rapport_equipe.docx"; - String newIdentityRootWebDavPath = "/Marketing%20Space%20%2842%29"; // NOSONAR + String newIdentityRootWebDavPath = "/marketing%20%2842%29"; // NOSONAR NodeImpl movedNode = mock(NodeImpl.class); NodeImpl movedParent = mock(NodeImpl.class); @@ -569,6 +569,66 @@ public void testRefreshMappingOrDeleteRefreshesExistingMappingAfterCrossIdentity assertEquals(newIdentityRootWebDavPath + "/Folder/Rapport%20%C3%89quipe.docx", refreshed.getWebDavPath()); } + @Test + public void testToWebDavSegmentReplacesCharactersUnusableInAPathSegment() { + assertEquals("R&D _ Ops", PathCommandHandler.toWebDavSegment("R&D / Ops")); + assertEquals("a_b", PathCommandHandler.toWebDavSegment("a\\b")); + assertEquals("50_ Club", PathCommandHandler.toWebDavSegment("50% Club")); + assertEquals("a_b", PathCommandHandler.toWebDavSegment("a;b")); + assertEquals("a_b", PathCommandHandler.toWebDavSegment("a\nb")); + assertEquals("Marketing Équipe", PathCommandHandler.toWebDavSegment("Marketing Équipe")); + assertEquals("", PathCommandHandler.toWebDavSegment(null)); + } + + @Test + @SneakyThrows + public void testGetOrCreateWebDavPathAddressesSpaceDriveByPrettyNameWhenDisplayNameHasSlash() { + String spaceBaseJcrPath = "/groups/spaces/marketing/Documents"; // NOSONAR + String spaceFileJcrPath = spaceBaseJcrPath + "/rapport.docx"; + + NodeImpl spaceFileNode = mock(NodeImpl.class); + NodeImpl spaceRootNode = mock(NodeImpl.class); + SessionImpl spaceSession = mock(SessionImpl.class); + Identity spaceIdentity = mock(Identity.class); + Space marketingSpace = mock(Space.class); + + when(spaceFileNode.getPath()).thenReturn(spaceFileJcrPath); + when(spaceFileNode.getName()).thenReturn("rapport.docx"); + when(spaceFileNode.getIdentifier()).thenReturn("space-file-id"); + when(spaceFileNode.getParent()).thenReturn(spaceRootNode); + when(spaceFileNode.getSession()).thenReturn(spaceSession); + when(spaceSession.getUserID()).thenReturn(USER1); + when(spaceRootNode.getPath()).thenReturn(spaceBaseJcrPath); + + when(spaceService.getSpaceByGroupId("/spaces/marketing")).thenReturn(marketingSpace); + when(spaceService.getSpaceByPrettyName(SPACE_NAME)).thenReturn(marketingSpace); + when(marketingSpace.getPrettyName()).thenReturn(SPACE_NAME); + when(marketingSpace.getGroupId()).thenReturn("/spaces/marketing"); + when(marketingSpace.getDisplayName()).thenReturn("R&D / Ops"); + when(identityManager.getOrCreateSpaceIdentity(SPACE_NAME)).thenReturn(spaceIdentity); + when(identityManager.getIdentity(42L)).thenReturn(spaceIdentity); + when(spaceIdentity.getIdentityId()).thenReturn(42L); + when(spaceIdentity.isSpace()).thenReturn(true); + when(spaceIdentity.getRemoteId()).thenReturn(SPACE_NAME); + + when(webDavPathMappingStorage.findByNodeIdentifier(anyString())).thenReturn(Optional.empty()); + when(webDavPathMappingStorage.findByJcrPath(anyString())).thenReturn(Optional.empty()); + when(webDavPathMappingStorage.findByParentJcrPathAndNormalizedVisibleName(anyString(), + anyString())).thenReturn(Optional.empty()); + + String result = handler.getOrCreateWebDavPath(spaceFileNode); + + // the drive is addressed by the Space pretty name — the name its JCR drive + // is created under — so the '/' of the display name never reaches the path: + // %2F is rejected before any handler runs, and once decoded it would split + // the drive into two segments + assertEquals("/marketing%20%2842%29/rapport.docx", result); + assertFalse(result.contains("%2F")); + + // the identity id is still readable back from the path the client sends + assertEquals(Long.valueOf(42L), handler.getIdentityIdFromWebDavPath(handler.decodeUrlString(result))); + } + @Test public void testIsTitlePropertyPath() { assertTrue(handler.isTitlePropertyPath(JCR_PATH + "/exo:title")); diff --git a/documents-storage-jcr/src/test/java/org/exoplatform/documents/storage/jcr/webdav/plugin/WebdavReadCommandHandlerTest.java b/documents-storage-jcr/src/test/java/org/exoplatform/documents/storage/jcr/webdav/plugin/WebdavReadCommandHandlerTest.java index 6d78ac6cad..496658ef53 100644 --- a/documents-storage-jcr/src/test/java/org/exoplatform/documents/storage/jcr/webdav/plugin/WebdavReadCommandHandlerTest.java +++ b/documents-storage-jcr/src/test/java/org/exoplatform/documents/storage/jcr/webdav/plugin/WebdavReadCommandHandlerTest.java @@ -25,6 +25,7 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.io.ByteArrayInputStream; @@ -189,6 +190,60 @@ public void testGetRootPath() { assertNotNull(webDavItem.getProperties()); } + @Test + @SneakyThrows + public void testGetRootPathAddressesSpaceByPrettyNameAndPersonalDriveByFullName() { + String spaceBaseJcrPath = "/groups/spaces/rd-ops/Documents"; // NOSONAR + Space space = mock(Space.class); + Identity spaceIdentity = mock(Identity.class); + Profile spaceProfile = mock(Profile.class); + + when(memberSpacesListAccess.getSize()).thenReturn(1); + when(spaceService.getMemberSpacesIds(USERNAME, 0, 1)).thenReturn(List.of("s1")); + when(spaceService.getSpaceById("s1")).thenReturn(space); + when(space.getPrettyName()).thenReturn("rd-ops"); + when(space.getDisplayName()).thenReturn("R&D / Ops"); + when(identityManager.getOrCreateSpaceIdentity("rd-ops")).thenReturn(spaceIdentity); + when(identityManager.getIdentity(42L)).thenReturn(spaceIdentity); + when(spaceIdentity.getProfile()).thenReturn(spaceProfile); + when(spaceProfile.getFullName()).thenReturn("R&D / Ops"); + when(spaceIdentity.getIdentityId()).thenReturn(42L); + when(spaceIdentity.isSpace()).thenReturn(true); + when(spaceIdentity.getRemoteId()).thenReturn("rd-ops"); + when(pathCommandHandler.getIdentityBaseJcrPath(42L)).thenReturn(spaceBaseJcrPath); + when(session.getItem(spaceBaseJcrPath)).thenReturn(node); + + WebDavItem webDavItem = handler.get(session, + "/", + REQUESTED_PROPERTY_NAMES, + false, + 1, + BASE_URI, + USERNAME); + + assertNotNull(webDavItem.getChildren()); + WebDavItem spaceItem = webDavItem.getChildren() + .stream() + .filter(child -> child.getIdentifier().toString().contains("%2842%29")) + .findFirst() + .orElse(null); + assertNotNull(spaceItem); + // the drive is addressed by the Space pretty name, so the '/' of the + // display name never reaches the href + assertEquals(BASE_URI + "/rd-ops%20%2842%29", spaceItem.getIdentifier().toString()); + // while it stays presented under its real display name + assertEquals("R&D / Ops", spaceItem.getProperty(DISPLAYNAME).getValue()); + + // the personal drive keeps the user full name, it is not slugified + WebDavItem userItem = webDavItem.getChildren() + .stream() + .filter(child -> child.getIdentifier().toString().contains("%281%29")) + .findFirst() + .orElse(null); + assertNotNull(userItem); + assertEquals(BASE_URI + "/John%20Doe%20%281%29", userItem.getIdentifier().toString()); + } + @Test @SneakyThrows public void testGetWithNodePathUsesMappedWebDavPath() { diff --git a/documents-webapp/src/main/webapp/vue-app/documents-user-setting/components/drawers/DocumentsWebdavMapDrivesDrawer.vue b/documents-webapp/src/main/webapp/vue-app/documents-user-setting/components/drawers/DocumentsWebdavMapDrivesDrawer.vue index 1b35b49653..a08f38aecb 100644 --- a/documents-webapp/src/main/webapp/vue-app/documents-user-setting/components/drawers/DocumentsWebdavMapDrivesDrawer.vue +++ b/documents-webapp/src/main/webapp/vue-app/documents-user-setting/components/drawers/DocumentsWebdavMapDrivesDrawer.vue @@ -188,6 +188,7 @@ export default { userIdentity: null, spaceIdentity: null, spaceIdentityId: null, + spaceIdentityRemoteId: null, hrefCopied: false, canCopy: false, tipsByOs: { @@ -269,12 +270,15 @@ export default { }; }, href() { + // The personal drive is addressed by the user full name, a Space by its + // pretty name — never by the Space display name, which may hold a '/' + // and would split the drive into two path segments if (this.driveType === 'ALL') { return `${window.location.origin}/webdav/drives`; } else if (this.driveType === 'PERSONAL') { return `${window.location.origin}/webdav/drives/d/${this.userFullName} (${eXo.env.portal.userIdentityId})`; } else if (this.driveType === 'SPACE' && this.spaceIdentityId) { - return `${window.location.origin}/webdav/drives/d/${this.spaceIdentity.displayName} (${this.spaceIdentityId})`; + return `${window.location.origin}/webdav/drives/d/${this.spaceIdentityRemoteId} (${this.spaceIdentityId})`; } else { return null; } @@ -289,9 +293,11 @@ export default { }, async spaceIdentity() { this.spaceIdentityId = null; + this.spaceIdentityRemoteId = null; if (this.spaceIdentity) { const identity = await this.$identityService.getIdentityByProviderIdAndRemoteId(this.spaceIdentity.providerId, this.spaceIdentity.remoteId); this.spaceIdentityId = identity?.id; + this.spaceIdentityRemoteId = identity?.remoteId || this.spaceIdentity.remoteId; } }, },