Merge branch 'trisha-dev' into 'master'

Plugin Design (Create, install & uninstall)

See merge request theappsmith/internal-tools-server!4
This commit is contained in:
Trisha Anand 2019-08-22 12:03:42 +00:00
commit e8b7b234b3
24 changed files with 344 additions and 18 deletions

View File

@ -3,9 +3,9 @@ package com.mobtools.server.configurations;
import com.mobtools.server.domains.LoginSource;
import com.mobtools.server.domains.User;
import com.mobtools.server.domains.UserState;
import com.mobtools.server.repositories.UserRepository;
import com.mobtools.server.services.TenantService;
import com.mobtools.server.services.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
@ -39,12 +39,15 @@ import java.util.Map;
* saveAuthorizedClient is called on every successful OAuth2 authentication, this solves the problem
* of plugging a handler for the same purpose.
*/
@Configuration
public class ClientUserRepository implements ServerOAuth2AuthorizedClientRepository {
UserService userService;
TenantService tenantService;
public ClientUserRepository(UserService userService) {
public ClientUserRepository(UserService userService, TenantService tenantService) {
this.userService = userService;
this.tenantService = tenantService;
}
private static final String DEFAULT_AUTHORIZED_CLIENTS_ATTR_NAME =
@ -83,7 +86,7 @@ public class ClientUserRepository implements ServerOAuth2AuthorizedClientReposit
.then(Mono.empty());
}
private Mono<User> checkAndCreateUser(OidcUser user) {
public Mono<User> checkAndCreateUser(OidcUser user) {
User newUser = new User();
newUser.setName(user.getFullName());
newUser.setEmail(user.getEmail());
@ -91,7 +94,19 @@ public class ClientUserRepository implements ServerOAuth2AuthorizedClientReposit
newUser.setState(UserState.ACTIVATED);
newUser.setIsEnabled(true);
return userService.findByEmail(user.getEmail())
/** TODO
* Tenant here is being hardcoded. This is a stop gap measure
* A flow needs to be added to connect a User to a Tenant. Once
* that is done, during the login, the tenant should be picked up
* and a user should be hence created.
*/
return tenantService.findById("5d3e90a2dfec7c00047a81ea")
.map(tenant -> {
newUser.setTenant(tenant);
return newUser;
})
.then(userService.findByEmail(user.getEmail()))
.switchIfEmpty(userService.save(newUser));
}

View File

@ -2,7 +2,7 @@ package com.mobtools.server.configurations;
import com.mobtools.server.constants.Security;
import com.mobtools.server.repositories.UserRepository;
import com.mobtools.server.services.TenantService;
import com.mobtools.server.services.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
@ -26,6 +26,9 @@ public class SecurityConfig {
@Autowired
private UserService userService;
@Autowired
private TenantService tenantService;
/**
* This configuration enables CORS requests for the most common HTTP Methods
*
@ -69,7 +72,7 @@ public class SecurityConfig {
.authenticated()
.and().httpBasic()
.and().oauth2Login()
.authorizedClientRepository(new ClientUserRepository(userService))
.authorizedClientRepository(new ClientUserRepository(userService, tenantService))
.and().formLogin()
.and().build();
}

View File

@ -2,10 +2,16 @@ package com.mobtools.server.controllers;
import com.mobtools.server.domains.BaseDomain;
import com.mobtools.server.dtos.ResponseDto;
import com.mobtools.server.exceptions.MobtoolsException;
import com.mobtools.server.services.CrudService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@ -18,7 +24,7 @@ public abstract class BaseController<S extends CrudService, T extends BaseDomain
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<ResponseDto<T>> create(@Valid @RequestBody T resource) {
public Mono<ResponseDto<T>> create(@Valid @RequestBody T resource) throws MobtoolsException {
return service.create(resource)
.map(created -> new ResponseDto<>(HttpStatus.CREATED.value(), created, null));
}

View File

@ -2,10 +2,20 @@ package com.mobtools.server.controllers;
import com.mobtools.server.constants.Url;
import com.mobtools.server.domains.Plugin;
import com.mobtools.server.domains.Tenant;
import com.mobtools.server.dtos.PluginTenantDTO;
import com.mobtools.server.dtos.ResponseDto;
import com.mobtools.server.services.PluginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import javax.validation.Valid;
@RestController
@ -16,4 +26,19 @@ public class PluginController extends BaseController<PluginService, Plugin, Stri
public PluginController(PluginService service) {
super(service);
}
@PostMapping("/install")
@ResponseStatus(HttpStatus.CREATED)
public Mono<ResponseDto<Tenant>> install(@Valid @RequestBody PluginTenantDTO plugin) {
return service.installPlugin(plugin)
.map(tenant -> new ResponseDto<>(HttpStatus.CREATED.value(), tenant, null));
}
@PostMapping("/uninstall")
@ResponseStatus(HttpStatus.CREATED)
public Mono<ResponseDto<Tenant>> uninstall(@Valid @RequestBody PluginTenantDTO plugin) {
return service.uninstallPlugin(plugin)
.map(tenant -> new ResponseDto<>(HttpStatus.CREATED.value(), tenant, null));
}
}

View File

@ -5,7 +5,11 @@ import com.mobtools.server.domains.Query;
import com.mobtools.server.dtos.CommandQueryParams;
import com.mobtools.server.services.QueryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController

View File

@ -3,7 +3,11 @@ package com.mobtools.server.domains;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.annotation.*;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.domain.Persistable;
import java.util.Date;

View File

@ -23,5 +23,15 @@ public class Plugin extends BaseDomain {
String executorClass;
List<Property> properties;
String jarLocation;
List<PluginParameterType> resourceParams;
List<PluginParameterType> actionParams;
String minAppsmithVersionSupported;
String maxAppsmithVersionSupported;
String version;
}

View File

@ -0,0 +1,15 @@
package com.mobtools.server.domains;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
@NoArgsConstructor
public class PluginParameterType {
String key;
String dataType;
}

View File

@ -24,4 +24,6 @@ public class Tenant extends BaseDomain {
private List<TenantSetting> tenantSettings;
private List<TenantPlugin> plugins;
}

View File

@ -0,0 +1,21 @@
package com.mobtools.server.domains;
import com.mobtools.server.dtos.TenantPluginStatus;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.mongodb.core.mapping.DBRef;
@Getter
@Setter
@ToString
@NoArgsConstructor
public class TenantPlugin extends BaseDomain {
@DBRef
private Plugin plugin;
TenantPluginStatus status;
}

View File

@ -9,7 +9,6 @@ import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;
@ -36,6 +35,8 @@ public class User extends BaseDomain implements UserDetails {
private Boolean isEnabled;
private Tenant tenant;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {

View File

@ -0,0 +1,18 @@
package com.mobtools.server.dtos;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class AuthenticationDTO {
String authType;
String username;
String password;
}

View File

@ -0,0 +1,14 @@
package com.mobtools.server.dtos;
import com.mobtools.server.domains.PluginType;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class PluginDTO {
String name;
PluginType type;
String executorClass;
String jarLocation;
}

View File

@ -0,0 +1,11 @@
package com.mobtools.server.dtos;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class PluginTenantDTO {
String name;
TenantPluginStatus status;
}

View File

@ -1,6 +1,10 @@
package com.mobtools.server.dtos;
import lombok.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;

View File

@ -0,0 +1,5 @@
package com.mobtools.server.dtos;
public enum TenantPluginStatus {
FREE, TRIAL, ACTIVATED
}

View File

@ -8,7 +8,12 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import java.sql.*;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;

View File

@ -2,7 +2,9 @@ package com.mobtools.server.repositories;
import com.mobtools.server.domains.Plugin;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Mono;
@Repository
public interface PluginRepository extends BaseRepository<Plugin, String> {
Mono<Plugin> findByName(String name);
}

View File

@ -1,6 +1,7 @@
package com.mobtools.server.services;
import com.mobtools.server.domains.BaseDomain;
import com.mobtools.server.exceptions.MobtoolsException;
import com.mobtools.server.repositories.BaseRepository;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
@ -65,7 +66,7 @@ public abstract class BaseService<R extends BaseRepository, T extends BaseDomain
}
@Override
public Mono<T> create(T object) {
public Mono<T> create(T object) throws MobtoolsException {
return repository.save(object);
}

View File

@ -1,6 +1,7 @@
package com.mobtools.server.services;
import com.mobtools.server.domains.BaseDomain;
import com.mobtools.server.exceptions.MobtoolsException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@ -8,7 +9,7 @@ public interface CrudService<T extends BaseDomain, ID> {
Flux<T> get();
Mono<T> create(T resource);
Mono<T> create(T resource) throws MobtoolsException;
Mono<T> update(ID id, T resource) throws Exception;

View File

@ -2,6 +2,10 @@ package com.mobtools.server.services;
import com.mobtools.server.domains.Plugin;
import com.mobtools.server.domains.PluginType;
import com.mobtools.server.domains.Tenant;
import com.mobtools.server.dtos.PluginTenantDTO;
import com.mobtools.server.exceptions.MobtoolsException;
import reactor.core.publisher.Mono;
public interface PluginService extends CrudService<Plugin, String> {
@ -14,4 +18,11 @@ public interface PluginService extends CrudService<Plugin, String> {
* @return PluginExecutor
*/
PluginExecutor getPluginExecutor(PluginType pluginType, String className);
public Mono<Plugin> create(Plugin plugin) throws MobtoolsException;
public Mono<Tenant> installPlugin(PluginTenantDTO plugin);
public Mono<Tenant> uninstallPlugin(PluginTenantDTO plugin);
}

View File

@ -1,30 +1,56 @@
package com.mobtools.server.services;
import com.mobtools.server.configurations.ClientUserRepository;
import com.mobtools.server.domains.Plugin;
import com.mobtools.server.domains.PluginType;
import com.mobtools.server.domains.Tenant;
import com.mobtools.server.domains.TenantPlugin;
import com.mobtools.server.dtos.PluginTenantDTO;
import com.mobtools.server.dtos.TenantPluginStatus;
import com.mobtools.server.exceptions.MobtoolsException;
import com.mobtools.server.repositories.PluginRepository;
import com.mobtools.server.repositories.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Service
public class PluginServiceImpl extends BaseService<PluginRepository, Plugin, String> implements PluginService {
private final PluginRepository pluginRepository;
private final UserRepository userRepository;
private final ApplicationContext applicationContext;
private final ClientUserRepository clientUserRepository;
private final TenantService tenantService;
@Autowired
public PluginServiceImpl(Scheduler scheduler,
MongoConverter mongoConverter,
ReactiveMongoTemplate reactiveMongoTemplate,
PluginRepository repository,
ApplicationContext applicationContext) {
UserRepository userRepository,
ApplicationContext applicationContext,
ClientUserRepository clientUserRepository,
TenantService tenantService) {
super(scheduler, mongoConverter, reactiveMongoTemplate, repository);
this.userRepository = userRepository;
this.applicationContext = applicationContext;
pluginRepository = repository;
this.clientUserRepository = clientUserRepository;
this.tenantService = tenantService;
}
public PluginExecutor getPluginExecutor(PluginType pluginType, String className) {
@ -37,4 +63,112 @@ public class PluginServiceImpl extends BaseService<PluginRepository, Plugin, Str
}
return null;
}
@Override
public Mono<Plugin> create(Plugin plugin) throws MobtoolsException {
if (plugin.getId() != null) {
throw new MobtoolsException("During create plugin, Id is not null. Can't create new plugin.");
}
plugin.setDeleted(false);
return pluginRepository.save(plugin);
}
@Override
public Mono<Tenant> installPlugin(PluginTenantDTO pluginTenantDTO) {
return pluginRepository
.findByName(pluginTenantDTO.getName())
.flatMap(plugin1 -> storeTenantPlugin(plugin1, pluginTenantDTO.getStatus()))
.switchIfEmpty(Mono.empty());
}
@Override
public Mono<Tenant> uninstallPlugin(PluginTenantDTO plugin) {
/*TODO
* Tenant & user association is being mocked here by forcefully
* only using a hardcoded tenant. This needs to be replaced by
* a user-tenant association flow. The Tenant needs to be picked
* up from a user object. This is being used in install/uninstall
* plugin from a tenant flow. Instead, the current user should be read
* using the following :
* ReactiveSecurityContextHolder.getContext()
* .map(SecurityContext::getAuthentication)
* .map(Authentication::getPrincipal);
* Once the user has been pulled using this, tenant should already
* be stored as part of user and this tenant should be used to store
* the installed plugin or to delete plugin during uninstallation.
*/
Mono<Tenant> tenantMono = tenantService.findById("5d3e90a2dfec7c00047a81ea");
return tenantMono
.map(tenant -> {
List<TenantPlugin> tenantPluginList = tenant.getPlugins();
if (tenantPluginList == null || tenantPluginList.isEmpty()) {
return tenant;
}
for (TenantPlugin listPlugin : tenantPluginList) {
if (listPlugin.getPlugin().getName().equals(plugin.getName())) {
log.debug("Plugin {} found. Uninstalling now from Tenant {}.",
plugin.getName(), tenant.getName());
tenantPluginList.remove(listPlugin);
tenant.setPlugins(tenantPluginList);
return tenant;
}
}
log.debug("Plugin {} not found. Can't uninstall a plugin which is not installed",
plugin.getName());
return tenant;
})
/* TODO
* Extra save is happening below in the edge case scenario of a plugin
* which needs to be removed from the installed list, didnt exist in this list
* to be begin with. Small optimization opportunity.
*/
.flatMap(tenantService::save);
}
private Mono<Tenant> storeTenantPlugin(Plugin plugin, TenantPluginStatus status) {
/*TODO
* Tenant & user association is being mocked here by forcefully
* only using a hardcoded tenant. This needs to be replaced by
* a user-tenant association flow. The Tenant needs to be picked
* up from a user object. This is being used in install/uninstall
* plugin from a tenant flow. Instead, the current user should be read
* using the following :
* ReactiveSecurityContextHolder.getContext()
* .map(SecurityContext::getAuthentication)
* .map(Authentication::getPrincipal);
* Once the user has been pulled using this, tenant should already
* be stored as part of user and this tenant should be used to store
* the installed plugin or to delete plugin during uninstallation.
*/
Mono<Tenant> tenantMono = tenantService.findById("5d3e90a2dfec7c00047a81ea");
Mono<Object> userObjectMono = ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.map(Authentication::getPrincipal);
return Mono.zip(tenantMono, userObjectMono, (tenant, user) -> {
List<TenantPlugin> tenantPluginList = tenant.getPlugins();
if (tenantPluginList == null) {
tenantPluginList = new ArrayList<TenantPlugin>();
}
for (TenantPlugin listPlugin : tenantPluginList) {
if (listPlugin.getPlugin().getName().equals(plugin.getName())) {
log.debug("Plugin {} is already installed for Tenant {}. Don't add again.",
plugin.getName(), tenant.getName());
return tenant;
}
}
TenantPlugin tenantPlugin = new TenantPlugin();
//Set an ID in the nested document so that installed plugins can be referred to uniquely using IDs
ObjectId objectId = new ObjectId();
tenantPlugin.setId(objectId.toString());
tenantPlugin.setPlugin(plugin);
tenantPlugin.setStatus(status);
tenantPluginList.add(tenantPlugin);
tenant.setPlugins(tenantPluginList);
return tenant;
}).flatMap(tenantService::save);
}
}

View File

@ -8,4 +8,8 @@ public interface TenantService extends CrudService<Tenant, String> {
Mono<Tenant> getByName(String name);
Mono<Tenant> create(Tenant object);
Mono<Tenant> findById(String id);
Mono<Tenant> save (Tenant tenant);
}

View File

@ -79,4 +79,14 @@ public class TenantServiceImpl extends BaseService<TenantRepository, Tenant, Str
return tenantSetting;
});
}
@Override
public Mono<Tenant> findById(String id) {
return repository.findById(id);
}
@Override
public Mono<Tenant> save(Tenant tenant) {
return repository.save(tenant);
}
}