Skip to content

Instantly share code, notes, and snippets.

@MuellerConstantin
Last active March 3, 2021 16:33
Show Gist options
  • Save MuellerConstantin/ad7c0fd718945d5c38a09e5398d6da19 to your computer and use it in GitHub Desktop.
Save MuellerConstantin/ad7c0fd718945d5c38a09e5398d6da19 to your computer and use it in GitHub Desktop.

Revisions

  1. MuellerConstantin revised this gist Apr 14, 2020. 1 changed file with 299 additions and 0 deletions.
    299 changes: 299 additions & 0 deletions SimpleAclJpaRepository.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,299 @@
    import org.springframework.data.domain.Page;
    import org.springframework.data.domain.PageImpl;
    import org.springframework.data.domain.Pageable;
    import org.springframework.data.domain.Sort;
    import org.springframework.data.jpa.domain.Specification;
    import org.springframework.data.jpa.repository.query.QueryUtils;
    import org.springframework.data.jpa.repository.support.JpaEntityInformation;
    import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport;
    import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
    import org.springframework.data.repository.support.PageableExecutionUtils;
    import org.springframework.lang.Nullable;
    import org.springframework.security.acls.domain.PrincipalSid;
    import org.springframework.security.acls.model.Permission;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.util.Assert;

    import javax.persistence.EntityManager;
    import javax.persistence.TypedQuery;
    import javax.persistence.criteria.*;
    import java.io.Serializable;
    import java.util.Collections;
    import java.util.List;

    /**
    * Default implementation of the {@link AclJpaRepository} interface.
    * This will offer you a more sophisticated interface than the plain EntityManager.
    *
    * @param <T> Entity domain type
    * @param <ID> Unique identifier's type
    */

    @SuppressWarnings({"unchecked", "WeakerAccess"})
    public class SimpleAclJpaRepository<T, ID extends Serializable>
    extends SimpleJpaRepository<T, ID> implements AclJpaRepository<T, ID> {

    private JpaEntityInformation<T, ?> entityInformation;
    private EntityManager entityManager;

    public SimpleAclJpaRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {

    super(entityInformation, entityManager);

    this.entityInformation = entityInformation;
    this.entityManager = entityManager;
    }

    public SimpleAclJpaRepository(Class<T> domainClass, EntityManager entityManager) {
    this(JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager), entityManager);
    }

    private static long executeCountQuery(TypedQuery<Long> query) {

    Assert.notNull(query, "TypedQuery must not be null!");
    List<Long> totals = query.getResultList();

    return totals.stream().mapToLong(total -> null == total ? 0 : total).sum();
    }

    @Override
    public List<T> findAll(Permission permission) {
    return findAll((Specification) null, permission);
    }

    @Override
    public Page<T> findAll(Pageable pageable, Permission permission) {
    return findAll(null, pageable, permission);
    }

    @Override
    public List<T> findAll(Specification<T> spec, Permission permission) {

    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

    if (null == authentication || !authentication.isAuthenticated()) {
    throw new IllegalStateException("Permission filtering not possible for anonymous user");
    }

    UserDetails userDetails = (UserDetails) authentication.getPrincipal();
    PrincipalSid sid = new PrincipalSid(userDetails.getUsername());

    return this.getQuery(spec, Sort.unsorted(), sid, permission).getResultList();
    }

    @Override
    public Page<T> findAll(Specification<T> spec, Pageable pageable, Permission permission) {

    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

    if (null == authentication || !authentication.isAuthenticated()) {
    throw new IllegalStateException("Permission filtering not possible for anonymous user");
    }

    UserDetails userDetails = (UserDetails) authentication.getPrincipal();
    PrincipalSid sid = new PrincipalSid(userDetails.getUsername());

    TypedQuery<T> query = getQuery(spec, pageable, sid, permission);

    return pageable.isUnpaged() ? new PageImpl<>(query.getResultList()) :
    readPage(query, this.getDomainClass(), pageable, spec, sid, permission);
    }

    /**
    * Reads the given {@link TypedQuery} into a {@link Page} applying the given {@link Pageable},
    * {@link Specification} and permission filter.
    *
    * @param query Typed JPA query
    * @param domainClass Class of domain entity
    * @param pageable Pageable configuration
    * @param spec Additional specification
    * @param sid ACL authorization principal
    * @param permission Permission filter criteria
    * @param <S> Domain type
    * @return Returns a Page of entities matching the permission criteria
    */

    protected <S extends T> Page<S> readPage(TypedQuery<S> query, Class<S> domainClass, Pageable pageable,
    @Nullable Specification<S> spec, PrincipalSid sid, Permission permission) {

    if (pageable.isPaged()) {

    query.setFirstResult((int) pageable.getOffset());
    query.setMaxResults(pageable.getPageSize());
    }

    return PageableExecutionUtils.getPage(query.getResultList(), pageable,
    () -> executeCountQuery(getCountQuery(spec, domainClass, sid, permission)));
    }

    /**
    * Creates a new {@link TypedQuery} based for the given {@link Specification specification} and
    * {@link Permission permission} filter criteria.
    *
    * @param spec Additional specification
    * @param pageable Pageable configuration
    * @param sid ACL authorization principal
    * @param permission Permission filter criteria
    * @return Returns the related TypedQuery
    */

    protected TypedQuery<T> getQuery(@Nullable Specification<T> spec, Pageable pageable,
    PrincipalSid sid, Permission permission) {

    Sort sort = pageable.isPaged() ? pageable.getSort() : Sort.unsorted();
    return getQuery(spec, this.getDomainClass(), sort, sid, permission);
    }

    /**
    * Creates a new {@link TypedQuery} based for the given {@link Specification specification},
    * {@link Permission permission} filter criteria and {@link Sort}.
    *
    * @param spec Additional specification
    * @param sort Sorting configuration
    * @param sid ACL authorization principal
    * @param permission Permission filter criteria
    * @return Returns the related TypedQuery
    */

    protected TypedQuery<T> getQuery(@Nullable Specification<T> spec, Sort sort,
    PrincipalSid sid, Permission permission) {

    return this.getQuery(spec, this.getDomainClass(), sort, sid, permission);
    }

    /**
    * Creates a new {@link TypedQuery} based for the given {@link Specification specification},
    * {@link Permission permission} filter criteria and {@link Sort}.
    *
    * @param spec Additional specification
    * @param domainClass Class of domain entity
    * @param sort Sorting configuration
    * @param sid ACL authorization principal
    * @param permission Permission filter criteria
    * @param <S> Domain type
    * @return Returns the related TypedQuery
    */

    protected <S extends T> TypedQuery<S> getQuery(@Nullable Specification<S> spec, Class<S> domainClass, Sort sort,
    PrincipalSid sid, Permission permission) {

    CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
    CriteriaQuery<S> criteriaQuery = criteriaBuilder.createQuery(domainClass);
    Root<S> root = applySpecificationToCriteria(spec, domainClass, criteriaQuery, sid, permission);

    criteriaQuery.select(root);

    if (sort.isSorted()) {
    criteriaQuery.orderBy(QueryUtils.toOrders(sort, root, criteriaBuilder));
    }

    return entityManager.createQuery(criteriaQuery);
    }

    protected <S extends T> TypedQuery<Long> getCountQuery(@Nullable Specification<S> spec, Class<S> domainClass,
    PrincipalSid sid, Permission permission) {

    CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
    CriteriaQuery<Long> criteriaQuery = criteriaBuilder.createQuery(Long.class);
    Root<S> root = applySpecificationToCriteria(spec, domainClass, criteriaQuery, sid, permission);

    if (criteriaQuery.isDistinct()) {

    criteriaQuery.select(criteriaBuilder.countDistinct(root));

    } else {

    criteriaQuery.select(criteriaBuilder.count(root));
    }

    criteriaQuery.orderBy(Collections.emptyList());
    return entityManager.createQuery(criteriaQuery);
    }

    private <S, U extends T> Root<U> applySpecificationToCriteria(@Nullable Specification<U> spec,
    Class<U> domainClass, CriteriaQuery<S> query,
    PrincipalSid sid, Permission permission) {

    Assert.notNull(domainClass, "Domain class must not be null!");
    Assert.notNull(query, "CriteriaQuery must not be null!");

    Root<U> root = query.from(domainClass);

    if (null == spec) {

    query.where(filterPermitted(root, query, domainClass, sid, permission));
    return root;

    } else {

    CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
    Predicate predicate = spec.toPredicate(root, query, criteriaBuilder);

    if (null != predicate) {

    query.where(criteriaBuilder.and(predicate, filterPermitted(root, query, domainClass, sid, permission)));

    } else {

    query.where(filterPermitted(root, query, domainClass, sid, permission));
    }

    return root;
    }
    }

    private <S, U extends T> Predicate filterPermitted(Root<U> root, CriteriaQuery<S> query,
    Class<U> domainClass, PrincipalSid sid, Permission permission) {

    return root.<Long>get(entityInformation.getRequiredIdAttribute().getName())
    .in(selectPermittedIds(query, domainClass, sid, permission));
    }

    private <S> Subquery<Long> selectPermittedIds(CriteriaQuery<S> query, Class<?> targetType, PrincipalSid sid,
    Permission permission) {

    CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
    Subquery<Long> aclEntryQuery = query.subquery(Long.class);
    Root<AclEntry> root = aclEntryQuery.from(AclEntry.class);
    Join<AclEntry, AclObjectIdentity> aclObjectIdentityJoin = root.join("aclObjectIdentity");

    return aclEntryQuery.select(aclObjectIdentityJoin.get("objectIdIdentity"))
    .where(criteriaBuilder.and(
    root.<Long>get("aclObjectIdentity").in(selectAclObjectIdentityId(aclEntryQuery, targetType)),
    criteriaBuilder.equal(root.<Long>get("aclSid"), selectAclSidId(aclEntryQuery, sid)),
    criteriaBuilder.equal(root.<Integer>get("mask"), permission.getMask())));
    }

    private <S> Subquery<Long> selectAclObjectIdentityId(Subquery<S> query, Class<?> targetType) {

    CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
    Subquery<Long> aclObjectIdentityQuery = query.subquery(Long.class);
    Root<AclObjectIdentity> root = aclObjectIdentityQuery.from(AclObjectIdentity.class);

    return aclObjectIdentityQuery.select(root.get("id"))
    .where(criteriaBuilder.equal(root.<Long>get("objectIdClass"),
    selectAclClassId(aclObjectIdentityQuery, targetType)));
    }

    private <S> Subquery<Long> selectAclSidId(Subquery<S> query, PrincipalSid sid) {

    CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
    Subquery<Long> aclSidQuery = query.subquery(Long.class);
    Root<AclSid> root = aclSidQuery.from(AclSid.class);

    return aclSidQuery.select(root.get("id"))
    .where(criteriaBuilder.equal(root.<String>get("sid"), sid.getPrincipal()));
    }

    private <S> Subquery<Long> selectAclClassId(Subquery<S> query, Class<?> targetType) {

    CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
    Subquery<Long> aclClassQuery = query.subquery(Long.class);
    Root<AclClass> root = aclClassQuery.from(AclClass.class);

    return aclClassQuery.select(root.get("id"))
    .where(criteriaBuilder.equal(root.<String>get("className"), targetType.getSimpleName()));
    }
    }
  2. MuellerConstantin created this gist Apr 14, 2020.
    22 changes: 22 additions & 0 deletions AclClass.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,22 @@
    import lombok.*;
    import org.springframework.data.annotation.Immutable;

    import javax.persistence.*;

    @Entity
    @Immutable
    @Table(name = "acl_class")
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @EqualsAndHashCode
    @ToString
    public final class AclClass {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "class", nullable = false)
    private String className;
    }
    44 changes: 44 additions & 0 deletions AclEntry.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,44 @@
    import lombok.*;
    import org.springframework.data.annotation.Immutable;

    import javax.persistence.*;

    @Entity
    @Immutable
    @Table(name = "acl_entry", uniqueConstraints = {
    @UniqueConstraint(name = "_ak_acl_object_identity_ace_order", columnNames = {"acl_object_identity", "ace_order"})
    })
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @EqualsAndHashCode
    @ToString
    public final class AclEntry {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(optional = false)
    @JoinColumn(name = "acl_object_identity", referencedColumnName = "id", nullable = false)
    private AclObjectIdentity aclObjectIdentity;

    @Column(name = "ace_order", nullable = false)
    private int aceOrder;

    @ManyToOne(optional = false)
    @JoinColumn(name = "sid", referencedColumnName = "id", nullable = false)
    private AclSid aclSid;

    @Column(name = "mask", nullable = false)
    private int mask;

    @Column(name = "granting", nullable = false)
    private boolean granting;

    @Column(name = "audit_success", nullable = false)
    private boolean auditSuccess;

    @Column(name = "audit_failure", nullable = false)
    private boolean auditFailure;
    }
    61 changes: 61 additions & 0 deletions AclJpaRepository.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,61 @@
    import org.springframework.data.domain.Page;
    import org.springframework.data.domain.Pageable;
    import org.springframework.data.jpa.domain.Specification;
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.data.repository.NoRepositoryBean;
    import org.springframework.security.acls.model.Permission;

    import java.util.List;

    /**
    * ACL specific extension of {@link JpaRepository}. Extends by supporting collection filtering
    * based on ACL {@link Permission permissions}.
    *
    * @param <T> Entity domain type
    * @param <ID> Unique identifier's type
    * @author 0x1C1B
    */

    @NoRepositoryBean
    public interface AclJpaRepository<T, ID> extends JpaRepository<T, ID> {

    /**
    * Finds all available entities filtered by ACL permission.
    *
    * @param permission Permission filter criteria
    * @return Returns a list of all matching entities
    */

    List<T> findAll(Permission permission);

    /**
    * Fetches all available entities filtered by ACL permission as a {@link Page}.
    *
    * @param permission Permission filter criteria
    * @return Returns a Page of entities matching the permission criteria
    */

    Page<T> findAll(Pageable pageable, Permission permission);

    /**
    * Finds all available entities filtered by ACL permission and matching the given
    * {@link Specification}.
    *
    * @param spec Given specification
    * @param permission Permission filter criteria
    * @return Returns a list of all matching entities
    */

    List<T> findAll(Specification<T> spec, Permission permission);

    /**
    * Fetches all available entities matching the given {@link Specification} and
    * filtered by ACL permission as a {@link Page}.
    *
    * @param spec Given specification
    * @param permission Permission filter criteria
    * @return Returns a Page of entities matching the permission criteria
    */

    Page<T> findAll(Specification<T> spec, Pageable pageable, Permission permission);
    }
    36 changes: 36 additions & 0 deletions AclObjectIdentity.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,36 @@
    import lombok.*;
    import org.springframework.data.annotation.Immutable;

    import javax.persistence.*;

    @Entity
    @Immutable
    @Table(name = "acl_object_identity", uniqueConstraints = {
    @UniqueConstraint(name = "_ak_object_id_class_object_id_identity", columnNames = {"object_id_class", "object_id_identity"})
    })
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @EqualsAndHashCode
    @ToString
    public final class AclObjectIdentity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(optional = false)
    @JoinColumn(name = "object_id_class", referencedColumnName = "id", nullable = false)
    private AclClass objectIdClass;

    @Column(name = "object_id_identity", nullable = false)
    private Long objectIdIdentity;

    @ManyToOne
    @JoinColumn(name = "parent_object", referencedColumnName = "id")
    private AclObjectIdentity parentObject;

    @ManyToOne(optional = false)
    @JoinColumn(name = "owner_sid", referencedColumnName = "id", nullable = false)
    private AclSid ownerSid;
    }
    27 changes: 27 additions & 0 deletions AclSid.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,27 @@
    import lombok.*;
    import org.springframework.data.annotation.Immutable;

    import javax.persistence.*;

    @Entity
    @Immutable
    @Table(name = "acl_sid", uniqueConstraints = {
    @UniqueConstraint(name = "_ak_sid_principal", columnNames = {"sid", "principal"})
    })
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @EqualsAndHashCode
    @ToString
    public final class AclSid {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "principal", nullable = false)
    private boolean principal;

    @Column(name = "sid", nullable = false, length = 100)
    private String sid;
    }