PHP Classes

File: src/Chronicle/Handlers/Revoke.php

Recommend this page to a friend!
  Classes of Scott Arciszewski   Chronicle   src/Chronicle/Handlers/Revoke.php   Download  
File: src/Chronicle/Handlers/Revoke.php
Role: Class source
Content type: text/plain
Description: Class source
Class: Chronicle
Append arbitrary data to a storage container
Author: By
Last change: Proper whitespace
Docblocks
Fix API endpoints
Concurrent Chronicles

Add support for multiple instances via the ?instance=name parameter.

To implement, add something like this to your local/settings.json in the
instances key:

"public_prefix" => "table_name_prefix"

Then run bin/make-tables.php as normal.

Every instance is totally independent of each other. They have their own

* Clients
* Chain data
* Cross-Signing Targets and Policies
* Replications

If merged, I will document these features and roll it into v1.1.0
Boyscouting. Update easydb to 2.7 to eliminate boolean workaround.
Type safety
Minor bugfixes.
Date: 1 year ago
Size: 6,674 bytes
 

Contents

Class file image Download
<?php
namespace ParagonIE\Chronicle\Handlers;

use
GuzzleHttp\Exception\GuzzleException;
use
ParagonIE\Chronicle\{
   
Chronicle,
   
Exception\AccessDenied,
   
Exception\BaseException,
   
Exception\FilesystemException,
   
Exception\InvalidInstanceException,
   
Exception\TargetNotFound,
   
HandlerInterface,
   
Scheduled
};
use
ParagonIE\ConstantTime\Base64UrlSafe;
use
ParagonIE\Sapient\Exception\InvalidMessageException;
use
Psr\Http\Message\{
   
RequestInterface,
   
ResponseInterface
};
use
Slim\Http\Request;

/**
 * Class Revoke
 * @package ParagonIE\Chronicle\Handlers
 */
class Revoke implements HandlerInterface
{
   
/**
     * The handler gets invoked by the router. This accepts a Request
     * and returns a Response.
     *
     * @param RequestInterface $request
     * @param ResponseInterface $response
     * @param array $args
     * @return ResponseInterface
     *
     * @throws AccessDenied
     * @throws BaseException
     * @throws FilesystemException
     * @throws GuzzleException
     * @throws InvalidInstanceException
     * @throws InvalidMessageException
     * @throws TargetNotFound
     * @throws \SodiumException
     */
   
public function __invoke(
       
RequestInterface $request,
       
ResponseInterface $response,
        array
$args = []
    ):
ResponseInterface {
       
// Sanity checks:
       
if ($request instanceof Request) {
            if (!
$request->getAttribute('authenticated')) {
                throw new
AccessDenied('Unauthenticated request');
            }
            if (!
$request->getAttribute('administrator')) {
                throw new
AccessDenied('Unprivileged request');
            }
        } else {
            throw new \
TypeError('Something unexpected happen when attempting to revoke.');
        }

       
/* Revoking a public key cannot be replayed. */
       
try {
           
Chronicle::validateTimestamps($request);
        } catch (\
Throwable $ex) {
            return
Chronicle::errorResponse(
               
$response,
               
$ex->getMessage(),
               
$ex->getCode()
            );
        }

       
// Get the parsed POST body:
       
$post = $request->getParsedBody();
        if (!\
is_array($post)) {
            return
Chronicle::errorResponse($response, 'POST body empty or invalid', 406);
        }
        if (empty(
$post['clientid'])) {
            return
Chronicle::errorResponse($response, 'Error: Client ID expected', 401);
        }
        if (empty(
$post['publickey'])) {
            return
Chronicle::errorResponse($response, 'Error: Public key expected', 401);
        }

       
$db = Chronicle::getDatabase();
       
$db->beginTransaction();

       
/** @var bool $found */
       
$found = $db->exists(
           
'SELECT count(id) FROM ' . Chronicle::getTableName('clients') . ' WHERE publicid = ? AND publickey = ?',
           
$post['clientid'],
           
$post['publickey']
        );
        if (!
$found) {
            return
Chronicle::errorResponse(
               
$response,
               
'Error: Client not found. It may have already been deleted.',
               
404
           
);
        }
       
/** @var bool $isAdmin */
       
$isAdmin = $db->cell(
           
'SELECT isAdmin FROM ' . Chronicle::getTableName('clients') . ' WHERE publicid = ? AND publickey = ?',
           
$post['clientid'],
           
$post['publickey']
        );
        if (
$isAdmin) {
            return
Chronicle::errorResponse(
               
$response,
               
'You cannot delete administrators from this API.',
               
403
           
);
        }

       
$db->delete(
           
Chronicle::getTableName('clients', true),
            [
               
'publicid' => $post['clientid'],
               
'publickey' => $post['publickey'],
               
'isAdmin' => false
           
]
        );
        if (
$db->commit()) {
           
// Confirm deletion:
           
$result = [
               
'deleted' => !$db->exists(
                   
'SELECT count(id) FROM ' .
                   
Chronicle::getTableName('clients') .
                   
' WHERE publicid = ? AND publickey = ?',
                   
$post['clientid'],
                   
$post['publickey']
                )
            ];

            if (!
$result['deleted']) {
               
$result['reason'] = 'Delete operation was unsuccessful due to unknown reasons.';
            }
            try {
               
$now = (new \DateTime())->format(\DateTime::ATOM);
            } catch (\
Exception $ex) {
                return
Chronicle::errorResponse($response, $ex->getMessage(), 500);
            }

           
$settings = Chronicle::getSettings();
            if (!empty(
$settings['publish-revoked-clients'])) {
               
$serverKey = Chronicle::getSigningKey();
               
$message = \json_encode(
                    [
                       
'server-action' => 'Client Access Revocation',
                       
'now' => $now,
                       
'clientid' => $post['clientid'],
                       
'publickey' => $post['publickey']
                    ],
                   
JSON_PRETTY_PRINT
               
);
                if (!\
is_string($message)) {
                    throw new \
TypeError('Invalid messsage');
                }
               
$signature = Base64UrlSafe::encode(
                    \
ParagonIE_Sodium_Compat::crypto_sign_detached(
                       
$message,
                       
$serverKey->getString(true)
                    )
                );
               
$result['revoke'] = Chronicle::extendBlakechain(
                   
$signature,
                   
$message,
                   
$serverKey->getPublicKey()
                );

               
// If we need to do a cross-sign, do it now:
               
(new Scheduled())->doCrossSigns();
            }
        } else {
           
/* PDO should have already thrown an exception. */
           
$db->rollBack();
           
/** @var array<int, string> $errorInfo */
           
$errorInfo = $db->errorInfo();
            return
Chronicle::errorResponse(
               
$response,
               
$errorInfo[0],
               
500
           
);
        }

        return
Chronicle::getSapient()->createSignedJsonResponse(
           
200,
            [
               
'version' => Chronicle::VERSION,
               
'datetime' => (new \DateTime())->format(\DateTime::ATOM),
               
'status' => 'OK',
               
'results' => $result
           
],
           
Chronicle::getSigningKey(),
           
$response->getHeaders(),
           
$response->getProtocolVersion()
        );
    }
}