'array', 'is_current' => 'boolean', 'rbac_last_checked_at' => 'datetime', 'removed_from_workspace_at' => 'datetime', ]; public function getExternalIdAttribute(): ?string { return $this->attributes['slug'] ?? null; } public function setExternalIdAttribute(mixed $value): void { $this->attributes['slug'] = is_string($value) ? $value : null; } public function getStatusAttribute(): ?string { return $this->attributes['lifecycle_status'] ?? null; } public function setStatusAttribute(mixed $value): void { $this->attributes['lifecycle_status'] = is_string($value) ? $value : null; } public function getEnvironmentAttribute(): ?string { return $this->attributes['kind'] ?? null; } public function setEnvironmentAttribute(mixed $value): void { $this->attributes['kind'] = is_string($value) ? $value : null; } public function getManagedEnvironmentIdAttribute(): ?string { return $this->attributes['slug'] ?? null; } public function setManagedEnvironmentIdAttribute(mixed $value): void { $this->attributes['slug'] = is_string($value) ? $value : null; } public function getAppClientIdAttribute(): ?string { return $this->legacyProviderClientId(); } public function setAppClientIdAttribute(mixed $value): void { $this->setLegacyMetadataValue('app_client_id', $value); } public function getAppClientSecretAttribute(): ?string { return $this->legacyMetadataValue('app_client_secret'); } public function setAppClientSecretAttribute(mixed $value): void { $this->setLegacyMetadataValue('app_client_secret', $value); } public function getAppCertificateThumbprintAttribute(): ?string { return $this->legacyMetadataValue('app_certificate_thumbprint'); } public function setAppCertificateThumbprintAttribute(mixed $value): void { $this->setLegacyMetadataValue('app_certificate_thumbprint', $value); } public function getAppNotesAttribute(): ?string { return $this->legacyMetadataValue('app_notes'); } public function setAppNotesAttribute(mixed $value): void { $this->setLegacyMetadataValue('app_notes', $value); } public function getAppStatusAttribute(): ?string { $metadata = $this->metadata; return is_array($metadata) && is_string($metadata['app_status'] ?? null) ? $metadata['app_status'] : null; } public function setAppStatusAttribute(mixed $value): void { $metadata = $this->metadata; $metadata = is_array($metadata) ? $metadata : []; if (is_string($value) && $value !== '') { $metadata['app_status'] = $value; } else { unset($metadata['app_status']); } $this->attributes['metadata'] = json_encode($metadata, JSON_THROW_ON_ERROR); } public function getDomainAttribute(): ?string { $metadata = $this->metadata; return is_array($metadata) && is_string($metadata['domain'] ?? null) ? $metadata['domain'] : null; } public function setDomainAttribute(mixed $value): void { $metadata = $this->metadata; $metadata = is_array($metadata) ? $metadata : []; if (is_string($value) && $value !== '') { $metadata['domain'] = $value; } else { unset($metadata['domain']); } $this->attributes['metadata'] = json_encode($metadata, JSON_THROW_ON_ERROR); } public function getRbacCanaryResultsAttribute($value): array { if (is_string($value)) { $decoded = json_decode($value, true); return is_array($decoded) ? $decoded : []; } return $value ?? []; } public function getRbacLastWarningsAttribute($value): array { if (is_string($value)) { $decoded = json_decode($value, true); return is_array($decoded) ? $decoded : []; } $warnings = $value ?? []; if ($this->rbac_scope_mode === 'scope_group' || filled($this->rbac_scope_id)) { $warnings[] = 'scope_limited'; } return $warnings; } protected static function booted(): void { static::creating(function (ManagedEnvironment $tenant) { if (empty($tenant->slug)) { $tenant->slug = Str::slug((string) $tenant->name); if ($tenant->slug === '') { $tenant->slug = (string) Str::uuid(); } } if (empty($tenant->kind)) { $tenant->kind = 'managed_environment'; } if (empty($tenant->lifecycle_status)) { $tenant->lifecycle_status = self::STATUS_ACTIVE; } if ($tenant->workspace_id === null && app()->runningUnitTests() && ! static::$skipTestWorkspaceProvisioning) { $workspace = Workspace::query()->create([ 'name' => 'Test Workspace', 'slug' => 'test-'.Str::lower(Str::random(10)), ]); $tenant->workspace_id = (int) $workspace->getKey(); } }); static::deleting(function (ManagedEnvironment $tenant) { if ($tenant->isForceDeleting()) { return; } $tenant->lifecycle_status = self::STATUS_ARCHIVED; $tenant->saveQuietly(); }); static::restored(function (ManagedEnvironment $tenant) { $tenant->forceFill(['lifecycle_status' => self::STATUS_ACTIVE])->saveQuietly(); }); } public static function activeQuery(): Builder { return static::query() ->whereNull('deleted_at') ->whereNull('removed_from_workspace_at') ->where('lifecycle_status', TenantLifecycle::Active->value) ->whereHas('workspace', fn (Builder $query): Builder => $query ->whereNull('archived_at') ->whereNull('closed_at')); } public static function skipTestWorkspaceProvisioning(bool $skip = true): void { static::$skipTestWorkspaceProvisioning = $skip; } public function makeCurrent(): void { if (! $this->isSelectableAsContext()) { throw new RuntimeException('Only active managed environments can be made current.'); } DB::transaction(function () { static::activeQuery()->update(['is_current' => false]); static::query() ->whereKey($this->getKey()) ->update(['is_current' => true]); }); $this->forceFill(['is_current' => true]); } public static function current(): ?self { $filamentTenant = Filament::getTenant(); if ($filamentTenant instanceof self) { return $filamentTenant; } $envTenantId = getenv('INTUNE_TENANT_ID') ?: null; if ($envTenantId) { $tenant = static::activeQuery() ->where(function (Builder $query) use ($envTenantId) { $query->where('slug', $envTenantId); }) ->first(); if (! $tenant) { throw new RuntimeException('Configured INTUNE_TENANT_ID managed environment is missing or inactive.'); } return $tenant; } $tenant = static::activeQuery() ->where('is_current', true) ->first(); return $tenant; } public static function currentOrFail(): self { $tenant = static::current(); if (! $tenant) { throw new RuntimeException('No current managed environment selected.'); } return $tenant; } public function getRouteKeyName(): string { return 'slug'; } public function resolveRouteBinding($value, $field = null): ?Model { $field ??= $this->getRouteKeyName(); $query = static::query(); if ($field === 'slug') { $query = $query->withTrashed(); } return $query->where($field, $value)->first(); } public function memberships(): HasMany { return $this->hasMany(ManagedEnvironmentMembership::class); } public function workspace(): BelongsTo { return $this->belongsTo(Workspace::class); } public function removedFromWorkspaceByUser(): BelongsTo { return $this->belongsTo(User::class, 'removed_from_workspace_by_user_id'); } public function roleMappings(): HasMany { return $this->hasMany(TenantRoleMapping::class); } public function getFilamentName(): string { $environment = strtoupper((string) ($this->kind ?? 'managed_environment')); return ($this->display_name ?: $this->name)." ({$environment})"; } public function users(): BelongsToMany { return $this->belongsToMany(User::class, 'managed_environment_memberships') ->using(ManagedEnvironmentMembership::class) ->withPivot(['id', 'role', 'source', 'source_ref', 'created_by_user_id']) ->withTimestamps(); } public function policies(): HasMany { return $this->hasMany(Policy::class); } public function backupSets(): HasMany { return $this->hasMany(BackupSet::class); } public function backupSchedules(): HasMany { return $this->hasMany(BackupSchedule::class); } public function policyVersions(): HasMany { return $this->hasMany(PolicyVersion::class); } public function restoreRuns(): HasMany { return $this->hasMany(RestoreRun::class); } public function entraGroups(): HasMany { return $this->hasMany(EntraGroup::class); } public function auditLogs(): HasMany { return $this->hasMany(AuditLog::class); } public function findingExceptions(): HasMany { return $this->hasMany(FindingException::class); } public function evidenceSnapshots(): HasMany { return $this->hasMany(EvidenceSnapshot::class); } public function tenantReviews(): HasMany { return $this->hasMany(TenantReview::class); } public function settings(): HasMany { return $this->hasMany(TenantSetting::class); } public function permissions(): HasMany { return $this->hasMany(TenantPermission::class); } public function providerConnections(): HasMany { return $this->hasMany(ProviderConnection::class); } public function providerCredentials(): HasManyThrough { return $this->hasManyThrough(ProviderCredential::class, ProviderConnection::class, 'managed_environment_id', 'provider_connection_id'); } public function graphTenantId(): ?string { $connection = $this->providerConnections() ->where('provider', 'microsoft') ->where('is_default', true) ->first(); return is_string($connection?->entra_tenant_id) && $connection->entra_tenant_id !== '' ? $connection->entra_tenant_id : null; } public function providerTenantContext(): string { return (string) ($this->graphTenantId() ?? $this->managed_environment_id ?? $this->external_id ?? $this->getKey()); } public function legacyProviderClientId(): ?string { $clientId = trim((string) $this->providerConnections() ->where('provider', 'microsoft') ->where('is_default', true) ->first()?->effectiveAppId()); if ($clientId !== '') { return $clientId; } return $this->legacyMetadataValue('app_client_id'); } public function hasLegacyProviderSecret(): bool { return (bool) $this->providerConnections() ->where('provider', 'microsoft') ->where('is_default', true) ->whereHas('credential') ->exists(); } /** * @return array{client_id: ?string, has_secret: bool} */ public function legacyProviderIdentity(): array { return [ 'client_id' => $this->legacyProviderClientId(), 'has_secret' => $this->hasLegacyProviderSecret(), ]; } private function legacyMetadataValue(string $key): ?string { $metadata = $this->metadata; return is_array($metadata) && is_string($metadata[$key] ?? null) && $metadata[$key] !== '' ? $metadata[$key] : null; } private function setLegacyMetadataValue(string $key, mixed $value): void { $metadata = $this->metadata; $metadata = is_array($metadata) ? $metadata : []; if (is_string($value) && $value !== '') { $metadata[$key] = $value; } else { unset($metadata[$key]); } $this->attributes['metadata'] = json_encode($metadata, JSON_THROW_ON_ERROR); } /** * @deprecated Runtime provider calls must resolve ProviderConnection + ProviderGateway. * * @return array{tenant:?string,client_id:?string,client_secret:?string} */ public function graphOptions(): array { throw new \BadMethodCallException( 'ManagedEnvironment::graphOptions() has been removed. Resolve a ProviderConnection via ProviderConnectionResolver and build options via ProviderGateway (or use MicrosoftGraphOptionsResolver).' ); } public function scopeForTenant(Builder $query, self|int|string $tenant): Builder { if ($tenant instanceof self) { return $query->whereKey($tenant->getKey()); } if (is_int($tenant) || ctype_digit((string) $tenant)) { return $query->whereKey($tenant); } return $query->where('slug', $tenant); } public function isActive(): bool { return $this->isSelectableAsContext(); } public function lifecycle(): TenantLifecycle { return TenantLifecycle::fromTenant($this); } public function isDraft(): bool { return $this->lifecycle() === TenantLifecycle::Draft; } public function isOnboarding(): bool { return $this->lifecycle() === TenantLifecycle::Onboarding; } public function isArchived(): bool { return $this->lifecycle() === TenantLifecycle::Archived; } public function isSelectableAsContext(): bool { if ($this->trashed() || $this->isRemovedFromWorkspace() || ! $this->lifecycle()->canSelectAsContext()) { return false; } if ($this->workspace_id === null) { return false; } $workspace = $this->relationLoaded('workspace') ? $this->workspace : $this->workspace()->first(['id', 'archived_at', 'closed_at']); return $workspace instanceof Workspace && $workspace->isSelectableAsContext(); } public function canResumeOnboarding(): bool { return ! $this->trashed() && ! $this->isRemovedFromWorkspace() && $this->lifecycle()->canResumeOnboarding(); } public function isRemovedFromWorkspace(): bool { return $this->removed_from_workspace_at !== null; } public function workspaceRemovalReason(): ?string { $reason = trim((string) $this->removed_from_workspace_reason); return $reason === '' ? null : $reason; } }