Announcement

Collapse
No announcement yet.

Is there a hook for AfterRead?

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

  • Is there a hook for AfterRead?

    I am trying to implement database level encryption at the entity level.

    I can create a `beforeSave` hook on the entity to encrypt data.

    However, I do not see any `afterRead` hook in documentation or code base, where I would decrypt.

    There is a `beforeRead` hook in the RecordService class:

    PHP Code:
        /**
         * Read a record by ID. Access control check is performed.
         *
         * @param non-empty-string $id
         * @return TEntity
         * @throws NotFoundSilent If not found.
         * @throws Forbidden If no read access.
         */
        
    public function read(string $idReadParams $params): Entity
        
    {
            if (
    $id === '') {
                throw new 
    InvalidArgumentException();
            }

            if (!
    $this->acl->check($this->entityTypeAclTable::ACTION_READ)) {
                throw new 
    ForbiddenSilent();
            }

            
    $entity $this->getEntity($id);

            if (!
    $entity) {
                throw new 
    NotFoundSilent("Record $id does not exist.");
            }

            
    $this->recordHookManager->processBeforeRead($entity$params);
            
    $this->processActionHistoryRecord(Action::READ$entity);

            return 
    $entity;
        }

    ​ 
    From this, it seems that the `beforeRead` is called after the entity is retrieved from the database, and so decryption in this hook would be workable.

    Am I reading this correctly?

    Thanks!

  • #2
    In metadata > recordDefs, for your entity type:

    Code:
    {
        "beforeReadHookClassNameList": [
             "Espo\\Custom\\Record\\MyEntityType\\BeforeReadHook
        ]
    }

    Comment


    • aldisa
      aldisa commented
      Editing a comment
      Thank you, Yuri!

      Similarly then, by using recordDefs, I can implement encryption by specifying a class in both beforeUpdateHookClassNameList and beforeCreateHookClassNameList?

  • #3
    I have implemented a beforeRead hook, but it is not executing.

    custom/Modules/MyModule/Resources/metadata/recordDefs/MyEntity.json
    PHP Code:
    {
      
    "beforeReadHookClassNameList": [
        
    "Espo\\Modules\\MyModule\\Hooks\\BeforeReadHook"
      
    ]
    }
    ​ 
    custom/Modules/MyModule/Hooks/BeforeReadHook.php
    PHP Code:
    <?php

    namespace Espo\Modules\MyModule\Hooks;

    use 
    Espo\ORM\Entity;
    use 
    Espo\Core\Record\Hook\ReadHook;
    use 
    Espo\Core\Record\ReadParams;

    class 
    BeforeReadHook implements ReadHook
    {   
        public function 
    __construct()
        {
           
    /** log initialization **/
        
    }

        public function 
    process(Entity $entityReadParams $params): void
        
    {
           
    /** log read **/
        
    }
    }
    Then if I run this code, there is no log of the hook getting initialized or processed.
    PHP Code:
    $entity $entityManager->getEntityById('MyEntity''idvalue'); 
    My understanding was that reading an entity using Entity Manager will execute the BeforeRead hook that is defined in the recordDefs.

    Also, since my objective is to decrypt the contents of an encrypted field, I am wondering whether the hook will succeed since the amended entity is not returned. Although I think Objects are handled by reference by default so technically it should.

    yuri please provide input.

    Comment


    • #4
      The Record hook affects only reading through the API.

      You can implement the logic in the Entity or in the Repository. You can inject dependencies to the entity constructor.

      Comment


      • #5
        Thanks, yuri.

        I see in the class \Espo\ORM\BaseEntity that it provides the ability to implement
        PHP Code:
        _set<Attribute
        and
        PHP Code:
        _get<Attribute
        custom methods.

        For the custom entity with an encrypted attribute = 'enattr', I can create a custom method as follows:

        PHP Code:
        namespace Espo\Modules\MyModule\Entities;

        use 
        Espo\ORM\BaseEntity;
        use 
        Espo\ORM\EntityManager;
        use 
        Espo\ORM\Value\ValueAccessorFactory;
        use 
        Espo\Core\Utils\Crypt;

        class 
        MyEntity extends BaseEntity
        {
            public function 
        __construct(
                
        string $entityType,
                array 
        $defs,
                ?
        EntityManager $entityManager null,
                ?
        ValueAccessorFactory $valueAccessorFactory null,
                private 
        Crypt $crypt,
            )
            {
                
        parent::__construct(
                    
        $entityType,
                    
        $defs,
                    
        $entityManager,
                    
        $valueAccessorFactory
                
        );
            }

            protected function 
        _setEnattr(string $preparedValue): void
            
        {
                
        $encryptedValue $this->crypt->encrypt($preparedValue);

                
        $this->setInContainer('enattr'$encryptedValue);
                
        $this->writtenMap['enattr'] = true;

                return;
            }

        The problem is that the 'writtenMap' array is set to private in BaseEntity, and there is a public method <isAttributeWritten> that tests against this value.

        I do not use this method anywhere is my code, but I am concerned whether the ORM functionality relies on this method or not.

        Comment


        • #6
          The isAttributeWritten method is used outside of the ORM for quite specific purposes. You can neglect it.

          Comment


          • #7
            BTW you could also extend getFromContainer and setInContainer. Underscore setters and getters is a legacy I would not encourage to use.

            Comment


            • #8
              In testing, I discovered that the `set` method is used to populate the entity when data is read from the DB. At this point, the encrypted field value is already encrypted, so should not be encrypted again. But when the set method is used to create or update the values in an entity, we need to encrypt.

              My solution to this issue is as follows:

              PHP Code:
                  protected function _setEnattr(string $preparedValue): void
                  
              {
                      
              $attribute 'enattr';

                      if (
              $this->hasFetched($attribute) || $this->isNew()) {
                          
              $encrypted $this->crypt->encrypt($preparedValue);
                          
              $this->setInContainer($attribute$encrypted);
                      } else {
                          
              $this->setInContainer($attribute$preparedValue);
                      }

                      return;
                  } 
              I will appreciate any input about whether this seems like a appropriate solution, especially within the context of Entity Collections. I am not able to figure out whether all the different ways that data can be queried using the RDBRepository with DataMapper ultimately use the Entity's set method to populate the Entity object with pulled data.

              Comment


              • aldisa
                aldisa commented
                Editing a comment
                I had not seen your latest suggestion. I think I will encounter this issue in that case as well, but can use the isFetched and isNew tests to decide whether to encrypt in the setInContainer method.

            • #9
              This is my final solution (in case someone else is trying to figure this out). I could not use the 'isNew' method, as this returns true when the Entity is being loaded from the database as well. What this means is that encrypted data gets double encrypted when it comes from the database. So I had to resort to unencrypting the data when the 'setFetched' method is used to signify that data has been fetched from the database. As I have several entities that will have specified fields that need to be encrypted, I created an abstract Entity class, and will rely on setting an 'isEncrypted' flag in the entityDefs.

              File: custom/Espo/Custom/Entities/EncryptedEntity.php
              PHP Code:
              <?php

              namespace Espo\Custom\Entities;

              use 
              Espo\ORM\BaseEntity;
              use 
              Espo\ORM\EntityManager;
              use 
              Espo\ORM\Value\ValueAccessorFactory;
              use 
              Espo\Core\Utils\Crypt;

              abstract class 
              EncryptedEntity extends BaseEntity
              {
                  private 
              $isEncrypted;

                  public function 
              __construct(
                      
              string $entityType,
                      array 
              $defs,
                      ?
              EntityManager $entityManager null,
                      ?
              ValueAccessorFactory $valueAccessorFactory null,
                      private 
              Crypt $crypt,
                  ) {
                      
              parent::__construct(
                          
              $entityType,
                          
              $defs,
                          
              $entityManager,
                          
              $valueAccessorFactory
                      
              );

                      
              $this->isEncrypted = [];
                      foreach (
              $defs['fields'] as $field => $value) {
                          if (
              array_key_exists('isEncrypted'$value) && $value['isEncrypted']) {
                              
              $this->isEncrypted[] = $field;
                          }
                      }
                  }

                  protected function 
              setInContainer(string $attribute$value): void
                  
              {
                      if (
              in_array($attribute$this->isEncrypted)) {
                          
              $value $this->crypt->encrypt($value);
                      }

                      
              parent::setInContainer($attribute$value);

                      return;
                  }

                  public function 
              setFetched(string $attribute$value): void
                  
              {
                      if (
              in_array($attribute$this->isEncrypted)) {
                          
              $value $this->crypt->decrypt($value);
                          
              parent::setInContainer($attribute$value);
                      }

                      
              parent::setFetched($attribute$value);
                  }

                  protected function 
              getFromContainer(string $attribute)
                  {
                      
              $value parent::getFromContainer($attribute);

                      if (
              in_array($attribute$this->isEncrypted)) {
                          return 
              $this->crypt->decrypt($value);
                      }

                      return 
              $value;
                  }

                  public function 
              getFetched(string $attribute)
                  {
                      
              $value parent::getFetched($attribute);

                      if (
              in_array($attribute$this->isEncrypted) && !is_null($value)) {
                          
              $value $this->crypt->decrypt($value);
                      }

                      return 
              $value;
                  }
              }
              File: custom/Espo/Custom/Resources/metadata/entityDefs/MyEntity.php
              PHP Code:
              {
                  
              "fields": {

                      
              /** other definitions **/

                      
              "<field name>": {
                        
              /** field attributes **/
                        
              "isEncrypted"true
                      
              },
                  },
                  
              "indexes": {
                     
              /** index definitions **/
                  
              }

              File: custom/Espo/Custom/Entities/MyEntity.php
              PHP Code:
              namespace Espo/Custom/Entities;

              use 
              Espo/Custom/Entities/EncryptedEntity;

              class 
              MyEntity extends EncryptedEntity
              {} 
              So far, in my testing this implementation is working.

              If anyone has any input or suggestions, please share.

              Comment

              Working...
              X