Фильтр разбиения на страницы Spring QueryDsl по разрешениям ACL - PullRequest
2 голосов
/ 22 марта 2020

Предположим, что следующий репозиторий на основе Spring JPA с поддержкой QueryDsl.

@Repository
public interface TeamRepository extends JpaRepository<Team, Long>, QuerydslPredicateExecutor<Team> {

}

Приложение использует Списки контроля доступа (ACL) в сервисном слое для проверки разрешения для отдельных ресурсов с использованием * Например, 1006 *.

Я хочу разрешить пользователю запрашивать все команды, для которых у него есть разрешение на чтение. Я пытался использовать @PostFilter(hasPermission(filterObject, 'READ'), это работает довольно хорошо, пока я использую Iterable<Team> findAll(Predicate predicate). Но когда я пытаюсь использовать нумерацию страниц, @PostFilter, похоже, выдает исключение.

java.lang.IllegalArgumentException: Filter target must be a collection, array, or stream type, but was Page 1 of 0 containing UNKNOWN instances

Официальная Справочная документация по безопасности Spring рекомендует написать пользовательский запрос, используя @Query который поддерживает разбиение на страницы.

Как можно написать такой сложный запрос, который поддерживает Предикат QueryDsl , Пагинация и фильтрация на основе разрешений ?

Подход 03/24/20

В другом форуме Я столкнулся со следующим подходом на основе QueryDsl: Вместо собственного или пользовательского запроса, таблицы ACL отображаются как @Immutable сущности JPA, таким образом генерируя классы Q и используя их для ручной фильтрации разрешений.

@Entity
@Immutable
@Table(name = "acl_object_identity")
public class AclObjectIdentity implements Serializable {

    ...
}

Как это можно сделать с помощью собственного репозитория, расширяющего QueryDslRepositorySupport, чтобы часть запроса, проверяющая разрешения, автоматически добавлялась и скрывалась внутри пользовательской реализации репозитория?

1 Ответ

2 голосов
/ 28 марта 2020

На основе этого подхода я разработал возможность, которая является более грязным обходным путем, чем решение.

Подход заключается в добавлении дополнительного фильтра разрешений к существующим предикатам, например сгенерировано веб-поддержкой . Для этого таблицы ACL должны сначала отображаться как @Immutable объекты JPA, чтобы QueryDsl мог генерировать соответствующие классы Q.

Такие предикаты, к которым должен быть добавлен фильтр разрешений ACL, помечаются следующей аннотацией.

public Page<PostDTO> findAll(@QueryDslAclPermission(root = Post.class, permission = "READ") Predicate predicate, Pageable pageable) {

    ...
}

Эта аннотация содержит в основном метаинформацию о типе домена, необходимую для построения запроса фильтра.

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.TYPE})
public @interface QueryDslAclPermission {

    Class<?> root();

    String permission();

    String identifier() default "id";
}

Фактический запрос фильтра создается и добавляется с использованием следующего класса и Модуль AOP Spring .

@Aspect
@Component
public class QueryDslAclPermissionAspect {

    private PermissionFactory permissionFactory;

    @Autowired
    public QueryDslAclPermissionAspect(PermissionFactory permissionFactory) {
        this.permissionFactory = permissionFactory;
    }

    @Around(value = "execution(* *(.., @QueryDslAclPermission (*), ..))")
    public Object addPermissionFilter(ProceedingJoinPoint joinPoint) throws Throwable {

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Parameter[] parameters = method.getParameters();
        Object[] arguments = joinPoint.getArgs();

        for(int index = 0; index < parameters.length; ++index) {

            if(parameters[index].getType().equals(Predicate.class) &&
                    parameters[index].isAnnotationPresent(QueryDslAclPermission.class)) {

                Predicate predicate = (Predicate) arguments[index];
                QueryDslAclPermission aclPermission = parameters[index].getAnnotation(QueryDslAclPermission.class);

                arguments[index] = addPermissionFilter(predicate, aclPermission);
            }
        }

        return joinPoint.proceed(arguments);
    }

    private Predicate addPermissionFilter(Predicate predicate, QueryDslAclPermission aclPermission) {

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

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

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

        NumberPath<Long> idPath = new PathBuilderFactory().create(aclPermission.root())
                .getNumber(aclPermission.identifier(), Long.class);

        return idPath.in(selectPermitted(aclPermission.root(), principalSid,
                permissionFactory.buildFromName(aclPermission.permission()))).and(predicate);
    }

    private JPQLQuery<Long> selectPermitted(Class<?> targetType, PrincipalSid sid, Permission permission) {

        return selectAclEntry(targetType, sid, permission)
                .select(QAclEntry.aclEntry.aclObjectIdentity.objectIdIdentity);
    }

    private JPQLQuery<AclEntry> selectAclEntry(Class<?> targetType, PrincipalSid sid, Permission permission) {

        return new JPAQuery<AclEntry>().from(QAclEntry.aclEntry)
                .where(QAclEntry.aclEntry.aclObjectIdentity.id.in(selectAclObjectIdentity(targetType)
                        .select(QAclObjectIdentity.aclObjectIdentity.id))
                        .and(QAclEntry.aclEntry.aclSid.id.eq(selectAclSid(sid).select(QAclSid.aclSid.id)))
                        .and(QAclEntry.aclEntry.mask.eq(permission.getMask())));
    }

    private JPQLQuery<AclObjectIdentity> selectAclObjectIdentity(Class<?> targetType) {

        return new JPAQuery<AclObjectIdentity>().from(QAclObjectIdentity.aclObjectIdentity)
                .where(QAclObjectIdentity.aclObjectIdentity.objectIdClass.id.eq(selectAclClass(targetType)
                        .select(QAclClass.aclClass.id)));
    }

    private JPQLQuery<AclSid> selectAclSid(PrincipalSid sid) {

        return new JPAQuery<AclSid>().from(QAclSid.aclSid)
                .where(QAclSid.aclSid.sid.eq(sid.getPrincipal()));
    }

    private JPQLQuery<AclClass> selectAclClass(Class<?> targetType) {

        return new JPAQuery<AclClass>().from(QAclClass.aclClass)
                .where(QAclClass.aclClass.className.eq(targetType.getSimpleName()));
    }
}

Полный исходный код и конфигурацию см. в этом GitHub Gist .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...