merge: agent session work
This commit is contained in:
commit
8d90550abe
83
app/Filament/Pages/Tenancy/RegisterTenant.php
Normal file
83
app/Filament/Pages/Tenancy/RegisterTenant.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Tenancy;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Support\TenantRole;
|
||||
use Filament\Forms;
|
||||
use Filament\Pages\Tenancy\RegisterTenant as BaseRegisterTenant;
|
||||
use Filament\Schemas\Schema;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class RegisterTenant extends BaseRegisterTenant
|
||||
{
|
||||
public static function getLabel(): string
|
||||
{
|
||||
return 'Register tenant';
|
||||
}
|
||||
|
||||
public static function canView(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\Select::make('environment')
|
||||
->options([
|
||||
'prod' => 'PROD',
|
||||
'dev' => 'DEV',
|
||||
'staging' => 'STAGING',
|
||||
'other' => 'Other',
|
||||
])
|
||||
->default('other')
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('tenant_id')
|
||||
->label('Tenant ID (GUID)')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true),
|
||||
Forms\Components\TextInput::make('domain')
|
||||
->label('Primary domain')
|
||||
->maxLength(255),
|
||||
Forms\Components\TextInput::make('app_client_id')
|
||||
->label('App Client ID')
|
||||
->maxLength(255),
|
||||
Forms\Components\TextInput::make('app_client_secret')
|
||||
->label('App Client Secret')
|
||||
->password()
|
||||
->dehydrateStateUsing(fn ($state) => filled($state) ? $state : null)
|
||||
->dehydrated(fn ($state) => filled($state)),
|
||||
Forms\Components\TextInput::make('app_certificate_thumbprint')
|
||||
->label('Certificate thumbprint')
|
||||
->maxLength(255),
|
||||
Forms\Components\Textarea::make('app_notes')
|
||||
->label('Notes')
|
||||
->rows(3),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
protected function handleRegistration(array $data): Model
|
||||
{
|
||||
$tenant = Tenant::create($data);
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
if ($user instanceof User) {
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => TenantRole::Owner->value],
|
||||
]);
|
||||
}
|
||||
|
||||
return $tenant;
|
||||
}
|
||||
}
|
||||
@ -112,8 +112,16 @@ public function table(Table $table): Table
|
||||
Actions\ActionGroup::make([
|
||||
Actions\ViewAction::make()
|
||||
->label('View policy')
|
||||
->url(fn ($record) => $record->policy_id ? PolicyResource::getUrl('view', ['record' => $record->policy_id]) : null)
|
||||
->hidden(fn ($record) => ! $record->policy_id)
|
||||
->url(function (BackupItem $record): ?string {
|
||||
if (! $record->policy_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tenant = $this->getOwnerRecord()->tenant ?? \App\Models\Tenant::current();
|
||||
|
||||
return PolicyResource::getUrl('view', ['record' => $record->policy_id], tenant: $tenant);
|
||||
})
|
||||
->hidden(fn (BackupItem $record) => ! $record->policy_id)
|
||||
->openUrlInNewTab(true),
|
||||
Actions\Action::make('remove')
|
||||
->label('Remove')
|
||||
|
||||
@ -186,9 +186,7 @@ public static function table(Table $table): Table
|
||||
->falseLabel('Archived'),
|
||||
])
|
||||
->actions([
|
||||
Actions\ViewAction::make()
|
||||
->url(fn (PolicyVersion $record) => static::getUrl('view', ['record' => $record]))
|
||||
->openUrlInNewTab(false),
|
||||
Actions\ViewAction::make(),
|
||||
Actions\ActionGroup::make([
|
||||
Actions\Action::make('restore_via_wizard')
|
||||
->label('Restore via Wizard')
|
||||
|
||||
@ -4,13 +4,16 @@
|
||||
|
||||
use App\Filament\Resources\TenantResource\Pages;
|
||||
use App\Http\Controllers\RbacDelegatedAuthController;
|
||||
use App\Jobs\SyncPoliciesJob;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Services\Graph\GraphClientInterface;
|
||||
use App\Services\Intune\AuditLogger;
|
||||
use App\Services\Intune\RbacHealthService;
|
||||
use App\Services\Intune\RbacOnboardingService;
|
||||
use App\Services\Intune\TenantConfigService;
|
||||
use App\Services\Intune\TenantPermissionService;
|
||||
use App\Support\TenantRole;
|
||||
use BackedEnum;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\ActionGroup;
|
||||
@ -23,6 +26,8 @@
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
@ -33,6 +38,8 @@ class TenantResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Tenant::class;
|
||||
|
||||
protected static bool $isScopedToTenant = false;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-building-office-2';
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Settings';
|
||||
@ -44,6 +51,15 @@ public static function form(Schema $schema): Schema
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\Select::make('environment')
|
||||
->options([
|
||||
'prod' => 'PROD',
|
||||
'dev' => 'DEV',
|
||||
'staging' => 'STAGING',
|
||||
'other' => 'Other',
|
||||
])
|
||||
->default('other')
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('tenant_id')
|
||||
->label('Tenant ID (GUID)')
|
||||
->required()
|
||||
@ -69,10 +85,28 @@ public static function form(Schema $schema): Schema
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user instanceof User) {
|
||||
return parent::getEloquentQuery()->whereRaw('1 = 0');
|
||||
}
|
||||
|
||||
$tenantIds = $user->tenants()
|
||||
->withTrashed()
|
||||
->pluck('tenants.id');
|
||||
|
||||
return parent::getEloquentQuery()
|
||||
->withTrashed()
|
||||
->whereIn('id', $tenantIds)
|
||||
->withCount('policies')
|
||||
->withMax('policies as last_policy_sync_at', 'last_synced_at');
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->modifyQueryUsing(fn (\Illuminate\Database\Eloquent\Builder $query) => $query->withTrashed())
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->searchable(),
|
||||
@ -80,6 +114,23 @@ public static function table(Table $table): Table
|
||||
->label('Tenant ID')
|
||||
->copyable()
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('environment')
|
||||
->badge()
|
||||
->color(fn (?string $state) => match ($state) {
|
||||
'prod' => 'danger',
|
||||
'dev' => 'warning',
|
||||
'staging' => 'info',
|
||||
default => 'gray',
|
||||
})
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('policies_count')
|
||||
->label('Policies')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('last_policy_sync_at')
|
||||
->label('Last Sync')
|
||||
->since()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('domain')
|
||||
->copyable()
|
||||
->toggleable(),
|
||||
@ -102,6 +153,13 @@ public static function table(Table $table): Table
|
||||
->trueLabel('All')
|
||||
->falseLabel('Archived')
|
||||
->default(true),
|
||||
Tables\Filters\SelectFilter::make('environment')
|
||||
->options([
|
||||
'prod' => 'PROD',
|
||||
'dev' => 'DEV',
|
||||
'staging' => 'STAGING',
|
||||
'other' => 'Other',
|
||||
]),
|
||||
Tables\Filters\SelectFilter::make('app_status')
|
||||
->options([
|
||||
'ok' => 'OK',
|
||||
@ -113,6 +171,50 @@ public static function table(Table $table): Table
|
||||
->actions([
|
||||
Actions\ViewAction::make(),
|
||||
ActionGroup::make([
|
||||
Actions\Action::make('syncTenant')
|
||||
->label('Sync')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->color('warning')
|
||||
->requiresConfirmation()
|
||||
->visible(function (Tenant $record): bool {
|
||||
if (! $record->isActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->canSyncTenant($record);
|
||||
})
|
||||
->action(function (Tenant $record, AuditLogger $auditLogger): void {
|
||||
SyncPoliciesJob::dispatch($record->getKey());
|
||||
|
||||
$auditLogger->log(
|
||||
tenant: $record,
|
||||
action: 'tenant.sync_dispatched',
|
||||
resourceType: 'tenant',
|
||||
resourceId: (string) $record->id,
|
||||
status: 'success',
|
||||
context: ['metadata' => ['tenant_id' => $record->tenant_id]],
|
||||
);
|
||||
|
||||
Notification::make()
|
||||
->title('Sync started')
|
||||
->body("Sync dispatched for {$record->name}.")
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->iconColor('warning')
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
Actions\Action::make('openTenant')
|
||||
->label('Open')
|
||||
->icon('heroicon-o-arrow-right')
|
||||
->color('primary')
|
||||
->url(fn (Tenant $record) => \App\Filament\Resources\PolicyResource::getUrl('index', tenant: $record))
|
||||
->visible(fn (Tenant $record) => $record->isActive()),
|
||||
Actions\EditAction::make(),
|
||||
Actions\RestoreAction::make()
|
||||
->label('Restore')
|
||||
@ -242,7 +344,78 @@ public static function table(Table $table): Table
|
||||
}),
|
||||
])->icon('heroicon-o-ellipsis-vertical'),
|
||||
])
|
||||
->bulkActions([])
|
||||
->bulkActions([
|
||||
Actions\BulkAction::make('syncSelected')
|
||||
->label('Sync selected')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->color('warning')
|
||||
->requiresConfirmation()
|
||||
->visible(function (): bool {
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->tenants()
|
||||
->whereIn('role', [
|
||||
TenantRole::Owner->value,
|
||||
TenantRole::Manager->value,
|
||||
TenantRole::Operator->value,
|
||||
])
|
||||
->exists();
|
||||
})
|
||||
->authorize(function (): bool {
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->tenants()
|
||||
->whereIn('role', [
|
||||
TenantRole::Owner->value,
|
||||
TenantRole::Manager->value,
|
||||
TenantRole::Operator->value,
|
||||
])
|
||||
->exists();
|
||||
})
|
||||
->action(function (Collection $records, AuditLogger $auditLogger): void {
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user instanceof User) {
|
||||
return;
|
||||
}
|
||||
|
||||
$eligible = $records
|
||||
->filter(fn ($record) => $record instanceof Tenant && $record->isActive())
|
||||
->filter(fn (Tenant $tenant) => $user->canSyncTenant($tenant));
|
||||
|
||||
foreach ($eligible as $tenant) {
|
||||
SyncPoliciesJob::dispatch($tenant->getKey());
|
||||
|
||||
$auditLogger->log(
|
||||
tenant: $tenant,
|
||||
action: 'tenant.sync_dispatched',
|
||||
resourceType: 'tenant',
|
||||
resourceId: (string) $tenant->id,
|
||||
status: 'success',
|
||||
context: ['metadata' => ['tenant_id' => $tenant->tenant_id]],
|
||||
);
|
||||
}
|
||||
|
||||
$count = $eligible->count();
|
||||
|
||||
Notification::make()
|
||||
->title('Bulk sync started')
|
||||
->body("Dispatched sync for {$count} tenant(s).")
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->iconColor('warning')
|
||||
->success()
|
||||
->send();
|
||||
})
|
||||
->deselectRecordsAfterCompletion(),
|
||||
])
|
||||
->headerActions([]);
|
||||
}
|
||||
|
||||
@ -440,7 +613,10 @@ public static function rbacAction(): Actions\Action
|
||||
->label('Open RBAC login')
|
||||
->url(route('admin.rbac.start', [
|
||||
'tenant' => $record->graphTenantId(),
|
||||
'return' => route('filament.admin.resources.tenants.view', $record),
|
||||
'return' => route('filament.admin.resources.tenants.view', [
|
||||
'tenant' => $record->external_id,
|
||||
'record' => $record,
|
||||
]),
|
||||
])),
|
||||
])
|
||||
->warning()
|
||||
@ -579,7 +755,10 @@ private static function loginToSearchRolesAction(?Tenant $tenant): ?Actions\Acti
|
||||
->label('Login to load roles')
|
||||
->url(route('admin.rbac.start', [
|
||||
'tenant' => $tenant->graphTenantId(),
|
||||
'return' => route('filament.admin.resources.tenants.view', $tenant),
|
||||
'return' => route('filament.admin.resources.tenants.view', [
|
||||
'tenant' => $tenant->external_id,
|
||||
'record' => $tenant,
|
||||
]),
|
||||
]));
|
||||
}
|
||||
|
||||
@ -761,7 +940,10 @@ private static function loginToSearchGroupsAction(?Tenant $tenant): ?Actions\Act
|
||||
->label('Login to search groups')
|
||||
->url(route('admin.rbac.start', [
|
||||
'tenant' => $tenant->graphTenantId(),
|
||||
'return' => route('filament.admin.resources.tenants.view', $tenant),
|
||||
'return' => route('filament.admin.resources.tenants.view', [
|
||||
'tenant' => $tenant->external_id,
|
||||
'record' => $tenant,
|
||||
]),
|
||||
]));
|
||||
}
|
||||
|
||||
|
||||
@ -3,9 +3,24 @@
|
||||
namespace App\Filament\Resources\TenantResource\Pages;
|
||||
|
||||
use App\Filament\Resources\TenantResource;
|
||||
use App\Models\User;
|
||||
use App\Support\TenantRole;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateTenant extends CreateRecord
|
||||
{
|
||||
protected static string $resource = TenantResource::class;
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user instanceof User) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$this->record->getKey() => ['role' => TenantRole::Owner->value],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,16 +2,19 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Models\Contracts\HasName;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
|
||||
class Tenant extends Model
|
||||
class Tenant extends Model implements HasName
|
||||
{
|
||||
use HasFactory;
|
||||
use SoftDeletes;
|
||||
@ -114,6 +117,12 @@ public function makeCurrent(): void
|
||||
|
||||
public static function current(): self
|
||||
{
|
||||
$filamentTenant = Filament::getTenant();
|
||||
|
||||
if ($filamentTenant instanceof self) {
|
||||
return $filamentTenant;
|
||||
}
|
||||
|
||||
$envTenantId = getenv('INTUNE_TENANT_ID') ?: null;
|
||||
|
||||
if ($envTenantId) {
|
||||
@ -142,6 +151,20 @@ public static function current(): self
|
||||
return $tenant;
|
||||
}
|
||||
|
||||
public function getFilamentName(): string
|
||||
{
|
||||
$environment = strtoupper((string) ($this->environment ?? 'other'));
|
||||
|
||||
return "{$this->name} ({$environment})";
|
||||
}
|
||||
|
||||
public function users(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class)
|
||||
->withPivot('role')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function policies(): HasMany
|
||||
{
|
||||
return $this->hasMany(Policy::class);
|
||||
|
||||
@ -2,13 +2,21 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Support\TenantRole;
|
||||
use Filament\Models\Contracts\FilamentUser;
|
||||
use Filament\Models\Contracts\HasDefaultTenant;
|
||||
use Filament\Models\Contracts\HasTenants;
|
||||
use Filament\Panel;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class User extends Authenticatable implements FilamentUser
|
||||
class User extends Authenticatable implements FilamentUser, HasDefaultTenant, HasTenants
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
@ -51,4 +59,113 @@ public function canAccessPanel(Panel $panel): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function tenants(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Tenant::class)
|
||||
->withPivot('role')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function tenantPreferences(): HasMany
|
||||
{
|
||||
return $this->hasMany(UserTenantPreference::class);
|
||||
}
|
||||
|
||||
private function tenantPivotTableExists(): bool
|
||||
{
|
||||
static $exists;
|
||||
|
||||
return $exists ??= Schema::hasTable('tenant_user');
|
||||
}
|
||||
|
||||
private function tenantPreferencesTableExists(): bool
|
||||
{
|
||||
static $exists;
|
||||
|
||||
return $exists ??= Schema::hasTable('user_tenant_preferences');
|
||||
}
|
||||
|
||||
public function tenantRole(Tenant $tenant): ?TenantRole
|
||||
{
|
||||
if (! $this->tenantPivotTableExists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$role = $this->tenants()
|
||||
->whereKey($tenant->getKey())
|
||||
->value('role');
|
||||
|
||||
if (! is_string($role)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return TenantRole::tryFrom($role);
|
||||
}
|
||||
|
||||
public function canSyncTenant(Tenant $tenant): bool
|
||||
{
|
||||
$role = $this->tenantRole($tenant);
|
||||
|
||||
return $role?->canSync() ?? false;
|
||||
}
|
||||
|
||||
public function canAccessTenant(Model $tenant): bool
|
||||
{
|
||||
if (! $tenant instanceof Tenant) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $this->tenantPivotTableExists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->tenants()
|
||||
->whereKey($tenant->getKey())
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function getTenants(Panel $panel): array|Collection
|
||||
{
|
||||
if (! $this->tenantPivotTableExists()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return $this->tenants()
|
||||
->where('status', 'active')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function getDefaultTenant(Panel $panel): ?Model
|
||||
{
|
||||
if (! $this->tenantPivotTableExists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tenantId = null;
|
||||
|
||||
if ($this->tenantPreferencesTableExists()) {
|
||||
$tenantId = $this->tenantPreferences()
|
||||
->whereNotNull('last_used_at')
|
||||
->orderByDesc('last_used_at')
|
||||
->value('tenant_id');
|
||||
}
|
||||
|
||||
if ($tenantId !== null) {
|
||||
$tenant = $this->tenants()
|
||||
->where('status', 'active')
|
||||
->whereKey($tenantId)
|
||||
->first();
|
||||
|
||||
if ($tenant !== null) {
|
||||
return $tenant;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->tenants()
|
||||
->where('status', 'active')
|
||||
->orderBy('name')
|
||||
->first();
|
||||
}
|
||||
}
|
||||
|
||||
26
app/Models/UserTenantPreference.php
Normal file
26
app/Models/UserTenantPreference.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class UserTenantPreference extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
'is_favorite' => 'boolean',
|
||||
'last_used_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,9 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Models\UserTenantPreference;
|
||||
use App\Services\Graph\GraphClientInterface;
|
||||
use App\Services\Graph\MicrosoftGraphClient;
|
||||
use App\Services\Graph\NullGraphClient;
|
||||
@ -18,6 +21,9 @@
|
||||
use App\Services\Intune\WindowsFeatureUpdateProfileNormalizer;
|
||||
use App\Services\Intune\WindowsQualityUpdateProfileNormalizer;
|
||||
use App\Services\Intune\WindowsUpdateRingNormalizer;
|
||||
use Filament\Events\TenantSet;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
@ -66,6 +72,35 @@ public function register(): void
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
Event::listen(TenantSet::class, function (TenantSet $event): void {
|
||||
static $hasPreferencesTable;
|
||||
|
||||
$hasPreferencesTable ??= Schema::hasTable('user_tenant_preferences');
|
||||
|
||||
if (! $hasPreferencesTable) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tenant = $event->getTenant();
|
||||
$user = $event->getUser();
|
||||
|
||||
if (! $tenant instanceof Tenant) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $user instanceof User) {
|
||||
return;
|
||||
}
|
||||
|
||||
UserTenantPreference::query()->updateOrCreate(
|
||||
[
|
||||
'user_id' => $user->getKey(),
|
||||
'tenant_id' => $tenant->getKey(),
|
||||
],
|
||||
[
|
||||
'last_used_at' => now(),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Providers\Filament;
|
||||
|
||||
use App\Filament\Pages\Tenancy\RegisterTenant;
|
||||
use App\Models\Tenant;
|
||||
use Filament\Http\Middleware\Authenticate;
|
||||
use Filament\Http\Middleware\AuthenticateSession;
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
@ -29,6 +31,10 @@ public function panel(Panel $panel): Panel
|
||||
->id('admin')
|
||||
->path('admin')
|
||||
->login()
|
||||
->tenant(Tenant::class, slugAttribute: 'external_id')
|
||||
->tenantRoutePrefix('t')
|
||||
->searchableTenantMenu()
|
||||
->tenantRegistration(RegisterTenant::class)
|
||||
->colors([
|
||||
'primary' => Color::Amber,
|
||||
])
|
||||
|
||||
21
app/Support/TenantRole.php
Normal file
21
app/Support/TenantRole.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
enum TenantRole: string
|
||||
{
|
||||
case Owner = 'owner';
|
||||
case Manager = 'manager';
|
||||
case Operator = 'operator';
|
||||
case Readonly = 'readonly';
|
||||
|
||||
public function canSync(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::Owner,
|
||||
self::Manager,
|
||||
self::Operator => true,
|
||||
self::Readonly => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -26,6 +26,7 @@ public function definition(): array
|
||||
'app_status' => 'ok',
|
||||
'app_notes' => null,
|
||||
'status' => 'active',
|
||||
'environment' => 'other',
|
||||
'is_current' => false,
|
||||
'metadata' => [],
|
||||
];
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('tenants', function (Blueprint $table) {
|
||||
$table->string('environment')->default('other')->after('status');
|
||||
$table->index('environment');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('tenants', function (Blueprint $table) {
|
||||
$table->dropIndex(['environment']);
|
||||
$table->dropColumn('environment');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('tenant_user', function (Blueprint $table) {
|
||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('role')->default('owner');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'user_id']);
|
||||
});
|
||||
|
||||
$now = now();
|
||||
|
||||
$tenantIds = DB::table('tenants')
|
||||
->whereNull('deleted_at')
|
||||
->pluck('id');
|
||||
|
||||
$userIds = DB::table('users')->pluck('id');
|
||||
|
||||
if ($tenantIds->isEmpty() || $userIds->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
|
||||
foreach ($tenantIds as $tenantId) {
|
||||
foreach ($userIds as $userId) {
|
||||
$rows[] = [
|
||||
'tenant_id' => $tenantId,
|
||||
'user_id' => $userId,
|
||||
'role' => 'owner',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
if (count($rows) >= 500) {
|
||||
DB::table('tenant_user')->insertOrIgnore($rows);
|
||||
$rows = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($rows !== []) {
|
||||
DB::table('tenant_user')->insertOrIgnore($rows);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('tenant_user');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_tenant_preferences', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||
$table->boolean('is_favorite')->default(false);
|
||||
$table->timestamp('last_used_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'tenant_id']);
|
||||
$table->index(['user_id', 'last_used_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_tenant_preferences');
|
||||
}
|
||||
};
|
||||
@ -31,7 +31,11 @@
|
||||
<p>Admin consent wurde bestätigt.</p>
|
||||
@endif
|
||||
|
||||
<p><a href="{{ route('filament.admin.resources.tenants.view', $tenant) }}">Zurück zur Tenant-Detailseite</a></p>
|
||||
<p>
|
||||
<a href="{{ route('filament.admin.resources.tenants.view', ['tenant' => $tenant->external_id, 'record' => $tenant]) }}">
|
||||
Zurück zur Tenant-Detailseite
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -3,12 +3,11 @@ # Requirements Checklist (031)
|
||||
**Created**: 2026-01-04
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
- [ ] Tenant memberships/roles exist and are enforced.
|
||||
- [ ] Current Tenant context is per-user and always visible.
|
||||
- [ ] Portfolio shows only accessible tenants with environment + health/status.
|
||||
- [ ] “Open tenant” changes context and redirects into tenant-scoped area.
|
||||
- [ ] Tenant-scoped resources are filtered by context and deny unauthorized access.
|
||||
- [ ] Bulk “Sync selected” dispatches per-tenant jobs and is role-gated.
|
||||
- [ ] Restore flows show target tenant + environment and require tenant-aware confirmation.
|
||||
- [ ] Pest tests cover authorization + context switching + bulk actions.
|
||||
|
||||
- [x] Tenant memberships/roles exist and are enforced.
|
||||
- [x] Current Tenant context is per-user and always visible.
|
||||
- [x] Portfolio shows only accessible tenants with environment + health/status.
|
||||
- [x] “Open tenant” changes context and redirects into tenant-scoped area.
|
||||
- [x] Tenant-scoped resources are filtered by context and deny unauthorized access.
|
||||
- [x] Bulk “Sync selected” dispatches per-tenant jobs and is role-gated.
|
||||
- [x] Restore flows show target tenant + environment and require tenant-aware confirmation.
|
||||
- [x] Pest tests cover authorization + context switching + bulk actions.
|
||||
|
||||
@ -2,7 +2,7 @@ # Feature Specification: Tenant Portfolio & Context Switch (031)
|
||||
|
||||
**Feature Branch**: `feat/031-tenant-portfolio-context-switch`
|
||||
**Created**: 2026-01-04
|
||||
**Status**: Proposed
|
||||
**Status**: Implemented (ready to merge)
|
||||
**Risk**: Medium
|
||||
**Priority**: P1
|
||||
|
||||
|
||||
@ -8,27 +8,26 @@ ## Phase 1: Setup
|
||||
- [x] T001 Create spec/plan/tasks and checklist.
|
||||
|
||||
## Phase 2: Research & Design
|
||||
- [ ] T002 Review Filament tenancy support and choose the context mechanism (route vs session).
|
||||
- [ ] T003 Define tenant access roles and mapping (user memberships; future org/group principals).
|
||||
- [ ] T004 Decide how to store `environment` (column vs JSONB) and whether MSP “customer grouping” is in scope.
|
||||
- [ ] T005 Define context precedence rules (env override, route tenant, session/default tenant) and cross-tab safety expectations.
|
||||
- [x] T002 Review Filament tenancy support and choose the context mechanism (route vs session).
|
||||
- [x] T003 Define tenant access roles and mapping (user memberships; future org/group principals).
|
||||
- [x] T004 Decide how to store `environment` (column vs JSONB) and whether MSP “customer grouping” is in scope.
|
||||
- [x] T005 Define context precedence rules (env override, route tenant, session/default tenant) and cross-tab safety expectations.
|
||||
|
||||
## Phase 3: Tests (TDD)
|
||||
- [ ] T006 Authorization: user cannot open unauthorized tenant (403).
|
||||
- [ ] T007 Authorization: tenant-scoped resources deny cross-tenant access via URL (403/404).
|
||||
- [ ] T008 Context switching: “Open tenant” sets context and tenant-scoped pages filter correctly.
|
||||
- [ ] T009 Bulk sync: dispatches one job per selected tenant; readonly role cannot run it.
|
||||
- [x] T006 Authorization: user cannot access unauthorized tenant (404).
|
||||
- [x] T007 Authorization: tenant-scoped resources deny cross-tenant access via URL (404).
|
||||
- [x] T008 Context switching: “Open tenant” navigates into tenant-scoped pages (tenant in URL) and data filters correctly.
|
||||
- [x] T009 Bulk sync: dispatches one job per selected tenant; readonly role cannot run it.
|
||||
- [ ] T010 UI (optional browser tests): tenant switcher visible and environment badge shown.
|
||||
|
||||
## Phase 4: Implementation
|
||||
- [ ] T011 Add migrations for tenant memberships/roles and environment attribute (and optional preferences).
|
||||
- [ ] T012 Implement `TenantContext` + authorization gate/policy (`canAccessTenant`).
|
||||
- [ ] T013 Integrate tenant switcher into Filament topbar and make Current Tenant always visible.
|
||||
- [ ] T014 Scope tenant resources (Policies/Backups/RestoreRuns/etc.) via TenantContext; replace direct `Tenant::current()` usage.
|
||||
- [ ] T015 Update `TenantResource` into a portfolio view: access-scoped query, columns, filters, “Open”, “Sync”, bulk “Sync selected”.
|
||||
- [ ] T016 Add restore guardrails (target tenant header + tenant-aware confirmations).
|
||||
- [x] T011 Add migrations for tenant memberships/roles and environment attribute (and optional preferences).
|
||||
- [x] T012 Implement `TenantContext` + authorization gate/policy (`canAccessTenant`).
|
||||
- [x] T013 Integrate tenant switcher into Filament topbar and make Current Tenant always visible.
|
||||
- [x] T014 Scope tenant resources (Policies/Backups/RestoreRuns/etc.) via TenantContext; replace direct `Tenant::current()` usage.
|
||||
- [x] T015 Update `TenantResource` into a portfolio view: access-scoped query, columns, filters, “Open”, “Sync”, bulk “Sync selected”.
|
||||
- [x] T016 Add restore guardrails (target tenant header + tenant-aware confirmations).
|
||||
|
||||
## Phase 5: Verification
|
||||
- [ ] T017 Run targeted tests.
|
||||
- [ ] T018 Run Pint (`./vendor/bin/pint --dirty`).
|
||||
|
||||
- [x] T017 Run targeted tests.
|
||||
- [x] T018 Run Pint (`./vendor/bin/pint --dirty`).
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
use App\Models\RestoreRun;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -14,8 +15,12 @@
|
||||
|
||||
test('backup sets table bulk archive creates a run and archives selected sets', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$tenant->makeCurrent();
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$sets = collect(range(1, 3))->map(function (int $i) use ($tenant) {
|
||||
return BackupSet::create([
|
||||
@ -58,8 +63,12 @@
|
||||
|
||||
test('backup sets can be archived even when referenced by restore runs', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$tenant->makeCurrent();
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$set = BackupSet::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
@ -87,8 +96,12 @@
|
||||
|
||||
test('backup sets table bulk archive requires type-to-confirm for 10+ sets', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$tenant->makeCurrent();
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$sets = collect(range(1, 10))->map(function (int $i) use ($tenant) {
|
||||
return BackupSet::create([
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
use App\Models\RestoreRun;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -13,8 +14,12 @@
|
||||
|
||||
test('bulk delete restore runs skips running items', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$tenant->makeCurrent();
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$backupSet = BackupSet::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
use App\Models\RestoreRun;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -13,8 +14,12 @@
|
||||
|
||||
test('bulk delete restore runs soft deletes selected runs', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$tenant->makeCurrent();
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$backupSet = BackupSet::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
use App\Models\BulkOperationRun;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -13,8 +14,12 @@
|
||||
|
||||
test('backup sets table bulk force delete permanently deletes archived sets and their items', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$tenant->makeCurrent();
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$set = BackupSet::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -14,6 +15,11 @@
|
||||
test('policy versions table bulk force delete creates a run and skips non-archived records', function () {
|
||||
$tenant = Tenant::factory()->create(['is_current' => true]);
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$policy = Policy::factory()->create(['tenant_id' => $tenant->id]);
|
||||
$version = PolicyVersion::factory()->create([
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
use App\Models\RestoreRun;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -13,8 +14,12 @@
|
||||
|
||||
test('bulk force delete restore runs permanently deletes archived runs', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$tenant->makeCurrent();
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$backupSet = BackupSet::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -14,6 +15,11 @@
|
||||
test('bulk prune records skip reasons', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$policyA = Policy::factory()->create(['tenant_id' => $tenant->id]);
|
||||
$current = PolicyVersion::factory()->create([
|
||||
@ -37,8 +43,6 @@
|
||||
'captured_at' => now()->subDays(10),
|
||||
]);
|
||||
|
||||
$tenant->forceFill(['is_current' => true])->save();
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(PolicyVersionResource\Pages\ListPolicyVersions::class)
|
||||
->callTableBulkAction('bulk_prune_versions', collect([$current, $tooRecent]), data: [
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -13,6 +14,11 @@
|
||||
test('bulk prune archives eligible policy versions', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$policy = Policy::factory()->create(['tenant_id' => $tenant->id]);
|
||||
|
||||
@ -30,8 +36,6 @@
|
||||
'captured_at' => now()->subDays(120),
|
||||
]);
|
||||
|
||||
$tenant->forceFill(['is_current' => true])->save();
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(PolicyVersionResource\Pages\ListPolicyVersions::class)
|
||||
->callTableBulkAction('bulk_prune_versions', collect([$eligible, $current]), data: [
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
use App\Models\BulkOperationRun;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -13,8 +14,12 @@
|
||||
|
||||
test('backup sets table bulk restore restores archived sets and their items', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$tenant->makeCurrent();
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$set = BackupSet::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -14,6 +15,11 @@
|
||||
test('policy versions table bulk restore creates a run and restores archived records', function () {
|
||||
$tenant = Tenant::factory()->create(['is_current' => true]);
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$policy = Policy::factory()->create(['tenant_id' => $tenant->id]);
|
||||
$version = PolicyVersion::factory()->create([
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
use App\Models\RestoreRun;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -13,8 +14,12 @@
|
||||
|
||||
test('restore runs table bulk restore creates a run and restores archived records', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$tenant->makeCurrent();
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$backupSet = BackupSet::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
use App\Models\Policy;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -11,8 +12,12 @@
|
||||
|
||||
test('bulk delete requires confirmation string for large batches', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$tenant->makeCurrent();
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
$policies = Policy::factory()->count(20)->create(['tenant_id' => $tenant->id]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
@ -27,8 +32,12 @@
|
||||
|
||||
test('bulk delete fails with incorrect confirmation string', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$tenant->makeCurrent();
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
$policies = Policy::factory()->count(20)->create(['tenant_id' => $tenant->id]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
@ -43,8 +52,12 @@
|
||||
|
||||
test('bulk delete does not require confirmation string for small batches', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$tenant->makeCurrent();
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
$policies = Policy::factory()->count(10)->create(['tenant_id' => $tenant->id]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
|
||||
@ -47,9 +47,12 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Data Protection');
|
||||
|
||||
@ -22,6 +22,9 @@
|
||||
|
||||
$this->tenant = $tenant;
|
||||
$this->user = User::factory()->create();
|
||||
$this->user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('policy detail renders normalized settings for Autopilot profiles', function () {
|
||||
@ -54,7 +57,7 @@
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $this->tenant));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Settings');
|
||||
@ -95,7 +98,7 @@
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $this->tenant));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Settings');
|
||||
@ -139,7 +142,7 @@
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $this->tenant));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Settings');
|
||||
|
||||
@ -133,10 +133,13 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->get(route('filament.admin.resources.policies.view', ['record' => $policy]));
|
||||
->get(route('filament.admin.resources.policies.view', array_merge(filamentTenantRouteParams($tenant), ['record' => $policy])));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Block legacy auth');
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
use App\Models\RestoreRun;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -43,6 +44,10 @@
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(ListBackupSets::class)
|
||||
->callTableAction('archive', $backupSet);
|
||||
@ -78,6 +83,10 @@
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(ListBackupSets::class)
|
||||
->callTableAction('archive', $backupSet);
|
||||
@ -117,6 +126,10 @@
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(ListBackupSets::class)
|
||||
->callTableAction('archive', $backupSet)
|
||||
@ -158,6 +171,10 @@
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(ListBackupSets::class)
|
||||
->callTableAction('archive', $backupSet)
|
||||
@ -197,6 +214,10 @@
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(ListRestoreRuns::class)
|
||||
->callTableAction('archive', $restoreRun)
|
||||
@ -235,6 +256,10 @@
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(ListRestoreRuns::class)
|
||||
->callTableAction('archive', $restoreRun)
|
||||
@ -269,6 +294,10 @@
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(ListPolicies::class)
|
||||
->callTableAction('ignore', $policy);
|
||||
@ -309,6 +338,10 @@
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(ListPolicyVersions::class)
|
||||
->callTableAction('archive', $version);
|
||||
@ -346,6 +379,10 @@
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(ListPolicyVersions::class)
|
||||
->callTableAction('archive', $version)
|
||||
@ -368,6 +405,10 @@
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(ListTenants::class)
|
||||
->callTableAction('archive', $tenant);
|
||||
@ -409,6 +450,11 @@
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$active->getKey() => ['role' => 'owner'],
|
||||
$archived->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($active, true);
|
||||
|
||||
$component = Livewire::test(ListTenants::class)
|
||||
->assertSee($active->name)
|
||||
@ -433,8 +479,18 @@
|
||||
|
||||
$tenant->delete();
|
||||
|
||||
$contextTenant = Tenant::create([
|
||||
'tenant_id' => 'tenant-restore-context',
|
||||
'name' => 'Restore Context Tenant',
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
$contextTenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($contextTenant, true);
|
||||
|
||||
Livewire::test(ListTenants::class)
|
||||
->set('tableFilters.trashed.value', 1)
|
||||
|
||||
@ -41,14 +41,17 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$policyResponse = $this->actingAs($user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||
|
||||
$policyResponse->assertSee('This snapshot may be incomplete or malformed');
|
||||
|
||||
$versionResponse = $this->actingAs($user)
|
||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version]));
|
||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant));
|
||||
|
||||
$versionResponse->assertSee('This snapshot may be incomplete or malformed');
|
||||
});
|
||||
|
||||
@ -100,9 +100,12 @@ public function getServicePrincipalPermissions(array $options = []): GraphRespon
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$detailResponse = $this->actingAs($user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||
|
||||
$detailResponse->assertSee('@odata.type mismatch');
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
use App\Services\Graph\AssignmentFetcher;
|
||||
use App\Services\Graph\ScopeTagResolver;
|
||||
use App\Services\Intune\PolicySnapshotService;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Mockery\MockInterface;
|
||||
@ -22,6 +23,10 @@
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$this->mock(PolicySnapshotService::class, function (MockInterface $mock) use ($policy) {
|
||||
$mock->shouldReceive('fetch')
|
||||
|
||||
@ -7,13 +7,7 @@
|
||||
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
|
||||
|
||||
test('policies are listed for the active tenant', function () {
|
||||
$tenant = Tenant::create([
|
||||
'tenant_id' => 'local-tenant',
|
||||
'name' => 'Tenant One',
|
||||
'metadata' => [],
|
||||
]);
|
||||
|
||||
$tenant->makeCurrent();
|
||||
$tenant = Tenant::factory()->create();
|
||||
|
||||
Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
@ -24,11 +18,7 @@
|
||||
'last_synced_at' => now(),
|
||||
]);
|
||||
|
||||
$otherTenant = Tenant::create([
|
||||
'tenant_id' => 'tenant-2',
|
||||
'name' => 'Tenant Two',
|
||||
'metadata' => [],
|
||||
]);
|
||||
$otherTenant = Tenant::factory()->create();
|
||||
|
||||
Policy::create([
|
||||
'tenant_id' => $otherTenant->id,
|
||||
@ -40,9 +30,13 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
$otherTenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('filament.admin.resources.policies.index'))
|
||||
->get(route('filament.admin.resources.policies.index', filamentTenantRouteParams($tenant)))
|
||||
->assertOk()
|
||||
->assertSee('Policy A')
|
||||
->assertDontSee('Policy B');
|
||||
|
||||
@ -49,9 +49,12 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Settings');
|
||||
|
||||
@ -48,9 +48,12 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]).'?tab=settings');
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant).'?tab=settings');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Settings');
|
||||
|
||||
@ -58,9 +58,12 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version]));
|
||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Normalized settings');
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Services\Graph\GroupResolver;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Mockery\MockInterface;
|
||||
@ -49,11 +50,15 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create(['email' => 'tester@example.com']);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(ListPolicyVersions::class)
|
||||
->callTableAction('restore_via_wizard', $version)
|
||||
->assertRedirectContains(RestoreRunResource::getUrl('create', [], false));
|
||||
->assertRedirectContains(RestoreRunResource::getUrl('create', [], false, tenant: $tenant));
|
||||
|
||||
$backupSet = BackupSet::query()->where('metadata->source', 'policy_version')->first();
|
||||
expect($backupSet)->not->toBeNull();
|
||||
@ -141,7 +146,11 @@
|
||||
});
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$component = Livewire::withQueryParams([
|
||||
'backup_set_id' => $backupSet->id,
|
||||
|
||||
@ -47,9 +47,12 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version]));
|
||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Scope Tags');
|
||||
|
||||
@ -45,9 +45,12 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version]));
|
||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Raw JSON');
|
||||
@ -132,9 +135,12 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version]).'?tab=normalized-settings');
|
||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant).'?tab=normalized-settings');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Enrollment notifications');
|
||||
|
||||
@ -31,9 +31,12 @@
|
||||
$service->captureVersion($policy, ['value' => 2], 'tester');
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('filament.admin.resources.policy-versions.index'))
|
||||
->get(route('filament.admin.resources.policy-versions.index', filamentTenantRouteParams($tenant)))
|
||||
->assertOk()
|
||||
->assertSee('Policy A')
|
||||
->assertSee((string) PolicyVersion::max('version_number'));
|
||||
|
||||
@ -71,9 +71,12 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Settings'); // Settings tab should appear for Settings Catalog
|
||||
@ -130,9 +133,12 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||
|
||||
$response->assertOk();
|
||||
// TODO: Manual verification - check UI for display name "Allow Real-time Monitoring"
|
||||
@ -181,9 +187,12 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||
|
||||
$response->assertOk();
|
||||
// TODO: Manual verification - check UI shows prettified fallback label
|
||||
@ -225,9 +234,12 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('General');
|
||||
@ -281,8 +293,11 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
$response = $this->actingAs($user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||
|
||||
$response->assertOk();
|
||||
// Policy view should render successfully with Settings Catalog data
|
||||
@ -356,8 +371,11 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
$response = $this->actingAs($user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||
|
||||
$response->assertOk();
|
||||
// Value formatting verified by manual UI inspection
|
||||
@ -419,8 +437,11 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
$response = $this->actingAs($user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||
|
||||
$response->assertOk();
|
||||
// Search functionality is Alpine.js client-side, requires browser testing
|
||||
@ -465,8 +486,11 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
$response = $this->actingAs($user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||
|
||||
$response->assertOk();
|
||||
// Page renders without crash - actual fallback display requires UI verification
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
use App\Models\Policy;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -99,6 +100,10 @@
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(CreateRestoreRun::class)
|
||||
->fillForm([
|
||||
|
||||
@ -12,9 +12,12 @@
|
||||
$originalEnv = getenv('INTUNE_TENANT_ID');
|
||||
putenv('INTUNE_TENANT_ID=');
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$tenant = Tenant::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
||||
$tenant->makeCurrent();
|
||||
|
||||
@ -51,10 +54,10 @@
|
||||
],
|
||||
]);
|
||||
|
||||
$this->get(\App\Filament\Resources\PolicyVersionResource::getUrl('index'))
|
||||
$this->get(\App\Filament\Resources\PolicyVersionResource::getUrl('index', tenant: $tenant))
|
||||
->assertSuccessful();
|
||||
|
||||
$this->get(\App\Filament\Resources\PolicyVersionResource::getUrl('view', ['record' => $version]).'?tab=normalized-settings')
|
||||
$this->get(\App\Filament\Resources\PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant).'?tab=normalized-settings')
|
||||
->assertSuccessful();
|
||||
|
||||
$originalEnv !== false
|
||||
@ -71,14 +74,17 @@
|
||||
$originalEnv = getenv('INTUNE_TENANT_ID');
|
||||
putenv('INTUNE_TENANT_ID=');
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
config([
|
||||
'tenantpilot.display.show_script_content' => true,
|
||||
'tenantpilot.display.max_script_content_chars' => 5000,
|
||||
]);
|
||||
|
||||
$tenant = Tenant::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
||||
$tenant->makeCurrent();
|
||||
|
||||
@ -117,7 +123,7 @@
|
||||
],
|
||||
]);
|
||||
|
||||
$url = \App\Filament\Resources\PolicyVersionResource::getUrl('view', ['record' => $v2]);
|
||||
$url = \App\Filament\Resources\PolicyVersionResource::getUrl('view', ['record' => $v2], tenant: $tenant);
|
||||
|
||||
$this->get($url.'?tab=diff')
|
||||
->assertSuccessful()
|
||||
@ -136,14 +142,17 @@
|
||||
$originalEnv = getenv('INTUNE_TENANT_ID');
|
||||
putenv('INTUNE_TENANT_ID=');
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
config([
|
||||
'tenantpilot.display.show_script_content' => true,
|
||||
'tenantpilot.display.max_script_content_chars' => 5000,
|
||||
]);
|
||||
|
||||
$tenant = Tenant::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
||||
$tenant->makeCurrent();
|
||||
|
||||
@ -182,7 +191,7 @@
|
||||
],
|
||||
]);
|
||||
|
||||
$url = \App\Filament\Resources\PolicyVersionResource::getUrl('view', ['record' => $v2]);
|
||||
$url = \App\Filament\Resources\PolicyVersionResource::getUrl('view', ['record' => $v2], tenant: $tenant);
|
||||
|
||||
$this->get($url.'?tab=diff')
|
||||
->assertSuccessful()
|
||||
|
||||
@ -105,10 +105,13 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->get(route('filament.admin.resources.policies.view', ['record' => $policy]));
|
||||
->get(route('filament.admin.resources.policies.view', array_merge(filamentTenantRouteParams($tenant), ['record' => $policy])));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Setting A');
|
||||
@ -145,10 +148,13 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
$versions->captureFromGraph($tenant, $policy, createdBy: 'tester@example.com');
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->get(route('filament.admin.resources.policies.view', ['record' => $policy]));
|
||||
->get(route('filament.admin.resources.policies.view', array_merge(filamentTenantRouteParams($tenant), ['record' => $policy])));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Setting A');
|
||||
|
||||
@ -90,9 +90,12 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$policyResponse = $this->actingAs($user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||
|
||||
$policyResponse->assertOk();
|
||||
$policyResponse->assertSee('Definition');
|
||||
@ -104,7 +107,7 @@
|
||||
$policyResponse->assertSee('tp-policy-general-card');
|
||||
|
||||
$versionResponse = $this->actingAs($user)
|
||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version]));
|
||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant));
|
||||
|
||||
$versionResponse->assertOk();
|
||||
$versionResponse->assertSee('Normalized settings');
|
||||
|
||||
@ -111,10 +111,13 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->get(route('filament.admin.resources.policies.index'));
|
||||
->get(route('filament.admin.resources.policies.index', filamentTenantRouteParams($tenant)));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Settings Catalog Policy');
|
||||
|
||||
@ -147,6 +147,9 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
|
||||
$service = app(RestoreService::class);
|
||||
@ -185,7 +188,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
|
||||
$run->update(['results' => $results]);
|
||||
|
||||
$response = $this->get(route('filament.admin.resources.restore-runs.view', ['record' => $run]));
|
||||
$response = $this->get(route('filament.admin.resources.restore-runs.view', array_merge(filamentTenantRouteParams($tenant), ['record' => $run])));
|
||||
$response->assertOk();
|
||||
$response->assertSee('Graph bulk apply failed');
|
||||
$response->assertSee('Setting missing');
|
||||
|
||||
@ -162,6 +162,9 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
|
||||
$service = app(RestoreService::class);
|
||||
@ -201,7 +204,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
->toBe('#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance');
|
||||
|
||||
$response = $this
|
||||
->get(route('filament.admin.resources.restore-runs.view', ['record' => $run]));
|
||||
->get(route('filament.admin.resources.restore-runs.view', array_merge(filamentTenantRouteParams($tenant), ['record' => $run])));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('settings are read-only');
|
||||
|
||||
@ -56,9 +56,12 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$policyResponse = $this->actingAs($user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]).'?tab=settings');
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant).'?tab=settings');
|
||||
|
||||
$policyResponse->assertOk();
|
||||
$policyResponse->assertSee('fi-width-full');
|
||||
@ -69,7 +72,7 @@
|
||||
$policyResponse->assertSee('fi-ta-table');
|
||||
|
||||
$versionResponse = $this->actingAs($user)
|
||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version]));
|
||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant));
|
||||
|
||||
$versionResponse->assertOk();
|
||||
$versionResponse->assertSee('fi-width-full');
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
use App\Filament\Resources\TenantResource\Pages\ListTenants;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -26,6 +27,11 @@
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$first->getKey() => ['role' => 'owner'],
|
||||
$second->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($first, true);
|
||||
|
||||
Livewire::test(ListTenants::class)
|
||||
->callTableAction('makeCurrent', $second);
|
||||
|
||||
99
tests/Feature/Filament/TenantPortfolioContextSwitchTest.php
Normal file
99
tests/Feature/Filament/TenantPortfolioContextSwitchTest.php
Normal file
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
use App\Filament\Resources\TenantResource\Pages\ListTenants;
|
||||
use App\Jobs\SyncPoliciesJob;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Events\TenantSet;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
test('tenant-scoped pages return 404 for unauthorized tenant', function () {
|
||||
[$user, $authorizedTenant] = createUserWithTenant();
|
||||
$unauthorizedTenant = Tenant::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('filament.admin.resources.policies.index', filamentTenantRouteParams($unauthorizedTenant)))
|
||||
->assertNotFound();
|
||||
});
|
||||
|
||||
test('tenant portfolio lists only tenants the user can access', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$authorizedTenant = Tenant::factory()->create([
|
||||
'tenant_id' => 'tenant-portfolio-authorized',
|
||||
'name' => 'Authorized Tenant',
|
||||
]);
|
||||
|
||||
$unauthorizedTenant = Tenant::factory()->create([
|
||||
'tenant_id' => 'tenant-portfolio-unauthorized',
|
||||
'name' => 'Unauthorized Tenant',
|
||||
]);
|
||||
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$authorizedTenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$this->get(route('filament.admin.resources.tenants.index', filamentTenantRouteParams($authorizedTenant)))
|
||||
->assertOk()
|
||||
->assertSee($authorizedTenant->name)
|
||||
->assertDontSee($unauthorizedTenant->name);
|
||||
});
|
||||
|
||||
test('tenant portfolio bulk sync dispatches one job per eligible tenant', function () {
|
||||
Bus::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$tenantA = Tenant::factory()->create(['tenant_id' => 'tenant-bulk-a']);
|
||||
$tenantB = Tenant::factory()->create(['tenant_id' => 'tenant-bulk-b']);
|
||||
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenantA->getKey() => ['role' => 'owner'],
|
||||
$tenantB->getKey() => ['role' => 'operator'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenantA, true);
|
||||
|
||||
Livewire::test(ListTenants::class)
|
||||
->assertTableBulkActionVisible('syncSelected')
|
||||
->callTableBulkAction('syncSelected', collect([$tenantA, $tenantB]));
|
||||
|
||||
Bus::assertDispatchedTimes(SyncPoliciesJob::class, 2);
|
||||
|
||||
Bus::assertDispatched(SyncPoliciesJob::class, fn (SyncPoliciesJob $job) => $job->tenantId === $tenantA->id);
|
||||
Bus::assertDispatched(SyncPoliciesJob::class, fn (SyncPoliciesJob $job) => $job->tenantId === $tenantB->id);
|
||||
});
|
||||
|
||||
test('tenant portfolio bulk sync is hidden for readonly users', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$tenant = Tenant::factory()->create(['tenant_id' => 'tenant-bulk-readonly']);
|
||||
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'readonly'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(ListTenants::class)
|
||||
->assertTableBulkActionHidden('syncSelected');
|
||||
});
|
||||
|
||||
test('tenant set event updates user tenant preference last used timestamp', function () {
|
||||
[$user, $tenant] = createUserWithTenant();
|
||||
|
||||
TenantSet::dispatch($tenant, $user);
|
||||
|
||||
$this->assertDatabaseHas('user_tenant_preferences', [
|
||||
'user_id' => $user->id,
|
||||
'tenant_id' => $tenant->id,
|
||||
]);
|
||||
});
|
||||
@ -6,6 +6,7 @@
|
||||
use App\Models\User;
|
||||
use App\Services\Graph\GraphClientInterface;
|
||||
use App\Services\Graph\GraphResponse;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Livewire;
|
||||
@ -32,6 +33,10 @@ function tenantWithApp(): Tenant
|
||||
$tenant = tenantWithApp();
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
||||
->mountAction('setup_rbac')
|
||||
@ -51,6 +56,10 @@ function tenantWithApp(): Tenant
|
||||
$tenant = tenantWithApp();
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null);
|
||||
Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5));
|
||||
@ -155,6 +164,10 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
$tenant = tenantWithApp();
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null);
|
||||
Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5));
|
||||
@ -265,6 +278,10 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
$tenant = tenantWithApp();
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null);
|
||||
Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5));
|
||||
@ -365,6 +382,10 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
$tenant = tenantWithApp();
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
||||
->mountAction('setup_rbac')
|
||||
@ -380,6 +401,10 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
$tenant = tenantWithApp();
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
||||
->mountAction('setup_rbac')
|
||||
@ -394,6 +419,10 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
$tenant = tenantWithApp();
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null);
|
||||
Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5));
|
||||
@ -505,6 +534,10 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
$tenant = tenantWithApp();
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null);
|
||||
Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5));
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
use App\Models\User;
|
||||
use App\Services\Graph\GraphClientInterface;
|
||||
use App\Services\Graph\GraphResponse;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -54,9 +55,19 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$contextTenant = Tenant::create([
|
||||
'tenant_id' => 'tenant-context',
|
||||
'name' => 'Context Tenant',
|
||||
]);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$contextTenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
Filament::setTenant($contextTenant, true);
|
||||
|
||||
Livewire::test(CreateTenant::class)
|
||||
->fillForm([
|
||||
'name' => 'Contoso',
|
||||
'environment' => 'other',
|
||||
'tenant_id' => 'tenant-guid',
|
||||
'domain' => 'contoso.com',
|
||||
'app_client_id' => 'client-123',
|
||||
@ -65,7 +76,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
->call('create')
|
||||
->assertHasNoFormErrors();
|
||||
|
||||
$tenant = Tenant::first();
|
||||
$tenant = Tenant::query()->where('tenant_id', 'tenant-guid')->first();
|
||||
expect($tenant)->not->toBeNull();
|
||||
|
||||
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
||||
@ -129,6 +140,11 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
'tenant_id' => 'tenant-error',
|
||||
'name' => 'Error Tenant',
|
||||
]);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
||||
->callAction('verify');
|
||||
@ -157,6 +173,9 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
'tenant_id' => 'tenant-ui',
|
||||
'name' => 'UI Tenant',
|
||||
]);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
config(['intune_permissions.granted_stub' => []]);
|
||||
|
||||
@ -169,7 +188,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
'status' => 'ok',
|
||||
]);
|
||||
|
||||
$response = $this->get(route('filament.admin.resources.tenants.view', $tenant));
|
||||
$response = $this->get(route('filament.admin.resources.tenants.view', array_merge(filamentTenantRouteParams($tenant), ['record' => $tenant])));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Actions');
|
||||
@ -182,13 +201,17 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
Tenant::create([
|
||||
$tenant = Tenant::create([
|
||||
'tenant_id' => 'tenant-ui-list',
|
||||
'name' => 'UI Tenant List',
|
||||
'app_client_id' => 'client-123',
|
||||
]);
|
||||
|
||||
$response = $this->get(route('filament.admin.resources.tenants.index'));
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$response = $this->get(route('filament.admin.resources.tenants.index', filamentTenantRouteParams($tenant)));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Open in Entra');
|
||||
@ -202,6 +225,11 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
'tenant_id' => 'tenant-ui-deactivate',
|
||||
'name' => 'UI Tenant Deactivate',
|
||||
]);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
||||
->callAction('archive');
|
||||
|
||||
@ -48,9 +48,12 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
|
||||
@ -10,11 +10,13 @@
|
||||
unset($_ENV['INTUNE_TENANT_ID'], $_SERVER['INTUNE_TENANT_ID']);
|
||||
|
||||
$this->tenant = Tenant::factory()->create();
|
||||
$this->tenant->makeCurrent();
|
||||
$this->policy = Policy::factory()->create([
|
||||
'tenant_id' => $this->tenant->id,
|
||||
]);
|
||||
$this->user = User::factory()->create();
|
||||
$this->user->tenants()->syncWithoutDetaching([
|
||||
$this->tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('displays policy version page', function () {
|
||||
@ -26,7 +28,10 @@
|
||||
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$response = $this->get("/admin/policy-versions/{$version->id}");
|
||||
$response = $this->get(route('filament.admin.resources.policy-versions.view', array_merge(
|
||||
filamentTenantRouteParams($this->tenant),
|
||||
['record' => $version],
|
||||
)));
|
||||
|
||||
$response->assertOk();
|
||||
});
|
||||
@ -67,7 +72,10 @@
|
||||
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$response = $this->get("/admin/policy-versions/{$version->id}");
|
||||
$response = $this->get(route('filament.admin.resources.policy-versions.view', array_merge(
|
||||
filamentTenantRouteParams($this->tenant),
|
||||
['record' => $version],
|
||||
)));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSeeLivewire('policy-version-assignments-widget');
|
||||
@ -87,7 +95,10 @@
|
||||
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$response = $this->get("/admin/policy-versions/{$version->id}");
|
||||
$response = $this->get(route('filament.admin.resources.policy-versions.view', array_merge(
|
||||
filamentTenantRouteParams($this->tenant),
|
||||
['record' => $version],
|
||||
)));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Assignments were not captured for this version');
|
||||
@ -107,7 +118,10 @@
|
||||
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$response = $this->get("/admin/policy-versions/{$version->id}");
|
||||
$response = $this->get(route('filament.admin.resources.policy-versions.view', array_merge(
|
||||
filamentTenantRouteParams($this->tenant),
|
||||
['record' => $version],
|
||||
)));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('No assignments found for this version');
|
||||
@ -137,7 +151,10 @@
|
||||
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$response = $this->get("/admin/policy-versions/{$version->id}");
|
||||
$response = $this->get(route('filament.admin.resources.policy-versions.view', array_merge(
|
||||
filamentTenantRouteParams($this->tenant),
|
||||
['record' => $version],
|
||||
)));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Compliance notifications');
|
||||
@ -169,7 +186,10 @@
|
||||
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$response = $this->get("/admin/policy-versions/{$version->id}");
|
||||
$response = $this->get(route('filament.admin.resources.policy-versions.view', array_merge(
|
||||
filamentTenantRouteParams($this->tenant),
|
||||
['record' => $version],
|
||||
)));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Compliance notifications');
|
||||
@ -192,7 +212,10 @@
|
||||
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$response = $this->get("/admin/policy-versions/{$version->id}?tab=normalized-settings");
|
||||
$response = $this->get(route('filament.admin.resources.policy-versions.view', array_merge(
|
||||
filamentTenantRouteParams($this->tenant),
|
||||
['record' => $version],
|
||||
)).'?tab=normalized-settings');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Password & Access');
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Services\Graph\GroupResolver;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Mockery\MockInterface;
|
||||
@ -76,6 +77,11 @@
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$component = Livewire::test(CreateRestoreRun::class)
|
||||
->fillForm([
|
||||
@ -157,6 +163,11 @@
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(CreateRestoreRun::class)
|
||||
->fillForm([
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
use App\Models\RestoreRun;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -86,6 +87,11 @@
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$component = Livewire::test(CreateRestoreRun::class)
|
||||
->fillForm([
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Services\Graph\GroupResolver;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Mockery\MockInterface;
|
||||
@ -77,6 +78,11 @@
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$component = Livewire::test(CreateRestoreRun::class)
|
||||
->fillForm([
|
||||
@ -188,6 +194,11 @@
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$component = Livewire::test(CreateRestoreRun::class)
|
||||
->fillForm([
|
||||
@ -270,6 +281,11 @@
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$component = Livewire::test(CreateRestoreRun::class)
|
||||
->fillForm([
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
use App\Models\RestoreRun;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -28,6 +29,11 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(ListRestoreRuns::class)
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
use App\Models\RestoreRun;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -13,7 +14,6 @@
|
||||
|
||||
test('rerun action creates a new restore run with the same selections', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$tenant->makeCurrent();
|
||||
|
||||
$backupSet = BackupSet::factory()->for($tenant)->create([
|
||||
'status' => 'completed',
|
||||
@ -47,6 +47,11 @@
|
||||
]);
|
||||
|
||||
$user = User::factory()->create(['email' => 'tester@example.com']);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(ListRestoreRuns::class)
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Support\RestoreRunStatus;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Livewire\Livewire;
|
||||
@ -62,6 +63,11 @@
|
||||
'name' => 'Tester',
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(CreateRestoreRun::class)
|
||||
->fillForm([
|
||||
@ -130,6 +136,11 @@
|
||||
'name' => 'Executor',
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(CreateRestoreRun::class)
|
||||
->fillForm([
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
use App\Models\RestoreRun;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -50,6 +51,11 @@
|
||||
'name' => 'Tester',
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::test(CreateRestoreRun::class)
|
||||
->fillForm([
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
/*
|
||||
@ -60,3 +62,26 @@ function something()
|
||||
{
|
||||
// ..
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: User, 1: Tenant}
|
||||
*/
|
||||
function createUserWithTenant(?Tenant $tenant = null, ?User $user = null, string $role = 'owner'): array
|
||||
{
|
||||
$user ??= User::factory()->create();
|
||||
$tenant ??= Tenant::factory()->create();
|
||||
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => $role],
|
||||
]);
|
||||
|
||||
return [$user, $tenant];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{tenant: string}
|
||||
*/
|
||||
function filamentTenantRouteParams(Tenant $tenant): array
|
||||
{
|
||||
return ['tenant' => (string) $tenant->external_id];
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
use App\Models\Policy;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
@ -12,8 +13,12 @@
|
||||
|
||||
test('policies bulk actions are available for authenticated users', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$tenant->makeCurrent();
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
$policies = Policy::factory()->count(2)->create(['tenant_id' => $tenant->id]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user