- 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
150 lines
4.4 KiB
Markdown
150 lines
4.4 KiB
Markdown
# 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 of `TenantPermissionService::compare()` — see schema below
|
||
- `$operationRun`: Optional OperationRun for tracking (created by caller if not provided)
|
||
|
||
**Input schema** (`$permissionComparison`):
|
||
```php
|
||
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):
|
||
```php
|
||
PostureResult {
|
||
int $findingsCreated,
|
||
int $findingsResolved,
|
||
int $findingsReopened,
|
||
int $findingsUnchanged,
|
||
int $errorsRecorded,
|
||
int $postureScore,
|
||
int $storedReportId,
|
||
}
|
||
```
|
||
|
||
**Side effects**:
|
||
1. Creates/updates/resolves `Finding` records (type: `permission_posture`)
|
||
2. Creates a `StoredReport` (type: `permission_posture`)
|
||
3. 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
|
||
|
||
```php
|
||
public function __construct(
|
||
public readonly int $tenantId,
|
||
public readonly array $permissionComparison,
|
||
)
|
||
```
|
||
|
||
### `handle(PermissionPostureFindingGenerator $generator): void`
|
||
|
||
**Flow**:
|
||
1. Load Tenant (fail if not found)
|
||
2. Skip if tenant has no active provider connection (FR-016)
|
||
3. Create OperationRun (`type = permission_posture_check`)
|
||
4. Call `$generator->generate($tenant, $permissionComparison, $run)`
|
||
5. Record summary counts on OperationRun
|
||
6. 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()`):
|
||
|
||
```php
|
||
[
|
||
'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 rule
|
||
- `created_at` or `updated_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`.
|