293 lines
9.1 KiB
PHP
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);
|
|
}
|
|
}
|