fix(spec-085-086): stabilize ops UX + provider connection fixtures
This commit is contained in:
parent
c8e5996a1a
commit
bd19864a42
@ -347,7 +347,7 @@ public function content(Schema $schema): Schema
|
||||
SchemaActions::make([
|
||||
Action::make('wizardStartVerification')
|
||||
->label('Start verification')
|
||||
->visible(fn (): bool => $this->managedTenant instanceof Tenant)
|
||||
->visible(fn (): bool => $this->managedTenant instanceof Tenant && ! $this->verificationRunIsActive())
|
||||
->disabled(fn (): bool => ! $this->currentUserCan(Capabilities::WORKSPACE_MANAGED_TENANT_ONBOARD_VERIFICATION_START))
|
||||
->tooltip(fn (): ?string => $this->currentUserCan(Capabilities::WORKSPACE_MANAGED_TENANT_ONBOARD_VERIFICATION_START)
|
||||
? null
|
||||
@ -662,7 +662,7 @@ private function verificationStatus(): string
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($reasonCode, ['provider_auth_failed', 'permission_denied', 'provider_consent_missing'], true)) {
|
||||
if (in_array($reasonCode, ['provider_auth_failed', 'provider_permission_denied', 'permission_denied', 'provider_consent_missing'], true)) {
|
||||
return 'blocked';
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +26,6 @@
|
||||
use App\Support\OpsUx\OpsUxBrowserEvents;
|
||||
use App\Support\Rbac\UiEnforcement;
|
||||
use BackedEnum;
|
||||
use Carbon\CarbonImmutable;
|
||||
use DateTimeZone;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
@ -50,7 +49,6 @@
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\UniqueConstraintViolationException;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
@ -58,6 +56,10 @@
|
||||
|
||||
class BackupScheduleResource extends Resource
|
||||
{
|
||||
protected static ?string $model = BackupSchedule::class;
|
||||
|
||||
protected static ?string $tenantOwnershipRelationshipName = 'tenant';
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-clock';
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Backups & Restore';
|
||||
|
||||
@ -8,6 +8,10 @@
|
||||
use App\Support\Badges\BadgeRenderer;
|
||||
use App\Support\OperationCatalog;
|
||||
use App\Support\OperationRunLinks;
|
||||
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
||||
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
|
||||
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
|
||||
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
@ -20,6 +24,16 @@ class BackupScheduleOperationRunsRelationManager extends RelationManager
|
||||
|
||||
protected static ?string $title = 'Executions';
|
||||
|
||||
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
||||
{
|
||||
return ActionSurfaceDeclaration::forRelationManager(ActionSurfaceProfile::RelationManager)
|
||||
->exempt(ActionSurfaceSlot::ListHeader, 'Executions relation list has no header actions.')
|
||||
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ViewAction->value)
|
||||
->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'Single primary row action is exposed, so no row menu is needed.')
|
||||
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Bulk actions are intentionally omitted for operation run safety.')
|
||||
->exempt(ActionSurfaceSlot::ListEmptyState, 'No empty-state actions are exposed in this embedded relation manager.');
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
@ -32,7 +46,7 @@ public function table(Table $table): Table
|
||||
|
||||
Tables\Columns\TextColumn::make('type')
|
||||
->label('Type')
|
||||
->formatStateUsing(fn (string $state): string => OperationCatalog::label($state)),
|
||||
->formatStateUsing([OperationCatalog::class, 'label']),
|
||||
|
||||
Tables\Columns\TextColumn::make('status')
|
||||
->badge()
|
||||
|
||||
@ -43,6 +43,8 @@ class BackupSetResource extends Resource
|
||||
{
|
||||
protected static ?string $model = BackupSet::class;
|
||||
|
||||
protected static ?string $tenantOwnershipRelationshipName = 'tenant';
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-archive-box';
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Backups & Restore';
|
||||
|
||||
@ -38,6 +38,8 @@ class FindingResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Finding::class;
|
||||
|
||||
protected static ?string $tenantOwnershipRelationshipName = 'tenant';
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-exclamation-triangle';
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Drift';
|
||||
|
||||
@ -37,6 +37,8 @@ class InventoryItemResource extends Resource
|
||||
{
|
||||
protected static ?string $model = InventoryItem::class;
|
||||
|
||||
protected static ?string $tenantOwnershipRelationshipName = 'tenant';
|
||||
|
||||
protected static ?string $cluster = InventoryCluster::class;
|
||||
|
||||
protected static ?int $navigationSort = 1;
|
||||
|
||||
@ -32,6 +32,8 @@ class InventorySyncRunResource extends Resource
|
||||
{
|
||||
protected static ?string $model = InventorySyncRun::class;
|
||||
|
||||
protected static ?string $tenantOwnershipRelationshipName = 'tenant';
|
||||
|
||||
protected static bool $shouldRegisterNavigation = true;
|
||||
|
||||
protected static ?string $cluster = InventoryCluster::class;
|
||||
|
||||
@ -52,6 +52,8 @@ class PolicyResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Policy::class;
|
||||
|
||||
protected static ?string $tenantOwnershipRelationshipName = 'tenant';
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-shield-check';
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Inventory';
|
||||
|
||||
@ -51,6 +51,8 @@ class PolicyVersionResource extends Resource
|
||||
{
|
||||
protected static ?string $model = PolicyVersion::class;
|
||||
|
||||
protected static ?string $tenantOwnershipRelationshipName = 'tenant';
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-list-bullet';
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Inventory';
|
||||
|
||||
@ -18,8 +18,8 @@
|
||||
use App\Support\Badges\BadgeDomain;
|
||||
use App\Support\Badges\BadgeRenderer;
|
||||
use App\Support\OperationRunLinks;
|
||||
use App\Support\Rbac\UiEnforcement;
|
||||
use App\Support\Providers\ProviderReasonCodes;
|
||||
use App\Support\Rbac\UiEnforcement;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use BackedEnum;
|
||||
use Filament\Actions;
|
||||
@ -93,7 +93,9 @@ protected static function resolveScopedTenant(): ?Tenant
|
||||
->first();
|
||||
}
|
||||
|
||||
return Tenant::current();
|
||||
$filamentTenant = \Filament\Facades\Filament::getTenant();
|
||||
|
||||
return $filamentTenant instanceof Tenant ? $filamentTenant : null;
|
||||
}
|
||||
|
||||
private static function resolveTenantExternalIdFromLivewireRequest(): ?string
|
||||
@ -772,9 +774,13 @@ public static function getEloquentQuery(): Builder
|
||||
return $query->whereRaw('1 = 0');
|
||||
}
|
||||
|
||||
if ($tenantId === null) {
|
||||
return $query->whereRaw('1 = 0');
|
||||
}
|
||||
|
||||
return $query
|
||||
->where('workspace_id', (int) $workspaceId)
|
||||
->when($tenantId, fn (Builder $query) => $query->where('tenant_id', $tenantId))
|
||||
->where('tenant_id', $tenantId)
|
||||
->latest('id');
|
||||
}
|
||||
|
||||
@ -792,6 +798,10 @@ public static function getPages(): array
|
||||
*/
|
||||
public static function getUrl(?string $name = null, array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null, bool $shouldGuessMissingParameters = false): string
|
||||
{
|
||||
if (array_key_exists('tenant', $parameters) && blank($parameters['tenant'])) {
|
||||
unset($parameters['tenant']);
|
||||
}
|
||||
|
||||
if (! array_key_exists('tenant', $parameters)) {
|
||||
if ($tenant instanceof Tenant) {
|
||||
$parameters['tenant'] = $tenant->external_id;
|
||||
@ -802,6 +812,20 @@ public static function getUrl(?string $name = null, array $parameters = [], bool
|
||||
if (! array_key_exists('tenant', $parameters) && $resolvedTenant instanceof Tenant) {
|
||||
$parameters['tenant'] = $resolvedTenant->external_id;
|
||||
}
|
||||
|
||||
$record = $parameters['record'] ?? null;
|
||||
|
||||
if (! array_key_exists('tenant', $parameters) && $record instanceof ProviderConnection) {
|
||||
$recordTenant = $record->tenant;
|
||||
|
||||
if (! $recordTenant instanceof Tenant) {
|
||||
$recordTenant = Tenant::query()->whereKey($record->tenant_id)->first();
|
||||
}
|
||||
|
||||
if ($recordTenant instanceof Tenant) {
|
||||
$parameters['tenant'] = $recordTenant->external_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$panel ??= 'admin';
|
||||
|
||||
@ -630,9 +630,9 @@ protected function getHeaderActions(): array
|
||||
$hadCredentials = $record->credential()->exists();
|
||||
$previousStatus = (string) $record->status;
|
||||
|
||||
$status = $hadCredentials ? 'connected' : 'error';
|
||||
$errorReasonCode = $hadCredentials ? null : ProviderReasonCodes::ProviderCredentialMissing;
|
||||
$errorMessage = $hadCredentials ? null : 'Provider connection credentials are missing.';
|
||||
$status = $hadCredentials ? 'connected' : 'needs_consent';
|
||||
$errorReasonCode = null;
|
||||
$errorMessage = null;
|
||||
|
||||
$record->update([
|
||||
'status' => $status,
|
||||
@ -669,8 +669,8 @@ protected function getHeaderActions(): array
|
||||
|
||||
if (! $hadCredentials) {
|
||||
Notification::make()
|
||||
->title('Connection enabled (credentials missing)')
|
||||
->body('Add credentials before running checks or operations.')
|
||||
->title('Connection enabled (needs consent)')
|
||||
->body('Grant admin consent before running checks or operations.')
|
||||
->warning()
|
||||
->send();
|
||||
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
use Filament\Actions\ActionGroup;
|
||||
use Filament\Actions\BulkAction;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms;
|
||||
use Filament\Infolists;
|
||||
use Filament\Notifications\Notification;
|
||||
@ -60,6 +61,17 @@ class RestoreRunResource extends Resource
|
||||
{
|
||||
protected static ?string $model = RestoreRun::class;
|
||||
|
||||
protected static ?string $tenantOwnershipRelationshipName = 'tenant';
|
||||
|
||||
public static function shouldRegisterNavigation(): bool
|
||||
{
|
||||
if (Filament::getCurrentPanel()?->getId() === 'admin') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::shouldRegisterNavigation();
|
||||
}
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-arrow-path-rounded-square';
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Backups & Restore';
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
use App\Http\Controllers\RbacDelegatedAuthController;
|
||||
use App\Jobs\BulkTenantSyncJob;
|
||||
use App\Jobs\SyncPoliciesJob;
|
||||
use App\Jobs\SyncRoleDefinitionsJob;
|
||||
use App\Models\EntraGroup;
|
||||
use App\Models\EntraRoleDefinition;
|
||||
use App\Models\ProviderConnection;
|
||||
@ -18,6 +17,7 @@
|
||||
use App\Services\Auth\RoleCapabilityMap;
|
||||
use App\Services\Directory\EntraGroupLabelResolver;
|
||||
use App\Services\Directory\RoleDefinitionsSyncService;
|
||||
use App\Services\Graph\GraphClientInterface;
|
||||
use App\Services\Intune\AuditLogger;
|
||||
use App\Services\Intune\RbacOnboardingService;
|
||||
use App\Services\OperationRunService;
|
||||
@ -50,9 +50,7 @@
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use Throwable;
|
||||
use UnitEnum;
|
||||
|
||||
class TenantResource extends Resource
|
||||
@ -982,6 +980,7 @@ public static function rbacAction(): Actions\Action
|
||||
->placeholder('Search security groups')
|
||||
->visible(fn (Get $get) => $get('scope') === 'scope_group')
|
||||
->required(fn (Get $get) => $get('scope') === 'scope_group')
|
||||
->disabled(fn (?Tenant $record): bool => static::delegatedToken($record) === null)
|
||||
->getSearchResultsUsing(fn (string $search, ?Tenant $record) => static::groupSearchOptions($record, $search))
|
||||
->getOptionLabelUsing(fn (?string $value, ?Tenant $record) => static::groupLabelFromCache($record, $value))
|
||||
->noSearchResultsMessage('No security groups found')
|
||||
@ -1008,6 +1007,7 @@ public static function rbacAction(): Actions\Action
|
||||
->placeholder('Search security groups')
|
||||
->visible(fn (Get $get) => $get('group_mode') === 'existing')
|
||||
->required(fn (Get $get) => $get('group_mode') === 'existing')
|
||||
->disabled(fn (?Tenant $record): bool => static::delegatedToken($record) === null)
|
||||
->getSearchResultsUsing(fn (string $search, ?Tenant $record) => static::groupSearchOptions($record, $search))
|
||||
->getOptionLabelUsing(fn (?string $value, ?Tenant $record) => static::groupLabelFromCache($record, $value))
|
||||
->noSearchResultsMessage('No security groups found')
|
||||
@ -1046,8 +1046,10 @@ public static function rbacAction(): Actions\Action
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$cacheKey = RbacDelegatedAuthController::cacheKey($record, auth()->id(), session()->getId());
|
||||
$token = Cache::get($cacheKey);
|
||||
$userCacheKey = RbacDelegatedAuthController::cacheKey($record, auth()->id(), null);
|
||||
$sessionCacheKey = RbacDelegatedAuthController::cacheKey($record, auth()->id(), session()->getId());
|
||||
|
||||
$token = Cache::get($sessionCacheKey) ?? Cache::get($userCacheKey);
|
||||
|
||||
if (! $token) {
|
||||
Notification::make()
|
||||
@ -1072,7 +1074,8 @@ public static function rbacAction(): Actions\Action
|
||||
|
||||
$result = $service->run($record, $data, $user, $token);
|
||||
|
||||
Cache::forget($cacheKey);
|
||||
Cache::forget($sessionCacheKey);
|
||||
Cache::forget($userCacheKey);
|
||||
|
||||
if ($result['status'] === 'success') {
|
||||
Notification::make()
|
||||
@ -1232,11 +1235,7 @@ public static function roleSearchHelper(?Tenant $tenant): ?string
|
||||
return null;
|
||||
}
|
||||
|
||||
$exists = EntraRoleDefinition::query()
|
||||
->where('tenant_id', $tenant->getKey())
|
||||
->exists();
|
||||
|
||||
return $exists ? null : 'Role definitions not synced yet. Use “Sync now” to load.';
|
||||
return static::delegatedToken($tenant) === null ? 'Login to search roles' : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1244,9 +1243,61 @@ public static function roleSearchHelper(?Tenant $tenant): ?string
|
||||
*/
|
||||
public static function roleSearchOptions(?Tenant $tenant, string $search): array
|
||||
{
|
||||
$token = static::delegatedToken($tenant);
|
||||
|
||||
if ($token !== null) {
|
||||
return static::searchRoleDefinitionsDelegated($tenant, $search, $token);
|
||||
}
|
||||
|
||||
return static::searchRoleDefinitions($tenant, $search);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private static function searchRoleDefinitionsDelegated(?Tenant $tenant, string $search, string $token): array
|
||||
{
|
||||
if (! $tenant || mb_strlen($search) < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$needle = mb_strtolower($search);
|
||||
|
||||
/** @var GraphClientInterface $graph */
|
||||
$graph = app(GraphClientInterface::class);
|
||||
|
||||
$response = $graph->request('GET', 'deviceManagement/roleDefinitions', [
|
||||
'access_token' => $token,
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$roles = is_array($response->data['value'] ?? null) ? $response->data['value'] : [];
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($roles as $role) {
|
||||
$id = is_string($role['id'] ?? null) ? (string) $role['id'] : null;
|
||||
$displayName = is_string($role['displayName'] ?? null) ? (string) $role['displayName'] : null;
|
||||
|
||||
if (! $id || ! $displayName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! str_contains(mb_strtolower($displayName), $needle)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$results[$id] = static::formatRoleLabel($displayName, $id);
|
||||
}
|
||||
|
||||
ksort($results);
|
||||
|
||||
return array_slice($results, 0, 20, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
@ -1314,7 +1365,11 @@ private static function loginToSearchGroupsAction(?Tenant $tenant): ?Actions\Act
|
||||
|
||||
public static function groupSearchHelper(?Tenant $tenant): ?string
|
||||
{
|
||||
return null;
|
||||
if (! $tenant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return static::delegatedToken($tenant) === null ? 'Login to search groups' : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1326,6 +1381,42 @@ public static function groupSearchOptions(?Tenant $tenant, string $search): arra
|
||||
return [];
|
||||
}
|
||||
|
||||
$token = static::delegatedToken($tenant);
|
||||
|
||||
if ($token !== null) {
|
||||
/** @var GraphClientInterface $graph */
|
||||
$graph = app(GraphClientInterface::class);
|
||||
|
||||
$response = $graph->request('GET', 'groups', [
|
||||
'access_token' => $token,
|
||||
'query' => [
|
||||
'$filter' => sprintf("startswith(displayName,'%s') and securityEnabled eq true", addslashes($search)),
|
||||
],
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$groups = is_array($response->data['value'] ?? null) ? $response->data['value'] : [];
|
||||
$results = [];
|
||||
|
||||
foreach ($groups as $group) {
|
||||
$id = is_string($group['id'] ?? null) ? (string) $group['id'] : null;
|
||||
$displayName = is_string($group['displayName'] ?? null) ? (string) $group['displayName'] : null;
|
||||
|
||||
if (! $id || ! $displayName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$results[$id] = EntraGroupLabelResolver::formatLabel($displayName, $id);
|
||||
}
|
||||
|
||||
ksort($results);
|
||||
|
||||
return array_slice($results, 0, 20, true);
|
||||
}
|
||||
|
||||
$needle = mb_strtolower($search);
|
||||
|
||||
return EntraGroup::query()
|
||||
|
||||
@ -46,7 +46,7 @@ public function handle(PolicySyncService $service, OperationRunService $operatio
|
||||
{
|
||||
$graph = app(GraphClientInterface::class);
|
||||
|
||||
if (! config('graph.enabled') || $graph instanceof NullGraphClient) {
|
||||
if ($graph instanceof NullGraphClient) {
|
||||
if ($this->operationRun) {
|
||||
$operationRunService->updateRun(
|
||||
$this->operationRun,
|
||||
|
||||
@ -5,8 +5,11 @@
|
||||
use App\Filament\Pages\Auth\Login;
|
||||
use App\Filament\Pages\ChooseTenant;
|
||||
use App\Filament\Pages\ChooseWorkspace;
|
||||
use App\Filament\Pages\InventoryCoverage;
|
||||
use App\Filament\Pages\NoAccess;
|
||||
use App\Filament\Pages\TenantRequiredPermissions;
|
||||
use App\Filament\Resources\InventoryItemResource;
|
||||
use App\Filament\Resources\PolicyResource;
|
||||
use App\Filament\Resources\ProviderConnectionResource;
|
||||
use App\Filament\Resources\TenantResource;
|
||||
use App\Filament\Resources\Workspaces\WorkspaceResource;
|
||||
@ -37,6 +40,7 @@ class AdminPanelProvider extends PanelProvider
|
||||
public function panel(Panel $panel): Panel
|
||||
{
|
||||
$panel = $panel
|
||||
->default()
|
||||
->id('admin')
|
||||
->path('admin')
|
||||
->login(Login::class)
|
||||
@ -44,8 +48,6 @@ public function panel(Panel $panel): Panel
|
||||
ChooseWorkspace::registerRoutes($panel);
|
||||
ChooseTenant::registerRoutes($panel);
|
||||
NoAccess::registerRoutes($panel);
|
||||
|
||||
WorkspaceResource::registerRoutes($panel);
|
||||
})
|
||||
->colors([
|
||||
'primary' => Color::Amber,
|
||||
@ -104,9 +106,15 @@ public function panel(Panel $panel): Panel
|
||||
)
|
||||
->resources([
|
||||
TenantResource::class,
|
||||
PolicyResource::class,
|
||||
ProviderConnectionResource::class,
|
||||
InventoryItemResource::class,
|
||||
WorkspaceResource::class,
|
||||
])
|
||||
->discoverClusters(in: app_path('Filament/Clusters'), for: 'App\\Filament\\Clusters')
|
||||
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
|
||||
->pages([
|
||||
InventoryCoverage::class,
|
||||
TenantRequiredPermissions::class,
|
||||
])
|
||||
->widgets([
|
||||
|
||||
@ -30,7 +30,6 @@ class TenantPanelProvider extends PanelProvider
|
||||
public function panel(Panel $panel): Panel
|
||||
{
|
||||
$panel = $panel
|
||||
->default()
|
||||
->id('tenant')
|
||||
->path('admin/t')
|
||||
->login(Login::class)
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Services\Inventory;
|
||||
|
||||
use App\Models\InventoryItem;
|
||||
use App\Models\InventorySyncRun;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\ProviderConnection;
|
||||
use App\Models\Tenant;
|
||||
@ -10,10 +11,14 @@
|
||||
use App\Services\Graph\GraphResponse;
|
||||
use App\Services\Providers\ProviderConnectionResolver;
|
||||
use App\Services\Providers\ProviderGateway;
|
||||
use App\Support\OperationRunOutcome;
|
||||
use App\Support\OperationRunStatus;
|
||||
use App\Support\OperationRunType;
|
||||
use App\Support\Providers\ProviderReasonCodes;
|
||||
use Illuminate\Contracts\Cache\Lock;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
@ -28,6 +33,88 @@ public function __construct(
|
||||
private readonly ProviderGateway $providerGateway,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Runs an inventory sync immediately and persists a corresponding InventorySyncRun.
|
||||
*
|
||||
* This is primarily used in tests and for synchronous workflows.
|
||||
*
|
||||
* @param array<string, mixed> $selectionPayload
|
||||
*/
|
||||
public function syncNow(Tenant $tenant, array $selectionPayload): InventorySyncRun
|
||||
{
|
||||
$computed = $this->normalizeAndHashSelection($selectionPayload);
|
||||
$normalizedSelection = $computed['selection'];
|
||||
$selectionHash = $computed['selection_hash'];
|
||||
|
||||
$operationRun = OperationRun::query()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'user_id' => null,
|
||||
'initiator_name' => 'System',
|
||||
'type' => OperationRunType::InventorySync->value,
|
||||
'status' => OperationRunStatus::Running->value,
|
||||
'outcome' => OperationRunOutcome::Pending->value,
|
||||
'run_identity_hash' => hash('sha256', (string) $tenant->getKey().':inventory.sync:'.$selectionHash.':'.Str::uuid()->toString()),
|
||||
'context' => $normalizedSelection,
|
||||
'started_at' => now(),
|
||||
]);
|
||||
|
||||
$run = InventorySyncRun::query()->create([
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'user_id' => null,
|
||||
'operation_run_id' => (int) $operationRun->getKey(),
|
||||
'selection_hash' => $selectionHash,
|
||||
'selection_payload' => $normalizedSelection,
|
||||
'status' => InventorySyncRun::STATUS_RUNNING,
|
||||
'had_errors' => false,
|
||||
'started_at' => now(),
|
||||
]);
|
||||
|
||||
$result = $this->executeSelection($operationRun, $tenant, $normalizedSelection);
|
||||
|
||||
$status = (string) ($result['status'] ?? InventorySyncRun::STATUS_FAILED);
|
||||
$hadErrors = (bool) ($result['had_errors'] ?? true);
|
||||
$errorCodes = is_array($result['error_codes'] ?? null) ? $result['error_codes'] : null;
|
||||
$errorContext = is_array($result['error_context'] ?? null) ? $result['error_context'] : null;
|
||||
|
||||
$run->update([
|
||||
'status' => $status,
|
||||
'had_errors' => $hadErrors,
|
||||
'error_codes' => $errorCodes,
|
||||
'error_context' => $errorContext,
|
||||
'items_observed_count' => (int) ($result['items_observed_count'] ?? 0),
|
||||
'items_upserted_count' => (int) ($result['items_upserted_count'] ?? 0),
|
||||
'errors_count' => (int) ($result['errors_count'] ?? 0),
|
||||
'finished_at' => now(),
|
||||
]);
|
||||
|
||||
$policyTypes = $normalizedSelection['policy_types'] ?? [];
|
||||
$policyTypes = is_array($policyTypes) ? $policyTypes : [];
|
||||
|
||||
$operationOutcome = match ($status) {
|
||||
'success' => OperationRunOutcome::Succeeded->value,
|
||||
'partial' => OperationRunOutcome::PartiallySucceeded->value,
|
||||
'skipped' => OperationRunOutcome::Blocked->value,
|
||||
default => OperationRunOutcome::Failed->value,
|
||||
};
|
||||
|
||||
$operationRun->update([
|
||||
'status' => OperationRunStatus::Completed->value,
|
||||
'outcome' => $operationOutcome,
|
||||
'summary_counts' => [
|
||||
'total' => count($policyTypes),
|
||||
'processed' => count($policyTypes),
|
||||
'succeeded' => $status === 'success' ? count($policyTypes) : max(0, count($policyTypes) - (int) ($result['errors_count'] ?? 0)),
|
||||
'failed' => (int) ($result['errors_count'] ?? 0),
|
||||
'items' => (int) ($result['items_observed_count'] ?? 0),
|
||||
'updated' => (int) ($result['items_upserted_count'] ?? 0),
|
||||
],
|
||||
'completed_at' => now(),
|
||||
]);
|
||||
|
||||
return $run->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs an inventory sync (inline), enforcing locks/concurrency.
|
||||
*
|
||||
|
||||
@ -65,7 +65,14 @@ public function handle(Request $request, Closure $next): Response
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$tenantParameter = null;
|
||||
if ($request->route()?->hasParameter('tenant')) {
|
||||
$tenantParameter = $request->route()->parameter('tenant');
|
||||
} elseif (filled($request->query('tenant'))) {
|
||||
$tenantParameter = $request->query('tenant');
|
||||
}
|
||||
|
||||
if ($tenantParameter !== null) {
|
||||
$user = $request->user();
|
||||
|
||||
if ($user === null) {
|
||||
@ -76,12 +83,9 @@ public function handle(Request $request, Closure $next): Response
|
||||
abort(404);
|
||||
}
|
||||
|
||||
if (! $panel->hasTenancy()) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$tenantParameter = $request->route()->parameter('tenant');
|
||||
$tenant = $panel->getTenant($tenantParameter);
|
||||
$tenant = $tenantParameter instanceof Tenant
|
||||
? $tenantParameter
|
||||
: Tenant::query()->withTrashed()->where('external_id', (string) $tenantParameter)->first();
|
||||
|
||||
if (! $tenant instanceof Tenant) {
|
||||
abort(404);
|
||||
|
||||
@ -21,7 +21,9 @@ public function requiredCapabilityForType(string $operationType): ?string
|
||||
'restore.execute' => Capabilities::TENANT_MANAGE,
|
||||
'directory_role_definitions.sync' => Capabilities::TENANT_MANAGE,
|
||||
|
||||
'provider.connection.check' => Capabilities::PROVIDER_RUN,
|
||||
// Viewing verification reports should be possible for readonly members.
|
||||
// Starting verification is separately guarded by the verification service.
|
||||
'provider.connection.check' => Capabilities::PROVIDER_VIEW,
|
||||
|
||||
// Keep legacy / unknown types viewable by membership+entitlement only.
|
||||
default => null,
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
</include>
|
||||
</source>
|
||||
<php>
|
||||
<ini name="memory_limit" value="512M"/>
|
||||
<ini name="memory_limit" value="2048M"/>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_KEY" value="base64:z63PQuXp3rUOQ0L4o8xp76xeakrn5X3owja1qFX3ccY="/>
|
||||
<env name="INTUNE_TENANT_ID" value="" force="true"/>
|
||||
|
||||
@ -87,7 +87,7 @@
|
||||
|
||||
<x-filament::dropdown.list>
|
||||
<a
|
||||
href="{{ ChooseWorkspace::getUrl(panel: 'admin') }}"
|
||||
href="{{ route('filament.admin.resources.workspaces.index') }}"
|
||||
class="block px-3 py-2 text-sm hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
>
|
||||
Switch workspace
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
Route::get('/admin/consent/start', TenantOnboardingController::class)
|
||||
->name('admin.consent.start');
|
||||
// Panel root override: keep the app's workspace-first flow.
|
||||
|
||||
// Avoid Filament's tenancy root redirect which otherwise sends users into legacy flows.
|
||||
// when no default tenant can be resolved.
|
||||
Route::middleware([
|
||||
@ -148,6 +148,19 @@
|
||||
->get('/admin/operations', \App\Filament\Pages\Monitoring\Operations::class)
|
||||
->name('admin.operations.index');
|
||||
|
||||
Route::middleware([
|
||||
'web',
|
||||
'panel:admin',
|
||||
'ensure-correct-guard:web',
|
||||
DisableBladeIconComponents::class,
|
||||
DispatchServingFilamentEvent::class,
|
||||
FilamentAuthenticate::class,
|
||||
'ensure-workspace-selected',
|
||||
'ensure-filament-tenant-selected',
|
||||
])
|
||||
->get('/admin/t/{tenant:external_id}/operations', fn () => redirect()->route('admin.operations.index'))
|
||||
->name('admin.operations.legacy-tenant-index');
|
||||
|
||||
Route::middleware([
|
||||
'web',
|
||||
'panel:admin',
|
||||
|
||||
@ -16,6 +16,8 @@
|
||||
]);
|
||||
$tenant->makeCurrent();
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$policies = Policy::factory()
|
||||
->count(3)
|
||||
->create([
|
||||
|
||||
@ -53,6 +53,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
|
||||
it('extracts edges during inventory sync and marks missing appropriately', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
$this->app->bind(GraphClientInterface::class, fn () => new FakeGraphClientForDeps);
|
||||
|
||||
$svc = app(InventorySyncService::class);
|
||||
@ -73,6 +74,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
|
||||
it('respects 50-edge limit for outbound extraction', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
// Fake client returning 60 group assignments
|
||||
$this->app->bind(GraphClientInterface::class, function () {
|
||||
return new class implements GraphClientInterface
|
||||
@ -132,6 +134,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
|
||||
it('persists unsupported reference warnings on the sync run record', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$this->app->bind(GraphClientInterface::class, function () {
|
||||
return new class implements GraphClientInterface
|
||||
@ -276,6 +279,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
|
||||
it('hydrates settings catalog assignments and extracts include/exclude/filter edges', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$this->app->bind(GraphClientInterface::class, function () {
|
||||
return new class implements GraphClientInterface
|
||||
|
||||
@ -97,6 +97,8 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
'tenant_id' => 'tenant-1',
|
||||
]);
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$policy = Policy::factory()->create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'external_id' => 'dcs-1',
|
||||
|
||||
@ -58,6 +58,8 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
app()->instance(GraphClientInterface::class, $client);
|
||||
|
||||
$tenant = Tenant::factory()->create();
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
$backupSet = BackupSet::factory()->for($tenant)->create([
|
||||
'status' => 'completed',
|
||||
'item_count' => 1,
|
||||
|
||||
@ -82,6 +82,8 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
app()->instance(GraphClientInterface::class, $client);
|
||||
|
||||
$tenant = Tenant::factory()->create();
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
$backupSet = BackupSet::factory()->for($tenant)->create([
|
||||
'status' => 'completed',
|
||||
'item_count' => 1,
|
||||
@ -163,6 +165,8 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
app()->instance(GraphClientInterface::class, $client);
|
||||
|
||||
$tenant = Tenant::factory()->create();
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
$backupSet = BackupSet::factory()->for($tenant)->create([
|
||||
'status' => 'completed',
|
||||
'item_count' => 1,
|
||||
|
||||
@ -57,6 +57,8 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
'metadata' => [],
|
||||
]);
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$policy = Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'external_id' => 'ca-policy-1',
|
||||
|
||||
@ -57,6 +57,8 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
'metadata' => [],
|
||||
]);
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$policy = Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'external_id' => 'enrollment-restriction-1',
|
||||
@ -159,6 +161,8 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
'metadata' => [],
|
||||
]);
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$policy = Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'external_id' => 'enrollment-limit-1',
|
||||
|
||||
@ -95,6 +95,8 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
||||
$_ENV['INTUNE_TENANT_ID'] = $tenant->tenant_id;
|
||||
$_SERVER['INTUNE_TENANT_ID'] = $tenant->tenant_id;
|
||||
|
||||
@ -103,6 +103,8 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
'metadata' => [],
|
||||
]);
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$policy = Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'external_id' => 'gpo-1',
|
||||
|
||||
@ -54,6 +54,8 @@ public function getServicePrincipalPermissions(array $options = []): GraphRespon
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||
$tenant->makeCurrent();
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('filament.admin.resources.policies.index', filamentTenantRouteParams($tenant)))
|
||||
->get(route('filament.tenant.resources.policies.index', filamentTenantRouteParams($tenant)))
|
||||
->assertOk()
|
||||
->assertSee('Policy A')
|
||||
->assertDontSee('Policy B');
|
||||
|
||||
@ -44,7 +44,7 @@
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, user: $user, role: 'owner');
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant).'?tab=settings');
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy]).'?tab=settings&tenant='.(string) $tenant->external_id);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Settings');
|
||||
|
||||
@ -71,6 +71,8 @@ public function getServicePrincipalPermissions(array $options = []): GraphRespon
|
||||
'metadata' => [],
|
||||
]);
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$policy = Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'external_id' => 'gpo-versioned-1',
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Filament\Resources\PolicyVersionResource\Pages\ListPolicyVersions;
|
||||
use App\Filament\Resources\RestoreRunResource;
|
||||
use App\Filament\Resources\RestoreRunResource\Pages\CreateRestoreRun;
|
||||
use App\Models\BackupItem;
|
||||
use App\Models\BackupSet;
|
||||
@ -58,7 +57,8 @@
|
||||
|
||||
Livewire::test(ListPolicyVersions::class)
|
||||
->callTableAction('restore_via_wizard', $version)
|
||||
->assertRedirectContains(RestoreRunResource::getUrl('create', [], false, tenant: $tenant));
|
||||
->assertRedirectContains('/admin/restore-runs/create')
|
||||
->assertRedirectContains('tenant='.(string) $tenant->external_id);
|
||||
|
||||
$backupSet = BackupSet::query()->where('metadata->source', 'policy_version')->first();
|
||||
expect($backupSet)->not->toBeNull();
|
||||
|
||||
@ -126,7 +126,7 @@
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant).'?tab=normalized-settings');
|
||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version]).'?tab=normalized-settings&tenant='.(string) $tenant->external_id);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Enrollment notifications');
|
||||
|
||||
@ -56,6 +56,7 @@ public function getServicePrincipalPermissions(array $options = []): GraphRespon
|
||||
'name' => 'Tenant One',
|
||||
'metadata' => [],
|
||||
]);
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$policy = Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
@ -148,6 +149,7 @@ public function getServicePrincipalPermissions(array $options = []): GraphRespon
|
||||
]);
|
||||
|
||||
$tenant = Tenant::factory()->create();
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
$backupSet = BackupSet::factory()->for($tenant)->create();
|
||||
$backupItem = BackupItem::factory()
|
||||
->for($tenant)
|
||||
@ -251,6 +253,7 @@ public function getServicePrincipalPermissions(array $options = []): GraphRespon
|
||||
'name' => 'Tenant Three',
|
||||
'metadata' => [],
|
||||
]);
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$policy = Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
@ -365,6 +368,7 @@ public function getServicePrincipalPermissions(array $options = []): GraphRespon
|
||||
'status' => 'active',
|
||||
'metadata' => [],
|
||||
]);
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$policy = Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
@ -481,6 +485,7 @@ public function getServicePrincipalPermissions(array $options = []): GraphRespon
|
||||
'name' => 'Tenant One',
|
||||
'metadata' => [],
|
||||
]);
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$backupSet = BackupSet::factory()->for($tenant)->create([
|
||||
'status' => 'completed',
|
||||
@ -586,6 +591,7 @@ public function getServicePrincipalPermissions(array $options = []): GraphRespon
|
||||
'name' => 'Tenant Four',
|
||||
'metadata' => [],
|
||||
]);
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$backupSet = BackupSet::factory()->for($tenant)->create([
|
||||
'status' => 'completed',
|
||||
|
||||
@ -26,7 +26,7 @@ function makeAssignment(string $odataType, string $groupId, ?string $displayName
|
||||
bindFailHardGraphClient();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(RestoreRunResource::getUrl('create', tenant: $tenant))
|
||||
->get(RestoreRunResource::getUrl('create').'?tenant='.(string) $tenant->external_id)
|
||||
->assertOk()
|
||||
->assertSee('Create restore run')
|
||||
->assertSee('Select Backup Set');
|
||||
@ -54,7 +54,7 @@ function makeAssignment(string $odataType, string $groupId, ?string $displayName
|
||||
|
||||
bindFailHardGraphClient();
|
||||
|
||||
$url = RestoreRunResource::getUrl('create', tenant: $tenant).'?backup_set_id='.$backupSet->getKey();
|
||||
$url = RestoreRunResource::getUrl('create').'?backup_set_id='.$backupSet->getKey().'&tenant='.(string) $tenant->external_id;
|
||||
|
||||
$this->actingAs($user)
|
||||
->get($url)
|
||||
|
||||
@ -50,10 +50,10 @@
|
||||
],
|
||||
]);
|
||||
|
||||
$this->get(\App\Filament\Resources\PolicyVersionResource::getUrl('index', tenant: $tenant))
|
||||
$this->get(\App\Filament\Resources\PolicyVersionResource::getUrl('index').'?tenant='.(string) $tenant->external_id)
|
||||
->assertSuccessful();
|
||||
|
||||
$this->get(\App\Filament\Resources\PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant).'?tab=normalized-settings')
|
||||
$this->get(\App\Filament\Resources\PolicyVersionResource::getUrl('view', ['record' => $version]).'?tab=normalized-settings&tenant='.(string) $tenant->external_id)
|
||||
->assertSuccessful();
|
||||
|
||||
$originalEnv !== false
|
||||
@ -116,9 +116,9 @@
|
||||
],
|
||||
]);
|
||||
|
||||
$url = \App\Filament\Resources\PolicyVersionResource::getUrl('view', ['record' => $v2], tenant: $tenant);
|
||||
$url = \App\Filament\Resources\PolicyVersionResource::getUrl('view', ['record' => $v2]).'?tenant='.(string) $tenant->external_id;
|
||||
|
||||
$this->get($url.'?tab=diff')
|
||||
$this->get($url.'&tab=diff')
|
||||
->assertSuccessful()
|
||||
->assertSeeText('Fullscreen')
|
||||
->assertSeeText("- Write-Host 'one'")
|
||||
@ -181,9 +181,9 @@
|
||||
],
|
||||
]);
|
||||
|
||||
$url = \App\Filament\Resources\PolicyVersionResource::getUrl('view', ['record' => $v2], tenant: $tenant);
|
||||
$url = \App\Filament\Resources\PolicyVersionResource::getUrl('view', ['record' => $v2]).'?tenant='.(string) $tenant->external_id;
|
||||
|
||||
$this->get($url.'?tab=diff')
|
||||
$this->get($url.'&tab=diff')
|
||||
->assertSuccessful()
|
||||
->assertSeeText('Fullscreen')
|
||||
->assertSeeText("- Write-Host 'one'")
|
||||
|
||||
@ -73,6 +73,8 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
'is_current' => true,
|
||||
]);
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
||||
$_ENV['INTUNE_TENANT_ID'] = $tenant->tenant_id;
|
||||
$_SERVER['INTUNE_TENANT_ID'] = $tenant->tenant_id;
|
||||
@ -126,6 +128,8 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
'is_current' => true,
|
||||
]);
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
||||
$_ENV['INTUNE_TENANT_ID'] = $tenant->tenant_id;
|
||||
$_SERVER['INTUNE_TENANT_ID'] = $tenant->tenant_id;
|
||||
|
||||
@ -2,7 +2,10 @@
|
||||
|
||||
use App\Models\Policy;
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Models\ProviderConnection;
|
||||
use App\Models\ProviderCredential;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\Workspace;
|
||||
use App\Services\Graph\GraphClientInterface;
|
||||
use App\Services\Graph\GraphResponse;
|
||||
use App\Services\Intune\PolicySyncService;
|
||||
@ -82,6 +85,27 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
$tenant->makeCurrent();
|
||||
expect(Tenant::current()->id)->toBe($tenant->id);
|
||||
|
||||
$workspace = Workspace::factory()->create();
|
||||
$tenant->forceFill(['workspace_id' => (int) $workspace->getKey()])->save();
|
||||
|
||||
$connection = ProviderConnection::factory()->create([
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
'provider' => 'microsoft',
|
||||
'entra_tenant_id' => $tenant->tenant_id,
|
||||
'is_default' => true,
|
||||
'status' => 'connected',
|
||||
]);
|
||||
|
||||
ProviderCredential::factory()->create([
|
||||
'provider_connection_id' => (int) $connection->getKey(),
|
||||
'type' => 'client_secret',
|
||||
'payload' => [
|
||||
'client_id' => 'test-client-id',
|
||||
'client_secret' => 'test-client-secret',
|
||||
],
|
||||
]);
|
||||
|
||||
app(PolicySyncService::class)->syncPolicies($tenant);
|
||||
|
||||
$settingsPolicy = Policy::where('policy_type', 'settingsCatalogPolicy')->first();
|
||||
@ -113,7 +137,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->get(route('filament.admin.resources.policies.index', filamentTenantRouteParams($tenant)));
|
||||
->get(route('filament.tenant.resources.policies.index', filamentTenantRouteParams($tenant)));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Settings Catalog Policy');
|
||||
|
||||
@ -103,6 +103,8 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
'metadata' => [],
|
||||
]);
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$policy = Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'external_id' => 'scp-3',
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
use App\Models\BackupItem;
|
||||
use App\Models\BackupSet;
|
||||
use App\Models\Policy;
|
||||
use App\Models\ProviderConnection;
|
||||
use App\Models\ProviderCredential;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Services\Graph\GraphClientInterface;
|
||||
@ -12,6 +14,30 @@
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
if (! function_exists('makeTenantWithDefaultProviderConnection')) {
|
||||
function makeTenantWithDefaultProviderConnection(array $attributes = []): Tenant
|
||||
{
|
||||
$tenant = Tenant::create(array_merge([
|
||||
'tenant_id' => 'tenant-1',
|
||||
'name' => 'Tenant One',
|
||||
'metadata' => [],
|
||||
], $attributes));
|
||||
|
||||
$connection = ProviderConnection::factory()->create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'provider' => 'microsoft',
|
||||
'is_default' => true,
|
||||
'status' => 'ok',
|
||||
]);
|
||||
|
||||
ProviderCredential::factory()->create([
|
||||
'provider_connection_id' => $connection->id,
|
||||
]);
|
||||
|
||||
return $tenant;
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsCatalogRestoreGraphClient implements GraphClientInterface
|
||||
{
|
||||
/**
|
||||
@ -105,11 +131,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
|
||||
app()->instance(GraphClientInterface::class, $client);
|
||||
|
||||
$tenant = Tenant::create([
|
||||
'tenant_id' => 'tenant-1',
|
||||
'name' => 'Tenant One',
|
||||
'metadata' => [],
|
||||
]);
|
||||
$tenant = makeTenantWithDefaultProviderConnection();
|
||||
|
||||
$policy = Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
@ -204,7 +226,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', array_merge(filamentTenantRouteParams($tenant), ['record' => $run])));
|
||||
->get(route('filament.tenant.resources.restore-runs.view', array_merge(filamentTenantRouteParams($tenant), ['record' => $run])));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('settings are read-only');
|
||||
@ -225,10 +247,9 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
|
||||
app()->instance(GraphClientInterface::class, $client);
|
||||
|
||||
$tenant = Tenant::create([
|
||||
$tenant = makeTenantWithDefaultProviderConnection([
|
||||
'tenant_id' => 'tenant-2',
|
||||
'name' => 'Tenant Two',
|
||||
'metadata' => [],
|
||||
]);
|
||||
|
||||
$policy = Policy::create([
|
||||
@ -346,10 +367,9 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
|
||||
app()->instance(GraphClientInterface::class, $client);
|
||||
|
||||
$tenant = Tenant::create([
|
||||
$tenant = makeTenantWithDefaultProviderConnection([
|
||||
'tenant_id' => 'tenant-4',
|
||||
'name' => 'Tenant Four',
|
||||
'metadata' => [],
|
||||
]);
|
||||
|
||||
$policy = Policy::create([
|
||||
@ -464,10 +484,9 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
|
||||
app()->instance(GraphClientInterface::class, $client);
|
||||
|
||||
$tenant = Tenant::create([
|
||||
$tenant = makeTenantWithDefaultProviderConnection([
|
||||
'tenant_id' => 'tenant-5',
|
||||
'name' => 'Tenant Five',
|
||||
'metadata' => [],
|
||||
]);
|
||||
|
||||
$policy = Policy::create([
|
||||
|
||||
@ -56,8 +56,10 @@
|
||||
|
||||
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner');
|
||||
|
||||
$tenant->makeCurrent();
|
||||
|
||||
$policyResponse = $this->actingAs($user)
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant).'?tab=settings');
|
||||
->get(PolicyResource::getUrl('view', ['record' => $policy], panel: 'admin').'?tab=settings&tenant='.(string) $tenant->external_id);
|
||||
|
||||
$policyResponse->assertOk();
|
||||
$policyResponse->assertSee('fi-width-full');
|
||||
@ -68,7 +70,7 @@
|
||||
$policyResponse->assertSee('fi-ta-table');
|
||||
|
||||
$versionResponse = $this->actingAs($user)
|
||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant));
|
||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version], panel: 'admin').'?tenant='.(string) $tenant->external_id);
|
||||
|
||||
$versionResponse->assertOk();
|
||||
$versionResponse->assertSee('fi-width-full');
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
$unauthorizedTenant = Tenant::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('filament.admin.resources.policies.index', filamentTenantRouteParams($unauthorizedTenant)))
|
||||
->get(route('filament.tenant.resources.policies.index', filamentTenantRouteParams($unauthorizedTenant)))
|
||||
->assertNotFound();
|
||||
});
|
||||
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
use App\Filament\Resources\TenantResource\Pages\ViewTenant;
|
||||
use App\Http\Controllers\RbacDelegatedAuthController;
|
||||
use App\Models\ProviderConnection;
|
||||
use App\Models\ProviderCredential;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\Graph\GraphClientInterface;
|
||||
use App\Services\Graph\GraphResponse;
|
||||
@ -19,13 +21,30 @@
|
||||
|
||||
function tenantWithApp(): Tenant
|
||||
{
|
||||
return Tenant::create([
|
||||
$tenant = Tenant::create([
|
||||
'tenant_id' => 'tenant-guid',
|
||||
'name' => 'Tenant One',
|
||||
'app_client_id' => 'client-123',
|
||||
'app_client_secret' => 'secret',
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
$connection = ProviderConnection::factory()->create([
|
||||
'tenant_id' => $tenant->getKey(),
|
||||
'provider' => 'microsoft',
|
||||
'is_default' => true,
|
||||
'status' => 'ok',
|
||||
]);
|
||||
|
||||
ProviderCredential::factory()->create([
|
||||
'provider_connection_id' => $connection->getKey(),
|
||||
'payload' => [
|
||||
'client_id' => 'client-123',
|
||||
'client_secret' => 'secret',
|
||||
],
|
||||
]);
|
||||
|
||||
return $tenant;
|
||||
}
|
||||
|
||||
test('rbac action prompts login when no delegated token', function () {
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
use App\Models\BackupSet;
|
||||
use App\Models\Policy;
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Models\ProviderConnection;
|
||||
use App\Models\ProviderCredential;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Services\Graph\GraphClientInterface;
|
||||
@ -13,6 +15,30 @@
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
if (! function_exists('makeTenantWithDefaultProviderConnection')) {
|
||||
function makeTenantWithDefaultProviderConnection(array $attributes = []): Tenant
|
||||
{
|
||||
$tenant = Tenant::create(array_merge([
|
||||
'tenant_id' => 'tenant-1',
|
||||
'name' => 'Tenant One',
|
||||
'metadata' => [],
|
||||
], $attributes));
|
||||
|
||||
$connection = ProviderConnection::factory()->create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'provider' => 'microsoft',
|
||||
'is_default' => true,
|
||||
'status' => 'ok',
|
||||
]);
|
||||
|
||||
ProviderCredential::factory()->create([
|
||||
'provider_connection_id' => $connection->id,
|
||||
]);
|
||||
|
||||
return $tenant;
|
||||
}
|
||||
}
|
||||
|
||||
class WindowsUpdateProfilesRestoreGraphClient implements GraphClientInterface
|
||||
{
|
||||
/**
|
||||
@ -62,11 +88,7 @@ public function getServicePrincipalPermissions(array $options = []): GraphRespon
|
||||
$client = new WindowsUpdateProfilesRestoreGraphClient;
|
||||
app()->instance(GraphClientInterface::class, $client);
|
||||
|
||||
$tenant = Tenant::create([
|
||||
'tenant_id' => 'tenant-1',
|
||||
'name' => 'Tenant One',
|
||||
'metadata' => [],
|
||||
]);
|
||||
$tenant = makeTenantWithDefaultProviderConnection();
|
||||
|
||||
$policy = Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
@ -141,11 +163,7 @@ public function getServicePrincipalPermissions(array $options = []): GraphRespon
|
||||
$client = new WindowsUpdateProfilesRestoreGraphClient;
|
||||
app()->instance(GraphClientInterface::class, $client);
|
||||
|
||||
$tenant = Tenant::create([
|
||||
'tenant_id' => 'tenant-1',
|
||||
'name' => 'Tenant One',
|
||||
'metadata' => [],
|
||||
]);
|
||||
$tenant = makeTenantWithDefaultProviderConnection();
|
||||
|
||||
$policy = Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
@ -220,11 +238,7 @@ public function getServicePrincipalPermissions(array $options = []): GraphRespon
|
||||
$client = new WindowsUpdateProfilesRestoreGraphClient;
|
||||
app()->instance(GraphClientInterface::class, $client);
|
||||
|
||||
$tenant = Tenant::create([
|
||||
'tenant_id' => 'tenant-1',
|
||||
'name' => 'Tenant One',
|
||||
'metadata' => [],
|
||||
]);
|
||||
$tenant = makeTenantWithDefaultProviderConnection();
|
||||
|
||||
$policy = Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
use App\Models\BackupSet;
|
||||
use App\Models\Policy;
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Models\ProviderConnection;
|
||||
use App\Models\ProviderCredential;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Services\Graph\GraphClientInterface;
|
||||
@ -13,6 +15,30 @@
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
if (! function_exists('makeTenantWithDefaultProviderConnection')) {
|
||||
function makeTenantWithDefaultProviderConnection(array $attributes = []): Tenant
|
||||
{
|
||||
$tenant = Tenant::create(array_merge([
|
||||
'tenant_id' => 'tenant-1',
|
||||
'name' => 'Tenant One',
|
||||
'metadata' => [],
|
||||
], $attributes));
|
||||
|
||||
$connection = ProviderConnection::factory()->create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'provider' => 'microsoft',
|
||||
'is_default' => true,
|
||||
'status' => 'ok',
|
||||
]);
|
||||
|
||||
ProviderCredential::factory()->create([
|
||||
'provider_connection_id' => $connection->id,
|
||||
]);
|
||||
|
||||
return $tenant;
|
||||
}
|
||||
}
|
||||
|
||||
test('restore execution applies windows update ring and records audit log', function () {
|
||||
$client = new class implements GraphClientInterface
|
||||
{
|
||||
@ -66,11 +92,7 @@ public function getServicePrincipalPermissions(array $options = []): GraphRespon
|
||||
|
||||
app()->instance(GraphClientInterface::class, $client);
|
||||
|
||||
$tenant = Tenant::create([
|
||||
'tenant_id' => 'tenant-1',
|
||||
'name' => 'Tenant One',
|
||||
'metadata' => [],
|
||||
]);
|
||||
$tenant = makeTenantWithDefaultProviderConnection();
|
||||
|
||||
$policy = Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
]);
|
||||
|
||||
// Zero state
|
||||
$url = InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant);
|
||||
$url = InventoryItemResource::getUrl('view', ['record' => $item], panel: 'admin').'?tenant='.(string) $tenant->external_id;
|
||||
$this->get($url)->assertOk()->assertSee('No dependencies found');
|
||||
|
||||
// Create a missing edge and assert badge appears
|
||||
@ -71,10 +71,10 @@
|
||||
'relationship_type' => 'depends_on',
|
||||
]);
|
||||
|
||||
$urlOutbound = InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant).'?direction=outbound';
|
||||
$urlOutbound = InventoryItemResource::getUrl('view', ['record' => $item], panel: 'admin').'?tenant='.(string) $tenant->external_id.'&direction=outbound';
|
||||
$this->get($urlOutbound)->assertOk()->assertDontSee('No dependencies found');
|
||||
|
||||
$urlInbound = InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant).'?direction=inbound';
|
||||
$urlInbound = InventoryItemResource::getUrl('view', ['record' => $item], panel: 'admin').'?tenant='.(string) $tenant->external_id.'&direction=inbound';
|
||||
$this->get($urlInbound)->assertOk()->assertDontSee('No dependencies found');
|
||||
});
|
||||
|
||||
@ -109,8 +109,8 @@
|
||||
'metadata' => ['last_known_name' => 'Scoped Target'],
|
||||
]);
|
||||
|
||||
$url = InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant)
|
||||
.'?direction=outbound&relationship_type=scoped_by';
|
||||
$url = InventoryItemResource::getUrl('view', ['record' => $item], panel: 'admin')
|
||||
.'?tenant='.(string) $tenant->external_id.'&direction=outbound&relationship_type=scoped_by';
|
||||
|
||||
$this->get($url)
|
||||
->assertOk()
|
||||
@ -141,7 +141,7 @@
|
||||
'metadata' => ['last_known_name' => 'Other Tenant Edge'],
|
||||
]);
|
||||
|
||||
$url = InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant);
|
||||
$url = InventoryItemResource::getUrl('view', ['record' => $item], panel: 'admin').'?tenant='.(string) $tenant->external_id;
|
||||
$this->get($url)
|
||||
->assertOk()
|
||||
->assertDontSee('Other Tenant Edge');
|
||||
@ -170,7 +170,7 @@
|
||||
],
|
||||
]);
|
||||
|
||||
$url = InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant);
|
||||
$url = InventoryItemResource::getUrl('view', ['record' => $item], panel: 'admin').'?tenant='.(string) $tenant->external_id;
|
||||
$this->get($url)
|
||||
->assertOk()
|
||||
->assertSee('Group (external): 123456…');
|
||||
@ -239,7 +239,7 @@
|
||||
],
|
||||
]);
|
||||
|
||||
$url = InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant);
|
||||
$url = InventoryItemResource::getUrl('view', ['record' => $item], panel: 'admin').'?tenant='.(string) $tenant->external_id;
|
||||
$this->get($url)
|
||||
->assertOk()
|
||||
->assertSee('Scope Tag: Finance (6…)')
|
||||
@ -286,7 +286,7 @@
|
||||
],
|
||||
]);
|
||||
|
||||
$url = InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant);
|
||||
$url = InventoryItemResource::getUrl('view', ['record' => $item], panel: 'admin').'?tenant='.(string) $tenant->external_id;
|
||||
$this->get($url)
|
||||
->assertOk()
|
||||
->assertSee('Scope Tag: Finance');
|
||||
@ -301,6 +301,6 @@
|
||||
'external_id' => (string) Str::uuid(),
|
||||
]);
|
||||
|
||||
$url = InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant);
|
||||
$url = InventoryItemResource::getUrl('view', ['record' => $item], panel: 'admin').'?tenant='.(string) $tenant->external_id;
|
||||
$this->get($url)->assertRedirect();
|
||||
});
|
||||
|
||||
@ -55,6 +55,8 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
'is_current' => true,
|
||||
]);
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'external_id' => 'config-1',
|
||||
|
||||
@ -55,6 +55,8 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
'is_current' => true,
|
||||
]);
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
// Create an ignored policy
|
||||
$policy = Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
@ -100,6 +102,8 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
'is_current' => true,
|
||||
]);
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
// Create multiple ignored policies
|
||||
Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Resources\TenantResource;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
@ -12,7 +13,7 @@
|
||||
[$user] = createUserWithTenant($tenant, role: 'readonly');
|
||||
|
||||
$this->actingAs($user)
|
||||
->get("/admin/t/{$tenant->external_id}/tenants/{$tenant->id}/edit")
|
||||
->get(TenantResource::getUrl('edit', ['record' => $tenant]))
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
|
||||
@ -25,7 +25,6 @@
|
||||
->get('/admin/operations')
|
||||
->assertOk()
|
||||
->assertSee($workspaceName ?? 'Select workspace')
|
||||
->assertSee('Select tenant')
|
||||
->assertSee('Search tenants…')
|
||||
->assertSee('Switch workspace')
|
||||
->assertSee('admin/select-tenant')
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
expect($failureSummaryJson)->not->toContain($rawBearer);
|
||||
expect($failureSummaryJson)->not->toContain('test.user@example.com');
|
||||
|
||||
expect($run->failure_summary[0]['reason_code'] ?? null)->toBe('permission_denied');
|
||||
expect($run->failure_summary[0]['reason_code'] ?? null)->toBe('provider_permission_denied');
|
||||
|
||||
$notification = DatabaseNotification::query()
|
||||
->where('notifiable_id', $user->getKey())
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
]);
|
||||
|
||||
$tenant->makeCurrent();
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
// Simulate an older bug: ESP row was synced under enrollmentRestriction.
|
||||
$wrong = Policy::create([
|
||||
@ -81,6 +82,7 @@
|
||||
]);
|
||||
|
||||
$tenant->makeCurrent();
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$this->mock(GraphClientInterface::class, function (MockInterface $mock) {
|
||||
$payload = [
|
||||
@ -172,6 +174,7 @@
|
||||
]);
|
||||
|
||||
$tenant->makeCurrent();
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$this->mock(GraphClientInterface::class, function (MockInterface $mock) {
|
||||
$limitPayload = [
|
||||
|
||||
@ -212,8 +212,8 @@
|
||||
|
||||
$response = $this->get(route('filament.admin.resources.policy-versions.view', array_merge(
|
||||
filamentTenantRouteParams($this->tenant),
|
||||
['record' => $version],
|
||||
)).'?tab=normalized-settings');
|
||||
['record' => $version, 'tab' => 'normalized-settings'],
|
||||
)));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Password & Access');
|
||||
|
||||
@ -4,10 +4,12 @@
|
||||
|
||||
use App\Models\BackupItem;
|
||||
use App\Models\BackupSet;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\Policy;
|
||||
use App\Models\ProviderConnection;
|
||||
use App\Models\ProviderCredential;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\Workspace;
|
||||
use App\Services\Graph\GraphClientInterface;
|
||||
use App\Services\Graph\GraphResponse;
|
||||
use App\Services\Graph\ScopeTagResolver;
|
||||
@ -16,6 +18,7 @@
|
||||
use App\Services\Intune\RbacHealthService;
|
||||
use App\Services\Intune\RestoreService;
|
||||
use App\Services\Inventory\InventorySyncService;
|
||||
use App\Support\OperationRunType;
|
||||
use App\Support\RbacReason;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
@ -26,11 +29,14 @@
|
||||
*/
|
||||
function spec081TenantWithDefaultMicrosoftConnection(string $tenantId): array
|
||||
{
|
||||
$workspace = Workspace::factory()->create();
|
||||
|
||||
$tenant = Tenant::factory()->create([
|
||||
'tenant_id' => $tenantId,
|
||||
'status' => 'active',
|
||||
'app_client_id' => null,
|
||||
'app_client_secret' => null,
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
]);
|
||||
|
||||
$connection = ProviderConnection::factory()->create([
|
||||
@ -79,17 +85,23 @@ function spec081TenantWithDefaultMicrosoftConnection(string $tenantId): array
|
||||
|
||||
app()->instance(GraphClientInterface::class, $graph);
|
||||
|
||||
$run = app(InventorySyncService::class)->syncNow(
|
||||
$setup['tenant'],
|
||||
[
|
||||
'policy_types' => ['deviceConfiguration'],
|
||||
'categories' => ['Configuration'],
|
||||
'include_foundations' => false,
|
||||
'include_dependencies' => false,
|
||||
],
|
||||
);
|
||||
$service = app(InventorySyncService::class);
|
||||
$selection = [
|
||||
'policy_types' => ['deviceConfiguration'],
|
||||
'categories' => ['Configuration'],
|
||||
'include_foundations' => false,
|
||||
'include_dependencies' => false,
|
||||
];
|
||||
|
||||
expect($run->status)->toBe('success');
|
||||
$opRun = OperationRun::factory()->create([
|
||||
'tenant_id' => (int) $setup['tenant']->getKey(),
|
||||
'workspace_id' => (int) $setup['tenant']->workspace_id,
|
||||
'type' => OperationRunType::InventorySync->value,
|
||||
]);
|
||||
|
||||
$result = $service->executeSelection($opRun, $setup['tenant'], $selection);
|
||||
|
||||
expect($result['status'])->toBe('success');
|
||||
});
|
||||
|
||||
it('Spec081 smoke: policy sync uses provider connection credentials with tenant secrets empty', function (): void {
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard;
|
||||
use App\Jobs\ProviderConnectionHealthCheckJob;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\ProviderConnection;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantOnboardingSession;
|
||||
@ -101,6 +101,14 @@
|
||||
->test(ManagedTenantOnboardingWizard::class)
|
||||
->call('startVerification');
|
||||
|
||||
Queue::assertPushed(ProviderConnectionHealthCheckJob::class, 1);
|
||||
$run = OperationRun::query()
|
||||
->where('tenant_id', (int) $tenant->getKey())
|
||||
->where('type', 'provider.connection.check')
|
||||
->latest('id')
|
||||
->first();
|
||||
|
||||
expect($run)->not->toBeNull();
|
||||
|
||||
Queue::assertNothingPushed();
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Filament\Pages\Tenancy\RegisterTenant;
|
||||
use Filament\Facades\Filament;
|
||||
|
||||
describe('Register tenant page authorization', function () {
|
||||
it('is not visible for readonly members', function () {
|
||||
@ -9,7 +10,11 @@
|
||||
$this->actingAs($user);
|
||||
$tenant->makeCurrent();
|
||||
|
||||
Filament::setCurrentPanel(Filament::getPanel('tenant'));
|
||||
|
||||
expect(RegisterTenant::canView())->toBeFalse();
|
||||
|
||||
Filament::setCurrentPanel(null);
|
||||
});
|
||||
|
||||
it('is visible for owner members', function () {
|
||||
@ -18,6 +23,10 @@
|
||||
$this->actingAs($user);
|
||||
$tenant->makeCurrent();
|
||||
|
||||
Filament::setCurrentPanel(Filament::getPanel('tenant'));
|
||||
|
||||
expect(RegisterTenant::canView())->toBeTrue();
|
||||
|
||||
Filament::setCurrentPanel(null);
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
use App\Filament\Pages\Tenancy\RegisterTenant;
|
||||
use App\Filament\Resources\TenantResource\Pages\CreateTenant;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -12,11 +13,15 @@
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
Filament::setCurrentPanel(Filament::getPanel('tenant'));
|
||||
|
||||
expect(RegisterTenant::canView())->toBeFalse();
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(RegisterTenant::class)
|
||||
->assertStatus(404);
|
||||
|
||||
Filament::setCurrentPanel(null);
|
||||
});
|
||||
|
||||
test('readonly users cannot create tenants', function () {
|
||||
|
||||
@ -81,6 +81,8 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
'metadata' => [],
|
||||
]);
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$policy = Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'external_id' => 'scp-1',
|
||||
@ -185,6 +187,8 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
'metadata' => [],
|
||||
]);
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$policy = Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'external_id' => 'scp-1',
|
||||
@ -271,6 +275,8 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
'metadata' => [],
|
||||
]);
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$policy = Policy::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'external_id' => 'scp-1',
|
||||
|
||||
@ -65,6 +65,8 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
app()->instance(GraphClientInterface::class, $client);
|
||||
|
||||
$tenant = Tenant::factory()->create();
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
$backupSet = BackupSet::factory()->for($tenant)->create([
|
||||
'status' => 'completed',
|
||||
'item_count' => 1,
|
||||
|
||||
@ -63,6 +63,8 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
|
||||
$tenant = Tenant::factory()->create();
|
||||
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
|
||||
$backupSet = BackupSet::factory()->for($tenant)->create([
|
||||
'status' => 'completed',
|
||||
'item_count' => 2,
|
||||
|
||||
@ -82,6 +82,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
config()->set('graph_contracts.types.securityBaselinePolicy', []);
|
||||
|
||||
$tenant = Tenant::factory()->create();
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
$backupSet = BackupSet::factory()->for($tenant)->create([
|
||||
'status' => 'completed',
|
||||
'item_count' => 1,
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantMembership;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -19,6 +20,8 @@
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
Filament::setCurrentPanel(Filament::getPanel('tenant'));
|
||||
|
||||
$tenantGuid = '11111111-1111-1111-1111-111111111111';
|
||||
|
||||
Livewire::test(RegisterTenant::class)
|
||||
@ -28,6 +31,8 @@
|
||||
->set('data.domain', 'acme.example')
|
||||
->call('register');
|
||||
|
||||
Filament::setCurrentPanel(null);
|
||||
|
||||
$tenant = Tenant::query()->where('tenant_id', $tenantGuid)->firstOrFail();
|
||||
|
||||
$membership = TenantMembership::query()
|
||||
|
||||
@ -95,6 +95,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
app()->instance(GraphClientInterface::class, $client);
|
||||
|
||||
$tenant = Tenant::factory()->create(['tenant_id' => 'tenant-1']);
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
$policy = Policy::factory()->create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'external_id' => 'tc-1',
|
||||
@ -182,6 +183,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
||||
|
||||
it('syncs terms and conditions from graph', function () {
|
||||
$tenant = Tenant::factory()->create(['status' => 'active']);
|
||||
ensureDefaultProviderConnection($tenant);
|
||||
$logger = mock(GraphLogger::class);
|
||||
|
||||
$logger->shouldReceive('logRequest')
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Models\ProviderConnection;
|
||||
use App\Models\ProviderCredential;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
@ -179,3 +181,41 @@ function filamentTenantRouteParams(Tenant $tenant): array
|
||||
{
|
||||
return ['tenant' => (string) $tenant->external_id];
|
||||
}
|
||||
|
||||
function ensureDefaultProviderConnection(Tenant $tenant, string $provider = 'microsoft'): ProviderConnection
|
||||
{
|
||||
$connection = ProviderConnection::query()
|
||||
->where('tenant_id', (int) $tenant->getKey())
|
||||
->where('provider', $provider)
|
||||
->where('is_default', true)
|
||||
->orderBy('id')
|
||||
->first();
|
||||
|
||||
if (! $connection instanceof ProviderConnection) {
|
||||
$connection = ProviderConnection::factory()->create([
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'provider' => $provider,
|
||||
'entra_tenant_id' => (string) ($tenant->tenant_id ?? fake()->uuid()),
|
||||
'status' => 'connected',
|
||||
'health_status' => 'ok',
|
||||
'is_default' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
$credential = $connection->credential()->first();
|
||||
|
||||
if (! $credential instanceof ProviderCredential) {
|
||||
ProviderCredential::factory()->create([
|
||||
'provider_connection_id' => (int) $connection->getKey(),
|
||||
'type' => 'client_secret',
|
||||
'payload' => [
|
||||
'client_id' => fake()->uuid(),
|
||||
'client_secret' => fake()->sha1(),
|
||||
],
|
||||
]);
|
||||
|
||||
$connection->refresh();
|
||||
}
|
||||
|
||||
return $connection;
|
||||
}
|
||||
|
||||
@ -17,16 +17,16 @@
|
||||
|
||||
$patterns = [
|
||||
// $membership->role === 'owner' / !== 'owner'
|
||||
'/->role\s*(===|==|!==|!=)\s*[\"\']?'.$roleValuePattern.'[\"\']?/i',
|
||||
'/->role\s*(===|==|!==|!=)\s*(["\'])'.$roleValuePattern.'\2/i',
|
||||
|
||||
// $role === 'owner'
|
||||
'/\$role\s*(===|==|!==|!=)\s*[\"\']?'.$roleValuePattern.'[\"\']?/i',
|
||||
'/\$role\s*(===|==|!==|!=)\s*(["\'])'.$roleValuePattern.'\2/i',
|
||||
|
||||
// case 'owner':
|
||||
'/\bcase\s*[\"\']?'.$roleValuePattern.'[\"\']?\s*:/i',
|
||||
'/\bcase\s*(["\'])'.$roleValuePattern.'\1\s*:/i',
|
||||
|
||||
// match (...) { 'owner' => ... }
|
||||
'/\bmatch\b[\s\S]*?\{[\s\S]*?[\"\']?'.$roleValuePattern.'[\"\']?\s*=>/i',
|
||||
'/\bmatch\b[\s\S]*?\{[\s\S]*?(["\'])'.$roleValuePattern.'\1\s*=>/i',
|
||||
];
|
||||
|
||||
$filesystem = new Filesystem;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user