diff --git a/TODO.md b/TODO.md index 22a91ca..4c568b1 100644 --- a/TODO.md +++ b/TODO.md @@ -4,3 +4,18 @@ - [ ] JWT authentication - [ ] Access control on groups, slots and props - [ ] Token authentication + - [ ] HTTPS support +- [ ] Handle remaining CLI options +- [ ] Message bus + - [ ] Slot update notifications + - [ ] Slot creation and deletion + - [ ] Group update notifications + - [ ] Group creation and deletion + - [ ] Group join/leave +- [ ] Triggers + - [ ] Listen for messages on bus to trigger events + - [ ] Dynamic trigger management (post, delete) +- [ ] Schemas + - [ ] Apply schemas to group + - [ ] Apply schemas to slot + - [ ] Check schemas on update diff --git a/src/Daemon.php b/src/Daemon.php index 850761d..fe620ab 100644 --- a/src/Daemon.php +++ b/src/Daemon.php @@ -18,6 +18,7 @@ use SlotDb\SlotDb\Data\Group\Group; use SlotDb\SlotDb\Data\Schema\Schema; use SlotDb\SlotDb\Data\Slot\Slot; use SlotDb\SlotDb\Data\Slot\SlotRepository; +use SlotDb\SlotDb\Security\AccessChecker; class Daemon { @@ -57,8 +58,10 @@ class Daemon $this->messageBus = new MessageBus(); + $accessChecker = new AccessChecker(); + $routes = new RouteCollection(); - $routes->addController(new Http\Controller\SlotsController($em->getRepository(Slot::class), $this->messageBus)); + $routes->addController(new Http\Controller\SlotsController($em->getRepository(Slot::class), $this->messageBus, $accessChecker)); $routes->addController(new Http\Controller\GroupsController($em->getRepository(Group::class), $em->getRepository(Slot::class))); $routes->addController(new Http\Controller\SchemassController($em->getRepository(Schema::class))); diff --git a/src/Http/Controller/SlotsController.php b/src/Http/Controller/SlotsController.php index f576978..a56cb12 100644 --- a/src/Http/Controller/SlotsController.php +++ b/src/Http/Controller/SlotsController.php @@ -9,7 +9,7 @@ use SlotDb\SlotDb\Bus\MessageBus; use SlotDb\SlotDb\Data\Slot\Slot; use SlotDb\SlotDb\Data\Slot\SlotNotFoundException; use SlotDb\SlotDb\Data\Slot\SlotRepository; - +use SlotDb\SlotDb\Security\AccessChecker; class SlotsController extends Controller { @@ -17,6 +17,7 @@ class SlotsController extends Controller public function __construct( private readonly SlotRepository $slots, private readonly MessageBus $bus, + private readonly AccessChecker $accessChecker, ) { @@ -60,6 +61,7 @@ class SlotsController extends Controller #[Route(path:"/api/slotdb/v1/slots", methods:["POST"])] public function createSlots(ServerRequestInterface $request) { + $data = json_decode($request->getBody()); $slots = []; foreach ($data as $item) { @@ -87,6 +89,8 @@ class SlotsController extends Controller return Response::json([ 'error'=>"Slot not found" ])->withStatus(404); } + $acl = $request->getAttribute("acl"); + return Response::json($slotObj); } diff --git a/src/Http/Middleware/SecurityMiddleware.php b/src/Http/Middleware/SecurityMiddleware.php index 382d33f..e99569f 100644 --- a/src/Http/Middleware/SecurityMiddleware.php +++ b/src/Http/Middleware/SecurityMiddleware.php @@ -9,11 +9,57 @@ use React\Promise\PromiseInterface; class SecurityMiddleware { + + private bool $useJwt = false; + + private bool $useToken = false; + + public function __construct( + private readonly ?string $jwtSecret = null, + private readonly ?string $jwtClaim = "slotdb", + private readonly ?string $tokenFile = null, + ) + { + + } + public function __invoke(ServerRequestInterface $request, ?callable $next = null) { + + if (!$this->useJwt && !$this->useToken) { + // No authentication + $request = $request->withAttribute("acl", [ "*/rw"]); + } else { + if ($this->useJwt && $request->hasHeader('authorization')) { + $auth = $request->getHeader('authorization')[0]; + [$type,$token] = explode(" ", $auth, 2); + $acl = $this->checkJwt($token); + if ($acl === null) { + return Response::json(['error'=>'Unauthorized'])->withStatus(Response::STATUS_UNAUTHORIZED); + } + $request = $request->withAttribute("acl", $acl); + } elseif ($this->useToken && $request->hasHeader('x-token')) { + $token = $request->getHeader('x-token')[0]; + $acl = $this->checkToken($token); + if ($acl === null) { + return Response::json(['error'=>'Unauthorized'])->withStatus(Response::STATUS_UNAUTHORIZED); + } + $request = $request->withAttribute("acl", $acl); + } + } + $response = $next($request); return $response; } + private function checkJwt(string $token): ?array + { + return null; + } + + private function checkToken(string $token): ?array + { + return null; + } } \ No newline at end of file diff --git a/src/bootstrap.php b/src/bootstrap.php index 8c38b47..59eb6c4 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -32,16 +32,18 @@ function print_usage(): never { -v,--verbose Include debug level logging to STDOUT -V,--version Print version and exit -l,--listen ADDR Set listen address for HTTP requests (env: SLOTDB_LISTEN) - -c,--config FILE Read configuration from file (env: SLOTDB_CONFIG) - --db URI Use custom database connection URI (env: SLOTDB_DATABASE) - --init-db Initialize database schema - --upgrade-db Upgrade database schema - --root FILE Set root CA certificate for SSL (env: SLOTDB_SSL_ROOT) - --cert FILE Set certificate for SSL (env: SLOTDB_SSL_CERT) - --key FILE Set private key file for SSL (env: SLOTDB_SSL_KEY) - --jwt-secret SECRET Use JWT auth with secret (env: SLOTDB_JWT_SECRET) - --jwt-claim CLAIM Claim key to use for matching properties (env: SLOTDB_JWT_CLAIM) + -c,--config FILE ¤ Read configuration from file (env: SLOTDB_CONFIG) + --db URI ¤ Use custom database connection URI (env: SLOTDB_DATABASE) + --init-db ¤ Initialize database schema + --upgrade-db ¤ Upgrade database schema + --root FILE ¤ Set root CA certificate for SSL (env: SLOTDB_SSL_ROOT) + --cert FILE ¤ Set certificate for SSL (env: SLOTDB_SSL_CERT) + --key FILE ¤ Set private key file for SSL (env: SLOTDB_SSL_KEY) + --jwt-secret SECRET ¤ Use JWT auth with secret (env: SLOTDB_JWT_SECRET) + --jwt-claim CLAIM ¤ Key in JWT claims that contains ACLs (env: SLOTDB_JWT_CLAIM) + --tokens FILE ¤ Read access tokens from file (env: SLOTDB_TOKENS_FILE) --http-log FILE Log HTTP requests to file (env: SLOTDB_HTTP_LOG) + ¤=not yet implemented Defaults: HTTP listen address: 0.0.0.0:8080 @@ -52,11 +54,11 @@ function print_usage(): never { The key pointed to by the claim option should be a string or an array of strings, with each string granting access to a group, slot or property. - "*#*:*/r" Read all groups, all slots, all properties + "*#*.*/r" Read all groups, all slots, all properties "first#/rw" Read-write everything in the group first "slot*/rw" Read-write all slots starting with 'slot' in all groups - "slota,slotb/w" Write-only access to slots 'slota' and 'slotb' - "*:available" Read-only access to the 'available' prop on all slots + "slot[ab]/w" Write-only access to slots 'slota' and 'slotb' + "*.available" Read-only access to the 'available' prop on all slots Examples: SLOTDB_LISTEN=0.0.0.0:9876 slotdb