lekoala /
silverstripe-encrypt
| 1 | <?php |
||
| 2 | |||
| 3 | namespace LeKoala\Encrypt; |
||
| 4 | |||
| 5 | use SilverStripe\Security\Security; |
||
| 6 | use ParagonIE\CipherSweet\Contract\KeyProviderInterface; |
||
| 7 | use ParagonIE\CipherSweet\Exception\CipherSweetException; |
||
| 8 | use ParagonIE\CipherSweet\KeyProvider\MultiTenantProvider; |
||
| 9 | |||
| 10 | /** |
||
| 11 | * This class provides a multi tenant key provider |
||
| 12 | * Each user gets its own key to encrypt its data |
||
| 13 | * |
||
| 14 | * - getTenant() selects a KeyProvider based on a given tenant. |
||
| 15 | * - getTenantFromRow() gets the tenant ID (array key) based on the data stored in an encrypted row. |
||
| 16 | * - injectTenantMetadata() injects some breadcrumb for getTenantFromRow() to use to select the appropriate key. |
||
| 17 | * |
||
| 18 | * These methods were designed to be generalizable: |
||
| 19 | * If you implement AWS KMS support, for example, you'd probably store an encrypted data key with injectTenantMetadata() |
||
| 20 | * and then ask KMS to decrypt it in getTenantFromRow() (unless it's cached). |
||
| 21 | */ |
||
| 22 | class MemberKeyProvider extends MultiTenantProvider |
||
| 23 | { |
||
| 24 | protected ?int $forcedTenant = null; |
||
| 25 | |||
| 26 | /** |
||
| 27 | * MultiTenantProvider constructor. |
||
| 28 | * |
||
| 29 | * @param array<array-key,KeyProviderInterface> $keyProviders |
||
|
0 ignored issues
–
show
Documentation
Bug
introduced
by
Loading history...
|
|||
| 30 | * @param array-key|null $active |
||
| 31 | */ |
||
| 32 | public function __construct(array $keyProviders, string|int|null $active = null) |
||
| 33 | { |
||
| 34 | if ($active === null && Security::getCurrentUser()) { |
||
| 35 | $active = Security::getCurrentUser()->ID; |
||
| 36 | } |
||
| 37 | parent::__construct($keyProviders, $active); |
||
| 38 | } |
||
| 39 | |||
| 40 | /** |
||
| 41 | * @param array-key $name |
||
|
0 ignored issues
–
show
|
|||
| 42 | * @return KeyProviderInterface |
||
| 43 | * @throws CipherSweetException |
||
| 44 | * @psalm-suppress PossiblyNullArrayOffset |
||
| 45 | */ |
||
| 46 | public function getTenant(string|int $name): KeyProviderInterface |
||
| 47 | { |
||
| 48 | if (!array_key_exists($name, $this->tenants)) { |
||
| 49 | throw new CipherSweetException("Tenant '{$name}' does not exist"); |
||
| 50 | } |
||
| 51 | return $this->tenants[$this->active]; |
||
| 52 | } |
||
| 53 | |||
| 54 | /** |
||
| 55 | * @return KeyProviderInterface |
||
| 56 | * @throws CipherSweetException |
||
| 57 | */ |
||
| 58 | public function getActiveTenant(): KeyProviderInterface |
||
| 59 | { |
||
| 60 | if ($this->forcedTenant) { |
||
|
0 ignored issues
–
show
The expression
$this->forcedTenant of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.
In PHP, under loose comparison (like For 0 == false // true
0 == null // true
123 == false // false
123 == null // false
// It is often better to use strict comparison
0 === false // false
0 === null // false
Loading history...
|
|||
| 61 | return $this->tenants[$this->forcedTenant]; |
||
| 62 | } |
||
| 63 | $this->active = Security::getCurrentUser()->ID ?? $this->active; |
||
| 64 | if (is_null($this->active)) { |
||
| 65 | throw new CipherSweetException('Active tenant not set'); |
||
| 66 | } |
||
| 67 | if (!array_key_exists($this->active, $this->tenants)) { |
||
| 68 | throw new CipherSweetException("Tenant '{$this->active}' does not exist"); |
||
| 69 | } |
||
| 70 | return $this->tenants[$this->active]; |
||
| 71 | } |
||
| 72 | |||
| 73 | /** |
||
| 74 | * @param array-key $index |
||
|
0 ignored issues
–
show
|
|||
| 75 | * @return static |
||
| 76 | */ |
||
| 77 | public function setActiveTenant(string|int $index): static |
||
| 78 | { |
||
| 79 | if (!$index && Security::getCurrentUser()) { |
||
| 80 | $index = Security::getCurrentUser(); |
||
| 81 | } |
||
| 82 | $this->active = $index; |
||
| 83 | return $this; |
||
| 84 | } |
||
| 85 | |||
| 86 | public function getForcedTenant(): ?int |
||
| 87 | { |
||
| 88 | return $this->forcedTenant; |
||
| 89 | } |
||
| 90 | |||
| 91 | public function setForcedTenant(?int $index): self |
||
| 92 | { |
||
| 93 | $this->forcedTenant = $index; |
||
| 94 | return $this; |
||
| 95 | } |
||
| 96 | |||
| 97 | /** |
||
| 98 | * Given a row of data, determine which tenant should be selected. |
||
| 99 | * |
||
| 100 | * This is not super useful since we mostly go through the ORM |
||
| 101 | * |
||
| 102 | * @param array<string,mixed> $row |
||
| 103 | * @param string $tableName |
||
| 104 | * @return string|int |
||
| 105 | * |
||
| 106 | * @throws CipherSweetException |
||
| 107 | */ |
||
| 108 | public function getTenantFromRow(array $row, string $tableName): string|int |
||
| 109 | { |
||
| 110 | // Expect member bound encryption to have a Member relation |
||
| 111 | if (isset($row['MemberID'])) { |
||
| 112 | return $row['MemberID']; |
||
| 113 | } |
||
| 114 | return $this->active ?? 0; |
||
| 115 | } |
||
| 116 | |||
| 117 | /** |
||
| 118 | * This is called when you encrypt a row, extra fields can be added |
||
| 119 | * It's not really used in our case because we encrypt each fields |
||
| 120 | * one by one anyway |
||
| 121 | * |
||
| 122 | * @param array<string,mixed> $row |
||
| 123 | * @param string $tableName |
||
| 124 | * @return array<string,mixed> |
||
| 125 | */ |
||
| 126 | public function injectTenantMetadata(array $row, string $tableName): array |
||
| 127 | { |
||
| 128 | // If our class uses encryption per user, inject member id |
||
| 129 | $row['MemberID'] = $this->active; |
||
| 130 | return $row; |
||
| 131 | } |
||
| 132 | } |
||
| 133 |