Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.openmetadata.service.resources.teams.UserResource Maven / Gradle / Ivy
/*
* Copyright 2021 Collate
* Licensed 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
* http://www.apache.org/licenses/LICENSE-2.0
* 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.openmetadata.service.resources.teams;
import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
import static javax.ws.rs.core.Response.Status.CONFLICT;
import static javax.ws.rs.core.Response.Status.FORBIDDEN;
import static javax.ws.rs.core.Response.Status.OK;
import static org.openmetadata.common.utils.CommonUtil.listOf;
import static org.openmetadata.schema.api.teams.CreateUser.CreatePasswordType.ADMIN_CREATE;
import static org.openmetadata.schema.auth.ChangePasswordRequest.RequestType.SELF;
import static org.openmetadata.schema.entity.teams.AuthenticationMechanism.AuthType.BASIC;
import static org.openmetadata.schema.entity.teams.AuthenticationMechanism.AuthType.JWT;
import static org.openmetadata.schema.type.Include.ALL;
import static org.openmetadata.service.exception.CatalogExceptionMessage.EMAIL_SENDING_ISSUE;
import static org.openmetadata.service.jdbi3.UserRepository.AUTH_MECHANISM_FIELD;
import static org.openmetadata.service.secrets.ExternalSecretsManager.NULL_SECRET_STRING;
import static org.openmetadata.service.security.jwt.JWTTokenGenerator.getExpiryDate;
import static org.openmetadata.service.util.EmailUtil.getSmtpSettings;
import static org.openmetadata.service.util.UserUtil.getRoleListFromUser;
import static org.openmetadata.service.util.UserUtil.getRolesFromAuthorizationToken;
import static org.openmetadata.service.util.UserUtil.getUser;
import static org.openmetadata.service.util.UserUtil.reSyncUserRolesFromToken;
import static org.openmetadata.service.util.UserUtil.validateAndGetRolesRef;
import at.favre.lib.crypto.bcrypt.BCrypt;
import freemarker.template.TemplateException;
import io.dropwizard.jersey.PATCH;
import io.dropwizard.jersey.errors.ErrorMessage;
import io.swagger.v3.oas.annotations.ExternalDocumentation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Base64;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.json.JsonObject;
import javax.json.JsonPatch;
import javax.json.JsonValue;
import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.Nullable;
import org.openmetadata.common.utils.CommonUtil;
import org.openmetadata.schema.EntityInterface;
import org.openmetadata.schema.TokenInterface;
import org.openmetadata.schema.api.data.RestoreEntity;
import org.openmetadata.schema.api.security.AuthenticationConfiguration;
import org.openmetadata.schema.api.security.AuthorizerConfiguration;
import org.openmetadata.schema.api.teams.CreateUser;
import org.openmetadata.schema.auth.BasicAuthMechanism;
import org.openmetadata.schema.auth.ChangePasswordRequest;
import org.openmetadata.schema.auth.CreatePersonalToken;
import org.openmetadata.schema.auth.EmailRequest;
import org.openmetadata.schema.auth.GenerateTokenRequest;
import org.openmetadata.schema.auth.JWTAuthMechanism;
import org.openmetadata.schema.auth.JWTTokenExpiry;
import org.openmetadata.schema.auth.LoginRequest;
import org.openmetadata.schema.auth.LogoutRequest;
import org.openmetadata.schema.auth.PasswordResetRequest;
import org.openmetadata.schema.auth.PersonalAccessToken;
import org.openmetadata.schema.auth.RegistrationRequest;
import org.openmetadata.schema.auth.RevokePersonalTokenRequest;
import org.openmetadata.schema.auth.RevokeTokenRequest;
import org.openmetadata.schema.auth.SSOAuthMechanism;
import org.openmetadata.schema.auth.ServiceTokenType;
import org.openmetadata.schema.auth.TokenRefreshRequest;
import org.openmetadata.schema.auth.TokenType;
import org.openmetadata.schema.email.SmtpSettings;
import org.openmetadata.schema.entity.teams.AuthenticationMechanism;
import org.openmetadata.schema.entity.teams.User;
import org.openmetadata.schema.services.connections.metadata.AuthProvider;
import org.openmetadata.schema.type.EntityHistory;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.MetadataOperation;
import org.openmetadata.schema.type.Relationship;
import org.openmetadata.schema.type.csv.CsvImportResult;
import org.openmetadata.service.Entity;
import org.openmetadata.service.OpenMetadataApplicationConfig;
import org.openmetadata.service.auth.JwtResponse;
import org.openmetadata.service.exception.CatalogExceptionMessage;
import org.openmetadata.service.exception.CustomExceptionMessage;
import org.openmetadata.service.exception.EntityNotFoundException;
import org.openmetadata.service.jdbi3.CollectionDAO;
import org.openmetadata.service.jdbi3.ListFilter;
import org.openmetadata.service.jdbi3.TokenRepository;
import org.openmetadata.service.jdbi3.UserRepository;
import org.openmetadata.service.jdbi3.UserRepository.UserCsv;
import org.openmetadata.service.limits.Limits;
import org.openmetadata.service.resources.Collection;
import org.openmetadata.service.resources.EntityResource;
import org.openmetadata.service.secrets.SecretsManager;
import org.openmetadata.service.secrets.SecretsManagerFactory;
import org.openmetadata.service.secrets.masker.EntityMaskerFactory;
import org.openmetadata.service.security.AuthorizationException;
import org.openmetadata.service.security.Authorizer;
import org.openmetadata.service.security.CatalogPrincipal;
import org.openmetadata.service.security.auth.AuthenticatorHandler;
import org.openmetadata.service.security.auth.BotTokenCache;
import org.openmetadata.service.security.auth.CatalogSecurityContext;
import org.openmetadata.service.security.auth.UserTokenCache;
import org.openmetadata.service.security.jwt.JWTTokenGenerator;
import org.openmetadata.service.security.mask.PIIMasker;
import org.openmetadata.service.security.policyevaluator.OperationContext;
import org.openmetadata.service.security.policyevaluator.ResourceContext;
import org.openmetadata.service.security.saml.JwtTokenCacheManager;
import org.openmetadata.service.util.EmailUtil;
import org.openmetadata.service.util.EntityUtil;
import org.openmetadata.service.util.EntityUtil.Fields;
import org.openmetadata.service.util.JsonUtils;
import org.openmetadata.service.util.PasswordUtil;
import org.openmetadata.service.util.RestUtil.PutResponse;
import org.openmetadata.service.util.ResultList;
import org.openmetadata.service.util.TokenUtil;
@Slf4j
@Path("/v1/users")
@Tag(
name = "Users",
description =
"A `User` represents a user of OpenMetadata. A user can be part of 0 or more teams. A special type of user called Bot is used for automation. A user can be an owner of zero or more data assets. A user can also follow zero or more data assets.")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Collection(
name = "users",
order = 3,
requiredForOps = true) // Initialize user resource before bot resource (at default order 9)
public class UserResource extends EntityResource {
public static final String COLLECTION_PATH = "v1/users/";
public static final String USER_PROTECTED_FIELDS = "authenticationMechanism";
private final JWTTokenGenerator jwtTokenGenerator;
private final TokenRepository tokenRepository;
private boolean isEmailServiceEnabled;
private AuthenticationConfiguration authenticationConfiguration;
private AuthorizerConfiguration authorizerConfiguration;
private final AuthenticatorHandler authHandler;
private boolean isSelfSignUpEnabled = false;
static final String FIELDS = "profile,roles,teams,follows,owns,domains,personas,defaultPersona";
@Override
public User addHref(UriInfo uriInfo, User user) {
super.addHref(uriInfo, user);
Entity.withHref(uriInfo, user.getTeams());
Entity.withHref(uriInfo, user.getRoles());
Entity.withHref(uriInfo, user.getPersonas());
Entity.withHref(uriInfo, user.getInheritedRoles());
Entity.withHref(uriInfo, user.getOwns());
Entity.withHref(uriInfo, user.getFollows());
return user;
}
private boolean isEmailServiceEnabled() {
return getSmtpSettings().getEnableSmtpServer();
}
public UserResource(
Authorizer authorizer, Limits limits, AuthenticatorHandler authenticatorHandler) {
super(Entity.USER, authorizer, limits);
jwtTokenGenerator = JWTTokenGenerator.getInstance();
allowedFields.remove(USER_PROTECTED_FIELDS);
tokenRepository = Entity.getTokenRepository();
UserTokenCache.initialize();
authHandler = authenticatorHandler;
}
@Override
protected List getEntitySpecificOperations() {
addViewOperation("profile,roles,teams,follows,owns", MetadataOperation.VIEW_BASIC);
return listOf(MetadataOperation.EDIT_TEAMS);
}
@Override
public void initialize(OpenMetadataApplicationConfig config) throws IOException {
super.initialize(config);
this.authenticationConfiguration = config.getAuthenticationConfiguration();
this.authorizerConfiguration = config.getAuthorizerConfiguration();
SmtpSettings smtpSettings = config.getSmtpSettings();
this.isEmailServiceEnabled = smtpSettings != null && smtpSettings.getEnableSmtpServer();
this.repository.initializeUsers(config);
this.isSelfSignUpEnabled = authenticationConfiguration.getEnableSelfSignup();
}
public static class UserList extends ResultList {
/* Required for serde */
}
public static class PersonalAccessTokenList extends ResultList {
/* Required for serde */
}
@GET
@Valid
@Operation(
operationId = "listUsers",
summary = "List users",
description =
"Get a list of users. Use `fields` "
+ "parameter to get only necessary fields. Use cursor-based pagination to limit the number "
+ "entries in the list using `limit` and `before` or `after` query params.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The user ",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = UserList.class)))
})
public ResultList list(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(
description = "Fields requested in the returned resource",
schema = @Schema(type = "string", example = FIELDS))
@QueryParam("fields")
String fieldsParam,
@Parameter(
description = "Filter users by team",
schema = @Schema(type = "string", example = "Legal"))
@QueryParam("team")
String teamParam,
@Parameter(description = "Limit the number users returned. (1 to 1000000, default = 10)")
@DefaultValue("10")
@Min(0)
@Max(1000000)
@QueryParam("limit")
int limitParam,
@Parameter(
description = "Returns list of users before this cursor",
schema = @Schema(type = "string"))
@QueryParam("before")
String before,
@Parameter(
description = "Returns list of users after this cursor",
schema = @Schema(type = "string"))
@QueryParam("after")
String after,
@Parameter(
description = "Returns list of admin users if set to true",
schema = @Schema(type = "boolean"))
@QueryParam("isAdmin")
Boolean isAdmin,
@Parameter(
description = "Returns list of bot users if set to true",
schema = @Schema(type = "boolean"))
@QueryParam("isBot")
Boolean isBot,
@Parameter(
description = "Include all, deleted, or non-deleted entities.",
schema = @Schema(implementation = Include.class))
@QueryParam("include")
@DefaultValue("non-deleted")
Include include) {
ListFilter filter = new ListFilter(include).addQueryParam("team", teamParam);
if (isAdmin != null) {
filter.addQueryParam("isAdmin", String.valueOf(isAdmin));
}
if (isBot != null) {
filter.addQueryParam("isBot", String.valueOf(isBot));
}
ResultList users =
listInternal(uriInfo, securityContext, fieldsParam, filter, limitParam, before, after);
users.getData().forEach(user -> decryptOrNullify(securityContext, user));
return users;
}
@GET
@Path("/{id}/versions")
@Operation(
operationId = "listAllUserVersion",
summary = "List user versions",
description = "Get a list of all the versions of a user identified by `id`",
responses = {
@ApiResponse(
responseCode = "200",
description = "List of user versions",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = EntityHistory.class)))
})
public EntityHistory listVersions(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(description = "Id of the user", schema = @Schema(type = "UUID")) @PathParam("id")
UUID id) {
return super.listVersionsInternal(securityContext, id);
}
@GET
@Path("/generateRandomPwd")
@Operation(
operationId = "generateRandomPwd",
summary = "Generate a random password",
description = "Generate a random password",
responses = {@ApiResponse(responseCode = "200", description = "Random pwd")})
public Response generateRandomPassword(
@Context UriInfo uriInfo, @Context SecurityContext securityContext) {
authorizer.authorizeAdmin(securityContext);
return Response.status(OK).entity(PasswordUtil.generateRandomPassword()).build();
}
@GET
@Valid
@Path("/{id}")
@Operation(
operationId = "getUserByID",
summary = "Get a user",
description = "Get a user by `id`",
responses = {
@ApiResponse(
responseCode = "200",
description = "The user",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = User.class))),
@ApiResponse(responseCode = "404", description = "User for instance {id} is not found")
})
public User get(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(description = "Id of the user", schema = @Schema(type = "UUID")) @PathParam("id")
UUID id,
@Parameter(
description = "Fields requested in the returned resource",
schema = @Schema(type = "string", example = FIELDS))
@QueryParam("fields")
String fieldsParam,
@Parameter(
description = "Include all, deleted, or non-deleted entities.",
schema = @Schema(implementation = Include.class))
@QueryParam("include")
@DefaultValue("non-deleted")
Include include) {
User user = getInternal(uriInfo, securityContext, id, fieldsParam, include);
decryptOrNullify(securityContext, user);
return user;
}
@GET
@Valid
@Path("/name/{name}")
@Operation(
operationId = "getUserByFQN",
summary = "Get a user by name",
description = "Get a user by `name`.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The user",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = User.class))),
@ApiResponse(responseCode = "404", description = "User for instance {name} is not found")
})
public User getByName(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(description = "Name of the user", schema = @Schema(type = "string"))
@PathParam("name")
String name,
@Parameter(
description = "Fields requested in the returned resource",
schema = @Schema(type = "string", example = FIELDS))
@QueryParam("fields")
String fieldsParam,
@Parameter(
description = "Include all, deleted, or non-deleted entities.",
schema = @Schema(implementation = Include.class))
@QueryParam("include")
@DefaultValue("non-deleted")
Include include) {
User user = getByNameInternal(uriInfo, securityContext, name, fieldsParam, include);
decryptOrNullify(securityContext, user);
return user;
}
@GET
@Valid
@Path("/loggedInUser")
@Operation(
operationId = "getCurrentLoggedInUser",
summary = "Get current logged in user",
description = "Get the user who is authenticated and is currently logged in.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The user",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = User.class))),
@ApiResponse(responseCode = "404", description = "User not found")
})
public User getCurrentLoggedInUser(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Context ContainerRequestContext containerRequestContext,
@Parameter(
description = "Fields requested in the returned resource",
schema = @Schema(type = "string", example = FIELDS))
@QueryParam("fields")
String fieldsParam) {
CatalogSecurityContext catalogSecurityContext =
(CatalogSecurityContext) containerRequestContext.getSecurityContext();
Fields fields = getFields(fieldsParam);
String currentEmail = ((CatalogPrincipal) catalogSecurityContext.getUserPrincipal()).getEmail();
User user =
repository.getLoggedInUserByNameAndEmail(
uriInfo, catalogSecurityContext.getUserPrincipal().getName(), currentEmail, fields);
// Sync the Roles from token to User
if (Boolean.TRUE.equals(authorizerConfiguration.getUseRolesFromProvider())
&& Boolean.FALSE.equals(user.getIsBot() != null && user.getIsBot())) {
reSyncUserRolesFromToken(
uriInfo, user, getRolesFromAuthorizationToken(catalogSecurityContext));
}
return addHref(uriInfo, user);
}
@GET
@Valid
@Path("/loggedInUser/groupTeams")
@Operation(
operationId = "getCurrentLoggedInUserGroupTeams",
summary = "Get group type of teams for current logged in user",
description =
"Get the group type of teams of user who is authenticated and is currently logged in.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The teams of type 'Group' that a user belongs to",
content =
@Content(
mediaType = "application/json",
array =
@ArraySchema(schema = @Schema(implementation = EntityReference.class)))),
@ApiResponse(responseCode = "404", description = "User not found")
})
public List getCurrentLoggedInUser(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Context ContainerRequestContext containerRequestContext) {
CatalogSecurityContext catalogSecurityContext =
(CatalogSecurityContext) containerRequestContext.getSecurityContext();
String currentEmail = ((CatalogPrincipal) catalogSecurityContext.getUserPrincipal()).getEmail();
return repository.getGroupTeams(uriInfo, catalogSecurityContext, currentEmail);
}
@POST
@Path("/logout")
@Operation(
operationId = "logoutUser",
summary = "Logout a User(Only called for saml and basic Auth)",
description = "Logout a User(Only called for saml and basic Auth)",
responses = {
@ApiResponse(responseCode = "200", description = "The user "),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response logoutUser(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Valid LogoutRequest request) {
Date logoutTime = Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant());
JwtTokenCacheManager.getInstance()
.markLogoutEventForToken(
new LogoutRequest()
.withUsername(securityContext.getUserPrincipal().getName())
.withToken(request.getToken())
.withLogoutTime(logoutTime));
if (isBasicAuth() && request.getRefreshToken() != null) {
// need to clear the refresh token as well
tokenRepository.deleteToken(request.getRefreshToken());
}
return Response.status(200).entity("Logout Successful").build();
}
@GET
@Path("/{id}/versions/{version}")
@Operation(
operationId = "getSpecificUserVersion",
summary = "Get a version of the user",
description = "Get a version of the user by given `id`",
responses = {
@ApiResponse(
responseCode = "200",
description = "user",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = User.class))),
@ApiResponse(
responseCode = "404",
description = "User for instance {id} and version {version} is not found")
})
public User getVersion(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(description = "Id of the user", schema = @Schema(type = "UUID")) @PathParam("id")
UUID id,
@Parameter(
description = "User version number in the form `major`.`minor`",
schema = @Schema(type = "string", example = "0.1 or 1.1"))
@PathParam("version")
String version) {
return super.getVersionInternal(securityContext, id, version);
}
@POST
@Operation(
operationId = "createUser",
summary = "Create a user",
description = "Create a new user.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The user ",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = User.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response createUser(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Context ContainerRequestContext containerRequestContext,
@Valid CreateUser create) {
User user = getUser(securityContext.getUserPrincipal().getName(), create);
if (Boolean.TRUE.equals(user.getIsBot())) {
addAuthMechanismToBot(user, create, uriInfo);
}
//
try {
// Email Validation
validateEmailAlreadyExists(user.getEmail());
addUserAuthForBasic(user, create);
} catch (RuntimeException ex) {
return Response.status(CONFLICT)
.type(MediaType.APPLICATION_JSON_TYPE)
.entity(
new ErrorMessage(
CONFLICT.getStatusCode(), CatalogExceptionMessage.ENTITY_ALREADY_EXISTS))
.build();
}
// Add the roles on user creation
updateUserRolesIfRequired(user, containerRequestContext);
Response createdUserRes;
try {
createdUserRes = create(uriInfo, securityContext, user);
} catch (EntityNotFoundException ex) {
if (isSelfSignUpEnabled) {
if (securityContext.getUserPrincipal().getName().equals(user.getName())) {
User created = addHref(uriInfo, repository.create(uriInfo, user));
createdUserRes = Response.created(created.getHref()).entity(created).build();
} else {
throw new CustomExceptionMessage(
FORBIDDEN,
CatalogExceptionMessage.OTHER_USER_SIGN_UP_ERROR,
CatalogExceptionMessage.OTHER_USER_SIGN_UP);
}
} else {
throw new CustomExceptionMessage(
FORBIDDEN,
CatalogExceptionMessage.SELF_SIGNUP_NOT_ENABLED,
CatalogExceptionMessage.SELF_SIGNUP_DISABLED_MESSAGE);
}
}
if (createdUserRes != null) {
// Send Invite mail to user
sendInviteMailToUserForBasicAuth(uriInfo, user, create);
// Update response to remove auth fields
decryptOrNullify(securityContext, (User) createdUserRes.getEntity());
return createdUserRes;
}
return Response.status(BAD_REQUEST).entity("User Cannot be created Successfully.").build();
}
private void addUserAuthForBasic(User user, CreateUser create) {
if (isBasicAuth()) {
user.setName(user.getEmail().split("@")[0]);
if (Boolean.FALSE.equals(create.getIsBot())
&& create.getCreatePasswordType() == ADMIN_CREATE) {
addAuthMechanismToUser(user, create);
}
}
}
private void updateUserRolesIfRequired(
User user, ContainerRequestContext containerRequestContext) {
CatalogSecurityContext catalogSecurityContext =
(CatalogSecurityContext) containerRequestContext.getSecurityContext();
if (Boolean.TRUE.equals(authorizerConfiguration.getUseRolesFromProvider())
&& Boolean.FALSE.equals(user.getIsBot() != null && user.getIsBot())) {
user.setRoles(validateAndGetRolesRef(getRolesFromAuthorizationToken(catalogSecurityContext)));
}
}
private void sendInviteMailToUserForBasicAuth(UriInfo uriInfo, User user, CreateUser create) {
if (isBasicAuth() && isEmailServiceEnabled()) {
try {
authHandler.sendInviteMailToUser(
uriInfo,
user,
String.format("Welcome to %s", EmailUtil.getEmailingEntity()),
create.getCreatePasswordType(),
create.getPassword());
} catch (Exception ex) {
LOG.error("Error in sending invite to User" + ex.getMessage());
}
}
}
private boolean isBasicAuth() {
return authenticationConfiguration.getProvider().equals(AuthProvider.BASIC);
}
@PUT
@Operation(
summary = "Update user",
description = "Create or Update a user.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The user ",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CreateUser.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response createOrUpdateUser(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Valid CreateUser create) {
User user = getUser(securityContext.getUserPrincipal().getName(), create);
repository.prepareInternal(user, true);
User existingUser = repository.findByNameOrNull(user.getFullyQualifiedName(), ALL);
if (existingUser == null) {
limits.enforceLimits(
securityContext,
getResourceContextByName(user.getFullyQualifiedName()),
new OperationContext(entityType, MetadataOperation.CREATE));
}
ResourceContext> resourceContext = getResourceContextByName(user.getFullyQualifiedName());
if (Boolean.TRUE.equals(create.getIsAdmin()) || Boolean.TRUE.equals(create.getIsBot())) {
authorizer.authorizeAdmin(securityContext);
} else if (!securityContext.getUserPrincipal().getName().equals(user.getName())) {
// doing authorization check outside of authorizer here. We are checking if the logged-in user
// is same as the user. We are trying to update. One option is to set users.owner as user,
// however that is not supported for User.
OperationContext createOperationContext =
new OperationContext(entityType, EntityUtil.createOrUpdateOperation(resourceContext));
authorizer.authorize(securityContext, createOperationContext, resourceContext);
}
if (Boolean.TRUE.equals(create.getIsBot())) { // TODO expect bot to be created separately
return createOrUpdateBot(user, create, uriInfo, securityContext);
}
PutResponse response = repository.createOrUpdate(uriInfo, user);
addHref(uriInfo, response.getEntity());
return response.toResponse();
}
@PUT
@Path("/generateToken/{id}")
@Operation(
operationId = "generateJWTTokenForBotUser",
summary = "Generate JWT Token for a Bot User",
description = "Generate JWT Token for a Bot User.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The user ",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = JWTTokenExpiry.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response generateToken(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(description = "Id of the user", schema = @Schema(type = "UUID")) @PathParam("id")
UUID id,
@Valid GenerateTokenRequest generateTokenRequest) {
authorizer.authorizeAdmin(securityContext);
User user = repository.get(uriInfo, id, repository.getFieldsWithUserAuth("*"));
JWTAuthMechanism jwtAuthMechanism =
jwtTokenGenerator.generateJWTToken(user, generateTokenRequest.getJWTTokenExpiry());
AuthenticationMechanism authenticationMechanism =
new AuthenticationMechanism()
.withConfig(jwtAuthMechanism)
.withAuthType(AuthenticationMechanism.AuthType.JWT);
user.setAuthenticationMechanism(authenticationMechanism);
User updatedUser = repository.createOrUpdate(uriInfo, user).getEntity();
jwtAuthMechanism =
JsonUtils.convertValue(
updatedUser.getAuthenticationMechanism().getConfig(), JWTAuthMechanism.class);
return Response.status(Response.Status.OK).entity(jwtAuthMechanism).build();
}
@PUT
@Path("/revokeToken")
@Operation(
operationId = "revokeJWTTokenForBotUser",
summary = "Revoke JWT Token for a Bot User",
description = "Revoke JWT Token for a Bot User.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The user ",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = JWTAuthMechanism.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response revokeToken(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Valid RevokeTokenRequest revokeTokenRequest) {
authorizer.authorizeAdmin(securityContext);
User user =
repository.get(uriInfo, revokeTokenRequest.getId(), repository.getFieldsWithUserAuth("*"));
if (Boolean.FALSE.equals(user.getIsBot())) {
throw new IllegalStateException(CatalogExceptionMessage.INVALID_BOT_USER);
}
JWTAuthMechanism jwtAuthMechanism = new JWTAuthMechanism().withJWTToken(StringUtils.EMPTY);
AuthenticationMechanism authenticationMechanism =
new AuthenticationMechanism().withConfig(jwtAuthMechanism).withAuthType(JWT);
user.setAuthenticationMechanism(authenticationMechanism);
PutResponse response = repository.createOrUpdate(uriInfo, user);
addHref(uriInfo, response.getEntity());
// Invalidate Bot Token in Cache
BotTokenCache.invalidateToken(user.getName());
return response.toResponse();
}
@GET
@Path("/token/{id}")
@Operation(
operationId = "getJWTTokenForBotUser",
summary = "Get JWT Token for a Bot User",
description = "Get JWT Token for a Bot User.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The user ",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = JWTAuthMechanism.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public JWTAuthMechanism getToken(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(description = "Id of the user", schema = @Schema(type = "UUID")) @PathParam("id")
UUID id) {
User user = repository.get(uriInfo, id, new Fields(Set.of(AUTH_MECHANISM_FIELD)));
if (!Boolean.TRUE.equals(user.getIsBot())) {
throw new IllegalArgumentException("JWT token is only supported for bot users");
}
decryptOrNullify(securityContext, user);
authorizer.authorizeAdmin(securityContext);
AuthenticationMechanism authenticationMechanism = user.getAuthenticationMechanism();
if (authenticationMechanism != null
&& authenticationMechanism.getConfig() != null
&& authenticationMechanism.getAuthType() == JWT) {
return JsonUtils.convertValue(authenticationMechanism.getConfig(), JWTAuthMechanism.class);
}
return new JWTAuthMechanism();
}
@GET
@Path("/auth-mechanism/{id}")
@Operation(
operationId = "getAuthenticationMechanismBotUser",
summary = "Get Authentication Mechanism for a Bot User",
description = "Get Authentication Mechanism for a Bot User.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The user ",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = AuthenticationMechanism.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public AuthenticationMechanism getAuthenticationMechanism(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(description = "Id of the user", schema = @Schema(type = "UUID")) @PathParam("id")
UUID id) {
User user = repository.get(uriInfo, id, new Fields(Set.of(AUTH_MECHANISM_FIELD)));
if (!Boolean.TRUE.equals(user.getIsBot())) {
throw new IllegalArgumentException("JWT token is only supported for bot users");
}
limits.enforceLimits(
securityContext,
getResourceContext(),
new OperationContext(entityType, MetadataOperation.GENERATE_TOKEN));
decryptOrNullify(securityContext, user);
authorizer.authorizeAdmin(securityContext);
return user.getAuthenticationMechanism();
}
@PATCH
@Path("/{id}")
@Consumes(MediaType.APPLICATION_JSON_PATCH_JSON)
@Operation(
operationId = "patchUser",
summary = "Update a user",
description = "Update an existing user using JsonPatch.",
externalDocs =
@ExternalDocumentation(
description = "JsonPatch RFC",
url = "https://tools.ietf.org/html/rfc6902"))
public Response patch(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(description = "Id of the user", schema = @Schema(type = "UUID")) @PathParam("id")
UUID id,
@RequestBody(
description = "JsonPatch with array of operations",
content =
@Content(
mediaType = MediaType.APPLICATION_JSON_PATCH_JSON,
examples = {
@ExampleObject("[{op:remove, path:/a},{op:add, path: /b, value: val}]")
}))
JsonPatch patch) {
for (JsonValue patchOp : patch.toJsonArray()) {
JsonObject patchOpObject = patchOp.asJsonObject();
if (patchOpObject.containsKey("path") && patchOpObject.containsKey("value")) {
String path = patchOpObject.getString("path");
if (path.equals("/isAdmin") || path.equals("/isBot") || path.contains("/roles")) {
authorizer.authorizeAdmin(securityContext);
continue;
}
// if path contains team, check if team is join able by any user
if (patchOpObject.containsKey("op")
&& patchOpObject.getString("op").equals("add")
&& path.startsWith("/teams/")) {
JsonObject value = null;
try {
value = patchOpObject.getJsonObject("value");
} catch (Exception ex) {
// ignore exception if value is not an object
}
if (value != null) {
String teamId = value.getString("id");
repository.validateTeamAddition(id, UUID.fromString(teamId));
if (!repository.isTeamJoinable(teamId)) {
authorizer.authorizeAdmin(securityContext); // Only admin can join closed teams
}
}
}
}
}
return patchInternal(uriInfo, securityContext, id, patch);
}
@DELETE
@Path("/{id}")
@Operation(
operationId = "deleteUser",
summary = "Delete a user",
description = "Users can't be deleted but are soft-deleted.",
responses = {
@ApiResponse(responseCode = "200", description = "OK"),
@ApiResponse(responseCode = "404", description = "User for instance {id} is not found")
})
public Response delete(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(description = "Hard delete the entity. (Default = `false`)")
@QueryParam("hardDelete")
@DefaultValue("false")
boolean hardDelete,
@Parameter(description = "Id of the user", schema = @Schema(type = "UUID")) @PathParam("id")
UUID id) {
Response response = delete(uriInfo, securityContext, id, false, hardDelete);
decryptOrNullify(securityContext, (User) response.getEntity());
return response;
}
@DELETE
@Path("/name/{name}")
@Operation(
operationId = "deleteUserByName",
summary = "Delete a user",
description = "Users can't be deleted but are soft-deleted.",
responses = {
@ApiResponse(responseCode = "200", description = "OK"),
@ApiResponse(responseCode = "404", description = "User for instance {name} is not found")
})
public Response delete(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(description = "Hard delete the entity. (Default = `false`)")
@QueryParam("hardDelete")
@DefaultValue("false")
boolean hardDelete,
@Parameter(description = "Name of the user", schema = @Schema(type = "string"))
@PathParam("name")
String name) {
return deleteByName(uriInfo, securityContext, name, false, hardDelete);
}
@PUT
@Path("/restore")
@Operation(
operationId = "restore",
summary = "Restore a soft deleted User.",
description = "Restore a soft deleted User.",
responses = {
@ApiResponse(
responseCode = "200",
description = "Successfully restored the User ",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = User.class)))
})
public Response restoreTable(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Valid RestoreEntity restore) {
return restoreEntity(uriInfo, securityContext, restore.getId());
}
@POST
@Path("/signup")
@Operation(
operationId = "registerUser",
summary = "Register User",
description = "Register a new User",
responses = {
@ApiResponse(responseCode = "200", description = "The user "),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response registerNewUser(@Context UriInfo uriInfo, @Valid RegistrationRequest create)
throws IOException {
User registeredUser = authHandler.registerUser(create);
authHandler.sendEmailVerification(uriInfo, registeredUser);
return Response.status(Response.Status.CREATED.getStatusCode(), "User Registration Successful.")
.entity(registeredUser)
.build();
}
@PUT
@Path("/registrationConfirmation")
@Operation(
operationId = "confirmUserEmail",
summary = "Confirm User Email",
description = "Confirm User Email",
responses = {
@ApiResponse(responseCode = "200", description = "The user "),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response confirmUserEmail(
@Context UriInfo uriInfo,
@Parameter(
description = "Token sent for Email Confirmation",
schema = @Schema(type = "string"))
@QueryParam("token")
String token) {
authHandler.confirmEmailRegistration(uriInfo, token);
return Response.status(Response.Status.OK).entity("Email Verified Successfully").build();
}
@PUT
@Path("/resendRegistrationToken")
@Operation(
operationId = "resendRegistrationToken",
summary = "Resend Registration Token",
description = "Resend Registration Token",
responses = {
@ApiResponse(responseCode = "200", description = "The user "),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response resendRegistrationToken(
@Context UriInfo uriInfo,
@Parameter(
description = "Token sent for Email Confirmation Earlier",
schema = @Schema(type = "string"))
@QueryParam("user")
String user)
throws IOException {
User registeredUser = repository.getByName(uriInfo, user, getFields("isEmailVerified"));
if (Boolean.TRUE.equals(registeredUser.getIsEmailVerified())) {
return Response.status(Response.Status.OK).entity("Email Already Verified.").build();
}
authHandler.resendRegistrationToken(uriInfo, registeredUser);
return Response.status(Response.Status.OK)
.entity("Email Verification Mail Sent. Please check your Mailbox.")
.build();
}
@POST
@Path("/generatePasswordResetLink")
@Operation(
operationId = "generatePasswordResetLink",
summary = "Generate Password Reset Link",
description = "Generate Password Reset Link",
responses = {
@ApiResponse(responseCode = "200", description = "The user "),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response generateResetPasswordLink(@Context UriInfo uriInfo, @Valid EmailRequest request) {
String userName = request.getEmail().split("@")[0];
User registeredUser;
try {
registeredUser =
repository.getByName(
uriInfo, userName, new Fields(Set.of(USER_PROTECTED_FIELDS), USER_PROTECTED_FIELDS));
} catch (EntityNotFoundException ex) {
LOG.error(
"[GeneratePasswordReset] Got Error while fetching user : {}, error message {}",
userName,
ex.getMessage());
return Response.status(Response.Status.OK)
.entity("Please check your mail to for Reset Password Link.")
.build();
}
try {
// send a mail to the User with the Update
authHandler.sendPasswordResetLink(
uriInfo,
registeredUser,
EmailUtil.getPasswordResetSubject(),
EmailUtil.PASSWORD_RESET_TEMPLATE_FILE);
} catch (Exception ex) {
LOG.error("Error in sending mail for reset password" + ex.getMessage());
return Response.status(424).entity(new ErrorMessage(424, EMAIL_SENDING_ISSUE)).build();
}
return Response.status(Response.Status.OK)
.entity("Please check your mail to for Reset Password Link.")
.build();
}
@POST
@Path("/password/reset")
@Operation(
operationId = "resetUserPassword",
summary = "Reset Password For User",
description = "Reset User Password",
responses = {
@ApiResponse(
responseCode = "200",
description = "The user ",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = User.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response resetUserPassword(@Context UriInfo uriInfo, @Valid PasswordResetRequest request)
throws IOException {
authHandler.resetUserPasswordWithToken(uriInfo, request);
return Response.status(200).entity("Password Changed Successfully").build();
}
@PUT
@Path("/changePassword")
@Operation(
operationId = "changeUserPassword",
summary = "Change Password For User",
description = "Create a new user.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The user ",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = User.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response changeUserPassword(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Valid ChangePasswordRequest request)
throws IOException {
if (request.getRequestType() == SELF) {
authHandler.changeUserPwdWithOldPwd(
uriInfo, securityContext.getUserPrincipal().getName(), request);
} else {
authorizer.authorizeAdmin(securityContext);
authHandler.changeUserPwdWithOldPwd(uriInfo, request.getUsername(), request);
}
return Response.status(OK).entity("Password Updated Successfully").build();
}
@POST
@Path("/checkEmailInUse")
@Operation(
operationId = "checkEmailInUse",
summary = "Check if a email is already in use",
description = "Check if a email is already in use",
responses = {
@ApiResponse(
responseCode = "200",
description = "Return true or false",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Boolean.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response checkEmailInUse(@Valid EmailRequest request) {
boolean emailExists = repository.checkEmailAlreadyExists(request.getEmail());
return Response.status(Response.Status.OK).entity(emailExists).build();
}
@POST
@Path("/checkEmailVerified")
@Operation(
operationId = "checkEmailIsVerified",
summary = "Check if a mail is verified",
description = "Check if a mail is already in use",
responses = {
@ApiResponse(
responseCode = "200",
description = "Return true or false",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Boolean.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response checkEmailVerified(@Context UriInfo uriInfo, @Valid EmailRequest request) {
User user =
repository.getByName(
uriInfo, request.getEmail().split("@")[0], getFields("isEmailVerified"));
return Response.status(Response.Status.OK).entity(user.getIsEmailVerified()).build();
}
@POST
@Path("/login")
@Operation(
operationId = "loginUserWithPwd",
summary = "Login User with email (plain-text) and Password (encoded in base 64)",
description = "Login User with email(plain-text) and Password (encoded in base 64)",
responses = {
@ApiResponse(
responseCode = "200",
description = "Returns the Jwt Token Response ",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = JwtResponse.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response loginUserWithPassword(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Valid LoginRequest loginRequest)
throws IOException, TemplateException {
byte[] decodedBytes;
try {
decodedBytes = Base64.getDecoder().decode(loginRequest.getPassword());
} catch (Exception ex) {
throw new IllegalArgumentException("Password needs to be encoded in Base-64.");
}
loginRequest.withPassword(new String(decodedBytes));
return Response.status(Response.Status.OK).entity(authHandler.loginUser(loginRequest)).build();
}
@POST
@Path("/refresh")
@Operation(
operationId = "refreshToken",
summary = "Provide access token to User with refresh token",
description = "Provide access token to User with refresh token",
responses = {
@ApiResponse(
responseCode = "200",
description = "The user ",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = JwtResponse.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response refreshToken(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Valid TokenRefreshRequest refreshRequest) {
return Response.status(Response.Status.OK)
.entity(authHandler.getNewAccessToken(refreshRequest))
.build();
}
@GET
@Path("/security/token")
@Operation(
operationId = "getPersonalAccessToken",
summary = "Get personal access token to User",
description = "Get a personal access token",
responses = {
@ApiResponse(
responseCode = "200",
description = "List Of Personal Access Tokens ",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = PersonalAccessTokenList.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response getPersonalAccessToken(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(description = "User Name of the User for which to get. (Default = `false`)")
@QueryParam("username")
String userName) {
limits.enforceLimits(
securityContext,
getResourceContext(),
new OperationContext(entityType, MetadataOperation.GENERATE_TOKEN));
if (userName != null) {
authorizer.authorizeAdmin(securityContext);
} else {
userName = securityContext.getUserPrincipal().getName();
}
User user = repository.getByName(null, userName, getFields("id"), Include.NON_DELETED, true);
List tokens =
tokenRepository.findByUserIdAndType(user.getId(), TokenType.PERSONAL_ACCESS_TOKEN.value());
return Response.status(Response.Status.OK).entity(new ResultList<>(tokens)).build();
}
@PUT
@Path("/security/token/revoke")
@Operation(
operationId = "revokePersonalAccessToken",
summary = "Revoke personal access token to User",
description = "Revoke personal access token",
responses = {
@ApiResponse(
responseCode = "200",
description = "The Personal access token ",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = PersonalAccessTokenList.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response revokePersonalAccessToken(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(description = "Username in case admin is revoking. (Default = `false`)")
@QueryParam("username")
String userName,
@Parameter(description = "Remove All tokens of the user. (Default = `false`)")
@QueryParam("removeAll")
@DefaultValue("false")
boolean removeAll,
@Valid RevokePersonalTokenRequest request) {
if (!CommonUtil.nullOrEmpty(userName)) {
authorizer.authorizeAdmin(securityContext);
} else {
userName = securityContext.getUserPrincipal().getName();
}
User user = repository.getByName(null, userName, getFields("id"), Include.NON_DELETED, false);
if (removeAll) {
tokenRepository.deleteTokenByUserAndType(
user.getId(), TokenType.PERSONAL_ACCESS_TOKEN.value());
} else {
List ids =
request.getTokenIds().stream().map(UUID::toString).collect(Collectors.toList());
tokenRepository.deleteAllToken(ids);
}
UserTokenCache.invalidateToken(user.getName());
List tokens =
tokenRepository.findByUserIdAndType(user.getId(), TokenType.PERSONAL_ACCESS_TOKEN.value());
return Response.status(Response.Status.OK).entity(new ResultList<>(tokens)).build();
}
@PUT
@Path("/security/token")
@Operation(
operationId = "createPersonalAccessToken",
summary = "Provide access token to User",
description = "Provide access token to User",
responses = {
@ApiResponse(
responseCode = "200",
description = "The user ",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = PersonalAccessToken.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response createAccessToken(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Valid CreatePersonalToken tokenRequest) {
limits.enforceLimits(
securityContext,
getResourceContext(),
new OperationContext(entityType, MetadataOperation.GENERATE_TOKEN));
String userName = securityContext.getUserPrincipal().getName();
User user =
repository.getByName(
null, userName, getFields("roles,email,isBot"), Include.NON_DELETED, false);
if (user.getIsBot() == null || Boolean.FALSE.equals(user.getIsBot())) {
// Create Personal Access Token
JWTAuthMechanism authMechanism =
JWTTokenGenerator.getInstance()
.getJwtAuthMechanism(
userName,
getRoleListFromUser(user),
user.getIsAdmin(),
user.getEmail(),
false,
ServiceTokenType.PERSONAL_ACCESS,
getExpiryDate(tokenRequest.getJWTTokenExpiry()),
null);
PersonalAccessToken personalAccessToken =
TokenUtil.getPersonalAccessToken(tokenRequest, user, authMechanism);
tokenRepository.insertToken(personalAccessToken);
UserTokenCache.invalidateToken(user.getName());
return Response.status(Response.Status.OK).entity(personalAccessToken).build();
}
throw new CustomExceptionMessage(
BAD_REQUEST, "NO_PERSONAL_TOKEN_FOR_BOTS", "Bots cannot have a Personal Access Token.");
}
@GET
@Path("/documentation/csv")
@Valid
@Operation(
operationId = "getCsvDocumentation",
summary = "Get CSV documentation for user import/export")
public String getUserCsvDocumentation(@Context SecurityContext securityContext) {
return JsonUtils.pojoToJson(UserCsv.DOCUMENTATION);
}
@GET
@Path("/export")
@Produces(MediaType.TEXT_PLAIN)
@Valid
@Operation(
operationId = "exportUsers",
summary = "Export users in a team in CSV format",
responses = {
@ApiResponse(
responseCode = "200",
description = "Exported csv with user information",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = String.class)))
})
public String exportUsersCsv(
@Context SecurityContext securityContext,
@Parameter(
description = "Name of the team to under which the users are imported to",
required = true,
schema = @Schema(type = "string"))
@QueryParam("team")
String team)
throws IOException {
return exportCsvInternal(securityContext, team);
}
@PUT
@Path("/import")
@Consumes(MediaType.TEXT_PLAIN)
@Valid
@Operation(
operationId = "importTeams",
summary = "Import from CSV to create, and update teams.",
responses = {
@ApiResponse(
responseCode = "200",
description = "Import result",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CsvImportResult.class)))
})
public CsvImportResult importCsv(
@Context SecurityContext securityContext,
@Parameter(
description = "Name of the team to under which the users are imported to",
required = true,
schema = @Schema(type = "string"))
@QueryParam("team")
String team,
@Parameter(
description =
"Dry-run when true is used for validating the CSV without really importing it. (default=true)",
schema = @Schema(type = "boolean"))
@DefaultValue("true")
@QueryParam("dryRun")
boolean dryRun,
String csv)
throws IOException {
return importCsvInternal(securityContext, team, csv, dryRun);
}
public void validateEmailAlreadyExists(String email) {
if (repository.checkEmailAlreadyExists(email)) {
throw new CustomExceptionMessage(
BAD_REQUEST, "EMAIL_EXISTS", "User with Email Already Exists");
}
}
private Response createOrUpdateBot(
User user, CreateUser create, UriInfo uriInfo, SecurityContext securityContext) {
User original = retrieveBotUser(user, uriInfo);
String botName = create.getBotName();
EntityInterface bot = retrieveBot(botName);
// check if the bot user exists
if (original != null
&& (original.getIsBot() == null || Boolean.FALSE.equals(original.getIsBot()))) {
throw new IllegalArgumentException(
String.format("User [%s] already exists.", original.getName()));
} else if (!botHasRelationshipWithUser(bot, original)
&& original != null
&& userHasRelationshipWithAnyBot(original, bot)) {
// throw an exception if user already has a relationship with a bot
List userBotRelationship =
retrieveBotRelationshipsFor(original);
bot =
Entity.getEntityRepository(Entity.BOT)
.get(
null,
userBotRelationship.stream().findFirst().orElseThrow().getId(),
Fields.EMPTY_FIELDS);
throw new IllegalArgumentException(
CatalogExceptionMessage.userAlreadyBot(user.getName(), bot.getName()));
}
// TODO: review this flow on https://github.com/open-metadata/OpenMetadata/issues/8321
if (original != null) {
EntityMaskerFactory.getEntityMasker()
.unmaskAuthenticationMechanism(
user.getName(),
create.getAuthenticationMechanism(),
original.getAuthenticationMechanism());
user.setRoles(original.getRoles());
}
// TODO remove this
addAuthMechanismToBot(user, create, uriInfo);
PutResponse response = repository.createOrUpdate(uriInfo, user);
decryptOrNullify(securityContext, response.getEntity());
return response.toResponse();
}
private EntityInterface retrieveBot(String botName) {
try {
return Entity.getEntityRepository(Entity.BOT).getByName(null, botName, Fields.EMPTY_FIELDS);
} catch (Exception e) {
return null;
}
}
private boolean userHasRelationshipWithAnyBot(User user, EntityInterface botUser) {
List userBotRelationship =
retrieveBotRelationshipsFor(user);
return !userBotRelationship.isEmpty()
&& (botUser == null
|| (userBotRelationship.stream()
.anyMatch(relationship -> !relationship.getId().equals(botUser.getId()))));
}
private List retrieveBotRelationshipsFor(User user) {
return repository.findFromRecords(user.getId(), Entity.USER, Relationship.CONTAINS, Entity.BOT);
}
private boolean botHasRelationshipWithUser(EntityInterface bot, User user) {
if (bot == null || user == null) {
return false;
}
List botUserRelationships =
retrieveBotRelationshipsFor(bot);
return !botUserRelationships.isEmpty()
&& botUserRelationships.get(0).getId().equals(user.getId());
}
private List retrieveBotRelationshipsFor(
EntityInterface bot) {
return repository.findToRecords(bot.getId(), Entity.BOT, Relationship.CONTAINS, Entity.USER);
}
// TODO remove this
private void addAuthMechanismToBot(User user, @Valid CreateUser create, UriInfo uriInfo) {
if (!Boolean.TRUE.equals(user.getIsBot())) {
throw new IllegalArgumentException(
"Authentication mechanism change is only supported for bot users");
}
if (isValidAuthenticationMechanism(create)) {
AuthenticationMechanism authMechanism = create.getAuthenticationMechanism();
AuthenticationMechanism.AuthType authType = authMechanism.getAuthType();
switch (authType) {
case JWT -> {
User original = retrieveBotUser(user, uriInfo);
if (original == null
|| !hasAJWTAuthMechanism(user, original.getAuthenticationMechanism())) {
JWTAuthMechanism jwtAuthMechanism =
JsonUtils.convertValue(authMechanism.getConfig(), JWTAuthMechanism.class);
authMechanism.setConfig(
jwtTokenGenerator.generateJWTToken(user, jwtAuthMechanism.getJWTTokenExpiry()));
} else {
authMechanism = original.getAuthenticationMechanism();
}
}
case SSO -> {
SSOAuthMechanism ssoAuthMechanism =
JsonUtils.convertValue(authMechanism.getConfig(), SSOAuthMechanism.class);
authMechanism.setConfig(ssoAuthMechanism);
}
default -> throw new IllegalArgumentException(
String.format("Not supported authentication mechanism type: [%s]", authType.value()));
}
user.setAuthenticationMechanism(authMechanism);
} else {
throw new IllegalArgumentException(
String.format("Authentication mechanism is empty bot user: [%s]", user.getName()));
}
}
@Nullable
private User retrieveBotUser(User user, UriInfo uriInfo) {
User original;
try {
original =
repository.getByName(
uriInfo, user.getFullyQualifiedName(), repository.getFieldsWithUserAuth("*"));
} catch (EntityNotFoundException exc) {
LOG.debug(
String.format("User not found when adding auth mechanism for: [%s]", user.getName()));
original = null;
}
return original;
}
private void addAuthMechanismToUser(User user, @Valid CreateUser create) {
if (!create.getPassword().equals(create.getConfirmPassword())) {
throw new IllegalArgumentException("Password and Confirm Password should be same.");
}
PasswordUtil.validatePassword(create.getPassword());
String newHashedPwd =
BCrypt.withDefaults().hashToString(12, create.getPassword().toCharArray());
BasicAuthMechanism newAuthForUser = new BasicAuthMechanism().withPassword(newHashedPwd);
user.setAuthenticationMechanism(
new AuthenticationMechanism().withAuthType(BASIC).withConfig(newAuthForUser));
}
private boolean hasAJWTAuthMechanism(User user, AuthenticationMechanism authMechanism) {
if (authMechanism != null && JWT.equals(authMechanism.getAuthType())) {
SecretsManager secretsManager = SecretsManagerFactory.getSecretsManager();
secretsManager.decryptAuthenticationMechanism(user.getName(), authMechanism);
JWTAuthMechanism jwtAuthMechanism =
JsonUtils.convertValue(authMechanism.getConfig(), JWTAuthMechanism.class);
return jwtAuthMechanism != null
&& jwtAuthMechanism.getJWTToken() != null
&& !Objects.equals(jwtAuthMechanism.getJWTToken(), NULL_SECRET_STRING)
&& !StringUtils.EMPTY.equals(jwtAuthMechanism.getJWTToken());
}
return false;
}
private boolean isValidAuthenticationMechanism(CreateUser create) {
if (create.getAuthenticationMechanism() == null) {
return false;
}
if (create.getAuthenticationMechanism().getConfig() != null
&& create.getAuthenticationMechanism().getAuthType() != null) {
return true;
}
throw new IllegalArgumentException(
String.format(
"Incomplete authentication mechanism parameters for bot user: [%s]", create.getName()));
}
private void decryptOrNullify(SecurityContext securityContext, User user) {
SecretsManager secretsManager = SecretsManagerFactory.getSecretsManager();
if (Boolean.TRUE.equals(user.getIsBot()) && user.getAuthenticationMechanism() != null) {
try {
authorizer.authorize(
securityContext,
new OperationContext(entityType, MetadataOperation.VIEW_ALL),
getResourceContextById(user.getId()));
} catch (AuthorizationException e) {
user.getAuthenticationMechanism().setConfig(null);
}
secretsManager.decryptAuthenticationMechanism(
user.getName(), user.getAuthenticationMechanism());
if (authorizer.shouldMaskPasswords(securityContext)) {
EntityMaskerFactory.getEntityMasker()
.maskAuthenticationMechanism(user.getName(), user.getAuthenticationMechanism());
}
}
// Remove mails for non-admin users
PIIMasker.maskUser(authorizer, securityContext, user);
}
}