Fix #8958: Add initialization ordering and remove user/bot initialization from Authorizer (#8959)

* Fix #8958: Add initialization ordering and remove user/bot initialization from Authorizer

* Refactor EmailUtil initialization

* Fix config values

* Fix smtpsettings check

* Fix uninitialize EmailUtil

* remove unused USER_REPOSITORY

Co-authored-by: mohitdeuex <mohit.y@deuexsolutions.com>
Co-authored-by: Mohit Yadav <105265192+mohityadav766@users.noreply.github.com>
This commit is contained in:
Sriharsha Chintalapani 2022-11-23 00:54:20 -08:00 committed by GitHub
parent f00ede11bf
commit 8a95ec1329
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 335 additions and 399 deletions

View File

@ -74,7 +74,6 @@ import org.openmetadata.service.jdbi3.locator.ConnectionAwareAnnotationSqlLocato
import org.openmetadata.service.migration.Migration;
import org.openmetadata.service.migration.MigrationConfiguration;
import org.openmetadata.service.resources.CollectionRegistry;
import org.openmetadata.service.resources.tags.TagLabelCache;
import org.openmetadata.service.secrets.SecretsManager;
import org.openmetadata.service.secrets.SecretsManagerFactory;
import org.openmetadata.service.secrets.SecretsManagerUpdateService;
@ -105,9 +104,7 @@ public class OpenMetadataApplication extends Application<OpenMetadataApplication
validateConfiguration(catalogConfig);
// init email Util for handling
if (catalogConfig.getSmtpSettings() != null && catalogConfig.getSmtpSettings().getEnableSmtpServer()) {
EmailUtil.EmailUtilBuilder.build(catalogConfig.getSmtpSettings());
}
EmailUtil.initialize(catalogConfig);
final Jdbi jdbi = createAndSetupJDBI(environment, catalogConfig.getDataSourceFactory());
final SecretsManager secretsManager =
SecretsManagerFactory.createSecretsManager(
@ -310,7 +307,6 @@ public class OpenMetadataApplication extends Application<OpenMetadataApplication
private void registerResources(OpenMetadataApplicationConfig config, Environment environment, Jdbi jdbi) {
CollectionRegistry.getInstance().registerResources(jdbi, environment, config, authorizer, authenticatorHandler);
TagLabelCache.initialize();
environment.jersey().register(new JsonPatchProvider());
ErrorPageErrorHandler eph = new ErrorPageErrorHandler();
eph.addErrorPage(Response.Status.NOT_FOUND.getStatusCode(), "/");

View File

@ -14,6 +14,7 @@
package org.openmetadata.service.jdbi3;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.openmetadata.schema.entity.Bot;
import org.openmetadata.schema.entity.teams.User;
import org.openmetadata.schema.type.EntityReference;
@ -23,6 +24,7 @@ import org.openmetadata.service.Entity;
import org.openmetadata.service.resources.bots.BotResource;
import org.openmetadata.service.util.EntityUtil.Fields;
@Slf4j
public class BotRepository extends EntityRepository<Bot> {
static final String BOT_UPDATE_FIELDS = "botUser";

View File

@ -27,6 +27,7 @@ import java.util.stream.Collectors;
import javax.ws.rs.core.UriInfo;
import lombok.extern.slf4j.Slf4j;
import org.openmetadata.schema.api.teams.CreateTeam.TeamType;
import org.openmetadata.schema.auth.SSOAuthMechanism;
import org.openmetadata.schema.entity.teams.AuthenticationMechanism;
import org.openmetadata.schema.entity.teams.Team;
import org.openmetadata.schema.entity.teams.User;
@ -34,15 +35,18 @@ import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.Relationship;
import org.openmetadata.service.Entity;
import org.openmetadata.service.OpenMetadataApplicationConfig;
import org.openmetadata.service.exception.CatalogExceptionMessage;
import org.openmetadata.service.jdbi3.CollectionDAO.EntityRelationshipRecord;
import org.openmetadata.service.resources.teams.UserResource;
import org.openmetadata.service.secrets.SecretsManager;
import org.openmetadata.service.secrets.SecretsManagerFactory;
import org.openmetadata.service.security.SecurityUtil;
import org.openmetadata.service.security.policyevaluator.SubjectCache;
import org.openmetadata.service.util.EntityUtil;
import org.openmetadata.service.util.EntityUtil.Fields;
import org.openmetadata.service.util.JsonUtils;
import org.openmetadata.service.util.UserUtil;
@Slf4j
public class UserRepository extends EntityRepository<User> {
@ -181,6 +185,22 @@ public class UserRepository extends EntityRepository<User> {
return daoCollection.userDAO().checkEmailExists(emailId) > 0;
}
public void initializeUsers(OpenMetadataApplicationConfig config) {
Set<String> adminUsers = new HashSet<>(config.getAuthorizerConfiguration().getAdminPrincipals());
LOG.debug("Checking user entries for admin users {}", adminUsers);
String domain = SecurityUtil.getDomain(config);
String providerType = config.getAuthenticationConfiguration().getProvider();
if (providerType.equals(SSOAuthMechanism.SsoServiceType.BASIC.value())) {
UserUtil.handleBasicAuth(adminUsers, domain);
} else {
UserUtil.addUsers(adminUsers, domain, true);
}
LOG.debug("Checking user entries for test users");
Set<String> testUsers = new HashSet<>(config.getAuthorizerConfiguration().getTestPrincipals());
UserUtil.addUsers(testUsers, domain, null);
}
private List<EntityReference> getOwns(User user) throws IOException {
// Compile entities owned by the user
List<EntityRelationshipRecord> ownedEntities =

View File

@ -13,8 +13,6 @@
package org.openmetadata.service.resources.bots;
import static org.openmetadata.service.security.DefaultAuthorizer.user;
import io.swagger.annotations.Api;
import io.swagger.v3.oas.annotations.ExternalDocumentation;
import io.swagger.v3.oas.annotations.Operation;
@ -67,18 +65,18 @@ import org.openmetadata.service.resources.Collection;
import org.openmetadata.service.resources.EntityResource;
import org.openmetadata.service.resources.teams.RoleResource;
import org.openmetadata.service.security.Authorizer;
import org.openmetadata.service.security.DefaultAuthorizer;
import org.openmetadata.service.security.SecurityUtil;
import org.openmetadata.service.util.EntityUtil;
import org.openmetadata.service.util.EntityUtil.Fields;
import org.openmetadata.service.util.ResultList;
import org.openmetadata.service.util.UserUtil;
@Slf4j
@Path("/v1/bots")
@Api(value = "Bot collection", tags = "Bot collection")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Collection(name = "bots", order = 8) // initialize after user resource
@Collection(name = "bots", order = 4) // initialize after user resource
public class BotResource extends EntityResource<Bot, BotRepository> {
public static final String COLLECTION_PATH = "/v1/bots/";
@ -93,12 +91,12 @@ public class BotResource extends EntityResource<Bot, BotRepository> {
String domain = SecurityUtil.getDomain(config);
for (Bot bot : bots) {
String userName = bot.getBotUser().getName();
User user = user(userName, domain, userName).withIsBot(true).withIsAdmin(false);
User user = UserUtil.user(userName, domain, userName).withIsBot(true).withIsAdmin(false);
// Add role corresponding to the bot to the user
// we need to set a mutable list here
user.setRoles(Arrays.asList(RoleResource.getRole(getRoleForBot(bot.getName()))));
user = DefaultAuthorizer.addOrUpdateBotUser(user, config);
user = UserUtil.addOrUpdateBotUser(user, config);
bot.withId(UUID.randomUUID())
.withBotUser(user.getEntityReference())

View File

@ -64,7 +64,7 @@ import org.openmetadata.service.util.ResultList;
@Api(value = "Glossary collection", tags = "Glossary collection")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Collection(name = "glossaries")
@Collection(name = "glossaries", order = 4)
public class GlossaryResource extends EntityResource<Glossary, GlossaryRepository> {
public static final String COLLECTION_PATH = "v1/glossaries/";

View File

@ -68,7 +68,7 @@ import org.openmetadata.service.util.ResultList;
@Api(value = "Glossary collection", tags = "Glossary collection")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Collection(name = "glossaries")
@Collection(name = "glossaries", order = 5)
public class GlossaryTermResource extends EntityResource<GlossaryTerm, GlossaryTermRepository> {
public static final String COLLECTION_PATH = "v1/glossaryTerms/";

View File

@ -102,6 +102,7 @@ public class PolicyResource extends EntityResource<Policy, PolicyRepository> {
// Load any existing rules from database, before loading seed data.
dao.initSeedDataFromResources();
ResourceRegistry.initialize(listOrEmpty(PolicyResource.getResourceDescriptors()));
PolicyCache.initialize();
}
@Override

View File

@ -75,7 +75,7 @@ import org.openmetadata.service.util.ResultList;
@Api(value = "Tags resources collection", tags = "Tags resources collection")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Collection(name = "tags")
@Collection(name = "tags", order = 6) // initialize after Glossary and GlossaryTerm
public class TagResource {
public static final String TAG_COLLECTION_PATH = "/v1/tags/";
private final TagRepository dao;
@ -119,6 +119,7 @@ public class TagResource {
.withProvider(tagCategory.getProvider()));
});
daoCategory.initCategory(tagCategory);
TagLabelCache.initialize();
}
}

View File

@ -102,6 +102,7 @@ public class RoleResource extends EntityResource<Role, RoleRepository> {
}
dao.initializeEntity(role);
}
RoleCache.initialize();
}
public static class RoleList extends ResultList<Role> {

View File

@ -71,7 +71,7 @@ import org.openmetadata.service.util.ResultList;
@Api(value = "Teams collection", tags = "Teams collection")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Collection(name = "teams", order = 3) // Load after roles, and policy resources
@Collection(name = "teams", order = 2) // Load after roles, and policy resources
public class TeamResource extends EntityResource<Team, TeamRepository> {
public static final String COLLECTION_PATH = "/v1/teams/";

View File

@ -110,6 +110,7 @@ import org.openmetadata.service.security.auth.BotTokenCache;
import org.openmetadata.service.security.jwt.JWTTokenGenerator;
import org.openmetadata.service.security.policyevaluator.OperationContext;
import org.openmetadata.service.security.policyevaluator.ResourceContext;
import org.openmetadata.service.security.policyevaluator.SubjectCache;
import org.openmetadata.service.security.saml.JwtTokenCacheManager;
import org.openmetadata.service.util.EmailUtil;
import org.openmetadata.service.util.EntityUtil;
@ -124,7 +125,7 @@ import org.openmetadata.service.util.ResultList;
@Api(value = "User collection", tags = "User collection")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Collection(name = "users", order = 7) // Initialize user resource before bot resource (at default order 9)
@Collection(name = "users", order = 3) // Initialize user resource before bot resource (at default order 9)
public class UserResource extends EntityResource<User, UserRepository> {
public static final String COLLECTION_PATH = "v1/users/";
public static final String USER_PROTECTED_FIELDS = "authenticationMechanism";
@ -157,6 +158,8 @@ public class UserResource extends EntityResource<User, UserRepository> {
this.authenticationConfiguration = config.getAuthenticationConfiguration();
SmtpSettings smtpSettings = config.getSmtpSettings();
this.isEmailServiceEnabled = smtpSettings != null && smtpSettings.getEnableSmtpServer();
this.dao.initializeUsers(config);
SubjectCache.initialize();
}
public static class UserList extends ResultList<User> {

View File

@ -13,171 +13,28 @@
package org.openmetadata.service.security;
import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty;
import static org.openmetadata.schema.auth.SSOAuthMechanism.SsoServiceType.AUTH_0;
import static org.openmetadata.schema.auth.SSOAuthMechanism.SsoServiceType.AZURE;
import static org.openmetadata.schema.auth.SSOAuthMechanism.SsoServiceType.CUSTOM_OIDC;
import static org.openmetadata.schema.auth.SSOAuthMechanism.SsoServiceType.GOOGLE;
import static org.openmetadata.schema.auth.SSOAuthMechanism.SsoServiceType.OKTA;
import static org.openmetadata.schema.entity.teams.AuthenticationMechanism.AuthType.JWT;
import static org.openmetadata.schema.entity.teams.AuthenticationMechanism.AuthType.SSO;
import static org.openmetadata.schema.type.Permission.Access.ALLOW;
import static org.openmetadata.service.Entity.ADMIN_USER_NAME;
import static org.openmetadata.service.exception.CatalogExceptionMessage.notAdmin;
import static org.openmetadata.service.resources.teams.UserResource.USER_PROTECTED_FIELDS;
import at.favre.lib.crypto.bcrypt.BCrypt;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import javax.ws.rs.core.SecurityContext;
import lombok.extern.slf4j.Slf4j;
import org.jdbi.v3.core.Jdbi;
import org.openmetadata.schema.api.configuration.airflow.AirflowConfiguration;
import org.openmetadata.schema.api.security.AuthenticationConfiguration;
import org.openmetadata.schema.auth.BasicAuthMechanism;
import org.openmetadata.schema.auth.JWTAuthMechanism;
import org.openmetadata.schema.auth.JWTTokenExpiry;
import org.openmetadata.schema.auth.SSOAuthMechanism;
import org.openmetadata.schema.email.SmtpSettings;
import org.openmetadata.schema.entity.teams.AuthenticationMechanism;
import org.openmetadata.schema.entity.teams.User;
import org.openmetadata.schema.security.client.OpenMetadataJWTClientConfig;
import org.openmetadata.schema.type.ResourcePermission;
import org.openmetadata.service.Entity;
import org.openmetadata.service.OpenMetadataApplicationConfig;
import org.openmetadata.service.exception.EntityNotFoundException;
import org.openmetadata.service.jdbi3.EntityRepository;
import org.openmetadata.service.jdbi3.UserRepository;
import org.openmetadata.service.security.jwt.JWTTokenGenerator;
import org.openmetadata.service.security.policyevaluator.OperationContext;
import org.openmetadata.service.security.policyevaluator.PolicyCache;
import org.openmetadata.service.security.policyevaluator.PolicyEvaluator;
import org.openmetadata.service.security.policyevaluator.ResourceContextInterface;
import org.openmetadata.service.security.policyevaluator.RoleCache;
import org.openmetadata.service.security.policyevaluator.SubjectCache;
import org.openmetadata.service.security.policyevaluator.SubjectContext;
import org.openmetadata.service.util.EmailUtil;
import org.openmetadata.service.util.EntityUtil;
import org.openmetadata.service.util.PasswordUtil;
import org.openmetadata.service.util.RestUtil;
@Slf4j
public class DefaultAuthorizer implements Authorizer {
private static final String COLON_DELIMITER = ":";
private boolean isSmtpEnabled;
@Override
public void init(OpenMetadataApplicationConfig config, Jdbi dbi) {
LOG.info("Initializing DefaultAuthorizer with config {}", config.getAuthorizerConfiguration());
SmtpSettings smtpSettings = config.getSmtpSettings();
this.isSmtpEnabled = smtpSettings != null && smtpSettings.getEnableSmtpServer();
SubjectCache.initialize();
PolicyCache.initialize();
RoleCache.initialize();
initializeUsers(config);
}
private void initializeUsers(OpenMetadataApplicationConfig config) {
Set<String> adminUsers = new HashSet<>(config.getAuthorizerConfiguration().getAdminPrincipals());
LOG.debug("Checking user entries for admin users {}", adminUsers);
String domain = SecurityUtil.getDomain(config);
String providerType = config.getAuthenticationConfiguration().getProvider();
if (providerType.equals(SSOAuthMechanism.SsoServiceType.BASIC.value())) {
handleBasicAuth(adminUsers, domain);
} else {
addUsers(adminUsers, domain, true);
}
LOG.debug("Checking user entries for test users");
Set<String> testUsers = new HashSet<>(config.getAuthorizerConfiguration().getTestPrincipals());
addUsers(testUsers, domain, null);
}
private void handleBasicAuth(Set<String> adminUsers, String domain) {
try {
for (String adminUser : adminUsers) {
if (adminUser.contains(COLON_DELIMITER)) {
String[] tokens = adminUser.split(COLON_DELIMITER);
addUserForBasicAuth(tokens[0], tokens[1], domain);
} else {
boolean isDefaultAdmin = adminUser.equals(ADMIN_USER_NAME);
String token = PasswordUtil.generateRandomPassword();
if (isDefaultAdmin || !isSmtpEnabled) {
token = ADMIN_USER_NAME;
}
addUserForBasicAuth(adminUser, token, domain);
}
}
} catch (IOException e) {
LOG.error("Failed in Basic Auth Setup. Reason : {}", e.getMessage());
}
}
private void addUserForBasicAuth(String username, String pwd, String domain) throws IOException {
EntityRepository<User> userRepository = Entity.getEntityRepository(Entity.USER);
User originalUser;
try {
List<String> fields = userRepository.getAllowedFieldsCopy();
fields.add(USER_PROTECTED_FIELDS);
originalUser = userRepository.getByName(null, username, new EntityUtil.Fields(fields, String.join(",", fields)));
if (originalUser.getAuthenticationMechanism() == null) {
updateUserWithHashedPwd(originalUser, pwd);
}
addOrUpdateUser(originalUser);
} catch (EntityNotFoundException e) {
// TODO: Not the best way ! :(
User user = user(username, domain, username).withIsAdmin(true).withIsEmailVerified(true);
updateUserWithHashedPwd(user, pwd);
addOrUpdateUser(user);
if (isSmtpEnabled) {
sendInviteMailToAdmin(user, pwd);
}
}
}
private void sendInviteMailToAdmin(User user, String pwd) {
Map<String, String> templatePopulator = new HashMap<>();
templatePopulator.put(EmailUtil.ENTITY, EmailUtil.getInstance().getEmailingEntity());
templatePopulator.put(EmailUtil.SUPPORT_URL, EmailUtil.getInstance().getSupportUrl());
templatePopulator.put(EmailUtil.USERNAME, user.getName());
templatePopulator.put(EmailUtil.PASSWORD, pwd);
templatePopulator.put(EmailUtil.APPLICATION_LOGIN_LINK, EmailUtil.getInstance().getOMUrl());
try {
EmailUtil.getInstance()
.sendMail(
EmailUtil.getInstance().getEmailInviteSubject(),
templatePopulator,
user.getEmail(),
EmailUtil.EMAIL_TEMPLATE_BASEPATH,
EmailUtil.INVITE_RANDOM_PWD);
} catch (Exception ex) {
LOG.error("Failed in sending Mail to user [{}]. Reason : {}", user.getEmail(), ex.getMessage());
}
}
private void updateUserWithHashedPwd(User user, String pwd) {
String hashedPwd = BCrypt.withDefaults().hashToString(12, pwd.toCharArray());
user.setAuthenticationMechanism(
new AuthenticationMechanism()
.withAuthType(AuthenticationMechanism.AuthType.BASIC)
.withConfig(new BasicAuthMechanism().withPassword(hashedPwd)));
}
public static User user(String name, String domain, String updatedBy) {
return new User()
.withId(UUID.randomUUID())
.withName(name)
.withFullyQualifiedName(name)
.withEmail(name + "@" + domain)
.withUpdatedBy(updatedBy)
.withUpdatedAt(System.currentTimeMillis())
.withIsBot(false);
}
@Override
@ -234,130 +91,6 @@ public class DefaultAuthorizer implements Authorizer {
return subjectContext.isAdmin() || subjectContext.isBot();
}
private void addUsers(Set<String> users, String domain, Boolean isAdmin) {
for (String userName : users) {
User user = user(userName, domain, userName).withIsAdmin(isAdmin);
addOrUpdateUser(user);
}
}
private static User addOrUpdateUser(User user) {
EntityRepository<User> userRepository = Entity.getEntityRepository(Entity.USER);
try {
RestUtil.PutResponse<User> addedUser = userRepository.createOrUpdate(null, user);
// should not log the user auth details in LOGS
LOG.debug("Added user entry: {}", addedUser.getEntity().getName());
return addedUser.getEntity();
} catch (Exception exception) {
// In HA set up the other server may have already added the user.
LOG.debug("Caught exception", exception);
user.setAuthenticationMechanism(null);
LOG.debug("User entry: {} already exists.", user.getName());
}
return null;
}
/**
* This method add auth mechanism in the following way:
*
* <ul>
* <li>If original user has already an authMechanism, add it to the user
* <li>Otherwise:
* <ul>
* <li>If airflow configuration is 'openmetadata' and server auth provider is not basic, add JWT auth
* mechanism from Airflow configuration
* <li>Otherwise:
* <ul>
* <li>If airflow configuration is 'basic', add JWT auth mechanism with a generated token which does not
* expire
* <li>Otherwise, add SSO auth mechanism from Airflow configuration
* </ul>
* </ul>
* </ul>
*/
public static User addOrUpdateBotUser(User user, OpenMetadataApplicationConfig openMetadataApplicationConfig) {
User originalUser = retrieveWithAuthMechanism(user);
// the user did not have an auth mechanism
AuthenticationMechanism authMechanism = originalUser != null ? originalUser.getAuthenticationMechanism() : null;
if (authMechanism == null) {
AuthenticationConfiguration authConfig = openMetadataApplicationConfig.getAuthenticationConfiguration();
AirflowConfiguration airflowConfig = openMetadataApplicationConfig.getAirflowConfiguration();
// if the auth provider is "openmetadata" in the configuration set JWT as auth mechanism
if ("openmetadata".equals(airflowConfig.getAuthProvider()) && !"basic".equals(authConfig.getProvider())) {
OpenMetadataJWTClientConfig jwtClientConfig = airflowConfig.getAuthConfig().getOpenmetadata();
authMechanism = buildAuthMechanism(JWT, buildJWTAuthMechanism(jwtClientConfig, user));
} else {
// Otherwise, set auth mechanism from airflow configuration
// TODO: https://github.com/open-metadata/OpenMetadata/issues/7712
if (airflowConfig.getAuthConfig() != null && !"basic".equals(authConfig.getProvider())) {
switch (authConfig.getProvider()) {
case "no-auth":
break;
case "azure":
authMechanism =
buildAuthMechanism(SSO, buildAuthMechanismConfig(AZURE, airflowConfig.getAuthConfig().getAzure()));
break;
case "google":
authMechanism =
buildAuthMechanism(SSO, buildAuthMechanismConfig(GOOGLE, airflowConfig.getAuthConfig().getGoogle()));
break;
case "okta":
authMechanism =
buildAuthMechanism(SSO, buildAuthMechanismConfig(OKTA, airflowConfig.getAuthConfig().getOkta()));
break;
case "auth0":
authMechanism =
buildAuthMechanism(SSO, buildAuthMechanismConfig(AUTH_0, airflowConfig.getAuthConfig().getAuth0()));
break;
case "custom-oidc":
authMechanism =
buildAuthMechanism(
SSO, buildAuthMechanismConfig(CUSTOM_OIDC, airflowConfig.getAuthConfig().getCustomOidc()));
break;
default:
throw new IllegalArgumentException(
String.format(
"Unexpected auth provider [%s] for bot [%s]", authConfig.getProvider(), user.getName()));
}
} else if ("basic".equals(authConfig.getProvider())) {
authMechanism = buildAuthMechanism(JWT, buildJWTAuthMechanism(null, user));
}
}
}
user.setAuthenticationMechanism(authMechanism);
user.setDescription(user.getDescription());
user.setDisplayName(user.getDisplayName());
user.setUpdatedBy(ADMIN_USER_NAME);
return addOrUpdateUser(user);
}
private static JWTAuthMechanism buildJWTAuthMechanism(OpenMetadataJWTClientConfig jwtClientConfig, User user) {
return Objects.isNull(jwtClientConfig) || nullOrEmpty(jwtClientConfig.getJwtToken())
? JWTTokenGenerator.getInstance().generateJWTToken(user, JWTTokenExpiry.Unlimited)
: new JWTAuthMechanism()
.withJWTToken(jwtClientConfig.getJwtToken())
.withJWTTokenExpiry(JWTTokenExpiry.Unlimited);
}
private static SSOAuthMechanism buildAuthMechanismConfig(
SSOAuthMechanism.SsoServiceType ssoServiceType, Object config) {
return new SSOAuthMechanism().withSsoServiceType(ssoServiceType).withAuthConfig(config);
}
private static AuthenticationMechanism buildAuthMechanism(AuthenticationMechanism.AuthType authType, Object config) {
return new AuthenticationMechanism().withAuthType(authType).withConfig(config);
}
private static User retrieveWithAuthMechanism(User user) {
EntityRepository<User> userRepository = UserRepository.class.cast(Entity.getEntityRepository(Entity.USER));
try {
return userRepository.getByName(null, user.getName(), new EntityUtil.Fields(List.of("authenticationMechanism")));
} catch (IOException | EntityNotFoundException e) {
LOG.debug("Bot entity: {} does not exists.", user);
return null;
}
}
public static SubjectContext getSubjectContext(SecurityContext securityContext) {
if (securityContext == null || securityContext.getUserPrincipal() == null) {
throw new AuthenticationException("No principal in security context");

View File

@ -15,11 +15,11 @@ import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.openmetadata.common.utils.CommonUtil;
import org.openmetadata.schema.email.EmailRequest;
import org.openmetadata.schema.email.SmtpSettings;
import org.openmetadata.schema.entity.feed.Thread;
import org.openmetadata.schema.entity.teams.User;
import org.openmetadata.schema.tests.type.TestCaseResult;
import org.openmetadata.service.OpenMetadataApplicationConfig;
import org.simplejavamail.api.email.Email;
import org.simplejavamail.api.email.EmailPopulatingBuilder;
import org.simplejavamail.api.mailer.Mailer;
@ -58,22 +58,35 @@ public class EmailUtil {
public static final String INVITE_CREATE_PWD = "invite-createPassword.ftl";
public static final String TASK_NOTIFICATION_TEMPLATE = "taskAssignment.ftl";
public static final String TEST_NOTIFICATION_TEMPLATE = "testResultStatus.ftl";
private static EmailUtil INSTANCE = null;
private SmtpSettings defaultSmtpSettings = null;
private Mailer mailer = null;
private Configuration templateConfiguration = null;
private static EmailUtil INSTANCE;
private static SmtpSettings DEFAULT_SMTP_SETTINGS;
private static Mailer MAILER;
private static Configuration TEMPLATE_CONFIGURATION;
private static volatile boolean INITIALIZED = false;
private EmailUtil(SmtpSettings smtpServerSettings) {
try {
this.defaultSmtpSettings = smtpServerSettings;
this.mailer = this.createMailer(smtpServerSettings);
this.templateConfiguration = new Configuration(VERSION_2_3_28);
} catch (Exception ex) {
LOG.warn("[MAILER] Smtp Configurations are missing : Reason {} ", ex.getMessage(), ex);
public static void initialize(OpenMetadataApplicationConfig config) {
if (!INITIALIZED) {
if (config.getSmtpSettings() != null && config.getSmtpSettings().getEnableSmtpServer()) {
try {
DEFAULT_SMTP_SETTINGS = config.getSmtpSettings();
MAILER = createMailer(DEFAULT_SMTP_SETTINGS);
TEMPLATE_CONFIGURATION = new Configuration(VERSION_2_3_28);
LOG.info("Email Util cache is initialized");
} catch (Exception ex) {
LOG.warn("[MAILER] Smtp Configurations are missing : Reason {} ", ex.getMessage(), ex);
}
} else {
DEFAULT_SMTP_SETTINGS = new SmtpSettings();
}
INSTANCE = new EmailUtil();
INITIALIZED = true;
} else {
INITIALIZED = false;
LOG.info("Email Util is already initialized");
}
}
private Mailer createMailer(SmtpSettings smtpServerSettings) {
private static Mailer createMailer(SmtpSettings smtpServerSettings) {
TransportStrategy strategy;
switch (smtpServerSettings.getTransportationStrategy()) {
case SMPTS:
@ -100,10 +113,10 @@ public class EmailUtil {
}
public void sendAccountStatus(User user, String action, String status) throws IOException, TemplateException {
if (defaultSmtpSettings.getEnableSmtpServer()) {
if (DEFAULT_SMTP_SETTINGS.getEnableSmtpServer()) {
Map<String, String> templatePopulator = new HashMap<>();
templatePopulator.put(ENTITY, EmailUtil.getInstance().getEmailingEntity());
templatePopulator.put(SUPPORT_URL, EmailUtil.getInstance().getSupportUrl());
templatePopulator.put(ENTITY, getEmailingEntity());
templatePopulator.put(SUPPORT_URL, getSupportUrl());
templatePopulator.put(USERNAME, user.getName());
templatePopulator.put(ACTION_KEY, action);
templatePopulator.put(ACTION_STATUS_KEY, status);
@ -117,7 +130,7 @@ public class EmailUtil {
}
public void sendEmailVerification(String emailVerificationLink, User user) throws IOException, TemplateException {
if (defaultSmtpSettings.getEnableSmtpServer()) {
if (DEFAULT_SMTP_SETTINGS.getEnableSmtpServer()) {
Map<String, String> templatePopulator = new HashMap<>();
templatePopulator.put(ENTITY, getEmailingEntity());
templatePopulator.put(SUPPORT_URL, getSupportUrl());
@ -135,7 +148,7 @@ public class EmailUtil {
public void sendPasswordResetLink(String passwordResetLink, User user, String subject, String templateFilePath)
throws IOException, TemplateException {
if (defaultSmtpSettings.getEnableSmtpServer()) {
if (DEFAULT_SMTP_SETTINGS.getEnableSmtpServer()) {
Map<String, String> templatePopulator = new HashMap<>();
templatePopulator.put(ENTITY, getEmailingEntity());
templatePopulator.put(SUPPORT_URL, getSupportUrl());
@ -150,7 +163,7 @@ public class EmailUtil {
public void sendTaskAssignmentNotificationToUser(
String assigneeName, String email, String taskLink, Thread thread, String subject, String templateFilePath)
throws IOException, TemplateException {
if (defaultSmtpSettings.getEnableSmtpServer()) {
if (DEFAULT_SMTP_SETTINGS.getEnableSmtpServer()) {
Map<String, String> templatePopulator = new HashMap<>();
templatePopulator.put("assignee", assigneeName);
templatePopulator.put("createdBy", thread.getCreatedBy());
@ -173,7 +186,7 @@ public class EmailUtil {
String subject,
String templateFilePath)
throws IOException, TemplateException {
if (defaultSmtpSettings.getEnableSmtpServer()) {
if (DEFAULT_SMTP_SETTINGS.getEnableSmtpServer()) {
Map<String, String> templatePopulator = new HashMap<>();
templatePopulator.put("receiverName", email.split("@")[0]);
templatePopulator.put("testResultName", testCaseName);
@ -186,84 +199,17 @@ public class EmailUtil {
}
}
public Email buildEmailWithDefaultSender(EmailRequest request) {
EmailPopulatingBuilder emailBuilder = EmailBuilder.startingBlank();
if (request.getRecipientMails() != null
&& request.getRecipientMails().size() != 0
&& request.getSubject() != null
&& !request.getSubject().equals("")) {
// Sender Details
emailBuilder.from(defaultSmtpSettings.getSenderMail());
// Recipient
request
.getRecipientMails()
.forEach(
(pair) -> {
if (pair.getName() != null && !pair.getName().equals("")) {
emailBuilder.to(pair.getName(), pair.getEmail());
} else {
emailBuilder.to(pair.getEmail());
}
});
// CC
if (request.getCcMails() != null) {
request
.getCcMails()
.forEach(
(pair) -> {
if (pair.getName() != null && !pair.getName().equals("")) {
emailBuilder.cc(pair.getName(), pair.getEmail());
} else {
emailBuilder.cc(pair.getEmail());
}
});
}
// BCC
if (request.getBccMails() != null) {
request
.getBccMails()
.forEach(
(pair) -> {
if (pair.getName() != null && !pair.getName().equals("")) {
emailBuilder.bcc(pair.getName(), pair.getEmail());
} else {
emailBuilder.bcc(pair.getEmail());
}
});
}
// Subject
if (request.getSubject() != null) {
emailBuilder.withSubject(request.getSubject());
}
if (request.getContent() != null) {
if (request.getContentType() == EmailRequest.ContentType.HTML) {
emailBuilder.withHTMLText(request.getContent());
} else {
emailBuilder.withPlainText(request.getContent());
}
}
} else {
throw new RuntimeException("Email Request is missing Required Details");
}
return emailBuilder.buildEmail();
}
public void sendMail(
String subject, Map<String, String> model, String to, String baseTemplatePackage, String templatePath)
throws IOException, TemplateException {
if (defaultSmtpSettings.getEnableSmtpServer()) {
if (DEFAULT_SMTP_SETTINGS.getEnableSmtpServer()) {
EmailPopulatingBuilder emailBuilder = EmailBuilder.startingBlank();
emailBuilder.withSubject(subject);
emailBuilder.to(to);
emailBuilder.from(defaultSmtpSettings.getSenderMail());
emailBuilder.from(DEFAULT_SMTP_SETTINGS.getSenderMail());
templateConfiguration.setClassForTemplateLoading(getClass(), baseTemplatePackage);
Template template = templateConfiguration.getTemplate(templatePath);
TEMPLATE_CONFIGURATION.setClassForTemplateLoading(getClass(), baseTemplatePackage);
Template template = TEMPLATE_CONFIGURATION.getTemplate(templatePath);
// write the freemarker output to a StringWriter
StringWriter stringWriter = new StringWriter();
@ -275,17 +221,17 @@ public class EmailUtil {
}
public void sendMail(Email email) {
if (mailer != null && defaultSmtpSettings.getEnableSmtpServer()) {
mailer.sendMail(email, true);
if (MAILER != null && DEFAULT_SMTP_SETTINGS.getEnableSmtpServer()) {
MAILER.sendMail(email, true);
}
}
public String buildBaseUrl(URI uri) {
try {
if (CommonUtil.nullOrEmpty(this.defaultSmtpSettings.getOpenMetadataUrl())) {
if (CommonUtil.nullOrEmpty(DEFAULT_SMTP_SETTINGS.getOpenMetadataUrl())) {
return String.format("%s://%s", uri.getScheme(), uri.getHost());
} else {
URI serverUrl = new URI(this.defaultSmtpSettings.getOpenMetadataUrl());
URI serverUrl = new URI(DEFAULT_SMTP_SETTINGS.getOpenMetadataUrl());
return String.format("%s://%s", serverUrl.getScheme(), serverUrl.getHost());
}
} catch (Exception ex) {
@ -293,53 +239,65 @@ public class EmailUtil {
}
}
public void testConnection() {
mailer.testConnection();
}
public static class EmailUtilBuilder {
private EmailUtilBuilder() {}
public static void build(SmtpSettings smtpServerSettings) {
if (INSTANCE == null) {
INSTANCE = new EmailUtil(smtpServerSettings);
public static void sendInviteMailToAdmin(User user, String pwd) {
if (DEFAULT_SMTP_SETTINGS.getEnableSmtpServer()) {
Map<String, String> templatePopulator = new HashMap<>();
templatePopulator.put(EmailUtil.ENTITY, EmailUtil.getInstance().getEmailingEntity());
templatePopulator.put(EmailUtil.SUPPORT_URL, EmailUtil.getInstance().getSupportUrl());
templatePopulator.put(EmailUtil.USERNAME, user.getName());
templatePopulator.put(EmailUtil.PASSWORD, pwd);
templatePopulator.put(EmailUtil.APPLICATION_LOGIN_LINK, EmailUtil.getInstance().getOMUrl());
try {
EmailUtil.getInstance()
.sendMail(
EmailUtil.getInstance().getEmailInviteSubject(),
templatePopulator,
user.getEmail(),
EmailUtil.EMAIL_TEMPLATE_BASEPATH,
EmailUtil.INVITE_RANDOM_PWD);
} catch (Exception ex) {
LOG.error("Failed in sending Mail to user [{}]. Reason : {}", user.getEmail(), ex.getMessage());
}
}
}
public String getEmailVerificationSubject() {
return String.format(EMAIL_VERIFICATION_SUBJECT, defaultSmtpSettings.getEmailingEntity());
public void testConnection() {
MAILER.testConnection();
}
private String getEmailVerificationSubject() {
return String.format(EMAIL_VERIFICATION_SUBJECT, DEFAULT_SMTP_SETTINGS.getEmailingEntity());
}
public String getPasswordResetSubject() {
return String.format(PASSWORD_RESET_SUBJECT, defaultSmtpSettings.getEmailingEntity());
return String.format(PASSWORD_RESET_SUBJECT, DEFAULT_SMTP_SETTINGS.getEmailingEntity());
}
public String getAccountStatusChangeSubject() {
return String.format(ACCOUNT_STATUS_SUBJECT, defaultSmtpSettings.getEmailingEntity());
private String getAccountStatusChangeSubject() {
return String.format(ACCOUNT_STATUS_SUBJECT, DEFAULT_SMTP_SETTINGS.getEmailingEntity());
}
public String getEmailInviteSubject() {
return String.format(INVITE_SUBJECT, defaultSmtpSettings.getEmailingEntity());
return String.format(INVITE_SUBJECT, DEFAULT_SMTP_SETTINGS.getEmailingEntity());
}
public String getTaskAssignmentSubject() {
return String.format(TASK_SUBJECT, defaultSmtpSettings.getEmailingEntity());
return String.format(TASK_SUBJECT, DEFAULT_SMTP_SETTINGS.getEmailingEntity());
}
public String getTestResultSubject() {
return String.format(TEST_SUBJECT, defaultSmtpSettings.getEmailingEntity());
return String.format(TEST_SUBJECT, DEFAULT_SMTP_SETTINGS.getEmailingEntity());
}
public String getEmailingEntity() {
return defaultSmtpSettings.getEmailingEntity();
return DEFAULT_SMTP_SETTINGS.getEmailingEntity();
}
public String getSupportUrl() {
return defaultSmtpSettings.getSupportUrl();
return DEFAULT_SMTP_SETTINGS.getSupportUrl();
}
public String getOMUrl() {
return defaultSmtpSettings.getOpenMetadataUrl();
return DEFAULT_SMTP_SETTINGS.getOpenMetadataUrl();
}
}

View File

@ -0,0 +1,223 @@
package org.openmetadata.service.util;
import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty;
import static org.openmetadata.schema.auth.SSOAuthMechanism.SsoServiceType.AUTH_0;
import static org.openmetadata.schema.auth.SSOAuthMechanism.SsoServiceType.AZURE;
import static org.openmetadata.schema.auth.SSOAuthMechanism.SsoServiceType.CUSTOM_OIDC;
import static org.openmetadata.schema.auth.SSOAuthMechanism.SsoServiceType.GOOGLE;
import static org.openmetadata.schema.auth.SSOAuthMechanism.SsoServiceType.OKTA;
import static org.openmetadata.schema.entity.teams.AuthenticationMechanism.AuthType.JWT;
import static org.openmetadata.schema.entity.teams.AuthenticationMechanism.AuthType.SSO;
import static org.openmetadata.service.Entity.ADMIN_USER_NAME;
import static org.openmetadata.service.resources.teams.UserResource.USER_PROTECTED_FIELDS;
import at.favre.lib.crypto.bcrypt.BCrypt;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.openmetadata.schema.api.configuration.airflow.AirflowConfiguration;
import org.openmetadata.schema.api.security.AuthenticationConfiguration;
import org.openmetadata.schema.auth.BasicAuthMechanism;
import org.openmetadata.schema.auth.JWTAuthMechanism;
import org.openmetadata.schema.auth.JWTTokenExpiry;
import org.openmetadata.schema.auth.SSOAuthMechanism;
import org.openmetadata.schema.entity.teams.AuthenticationMechanism;
import org.openmetadata.schema.entity.teams.User;
import org.openmetadata.schema.security.client.OpenMetadataJWTClientConfig;
import org.openmetadata.service.Entity;
import org.openmetadata.service.OpenMetadataApplicationConfig;
import org.openmetadata.service.exception.EntityNotFoundException;
import org.openmetadata.service.jdbi3.EntityRepository;
import org.openmetadata.service.jdbi3.UserRepository;
import org.openmetadata.service.security.jwt.JWTTokenGenerator;
@Slf4j
public final class UserUtil {
private static final String COLON_DELIMITER = ":";
public static void handleBasicAuth(Set<String> adminUsers, String domain) {
try {
for (String adminUser : adminUsers) {
if (adminUser.contains(COLON_DELIMITER)) {
String[] tokens = adminUser.split(COLON_DELIMITER);
addUserForBasicAuth(tokens[0], tokens[1], domain);
} else {
boolean isDefaultAdmin = adminUser.equals(ADMIN_USER_NAME);
String token = PasswordUtil.generateRandomPassword();
if (isDefaultAdmin) {
token = ADMIN_USER_NAME;
}
addUserForBasicAuth(adminUser, token, domain);
}
}
} catch (IOException e) {
LOG.error("Failed in Basic Auth Setup. Reason : {}", e.getMessage());
}
}
public static void addUserForBasicAuth(String username, String pwd, String domain) throws IOException {
EntityRepository<User> userRepository = Entity.getEntityRepository(Entity.USER);
User originalUser;
try {
List<String> fields = userRepository.getAllowedFieldsCopy();
fields.add(USER_PROTECTED_FIELDS);
originalUser = userRepository.getByName(null, username, new EntityUtil.Fields(fields, String.join(",", fields)));
if (originalUser.getAuthenticationMechanism() == null) {
updateUserWithHashedPwd(originalUser, pwd);
}
addOrUpdateUser(originalUser);
} catch (EntityNotFoundException e) {
// TODO: Not the best way ! :(
User user = user(username, domain, username).withIsAdmin(true).withIsEmailVerified(true);
updateUserWithHashedPwd(user, pwd);
addOrUpdateUser(user);
EmailUtil.sendInviteMailToAdmin(user, pwd);
}
}
public static void updateUserWithHashedPwd(User user, String pwd) {
String hashedPwd = BCrypt.withDefaults().hashToString(12, pwd.toCharArray());
user.setAuthenticationMechanism(
new AuthenticationMechanism()
.withAuthType(AuthenticationMechanism.AuthType.BASIC)
.withConfig(new BasicAuthMechanism().withPassword(hashedPwd)));
}
public static void addUsers(Set<String> users, String domain, Boolean isAdmin) {
for (String userName : users) {
User user = user(userName, domain, userName).withIsAdmin(isAdmin);
addOrUpdateUser(user);
}
}
public static User addOrUpdateUser(User user) {
EntityRepository<User> userRepository = Entity.getEntityRepository(Entity.USER);
try {
RestUtil.PutResponse<User> addedUser = userRepository.createOrUpdate(null, user);
// should not log the user auth details in LOGS
LOG.debug("Added user entry: {}", addedUser.getEntity().getName());
return addedUser.getEntity();
} catch (Exception exception) {
// In HA set up the other server may have already added the user.
LOG.debug("Caught exception", exception);
user.setAuthenticationMechanism(null);
LOG.debug("User entry: {} already exists.", user.getName());
}
return null;
}
public static User user(String name, String domain, String updatedBy) {
return new User()
.withId(UUID.randomUUID())
.withName(name)
.withFullyQualifiedName(name)
.withEmail(name + "@" + domain)
.withUpdatedBy(updatedBy)
.withUpdatedAt(System.currentTimeMillis())
.withIsBot(false);
}
/**
* This method add auth mechanism in the following way:
*
* <ul>
* <li>If original user has already an authMechanism, add it to the user
* <li>Otherwise:
* <ul>
* <li>If airflow configuration is 'openmetadata' and server auth provider is not basic, add JWT auth
* mechanism from Airflow configuration
* <li>Otherwise:
* <ul>
* <li>If airflow configuration is 'basic', add JWT auth mechanism with a generated token which does not
* expire
* <li>Otherwise, add SSO auth mechanism from Airflow configuration
* </ul>
* </ul>
* </ul>
*/
public static User addOrUpdateBotUser(User user, OpenMetadataApplicationConfig openMetadataApplicationConfig) {
User originalUser = retrieveWithAuthMechanism(user);
// the user did not have an auth mechanism
AuthenticationMechanism authMechanism = originalUser != null ? originalUser.getAuthenticationMechanism() : null;
if (authMechanism == null) {
AuthenticationConfiguration authConfig = openMetadataApplicationConfig.getAuthenticationConfiguration();
AirflowConfiguration airflowConfig = openMetadataApplicationConfig.getAirflowConfiguration();
// if the auth provider is "openmetadata" in the configuration set JWT as auth mechanism
if ("openmetadata".equals(airflowConfig.getAuthProvider()) && !"basic".equals(authConfig.getProvider())) {
OpenMetadataJWTClientConfig jwtClientConfig = airflowConfig.getAuthConfig().getOpenmetadata();
authMechanism = buildAuthMechanism(JWT, buildJWTAuthMechanism(jwtClientConfig, user));
} else {
// Otherwise, set auth mechanism from airflow configuration
// TODO: https://github.com/open-metadata/OpenMetadata/issues/7712
if (airflowConfig.getAuthConfig() != null && !"basic".equals(authConfig.getProvider())) {
switch (authConfig.getProvider()) {
case "no-auth":
break;
case "azure":
authMechanism =
buildAuthMechanism(SSO, buildAuthMechanismConfig(AZURE, airflowConfig.getAuthConfig().getAzure()));
break;
case "google":
authMechanism =
buildAuthMechanism(SSO, buildAuthMechanismConfig(GOOGLE, airflowConfig.getAuthConfig().getGoogle()));
break;
case "okta":
authMechanism =
buildAuthMechanism(SSO, buildAuthMechanismConfig(OKTA, airflowConfig.getAuthConfig().getOkta()));
break;
case "auth0":
authMechanism =
buildAuthMechanism(SSO, buildAuthMechanismConfig(AUTH_0, airflowConfig.getAuthConfig().getAuth0()));
break;
case "custom-oidc":
authMechanism =
buildAuthMechanism(
SSO, buildAuthMechanismConfig(CUSTOM_OIDC, airflowConfig.getAuthConfig().getCustomOidc()));
break;
default:
throw new IllegalArgumentException(
String.format(
"Unexpected auth provider [%s] for bot [%s]", authConfig.getProvider(), user.getName()));
}
} else if ("basic".equals(authConfig.getProvider())) {
authMechanism = buildAuthMechanism(JWT, buildJWTAuthMechanism(null, user));
}
}
}
user.setAuthenticationMechanism(authMechanism);
user.setDescription(user.getDescription());
user.setDisplayName(user.getDisplayName());
user.setUpdatedBy(ADMIN_USER_NAME);
return UserUtil.addOrUpdateUser(user);
}
private static JWTAuthMechanism buildJWTAuthMechanism(OpenMetadataJWTClientConfig jwtClientConfig, User user) {
return Objects.isNull(jwtClientConfig) || nullOrEmpty(jwtClientConfig.getJwtToken())
? JWTTokenGenerator.getInstance().generateJWTToken(user, JWTTokenExpiry.Unlimited)
: new JWTAuthMechanism()
.withJWTToken(jwtClientConfig.getJwtToken())
.withJWTTokenExpiry(JWTTokenExpiry.Unlimited);
}
private static SSOAuthMechanism buildAuthMechanismConfig(
SSOAuthMechanism.SsoServiceType ssoServiceType, Object config) {
return new SSOAuthMechanism().withSsoServiceType(ssoServiceType).withAuthConfig(config);
}
private static AuthenticationMechanism buildAuthMechanism(AuthenticationMechanism.AuthType authType, Object config) {
return new AuthenticationMechanism().withAuthType(authType).withConfig(config);
}
private static User retrieveWithAuthMechanism(User user) {
EntityRepository<User> userRepository = UserRepository.class.cast(Entity.getEntityRepository(Entity.USER));
try {
return userRepository.getByName(null, user.getName(), new EntityUtil.Fields(List.of("authenticationMechanism")));
} catch (IOException | EntityNotFoundException e) {
LOG.debug("Bot entity: {} does not exists.", user);
return null;
}
}
}