Adding the capability to associate a user with multiple organizations

This commit is contained in:
Trisha Anand 2019-11-13 10:23:23 +00:00 committed by Arpit Mohan
parent ea5a892da3
commit f81e22b1a5
20 changed files with 149 additions and 49 deletions

View File

@ -113,11 +113,6 @@
<artifactId>de.flapdoodle.embed.mongo</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.9.13</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>

View File

@ -1,6 +1,9 @@
package com.appsmith.server.configurations;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Value;
@ -35,6 +38,9 @@ public class CommonConfig {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return objectMapper;
}
}

View File

@ -2,6 +2,8 @@ package com.appsmith.server.configurations;
import com.appsmith.server.constants.Security;
import com.appsmith.server.domains.Role;
import com.appsmith.server.domains.User;
import com.appsmith.server.services.OrganizationService;
import com.appsmith.server.services.UserService;
import org.springframework.beans.factory.annotation.Autowired;
@ -10,8 +12,6 @@ import org.springframework.core.io.ClassPathResource;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
@ -23,6 +23,7 @@ import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import java.util.Arrays;
import java.util.Set;
@EnableWebFluxSecurity
public class SecurityConfig {
@ -75,11 +76,12 @@ public class SecurityConfig {
@Bean
public MapReactiveUserDetailsService userDetailsService() {
UserDetails user = User
.withUsername("api_user")
.password(passwordEncoder().encode("8uA@;&mB:cnvN~{#"))
.roles(Security.USER_ROLE)
.build();
User user = new com.appsmith.server.domains.User();
user.setEmail("api_user");
user.setName("api_user");
user.setPassword(passwordEncoder().encode("8uA@;&mB:cnvN~{#"));
user.setRoles(Set.of(new Role(Security.USER_ROLE.toString())));
return new MapReactiveUserDetailsService(user);
}

View File

@ -2,10 +2,15 @@ package com.appsmith.server.controllers;
import com.appsmith.server.constants.Url;
import com.appsmith.server.domains.User;
import com.appsmith.server.dtos.ResponseDTO;
import com.appsmith.server.services.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping(Url.USER_URL)
@ -15,4 +20,16 @@ public class UserController extends BaseController<UserService, User, String> {
public UserController(UserService service) {
super(service);
}
@PutMapping("/switchOrganization/{orgId}")
public Mono<ResponseDTO<User>> setCurrentOrganization(@PathVariable String orgId) {
return service.switchCurrentOrganization(orgId)
.map(user -> new ResponseDTO<>(HttpStatus.OK.value(), user, null));
}
@PutMapping("/addOrganization/{orgId}")
public Mono<ResponseDTO<User>> addUserToOrganization(@PathVariable String orgId) {
return service.addUserToOrganization(orgId)
.map(user -> new ResponseDTO<>(HttpStatus.OK.value(), user, null));
}
}

View File

@ -1,6 +1,7 @@
package com.appsmith.server.domains;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@ -13,6 +14,7 @@ import javax.validation.constraints.NotEmpty;
@Getter
@Setter
@ToString
@AllArgsConstructor
public class Role extends BaseDomain {
private static final long serialVersionUID = -9218373922209100577L;

View File

@ -37,7 +37,9 @@ public class User extends BaseDomain implements UserDetails {
private Boolean isEnabled = true;
private String organizationId;
private String currentOrganizationId;
private Set<String> organizationIds;
// There is a many-to-many relationship with groups. If this value is modified, please also modify the list of
// users in that particular group document as well.

View File

@ -16,10 +16,12 @@ public enum AppsmithError {
PAGE_DOESNT_BELONG_TO_USER_ORGANIZATION(400, 4006, "Page {0} does not belong to the current user {1} organization"),
UNSUPPORTED_OPERATION(400, 4007, "Unsupported operation"),
ACTION_RUN_KEY_VALUE_INVALID(400, 4008, "Invalid template param key value pair: {0}:{1}"),
NO_CONFIGURATION_FOUND_IN_DATASOURCE(400, 4009, "Datasource without any configuration is invalid. Please try again with datasourceConfiguration"),
UNAUTHORIZED_DOMAIN(401, 4001, "Invalid email domain provided. Please sign in with a valid work email ID"),
UNAUTHORIZED_ACCESS(401, 4002, "Unauthorized access"),
INVALID_ACTION_NAME(401, 4003, "Action name is invalid. Please input syntactically correct name"),
USER_DOESNT_BELONG_ANY_ORGANIZATION(400, 4009, "User {0} does not belong to any organization"),
USER_DOESNT_BELONG_TO_ORGANIZATION(400, 4010, "User {0} does not belong to an organization with id {1}"),
NO_CONFIGURATION_FOUND_IN_DATASOURCE(400, 4011, "Datasource without any configuration is invalid. Please try again with datasourceConfiguration"),
UNAUTHORIZED_DOMAIN(401, 4012, "Invalid email domain provided. Please sign in with a valid work email ID"),
UNAUTHORIZED_ACCESS(401, 4013, "Unauthorized access"),
INVALID_ACTION_NAME(401, 4014, "Action name is invalid. Please input syntactically correct name"),
INTERNAL_SERVER_ERROR(500, 5000, "Internal server error while processing request"),
REPOSITORY_SAVE_FAILED(500, 5001, "Repository save failed"),
PLUGIN_INSTALLATION_FAILED_DOWNLOAD_ERROR(500, 5002, "Due to error in downloading the plugin from remote repository, plugin installation has failed. Check the jar location and try again"),

View File

@ -48,8 +48,8 @@ public class AnalyticsService<T extends BaseDomain> {
HashMap<String, String> analyticsProperties = new HashMap<>();
analyticsProperties.put("id", ((BaseDomain) object).getId());
analyticsProperties.put("object", object.toString());
if(user.getOrganizationId() != null) {
analyticsProperties.put("organizationId", user.getOrganizationId());
if(user.getCurrentOrganizationId() != null) {
analyticsProperties.put("organizationId", user.getCurrentOrganizationId());
}
analytics.enqueue(

View File

@ -57,7 +57,7 @@ public class ApplicationServiceImpl extends BaseService<ApplicationRepository, A
Mono<User> userMono = sessionUserService.getCurrentUser();
return userMono
.map(user -> user.getOrganizationId())
.map(user -> user.getCurrentOrganizationId())
.map(orgId -> {
application.setOrganizationId(orgId);
return application;
@ -70,7 +70,7 @@ public class ApplicationServiceImpl extends BaseService<ApplicationRepository, A
Mono<User> userMono = sessionUserService.getCurrentUser();
return userMono
.map(user -> user.getOrganizationId())
.map(user -> user.getCurrentOrganizationId())
.flatMapMany(orgId -> repository.findByOrganizationId(orgId));
}
@ -83,7 +83,7 @@ public class ApplicationServiceImpl extends BaseService<ApplicationRepository, A
Mono<User> userMono = sessionUserService.getCurrentUser();
return userMono
.map(user -> user.getOrganizationId())
.map(user -> user.getCurrentOrganizationId())
.flatMap(orgId -> repository.findByIdAndOrganizationId(id, orgId))
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "resource", id)));
}

View File

@ -64,7 +64,7 @@ public class DatasourceServiceImpl extends BaseService<DatasourceRepository, Dat
Mono<User> userMono = sessionUserService.getCurrentUser();
Mono<Organization> organizationMono = userMono.flatMap(user -> organizationService.findByIdAndPluginsPluginId(user.getOrganizationId(), datasource.getPluginId()));
Mono<Organization> organizationMono = userMono.flatMap(user -> organizationService.findByIdAndPluginsPluginId(user.getCurrentOrganizationId(), datasource.getPluginId()));
//Add organization id to the datasource.
Mono<Datasource> updatedDatasourceMono = organizationMono

View File

@ -95,7 +95,7 @@ public class PageServiceImpl extends BaseService<PageRepository, Page, String> i
username[0] = user.getEmail();
return user;
})
.flatMap(user -> applicationService.findByIdAndOrganizationId(page.getApplicationId(), user.getOrganizationId()))
.flatMap(user -> applicationService.findByIdAndOrganizationId(page.getApplicationId(), user.getCurrentOrganizationId()))
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.PAGE_DOESNT_BELONG_TO_USER_ORGANIZATION, page.getId(), username[0])))
//If mono transmits, then application id belongs to the current user's organization. Return page.
.then(Mono.just(page));

View File

@ -113,7 +113,7 @@ public class PluginServiceImpl extends BaseService<PluginRepository, Plugin, Str
//Find the organization using id and plugin id -> This is to find if the organization has the plugin installed
Mono<User> userMono = sessionUserService.getCurrentUser();
Mono<Organization> organizationMono = userMono.flatMap(user ->
organizationService.findByIdAndPluginsPluginId(user.getOrganizationId(), pluginDTO.getPluginId()));
organizationService.findByIdAndPluginsPluginId(user.getCurrentOrganizationId(), pluginDTO.getPluginId()));
return organizationMono
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.PLUGIN_NOT_INSTALLED, pluginDTO.getPluginId())))
@ -134,7 +134,7 @@ public class PluginServiceImpl extends BaseService<PluginRepository, Plugin, Str
//Find the organization using id and plugin id -> This is to find if the organization already has the plugin installed
Mono<User> userMono = sessionUserService.getCurrentUser();
Mono<Organization> pluginInOrganizationMono = userMono.flatMap(user ->
organizationService.findByIdAndPluginsPluginId(user.getOrganizationId(), pluginDTO.getPluginId()));
organizationService.findByIdAndPluginsPluginId(user.getCurrentOrganizationId(), pluginDTO.getPluginId()));
//If plugin is already present for the organization, just return the organization, else install and return organization
@ -149,7 +149,7 @@ public class PluginServiceImpl extends BaseService<PluginRepository, Plugin, Str
log.debug("Before publishing to the redis queue");
//Publish the event to the pub/sub queue
InstallPluginRedisDTO installPluginRedisDTO = new InstallPluginRedisDTO();
installPluginRedisDTO.setOrganizationId(user.getOrganizationId());
installPluginRedisDTO.setOrganizationId(user.getCurrentOrganizationId());
installPluginRedisDTO.setPluginOrgDTO(pluginDTO);
String jsonString;
try {
@ -164,7 +164,7 @@ public class PluginServiceImpl extends BaseService<PluginRepository, Plugin, Str
})
//Now that the plugin jar has been successfully downloaded, go on and add the plugin to the organization
.then(userMono)
.flatMap(user -> organizationService.findById(user.getOrganizationId()))
.flatMap(user -> organizationService.findById(user.getCurrentOrganizationId()))
.map(organization -> {
List<OrganizationPlugin> organizationPluginList = organization.getPlugins();

View File

@ -4,5 +4,5 @@ import com.appsmith.server.domains.User;
import reactor.core.publisher.Mono;
public interface SessionUserService {
public Mono<User> getCurrentUser();
Mono<User> getCurrentUser();
}

View File

@ -9,6 +9,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
@Component
@Slf4j
public class SignupServiceImpl implements SignupService {
@ -61,7 +65,13 @@ public class SignupServiceImpl implements SignupService {
log.debug("Going to update userId: {} with orgId: {} and groupId: {}", user.getId(), org.getId(), group.getId());
// Assign the user to the new organization
// TODO: Make organizationId as an array and allow a user to be assigned to multiple orgs
user.setOrganizationId(org.getId());
user.setCurrentOrganizationId(org.getId());
Set<String> organizationIds = user.getOrganizationIds();
if (organizationIds == null) {
organizationIds = new HashSet<>();
}
organizationIds.add(org.getId());
user.setOrganizationIds(organizationIds);
// Assign the org-admin group to the user who created the new organization
user.getGroupIds().add(group.getId());
return userService.update(user.getId(), user)

View File

@ -9,4 +9,8 @@ public interface UserService extends CrudService<User, String> {
Mono<User> findByEmail(String email);
Mono<User> switchCurrentOrganization(String orgId);
Mono<User> addUserToOrganization(String orgId);
}

View File

@ -5,8 +5,6 @@ import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.BeanCopyUtils;
import com.appsmith.server.repositories.UserRepository;
import com.segment.analytics.Analytics;
import com.segment.analytics.messages.IdentifyMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
@ -19,8 +17,11 @@ import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import javax.validation.Validator;
import java.util.HashMap;
import java.util.Map;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@Slf4j
@Service
@ -29,6 +30,8 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
private UserRepository repository;
private final OrganizationService organizationService;
private final AnalyticsService analyticsService;
private final SessionUserService sessionUserService;
@Autowired
public UserServiceImpl(Scheduler scheduler,
Validator validator,
@ -36,11 +39,12 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
ReactiveMongoTemplate reactiveMongoTemplate,
UserRepository repository,
OrganizationService organizationService,
AnalyticsService analyticsService) {
AnalyticsService analyticsService, SessionUserService sessionUserService) {
super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService);
this.repository = repository;
this.organizationService = organizationService;
this.analyticsService = analyticsService;
this.sessionUserService = sessionUserService;
}
@Override
@ -54,8 +58,65 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
}
@Override
public Mono<User> create(User user) {
public Mono<User> switchCurrentOrganization(String orgId) {
if(orgId == null || orgId.isEmpty()) {
return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, "organizationId"));
}
return sessionUserService.getCurrentUser()
.flatMap(user -> {
log.debug("Going to set organizationId: {} for user: {}", orgId, user.getId());
if(user.getCurrentOrganizationId().equals(orgId)) {
return Mono.just(user);
}
Set<String> organizationIds = user.getOrganizationIds();
if (organizationIds == null || organizationIds.isEmpty()) {
return Mono.error(new AppsmithException(AppsmithError.USER_DOESNT_BELONG_ANY_ORGANIZATION, user.getId()));
}
Optional<String> maybeOrgId = organizationIds.stream()
.filter(organizationId -> organizationId.equals(orgId))
.findFirst();
if(maybeOrgId.isPresent()) {
user.setCurrentOrganizationId(maybeOrgId.get());
return repository.save(user);
}
// Throw an exception if the orgId is not part of the user's organizations
return Mono.error(new AppsmithException(AppsmithError.USER_DOESNT_BELONG_TO_ORGANIZATION, user.getId(), orgId));
});
}
@Override
public Mono<User> addUserToOrganization(String orgId) {
return organizationService.findById(orgId)
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "organization", orgId)))
.flatMap(org -> sessionUserService.getCurrentUser())
.map(user -> {
Set<String> organizationIds = user.getOrganizationIds();
if (organizationIds == null) {
organizationIds = new HashSet<>();
if(user.getCurrentOrganizationId() != null) {
// If the list of organizationIds for a user is null, add the current user org
// to the new list as well
organizationIds.add(user.getCurrentOrganizationId());
}
}
if (!organizationIds.contains(orgId)) {
// Only add to the organizationIds array if it's not already present
organizationIds.add(orgId);
user.setOrganizationIds(organizationIds);
}
return user;
})
.flatMap(repository::save);
}
@Override
public Mono<User> create(User user) {
Mono<User> savedUserMono = super.create(user);
return savedUserMono
.flatMap(analyticsService::trackNewUser);
@ -63,7 +124,9 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return repository.findByName(username).block();
return repository.findByName(username)
.switchIfEmpty(Mono.error(new UsernameNotFoundException("Unable to find username: " + username)))
.block();
}
@Override
@ -85,16 +148,15 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
//Validation for user update. Right now it only validates the organization id. Other checks can be added
//here in the future.
private Mono<User> validateUpdate(User updateUser) {
if (updateUser.getOrganizationId() == null) {
if (updateUser.getCurrentOrganizationId() == null) {
//No organization present implies the update to the user is not to the organization id. No checks currently
//for this scenario. Return the user successfully.
return Mono.just(updateUser);
}
return organizationService.findById(updateUser.getOrganizationId())
return organizationService.findById(updateUser.getCurrentOrganizationId())
//If the organization is not found in the repository, throw an error
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, updateUser.getOrganizationId())))
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, updateUser.getCurrentOrganizationId())))
.then(Mono.just(updateUser));
}
}

View File

@ -1,2 +0,0 @@
# Jackson Properties
spring.jackson.default-property-inclusion=non_null

View File

@ -93,7 +93,7 @@ public class SeedMongoData {
user.setName((String) array[0]);
user.setEmail((String) array[1]);
user.setState((UserState) array[2]);
user.setOrganizationId(orgId);
user.setCurrentOrganizationId(orgId);
return user;
})
.flatMap(userRepository::save)

View File

@ -59,14 +59,14 @@ public class UserServiceTest {
//Add valid organization id to the updateUser object.
organizationMono
.map(organization -> {
updateUser.setOrganizationId(organization.getId());
updateUser.setCurrentOrganizationId(organization.getId());
return updateUser;
}).block();
Mono<User> userMono1 = userMono.flatMap(user -> userService.update(user.getId(), updateUser));
StepVerifier.create(userMono1)
.assertNext(updatedUserInRepository -> {
assertThat(updatedUserInRepository.getOrganizationId()).isEqualTo(updateUser.getOrganizationId());
assertThat(updatedUserInRepository.getCurrentOrganizationId()).isEqualTo(updateUser.getCurrentOrganizationId());
})
.verifyComplete();
}
@ -74,7 +74,7 @@ public class UserServiceTest {
@Test
public void updateUserWithInvalidOrganization() {
User updateUser = new User();
updateUser.setOrganizationId("Random-OrgId-%Not-In_The-System_For_SUre");
updateUser.setCurrentOrganizationId("Random-OrgId-%Not-In_The-System_For_SUre");
Mono<User> userMono1 = userMono.flatMap(user -> userService.update(user.getId(), updateUser));
StepVerifier.create(userMono1)
.expectErrorMatches(throwable -> throwable instanceof AppsmithException &&

View File

@ -38,7 +38,7 @@ services:
volumes:
- ./appsmith-server/src/main/resources/opa/:/config
environment:
- APPSMITH_SERVER_URL=http://appsmith-internal-server:8080
- APPSMITH_SERVER_URL=http://appsmith-internal-server:8080/public
ports:
- "8181:8181"
networks: