This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch atlas-2.6
in repository https://gitbox.apache.org/repos/asf/atlas.git


The following commit(s) were added to refs/heads/atlas-2.6 by this push:
     new 9729bb9cc ATLAS-5347: Atlas React UI: Skeleton loader stuck 
indefinitely in Administration Audit tab when expanding Purge/Auto-Purge entity 
details (#707)
9729bb9cc is described below

commit 9729bb9cc24fa1f97d9fa54e87819aa61a6a1258
Author: Nixon Rodrigues <[email protected]>
AuthorDate: Wed Jul 29 11:59:51 2026 +0530

    ATLAS-5347: Atlas React UI: Skeleton loader stuck indefinitely in 
Administration Audit tab when expanding Purge/Auto-Purge entity details (#707)
    
    * ATLAS-5347: Atlas React UI: Skeleton loader stuck indefinitely in 
Administration Audit tab when expanding Purge/Auto-Purge entity details
    
    Co-authored-by: Brijesh Bhalala <[email protected]>
---
 .../views/Administrator/Audits/AuditResults.tsx    |  2 +-
 .../Audits/__tests__/AuditResults.test.tsx         |  5 +-
 .../EntityDetailTabs/AttributeProperties.tsx       |  2 +-
 .../__tests__/AttributeProperties.test.tsx         | 38 +++++++++++-
 .../store/graph/v2/AtlasRelationshipStoreV2.java   | 13 ++++
 .../org/apache/atlas/web/rest/DiscoveryREST.java   |  9 ++-
 .../web/rest/SearchDownloadFileValidator.java      | 71 ++++++++++++++++++++++
 7 files changed, 129 insertions(+), 11 deletions(-)

diff --git a/dashboard/src/views/Administrator/Audits/AuditResults.tsx 
b/dashboard/src/views/Administrator/Audits/AuditResults.tsx
index 984221b15..acde90ff3 100644
--- a/dashboard/src/views/Administrator/Audits/AuditResults.tsx
+++ b/dashboard/src/views/Administrator/Audits/AuditResults.tsx
@@ -232,7 +232,7 @@ const AuditResults = ({ componentProps, row }: any) => {
           button2Handler={undefined}
           maxWidth="lg"
         >
-          <AuditsTab auditResultGuid={currentPurgeResultObj} />
+          <AuditsTab auditResultGuid={currentPurgeResultObj} loading={false} />
         </CustomModal>
       )}
     </>
diff --git 
a/dashboard/src/views/Administrator/Audits/__tests__/AuditResults.test.tsx 
b/dashboard/src/views/Administrator/Audits/__tests__/AuditResults.test.tsx
index 0fc580d4d..1896838f7 100644
--- a/dashboard/src/views/Administrator/Audits/__tests__/AuditResults.test.tsx
+++ b/dashboard/src/views/Administrator/Audits/__tests__/AuditResults.test.tsx
@@ -112,8 +112,8 @@ jest.mock('@utils/Muiutils', () => ({
 
 jest.mock('@views/DetailPage/EntityDetailTabs/AuditsTab', () => ({
   __esModule: true,
-  default: ({ auditResultGuid }: any) => (
-    <div data-testid="audits-tab">AuditsTab - {auditResultGuid}</div>
+  default: ({ auditResultGuid, loading }: any) => (
+    <div data-testid="audits-tab" data-loading={loading}>AuditsTab - 
{auditResultGuid}</div>
   )
 }));
 
@@ -425,6 +425,7 @@ describe('AuditResults Component', () => {
 
       expect(screen.getByTestId('modal-title')).toHaveTextContent('Purged 
Entity Details: guid-1');
       expect(screen.getByTestId('audits-tab')).toBeInTheDocument();
+      expect(screen.getByTestId('audits-tab')).toHaveAttribute('data-loading', 
'false');
     });
 
     it('should open auto purge modal with correct title', async () => {
diff --git 
a/dashboard/src/views/DetailPage/EntityDetailTabs/AttributeProperties.tsx 
b/dashboard/src/views/DetailPage/EntityDetailTabs/AttributeProperties.tsx
index 0aaeb774e..bdf10246d 100644
--- a/dashboard/src/views/DetailPage/EntityDetailTabs/AttributeProperties.tsx
+++ b/dashboard/src/views/DetailPage/EntityDetailTabs/AttributeProperties.tsx
@@ -222,7 +222,7 @@ const AttributeProperties = ({
           </Stack>
         </AccordionSummary>
         <AccordionDetails>
-          {loading == undefined || loading || isEmpty(entityData) ? (
+          {loading === true || (!auditDetails && isEmpty(entityData)) ? (
             <>
               <SkeletonLoader count={3} animation="wave" />
             </>
diff --git 
a/dashboard/src/views/DetailPage/EntityDetailTabs/__tests__/AttributeProperties.test.tsx
 
b/dashboard/src/views/DetailPage/EntityDetailTabs/__tests__/AttributeProperties.test.tsx
index 2eb488a38..158b9b66f 100644
--- 
a/dashboard/src/views/DetailPage/EntityDetailTabs/__tests__/AttributeProperties.test.tsx
+++ 
b/dashboard/src/views/DetailPage/EntityDetailTabs/__tests__/AttributeProperties.test.tsx
@@ -325,7 +325,7 @@ describe('AttributeProperties', () => {
                        
expect(screen.getByTestId('skeleton-loader')).toBeInTheDocument();
                });
 
-               it('should render loading skeleton when loading is undefined', 
() => {
+               it('should render properties when loading is undefined and 
entityData is available', () => {
                        render(
                                <TestWrapper>
                                        <AttributeProperties
@@ -337,7 +337,7 @@ describe('AttributeProperties', () => {
                                </TestWrapper>
                        );
 
-                       
expect(screen.getByTestId('skeleton-loader')).toBeInTheDocument();
+                       
expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument();
                });
 
                it('should render loading skeleton when entityData is empty', 
() => {
@@ -364,6 +364,40 @@ describe('AttributeProperties', () => {
                        
expect(screen.getByTestId('skeleton-loader')).toBeInTheDocument();
                });
 
+               it('should not render skeleton in auditDetails mode when 
loading is undefined', () => {
+                       mockUseSelector.mockImplementation((selector: any) =>
+                               selector({ entity: { entityData: {} } })
+                       );
+                       render(
+                               <TestWrapper>
+                                       <AttributeProperties
+                                               entity={defaultMockEntity}
+                                               
referredEntities={defaultMockReferredEntities}
+                                               loading={undefined}
+                                               auditDetails={true}
+                                               propertiesName="Technical"
+                                       />
+                               </TestWrapper>
+                       );
+                       
expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument();
+                       expect(screen.getByText('Technical 
Properties')).toBeInTheDocument();
+               });
+
+               it('should render skeleton in auditDetails mode when loading is 
true', () => {
+                       render(
+                               <TestWrapper>
+                                       <AttributeProperties
+                                               entity={defaultMockEntity}
+                                               
referredEntities={defaultMockReferredEntities}
+                                               loading={true}
+                                               auditDetails={true}
+                                               propertiesName="Technical"
+                                       />
+                               </TestWrapper>
+                       );
+                       
expect(screen.getByTestId('skeleton-loader')).toBeInTheDocument();
+               });
+
                it('should render "No Record Found" when properties are empty', 
() => {
                        const emptyEntity = {
                                typeName: 'DataSet',
diff --git 
a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasRelationshipStoreV2.java
 
b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasRelationshipStoreV2.java
index 459fc1da8..de8b3b2a6 100644
--- 
a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasRelationshipStoreV2.java
+++ 
b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasRelationshipStoreV2.java
@@ -22,6 +22,7 @@ import org.apache.atlas.AtlasErrorCode;
 import org.apache.atlas.RequestContext;
 import org.apache.atlas.annotation.GraphTransaction;
 import org.apache.atlas.authorize.AtlasAuthorizationUtils;
+import org.apache.atlas.authorize.AtlasEntityAccessRequest;
 import org.apache.atlas.authorize.AtlasPrivilege;
 import org.apache.atlas.authorize.AtlasRelationshipAccessRequest;
 import org.apache.atlas.exception.AtlasBaseException;
@@ -214,6 +215,7 @@ public class AtlasRelationshipStoreV2 implements 
AtlasRelationshipStore {
         LOG.debug("==> getById({})", guid);
 
         AtlasEdge         edge = graphHelper.getEdgeForGUID(guid);
+        verifyRelationshipReadAccess(edge);
         AtlasRelationship ret  = 
entityRetriever.mapEdgeToAtlasRelationship(edge);
 
         LOG.debug("<== getById({}): {}", guid, ret);
@@ -227,6 +229,7 @@ public class AtlasRelationshipStoreV2 implements 
AtlasRelationshipStore {
         LOG.debug("==> getExtInfoById({})", guid);
 
         AtlasEdge                    edge = graphHelper.getEdgeForGUID(guid);
+        verifyRelationshipReadAccess(edge);
         AtlasRelationshipWithExtInfo ret  = 
entityRetriever.mapEdgeToAtlasRelationshipWithExtInfo(edge);
 
         LOG.debug("<== getExtInfoById({}): {}", guid, ret);
@@ -771,4 +774,14 @@ public class AtlasRelationshipStoreV2 implements 
AtlasRelationshipStore {
     private void createAndQueueTask(String taskType, AtlasEdge 
relationshipEdge, AtlasRelationship relationship) {
         deleteDelegate.getHandler().createAndQueueTask(taskType, 
relationshipEdge, relationship);
     }
+
+    private void verifyRelationshipReadAccess(AtlasEdge edge) throws 
AtlasBaseException {
+        AtlasEntityHeader end1Entity = 
entityRetriever.toAtlasEntityHeaderWithClassifications(edge.getOutVertex());
+        AtlasEntityHeader end2Entity = 
entityRetriever.toAtlasEntityHeaderWithClassifications(edge.getInVertex());
+
+        AtlasAuthorizationUtils.verifyAccess(new 
AtlasEntityAccessRequest(typeRegistry, AtlasPrivilege.ENTITY_READ, end1Entity),
+                "read relationship: end1 guid=", end1Entity.getGuid());
+        AtlasAuthorizationUtils.verifyAccess(new 
AtlasEntityAccessRequest(typeRegistry, AtlasPrivilege.ENTITY_READ, end2Entity),
+                "read relationship: end2 guid=", end2Entity.getGuid());
+    }
 }
diff --git a/webapp/src/main/java/org/apache/atlas/web/rest/DiscoveryREST.java 
b/webapp/src/main/java/org/apache/atlas/web/rest/DiscoveryREST.java
index e318a53b5..95253671e 100644
--- a/webapp/src/main/java/org/apache/atlas/web/rest/DiscoveryREST.java
+++ b/webapp/src/main/java/org/apache/atlas/web/rest/DiscoveryREST.java
@@ -412,17 +412,16 @@ public class DiscoveryREST {
     @Timed
     @Path("download/{filename}")
     @Produces(MediaType.APPLICATION_OCTET_STREAM)
-    public Response downloadSearchResultFile(@PathParam("filename") String 
fileName) {
+    public Response downloadSearchResultFile(@PathParam("filename") String 
fileName) throws AtlasBaseException {
         File dir     = new File(SearchResultDownloadTask.DOWNLOAD_DIR_PATH, 
RequestContext.getCurrentUser());
-        File csvFile = new File(dir, fileName);
+        File csvFile = 
SearchDownloadFileValidator.resolveDownloadFile(fileName, dir);
 
-        if (!csvFile.exists()) {
+        if (!csvFile.exists() || !csvFile.isFile()) {
             return Response.noContent().build();
         }
 
         Response.ResponseBuilder response = Response.ok(csvFile);
-
-        response.header("Content-Disposition", "attachment; filename=\"" + 
fileName + "\"");
+        response.header("Content-Disposition", "attachment; filename=\"" + 
csvFile.getName() + "\"");
 
         return response.build();
     }
diff --git 
a/webapp/src/main/java/org/apache/atlas/web/rest/SearchDownloadFileValidator.java
 
b/webapp/src/main/java/org/apache/atlas/web/rest/SearchDownloadFileValidator.java
new file mode 100644
index 000000000..2f3f53ebc
--- /dev/null
+++ 
b/webapp/src/main/java/org/apache/atlas/web/rest/SearchDownloadFileValidator.java
@@ -0,0 +1,71 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.atlas.web.rest;
+
+import org.apache.atlas.AtlasErrorCode;
+import org.apache.atlas.exception.AtlasBaseException;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+import static 
org.apache.atlas.repository.store.graph.v2.tasks.searchdownload.SearchResultDownloadTask.CSV_FILE_EXTENSION;
+
+final class SearchDownloadFileValidator {
+    static final String INVALID_DOWNLOAD_FILE_NAME_MSG = "Invalid download 
file name";
+
+    private SearchDownloadFileValidator() {
+    }
+
+    static File resolveDownloadFile(String fileName, File userDownloadDir) 
throws AtlasBaseException {
+        validateFileName(fileName);
+
+        File csvFile = new File(userDownloadDir, fileName);
+
+        try {
+            String userDirPath = userDownloadDir.getCanonicalPath();
+            String filePath    = csvFile.getCanonicalPath();
+
+            if (!filePath.startsWith(userDirPath + File.separator)) {
+                throw new AtlasBaseException(AtlasErrorCode.BAD_REQUEST, 
INVALID_DOWNLOAD_FILE_NAME_MSG);
+            }
+        } catch (IOException e) {
+            throw new AtlasBaseException(AtlasErrorCode.BAD_REQUEST, 
INVALID_DOWNLOAD_FILE_NAME_MSG);
+        }
+
+        return csvFile;
+    }
+
+    private static void validateFileName(String fileName) throws 
AtlasBaseException {
+        if (StringUtils.isBlank(fileName)) {
+            throw new AtlasBaseException(AtlasErrorCode.BAD_REQUEST, 
INVALID_DOWNLOAD_FILE_NAME_MSG);
+        }
+
+        if (fileName.indexOf('/') >= 0 || fileName.indexOf('\\') >= 0 || 
fileName.contains("..")) {
+            throw new AtlasBaseException(AtlasErrorCode.BAD_REQUEST, 
INVALID_DOWNLOAD_FILE_NAME_MSG);
+        }
+
+        if (!fileName.endsWith(CSV_FILE_EXTENSION)) {
+            throw new AtlasBaseException(AtlasErrorCode.BAD_REQUEST, 
INVALID_DOWNLOAD_FILE_NAME_MSG);
+        }
+
+        if (fileName.indexOf("_BASIC_") <= 0 && fileName.indexOf("_DSL_") <= 
0) {
+            throw new AtlasBaseException(AtlasErrorCode.BAD_REQUEST, 
INVALID_DOWNLOAD_FILE_NAME_MSG);
+        }
+    }
+}

Reply via email to