保护实体的某些属性不被修改

Protecting some properties of an entity from modification

我有一个使用 Doctrine ORM 进行实体管理的 Symfony 3 应用程序。目前,我正在致力于启用 CRUD 支持。我已经发现我可以使用 security voters 来限制对实体或控制器的访问。例如,我将其配置为只有管理员才能创建、更新或删除类型 A 的实体。

对于我的实体类型 B 的实例,我还想授予相应所有者更新(而不是创建或删除)的权力,我很容易做到这一点。但是,不应允许所有者修改实体的所有属性——只能修改其中的一部分。我如何使用 Symfony 实现这一点?另外,我正在使用 Form Bundle 来创建和验证表单。

编辑: 我添加了一些相关代码。控制器调用 denyAccessUnlessGranted,这会触发投票器。澄清一下,该代码已经可以正常工作了。我的问题与我还没有的代码有关。

控制器:

public function editAction(Request $request, int $id) {

    $em = $this->getDoctrine()->getManager();

    $project = $em->getRepository(Project::class)->findOneBy(['id'=>$id]);

    $this->denyAccessUnlessGranted(ProjectVoter::EDIT, $project);

    $users = $em->getRepository(EntityUser::class)->findAll();
    $groups = $em->getRepository(Group::class)->findAll();
    $tags = $em->getRepository(Tag::class)->findAll();

    $form = $this->createForm(ProjectType::class, $project, [
        'possibleAdmins' => $users,
        'possibleRequiredGroups' => $groups,
        'possibleTags' => $tags,
    ]);

    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {

        $project = $form->getData();
        $em->flush();

        return $this->redirectToRoute('projects_show', ['id'=>$project->getId()]);
    }

    return $this->render('project/editor.html.twig',
        ['project'=>$project, 'form'=>$form->createView()]);
}

选民:

protected function voteOnAttribute($attribute, $subject, TokenInterface $token) {

    /** @var UserInterface $user */
    $user = $token->getUser();

    if (!$user instanceof UserInterface) {
        // the user must be logged in; if not, deny access
        return false;
    }
    else if ($this->decisionManager->decide($token, ['ROLE_ADMIN'])) {
        return true; // system-wide admins shall always have access
    }

    switch($attribute) {

        case self::SHOW:
            return  ($subject->isVisible() || $subject->getAdmins()->contains($user);

        case self::EDIT:
            return $subject->getAdmins()->contains($user);

        case self::REMOVE:
            return false;
    }

    return false;
}

据我所知,没有专门针对个别属性的访问功能。当然,只要我 post 这个,其他人就会带着那个过来。

您可能会考虑定义两个编辑角色,EDIT_BY_ADMIN 和 EDIT_BY_OWNER。然后您可以测试条件和 select 要使用的表单类型。

$projectTypeClass = null;
if ($this->isGranted(ProjectVoter::EDIT_BY_ADMIN,$project)) {
    $projectTypeClass = ProjectAdminType::class);
}
elseif ($this->isGranted(ProjectVoter::EDIT_BY_OWNER,$project)) {
    $projectTypeClass = ProjectOwnerType::class);
}
if (!$projectTypeClass) {
    // throw access denied exception
}
$form = $this->createForm($projectTypeClass, $project, [

这应该可以解决问题。当然有很多变化。您可以坚持使用一种项目类型并在类型 class 内进行访问测试,尽管这需要表单侦听器。

如果您需要更细化,那么您可以添加一些 EDIT_PROP1、EDIT_PROP2 类型的角色。

当然,如果您真的喜欢它,那么您可以将一些访问代码移到某种数据库中。或者看看那里的一些访问控制列表组件。

最后我想到了这个解决方案:

我没有使用多个 FormType,而是只使用了一个,最终根据选民的结果启用或禁用 [​​=38=] 的表单字段。为此,我按照 Cerad 的建议定义了一个新的开关案例(出于演示目的,在此答案中命名为 ProjectVoter::MODIFY_PROTECTED_PROPERTY)并根据我的喜好添加了业务逻辑。

我刚刚启用或禁用了表单字段,因为我实际上希望用户看到 he/she 无法编辑 属性。但是很可能很容易也没有 add 字段,所以它是不可见的。

表单类型:

信息:$this->tokenStorage$this->accessDecisionManager是注入服务(分别是"security.token_storage""security.access.decision_manager")。

public function buildForm(FormBuilderInterface $builder, array $options) {

    $token = $options['token'] ?? $this->tokenStorage->getToken();

    $project = $builder->getData();

    $builder
        ->add('name')

        // ...

        ->add('protectedProperty', null, [
            'disabled' => !$this->accessDecisionManager->decide($token, [ProjectVoter::MODIFY_PROTECTED_PROPERTY], $project),
        ])
    ;
}

我还在其 configureOptions 函数中为名为 token 的表单类型添加了一个选项,默认为 null,以便可以为任意用户构建表单而不是当前登录的那个,如果需要的话。