- T001-T014: Foundation - StoredReport model/migration, Finding resolved lifecycle, badge mappings (resolved status, permission_posture type), OperationCatalog + AlertRule constants - T015-T022: US1 - PermissionPostureFindingGenerator with fingerprint-based idempotent upsert, severity from feature-impact count, auto-resolve on grant, auto-reopen on revoke, error findings (FR-015), stale finding cleanup; GeneratePermissionPostureFindingsJob dispatched from health check; PostureResult VO + PostureScoreCalculator - T023-T026: US2+US4 - Stored report payload validation, temporal ordering, polymorphic reusability, score accuracy acceptance tests - T027-T029: US3 - EvaluateAlertsJob.permissionMissingEvents() wired into alert pipeline, AlertRuleResource event type option, cooldown/dedupe tests - T030-T034: Polish - PruneStoredReportsCommand with config retention, scheduled daily, end-to-end integration test, Pint clean UI bug fixes found during testing: - FindingResource: hide Diff section for non-drift findings - TenantRequiredPermissions: fix re-run verification link - tenant-required-permissions.blade.php: preserve details open state 70 tests (50 PermissionPosture + 20 FindingResolved/Badge/Alert), 216 assertions
4.4 KiB
Internal Contracts: Provider Permission Posture (Spec 104)
Date: 2026-02-21 | Branch: 104-provider-permission-posture
This feature does NOT expose external HTTP APIs. All contracts are internal service interfaces.
Contract 1: PermissionPostureFindingGenerator
Service: App\Services\PermissionPosture\PermissionPostureFindingGenerator
generate(Tenant $tenant, array $permissionComparison, ?OperationRun $operationRun = null): PostureResult
Input:
$tenant: The tenant being evaluated$permissionComparison: Output ofTenantPermissionService::compare()— see schema below$operationRun: Optional OperationRun for tracking (created by caller if not provided)
Input schema ($permissionComparison):
array{
overall_status: 'granted'|'missing'|'error',
permissions: array<int, array{
key: string,
type: string,
description: ?string,
features: array<int, string>,
status: 'granted'|'missing'|'error',
details: ?array,
}>,
last_refreshed_at: ?string,
}
Output (PostureResult value object):
PostureResult {
int $findingsCreated,
int $findingsResolved,
int $findingsReopened,
int $findingsUnchanged,
int $errorsRecorded,
int $postureScore,
int $storedReportId,
}
Side effects:
- Creates/updates/resolves
Findingrecords (type:permission_posture) - Creates a
StoredReport(type:permission_posture) - Dispatches alert events for new/reopened findings via the alert pipeline
Idempotency: Fingerprint-based. Same input produces same findings (no duplicates).
Contract 2: PostureScoreCalculator
Service: App\Services\PermissionPosture\PostureScoreCalculator
calculate(array $permissionComparison): int
Input: $permissionComparison — same schema as Contract 1 input.
Output: Integer 0–100 representing round(granted_count / required_count * 100). Returns 100 if required_count is 0.
Pure function: No side effects, no DB access.
Contract 3: GeneratePermissionPostureFindingsJob
Job: App\Jobs\GeneratePermissionPostureFindingsJob
Constructor
public function __construct(
public readonly int $tenantId,
public readonly array $permissionComparison,
)
handle(PermissionPostureFindingGenerator $generator): void
Flow:
- Load Tenant (fail if not found)
- Skip if tenant has no active provider connection (FR-016)
- Create OperationRun (
type = permission_posture_check) - Call
$generator->generate($tenant, $permissionComparison, $run) - Record summary counts on OperationRun
- Complete OperationRun
Queue: default (same queue as health checks)
Dispatch point: ProviderConnectionHealthCheckJob::handle() after TenantPermissionService::compare() returns, when $permissionComparison['overall_status'] !== 'error'.
Contract 4: Alert Event Schema (EVENT_PERMISSION_MISSING)
Event array (passed to AlertDispatchService::dispatchEvent()):
[
'event_type' => 'permission_missing',
'tenant_id' => int,
'severity' => 'low'|'medium'|'high'|'critical',
'fingerprint_key' => "permission_missing:{tenant_id}:{permission_key}",
'title' => "Missing permission: {permission_key}",
'body' => "Tenant {tenant_name} is missing {permission_key}. Blocked features: {feature_list}.",
'metadata' => [
'permission_key' => string,
'permission_type' => string,
'blocked_features' => array<string>,
'posture_score' => int,
],
]
Produced by: PermissionPostureFindingGenerator for each newly created or re-opened finding.
Not produced for: Auto-resolved findings, existing unchanged findings, error findings.
Contract 5: EvaluateAlertsJob Extension
New method: permissionMissingEvents(): array
Query: Finding where:
finding_type = 'permission_posture'status IN ('new')(only new/re-opened, not acknowledged or resolved)severity>= minimum severity threshold from alert rulecreated_atorupdated_at>= window start
Output: Array of event arrays matching Contract 4 schema.
Contract 6: StoredReport Retention
PruneStoredReportsCommand (Artisan command)
Signature: stored-reports:prune {--days=90}
Behavior: Deletes stored_reports where created_at < now() - days. Outputs count of deleted records.
Schedule: Daily via routes/console.php.