Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Add admin email to config DB #36596

Closed
wants to merge 8 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ public Long getCurrentTimeInstantEpochMilli() {
return Instant.now().toEpochMilli();
}

@Deprecated(forRemoval = true)
public String getAdminEmailDomainHash() {
if (StringUtils.hasLength(adminEmailDomainHash)) {
return adminEmailDomainHash;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,5 @@ public class FieldNameCE {
public static final String ARTIFACT_CONTEXT = "artifactContext";
public static final String ARTIFACT_ID = "artifactId";
public static final String BODY = "body";
public static final String INSTANCE_ADMIN_CONFIG = "instanceAdminConfig";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.appsmith.server.dtos;

import com.appsmith.server.helpers.HashUtils;
import lombok.Data;
import net.minidev.json.JSONObject;

import java.util.Optional;

@Data
public class InstanceAdminMetaDTO {
String email;
String emailDomainHash;

static final String EMAIL = "email";
static final String EMAIL_DOMAIN_HASH = "emailDomainHash";

/**
* Converts an email address to a JSONObject containing the email and its domain hash.
* @param email The email address to convert
* @return A JSONObject containing the email and its domain hash
*/
public static JSONObject toJsonObject(String email) {
email = email == null ? "" : email;
JSONObject jsonObject = new JSONObject();
jsonObject.put(EMAIL, email);
jsonObject.put(EMAIL_DOMAIN_HASH, HashUtils.getEmailDomainHash(email));
return jsonObject;
}

/**
* Creates an InstanceAdminMetaDTO from a JSONObject.
* @param jsonObject The JSONObject containing email and emailDomainHash
* @return An InstanceAdminMetaDTO populated with data from the JSONObject
*/
public static InstanceAdminMetaDTO fromJsonObject(JSONObject jsonObject) {
if (jsonObject == null) {
return new InstanceAdminMetaDTO();
}
InstanceAdminMetaDTO instanceAdminMetaDTO = new InstanceAdminMetaDTO();
instanceAdminMetaDTO.setEmail(
Optional.ofNullable(jsonObject.get(EMAIL)).map(Object::toString).orElse(""));
instanceAdminMetaDTO.setEmailDomainHash(Optional.ofNullable(jsonObject.get(EMAIL_DOMAIN_HASH))
.map(Object::toString)
.orElse(""));
return instanceAdminMetaDTO;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.appsmith.server.helpers;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;

public class HashUtils {

public static String hash(String value) {
return StringUtils.isEmpty(value) ? "" : DigestUtils.sha256Hex(value);
}

public static String getEmailDomainHash(String email) {
if (email == null) {
return "";
}

return hash(email.contains("@") ? email.split("@", 2)[1] : "");
}
abhvsn marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving these methods from AnalyticsService to helper class to be reusable.

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.appsmith.server.domains.UsagePulse;
import com.appsmith.server.domains.User;
import com.appsmith.server.domains.Workspace;
import com.appsmith.server.dtos.InstanceAdminMetaDTO;
import com.appsmith.server.dtos.Permission;
import com.appsmith.server.helpers.TextUtils;
import com.appsmith.server.migrations.solutions.UpdateSuperUserMigrationHelper;
Expand All @@ -45,6 +46,7 @@
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.nio.charset.Charset;
Expand All @@ -65,12 +67,14 @@
import static com.appsmith.server.constants.EnvVariables.APPSMITH_ADMIN_EMAILS;
import static com.appsmith.server.constants.FieldName.DEFAULT_PERMISSION_GROUP;
import static com.appsmith.server.constants.FieldName.PERMISSION_GROUP_ID;
import static com.appsmith.server.constants.ce.FieldNameCE.INSTANCE_ADMIN_CONFIG;
import static com.appsmith.server.helpers.CollectionUtils.findSymmetricDiff;
import static com.appsmith.server.migrations.DatabaseChangelog1.dropIndexIfExists;
import static com.appsmith.server.migrations.DatabaseChangelog1.ensureIndexes;
import static com.appsmith.server.migrations.DatabaseChangelog1.installPluginToAllWorkspaces;
import static com.appsmith.server.migrations.DatabaseChangelog1.makeIndex;
import static com.appsmith.server.migrations.MigrationHelperMethods.evictPermissionCacheForUsers;
import static com.appsmith.server.migrations.db.ce.Migration064AddInstanceAdminDetailsToDB.verifyIfInstanceAdminDetailsArePresent;
import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Query.query;
import static org.springframework.data.mongodb.core.query.Update.update;
Expand Down Expand Up @@ -503,6 +507,21 @@ public void updateSuperUsers(

Update update = new Update().set(PermissionGroup.Fields.assignedToUserIds, userIds);
mongoTemplate.updateFirst(permissionGroupQuery, update, PermissionGroup.class);

// Check if instance admin details are already present in the DB
if (verifyIfInstanceAdminDetailsArePresent(mongoTemplate)) {
return;
}
String adminEmail = adminEmails.stream()
.filter(email -> email != null && email.contains("@"))
.findFirst()
.orElse(null);
abhvsn marked this conversation as resolved.
Show resolved Hide resolved
Config config = new Config();
config.setName(INSTANCE_ADMIN_CONFIG);
if (StringUtils.hasLength(adminEmail)) {
config.setConfig(InstanceAdminMetaDTO.toJsonObject(adminEmail));
mongoTemplate.save(config);
}
}

@ChangeSet(order = "034", id = "update-bad-theme-state", author = "")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ public class FieldName {
public static final String DELETED_AT = "deletedAt";
public static final String TENANT_ID = "tenantId";
public static final String PERMISSION_GROUPS = "permissionGroups";
public static final String DEFAULT_CLOUD_ADMIN_EMAIL = "[email protected]";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.appsmith.server.migrations.db.ce;

import com.appsmith.server.configurations.CommonConfig;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Config;
import com.appsmith.server.domains.PermissionGroup;
import com.appsmith.server.domains.User;
import com.appsmith.server.dtos.InstanceAdminMetaDTO;
import com.appsmith.server.helpers.CollectionUtils;
import io.mongock.api.annotations.ChangeUnit;
import io.mongock.api.annotations.Execution;
import io.mongock.api.annotations.RollbackExecution;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;

import static com.appsmith.server.constants.ce.FieldNameCE.INSTANCE_ADMIN_CONFIG;
import static com.appsmith.server.helpers.ce.bridge.BridgeQuery.where;
import static com.appsmith.server.migrations.constants.FieldName.DEFAULT_CLOUD_ADMIN_EMAIL;
import static com.appsmith.server.repositories.ce.BaseAppsmithRepositoryCEImpl.notDeleted;

@RequiredArgsConstructor
@Slf4j
@ChangeUnit(order = "064", id = "add_instance_admin_details_to_config_collection")
public class Migration064AddInstanceAdminDetailsToDB {

private final MongoTemplate mongoTemplate;
private final CommonConfig commonConfig;

@RollbackExecution
public void rollbackExecution() {}

@Execution
public void executeMigration() {
// Add instance admin details to the DB
// This migration is idempotent and can be run multiple times without any side effects
log.info("Adding instance admin details to the DB");
// Check if instance admin details are already present in the DB to make the migration idempotent
if (verifyIfInstanceAdminDetailsArePresent(mongoTemplate)) {
return;
}

// Add instance admin details to the DB
Query instanceAdminRoleQuery = new Query()
.addCriteria(where(FieldName.NAME).is(FieldName.INSTANCE_ADMIN_ROLE))
.addCriteria(notDeleted());
PermissionGroup instanceAdminPG = mongoTemplate.findOne(instanceAdminRoleQuery, PermissionGroup.class);
if (instanceAdminPG == null) {
log.error("Instance admin permission group not found in the DB. Skipping migration 064");
return;
}
String adminEmail = null;
if (commonConfig.isCloudHosting()) {
// As a fallback, use the default admin email for cloud hosting
adminEmail = DEFAULT_CLOUD_ADMIN_EMAIL;
} else if (!CollectionUtils.isNullOrEmpty(instanceAdminPG.getAssignedToUserIds())) {
adminEmail = Flux.fromIterable(instanceAdminPG.getAssignedToUserIds())
.map(userId -> {
User user = mongoTemplate.findOne(
new Query()
.addCriteria(where(FieldName.ID).is(userId))
.addCriteria(notDeleted()),
User.class);
return user != null ? user.getEmail() : "";
})
.filter(email -> email != null && email.contains("@"))
.blockFirst();
}

if (!StringUtils.hasLength(adminEmail)) {
adminEmail = commonConfig.getAdminEmails().stream()
.filter(email -> email != null && email.contains("@"))
.findFirst()
.orElse(null);
}
Config config = new Config();
config.setName(INSTANCE_ADMIN_CONFIG);
if (StringUtils.hasLength(adminEmail)) {
config.setConfig(InstanceAdminMetaDTO.toJsonObject(adminEmail));
mongoTemplate.save(config);
}
}

public static boolean verifyIfInstanceAdminDetailsArePresent(MongoTemplate mongoTemplate) {
Query instanceAdminConfigQuery = new Query()
.addCriteria(where(FieldName.NAME).is(FieldName.INSTANCE_ADMIN_CONFIG))
.addCriteria(notDeleted());
Config instanceAdminConfig = mongoTemplate.findOne(instanceAdminConfigQuery, Config.class);
boolean adminDetailsPresent = instanceAdminConfig != null
&& StringUtils.hasLength(InstanceAdminMetaDTO.fromJsonObject(instanceAdminConfig.getConfig())
.getEmail());
if (adminDetailsPresent) {
log.info("Instance admin details already present in the DB. Skipping migration 64");
}
return adminDetailsPresent;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.appsmith.server.services;

import com.appsmith.server.configurations.CommonConfig;
import com.appsmith.server.helpers.FeatureFlagMigrationHelper;
import com.appsmith.server.repositories.CacheableRepositoryHelper;
import com.appsmith.server.repositories.TenantRepository;
Expand All @@ -22,7 +21,6 @@ public TenantServiceImpl(
@Lazy EnvManager envManager,
FeatureFlagMigrationHelper featureFlagMigrationHelper,
CacheableRepositoryHelper cacheableRepositoryHelper,
CommonConfig commonConfig,
ObservationRegistry observationRegistry) {
super(
validator,
Expand All @@ -32,7 +30,6 @@ public TenantServiceImpl(
envManager,
featureFlagMigrationHelper,
cacheableRepositoryHelper,
commonConfig,
observationRegistry);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
import com.appsmith.server.configurations.DeploymentProperties;
import com.appsmith.server.configurations.ProjectProperties;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Config;
import com.appsmith.server.domains.NewPage;
import com.appsmith.server.domains.User;
import com.appsmith.server.domains.UserData;
import com.appsmith.server.dtos.InstanceAdminMetaDTO;
import com.appsmith.server.helpers.ExchangeUtils;
import com.appsmith.server.helpers.UserUtils;
import com.appsmith.server.repositories.UserDataRepository;
Expand All @@ -20,7 +22,6 @@
import com.segment.analytics.messages.IdentifyMessage;
import com.segment.analytics.messages.TrackMessage;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -37,9 +38,12 @@
import static com.appsmith.external.constants.AnalyticsConstants.IP;
import static com.appsmith.external.constants.AnalyticsConstants.IP_ADDRESS;
import static com.appsmith.server.constants.ce.FieldNameCE.EMAIL;
import static com.appsmith.server.constants.ce.FieldNameCE.INSTANCE_ADMIN_CONFIG;
import static com.appsmith.server.constants.ce.FieldNameCE.NAME;
import static com.appsmith.server.constants.ce.FieldNameCE.PROFICIENCY;
import static com.appsmith.server.constants.ce.FieldNameCE.ROLE;
import static com.appsmith.server.helpers.HashUtils.getEmailDomainHash;
import static com.appsmith.server.helpers.HashUtils.hash;

@Slf4j
public class AnalyticsServiceCEImpl implements AnalyticsServiceCE {
Expand All @@ -56,6 +60,8 @@ public class AnalyticsServiceCEImpl implements AnalyticsServiceCE {

private final UserDataRepository userDataRepository;

private static String adminEmailDomainHash;

@Autowired
public AnalyticsServiceCEImpl(
@Autowired(required = false) Analytics analytics,
Expand All @@ -80,18 +86,6 @@ public boolean isActive() {
return analytics != null;
}

private String hash(String value) {
return StringUtils.isEmpty(value) ? "" : DigestUtils.sha256Hex(value);
}

private String getEmailDomainHash(String email) {
if (email == null) {
return "";
}

return hash(email.contains("@") ? email.split("@", 2)[1] : "");
}

@Override
public Mono<User> identifyUser(User user, UserData userData) {
return identifyUser(user, userData, null);
Expand Down Expand Up @@ -230,14 +224,31 @@ public Mono<Void> sendEvent(String event, String userId, Map<String, ?> properti

final String finalUserId = userId;

Mono<String> instanceAdminEmailDomainMono = adminEmailDomainHash != null
? Mono.just(adminEmailDomainHash)
: configService
.getByName(INSTANCE_ADMIN_CONFIG)
.onErrorResume(e -> Mono.empty())
.switchIfEmpty(Mono.just(new Config()))
.map(config -> {
if (config.getConfig() == null) {
return "";
}
adminEmailDomainHash = InstanceAdminMetaDTO.fromJsonObject(config.getConfig())
.getEmailDomainHash();
return adminEmailDomainHash;
});

return Mono.zip(
ExchangeUtils.getAnonymousUserIdFromCurrentRequest(),
ExchangeUtils.getUserAgentFromCurrentRequest(),
configService.getInstanceId().defaultIfEmpty("unknown-instance-id"))
configService.getInstanceId().defaultIfEmpty("unknown-instance-id"),
instanceAdminEmailDomainMono)
.map(tuple -> {
final String userIdFromClient = tuple.getT1();
final String userAgent = tuple.getT2();
final String instanceId = tuple.getT3();
final String instanceAdminDomainHash = ObjectUtils.defaultIfNull(tuple.getT4(), "");
String userIdToSend = finalUserId;
if (FieldName.ANONYMOUS_USER.equals(finalUserId)) {
userIdToSend = StringUtils.defaultIfEmpty(userIdFromClient, FieldName.ANONYMOUS_USER);
Expand All @@ -259,7 +270,7 @@ public Mono<Void> sendEvent(String event, String userId, Map<String, ?> properti
analyticsProperties.put(ADMIN_EMAIL_DOMAIN_HASH, domainHash);
} else {
analyticsProperties.put(EMAIL_DOMAIN_HASH, emailDomainHash);
analyticsProperties.put(ADMIN_EMAIL_DOMAIN_HASH, commonConfig.getAdminEmailDomainHash());
analyticsProperties.put(ADMIN_EMAIL_DOMAIN_HASH, instanceAdminDomainHash);
}
analyticsProperties.put("originService", "appsmith-server");
analyticsProperties.put("instanceId", instanceId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,8 @@ public Mono<ConsolidatedAPIResponseDTO> getConsolidatedInfoForPageLoad(
/* Get tenant config data */
fetches.add(tenantService
.getTenantConfiguration()
.as(this::toResponseDTO)
.doOnError(e -> log.error("Error fetching tenant config", e))
.as(this::toResponseDTO)
.doOnSuccess(consolidatedAPIResponseDTO::setTenantConfig)
.name(getQualifiedSpanName(TENANT_SPAN, mode))
.tap(Micrometer.observation(observationRegistry)));
Expand Down
Loading
Loading