Assertions - evansims/openfga-php GitHub Wiki
Assertions can be thought of as unit tests for your permission system. They let you define what should and shouldn't be allowed, then verify your authorization model works correctly before deploying it to production.
Before working with assertions, ensure you have the following setup:
<?php
declare(strict_types=1);
use OpenFGA\Client;
$client = new Client(
url: $_ENV['FGA_API_URL'] ?? 'http://localhost:8080',
);
$storeId = $_ENV['FGA_STORE_ID'];
$modelId = $_ENV['FGA_MODEL_ID'];
Assertions are test cases that specify expected outcomes for permission checks. Each assertion says "user X should (or shouldn't) have permission Y on resource Z" and verifies this against your authorization model.
Let's say you're building a document management system. You want to test that owners can edit documents but viewers cannot:
use OpenFGA\Models\{Assertion, AssertionTupleKey};
use OpenFGA\Models\Collections\Assertions;
// Write your first test
$result = $client->writeAssertions(
store: $storeId,
model: $modelId,
assertions: new Assertions(
// Document owners can edit their documents
new Assertion(
tupleKey: new AssertionTupleKey('user:alice', 'can_edit', 'document:strategy'),
expectation: true,
),
// Users without any relationship cannot edit
new Assertion(
tupleKey: new AssertionTupleKey('user:bob', 'can_edit', 'document:strategy'),
expectation: false,
),
// Viewers cannot edit documents
new Assertion(
tupleKey: new AssertionTupleKey('user:charlie', 'can_edit', 'document:strategy'),
expectation: false,
),
),
);
if ($result->succeeded()) {
echo "✓ Assertions written successfully\n";
}
Complex authorization models often have inherited permissions. Test these relationships to ensure they work as expected:
$result = $client->writeAssertions(
store: $storeId,
model: $modelId,
assertions: new Assertions(
// Team members can access workspace documents
new Assertion(
tupleKey: new AssertionTupleKey('user:alice', 'can_view', 'document:q4-report'),
expectation: true,
),
// Non-team members cannot access workspace documents
new Assertion(
tupleKey: new AssertionTupleKey('user:external', 'can_view', 'document:q4-report'),
expectation: false,
),
// Workspace admins can delete documents
new Assertion(
tupleKey: new AssertionTupleKey('user:admin', 'can_delete', 'document:q4-report'),
expectation: true,
),
),
);
Test boundary conditions and special cases in your permission model:
$edgeCases = $client->writeAssertions(
store: $storeId,
model: $modelId,
assertions: new Assertions(
// Users cannot edit their own profile indirectly through groups
new Assertion(
tupleKey: new AssertionTupleKey('user:alice', 'can_edit', 'profile:alice'),
expectation: false,
),
// Super admins can access all resources
new Assertion(
tupleKey: new AssertionTupleKey('user:superadmin', 'can_manage', 'system:settings'),
expectation: true,
),
// Deleted users lose all permissions
new Assertion(
tupleKey: new AssertionTupleKey('user:deleted_user', 'can_view', 'document:any'),
expectation: false,
),
// Service accounts have limited permissions
new Assertion(
tupleKey: new AssertionTupleKey('service:backup', 'can_read', 'database:production'),
expectation: true,
),
new Assertion(
tupleKey: new AssertionTupleKey('service:backup', 'can_write', 'database:production'),
expectation: false,
),
),
);
Organize your assertions logically and keep them maintainable:
use OpenFGA\Models\{Assertion, AssertionTupleKey};
final class DocumentPermissionTests
{
public function __construct(
private Client $client,
) {
}
public function getAssertions(): array
{
return [
// Owner permissions
new Assertion(
tupleKey: new AssertionTupleKey(
user: 'user:document_owner',
relation: 'owner',
object: 'document:strategy_2024',
),
expectation: true,
),
new Assertion(
tupleKey: new AssertionTupleKey(
user: 'user:document_owner',
relation: 'can_edit',
object: 'document:strategy_2024',
),
expectation: true,
),
new Assertion(
tupleKey: new AssertionTupleKey(
user: 'user:document_owner',
relation: 'can_share',
object: 'document:strategy_2024',
),
expectation: true,
),
// Editor permissions
new Assertion(
tupleKey: new AssertionTupleKey(
user: 'user:editor',
relation: 'can_edit',
object: 'document:strategy_2024',
),
expectation: true,
),
new Assertion(
tupleKey: new AssertionTupleKey(
user: 'user:editor',
relation: 'can_share',
object: 'document:strategy_2024',
),
expectation: false, // Editors cannot share
),
// Viewer permissions
new Assertion(
tupleKey: new AssertionTupleKey(
user: 'user:viewer',
relation: 'can_view',
object: 'document:strategy_2024',
),
expectation: true,
),
new Assertion(
tupleKey: new AssertionTupleKey(
user: 'user:viewer',
relation: 'can_edit',
object: 'document:strategy_2024',
),
expectation: false,
),
];
}
}
Start with critical paths: test the most important permission checks first - admin access, user data privacy, billing permissions.
Test both positive and negative cases: don't just test what should work, test what should be blocked.
Use realistic data: test with actual user IDs, resource names, and permission types from your application.
Update tests when models change: assertions should evolve with your authorization model. Treat them like any other test suite.
Validate before deployment: run assertions in your CI/CD pipeline to catch permission regressions before they reach production.
$assertions = $client->readAssertions()->unwrap();
echo "Current test assertions:\n";
foreach ($assertions->getAssertions() as $assertion) {
$status = $assertion->getExpectation() ? '✓' : '✗';
echo sprintf(
"%s %s %s %s\n",
$status,
$assertion->getUser(),
$assertion->getRelation(),
$assertion->getObject(),
);
}
Integrate assertion testing into your deployment pipeline to catch permission regressions before they reach production.
# .github/workflows/authorization-tests.yml
name: Authorization Model Tests
on:
push:
paths:
- "authorization-models/**"
- "tests/authorization/**"
pull_request:
paths:
- "authorization-models/**"
- "tests/authorization/**"
jobs:
test-authorization:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
- name: Install dependencies
run: composer install --no-dev --optimize-autoloader
- name: Start OpenFGA Server
run: |
docker run -d --name openfga \
-p 8080:8080 \
openfga/openfga:latest \
run --playground-enabled
- name: Wait for OpenFGA
run: |
timeout 30 bash -c 'until curl -f http://localhost:8080/healthz; do sleep 1; done'
- name: Run Authorization Tests
run: php tests/authorization/run-assertions.php
env:
FGA_API_URL: http://localhost:8080
<?php
declare(strict_types=1);
use OpenFGA\Client;
use function OpenFGA\tuple;
final class AuthorizationTestRunner
{
private ?Client $client = null;
public function __construct(
private string $apiUrl,
private bool $verbose = false,
) {
}
public function runTests(string $storeId, string $modelId): bool
{
$this->log("Running authorization tests...\n");
// Read and validate assertions
$result = $this->getClient()->readAssertions(
store: $storeId,
model: $modelId,
);
if ($result->failed()) {
$this->log('❌ Failed to read assertions: ' . $result->err()->getMessage());
return false;
}
$assertions = $result->unwrap()->getAssertions();
$this->log(sprintf("Found %d assertions to test\n", count($assertions)));
$passed = 0;
$failed = 0;
foreach ($assertions as $assertion) {
$checkResult = $this->getClient()->check(
store: $storeId,
model: $modelId,
tupleKey: tuple(
user: $assertion->getUser(),
relation: $assertion->getRelation(),
object: $assertion->getObject(),
),
);
if ($checkResult->failed()) {
$this->log(sprintf(
"❌ Error checking %s %s %s: %s\n",
$assertion->getUser(),
$assertion->getRelation(),
$assertion->getObject(),
$checkResult->err()->getMessage(),
));
$failed++;
continue;
}
$allowed = $checkResult->unwrap()->getAllowed();
$expected = $assertion->getExpectation();
if ($allowed === $expected) {
$passed++;
if ($this->verbose) {
$this->log(sprintf(
"✅ %s %s %s = %s (expected)\n",
$assertion->getUser(),
$assertion->getRelation(),
$assertion->getObject(),
$allowed ? 'allowed' : 'denied',
));
}
} else {
$failed++;
$this->log(sprintf(
"❌ %s %s %s = %s (expected %s)\n",
$assertion->getUser(),
$assertion->getRelation(),
$assertion->getObject(),
$allowed ? 'allowed' : 'denied',
$expected ? 'allowed' : 'denied',
));
}
}
$this->log(sprintf(
"\nTest Results: %d passed, %d failed\n",
$passed,
$failed,
));
return 0 === $failed;
}
private function getClient(): Client
{
if (null === $this->client) {
$this->client = new Client(
url: $this->apiUrl,
);
}
return $this->client;
}
private function log(string $message): void
{
echo $message;
}
}
// Usage in CI/CD
$runner = new AuthorizationTestRunner(
apiUrl: $_ENV['OPENFGA_API_URL'] ?? 'http://localhost:8080',
verbose: $_ENV['VERBOSE'] ?? false,
);
$success = $runner->runTests(
storeId: $_ENV['FGA_STORE_ID'],
modelId: $_ENV['FGA_MODEL_ID'],
);
exit($success ? 0 : 1);
<?php
declare(strict_types=1);
use OpenFGA\Models\{Assertion, AssertionTupleKey};
use OpenFGA\Models\Collections\Assertions;
// authorization-models/document-system.assertions.php
return new Assertions(
// Document ownership tests
new Assertion(
tupleKey: new AssertionTupleKey(
user: 'user:alice',
relation: 'owner',
object: 'document:1',
),
expectation: true,
),
new Assertion(
tupleKey: new AssertionTupleKey(
user: 'user:bob',
relation: 'owner',
object: 'document:1',
),
expectation: false,
),
// Inherited permissions tests
new Assertion(
tupleKey: new AssertionTupleKey(
user: 'user:alice',
relation: 'editor',
object: 'document:1',
),
expectation: true, // Owner can edit
),
new Assertion(
tupleKey: new AssertionTupleKey(
user: 'user:alice',
relation: 'viewer',
object: 'document:1',
),
expectation: true, // Owner can view
),
// Team permissions tests
new Assertion(
tupleKey: new AssertionTupleKey(
user: 'team:engineering#member',
relation: 'viewer',
object: 'document:roadmap',
),
expectation: true,
),
new Assertion(
tupleKey: new AssertionTupleKey(
user: 'team:marketing#member',
relation: 'viewer',
object: 'document:roadmap',
),
expectation: false,
),
// Conditional permissions tests
new Assertion(
tupleKey: new AssertionTupleKey(
user: 'user:contractor',
relation: 'viewer',
object: 'document:sensitive',
),
expectation: true,
// Note: Conditional assertions require context to be set during testing
),
);
<?php
declare(strict_types=1);
use OpenFGA\Client;
use PHPUnit\Framework\TestCase;
use function OpenFGA\{dsl, model, store, tuple};
final class AuthorizationModelTest extends TestCase
{
private Client $client;
private string $modelId;
private string $storeId;
protected function setUp(): void
{
$this->client = new Client(
url: $_ENV['FGA_API_URL'] ?? 'http://localhost:8080',
);
// Create test store and model
$this->storeId = store(
name: 'test-' . uniqid(),
client: $this->client,
);
$authModel = dsl(file_get_contents(__DIR__ . '/../../authorization-models/main.fga'));
$this->modelId = model(
model: $authModel,
store: $this->storeId,
client: $this->client,
);
}
protected function tearDown(): void
{
// Clean up test store
$this->client->deleteStore($this->storeId);
}
public function testDocumentPermissions(): void
{
$assertions = require __DIR__ . '/../../authorization-models/document-system.assertions.php';
$result = $this->client->writeAssertions(
store: $this->storeId,
model: $this->modelId,
assertions: $assertions,
);
$this->assertTrue($result->succeeded());
// Verify the assertions by running checks
$checks = $this->client->readAssertions(
store: $this->storeId,
model: $this->modelId,
)->unwrap();
foreach ($checks->getAssertions() as $assertion) {
$checkResult = $this->client->check(
store: $this->storeId,
model: $this->modelId,
tupleKey: tuple(
user: $assertion->getUser(),
relation: $assertion->getRelation(),
object: $assertion->getObject(),
),
);
$this->assertTrue($checkResult->succeeded());
$this->assertEquals(
$assertion->getExpectation(),
$checkResult->unwrap()->getAllowed(),
sprintf(
'Failed assertion: %s %s %s',
$assertion->getUser(),
$assertion->getRelation(),
$assertion->getObject(),
),
);
}
}
}
# docker-compose.test.yml
version: "3.8"
services:
openfga:
image: openfga/openfga:latest
command: run --playground-enabled
ports:
- "8080:8080"
environment:
- OPENFGA_DATASTORE_ENGINE=memory
php-tests:
build: .
depends_on:
- openfga
environment:
- FGA_API_URL=http://openfga:8080
volumes:
- ./authorization-models:/app/authorization-models
- ./tests:/app/tests
command: |
sh -c "
composer install --no-interaction &&
vendor/bin/phpunit tests/Unit/AuthorizationModelTest.php
"
Run with: docker-compose -f docker-compose.test.yml up --build
Remember: assertions replace all existing tests for a model when you call writeAssertions()
. Always include your complete test suite in each call.