Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ public class AtlasSimpleAuthorizerTest {
private static final String USER_FINANCE_PII = "financePII";
private static final String USER_IN_ADMIN_GROUP = "admin-group-user";
private static final String USER_IN_UNKNOWN_GROUP = "unknown-group-user";
private static final String USER_REL_READER = "relreader";
private static final String USER_RANGER_TAG_SYNC = "rangertagsync";

private static final String SALES_FACT_TABLE_QN = "sales_fact@cl1";
private static final String SALES_FACT_SALES_COLUMN_QN = "Sales.sales_fact.sales@cl1";
private static final String SALES_FACT_TIME_COLUMN_QN = "Sales.sales_fact.time_id@cl1";

private static final Map<String, Set<String>> USER_GROUPS = new HashMap<>();
private static final List<AtlasPrivilege> ENTITY_PRIVILEGES = new ArrayList<>();
Expand Down Expand Up @@ -336,6 +342,92 @@ public void testBusinessMetadata() {
}
}

@Test
public void testRelationshipRead_AllowsWhenWildcardEntityIdsPermitBothEnds() {
assertRelationshipReadAllowed(USER_RANGER_TAG_SYNC, SALES_FACT_TABLE_QN, SALES_FACT_SALES_COLUMN_QN);
}

@Test
public void testRelationshipRead_AllowsWhenPartialEntityIdsPermitBothEnds() {
assertRelationshipReadAllowed(USER_REL_READER, SALES_FACT_TABLE_QN, SALES_FACT_SALES_COLUMN_QN);
}

@Test
public void testRelationshipRead_DeniesWhenPartialEntityIdsPermitOnlyTableEnd() {
assertEntityReadAllowed(USER_REL_READER, "Table", SALES_FACT_TABLE_QN);
assertEntityReadDenied(USER_REL_READER, "Column", SALES_FACT_TIME_COLUMN_QN);
assertRelationshipReadDenied(USER_REL_READER, SALES_FACT_TABLE_QN, SALES_FACT_TIME_COLUMN_QN);
}

@Test
public void testRelationshipRead_DeniesWhenPartialEntityIdsPermitOnlyColumnEnd() {
assertEntityReadDenied(USER_REL_READER, "Table", "other_table@cl1");
assertEntityReadAllowed(USER_REL_READER, "Column", SALES_FACT_SALES_COLUMN_QN);
assertRelationshipReadDenied(USER_REL_READER, "other_table@cl1", SALES_FACT_SALES_COLUMN_QN);
}

@Test
public void testRelationshipRead_DeniesWhenUserHasNoEntityRead() {
assertEntityReadDenied(USER_IN_UNKNOWN_GROUP, "Table", SALES_FACT_TABLE_QN);
assertEntityReadDenied(USER_IN_UNKNOWN_GROUP, "Column", SALES_FACT_SALES_COLUMN_QN);
assertRelationshipReadDenied(USER_IN_UNKNOWN_GROUP, SALES_FACT_TABLE_QN, SALES_FACT_SALES_COLUMN_QN);
}

private void assertRelationshipReadAllowed(String userName, String end1QualifiedName, String end2QualifiedName) {
assertEntityReadAllowed(userName, "Table", end1QualifiedName);
assertEntityReadAllowed(userName, "Column", end2QualifiedName);

AssertJUnit.assertTrue("user " + userName + " should be allowed relationship read on "
+ end1QualifiedName + " and " + end2QualifiedName,
isRelationshipReadAllowed(userName, end1QualifiedName, end2QualifiedName));
}

private void assertRelationshipReadDenied(String userName, String end1QualifiedName, String end2QualifiedName) {
AssertJUnit.assertFalse("user " + userName + " should be denied relationship read on "
+ end1QualifiedName + " and " + end2QualifiedName,
isRelationshipReadAllowed(userName, end1QualifiedName, end2QualifiedName));
}

private void assertEntityReadAllowed(String userName, String typeName, String qualifiedName) {
AssertJUnit.assertTrue("user " + userName + " should have entity-read on " + qualifiedName,
isEntityReadAllowed(userName, typeName, qualifiedName));
}

private void assertEntityReadDenied(String userName, String typeName, String qualifiedName) {
AssertJUnit.assertFalse("user " + userName + " should not have entity-read on " + qualifiedName,
isEntityReadAllowed(userName, typeName, qualifiedName));
}

private boolean isRelationshipReadAllowed(String userName, String end1QualifiedName, String end2QualifiedName) {
return isEntityReadAllowed(userName, "Table", end1QualifiedName)
&& isEntityReadAllowed(userName, "Column", end2QualifiedName);
}

private boolean isEntityReadAllowed(String userName, String typeName, String qualifiedName) {
try {
AtlasEntityAccessRequest request = new AtlasEntityAccessRequest(null, AtlasPrivilege.ENTITY_READ,
createEntityHeader(typeName, qualifiedName));

setUser(request, userName);

return authorizer.isAccessAllowed(request);
} catch (Exception e) {
LOG.error("Exception in AtlasSimpleAuthorizerTest", e);

AssertJUnit.fail();

return false;
}
}

private AtlasEntityHeader createEntityHeader(String typeName, String qualifiedName) {
Map<String, Object> attributes = new HashMap<>();

attributes.put("qualifiedName", qualifiedName);

return new AtlasEntityHeader(typeName, attributes);
}

private void setUser(AtlasAccessRequest request, String userName) {
Set<String> userGroups = USER_GROUPS.get(userName);

Expand All @@ -351,6 +443,8 @@ private void setUser(AtlasAccessRequest request, String userName) {
USER_GROUPS.put(USER_FINANCE_PII, Collections.singleton("FINANCE_PII"));
USER_GROUPS.put(USER_IN_ADMIN_GROUP, Collections.singleton("ROLE_ADMIN"));
USER_GROUPS.put(USER_IN_UNKNOWN_GROUP, Collections.singleton("UNKNOWN_GROUP"));
USER_GROUPS.put(USER_REL_READER, Collections.singleton("REL_PARTIAL_READ"));
USER_GROUPS.put(USER_RANGER_TAG_SYNC, Collections.singleton("RANGER_TAG_SYNC"));

ENTITY_PRIVILEGES.add(AtlasPrivilege.ENTITY_CREATE);
ENTITY_PRIVILEGES.add(AtlasPrivilege.ENTITY_UPDATE);
Expand Down
20 changes: 18 additions & 2 deletions authorization/src/test/resources/atlas-simple-authz-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,29 @@
"classifications": [ "PII.*" ]
}
]
},

"REL_PARTIAL_READ": {
"entityPermissions": [
{
"privileges": [ "entity-read", "entity-read-classification" ],
"entityTypes": [ ".*" ],
"entityIds": [ "sales_fact@cl1", "Sales.sales_fact.sales@cl1" ],
"entityClassifications": [ ".*" ],
"labels": [ ".*" ],
"businessMetadata": [ ".*" ],
"attributes": [ ".*" ]
}
]
}
},

"userRoles": {
"admin": [ "ROLE_ADMIN" ],
"rangertagsync": [ "DATA_SCIENTIST" ],
"dataScientist1": [ "DATA_SCIENTIST"],
"dataSteward1": [ "DATA_STEWARD"]
"dataSteward1": [ "DATA_STEWARD"],
"relreader": [ "REL_PARTIAL_READ" ]
},

"groupRoles": {
Expand All @@ -145,6 +160,7 @@
"FINANCE": [ "FINANCE" ],
"FINANCE_PII": [ "FINANCE_PII" ],
"RANGER_TAG_SYNC": [ "DATA_SCIENTIST" ],
"DATA_STEWARD_EX": [ "DATA_STEWARD_EX" ]
"DATA_STEWARD_EX": [ "DATA_STEWARD_EX" ],
"REL_PARTIAL_READ":[ "REL_PARTIAL_READ" ]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
/**
* 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.repository.store.graph.v2;

import org.apache.atlas.AtlasErrorCode;
import org.apache.atlas.authorize.AtlasAuthorizationUtils;
import org.apache.atlas.authorize.AtlasEntityAccessRequest;
import org.apache.atlas.authorize.AtlasPrivilege;
import org.apache.atlas.exception.AtlasBaseException;
import org.apache.atlas.model.instance.AtlasEntityHeader;
import org.apache.atlas.model.instance.AtlasRelationship;
import org.apache.atlas.model.instance.AtlasRelationship.AtlasRelationshipWithExtInfo;
import org.apache.atlas.repository.graph.GraphHelper;
import org.apache.atlas.repository.graphdb.AtlasEdge;
import org.apache.atlas.repository.graphdb.AtlasGraph;
import org.apache.atlas.repository.graphdb.AtlasVertex;
import org.apache.atlas.repository.store.graph.v1.DeleteHandlerDelegate;
import org.apache.atlas.type.AtlasTypeRegistry;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.MockitoAnnotations;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;

public class AtlasRelationshipStoreV2ReadAuthTest {
private static final String RELATIONSHIP_GUID = "relationship-guid-1";
private static final String END1_GUID = "end1-guid";
private static final String END2_GUID = "end2-guid";
private static final String END1_QUALIFIED_NAME = "sales_fact@cl1";
private static final String END2_QUALIFIED_NAME = "Sales.sales_fact.sales@cl1";

@Mock
private AtlasGraph graph;

@Mock
private AtlasTypeRegistry typeRegistry;

@Mock
private DeleteHandlerDelegate deleteDelegate;

@Mock
private IAtlasEntityChangeNotifier entityChangeNotifier;

@Mock
private GraphHelper graphHelper;

@Mock
private EntityGraphRetriever entityRetriever;

@Mock
private AtlasEdge edge;

@Mock
private AtlasVertex end1Vertex;

@Mock
private AtlasVertex end2Vertex;

private AtlasRelationshipStoreV2 relationshipStore;

private AutoCloseable mocks;

@BeforeMethod
public void setUp() throws Exception {
mocks = MockitoAnnotations.openMocks(this);

relationshipStore = new AtlasRelationshipStoreV2(graph, typeRegistry, deleteDelegate, entityChangeNotifier);

setField("graphHelper", graphHelper);
setField("entityRetriever", entityRetriever);

when(graphHelper.getEdgeForGUID(RELATIONSHIP_GUID)).thenReturn(edge);
when(edge.getOutVertex()).thenReturn(end1Vertex);
when(edge.getInVertex()).thenReturn(end2Vertex);

when(entityRetriever.toAtlasEntityHeaderWithClassifications(end1Vertex)).thenReturn(tableHeader(END1_QUALIFIED_NAME));
when(entityRetriever.toAtlasEntityHeaderWithClassifications(end2Vertex)).thenReturn(columnHeader(END2_QUALIFIED_NAME));
}

@AfterMethod
public void tearDown() throws Exception {
if (mocks != null) {
mocks.close();
}
}

@Test
public void testGetById_VerifiesEntityReadOnBothEnds() throws Exception {
AtlasRelationship expectedRelationship = new AtlasRelationship("hive_table_columns");

when(entityRetriever.mapEdgeToAtlasRelationship(edge)).thenReturn(expectedRelationship);

try (MockedStatic<AtlasAuthorizationUtils> authUtils = mockStatic(AtlasAuthorizationUtils.class)) {
authUtils.when(() -> AtlasAuthorizationUtils.verifyAccess(any(AtlasEntityAccessRequest.class), any(), any()))
.thenAnswer(invocation -> null);

AtlasRelationship actualRelationship = relationshipStore.getById(RELATIONSHIP_GUID);

assertEquals(actualRelationship, expectedRelationship);
authUtils.verify(() -> AtlasAuthorizationUtils.verifyAccess(any(AtlasEntityAccessRequest.class), any(), any()), times(2));
}
}

@Test
public void testGetExtInfoById_VerifiesEntityReadOnBothEnds() throws Exception {
AtlasRelationshipWithExtInfo expectedRelationship = new AtlasRelationshipWithExtInfo(new AtlasRelationship("hive_table_columns"));

when(entityRetriever.mapEdgeToAtlasRelationshipWithExtInfo(edge)).thenReturn(expectedRelationship);

try (MockedStatic<AtlasAuthorizationUtils> authUtils = mockStatic(AtlasAuthorizationUtils.class)) {
authUtils.when(() -> AtlasAuthorizationUtils.verifyAccess(any(AtlasEntityAccessRequest.class), any(), any()))
.thenAnswer(invocation -> null);

AtlasRelationshipWithExtInfo actualRelationship = relationshipStore.getExtInfoById(RELATIONSHIP_GUID);

assertEquals(actualRelationship, expectedRelationship);
authUtils.verify(() -> AtlasAuthorizationUtils.verifyAccess(any(AtlasEntityAccessRequest.class), any(), any()), times(2));
}
}

@Test(expectedExceptions = AtlasBaseException.class)
public void testGetById_DeniesWhenEnd1EntityReadFails() throws Exception {
when(entityRetriever.mapEdgeToAtlasRelationship(edge)).thenReturn(new AtlasRelationship("hive_table_columns"));

try (MockedStatic<AtlasAuthorizationUtils> authUtils = mockStatic(AtlasAuthorizationUtils.class)) {
authUtils.when(() -> AtlasAuthorizationUtils.verifyAccess(any(AtlasEntityAccessRequest.class), eq("read relationship: end1 guid="), eq(END1_GUID)))
.thenThrow(new AtlasBaseException(AtlasErrorCode.UNAUTHORIZED_ACCESS, END1_GUID, AtlasPrivilege.ENTITY_READ.getType()));

relationshipStore.getById(RELATIONSHIP_GUID);
}
}

@Test(expectedExceptions = AtlasBaseException.class)
public void testGetById_DeniesWhenEnd2EntityReadFails() throws Exception {
when(entityRetriever.mapEdgeToAtlasRelationship(edge)).thenReturn(new AtlasRelationship("hive_table_columns"));

try (MockedStatic<AtlasAuthorizationUtils> authUtils = mockStatic(AtlasAuthorizationUtils.class)) {
authUtils.when(() -> AtlasAuthorizationUtils.verifyAccess(any(AtlasEntityAccessRequest.class), eq("read relationship: end1 guid="), eq(END1_GUID)))
.thenAnswer(invocation -> null);
authUtils.when(() -> AtlasAuthorizationUtils.verifyAccess(any(AtlasEntityAccessRequest.class), eq("read relationship: end2 guid="), eq(END2_GUID)))
.thenThrow(new AtlasBaseException(AtlasErrorCode.UNAUTHORIZED_ACCESS, END2_GUID, AtlasPrivilege.ENTITY_READ.getType()));

relationshipStore.getById(RELATIONSHIP_GUID);
}
}

@Test
public void testGetById_PassesEndQualifiedNamesToEntityReadCheck() throws Exception {
when(entityRetriever.mapEdgeToAtlasRelationship(edge)).thenReturn(new AtlasRelationship("Table_Columns"));

ArgumentCaptor<AtlasEntityAccessRequest> requestCaptor = ArgumentCaptor.forClass(AtlasEntityAccessRequest.class);

try (MockedStatic<AtlasAuthorizationUtils> authUtils = mockStatic(AtlasAuthorizationUtils.class)) {
authUtils.when(() -> AtlasAuthorizationUtils.verifyAccess(requestCaptor.capture(), any(), any()))
.thenAnswer(invocation -> null);

relationshipStore.getById(RELATIONSHIP_GUID);

List<AtlasEntityAccessRequest> requests = requestCaptor.getAllValues();

assertEquals(requests.size(), 2);
assertEquals(requests.get(0).getEntityId(), END1_QUALIFIED_NAME);
assertEquals(requests.get(1).getEntityId(), END2_QUALIFIED_NAME);
assertEquals(requests.get(0).getAction(), AtlasPrivilege.ENTITY_READ);
assertEquals(requests.get(1).getAction(), AtlasPrivilege.ENTITY_READ);
}
}

@Test
public void testGetById_AllowsWhenBothEndQualifiedNamesPermitted() throws Exception {
when(entityRetriever.mapEdgeToAtlasRelationship(edge)).thenReturn(new AtlasRelationship("Table_Columns"));

try (MockedStatic<AtlasAuthorizationUtils> authUtils = mockStatic(AtlasAuthorizationUtils.class)) {
authUtils.when(() -> AtlasAuthorizationUtils.verifyAccess(any(AtlasEntityAccessRequest.class), any(), any()))
.thenAnswer(invocation -> {
AtlasEntityAccessRequest request = invocation.getArgument(0);
String entityId = request.getEntityId();

assertTrue(entityId.equals(END1_QUALIFIED_NAME) || entityId.equals(END2_QUALIFIED_NAME),
"unexpected entityId for relationship read: " + entityId);

return null;
});

AtlasRelationship actualRelationship = relationshipStore.getById(RELATIONSHIP_GUID);

assertEquals(actualRelationship.getTypeName(), "Table_Columns");
authUtils.verify(() -> AtlasAuthorizationUtils.verifyAccess(any(AtlasEntityAccessRequest.class), any(), any()), times(2));
}
}

private AtlasEntityHeader tableHeader(String qualifiedName) {
return createEntityHeader("Table", END1_GUID, qualifiedName);
}

private AtlasEntityHeader columnHeader(String qualifiedName) {
return createEntityHeader("Column", END2_GUID, qualifiedName);
}

private AtlasEntityHeader createEntityHeader(String typeName, String guid, String qualifiedName) {
Map<String, Object> attributes = new HashMap<>();

attributes.put("qualifiedName", qualifiedName);

return new AtlasEntityHeader(typeName, guid, attributes);
}

private void setField(String fieldName, Object value) throws Exception {
Field field = AtlasRelationshipStoreV2.class.getDeclaredField(fieldName);

field.setAccessible(true);
field.set(relationshipStore, value);
}
}
Loading
Loading