TenantAtlas/specs/104-provider-permission-posture/contracts/internal-services.md
ahmido ef380b67d1 feat(104): Provider Permission Posture (#127)
Implements Spec 104: Provider Permission Posture.

What changed
- Generates permission posture findings after each tenant permission compare (queued)
- Stores immutable posture snapshots as StoredReports (JSONB payload)
- Adds global Finding resolved lifecycle (`resolved_at`, `resolved_reason`) with `resolve()` / `reopen()`
- Adds alert pipeline event type `permission_missing` (Alerts v1) and Filament option for Alert Rules
- Adds retention pruning command + daily schedule for StoredReports
- Adds badge mappings for `resolved` finding status and `permission_posture` finding type

UX fixes discovered during manual verification
- Hide “Diff” section for non-drift findings (only drift findings show diff)
- Required Permissions page: “Re-run verification” now links to Tenant view (not onboarding)
- Preserve Technical Details `<details>` open state across Livewire re-renders (Alpine state)

Verification
- Ran `vendor/bin/sail artisan test --compact --filter=PermissionPosture` (50 tests)
- Ran `vendor/bin/sail artisan test --compact --filter="FindingResolved|FindingBadge|PermissionMissingAlert"` (20 tests)
- Ran `vendor/bin/sail bin pint --dirty`

Filament v5 / Livewire v4 compliance
- Filament v5 + Livewire v4: no Livewire v3 usage.

Panel provider registration (Laravel 11+)
- No new panels added. Existing panel providers remain registered via `bootstrap/providers.php`.

Global search rule
- No changes to global-searchable resources.

Destructive actions
- No new destructive Filament actions were added in this PR.

Assets / deploy notes
- No new Filament assets registered. Existing deploy step `php artisan filament:assets` remains unchanged.

Test coverage
- New/updated Pest feature tests cover generator behavior, job integration, alerting, retention pruning, and resolved lifecycle.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #127
2026-02-21 22:32:52 +00:00

150 lines
4.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 0100 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`.