PersonalDataPolicy
in package
Applies the personal-data policy: personal data of other users is returned to an unprivileged caller according to the global setting.
Fields marked PersonalData are returned:
- in full to a privileged caller (admin) or to the record owner (PersonalDataOwner);
- otherwise by the PersonalDataExposure mode in
1/security.personalDataExposure(sanitizedis the default, also when the setting row is missing;full;empty).
Text Hidden fields (author IPs) are emptied; non-text ones (user wallet state such as
credits, points, discount) are dropped from the row instead, because an empty string
would break the int setter of the consumer. Email is masked; phone, address, company id, VAT id and company
follow the mode. The seller card reads the public profile (publicProfile) and the phone
reveal showPhone, not includes=user.
Outside a request (cron, CLI, emails, invoice generators) the audience is unknown and nothing is masked, the same boundary as FieldAccess. Notifications that need the real email of the other party read the entity through the manager, not through this boundary.
IMPORTANT: the policy is applied to the finished payload at send time, never in the hydrator. The hydrator output feeds a query cache keyed independently of the caller; an owner-based mask inside the cache would be fixed by the first caller, and the owner's full data would be served to the next visitor. It runs on both API outputs, BasePresenter::sendJson() (external HTTP) and ApiBridge (internal calls), and on writes in self::stripForeignUpdate().
The whole payload is walked: every _references (nested ones too, e.g. data.user in a
login response) and the embedded collections inside rows (a OneToMany is embedded whole).
Table of Contents
Constants
- SETTING_KEY : string = 'security.personalDataExposure'
- SETTING_MODULE : string = '1'
Properties
- $audience : FieldAudienceContext
-
$classMeta
: array<string, PersonalDataKind, 1: bool}>, owner: ?string, via: ?string, ownerTarget: ?string, embedded: array
} > - $connection : Connection
- $em : EntityManagerInterface
- $exposure : PersonalDataExposure|null
Methods
- __construct() : mixed
- apply() : mixed
- Applies the policy to the payload sent to the caller.
- exposure() : PersonalDataExposure
- guardsUpdateOf() : bool
- Cheap pre-check for writes: is it worth loading the existing record for {@see self::stripForeignUpdate()}?
- mask() : string|null
- maskDetected() : string|null
- Masks a field with `detect: true`, where personal data is searched inside the value.
- stripForeignUpdate() : array<string, mixed>
- Strips personal data from the payload when writing a foreign record (an unprivileged caller who is not the owner).
- entityOwnerId() : int|null
- Returns the owner id of a loaded entity (for writes), also through the parent (`via`).
- maskRow() : void
-
meta()
: PersonalDataKind, 1: bool}>, owner: ?string, via: ?string, ownerTarget: ?string, embedded: array
} - ownedByCapability() : bool
- Returns whether the row belongs to a record the caller proved access to by a capability.
- pointerId() : int|null
- Extracts the id from a pointer `{id: {id: N}}`, `{id: N}` or a scalar.
- rowOwnerId() : int|null
- walk() : void
Constants
SETTING_KEY
public
string
SETTING_KEY
= 'security.personalDataExposure'
SETTING_MODULE
public
string
SETTING_MODULE
= '1'
Properties
$audience read-only
private
FieldAudienceContext
$audience
$classMeta
private
array<string, PersonalDataKind, 1: bool}>, owner: ?string, via: ?string, ownerTarget: ?string, embedded: array} >
$classMeta
= []
$connection read-only
private
Connection
$connection
$em read-only
private
EntityManagerInterface
$em
$exposure
private
PersonalDataExposure|null
$exposure
= null
Methods
__construct()
public
__construct(FieldAudienceContext $audience, Connection $connection, EntityManagerInterface $em) : mixed
Parameters
- $audience : FieldAudienceContext
- $connection : Connection
- $em : EntityManagerInterface
apply()
Applies the policy to the payload sent to the caller.
public
apply(mixed $payload[, array<string, array<int, int>> $capabilityRows = [] ]) : mixed
An unknown audience, a privileged caller and the full mode return the payload unchanged.
$capabilityRows lists records the caller proved access to by a capability (advert
password), not by identity: [entity class => [id, ...]]. The caller counts as owner
only of the row of that class with that id and of rows whose owner leads through the
parent (via) to it (advert parameter to advert). Every other row of the same response
(user from includes, reaction, another advert) stays masked.
IMPORTANT: build $capabilityRows only from an entity loaded from the DB (see
BasePresenter::exemptPersonalDataForCapabilityHolder()),
never from caller data, or the caller could exempt any row from masking.
Parameters
- $payload : mixed
- $capabilityRows : array<string, array<int, int>> = []
exposure()
public
exposure() : PersonalDataExposure
Return values
PersonalDataExposureguardsUpdateOf()
Cheap pre-check for writes: is it worth loading the existing record for {@see self::stripForeignUpdate()}?
public
guardsUpdateOf(string $entityClass) : bool
True only for an unprivileged caller and an entity with marked fields; other saves cost no extra query.
Parameters
- $entityClass : string
Return values
boolmask()
public
static mask(PersonalDataKind $kind, string|null $value, PersonalDataExposure $mode) : string|null
Parameters
- $kind : PersonalDataKind
- $value : string|null
- $mode : PersonalDataExposure
Return values
string|nullmaskDetected()
Masks a field with `detect: true`, where personal data is searched inside the value.
public
static maskDetected(PersonalDataKind $kind, string|null $value, PersonalDataExposure $mode) : string|null
Email replaces only email addresses (mask or empty by mode) and keeps the other text;
a value without an address stays unchanged (price, city).
Parameters
- $kind : PersonalDataKind
- $value : string|null
- $mode : PersonalDataExposure
Return values
string|nullstripForeignUpdate()
Strips personal data from the payload when writing a foreign record (an unprivileged caller who is not the owner).
public
stripForeignUpdate(array<string, mixed> $data, string $entityClass, object|null $existing) : array<string, mixed>
IMPORTANT: without the strip, a front end that read the record masked would overwrite the real email with the mask when saving the whole entity. Only the owner or the admin may change foreign contact data.
Parameters
- $data : array<string, mixed>
- $entityClass : string
- $existing : object|null
Return values
array<string, mixed>entityOwnerId()
Returns the owner id of a loaded entity (for writes), also through the parent (`via`).
private
entityOwnerId(object $entity, string $field, string|null $via) : int|null
Parameters
- $entity : object
- $field : string
- $via : string|null
Return values
int|nullmaskRow()
private
maskRow(string $class, array<string|int, mixed> &$row, PersonalDataExposure $mode, int|null $userId, array<string|int, mixed> $lookup, array<string, array<int, int>> $capabilityRows) : void
Parameters
- $class : string
- $row : array<string|int, mixed>
- $mode : PersonalDataExposure
- $userId : int|null
- $lookup : array<string|int, mixed>
-
_referencesof the same response - $capabilityRows : array<string, array<int, int>>
meta()
private
meta(string $class) : PersonalDataKind, 1: bool}>, owner: ?string, via: ?string, ownerTarget: ?string, embedded: array}
Parameters
- $class : string
Return values
PersonalDataKind, 1: bool}>, owner: ?string, via: ?string, ownerTarget: ?string, embedded: arrayownedByCapability()
Returns whether the row belongs to a record the caller proved access to by a capability.
private
ownedByCapability(string $class, PersonalDataKind, 1: bool}>, owner: ?string, via: ?string, ownerTarget: ?string, embedded: array} $meta, array<string|int, mixed> $row, array<string, array<int, int>> $capabilityRows) : bool
A row without via is compared by its own id and class; a row with via by the parent
id in the pointer and the parent class from Doctrine metadata (not from the payload).
A row without an owner (PersonalDataOwner missing) never gets the capability exemption;
reactions and reports on an advert are not data of its author.
Parameters
- $class : string
-
$meta
: PersonalDataKind, 1: bool}>, owner: ?string, via: ?string, ownerTarget: ?string, embedded: array
} - $row : array<string|int, mixed>
- $capabilityRows : array<string, array<int, int>>
Return values
boolpointerId()
Extracts the id from a pointer `{id: {id: N}}`, `{id: N}` or a scalar.
private
static pointerId(mixed $value) : int|null
Parameters
- $value : mixed
Return values
int|nullrowOwnerId()
private
rowOwnerId(PersonalDataKind, 1: bool}>, owner: ?string, via: ?string, ownerTarget: ?string, embedded: array} $meta, array<string|int, mixed> $row, array<string|int, mixed> $lookup) : int|null
Parameters
-
$meta
: PersonalDataKind, 1: bool}>, owner: ?string, via: ?string, ownerTarget: ?string, embedded: array
} - $row : array<string|int, mixed>
- $lookup : array<string|int, mixed>
Return values
int|nullwalk()
private
walk(array<string|int, mixed> &$node, PersonalDataExposure $mode, int|null $userId, array<string, array<int, int>> $capabilityRows) : void
Parameters
- $node : array<string|int, mixed>
- $mode : PersonalDataExposure
- $userId : int|null
- $capabilityRows : array<string, array<int, int>>