feat(access): Improve external role retrieval (#10160)

Co-authored-by: Chris Collins <chriscollins3456@gmail.com>
This commit is contained in:
Filipe Caetano - OVO 2024-04-10 21:11:11 +01:00 committed by GitHub
parent 98c0bf6499
commit 2624e1970c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 170 additions and 251 deletions

View File

@ -130,6 +130,7 @@ import com.linkedin.datahub.graphql.resolvers.dataproduct.ListDataProductAssetsR
import com.linkedin.datahub.graphql.resolvers.dataproduct.UpdateDataProductResolver;
import com.linkedin.datahub.graphql.resolvers.dataset.DatasetStatsSummaryResolver;
import com.linkedin.datahub.graphql.resolvers.dataset.DatasetUsageStatsResolver;
import com.linkedin.datahub.graphql.resolvers.dataset.IsAssignedToMeResolver;
import com.linkedin.datahub.graphql.resolvers.deprecation.UpdateDeprecationResolver;
import com.linkedin.datahub.graphql.resolvers.domain.CreateDomainResolver;
import com.linkedin.datahub.graphql.resolvers.domain.DeleteDomainResolver;
@ -389,7 +390,7 @@ import org.dataloader.DataLoader;
import org.dataloader.DataLoaderOptions;
/**
* A {@link GraphQLEngine} configured to provide access to the entities and aspects on the the GMS
* A {@link GraphQLEngine} configured to provide access to the entities and aspects on the GMS
* graph.
*/
@Slf4j
@ -716,7 +717,7 @@ public class GmsGraphQLEngine {
configureVersionedDatasetResolvers(builder);
configureAccessAccessTokenMetadataResolvers(builder);
configureTestResultResolvers(builder);
configureRoleResolvers(builder);
configureDataHubRoleResolvers(builder);
configureSchemaFieldResolvers(builder);
configureERModelRelationshipResolvers(builder);
configureEntityPathResolvers(builder);
@ -729,6 +730,7 @@ public class GmsGraphQLEngine {
configureFormResolvers(builder);
configureIncidentResolvers(builder);
configureRestrictedResolvers(builder);
configureRoleResolvers(builder);
}
private void configureOrganisationRoleResolvers(RuntimeWiring.Builder builder) {
@ -2690,7 +2692,7 @@ public class GmsGraphQLEngine {
})));
}
private void configureRoleResolvers(final RuntimeWiring.Builder builder) {
private void configureDataHubRoleResolvers(final RuntimeWiring.Builder builder) {
builder.type(
"DataHubRole",
typeWiring ->
@ -2925,4 +2927,10 @@ public class GmsGraphQLEngine {
siblingGraphService, restrictedService, this.authorizationConfiguration))
.dataFetcher("relationships", new EntityRelationshipsResultResolver(graphClient)));
}
private void configureRoleResolvers(final RuntimeWiring.Builder builder) {
builder.type(
"Role",
typeWiring -> typeWiring.dataFetcher("isAssignedToMe", new IsAssignedToMeResolver()));
}
}

View File

@ -0,0 +1,40 @@
package com.linkedin.datahub.graphql.resolvers.dataset;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.generated.CorpUser;
import com.linkedin.datahub.graphql.generated.Role;
import com.linkedin.datahub.graphql.generated.RoleUser;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class IsAssignedToMeResolver implements DataFetcher<CompletableFuture<Boolean>> {
@Override
public CompletableFuture<Boolean> get(final DataFetchingEnvironment environment)
throws Exception {
final QueryContext context = environment.getContext();
final Role role = environment.getSource();
return CompletableFuture.supplyAsync(
() -> {
try {
final Set<String> assignedUserUrns =
role.getActors() != null && role.getActors().getUsers() != null
? role.getActors().getUsers().stream()
.map(RoleUser::getUser)
.map(CorpUser::getUrn)
.collect(Collectors.toSet())
: Collections.emptySet();
return assignedUserUrns.contains(context.getActorUrn());
} catch (Exception e) {
throw new RuntimeException(
"Failed to determine if current user is assigned to Role", e);
}
});
}
}

View File

@ -1720,6 +1720,7 @@ type Role implements Entity {
"""
actors: Actor
isAssignedToMe: Boolean!
}

View File

@ -0,0 +1,81 @@
package com.linkedin.datahub.graphql.resolvers.role;
import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.generated.*;
import com.linkedin.datahub.graphql.resolvers.dataset.IsAssignedToMeResolver;
import graphql.schema.DataFetchingEnvironment;
import java.util.ArrayList;
import org.mockito.Mockito;
import org.testng.annotations.Test;
public class IsAssignedToMeResolverTest {
private static final Urn TEST_CORP_USER_URN_1 = UrnUtils.getUrn("urn:li:corpuser:test-user-1");
private static final Urn TEST_CORP_USER_URN_2 = UrnUtils.getUrn("urn:li:corpuser:test-user-2");
private static final Urn TEST_CORP_USER_URN_3 = UrnUtils.getUrn("urn:li:corpuser:test-user-3");
@Test
public void testReturnsTrueIfCurrentUserIsAssignedToRole() throws Exception {
CorpUser corpUser1 = new CorpUser();
corpUser1.setUrn(TEST_CORP_USER_URN_1.toString());
CorpUser corpUser2 = new CorpUser();
corpUser2.setUrn(TEST_CORP_USER_URN_2.toString());
CorpUser corpUser3 = new CorpUser();
corpUser3.setUrn(TEST_CORP_USER_URN_3.toString());
ArrayList<RoleUser> roleUsers = new ArrayList<>();
roleUsers.add(new RoleUser(corpUser1));
roleUsers.add(new RoleUser(corpUser2));
roleUsers.add(new RoleUser(corpUser3));
Actor actor = new Actor();
actor.setUsers(roleUsers);
Role role = new Role();
role.setUrn("urn:li:role:fake-role");
role.setActors(actor);
QueryContext mockContext = getMockAllowContext(TEST_CORP_USER_URN_1.toString());
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
Mockito.when(mockEnv.getSource()).thenReturn(role);
IsAssignedToMeResolver resolver = new IsAssignedToMeResolver();
assertTrue(resolver.get(mockEnv).get());
}
@Test
public void testReturnsFalseIfCurrentUserIsNotAssignedToRole() throws Exception {
CorpUser corpUser1 = new CorpUser();
corpUser1.setUrn(TEST_CORP_USER_URN_1.toString());
CorpUser corpUser2 = new CorpUser();
corpUser2.setUrn(TEST_CORP_USER_URN_2.toString());
CorpUser corpUser3 = new CorpUser();
corpUser3.setUrn(TEST_CORP_USER_URN_3.toString());
ArrayList<RoleUser> roleUsers = new ArrayList<>();
roleUsers.add(new RoleUser(corpUser2));
roleUsers.add(new RoleUser(corpUser3));
Actor actor = new Actor();
actor.setUsers(roleUsers);
Role role = new Role();
role.setUrn("urn:li:role:fake-role");
role.setActors(actor);
QueryContext mockContext = getMockAllowContext(TEST_CORP_USER_URN_1.toString());
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
Mockito.when(mockEnv.getSource()).thenReturn(role);
IsAssignedToMeResolver resolver = new IsAssignedToMeResolver();
assertFalse(resolver.get(mockEnv).get());
}
}

View File

@ -5,7 +5,6 @@ import { SpinProps } from 'antd/es/spin';
import { LoadingOutlined } from '@ant-design/icons';
import { useBaseEntity } from '../../../EntityContext';
import { GetDatasetQuery, useGetExternalRolesQuery } from '../../../../../../graphql/dataset.generated';
import { useGetMeQuery } from '../../../../../../graphql/me.generated';
import { handleAccessRoles } from './utils';
import AccessManagerDescription from './AccessManagerDescription';
@ -59,10 +58,10 @@ const AccessButton = styled(Button)`
`;
export default function AccessManagement() {
const { data: loggedInUser } = useGetMeQuery({ fetchPolicy: 'cache-first' });
const baseEntity = useBaseEntity<GetDatasetQuery>();
const { data: externalRoles, loading: isLoading } = useGetExternalRolesQuery({
variables: { urn: baseEntity?.dataset?.urn as string },
variables: { urn: baseEntity?.dataset?.urn as string, },
skip: !baseEntity?.dataset?.urn,
});
@ -114,7 +113,7 @@ export default function AccessManagement() {
return (
<StyledTable
loading={isLoading ? spinProps : false}
dataSource={handleAccessRoles(externalRoles, loggedInUser)}
dataSource={handleAccessRoles(externalRoles)}
columns={columns} pagination={false}
/>
)

View File

@ -1,27 +1,38 @@
import { handleAccessRoles } from '../utils';
import { GetExternalRolesQuery } from '../../../../../../../graphql/dataset.generated';
import { GetMeQuery } from '../../../../../../../graphql/me.generated';
describe('handleAccessRoles', () => {
it('should properly map the externalroles and loggedin user', () => {
it('should properly map the externalroles', () => {
const externalRolesQuery: GetExternalRolesQuery = {
dataset: {
access: {
roles: [
{
role: {
id: 'accessRole',
id: 'test-role-1',
properties: {
name: 'accessRole',
name: 'Test Role 1',
description:
'This role access is required by the developers to test and deploy the code also adding few more details to check the description length for the given data and hence check the condition of read more and read less ',
type: 'READ',
requestUrl: 'https://www.google.com/',
requestUrl: 'https://www.google.com/role-1',
},
urn: 'urn:li:role:accessRole',
actors: {
users: null,
urn: 'urn:li:role:test-role-1',
isAssignedToMe: true
},
},
{
role: {
id: 'test-role-2',
properties: {
name: 'Test Role 2',
description:
'This role access is required by the developers to test and deploy the code also adding few more details to check the description length for the given data and hence check the condition of read more and read less ',
type: 'READ',
requestUrl: 'https://www.google.com/role-2',
},
urn: 'urn:li:role:test-role-2',
isAssignedToMe: false
},
},
],
@ -30,72 +41,23 @@ describe('handleAccessRoles', () => {
},
};
const GetMeQueryUser: GetMeQuery = {
me: {
corpUser: {
urn: 'urn:li:corpuser:datahub',
username: 'datahub',
info: {
active: true,
displayName: 'DataHub',
title: 'DataHub Root User',
firstName: null,
lastName: null,
fullName: null,
email: null,
__typename: 'CorpUserInfo',
},
editableProperties: {
displayName: null,
title: null,
pictureLink:
'https://raw.githubusercontent.com/datahub-project/datahub/master/datahub-web-react/src/images/default_avatar.png',
teams: [],
skills: [],
__typename: 'CorpUserEditableProperties',
},
settings: {
appearance: {
showSimplifiedHomepage: false,
__typename: 'CorpUserAppearanceSettings',
},
views: null,
__typename: 'CorpUserSettings',
},
__typename: 'CorpUser',
},
platformPrivileges: {
viewAnalytics: true,
managePolicies: true,
manageIdentities: true,
generatePersonalAccessTokens: true,
manageIngestion: true,
manageSecrets: true,
manageDomains: true,
manageTests: true,
manageGlossaries: true,
manageUserCredentials: true,
manageTags: true,
createDomains: true,
createTags: true,
manageGlobalViews: true,
manageOwnershipTypes: true,
manageGlobalAnnouncements: true,
manageTokens: true,
__typename: 'PlatformPrivileges',
},
__typename: 'AuthenticatedUser',
},
};
const externalRole = handleAccessRoles(externalRolesQuery, GetMeQueryUser);
const externalRole = handleAccessRoles(externalRolesQuery);
expect(externalRole).toMatchObject([
{
name: 'accessRole',
name: 'Test Role 1',
description:
'This role access is required by the developers to test and deploy the code also adding few more details to check the description length for the given data and hence check the condition of read more and read less ',
accessType: 'READ',
hasAccess: true,
url: 'https://www.google.com/role-1',
},
{
name: 'Test Role 2',
description:
'This role access is required by the developers to test and deploy the code also adding few more details to check the description length for the given data and hence check the condition of read more and read less ',
accessType: 'READ',
hasAccess: false,
url: 'https://www.google.com/',
url: 'https://www.google.com/role-2',
},
]);
});
@ -107,167 +69,7 @@ describe('handleAccessRoles', () => {
},
};
const GetMeQueryUser: GetMeQuery = {
me: {
corpUser: {
urn: 'urn:li:corpuser:datahub',
username: 'datahub',
info: {
active: true,
displayName: 'DataHub',
title: 'DataHub Root User',
firstName: null,
lastName: null,
fullName: null,
email: null,
__typename: 'CorpUserInfo',
},
editableProperties: {
displayName: null,
title: null,
pictureLink:
'https://raw.githubusercontent.com/datahub-project/datahub/master/datahub-web-react/src/images/default_avatar.png',
teams: [],
skills: [],
__typename: 'CorpUserEditableProperties',
},
settings: {
appearance: {
showSimplifiedHomepage: false,
__typename: 'CorpUserAppearanceSettings',
},
views: null,
__typename: 'CorpUserSettings',
},
__typename: 'CorpUser',
},
platformPrivileges: {
viewAnalytics: true,
managePolicies: true,
manageIdentities: true,
generatePersonalAccessTokens: true,
manageIngestion: true,
manageSecrets: true,
manageDomains: true,
manageTests: true,
manageGlossaries: true,
manageUserCredentials: true,
manageTags: true,
createDomains: true,
createTags: true,
manageGlobalViews: true,
manageOwnershipTypes: true,
manageGlobalAnnouncements: true,
manageTokens: true,
__typename: 'PlatformPrivileges',
},
__typename: 'AuthenticatedUser',
},
};
const externalRole = handleAccessRoles(externalRolesQuery, GetMeQueryUser);
const externalRole = handleAccessRoles(externalRolesQuery);
expect(externalRole).toMatchObject([]);
});
it('should properly map the externalroles and loggedin user and access true', () => {
const externalRolesQuery: GetExternalRolesQuery = {
dataset: {
access: {
roles: [
{
role: {
id: 'accessRole',
properties: {
name: 'accessRole',
description:
'This role access is required by the developers to test and deploy the code also adding few more details to check the description length for the given data and hence check the condition of read more and read less ',
type: 'READ',
requestUrl: 'https://www.google.com/',
},
urn: 'urn:li:role:accessRole',
actors: {
users: [
{
user: {
urn: 'urn:li:corpuser:datahub',
},
},
],
},
},
},
],
},
__typename: 'Dataset',
},
};
const GetMeQueryUser: GetMeQuery = {
me: {
corpUser: {
urn: 'urn:li:corpuser:datahub',
username: 'datahub',
info: {
active: true,
displayName: 'DataHub',
title: 'DataHub Root User',
firstName: null,
lastName: null,
fullName: null,
email: null,
__typename: 'CorpUserInfo',
},
editableProperties: {
displayName: null,
title: null,
pictureLink:
'https://raw.githubusercontent.com/datahub-project/datahub/master/datahub-web-react/src/images/default_avatar.png',
teams: [],
skills: [],
__typename: 'CorpUserEditableProperties',
},
settings: {
appearance: {
showSimplifiedHomepage: false,
__typename: 'CorpUserAppearanceSettings',
},
views: null,
__typename: 'CorpUserSettings',
},
__typename: 'CorpUser',
},
platformPrivileges: {
viewAnalytics: true,
managePolicies: true,
manageIdentities: true,
generatePersonalAccessTokens: true,
manageIngestion: true,
manageSecrets: true,
manageDomains: true,
manageTests: true,
manageGlossaries: true,
manageUserCredentials: true,
manageTags: true,
createDomains: true,
createTags: true,
manageGlobalViews: true,
manageOwnershipTypes: true,
manageGlobalAnnouncements: true,
manageTokens: true,
__typename: 'PlatformPrivileges',
},
__typename: 'AuthenticatedUser',
},
};
const externalRole = handleAccessRoles(externalRolesQuery, GetMeQueryUser);
expect(externalRole).toMatchObject([
{
name: 'accessRole',
description:
'This role access is required by the developers to test and deploy the code also adding few more details to check the description length for the given data and hence check the condition of read more and read less ',
accessType: 'READ',
hasAccess: true,
url: 'https://www.google.com/',
},
]);
});
});

View File

@ -1,4 +1,4 @@
export function handleAccessRoles(externalRoles, loggedInUser) {
export function handleAccessRoles(externalRoles) {
const accessRoles = new Array<any>();
if (
externalRoles?.dataset?.access &&
@ -10,13 +10,7 @@ export function handleAccessRoles(externalRoles, loggedInUser) {
name: userRoles?.role?.properties?.name || ' ',
description: userRoles?.role?.properties?.description || ' ',
accessType: userRoles?.role?.properties?.type || ' ',
hasAccess:
(userRoles?.role?.actors?.users &&
userRoles?.role?.actors?.users.length > 0 &&
userRoles?.role?.actors?.users?.some(
(user) => user.user.urn === loggedInUser?.me?.corpUser.urn,
)) ||
false,
hasAccess: userRoles?.role?.isAssignedToMe,
url: userRoles?.role?.properties?.requestUrl || undefined,
};
accessRoles.push(role);

View File

@ -330,13 +330,7 @@ fragment getRoles on Access {
requestUrl
}
urn
actors {
users {
user {
urn
}
}
}
isAssignedToMe
}
}
}