epubli4/permission-bundle

简化权限使用的包

安装次数: 1,614

依赖项: 1

建议者: 0

安全: 0

星标: 3

关注者: 4

分支: 0

公开问题: 1

类型:symfony-bundle

v0.1.34 2021-07-02 07:33 UTC

README

为e4中的微服务简化使用api-platform的权限的包

安装

请确保已全局安装Composer,如Composer文档中的安装章节所述。

使用Symfony Flex的应用程序

打开命令行,进入您的项目目录,并执行

$ composer require epubli4/permission-bundle

推荐用于单元测试

$ composer require l0wskilled/api-platform-test >=0.1.21

不使用Symfony Flex的应用程序

步骤1:下载Bundle

打开命令行,进入您的项目目录,并执行以下命令以下载此bundle的最新稳定版本

$ composer require epubli4/permission-bundle

推荐用于单元测试

$ composer require l0wskilled/api-platform-test >=0.1.21

步骤2:启用Bundle

然后,通过将其添加到项目中config/bundles.php文件中已注册的bundle列表中来启用该bundle。

// config/bundles.php

return [
    // ...
    Epubli\PermissionBundle\EpubliPermissionBundle::class => ['all' => true],
];

配置

请确保在config/packages/epubli_permission.yaml(如果尚不存在,请创建此文件)中插入您的微服务名称。示例

// config/packages/epubli_permission.yaml

epubli_permission:
  microservice_name: CHANGE_ME_TO_THE_NAME_OF_YOUR_MICROSERVICE

  # where the permissions of this microservice should be send to
  permission_export_route:
    base_uri: http://user
    path: /api/permissions/import
    permission: user.permission.create_permissions

  # where to get all permissions for a specific user
  aggregated_permissions_route:
    base_uri: http://user
    # {user_id} will be dynamically replaced
    path: /api/users/{user_id}/aggregated-permissions
    permission: user.user.user_get_aggregated_permissions

如果尚不存在,请创建此文件config/packages/test/epubli_permission.yaml

// config/packages/test/epubli_permission.yaml

epubli_permission:
  is_test_environment: true

config/packages/doctrine.yaml中激活doctrine过滤器

// config/packages/doctrine.yaml

doctrine:
  orm:
    filters:
      epubli_permission_bundle_self_permission_filter:
        class: Epubli\PermissionBundle\Filter\SelfPermissionFilter

使用方法

通常

您需要指定security键以为此端点启用此bundle。

use ApiPlatform\Core\Annotation\ApiResource;

/**
 * @ApiResource(
 *     collectionOperations={
 *          "get"={
 *              "security"="is_granted(null, _api_resource_class)",
 *          },
 *          "post"={
 *              "security_post_denormalize"="is_granted(null, object)",
 *          },
 *     },
 *     itemOperations={
 *          "get"={
 *              "security"="is_granted(null, object)",
 *          },
 *          "delete"={
 *              "security"="is_granted(null, object)",
 *          },
 *          "put"={
 *              "security"="is_granted(null, object)",
 *          },
 *          "patch"={
 *              "security"="is_granted(null, object)",
 *          },
 *     }
 * )
 */
class ExampleEntity
{

}

如果您希望此bundle能够区分拥有此类实体或未拥有的用户,则需要实现SelfPermissionInterface

use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\EntityManagerInterface;
use Epubli\PermissionBundle\Interfaces\SelfPermissionInterface;

class ExampleEntity implements SelfPermissionInterface
{
    /**
     * @ORM\Column(type="integer")
     */
    private $user_id;

    public function getUserId(): ?int
    {
        return $this->user_id;
    }

    /**
     * @inheritDoc
     */
    public function getUserIdForPermissionBundle(): ?int
    {
        return $this->getUserId();
    }

    /**
     * @inheritDoc
     */
    public function getFieldNameOfUserIdForPermissionBundle(): string
    {
        return 'user_id';
    }

    /**
     * @inheritDoc
     */
    public function hasUserIdProperty(): bool
    {
        return true;
    }

    /**
     * @inheritDoc
     */
    public function getPrimaryIdsWhichBelongToUser(EntityManagerInterface $entityManager, int $userId): array
    {
        return [];
    }
}

或使用SelfPermissionTrait来实现SelfPermissionInterface的默认实现

use Doctrine\ORM\Mapping as ORM;
use Epubli\PermissionBundle\Interfaces\SelfPermissionInterface;
use Epubli\PermissionBundle\Traits\SelfPermissionTrait;

class ExampleEntity implements SelfPermissionInterface
{
    use SelfPermissionTrait;

    /**
     * @ORM\Column(type="integer")
     */
    private $user_id;

    public function getUserId(): ?int
    {
        return $this->user_id;
    }
}

如果您有一个没有userId但与具有userId的另一个实体有关联的实体,则需要自行实现SelfPermissionInterface的方法。

use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use Epubli\PermissionBundle\Interfaces\SelfPermissionInterface;

class ExampleEntity implements SelfPermissionInterface
{
    /**
     * @ORM\OneToOne(targetEntity=OtherEntity::class, inversedBy="exampleEntity", cascade={"persist", "remove"})
     * @ORM\JoinColumn(nullable=false)
     */
    private $otherEntity;

    public function getOtherEntity(): ?OtherEntity
    {
        return $this->otherEntity;
    }

    public function getPrimaryIdsWhichBelongToUser(EntityManagerInterface $entityManager, int $userId): array
    {
        /** @var Query $query */
        $query = $entityManager->getRepository(__CLASS__)
            ->createQueryBuilder('c')
            ->select('c.id')
            ->join('c.otherEntity', 'u')
            ->where('u.userId = :userId')
            ->setParameter('userId', $userId)
            ->getQuery();

        return array_column($query->getArrayResult(), 'id');
    }

    public function getUserIdForPermissionBundle(): ?int
    {
        return $this->getOtherEntity()->getUserId();
    }

    public function getFieldNameOfUserIdForPermissionBundle(): string
    {
        return '';
    }

    public function hasUserIdProperty(): bool
    {
        return false;
    }
}

访问令牌

您可以将它用作服务。它支持自动装配。这使您能够访问用户的访问令牌属性。

namespace App\Controller;

use Epubli\PermissionBundle\Service\AccessToken;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class TestAction extends AbstractController
{
    public function __invoke(AccessToken $accessToken)
    {
        var_dump('Is the token present and valid: ' . $accessToken->exists());
        var_dump('This is the unique json token identifier: ' . $accessToken->getJTI());
        var_dump('The id of the user: ' . $accessToken->getUserId());
        var_dump('Checking for permissions: ' . $accessToken->hasPermissionKey('user.user.delete'));
    }
}

自定义权限

为了使自定义权限正常工作,您需要将注释添加到使用它的方法中。

示例

namespace App\Controller;

use Epubli\PermissionBundle\Annotation\Permission;
use Epubli\PermissionBundle\Service\AccessToken;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;

class TestController extends AbstractController
{
    /**
     * @Permission(
     *     key="customPermission1",
     *     description="This is a description"
     * )
     * @Permission(
     *     key="customPermission2",
     *     description="This is a description"
     * )
     */
    public function postTest(AccessToken $accessToken)
    {
        if (!$accessToken->exists()){
            throw new UnauthorizedHttpException('Bearer', 'Access-Token is invalid.');
        }

        if (!$accessToken->hasPermissionKey('test.customPermission1')){
            throw new AccessDeniedHttpException('Missing permission key: test.customPermission1');
        }

        //User is now authenticated and authorized for customPermission1

        if (!$accessToken->hasPermissionKey('test.customPermission2')){
            throw new AccessDeniedHttpException('Missing permission key:  test.customPermission2');
        }

        //User is now authenticated and authorized for customPermission2
    }
}

您的微服务名称将自动添加到权限键的前面。

测试

为了使用此bundle测试您的应用程序,您需要一种向其发送JsonWebTokens的方式,否则测试端点将是不可能的,您的请求将被拒绝。

您需要至少v0.1.21版本的https://github.com/epubli/api-platform-test。

最简单的方法是只需将以下内容包含到您的测试用例中。这样,每个请求都将有权访问每个端点。

use Epubli\ApiPlatform\TestBundle\OrmApiPlatformTestCase;
use Epubli\PermissionBundle\Traits\JWTMockTrait;

class JsonWebTokenTest extends OrmApiPlatformTestCase
{
    use JWTMockTrait;

    public static function setUpBeforeClass(): void
    {
        self::setUpJsonWebTokenMockCreator();
    }

    public function setUp(): void
    {
        parent::setUp();
        self::$kernelBrowser->getCookieJar()->set(self::$cachedCookie);
    }
}

如果您想有更多的控制权,并且不想让每个请求都有令牌

use Epubli\ApiPlatform\TestBundle\OrmApiPlatformTestCase;
use Epubli\PermissionBundle\Traits\JWTMockTrait;

class JsonWebTokenTest extends OrmApiPlatformTestCase
{
    use JWTMockTrait;

    public static function setUpBeforeClass(): void
    {
        self::setUpJsonWebTokenMockCreator();
    }

    public function testRetrieveTheResourceList(): void
    {
        self::$kernelBrowser->getCookieJar()->set(self::$cachedCookie);
        $this->request(
            '/api/json_web_tokens',
            'GET'
        );
    }
}

存在UnitTestTrait特质来帮助您为常见用例编写单元测试。此特质有一个配置(self::$unitTestConfig),在其中您描述实体。此特质为您执行/生成单元测试。它要求您实现返回单元测试中使用的数据的方法。以下是如何用于支持任何操作的实体的示例。

    
use Epubli\ApiPlatform\TestBundle\OrmApiPlatformTestCase;
use Epubli\PermissionBundle\Traits\JWTMockTrait;
use Epubli\PermissionBundle\Traits\UnitTestTrait;
use Epubli\PermissionBundle\UnitTestHelpers\UnitTestConfig;
use Epubli\PermissionBundle\UnitTestHelpers\UnitTestDeleteData;
use Epubli\PermissionBundle\UnitTestHelpers\UnitTestGetCollectionData;
use Epubli\PermissionBundle\UnitTestHelpers\UnitTestGetItemData;
use Epubli\PermissionBundle\UnitTestHelpers\UnitTestPostData;
use Epubli\PermissionBundle\UnitTestHelpers\UnitTestUpdateData;

class CompanyDataTest extends OrmApiPlatformTestCase
{
    use JWTMockTrait;
    use UnitTestTrait;

    public const RESOURCE_URI = '/api/company_datas/';

    public static function setUpBeforeClass(): void
    {
        self::setUpJsonWebTokenMockCreator();
        self::$unitTestConfig = new UnitTestConfig();
    }

    public function setUp(): void
    {
        parent::setUp();
        self::$kernelBrowser->getCookieJar()->set(self::$cachedCookie);
    }

    protected function getDemoEntity(): CompanyData
    {
        $userProfileTestDummy = (new UserProfileTest())->getDemoEntity();
        $this->persistAndFlush($userProfileTestDummy);

        $companyData = new CompanyData();
        $companyData->setCompanyName(self::$faker->company);
        $companyData->setValueAddedTaxNumber((string)self::$faker->randomNumber());
        $companyData->setUserProfile($userProfileTestDummy);
        $companyData->setCreatedAt(self::$faker->dateTimeBetween('-200 days', 'now'));
        $companyData->setUpdatedAt(self::$faker->dateTimeBetween($companyData->getCreatedAt(), 'now'));
        return $companyData;
    }

    public function getDeleteDataForPermissionBundle(): ?UnitTestDeleteData
    {
        /** @var CompanyData $companyData */
        $companyData = $this->findOne(CompanyData::class);
        $userId = $companyData->getUserProfile()->getUserId();

        return new UnitTestDeleteData(
            self::RESOURCE_URI . $companyData->getId(),
            'user-profile.company_data.delete',
            $userId
        );
    }

    public function getUpdateDataForPermissionBundle(): ?UnitTestUpdateData
    {
        /** @var CompanyData $companyData */
        $companyData = $this->findOne(CompanyData::class);
        $userId = $companyData->getUserProfile()->getUserId();

        return new UnitTestUpdateData(
            self::RESOURCE_URI . $companyData->getId(),
            'user-profile.company_data.update.companyName',
            $userId,
            json_encode(
                [
                    'companyName' => 'new Company Name',
                ]
            ),
            'companyName',
            'new Company Name'
        );
    }

    public function getPostDataForPermissionBundle(): ?UnitTestPostData
    {
        $companyData = $this->getDemoEntity();
        $userId = $companyData->getUserProfile()->getUserId();

        return new UnitTestPostData(
            self::RESOURCE_URI,
            'user-profile.company_data.create',
            $userId,
            json_encode(
                [
                    'companyName' => $companyData->getCompanyName(),
                    'valueAddedTaxNumber' => $companyData->getValueAddedTaxNumber(),
                    'userProfile' => '/api/user_profiles/' . $companyData->getUserProfile()->getId(),
                ]
            )
        );
    }

    public function getGetItemDataForPermissionBundle(): ?UnitTestGetItemData
    {
        /** @var CompanyData $companyData */
        $companyData = $this->findOne(CompanyData::class);
        $userId = $companyData->getUserProfile()->getUserId();

        return new UnitTestGetItemData(
            self::RESOURCE_URI . $companyData->getId(),
            'user-profile.company_data.read',
            $userId
        );
    }

    public function getGetCollectionDataForPermissionBundle(): ?UnitTestGetCollectionData
    {
        /** @var CompanyData $companyData */
        $companyData = $this->findOne(CompanyData::class);
        $userId = $companyData->getUserProfile()->getUserId();

        return new UnitTestGetCollectionData(
            self::RESOURCE_URI,
            'user-profile.company_data.read',
            $userId,
            1
        );
    }
}

如果您的实体不支持每个操作,则需要调整配置

use Epubli\ApiPlatform\TestBundle\OrmApiPlatformTestCase;
use Epubli\PermissionBundle\Traits\UnitTestTrait;
use Epubli\PermissionBundle\UnitTestHelpers\UnitTestConfig;

class ExampleTest extends OrmApiPlatformTestCase
{
    use UnitTestTrait;

    public static function setUpBeforeClass(): void
    {
        self::$unitTestConfig = new UnitTestConfig();

        // If you implemented the SelfPermissionInterface in your entity
        // then set this to true (defaults to true):
        self::$unitTestConfig->implementsSelfPermissionInterface = true;
        
        // If you do not have a DELETE route for your entity
        // then set this to false (defaults to true):
        self::$unitTestConfig->hasDeleteRoute = true;

        // If your DELETE route requires no acccess control
        // then set this to false (defaults to true):
        self::$unitTestConfig->hasSecurityOnDeleteRoute = true;

        //The config has booleans for every standard HTTP operation
    }
}

导出命令

要将您的微服务的权限导出到用户微服务,您需要在 Docker 容器中执行以下操作

$ php bin/console epubli:export-permissions

测试

执行以下操作

$ make unit_test

或者

$ ./vendor/bin/simple-phpunit

如何更改/添加此捆绑包中的代码

进一步开发此捆绑包的最简单方法是将其 src 文件夹复制到另一个项目(例如,用户微服务)。

在项目内创建一个名为 permission-bundle 的文件夹,并将 src 文件夹复制到其中。

然后在 composer.json 中查找以下内容

  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  },

并将其替换为

  "autoload": {
    "psr-4": {
      "App\\": "src/",
      "Epubli\\PermissionBundle\\": "permission-bundle/src"
    }
  },

删除 vendor 文件夹中的原始 permission-bundle

执行

$ composer dump-autoload

您可能需要在 var/cache/dev 中删除一些内容。

问题

当通过 GET 请求请求多个实体时,使用 SelfPermissionInterfacehydra:totalItems 可能会不正确。

因为分页器在查询应用任何过滤器之前被调用,因此项目/实体数量将不正确。 hydra:totalItems 不等于返回的项目/实体数量。

此线程中的解决方案不起作用:[api-platform/core#1185](https://github.com/api-platform/core/issues/1185)

需要完成的事情

  • ApiPlatform 子资源
  • 如果不存在令牌,则需要应用匿名角色的权限。