Flow Authorization errors

Hello and thanks for your help and time in advance. I’m currently building an API where people can upload Jobs of any kind. These Jobs all have an APIKey assigned to them. Jobs also do have a type, which should also be restricted. An APIKey contains all the allowed types for which the APIKey was created.

In summary:

  1. Create an EntityPrivilege to prevent other people fetching Jobs which were not created with their APIKey
  2. Restrict creation of Jobs with types, which are not allowed for the specific APIKey

The Problem:
Fetching of other entities not created with an APIKey is still possible (Added extra privilege not being used because of suggestions => now returns empty array)

Creation of types which are not allowed is also still possible with now restrictions being made.

For this, following Codes has been written.

Policy.yaml

privilegeTargets:
  'Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilege':
    'Webco.AIApi:ApiUploadEndpoint':
      matcher: 'method(Webco\AIApi\Controller\JobController->(bulk|get|delete)Action())'
    'Webco.AIApi:ApiUploadEndpoint.allowedAI':
      matcher: 'method(Webco\AIApi\Controller\JobController->createAction(job.type in current.ai.allowedAIs))'
  'Neos\Flow\Security\Authorization\Privilege\Entity\Doctrine\EntityPrivilege':
    'Webco.AIApi:JobEntity':
      matcher: 'isType("Webco\AIApi\Domain\Model\Job") && property("apiKey").equals("current.apikey.key")'
    'Webco.AIApi:JobEntityExtra':
      matcher: 'isType("Webco\AIApi\Domain\Model\Job")'

roles:
  'Webco.AIApi:ApiKey':
    privileges:
      - privilegeTarget: 'Webco.AIApi:ApiUploadEndpoint'
        permission: GRANT
      - privilegeTarget: 'Webco.AIApi:ApiUploadEndpoint.allowedAI'
        permission: GRANT
      - privilegeTarget: 'Webco.AIApi:JobEntity'
        permission: GRANT

Also there are contexts being used:
AIContext (Type):

final class AIContext implements CacheAwareInterface
{
    /**
     * @Flow\Inject
     * @var Context
     */
    protected $securityContext;

    /**
     * @Flow\Inject
     * @var PsrSystemLoggerInterface
     */
    protected $systemLogger;

    /**
     * @Flow\Inject
     * @var APIKeyRepository
     */
    protected $apiKeyRepository;

    public function getAllowedAIs(): array
    {
        /** @var APIKey $apiKey */
        $apiKey = $this->apiKeyRepository->findByAccount($this->securityContext->getAccount());
        return $apiKey->getAllowedAIs();
    }

    public function getCacheEntryIdentifier(): string
    {
        $ais = $this->getAllowedAIs();
        if (!$ais) {
            return sha1("ENOAIS");
            //throw new Exception('Customer not available', 1597409719);
        }

        $names = [];
        foreach ($ais as $ai) {
            $names[] = $ai->getName();
        }

        return sha1(implode(";", $names));
    }
}

and APIKeyContext:

   /**
     * @Flow\Scope("singleton")
     */
    class APIKeyContext implements CacheAwareInterface
    {
        /**
         * @Flow\Inject
         * @var LoggerInterface
         */
        protected $systemLogger;

        /**
         * @Flow\Inject
         * @var Context
         */
        protected $securityContext;

        /**
         * @Flow\Inject
         * @var BrokerRepository
         */
        protected $brokerRepository;

        /**
         * @Flow\Inject
         * @var APIKeyRepository
         */
        protected $apiKeyRepository;

        /**
         * @Flow\Inject
         * @var PersistenceManagerInterface
         */
        protected $persistenceManager;

        /**
         * @Flow\Inject
         * @var PolicyService
         */
        protected $policyService;

        /**
         * @return ?Customer
         */
        public function getKey(): ?APIKey
        {
            $account = $this->securityContext->getAccount();
            if ($account) {
                $name = $account->getAccountIdentifier();
                switch (true) {
                    case $account->hasRole($this->policyService->getRole('Webco.AIApi:ApiKey')):
                        /** @var APIKey $apiKey */
                        $apiKey = $this->apiKeyRepository->findOneByAccount($account);
                        $key = $apiKey->getPublicKey();
                        $this->systemLogger->debug(__CLASS__ . " APIKEY: $key");
                        return $apiKey;
                        break;
                    case $account->hasRole($this->policyService->getRole('Webco.AIApi:Broker')):
                        // Broker has no customer associated
                        /** @var Broker $broker */
                        //$broker = $this->brokerRepository->findByAccount($account);
                        break;
                    default:
                        // user account
                        break;
                }
            }
        }

        /**
         * @return string
         */
        public function getCacheEntryIdentifier(): string
        {
            $apiKey = $this->getKey();
            if (!$apiKey) {
                return sha1("ENOAPIKEY");
                //throw new Exception('Customer not available', 1597409719);
            }

            return $this->persistenceManager->getIdentifierByObject($apiKey);
        }
    }

Output has been correctly identified with systemLogger for both contexts.

My createAction() contains a Model Job which has the property “type” (string) mapped from the request body to an “type” (AI Model).

Got the 2. Problem solved by creating two separate MethodPrivileges:

 'Webco.AIApi:ApiUploadEndpoint.allowedAI':
      matcher: 'method(Webco\AIApi\Controller\JobController->createAction(job.type.name in current.ai.allowedAIsAsCollection))'
    'Webco.AIApi:ApiUploadEndpoint.deniedAI':
      matcher: 'method(Webco\AIApi\Controller\JobController->createAction(job.type.name in current.ai.notAllowedAIsAsCollection))'

While adding them to the role itself like this:

  'Webco.AIApi:ApiKey':
privileges:
  - privilegeTarget: 'Webco.AIApi:ApiUploadEndpoint'
    permission: GRANT
  - privilegeTarget: 'Webco.AIApi:ApiUploadEndpoint.allowedAI'
    permission: GRANT
  - privilegeTarget: 'Webco.AIApi:ApiUploadEndpoint.deniedAI'
    permission: DENY

Hello Filip and welcome to our forum :slight_smile:

You should never really need DENY privileges, only if you want to overrule existing privileges.

In general your privilege describes the action not whether it is allowed or denied.

If I get your requirements right, these two privileges should be sufficient:

 'Webco.AIApi:ApiUploadEndpoint.createJob':
    matcher: 'method(Webco\AIApi\Controller\JobController->createAction())'
  'Webco.AIApi:ApiUploadEndpoint.createJobWithAllowedAI':
    matcher: 'method(Webco\AIApi\Controller\JobController->createAction(job.type.name in current.ai.allowedAIsAsCollection))'

By default all privileges are ÀBSTAINED` (=> forbidden), so you only need to explicitly GRANT the specific one via

roles:
  'Webco.AIApi:SomeRole':
    privileges:
      -
        privilegeTarget: 'Webco.AIApi:ApiUploadEndpoint.createJobWithAllowedAI'
        permission: GRANT

You could grant the other one to some Admin role in order to allow corresponding users to create all kinds of jobs.

A slightly related tip: Instead of protecting some controller action I would recommend to create a dedicated service as central authority and protect all public methods on it by default:

privilegeTargets:
‘Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilege’:

'Webco.AIApi:JobService.PublicMethods':
  matcher: 'within(Webco\AIApi\JobService) && method(public .*->.*())'
'Webco.AIApi:JobService.CreateJobWithAllowedAI':
  matcher: 'method(Webco\AIApi\JobService->createJob(job.type.name in current.ai.allowedAIsAsCollection))'
1 Like

Thanks for your time and your detailed answer!

Regarding the privilege: It worked! I know understand why it wasn’t working and why it was necessary to add this “default” privilege so to say.

The service idea sounds interesting => can I also directly use the controller?

You’re welcome, great that it worked out!

directly use the controller? What do you mean by that?
You can use the service from your controller by injecting it like so:

<?php
namespace Webco\AIApi\Controller;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\Controller\ActionController;
use Webco\AIApi\Domain\JobService;

final class JobController extends ActionController {

  /**
   * @Flow\Inject
   * @var JobService
   */
  protected $jobService;

  public function createAction(...) {
    $this->jobService->createJob(...);
    $this->redirect('confirmation');
  }
}
1 Like

Ah I see. Moving the logic to the service itself and protecting the methods there. Thanks!!

2 Likes

Hello! If anyone is still reading this thread, I still need to get the EntityPrivilege working.

I currently have these two EntityPrivileges enabled:

  'Neos\Flow\Security\Authorization\Privilege\Entity\Doctrine\EntityPrivilege':
    'Webco.AIApi:JobEntity':
      matcher: 'isType("Webco\AIApi\Domain\Model\Job")'
    'Webco.AIApi:JobEntity.sameKey':
      matcher: 'isType("Webco\AIApi\Domain\Model\Job") && property("apiKey.customer.Persistence_Object_Identifier").equals("context.apikey.key.customer")'

  'Webco.AIApi:ApiKey':
    privileges:
      - privilegeTarget: 'Webco.AIApi:ApiUploadEndpoint'
        permission: GRANT
      - privilegeTarget: 'Webco.AIApi:ApiUploadEndpoint.allowedAI'
        permission: GRANT
      - privilegeTarget: 'Webco.AIApi:JobEntity.sameKey'
        permission: GRANT
#      - privilegeTarget: 'Webco.AIApi:JobEntityAllowed'
#        permission: GRANT

When looking at the Query_Development.log, I get these types of selects:

SELECT 
w0_.persistence_object_identifier AS persistence_object_identifier_0, 
w0_.state AS state_1, 
w0_.laststatechange AS laststatechange_2, 
w0_.properties AS properties_3, 
w0_.created AS created_4, 
w0_.inputurls AS inputurls_5, 
w0_.outputurls AS outputurls_6, 
w0_.result AS result_7, 
w0_.type AS type_8, 
w0_.broker AS broker_9, 
w0_.apikey AS apikey_10 
FROM webco_aiapi_domain_model_job w0_ 
WHERE 
  (
    w0_.state IN ('pending', 'assigned', 'processing') 
    AND 
    w0_.persistence_object_identifier IN ('e17a0a79-41bf-4585-b0d2-65478d5b005b', '0c45ba0b-69bc-43ad-8fac-83bb49362999', 'c3b66dba-7089-47dc-8f54-19a385fe267a', '50f26f88-dc34-4d7f-8154-c4f34b1b6668', 'b28929f7-44ae-4b3b-bc4e-f5d498b438f8', '52885688-d777-4a16-923b-0e1c726ec8d5', 'fe7ac493-c91d-4aea-ac82-009b9ff0b422')
  )
  AND 
  ( 
    ( NOT ( (1=1) )
  )
)

Any idea what is going on here?

EDIT: I have to add that the first two statements are created by my conditions (only specific states and only the ids which were requested by the user from the API)

This comes from the TrueConditionGenerator class used in the EntityPrivilege. How does your roles: part of Policy.yaml look?

1 Like

Poste the specific part of the roles which are important on top, but here are all of them:

roles:
  'Neos.Flow:Everybody':
privileges:
  - privilegeTarget: 'Webco.AIApi:PublicEndpoints'
    permission: GRANT

  'Webco.AIApi:ApiKey':
privileges:
  - privilegeTarget: 'Webco.AIApi:ApiUploadEndpoint'
    permission: GRANT
  - privilegeTarget: 'Webco.AIApi:ApiUploadEndpoint.allowedAI'
    permission: GRANT
  - privilegeTarget: 'Webco.AIApi:JobEntity.sameKey'
    permission: GRANT
#      - privilegeTarget: 'Webco.AIApi:JobEntityAllowed'
#        permission: GRANT

  'Webco.AIApi:User':
parentRoles:
  - 'Webco.AIApi:ApiKey'

  'Webco.AIApi:Broker':
privileges:
  - privilegeTarget: 'Webco.AIApi:BrokerEndpoint'
    permission: GRANT

Any ideas? Still cant get the API to properly work…