Adding the user policy object that has permissions for ARN objects.

Now we need to parse the ARN and match it to the policy in the PreAuthorize & Custom repository functions.
This commit is contained in:
Arpit Mohan 2020-02-29 09:24:26 +05:30
parent bd5424095a
commit e078382b94
15 changed files with 289 additions and 121 deletions

View File

@ -1,5 +1,8 @@
package com.appsmith.server.configurations;
import com.appsmith.server.domains.User;
import com.appsmith.server.helpers.AclHelper;
import com.appsmith.server.services.AclEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
@ -7,6 +10,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.regex.Pattern;
@Slf4j
@Component
@ -15,8 +19,19 @@ public class CustomPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
log.debug("In hasPermission with permission: {}", permission);
SimpleGrantedAuthority authority = new SimpleGrantedAuthority((String) permission);
return authentication.getAuthorities().contains(authority);
AclEntity aclEntity = targetDomainObject.getClass().getAnnotation(AclEntity.class);
// Create the ARN
String arn = AclHelper.createArn(aclEntity, (User) authentication.getPrincipal(), null);
String authorityToCheck = AclHelper.concatenatePermissionWithArn((String) permission, arn);
log.debug("Got authority to check: {}", authorityToCheck);
boolean result = authentication.getAuthorities().stream()
.anyMatch(auth -> auth.getAuthority().equals(authorityToCheck)
|| auth.getAuthority().matches(authorityToCheck)
|| authorityToCheck.matches(auth.getAuthority())
);
log.debug("Got hasPermission result: {}", result);
return result;
}
@Override

View File

@ -138,4 +138,12 @@ public class SecurityConfig {
.logoutSuccessHandler(new LogoutSuccessHandler(objectMapper))
.and().build();
}
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler =
new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator());
return expressionHandler;
}
}

View File

@ -69,56 +69,7 @@ public class SoftDeleteMongoQueryLookupStrategy implements QueryLookupStrategy {
}
ReactivePartTreeMongoQuery partTreeQuery = (ReactivePartTreeMongoQuery) repositoryQuery;
return new SoftDeletePartTreeMongoQuery(method, partTreeQuery);
return new SoftDeletePartTreeMongoQuery(method, partTreeQuery, this.mongoOperations, EXPRESSION_PARSER, evaluationContextProvider);
}
private Criteria notDeleted() {
return new Criteria().orOperator(
where("deleted").exists(false),
where("deleted").is(false)
);
}
private class SoftDeletePartTreeMongoQuery extends ReactivePartTreeMongoQuery {
private ReactivePartTreeMongoQuery reactivePartTreeQuery;
private Method method;
SoftDeletePartTreeMongoQuery(Method method, ReactivePartTreeMongoQuery reactivePartTreeMongoQuery) {
super((ReactiveMongoQueryMethod) reactivePartTreeMongoQuery.getQueryMethod(),
mongoOperations, EXPRESSION_PARSER, evaluationContextProvider);
this.reactivePartTreeQuery = reactivePartTreeMongoQuery;
this.method = method;
}
@Override
protected Query createQuery(ConvertingParameterAccessor accessor) {
// SecurityContext securityContext = SecurityContextHolder.getContext();
// User userPrincipal = ReactiveSecurityContextHolder.getContext()
// .switchIfEmpty(Mono.error(new Exception("no context")))
// .map(ctx -> ctx.getAuthentication())
// .map(auth -> auth.getPrincipal())
// .map(principal -> {
// if (principal instanceof User) {
// return (User) principal;
// }
// return new User();
// }).block();
AclPermission aclPermission = method.getAnnotation(AclPermission.class);
if (aclPermission != null) {
log.debug("Got principal: {}", aclPermission.principal());
}
Query query = super.createQuery(accessor);
return withNotDeleted(query);
}
@Override
protected Query createCountQuery(ConvertingParameterAccessor accessor) {
Query query = super.createCountQuery(accessor);
return withNotDeleted(query);
}
private Query withNotDeleted(Query query) {
return query.addCriteria(notDeleted());
}
}
}

View File

@ -0,0 +1,72 @@
package com.appsmith.server.configurations.mongo;
import com.appsmith.server.services.AclPermission;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor;
import org.springframework.data.mongodb.repository.query.MongoQueryMethod;
import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryMethod;
import org.springframework.data.mongodb.repository.query.ReactivePartTreeMongoQuery;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import java.lang.reflect.Method;
import static org.springframework.data.mongodb.core.query.Criteria.where;
@Slf4j
public class SoftDeletePartTreeMongoQuery extends ReactivePartTreeMongoQuery {
private ReactivePartTreeMongoQuery reactivePartTreeQuery;
private Method method;
SoftDeletePartTreeMongoQuery(Method method, ReactivePartTreeMongoQuery reactivePartTreeMongoQuery,
ReactiveMongoOperations mongoOperations,
SpelExpressionParser expressionParser,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
super((ReactiveMongoQueryMethod) reactivePartTreeMongoQuery.getQueryMethod(),
mongoOperations, expressionParser, evaluationContextProvider);
this.reactivePartTreeQuery = reactivePartTreeMongoQuery;
this.method = method;
}
@Override
protected Query createQuery(ConvertingParameterAccessor accessor) {
// SecurityContext securityContext = SecurityContextHolder.getContext();
// User userPrincipal = ReactiveSecurityContextHolder.getContext()
// .switchIfEmpty(Mono.error(new Exception("no context")))
// .map(ctx -> ctx.getAuthentication())
// .map(auth -> auth.getPrincipal())
// .map(principal -> {
// if (principal instanceof User) {
// return (User) principal;
// }
// return new User();
// }).block();
AclPermission aclPermission = method.getAnnotation(AclPermission.class);
if (aclPermission != null) {
log.debug("Got principal: {}", aclPermission.principal());
}
MongoQueryMethod mongoQueryMethod = this.getQueryMethod();
Query query = super.createQuery(accessor);
return withNotDeleted(query);
}
@Override
protected Query createCountQuery(ConvertingParameterAccessor accessor) {
Query query = super.createCountQuery(accessor);
return withNotDeleted(query);
}
private Query withNotDeleted(Query query) {
return query.addCriteria(notDeleted());
}
private Criteria notDeleted() {
return new Criteria().orOperator(
where("deleted").exists(false),
where("deleted").is(false)
);
}
}

View File

@ -0,0 +1,22 @@
package com.appsmith.server.domains;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Arn {
String base = "arn:appsmith";
String organizationId;
String entity;
String entityId;
@Override
public String toString() {
return base + ":" + organizationId + ":" + entity + ":" + entityId;
}
}

View File

@ -8,6 +8,7 @@ import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.mapping.Document;
import javax.validation.constraints.NotNull;
import java.util.HashSet;
import java.util.Set;
@Getter
@ -25,7 +26,7 @@ public class Group extends BaseDomain {
@NotNull
private String organizationId;
/*
/**
* This is a list of name of permissions. We will query with permission collection by name
* This is because permissions are global in nature. They are not specific to a particular org/team.
*/
@ -33,6 +34,11 @@ public class Group extends BaseDomain {
private Boolean isDefault = false;
/**
* These are the policies attached to the group. All users who are part of this group will inherit these policies
*/
Set<Policy> policies = new HashSet<>();
/**
* If the display name is null or empty, then just return the actual group name. This is just to ensure that
* the client is never sent an empty group name for displaying on the UI.

View File

@ -0,0 +1,22 @@
package com.appsmith.server.domains;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.util.Set;
/*
TODO: Create a PolicyTemplate that will store all complex policies like "publishApp" which requires mulitple permissions
*/
@Getter
@Setter
@ToString
public class Policy implements Serializable {
Set<String> permissions;
Set<String> entities;
}

View File

@ -1,11 +1,13 @@
package com.appsmith.server.domains;
import com.appsmith.external.models.BaseDomain;
import com.appsmith.server.helpers.AclHelper;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.security.core.GrantedAuthority;
@ -13,7 +15,9 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@ -57,9 +61,26 @@ public class User extends BaseDomain implements UserDetails {
// During evaluation a union of the group permissions and user-specific permissions will take effect.
private Set<String> permissions = new HashSet<>();
private Set<Policy> policies = new HashSet<>();
@JsonIgnore
@Transient
Set<String> flatPermissions = new HashSet<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.getPermissions().stream()
// TODO: Also extract the policies from associated groups
if (this.flatPermissions != null) {
for (Policy policy : this.policies) {
for (String entity : policy.getEntities()) {
for (String permission : policy.getPermissions()) {
flatPermissions.add(AclHelper.concatenatePermissionWithArn(permission, entity));
}
}
}
}
return this.getFlatPermissions().stream()
.map(permission -> new SimpleGrantedAuthority(permission))
.collect(Collectors.toSet());
}

View File

@ -0,0 +1,26 @@
package com.appsmith.server.helpers;
import com.appsmith.server.domains.User;
import com.appsmith.server.services.AclEntity;
public class AclHelper {
private static final String ARN_PREFIX = "arn:appsmith:";
public static final String createArn(AclEntity aclEntity, User principal, String id) {
StringBuilder arnBuilder = new StringBuilder(ARN_PREFIX)
.append(principal.getCurrentOrganizationId())
.append(":").append(aclEntity.value());
arnBuilder = (id != null) ? arnBuilder.append(":").append(id) : arnBuilder.append(":*");
return arnBuilder.toString();
}
public static final String concatenatePermissionWithArn(String permission, String arn) {
return permission + "::" + arn;
}
public static final String extractArnFromString(String arn) {
return null;
}
}

View File

@ -10,8 +10,6 @@ import java.util.Set;
@Repository
public interface ActionRepository extends BaseRepository<Action, String> {
Mono<Action> findById(String id);
Mono<Action> findByNameAndPageId(String name, String pageId);
Flux<Action> findByPageId(String pageId);

View File

@ -10,26 +10,9 @@ import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@NoRepositoryBean
@Repository
@AclEntity("applications")
public interface ApplicationRepository extends BaseRepository<Application, String> {
default Mono<Application> findByIdAndOrganizationId(String id, String orgId) {
System.out.println("In the custom implementation");
return ReactiveSecurityContextHolder.getContext()
.map(ctx -> ctx.getAuthentication())
.map(auth -> auth.getPrincipal())
.flatMap(principal -> {
System.out.println("Got principal: " + principal);
return Mono.empty();
});
}
public interface ApplicationRepository extends BaseRepository<Application, String>, CustomApplicationRepository {
Mono<Application> findByName(String name);
// @Override
// Flux<Application> findAll(Example example);
@Override
Mono<Application> findById(String id);
}

View File

@ -1,46 +0,0 @@
package com.appsmith.server.repositories;
import com.appsmith.server.domains.Application;
import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.lang.annotation.Annotation;
import static org.springframework.data.mongodb.core.query.Criteria.where;
@Component
public class ApplicationRepositoryImpl extends BaseRepositoryImpl<Application, String> implements ApplicationRepository {
// public ApplicationRepositoryImpl(@NonNull MongoEntityInformation<Application, String> entityInformation,
// @NonNull ReactiveMongoOperations mongoOperations) {
// super(entityInformation, mongoOperations);
// }
// TODO: Not implemented yet
@Override
public Mono<Application> findByIdAndOrganizationId(String id, String orgId) {
Query query = new Query();
return Mono.empty() ;
}
@Override
public Mono<Application> findByName(String name) {
Query query = new Query();
query.addCriteria(notDeleted());
Annotation[] annotations = entityInformation.getJavaType().getAnnotations();
return mongoOperations.query(entityInformation.getJavaType())
.inCollection(entityInformation.getCollectionName())
.matching(query)
.one();
}
}

View File

@ -0,0 +1,8 @@
package com.appsmith.server.repositories;
import com.appsmith.server.domains.Application;
import reactor.core.publisher.Mono;
public interface CustomApplicationRepository {
Mono<Application> findByIdAndOrganizationId(String id, String orgId);
}

View File

@ -0,0 +1,80 @@
package com.appsmith.server.repositories;
import com.appsmith.server.constants.Entity;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.User;
import com.appsmith.server.services.AclEntity;
import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.Collection;
import java.util.Set;
import static org.springframework.data.mongodb.core.query.Criteria.where;
@Component
public class CustomApplicationRepositoryImpl implements CustomApplicationRepository {
private final ReactiveMongoOperations mongoOperations;
private final ReactiveMongoTemplate mongoTemplate;
@Autowired
public CustomApplicationRepositoryImpl(@NonNull ReactiveMongoOperations mongoOperations,
ReactiveMongoTemplate mongoTemplate) {
this.mongoOperations = mongoOperations;
this.mongoTemplate = mongoTemplate;
}
protected Criteria notDeleted() {
return new Criteria().orOperator(
where("deleted").exists(false),
where("deleted").is(false)
);
}
protected Criteria userAcl(User user, String permission) {
Set<String> flatPermissions = user.getFlatPermissions();
String entity = Entity.APPLICATIONS;
// flatPermissions.stream()
// .filter(flatPerm -> )
return new Criteria().orOperator(
where("acl.users").all(user.getUsername()),
where("acl.groups").all(user.getGroupIds())
);
}
protected Criteria getIdCriteria(Object id) {
return where("id").is(id);
}
@Override
public Mono<Application> findByIdAndOrganizationId(String id, String orgId) {
return ReactiveSecurityContextHolder.getContext()
.map(ctx -> ctx.getAuthentication())
.flatMap(auth -> {
User user = (User) auth.getPrincipal();
Query query = new Query(getIdCriteria(id));
query.addCriteria(where("organizationId").is(orgId));
query.addCriteria(new Criteria().andOperator(notDeleted(), userAcl(user, "read")));
return mongoOperations.query(Application.class)
.matching(query)
.one();
});
}
// @Override
// public Mono<Application> findByName(String name) {
// Query query = new Query();
// return Mono.empty();
// }
}

View File

@ -2,6 +2,7 @@ package com.appsmith.server.services;
import com.appsmith.external.models.BaseDomain;
import com.appsmith.server.constants.AclConstants;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.MultiValueMap;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@ -17,6 +18,7 @@ public interface CrudService<T extends BaseDomain, ID> {
@AclPermission(values = AclConstants.UPDATE_PERMISSION)
Mono<T> update(ID id, T resource);
@PreAuthorize("hasPermission(#this.this, 'read')")
@AclPermission(values = AclConstants.READ_PERMISSION)
Mono<T> getById(ID id);