Skip to content

Commit

Permalink
feat(roles): add ability to invite users into a role (#6015)
Browse files Browse the repository at this point in the history
  • Loading branch information
aditya-radhakrishnan committed Sep 23, 2022
1 parent 3b1a0c5 commit 325b959
Show file tree
Hide file tree
Showing 44 changed files with 1,563 additions and 645 deletions.
4 changes: 2 additions & 2 deletions datahub-frontend/app/client/AuthServiceClient.java
Expand Up @@ -123,7 +123,7 @@ public boolean signUp(@Nonnull final String userUrn, @Nonnull final String fullN
String.format("%s://%s:%s/%s", protocol, this.metadataServiceHost, this.metadataServicePort,
SIGN_UP_ENDPOINT));

// Build JSON request to verify credentials for a native user.
// Build JSON request to sign up a native user.
final ObjectMapper objectMapper = new ObjectMapper();
final ObjectNode objectNode = objectMapper.createObjectNode();
objectNode.put(USER_URN_FIELD, userUrn);
Expand All @@ -150,7 +150,7 @@ public boolean signUp(@Nonnull final String userUrn, @Nonnull final String fullN
response.getEntity().toString()));
}
} catch (Exception e) {
throw new RuntimeException("Failed to create user", e);
throw new RuntimeException(String.format("Failed to create user %s", userUrn), e);
} finally {
try {
httpClient.close();
Expand Down
12 changes: 4 additions & 8 deletions datahub-frontend/app/controllers/AuthenticationController.java
Expand Up @@ -150,8 +150,7 @@ public Result logIn(Http.Request request) {
}

/**
* Sign up a native user based on a name, email, title, and password. The invite token must match the global invite
* token stored for the DataHub instance.
* Sign up a native user based on a name, email, title, and password. The invite token must match an existing invite token.
*
*/
@Nonnull
Expand Down Expand Up @@ -199,7 +198,7 @@ public Result signUp(Http.Request request) {

final Urn userUrn = new CorpuserUrn(email);
final String userUrnString = userUrn.toString();
boolean isNativeUserCreated = _authClient.signUp(userUrnString, fullName, email, title, password, inviteToken);
_authClient.signUp(userUrnString, fullName, email, title, password, inviteToken);
final String accessToken = _authClient.generateSessionTokenForUser(userUrn.getId());
return ok().withSession(createSessionMap(userUrnString, accessToken))
.withCookies(Http.Cookie.builder(ACTOR, userUrnString)
Expand All @@ -209,7 +208,7 @@ public Result signUp(Http.Request request) {
}

/**
* Create a native user based on a name, email, and password.
* Reset a native user's credentials based on a username, old password, and new password.
*
*/
@Nonnull
Expand Down Expand Up @@ -245,9 +244,7 @@ public Result resetNativeUserCredentials(Http.Request request) {

final Urn userUrn = new CorpuserUrn(email);
final String userUrnString = userUrn.toString();
boolean areNativeUserCredentialsReset =
_authClient.resetNativeUserCredentials(userUrnString, password, resetToken);
_logger.debug(String.format("Are native user credentials reset: %b", areNativeUserCredentialsReset));
_authClient.resetNativeUserCredentials(userUrnString, password, resetToken);
final String accessToken = _authClient.generateSessionTokenForUser(userUrn.getId());
return ok().withSession(createSessionMap(userUrnString, accessToken))
.withCookies(Http.Cookie.builder(ACTOR, userUrnString)
Expand Down Expand Up @@ -288,7 +285,6 @@ private String encodeRedirectUri(final String redirectUri) {
}

private boolean tryLogin(String username, String password) {
JsonNode invalidCredsJson = Json.newObject().put("message", "Invalid Credentials");
boolean loginSucceeded = false;

// First try jaas login, if enabled
Expand Down
Expand Up @@ -2,9 +2,11 @@

import com.datahub.authentication.AuthenticationConfiguration;
import com.datahub.authentication.group.GroupService;
import com.datahub.authentication.invite.InviteTokenService;
import com.datahub.authentication.token.StatefulTokenService;
import com.datahub.authentication.user.NativeUserService;
import com.datahub.authorization.AuthorizationConfiguration;
import com.datahub.authorization.role.RoleService;
import com.google.common.collect.ImmutableList;
import com.linkedin.common.VersionedUrn;
import com.linkedin.common.urn.Urn;
Expand Down Expand Up @@ -174,7 +176,10 @@
import com.linkedin.datahub.graphql.resolvers.policy.ListPoliciesResolver;
import com.linkedin.datahub.graphql.resolvers.policy.UpsertPolicyResolver;
import com.linkedin.datahub.graphql.resolvers.recommendation.ListRecommendationsResolver;
import com.linkedin.datahub.graphql.resolvers.role.AcceptRoleResolver;
import com.linkedin.datahub.graphql.resolvers.role.BatchAssignRoleResolver;
import com.linkedin.datahub.graphql.resolvers.role.CreateInviteTokenResolver;
import com.linkedin.datahub.graphql.resolvers.role.GetInviteTokenResolver;
import com.linkedin.datahub.graphql.resolvers.role.ListRolesResolver;
import com.linkedin.datahub.graphql.resolvers.search.AutoCompleteForMultipleResolver;
import com.linkedin.datahub.graphql.resolvers.search.AutoCompleteResolver;
Expand All @@ -197,9 +202,7 @@
import com.linkedin.datahub.graphql.resolvers.type.PlatformSchemaUnionTypeResolver;
import com.linkedin.datahub.graphql.resolvers.type.ResultsTypeResolver;
import com.linkedin.datahub.graphql.resolvers.type.TimeSeriesAspectInterfaceTypeResolver;
import com.linkedin.datahub.graphql.resolvers.user.CreateNativeUserInviteTokenResolver;
import com.linkedin.datahub.graphql.resolvers.user.CreateNativeUserResetTokenResolver;
import com.linkedin.datahub.graphql.resolvers.user.GetNativeUserInviteTokenResolver;
import com.linkedin.datahub.graphql.resolvers.user.ListUsersResolver;
import com.linkedin.datahub.graphql.resolvers.user.RemoveUserResolver;
import com.linkedin.datahub.graphql.resolvers.user.UpdateUserStatusResolver;
Expand Down Expand Up @@ -305,6 +308,8 @@ public class GmsGraphQLEngine {
private final TimelineService timelineService;
private final NativeUserService nativeUserService;
private final GroupService groupService;
private final RoleService roleService;
private final InviteTokenService inviteTokenService;

private final FeatureFlags featureFlags;

Expand Down Expand Up @@ -369,25 +374,19 @@ public class GmsGraphQLEngine {
*/
public final List<BrowsableEntityType<?, ?>> browsableTypes;

public GmsGraphQLEngine(
final EntityClient entityClient,
final GraphClient graphClient,
final UsageClient usageClient,
final AnalyticsService analyticsService,
final EntityService entityService,
final RecommendationsService recommendationsService,
final StatefulTokenService statefulTokenService,
final TimeseriesAspectService timeseriesAspectService,
final EntityRegistry entityRegistry,
final SecretService secretService,
final NativeUserService nativeUserService, final IngestionConfiguration ingestionConfiguration,
public GmsGraphQLEngine(final EntityClient entityClient, final GraphClient graphClient,
final UsageClient usageClient, final AnalyticsService analyticsService, final EntityService entityService,
final RecommendationsService recommendationsService, final StatefulTokenService statefulTokenService,
final TimeseriesAspectService timeseriesAspectService, final EntityRegistry entityRegistry,
final SecretService secretService, final NativeUserService nativeUserService,
final IngestionConfiguration ingestionConfiguration,
final AuthenticationConfiguration authenticationConfiguration,
final AuthorizationConfiguration authorizationConfiguration, final GitVersion gitVersion,
final TimelineService timelineService, final boolean supportsImpactAnalysis,
final VisualConfiguration visualConfiguration, final TelemetryConfiguration telemetryConfiguration,
final TestsConfiguration testsConfiguration, final DatahubConfiguration datahubConfiguration,
final SiblingGraphService siblingGraphService, final GroupService groupService,
final FeatureFlags featureFlags) {
final SiblingGraphService siblingGraphService, final GroupService groupService, final RoleService roleService,
final InviteTokenService inviteTokenService, final FeatureFlags featureFlags) {

this.entityClient = entityClient;
this.graphClient = graphClient;
Expand All @@ -406,6 +405,8 @@ public GmsGraphQLEngine(
this.timelineService = timelineService;
this.nativeUserService = nativeUserService;
this.groupService = groupService;
this.roleService = roleService;
this.inviteTokenService = inviteTokenService;

this.ingestionConfiguration = Objects.requireNonNull(ingestionConfiguration);
this.authenticationConfiguration = Objects.requireNonNull(authenticationConfiguration);
Expand Down Expand Up @@ -671,10 +672,10 @@ private void configureQueryResolvers(final RuntimeWiring.Builder builder) {
.dataFetcher("getRootGlossaryTerms", new GetRootGlossaryTermsResolver(this.entityClient))
.dataFetcher("getRootGlossaryNodes", new GetRootGlossaryNodesResolver(this.entityClient))
.dataFetcher("entityExists", new EntityExistsResolver(this.entityService))
.dataFetcher("getNativeUserInviteToken", new GetNativeUserInviteTokenResolver(this.nativeUserService))
.dataFetcher("entity", getEntityResolver())
.dataFetcher("entities", getEntitiesResolver())
.dataFetcher("listRoles", new ListRolesResolver(this.entityClient))
.dataFetcher("getInviteToken", new GetInviteTokenResolver(this.inviteTokenService))
);
}

Expand Down Expand Up @@ -790,12 +791,13 @@ private void configureMutationResolvers(final RuntimeWiring.Builder builder) {
.dataFetcher("updateName", new UpdateNameResolver(entityService))
.dataFetcher("addRelatedTerms", new AddRelatedTermsResolver(this.entityService))
.dataFetcher("removeRelatedTerms", new RemoveRelatedTermsResolver(this.entityService))
.dataFetcher("createNativeUserInviteToken", new CreateNativeUserInviteTokenResolver(this.nativeUserService))
.dataFetcher("createNativeUserResetToken", new CreateNativeUserResetTokenResolver(this.nativeUserService))
.dataFetcher("batchUpdateSoftDeleted", new BatchUpdateSoftDeletedResolver(this.entityService))
.dataFetcher("updateUserSetting", new UpdateUserSettingResolver(this.entityService))
.dataFetcher("rollbackIngestion", new RollbackIngestionResolver(this.entityClient))
.dataFetcher("batchAssignRole", new BatchAssignRoleResolver(this.entityClient))
.dataFetcher("batchAssignRole", new BatchAssignRoleResolver(this.roleService))
.dataFetcher("createInviteToken", new CreateInviteTokenResolver(this.inviteTokenService))
.dataFetcher("acceptRole", new AcceptRoleResolver(this.roleService, this.inviteTokenService))

);
}
Expand Down
@@ -0,0 +1,51 @@
package com.linkedin.datahub.graphql.resolvers.role;

import com.datahub.authentication.Authentication;
import com.datahub.authentication.invite.InviteTokenService;
import com.datahub.authorization.role.RoleService;
import com.linkedin.common.urn.Urn;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.generated.AcceptRoleInput;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.concurrent.CompletableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*;


@Slf4j

@RequiredArgsConstructor
public class AcceptRoleResolver implements DataFetcher<CompletableFuture<Boolean>> {
private final RoleService _roleService;
private final InviteTokenService _inviteTokenService;

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

final AcceptRoleInput input = bindArgument(environment.getArgument("input"), AcceptRoleInput.class);
final String inviteTokenStr = input.getInviteToken();
final Authentication authentication = context.getAuthentication();

return CompletableFuture.supplyAsync(() -> {
try {
Urn inviteTokenUrn = _inviteTokenService.getInviteTokenUrn(inviteTokenStr);
if (!_inviteTokenService.isInviteTokenValid(inviteTokenUrn, authentication)) {
throw new RuntimeException(String.format("Invite token %s is invalid", inviteTokenStr));
}

Urn roleUrn = _inviteTokenService.getInviteTokenRole(inviteTokenUrn, authentication);
if (roleUrn != null) {
_roleService.assignRoleToActor(authentication.getActor().toUrnStr(), roleUrn, authentication);
}

return true;
} catch (Exception e) {
throw new RuntimeException(String.format("Failed to accept role using invite token %s", inviteTokenStr), e);
}
});
}
}
@@ -1,34 +1,26 @@
package com.linkedin.datahub.graphql.resolvers.role;

import com.datahub.authentication.Authentication;
import com.linkedin.common.UrnArray;
import com.datahub.authorization.role.RoleService;
import com.linkedin.common.urn.Urn;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.BatchAssignRoleInput;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.events.metadata.ChangeType;
import com.linkedin.identity.RoleMembership;
import com.linkedin.metadata.utils.GenericRecordUtils;
import com.linkedin.mxe.MetadataChangeProposal;
import com.linkedin.r2.RemoteInvocationException;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.net.URISyntaxException;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import static com.linkedin.datahub.graphql.authorization.AuthorizationUtils.*;
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*;
import static com.linkedin.metadata.Constants.*;


@Slf4j
@RequiredArgsConstructor
public class BatchAssignRoleResolver implements DataFetcher<CompletableFuture<Boolean>> {
private final EntityClient _entityClient;
private final RoleService _roleService;

@Override
public CompletableFuture<Boolean> get(DataFetchingEnvironment environment) throws Exception {
Expand All @@ -46,13 +38,13 @@ public CompletableFuture<Boolean> get(DataFetchingEnvironment environment) throw
return CompletableFuture.supplyAsync(() -> {
try {
Urn roleUrn = Urn.createFromString(roleUrnStr);
if (!_entityClient.exists(roleUrn, authentication)) {
if (!_roleService.exists(roleUrn, authentication)) {
throw new RuntimeException(String.format("Role %s does not exist", roleUrnStr));
}

actors.forEach(actor -> {
try {
assignRoleToActor(actor, roleUrn, authentication);
_roleService.assignRoleToActor(actor, roleUrn, authentication);
} catch (Exception e) {
log.warn(
String.format("Failed to assign role %s to actor %s. Skipping actor assignment", roleUrnStr, actor), e);
Expand All @@ -64,26 +56,4 @@ public CompletableFuture<Boolean> get(DataFetchingEnvironment environment) throw
}
});
}

private void assignRoleToActor(String actor, Urn roleUrn, Authentication authentication)
throws URISyntaxException, RemoteInvocationException {
Urn actorUrn = Urn.createFromString(actor);
if (!_entityClient.exists(actorUrn, authentication)) {
log.warn(String.format("Failed to assign role %s to actor %s, actor does not exist. Skipping actor assignment",
roleUrn.toString(), actor));
return;
}

RoleMembership roleMembership = new RoleMembership();
roleMembership.setRoles(new UrnArray(roleUrn));

// Finally, create the MetadataChangeProposal.
final MetadataChangeProposal proposal = new MetadataChangeProposal();
proposal.setEntityUrn(actorUrn);
proposal.setEntityType(CORP_USER_ENTITY_NAME);
proposal.setAspectName(ROLE_MEMBERSHIP_ASPECT_NAME);
proposal.setAspect(GenericRecordUtils.serializeAspect(roleMembership));
proposal.setChangeType(ChangeType.UPSERT);
_entityClient.ingestProposal(proposal, authentication);
}
}
@@ -0,0 +1,44 @@
package com.linkedin.datahub.graphql.resolvers.role;

import com.datahub.authentication.Authentication;
import com.datahub.authentication.invite.InviteTokenService;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.CreateInviteTokenInput;
import com.linkedin.datahub.graphql.generated.InviteToken;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.concurrent.CompletableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import static com.linkedin.datahub.graphql.authorization.AuthorizationUtils.*;
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*;


@Slf4j
@RequiredArgsConstructor
public class CreateInviteTokenResolver implements DataFetcher<CompletableFuture<InviteToken>> {
private final InviteTokenService _inviteTokenService;

@Override
public CompletableFuture<InviteToken> get(final DataFetchingEnvironment environment) throws Exception {
final QueryContext context = environment.getContext();
if (!canManagePolicies(context)) {
throw new AuthorizationException(
"Unauthorized to create invite tokens. Please contact your DataHub administrator if this needs corrective action.");
}

final CreateInviteTokenInput input = bindArgument(environment.getArgument("input"), CreateInviteTokenInput.class);
final String roleUrnStr = input.getRoleUrn();
final Authentication authentication = context.getAuthentication();

return CompletableFuture.supplyAsync(() -> {
try {
return new InviteToken(_inviteTokenService.getInviteToken(roleUrnStr, true, authentication));
} catch (Exception e) {
throw new RuntimeException(String.format("Failed to create invite token for role %s", roleUrnStr), e);
}
});
}
}
@@ -0,0 +1,44 @@
package com.linkedin.datahub.graphql.resolvers.role;

import com.datahub.authentication.Authentication;
import com.datahub.authentication.invite.InviteTokenService;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.GetInviteTokenInput;
import com.linkedin.datahub.graphql.generated.InviteToken;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.concurrent.CompletableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import static com.linkedin.datahub.graphql.authorization.AuthorizationUtils.*;
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*;


@Slf4j
@RequiredArgsConstructor
public class GetInviteTokenResolver implements DataFetcher<CompletableFuture<InviteToken>> {
private final InviteTokenService _inviteTokenService;

@Override
public CompletableFuture<InviteToken> get(final DataFetchingEnvironment environment) throws Exception {
final QueryContext context = environment.getContext();
if (!canManagePolicies(context)) {
throw new AuthorizationException(
"Unauthorized to get invite tokens. Please contact your DataHub administrator if this needs corrective action.");
}

final GetInviteTokenInput input = bindArgument(environment.getArgument("input"), GetInviteTokenInput.class);
final String roleUrnStr = input.getRoleUrn();
final Authentication authentication = context.getAuthentication();

return CompletableFuture.supplyAsync(() -> {
try {
return new InviteToken(_inviteTokenService.getInviteToken(roleUrnStr, false, authentication));
} catch (Exception e) {
throw new RuntimeException(String.format("Failed to get invite token for role %s", roleUrnStr), e);
}
});
}
}

0 comments on commit 325b959

Please sign in to comment.