feat: integrate Bugtracker module, UpdateService enhancements & Token hierarchy

This commit is contained in:
Deploymentcenter Bot
2026-08-06 12:45:58 +02:00
parent 70b35f7b8b
commit d71f90cdfc
28 changed files with 3494 additions and 32 deletions
+233
View File
@@ -0,0 +1,233 @@
<?php
namespace Deploymentcenter\Core;
use PDO;
class TokenManager
{
private PDO $db;
public function __construct(PDO $db)
{
$this->db = $db;
}
/**
* Create a new Master Token.
*/
public function createMasterToken(
string $name,
?string $projectSlug = null,
?string $licenseKey = null,
string $ownerType = 'custom',
?string $ownerIdentity = null,
array $scopes = ['*'],
string $environment = 'all'
): array {
$tokenId = 'tok_m_' . bin2hex(random_bytes(8));
$rawToken = 'dc_master_' . bin2hex(random_bytes(20));
$tokenHash = hash('sha256', $rawToken);
$stmt = $this->db->prepare('
INSERT INTO dc_tokens (
token_id, parent_token_id, token_hash, raw_token, name,
project_slug, license_key, owner_type, owner_identity,
type, scopes, environment, created_at
) VALUES (
:id, NULL, :hash, :raw, :name,
:proj, :lic, :type, :identity,
"master", :scopes, :env, NOW()
)
');
$stmt->execute([
':id' => $tokenId,
':hash' => $tokenHash,
':raw' => $rawToken,
':name' => $name,
':proj' => !empty($projectSlug) ? $projectSlug : null,
':lic' => !empty($licenseKey) ? $licenseKey : null,
':type' => in_array($ownerType, ['license', 'project', 'host', 'dev_agent', 'custom']) ? $ownerType : 'custom',
':identity' => !empty($ownerIdentity) ? $ownerIdentity : null,
':scopes' => json_encode(!empty($scopes) ? $scopes : ['*']),
':env' => in_array($environment, ['production', 'development', 'all']) ? $environment : 'all',
]);
return [
'token_id' => $tokenId,
'raw_token' => $rawToken,
'name' => $name,
'type' => 'master',
];
}
/**
* Provision a Sub-Token using a Master-Token.
*/
public function provisionSubToken(
string $rawMasterToken,
string $name,
?string $instanceIdentity = null,
array $requestedScopes = [],
string $environment = 'all'
): array {
$masterHash = hash('sha256', $rawMasterToken);
$stmt = $this->db->prepare('
SELECT * FROM dc_tokens
WHERE (token_hash = :hash OR raw_token = :raw)
AND type = "master"
AND revoked = 0
');
$stmt->execute([':hash' => $masterHash, ':raw' => $rawMasterToken]);
$master = $stmt->fetch();
if (!$master) {
throw new \InvalidArgumentException('Invalid or revoked Master Token.');
}
$masterScopes = json_decode($master['scopes'], true) ?: ['*'];
// Determine effective scopes
$effectiveScopes = [];
if (in_array('*', $masterScopes)) {
$effectiveScopes = !empty($requestedScopes) ? $requestedScopes : ['*'];
} else {
if (empty($requestedScopes)) {
$effectiveScopes = $masterScopes;
} else {
$effectiveScopes = array_intersect($requestedScopes, $masterScopes);
}
}
if (empty($effectiveScopes)) {
throw new \InvalidArgumentException('Requested scopes are not allowed by this Master Token.');
}
// Determine effective environment
$effectiveEnv = $environment;
if ($master['environment'] !== 'all') {
$effectiveEnv = $master['environment'];
}
$subTokenId = 'tok_s_' . bin2hex(random_bytes(8));
$rawSubToken = 'dc_sub_' . bin2hex(random_bytes(20));
$subHash = hash('sha256', $rawSubToken);
$ins = $this->db->prepare('
INSERT INTO dc_tokens (
token_id, parent_token_id, token_hash, raw_token, name,
project_slug, license_key, owner_type, owner_identity,
type, scopes, environment, created_at
) VALUES (
:id, :parent_id, :hash, :raw, :name,
:proj, :lic, :owner_type, :identity,
"sub", :scopes, :env, NOW()
)
');
$ins->execute([
':id' => $subTokenId,
':parent_id' => $master['token_id'],
':hash' => $subHash,
':raw' => $rawSubToken,
':name' => $name,
':proj' => $master['project_slug'],
':lic' => $master['license_key'],
':owner_type'=> $master['owner_type'],
':identity' => !empty($instanceIdentity) ? $instanceIdentity : $master['owner_identity'],
':scopes' => json_encode(array_values($effectiveScopes)),
':env' => $effectiveEnv,
]);
return [
'token_id' => $subTokenId,
'raw_token' => $rawSubToken,
'name' => $name,
'scopes' => array_values($effectiveScopes),
'environment'=> $effectiveEnv,
'type' => 'sub',
];
}
/**
* Validate any Token (Master or Sub) and check cascading revocation of parent tokens.
*/
public function validateToken(string $rawToken, ?string $requiredScope = null, ?string $environment = null): ?array
{
$hash = hash('sha256', $rawToken);
$stmt = $this->db->prepare('
SELECT t.*, p.revoked as parent_revoked
FROM dc_tokens t
LEFT JOIN dc_tokens p ON t.parent_token_id = p.token_id
WHERE (t.token_hash = :hash OR t.raw_token = :raw)
AND t.revoked = 0
');
$stmt->execute([':hash' => $hash, ':raw' => $rawToken]);
$token = $stmt->fetch();
if (!$token) {
return null;
}
// Cascading Revocation Check
if ($token['type'] === 'sub' && !empty($token['parent_token_id']) && (int)$token['parent_revoked'] === 1) {
return null;
}
// Scope Check
if ($requiredScope !== null) {
$scopes = json_decode($token['scopes'], true) ?: [];
if (!in_array('*', $scopes) && !in_array($requiredScope, $scopes)) {
return null;
}
}
// Environment Check
if ($environment !== null && $token['environment'] !== 'all' && $token['environment'] !== $environment) {
return null;
}
// Update Last Used Timestamp
$upd = $this->db->prepare('UPDATE dc_tokens SET last_used_at = NOW() WHERE id = :id');
$upd->execute([':id' => $token['id']]);
return $token;
}
/**
* Revoke a Token (Master or Sub). If Master, cascade revokes all child Sub-Tokens via DB foreign key or query.
*/
public function revokeToken(string $tokenId): bool
{
$stmt = $this->db->prepare('UPDATE dc_tokens SET revoked = 1 WHERE token_id = :id OR parent_token_id = :id');
return $stmt->execute([':id' => $tokenId]);
}
/**
* Get all Master Tokens with child count.
*/
public function getAllMasterTokens(): array
{
$stmt = $this->db->query('
SELECT m.*, COUNT(s.id) as sub_token_count
FROM dc_tokens m
LEFT JOIN dc_tokens s ON m.token_id = s.parent_token_id
WHERE m.type = "master"
GROUP BY m.id
ORDER BY m.created_at DESC
');
return $stmt->fetchAll() ?: [];
}
/**
* Get all Tokens (Master & Sub).
*/
public function getAllTokens(): array
{
$stmt = $this->db->query('SELECT * FROM dc_tokens ORDER BY created_at DESC');
return $stmt->fetchAll() ?: [];
}
}