Skip to content

Commit

Permalink
Merge branch 'datahub-project:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
treff7es authored Apr 12, 2024
2 parents 48864e8 + 8ed87d6 commit bc9c782
Show file tree
Hide file tree
Showing 82 changed files with 1,933 additions and 536 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish-datahub-jars.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
- name: checkout upstream repo
run: |
git remote add upstream https://github.com/datahub-project/datahub.git
git fetch upstream --tags --force
git fetch upstream --tags --force --filter=tree:0
- name: publish datahub-client jar snapshot
if: ${{ github.event_name != 'release' }}
env:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
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;
Expand Down Expand Up @@ -389,7 +390,7 @@
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
Expand Down Expand Up @@ -716,7 +717,7 @@ public void configureRuntimeWiring(final RuntimeWiring.Builder builder) {
configureVersionedDatasetResolvers(builder);
configureAccessAccessTokenMetadataResolvers(builder);
configureTestResultResolvers(builder);
configureRoleResolvers(builder);
configureDataHubRoleResolvers(builder);
configureSchemaFieldResolvers(builder);
configureERModelRelationshipResolvers(builder);
configureEntityPathResolvers(builder);
Expand All @@ -729,6 +730,7 @@ public void configureRuntimeWiring(final RuntimeWiring.Builder builder) {
configureFormResolvers(builder);
configureIncidentResolvers(builder);
configureRestrictedResolvers(builder);
configureRoleResolvers(builder);
}

private void configureOrganisationRoleResolvers(RuntimeWiring.Builder builder) {
Expand Down Expand Up @@ -973,6 +975,7 @@ private void configureQueryResolvers(final RuntimeWiring.Builder builder) {
.dataFetcher(
"getAccessTokenMetadata",
new GetAccessTokenMetadataResolver(statefulTokenService, this.entityClient))
.dataFetcher("debugAccess", new DebugAccessResolver(this.entityClient, graphClient))
.dataFetcher("container", getResolver(containerType))
.dataFetcher("listDomains", new ListDomainsResolver(this.entityClient))
.dataFetcher("listSecrets", new ListSecretsResolver(this.entityClient))
Expand Down Expand Up @@ -2690,7 +2693,7 @@ private void configurePolicyResolvers(final RuntimeWiring.Builder builder) {
})));
}

private void configureRoleResolvers(final RuntimeWiring.Builder builder) {
private void configureDataHubRoleResolvers(final RuntimeWiring.Builder builder) {
builder.type(
"DataHubRole",
typeWiring ->
Expand Down Expand Up @@ -2925,4 +2928,10 @@ private void configureRestrictedResolvers(final RuntimeWiring.Builder builder) {
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()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,8 @@ public static <T> T restrictEntity(@Nonnull Object entity, Class<T> clazz) {
// they are a `one of` non-null.
// i.e. ChartProperties or ChartEditableProperties are required.
if (field.getAnnotation(javax.annotation.Nonnull.class) != null
|| field.getName().toLowerCase().contains("properties")) {
|| field.getName().toLowerCase().contains("properties")
|| field.getType().isPrimitive()) {
try {
switch (field.getName()) {
// pass through to the restricted entity
Expand All @@ -303,21 +304,32 @@ public static <T> T restrictEntity(@Nonnull Object entity, Class<T> clazz) {
return fieldGetter.invoke(entity, (Object[]) null);
default:
switch (field.getType().getSimpleName()) {
case "boolean":
case "Boolean":
Method boolGetter =
MethodUtils.getMatchingMethod(
entity.getClass(),
"get" + StringUtils.capitalise(field.getName()));
return boolGetter.invoke(entity, (Object[]) null);
return Boolean.TRUE.equals(
boolGetter.invoke(entity, (Object[]) null));
// mask these fields in the restricted entity
case "char":
case "String":
return "";
case "short":
case "Short":
case "int":
case "Integer":
return 0;
case "long":
case "Long":
return 0L;
case "float":
case "Float":
return 0F;
case "double":
case "Double":
return 0.0;
return 0D;
case "List":
return List.of();
default:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package com.linkedin.datahub.graphql.resolvers.auth;

import static com.linkedin.metadata.Constants.*;

import com.google.common.collect.ImmutableSet;
import com.linkedin.common.EntityRelationship;
import com.linkedin.common.EntityRelationships;
import com.linkedin.common.urn.Urn;
import com.linkedin.data.template.StringArray;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.DebugAccessResult;
import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.Constants;
import com.linkedin.metadata.graph.GraphClient;
import com.linkedin.metadata.query.filter.Condition;
import com.linkedin.metadata.query.filter.ConjunctiveCriterion;
import com.linkedin.metadata.query.filter.ConjunctiveCriterionArray;
import com.linkedin.metadata.query.filter.Criterion;
import com.linkedin.metadata.query.filter.CriterionArray;
import com.linkedin.metadata.query.filter.Filter;
import com.linkedin.metadata.query.filter.RelationshipDirection;
import com.linkedin.metadata.query.filter.SortCriterion;
import com.linkedin.metadata.query.filter.SortOrder;
import com.linkedin.metadata.search.SearchEntity;
import com.linkedin.policy.DataHubPolicyInfo;
import com.linkedin.r2.RemoteInvocationException;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

public class DebugAccessResolver implements DataFetcher<CompletableFuture<DebugAccessResult>> {

private static final String LAST_UPDATED_AT_FIELD = "lastUpdatedTimestamp";
private final EntityClient _entityClient;
private final GraphClient _graphClient;

public DebugAccessResolver(EntityClient entityClient, GraphClient graphClient) {
_entityClient = entityClient;
_graphClient = graphClient;
}

@Override
public CompletableFuture<DebugAccessResult> get(DataFetchingEnvironment environment)
throws Exception {
return CompletableFuture.supplyAsync(
() -> {
final QueryContext context = environment.getContext();

if (!AuthorizationUtils.canManageUsersAndGroups(context)) {
throw new AuthorizationException(
"Unauthorized to perform this action. Please contact your DataHub administrator.");
}
final String userUrn = environment.getArgument("userUrn");

return populateDebugAccessResult(userUrn, context);
});
}

public DebugAccessResult populateDebugAccessResult(String userUrn, QueryContext context) {

try {
final String actorUrn = context.getActorUrn();
final DebugAccessResult result = new DebugAccessResult();
final List<String> types =
Arrays.asList("IsMemberOfRole", "IsMemberOfGroup", "IsMemberOfNativeGroup");
EntityRelationships entityRelationships = getEntityRelationships(userUrn, types, actorUrn);

List<String> roles =
getUrnsFromEntityRelationships(entityRelationships, Constants.DATAHUB_ROLE_ENTITY_NAME);

List<String> groups =
getUrnsFromEntityRelationships(entityRelationships, Constants.CORP_GROUP_ENTITY_NAME);
List<String> groupsWithRoles = new ArrayList<>();

Set<String> rolesViaGroups = new HashSet<>();
groups.forEach(
groupUrn -> {
EntityRelationships groupRelationships =
getEntityRelationships(groupUrn, List.of("IsMemberOfRole"), actorUrn);
List<String> rolesOfGroup =
getUrnsFromEntityRelationships(
groupRelationships, Constants.DATAHUB_ROLE_ENTITY_NAME);
if (rolesOfGroup.isEmpty()) {
return;
}
groupsWithRoles.add(groupUrn);
rolesViaGroups.addAll(rolesOfGroup);
});
Set<String> allRoles = new HashSet<>(roles);
allRoles.addAll(rolesViaGroups);

result.setRoles(roles);
result.setGroups(groups);
result.setGroupsWithRoles(groupsWithRoles);
result.setRolesViaGroups(new ArrayList<>(rolesViaGroups));
result.setAllRoles(new ArrayList<>(allRoles));

Set<Urn> policyUrns = getPoliciesFor(context, userUrn, groups, result.getAllRoles());

// List of Policy that apply to this user directly or indirectly.
result.setPolicies(policyUrns.stream().map(Urn::toString).collect(Collectors.toList()));

// List of privileges that this user has directly or indirectly.
result.setPrivileges(new ArrayList<>(getPrivileges(context, policyUrns)));

return result;
} catch (Exception e) {
throw new RuntimeException(e);
}
}

private Set<String> getPrivileges(final QueryContext context, Set<Urn> policyUrns) {
try {
final Map<Urn, EntityResponse> policies =
_entityClient.batchGetV2(
Constants.POLICY_ENTITY_NAME,
policyUrns,
ImmutableSet.of(Constants.DATAHUB_POLICY_INFO_ASPECT_NAME),
context.getAuthentication());

return policies.keySet().stream()
.filter(Objects::nonNull)
.filter(key -> policies.get(key) != null)
.filter(key -> policies.get(key).hasAspects())
.map(key -> policies.get(key).getAspects())
.filter(aspectMap -> aspectMap.containsKey(DATAHUB_POLICY_INFO_ASPECT_NAME))
.map(
aspectMap ->
new DataHubPolicyInfo(
aspectMap.get(DATAHUB_POLICY_INFO_ASPECT_NAME).getValue().data()))
.map(DataHubPolicyInfo::getPrivileges)
.flatMap(List::stream)
.collect(Collectors.toSet());
} catch (URISyntaxException | RemoteInvocationException e) {
throw new RuntimeException("Failed to retrieve privileges from GMS", e);
}
}

private List<String> getUrnsFromEntityRelationships(
EntityRelationships entityRelationships, String entityName) {
return entityRelationships.getRelationships().stream()
.map(EntityRelationship::getEntity)
.filter(entity -> entityName.equals(entity.getEntityType()))
.map(Urn::toString)
.distinct()
.collect(Collectors.toList());
}

private EntityRelationships getEntityRelationships(
final String urn, final List<String> types, final String actor) {
return _graphClient.getRelatedEntities(
urn, types, RelationshipDirection.OUTGOING, 0, 100, actor);
}

private Set<Urn> getPoliciesFor(
final QueryContext context,
final String user,
final List<String> groups,
final List<String> roles)
throws RemoteInvocationException {
final SortCriterion sortCriterion =
new SortCriterion().setField(LAST_UPDATED_AT_FIELD).setOrder(SortOrder.DESCENDING);
return _entityClient
.search(
context.getOperationContext().withSearchFlags(flags -> flags.setFulltext(true)),
Constants.POLICY_ENTITY_NAME,
"",
buildFilterToGetPolicies(user, groups, roles),
sortCriterion,
0,
10000)
.getEntities()
.stream()
.map(SearchEntity::getEntity)
.collect(Collectors.toSet());
}

private Filter buildFilterToGetPolicies(
final String user, final List<String> groups, final List<String> roles) {

// setOr(array(andArray(user), andArray(groups), andArray(roles), andArray(allUsers),
// andArray(allGroups))
ConjunctiveCriterionArray conjunctiveCriteria = new ConjunctiveCriterionArray();

final CriterionArray allUsersAndArray = new CriterionArray();
allUsersAndArray.add(
new Criterion().setField("allUsers").setValue("true").setCondition(Condition.EQUAL));
conjunctiveCriteria.add(new ConjunctiveCriterion().setAnd(allUsersAndArray));

final CriterionArray allGroupsAndArray = new CriterionArray();
allGroupsAndArray.add(
new Criterion().setField("allGroups").setValue("true").setCondition(Condition.EQUAL));
conjunctiveCriteria.add(new ConjunctiveCriterion().setAnd(allGroupsAndArray));

if (user != null && !user.isEmpty()) {
final CriterionArray userAndArray = new CriterionArray();
userAndArray.add(
new Criterion().setField("users").setValue(user).setCondition(Condition.EQUAL));
conjunctiveCriteria.add(new ConjunctiveCriterion().setAnd(userAndArray));
}

if (groups != null && !groups.isEmpty()) {
final CriterionArray groupsAndArray = new CriterionArray();
groupsAndArray.add(
new Criterion()
.setField("groups")
.setValue("")
.setValues(new StringArray(groups))
.setCondition(Condition.EQUAL));
conjunctiveCriteria.add(new ConjunctiveCriterion().setAnd(groupsAndArray));
}

if (roles != null && !roles.isEmpty()) {
final CriterionArray rolesAndArray = new CriterionArray();
rolesAndArray.add(
new Criterion()
.setField("roles")
.setValue("")
.setValues(new StringArray(roles))
.setCondition(Condition.EQUAL));
conjunctiveCriteria.add(new ConjunctiveCriterion().setAnd(rolesAndArray));
}
return new Filter().setOr(conjunctiveCriteria);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
});
}
}
Loading

0 comments on commit bc9c782

Please sign in to comment.