diff --git a/src/Database/AuditLoggerBase.php b/src/Database/AuditLoggerBase.php new file mode 100644 index 0000000..d8310fa --- /dev/null +++ b/src/Database/AuditLoggerBase.php @@ -0,0 +1,74 @@ +connection = $connection; + $this->logTable = $logTable; + } + + public function logInsert(string $localTable, $localId) + { + $data = [ + 'local_table' => $localTable, + 'local_id' => $localId, + 'type' => static::LOG_TYPE_INSERT, + 'modifier_id' => $this->getModifierId(), + ]; + + $query = 'INSERT INTO ' . Utils::backtick($this->logTable) . ' SET ' . Utils::generateColumnsWithBinding(array_keys($data)); + $stmt = $this->connection->prepare($query); + $stmt->execute($data); + } + + public function logUpdate(string $localTable, $localId, array $diff) + { + $data = [ + 'local_table' => $localTable, + 'local_id' => $localId, + 'type' => static::LOG_TYPE_UPDATE, + 'modifier_id' => $this->getModifierId(), + 'column' => null, + 'old' => null, + 'new' => null, + ]; + + $query = 'INSERT INTO ' . Utils::backtick($this->logTable) . ' SET ' . Utils::generateColumnsWithBinding(array_keys($data)); + $stmt = $this->connection->prepare($query); + + foreach ($diff as $name => $values) { + $data['column'] = $name; + $data['old'] = $values['old']; + $data['new'] = $values['new']; + $stmt->execute($data); + } + } + + public function logDelete(string $localTable, $localId, array $attributes) + { + $data = [ + 'local_table' => $localTable, + 'local_id' => $localId, + 'type' => static::LOG_TYPE_DELETE, + 'modifier_id' => $this->getModifierId(), + 'old' => $attributes, + ]; + + $query = 'INSERT INTO ' . Utils::backtick($this->logTable) . ' SET ' . Utils::generateColumnsWithBinding(array_keys($data)); + $stmt = $this->connection->prepare($query); + $stmt->execute($data); + } + + abstract protected function getModifierId(); +} diff --git a/src/Database/Query/Modify.php b/src/Database/Query/Modify.php index 1f554c8..fa5d7a4 100755 --- a/src/Database/Query/Modify.php +++ b/src/Database/Query/Modify.php @@ -2,6 +2,9 @@ use SokoWeb\Interfaces\Database\IConnection; use SokoWeb\Database\Utils; +use SokoWeb\Database\AuditLoggerBase; +use SokoWeb\Interfaces\Database\IAuditLogger; +use SokoWeb\Interfaces\Database\IResultSet; class Modify { @@ -9,6 +12,8 @@ class Modify private string $table; + private ?AuditLoggerBase $auditLogger; + private string $idName = 'id'; private array $attributes = []; @@ -17,10 +22,13 @@ class Modify private bool $autoIncrement = true; - public function __construct(IConnection $connection, string $table) + private ?array $diff = null; + + public function __construct(IConnection $connection, string $table, ?AuditLoggerBase $auditLogger = null) { $this->connection = $connection; $this->table = $table; + $this->auditLogger = $auditLogger; } public function setIdName(string $idName): Modify @@ -65,6 +73,13 @@ class Modify return $this; } + public function setDiff(array $diff): Modify + { + $this->diff = $diff; + + return $this; + } + public function getId() { return $this->attributes[$this->idName]; @@ -89,6 +104,10 @@ class Modify $stmt = $this->connection->prepare($query); $stmt->execute([$this->idName => $this->attributes[$this->idName]]); + + if ($this->auditLogger !== null) { + $this->auditLogger->logDelete($this->attributes[$this->idName], $this->attributes); + } } private function insert(): void @@ -99,7 +118,7 @@ class Modify $this->attributes[$this->idName] = $this->generateKey(); } - $set = $this->generateColumnsWithBinding(array_keys($this->attributes)); + $set = Utils::generateColumnsWithBinding(array_keys($this->attributes)); $query = 'INSERT INTO ' . Utils::backtick($this->table) . ' SET ' . $set; @@ -109,32 +128,60 @@ class Modify if ($this->autoIncrement) { $this->attributes[$this->idName] = $this->connection->lastId(); } + + if ($this->auditLogger !== null) { + $this->auditLogger->logInsert($this->attributes[$this->idName]); + } } private function update(): void { + $this->generateDiff(); + if (count($this->diff) === 0) { + return; + } + $attributes = $this->attributes; unset($attributes[$this->idName]); - $set = $this->generateColumnsWithBinding(array_keys($attributes)); + $set = Utils::generateColumnsWithBinding(array_keys($attributes)); $query = 'UPDATE ' . Utils::backtick($this->table) . ' SET ' . $set . ' WHERE ' . Utils::backtick($this->idName) . '=?'; $stmt = $this->connection->prepare($query); $stmt->execute(array_merge($attributes, [$this->idName => $this->attributes[$this->idName]])); - } - public static function generateColumnsWithBinding(array $columns): string - { - array_walk($columns, function(&$value, $key) { - $value = Utils::backtick($value) . '=?'; - }); - - return implode(',', $columns); + if ($this->auditLogger !== null) { + $this->auditLogger->logUpdate($this->attributes[$this->idName], $this->diff); + } } private function generateKey(): string { return substr(hash('sha256', serialize($this->attributes) . random_bytes(5) . microtime()), 0, 7); } + + private function generateDiff(): void + { + if (isset($this->diff)) { + return; + } + + $this->diff = []; + $original = $this->readFromDb(array_keys($this->attributes)); + foreach ($original as $key => $value) { + if ($value !== $this->attributes[$key]) { + $this->diff[$key] = ['old' => $value, 'new' => $this->attributes[$key]]; + } + } + } + + private function readFromDb($columns): array + { + return (new Select($this->connection, $this->table)) + ->columns($columns) + ->where($this->idName, '=', $this->attributes[$this->idName]) + ->execute() + ->fetch(IResultSet::FETCH_ASSOC); + } } diff --git a/src/Database/Utils.php b/src/Database/Utils.php index 658b13c..c9c4885 100644 --- a/src/Database/Utils.php +++ b/src/Database/Utils.php @@ -5,4 +5,13 @@ class Utils { { return '`' . $name . '`'; } + + public static function generateColumnsWithBinding(array $columns): string + { + array_walk($columns, function(&$value, $key) { + $value = static::backtick($value) . '=?'; + }); + + return implode(',', $columns); + } } diff --git a/src/Interfaces/Database/IAuditLogger.php b/src/Interfaces/Database/IAuditLogger.php new file mode 100644 index 0000000..e60d4cc --- /dev/null +++ b/src/Interfaces/Database/IAuditLogger.php @@ -0,0 +1,10 @@ +getSnapshot(); + $diff = []; foreach ($original as $key => $value) { if ($value === $modified[$key]) { unset($modified[$key]); + } else { + $diff[$key] = ['old' => $value, 'new' => $modified[$key]]; } } if (count($modified) > 0) { $modify->setId($id); + $modify->setDiff($original); $modify->fill($modified); $modify->save(); } diff --git a/src/Util/GoogleJwtValidator.php b/src/Util/GoogleJwtValidator.php new file mode 100644 index 0000000..17901a5 --- /dev/null +++ b/src/Util/GoogleJwtValidator.php @@ -0,0 +1,102 @@ +request = $request; + } + + public function getDialogUrl(string $state, string $redirectUrl, ?string $nonce = null, ?string $loginHint = null): string + { + $oauthParams = [ + 'response_type' => 'code', + 'client_id' => $_ENV['GOOGLE_OAUTH_CLIENT_ID'], + 'scope' => 'openid email', + 'redirect_uri' => $redirectUrl, + 'state' => $state, + ]; + + if ($nonce !== null) { + $oauthParams['nonce'] = $nonce; + } + + if ($loginHint !== null) { + $oauthParams['login_hint'] = $loginHint; + } + + return self::$dialogUrlBase . '?' . http_build_query($oauthParams); + } + + public function getToken(string $code, string $redirectUrl): array + { + $tokenParams = [ + 'code' => $code, + 'client_id' => $_ENV['GOOGLE_OAUTH_CLIENT_ID'], + 'client_secret' => $_ENV['GOOGLE_OAUTH_CLIENT_SECRET'], + 'redirect_uri' => $redirectUrl, + 'grant_type' => 'authorization_code', + ]; + + $this->request->setUrl(self::$tokenUrlBase); + $this->request->setMethod(IRequest::HTTP_POST); + $this->request->setQuery($tokenParams); + $response = $this->request->send(); + + return json_decode($response->getBody(), true); + } + + public function validateJwt($jwt): ?array + { + $request = new Request(self::$certsUrl, IRequest::HTTP_GET); + $response = $request->send(); + $certs = json_decode($response->getBody(), true)['keys']; + + foreach ($certs as $cert) { + $publicKey = $this->getPublicKey($cert); + + try { + return (array) JWT::decode($jwt, new Key($publicKey, 'RS256')); + } catch (ExpiredException $e) { + return null; + } catch (SignatureInvalidException $e) { + //continue + } catch (DomainException $e) { + //continue + } + } + + return null; + } + + private function getPublicKey($cert): string + { + $modulus = new BigInteger($this->base64Decode($cert['n']), 256); + $exponent = new BigInteger($this->base64Decode($cert['e']), 256); + $component = ['n' => $modulus, 'e' => $exponent]; + $rsa = new RSA(); + $rsa->loadKey($component); + return $rsa->getPublicKey(); + } + + private function base64Decode($input): string + { + $input = str_replace(['_', '-'], ['/', '+'], $input); + return base64_decode($input); + } +}