RelationshipTools.php•11.5 kB
<?php
declare(strict_types=1);
namespace OpenFGA\MCP\Tools;
use InvalidArgumentException;
use OpenFGA\ClientInterface;
use OpenFGA\Exceptions\{ClientException, ClientThrowable};
use OpenFGA\Models\Collections\{TupleKeys, UserTypeFilters};
use OpenFGA\Models\{TupleKey, UserObjectInterface, UserTypeFilter};
use OpenFGA\Responses\{CheckResponseInterface, ListObjectsResponseInterface, ListUsersResponseInterface, WriteTuplesResponseInterface};
use PhpMcp\Server\Attributes\{McpTool};
use ReflectionException;
use Throwable;
use function assert;
use function is_string;
final readonly class RelationshipTools extends AbstractTools
{
    public function __construct(
        private ClientInterface $client,
    ) {
    }
    /**
     * Check if something has a relation to an object. This answers the question, for example, can "user:1" (user) read (relation) "document:1" (object)?
     *
     * @param string $store    ID of the store to use
     * @param string $model    ID of the authorization model to use
     * @param string $user     ID of the user to check
     * @param string $relation relation to check
     * @param string $object   ID of the object to check
     *
     * @throws ClientException
     * @throws InvalidArgumentException
     * @throws ReflectionException
     * @throws Throwable
     *
     * @return string A success or error message
     */
    #[McpTool(name: 'check_permission')]
    public function checkPermission(
        string $store,
        string $model,
        string $user,
        string $relation,
        string $object,
    ): string {
        $error = $this->checkOfflineMode('Checking permissions');
        if (null !== $error) {
            return $error;
        }
        $failure = null;
        $success = '';
        $error = $this->checkRestrictedMode(storeId: $store, modelId: $model);
        if (null !== $error) {
            return $error;
        }
        $tuple = new TupleKey(
            user: $user,
            relation: $relation,
            object: $object,
        );
        $this->client->check(store: $store, model: $model, tuple: $tuple)
            ->failure(static function (Throwable $e) use (&$failure): void {
                $failure = '❌ Failed to check permission! Error: ' . $e->getMessage();
            })
            ->success(static function (mixed $response) use (&$success): void {
                assert($response instanceof CheckResponseInterface);
                $allowed = $response->getAllowed();
                $success = true === $allowed ? '✅ Permission allowed' : '❌ Permission denied';
            });
        return $failure ?? $success;
    }
    /**
     * Grant permission to something on an object.
     *
     * @param string $store    ID of the store to grant permission to
     * @param string $model    ID of the authorization model to grant permission to
     * @param string $user     ID of the user to grant permission to
     * @param string $relation relation to grant permission to
     * @param string $object   ID of the object to grant permission to
     *
     * @throws ClientException
     * @throws ClientThrowable
     * @throws InvalidArgumentException
     * @throws ReflectionException
     * @throws Throwable
     *
     * @return string a success message, or an error message
     */
    #[McpTool(name: 'grant_permission')]
    public function grantPermission(
        string $store,
        string $model,
        string $user,
        string $relation,
        string $object,
    ): string {
        $error = $this->checkOfflineMode('Granting permissions');
        if (null !== $error) {
            return $error;
        }
        $failure = null;
        $success = '';
        $called = false;
        $error = $this->checkWritePermission('grant permissions');
        if (null !== $error) {
            return $error;
        }
        $error = $this->checkRestrictedMode(storeId: $store, modelId: $model);
        if (null !== $error) {
            return $error;
        }
        $tuple = new TupleKey(
            user: $user,
            relation: $relation,
            object: $object,
        );
        $this->client->writeTuples(store: $store, model: $model, writes: new TupleKeys($tuple))
            ->failure(static function (Throwable $e) use (&$failure, &$called): void {
                $called = true;
                $failure = '❌ Failed to grant permission! Error: ' . $e->getMessage();
            })
            ->success(static function (mixed $response) use (&$success, &$called): void {
                $called = true;
                assert($response instanceof WriteTuplesResponseInterface);
                $success = '✅ Permission granted successfully';
            });
        // If neither callback was called, it means the promise chain wasn't resolved
        if (! $called) {
            return '❌ Promise was not resolved';
        }
        return $failure ?? $success;
    }
    /**
     * List objects of a type that something has a relation to.
     *
     * @param string $store    ID of the store to list objects for
     * @param string $model    ID of the authorization model to list objects for
     * @param string $type     Type of objects to list
     * @param string $user     ID of the user to list objects for
     * @param string $relation relation to list objects for
     *
     * @throws Throwable
     *
     * @return array<string>|string a list of objects, or an error message
     */
    #[McpTool(name: 'list_objects')]
    public function listObjects(
        string $store,
        string $model,
        string $type,
        string $user,
        string $relation,
    ): string | array {
        $error = $this->checkOfflineMode('Listing objects');
        if (null !== $error) {
            return $error;
        }
        $failure = null;
        $success = [];
        $error = $this->checkRestrictedMode(storeId: $store, modelId: $model);
        if (null !== $error) {
            return $error;
        }
        $this->client->listObjects(store: $store, model: $model, type: $type, relation: $relation, user: $user)
            ->failure(static function (Throwable $e) use (&$failure): void {
                $failure = '❌ Failed to list objects! Error: ' . $e->getMessage();
            })
            ->success(static function (mixed $response) use (&$success): void {
                assert($response instanceof ListObjectsResponseInterface);
                foreach ($response->getObjects() as $object) {
                    $success[] = $object;
                }
            });
        return $failure ?? $success;
    }
    /**
     * List users that have a given relationship with a given object.
     *
     * @param string $store    ID of the store to list users for
     * @param string $model    ID of the authorization model to list users for
     * @param string $object   ID of the object to list users for
     * @param string $relation relation to list users for
     *
     * @throws ClientThrowable
     * @throws InvalidArgumentException
     * @throws ReflectionException
     * @throws Throwable
     *
     * @return array<string>|string a list of users, or an error message
     */
    #[McpTool(name: 'list_users')]
    public function listUsers(
        string $store,
        string $model,
        string $object,
        string $relation,
    ): string | array {
        $error = $this->checkOfflineMode('Listing users');
        if (null !== $error) {
            return $error;
        }
        $failure = null;
        $success = [];
        $called = false;
        $error = $this->checkRestrictedMode(storeId: $store, modelId: $model);
        if (null !== $error) {
            return $error;
        }
        // Create a filter for 'user' type - this is the most common case
        // The API requires exactly 1 user filter
        $userFilter = new UserTypeFilter(type: 'user');
        $userFilters = new UserTypeFilters($userFilter);
        $this->client->listUsers(store: $store, model: $model, object: $object, relation: $relation, userFilters: $userFilters)
            ->failure(static function (Throwable $e) use (&$failure, &$called): void {
                $called = true;
                $failure = '❌ Failed to list users! Error: ' . $e->getMessage();
            })
            ->success(static function (mixed $response) use (&$success, &$called): void {
                $called = true;
                assert($response instanceof ListUsersResponseInterface);
                foreach ($response->getUsers() as $user) {
                    $userIdentifier = $user->getObject();
                    if (null !== $userIdentifier) {
                        if (is_string($userIdentifier)) {
                            $success[] = $userIdentifier;
                        } elseif ($userIdentifier instanceof UserObjectInterface) {
                            // Construct the user identifier from type and id
                            $success[] = $userIdentifier->getType() . ':' . $userIdentifier->getId();
                        }
                    }
                }
            });
        // If neither callback was called, it means the promise chain wasn't resolved
        if (! $called) {
            return '❌ Promise was not resolved';
        }
        return $failure ?? $success;
    }
    /**
     * Revoke permission from something on an object.
     *
     * @param string $store    ID of the store to revoke permission from
     * @param string $model    ID of the authorization model to revoke permission from
     * @param string $user     ID of the user to revoke permission from
     * @param string $relation relation to revoke permission from
     * @param string $object   ID of the object to revoke permission from
     *
     * @throws ClientException
     * @throws ClientThrowable
     * @throws InvalidArgumentException
     * @throws ReflectionException
     * @throws Throwable
     *
     * @return string a success message, or an error message
     */
    #[McpTool(name: 'revoke_permission')]
    public function revokePermission(
        string $store,
        string $model,
        string $user,
        string $relation,
        string $object,
    ): string {
        $error = $this->checkOfflineMode('Revoking permissions');
        if (null !== $error) {
            return $error;
        }
        $failure = null;
        $success = '';
        $error = $this->checkWritePermission('revoke permissions');
        if (null !== $error) {
            return $error;
        }
        $error = $this->checkRestrictedMode(storeId: $store, modelId: $model);
        if (null !== $error) {
            return $error;
        }
        $tuple = new TupleKey(
            user: $user,
            relation: $relation,
            object: $object,
        );
        $this->client->writeTuples(store: $store, model: $model, deletes: new TupleKeys($tuple))
            ->failure(static function (Throwable $e) use (&$failure): void {
                $failure = '❌ Failed to revoke permission! Error: ' . $e->getMessage();
            })
            ->success(static function (mixed $response) use (&$success): void {
                assert($response instanceof WriteTuplesResponseInterface);
                $success = '✅ Permission revoked successfully';
            });
        return $failure ?? $success;
    }
}