Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,21 @@ Using the `Admin` example above, we might return `admin` as an "identifier". The

As with the `ValueHolder`, this will all be more clear when viewing the example attributes below.

#### Ambient Context Providers

Some context providers represent ambient environmental state rather than primary access contexts — for example, "is any role active?" or "is a user logged in?". These are always active and don't represent discrete access levels.

When using `FilterStrategy::Strict`, ambient contexts could cause universal denial, if they're not defined in a filter's `context:` array. To prevent this, implement `AmbientContextProviderInterface` (a marker interface extending `ContextProviderInterface`) on those providers. Strict will skip them during its coverage check.

```php
use Rentpost\Doctrine\MultiTenancy\AmbientContextProviderInterface;

class UserContextProvider implements AmbientContextProviderInterface
{
// ...
}
```

## Usage

After you've gotten everything setup, the hard part is out of the way. Taking the time to properly evaludate how you'll setup your `ValueHolder` and `ContextProvider` classes will go a long way in making the usage clean and simple.
Expand Down
18 changes: 18 additions & 0 deletions src/AmbientContextProviderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types = 1);

namespace Rentpost\Doctrine\MultiTenancy;

/**
* Marker interface for context providers that represent ambient environmental
* state rather than primary access contexts.
*
* Context providers implementing this interface are excluded from the Strict
* strategy's coverage enforcement. Use this for "always on" contexts
* (e.g., "any role active", "user logged in") that don't represent discrete
* access levels.
*
* @author Jacob Thomason <jacob@rentpost.com>
*/
interface AmbientContextProviderInterface extends ContextProviderInterface {}
4 changes: 4 additions & 0 deletions src/ConditionResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ private function hasUncoveredContexts(array $filters): bool
}

foreach ($this->listener->getContextProviders() as $contextProvider) {
if ($contextProvider instanceof AmbientContextProviderInterface) {
continue;
}

if ($contextProvider->isContextual() && !isset($coveredContexts[$contextProvider->getIdentifier()])) {
return true;
}
Expand Down
28 changes: 28 additions & 0 deletions tests/Fixture/StubAmbientContextProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types = 1);

namespace Rentpost\Doctrine\MultiTenancy\Tests\Fixture;

use Rentpost\Doctrine\MultiTenancy\AmbientContextProviderInterface;

class StubAmbientContextProvider implements AmbientContextProviderInterface
{

public function __construct(
private readonly string $identifier,
private readonly bool $contextual,
) {}


public function getIdentifier(): string
{
return $this->identifier;
}


public function isContextual(): bool
{
return $this->contextual;
}
}
45 changes: 44 additions & 1 deletion tests/Unit/ConditionResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
use Rentpost\Doctrine\MultiTenancy\Tests\Fixture\Entity\Order;
use Rentpost\Doctrine\MultiTenancy\Tests\Fixture\Entity\Product;
use Rentpost\Doctrine\MultiTenancy\Tests\Fixture\Entity\Review;
use Rentpost\Doctrine\MultiTenancy\Tests\Fixture\Entity\UnmappedEntity;
use Rentpost\Doctrine\MultiTenancy\Tests\Fixture\Entity\StrictEntity;
use Rentpost\Doctrine\MultiTenancy\Tests\Fixture\Entity\StrictNoContextFreeEntity;
use Rentpost\Doctrine\MultiTenancy\Tests\Fixture\Entity\UnmappedEntity;
use Rentpost\Doctrine\MultiTenancy\Tests\Fixture\Entity\Wishlist;
use Rentpost\Doctrine\MultiTenancy\Tests\Fixture\StubAmbientContextProvider;
use Rentpost\Doctrine\MultiTenancy\Tests\Fixture\StubContextProvider;
use Rentpost\Doctrine\MultiTenancy\Tests\Fixture\StubValueHolder;

Expand Down Expand Up @@ -433,4 +434,46 @@ public function testStrictMultipleActiveContextsOneCovered(): void
$result,
);
}


public function testStrictAmbientContextIsSkipped(): void
{
$listener = $this->createListener(
[new StubValueHolder('storeId', '42')],
[
new StubContextProvider('staff', false),
new StubContextProvider('customer', false),
new StubContextProvider('publisher', false),
new StubAmbientContextProvider('role', true),
new StubAmbientContextProvider('user', true),
],
);
$resolver = new ConditionResolver($listener);

// StrictEntity: role and user are active but ambient → skipped, no denial
$result = $resolver->resolve(StrictEntity::class, 't0');

$this->assertSame('t0.store_id = 42', $result);
}


public function testStrictAmbientContextDoesNotMaskUncovered(): void
{
$listener = $this->createListener(
[new StubValueHolder('storeId', '42')],
[
new StubContextProvider('staff', false),
new StubContextProvider('customer', false),
new StubContextProvider('publisher', false),
new StubContextProvider('vendor', true),
new StubAmbientContextProvider('role', true),
],
);
$resolver = new ConditionResolver($listener);

// StrictEntity: role is ambient (skipped), but vendor is active and uncovered → denied
$result = $resolver->resolve(StrictEntity::class, 't0');

$this->assertSame('t0.store_id = 42 AND 1 = 0', $result);
}
}
Loading