TenantAtlas/apps/platform/app/Services/TenantConfiguration/ClaimGuard.php
Ahmed Darrazi 19037e1dd8
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m15s
feat: complete spec 421 Entra comparable/renderable pack
2026-06-27 23:42:58 +02:00

293 lines
9.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\TenantConfiguration;
use App\Support\TenantConfiguration\ClaimState;
use App\Support\TenantConfiguration\CoverageLevel;
use App\Support\TenantConfiguration\IdentityState;
use App\Support\TenantConfiguration\RestoreTier;
use App\Support\TenantConfiguration\SourceClass;
final class ClaimGuard
{
public function evaluateStatement(string $claim, bool $internalOperatorOnly = false): ClaimState
{
$tokens = $this->claimTokens($claim);
$registryScoped = $this->isRegistryScopedStatement($tokens);
if ($this->hasUnsafeBroadCoverageClaim($tokens, $registryScoped)) {
return ClaimState::ClaimBlocked;
}
if ($internalOperatorOnly && $this->hasScopedInternalComparableRenderableClaim($tokens)) {
return ClaimState::InternalOnly;
}
if ($internalOperatorOnly && $registryScoped) {
return ClaimState::InternalOnly;
}
return $registryScoped ? ClaimState::ClaimBlocked : ClaimState::ClaimLimited;
}
public function evaluate(
?string $scopeKey,
CoverageLevel|string $requestedLevel,
CoverageLevel|string $actualLevel,
bool $scopeComplete,
bool $customerFacing = false,
bool $customerClaimsAllowed = true,
bool $unscoped = false,
?int $percentage = null,
SourceClass|string|null $sourceClass = null,
RestoreTier|string|null $restoreTier = null,
IdentityState|string|null $identityState = null,
bool $restoreClaim = false,
bool $allowsBetaClaims = false,
bool $allowsCertifiedClaims = false,
bool $allowsDerivedIdentityClaims = false,
): ClaimState {
$requested = $this->coverageLevel($requestedLevel);
$actual = $this->coverageLevel($actualLevel);
$source = $this->sourceClass($sourceClass);
$restore = $this->restoreTier($restoreTier);
$identity = $this->identityState($identityState);
if (($scopeKey === null || $unscoped) && $percentage === 100) {
return ClaimState::ClaimBlocked;
}
if (in_array($identity, [
IdentityState::IdentityConflict,
IdentityState::MissingExternalId,
IdentityState::UnsupportedIdentity,
], true)) {
return ClaimState::ClaimBlocked;
}
if ($identity === IdentityState::Derived && ! $allowsDerivedIdentityClaims) {
return $customerFacing ? ClaimState::ClaimBlocked : ClaimState::ClaimLimited;
}
if ($source?->isBetaExperimental() === true) {
if (! $allowsBetaClaims) {
return ClaimState::ClaimBlocked;
}
if ($requested === CoverageLevel::Certified && ! $allowsCertifiedClaims) {
return ClaimState::ClaimBlocked;
}
}
if (($restoreClaim || $requested->meets(CoverageLevel::Restorable)) && $restore !== RestoreTier::Restorable) {
return ClaimState::ClaimBlocked;
}
if ($customerFacing && (! $scopeComplete || ! $customerClaimsAllowed)) {
return ClaimState::ClaimBlocked;
}
if ($scopeKey === null || ! $actual->meets($requested)) {
return ClaimState::ClaimLimited;
}
return ClaimState::ClaimAllowed;
}
private function coverageLevel(CoverageLevel|string $level): CoverageLevel
{
return $level instanceof CoverageLevel ? $level : CoverageLevel::from($level);
}
private function sourceClass(SourceClass|string|null $sourceClass): ?SourceClass
{
if ($sourceClass === null || $sourceClass instanceof SourceClass) {
return $sourceClass;
}
return SourceClass::from($sourceClass);
}
private function restoreTier(RestoreTier|string|null $restoreTier): ?RestoreTier
{
if ($restoreTier === null || $restoreTier instanceof RestoreTier) {
return $restoreTier;
}
return RestoreTier::from($restoreTier);
}
private function identityState(IdentityState|string|null $identityState): ?IdentityState
{
if ($identityState === null || $identityState instanceof IdentityState) {
return $identityState;
}
return IdentityState::from($identityState);
}
/**
* @return list<string>
*/
private function claimTokens(string $claim): array
{
$normalized = strtolower($claim);
$normalized = str_replace('%', ' percent ', $normalized);
$normalized = (string) preg_replace('/[^a-z0-9]+/', ' ', $normalized);
$normalized = (string) preg_replace('/\s+/', ' ', trim($normalized));
if ($normalized === '') {
return [];
}
return explode(' ', $normalized);
}
/**
* @param list<string> $tokens
*/
private function isRegistryScopedStatement(array $tokens): bool
{
return $this->hasToken($tokens, 'registry')
&& $this->hasToken($tokens, 'coverage')
&& $this->hasToken($tokens, 'seeded')
&& $this->hasToken($tokens, 'resource')
&& $this->hasToken($tokens, 'type')
&& $this->hasToken($tokens, 'entries');
}
/**
* @param list<string> $tokens
*/
private function hasUnsafeBroadCoverageClaim(array $tokens, bool $registryScoped): bool
{
$hasCoverageSurface = $this->hasAnyToken($tokens, [
'coverage',
'resource',
'resources',
'support',
'supported',
'tenant',
]);
$hasWorkloadReference = $this->hasMicrosoft365Reference($tokens)
|| $this->hasAnyToken($tokens, [
'entra',
'exchange',
'teams',
'defender',
'purview',
'tcm',
])
|| ($this->hasToken($tokens, 'security') && $this->hasToken($tokens, 'compliance'));
if ($this->hasHundredPercent($tokens) && ! $registryScoped) {
return true;
}
if ($this->hasCertificationTerm($tokens) && ($hasWorkloadReference || $hasCoverageSurface || $registryScoped)) {
return true;
}
if ($this->hasRestoreReadyTerm($tokens) && ($hasWorkloadReference || $hasCoverageSurface || $registryScoped)) {
return true;
}
if ($this->hasCustomerReadyTerm($tokens) && ($hasWorkloadReference || $hasCoverageSurface || $registryScoped)) {
return true;
}
if ($this->hasAnyToken($tokens, ['full', 'complete', 'all'])
&& ($hasCoverageSurface || $this->hasToken($tokens, 'tenant'))
&& ($hasWorkloadReference || $this->hasToken($tokens, 'tenant'))) {
return true;
}
return false;
}
/**
* @param list<string> $tokens
*/
private function hasScopedInternalComparableRenderableClaim(array $tokens): bool
{
return $this->hasToken($tokens, 'selected')
&& $this->hasToken($tokens, 'entra')
&& ($this->hasToken($tokens, 'comparable') || $this->hasToken($tokens, 'renderable'))
&& ($this->hasToken($tokens, 'internal') || $this->hasToken($tokens, 'operator'));
}
/**
* @param list<string> $tokens
*/
private function hasMicrosoft365Reference(array $tokens): bool
{
return $this->hasToken($tokens, 'm365')
|| ($this->hasToken($tokens, 'microsoft') && $this->hasToken($tokens, '365'));
}
/**
* @param list<string> $tokens
*/
private function hasHundredPercent(array $tokens): bool
{
return $this->hasToken($tokens, '100')
&& $this->hasToken($tokens, 'percent');
}
/**
* @param list<string> $tokens
*/
private function hasCertificationTerm(array $tokens): bool
{
return $this->hasAnyToken($tokens, [
'certified',
'certification',
'certify',
'certifies',
]);
}
/**
* @param list<string> $tokens
*/
private function hasRestoreReadyTerm(array $tokens): bool
{
return $this->hasToken($tokens, 'restorable')
|| ($this->hasToken($tokens, 'restore') && $this->hasAnyToken($tokens, ['ready', 'readiness']))
|| ($this->hasToken($tokens, 'restore') && $this->hasToken($tokens, 'coverage'));
}
/**
* @param list<string> $tokens
*/
private function hasCustomerReadyTerm(array $tokens): bool
{
return $this->hasToken($tokens, 'customer')
&& $this->hasAnyToken($tokens, ['ready', 'readiness', 'proof']);
}
/**
* @param list<string> $tokens
*/
private function hasAnyToken(array $tokens, array $expectedTokens): bool
{
foreach ($expectedTokens as $token) {
if ($this->hasToken($tokens, $token)) {
return true;
}
}
return false;
}
/**
* @param list<string> $tokens
*/
private function hasToken(array $tokens, string $expectedToken): bool
{
return in_array($expectedToken, $tokens, true);
}
}