Botovis

Security

Role-based access control, authorization, and multi-layer security in Botovis

Botovis implements a multi-layer security model to protect your data. Every request passes through authentication, role resolution, permission checking, and write confirmation before any database operation is executed.

Security Layers

Request → Authentication → Role Resolution → Permission Check → Tool Execution → Write Confirmation
  1. Authentication — User must be authenticated (configurable guard)
  2. Role Resolution — User's role is determined from the configured resolver
  3. Permission Check — The role's CRUD permissions are checked against the target table
  4. Schema Filtering — Only permitted tables/columns are visible in the AI's context
  5. Write Confirmation — Destructive operations require explicit user approval

Authentication Guard

'security' => [
    'guard'        => 'web',
    'require_auth' => true,
],

The guard setting specifies which Laravel authentication guard to use (default: 'web'). Set require_auth to false if you want to allow unauthenticated access.

If require_auth is true and no authenticated user is found, all requests return 401 Unauthorized.

Role Resolvers

Botovis needs to determine a user's role to apply permissions. The role_resolver config is a string that selects the resolver type, with separate config keys for each:

// Reads the role from a database column on the User model
'role_resolver'  => 'attribute',
'role_attribute' => 'role', // $user->role
// Calls a method on the User model
'role_resolver' => 'method',
'role_method'   => 'getRole', // $user->getRole()
// Uses spatie/laravel-permission package
'role_resolver' => 'spatie',
// Uses $user->getRoleNames()->first()
// Uses a custom callback function
'role_resolver' => 'callback',
'role_callback' => function ($user) {
    if ($user->is_super_admin) return 'admin';
    if ($user->department === 'analytics') return 'analyst';
    return 'viewer';
},

Role-Based Permissions

Each role maps tables to their allowed CRUD actions:

'roles' => [
    // Wildcard role — applies to all users by default
    '*' => [
        '*' => ['create', 'read', 'update', 'delete'], // All actions on all tables
    ],

    // Admin — full access to everything
    'admin' => [
        '*' => ['create', 'read', 'update', 'delete'],
    ],

    // Analyst — read-only access to all tables
    'analyst' => [
        '*' => ['read'],
    ],

    // Sales — mixed permissions per table
    'sales' => [
        'orders'    => ['create', 'read', 'update'],
        'customers' => ['read'],
        'products'  => ['read'],
    ],

    // Viewer — read-only on specific tables
    'viewer' => [
        'products'   => ['read'],
        'categories' => ['read'],
    ],
],

Permission Rules

  • Roles are matched by the user's resolved role name
  • Use '*' as a table key to grant access to all whitelisted models
  • CRUD actions: create, read, update, delete
  • The wildcard role '*' is the fallback when no specific role matches
  • Table names must match the actual database table names (not model names)
  • A table not mentioned in a role's config is completely invisible to that user

Write Confirmation

'require_confirmation' => ['create', 'update', 'delete'],

This setting lists which action types require explicit user confirmation before execution. The default requires confirmation for all write operations.

When a confirmed action is triggered:

  1. The agent detects a write tool call
  2. The agent pauses and sends a confirmation event to the widget
  3. The widget shows the user what will be modified, with Confirm and Reject buttons
  4. Only after the user confirms does the tool actually execute
  5. If rejected, the agent receives the rejection and responds accordingly

Set to an empty array [] to disable confirmation entirely.

Strongly Recommended

Keep at least ['create', 'update', 'delete'] in production. Removing action types means the AI can perform those operations without human approval.

Laravel Gates Integration

Enable gate-based authorization:

'use_gates' => true, // default: false

When enabled, Botovis checks Laravel Gates for each table/action combination using the botovis.{table}.{action} naming convention:

// In AuthServiceProvider or a Gate file
Gate::define('botovis.orders.read', function ($user) {
    return $user->hasVerifiedEmail() && $user->is_active;
});

Gate::define('botovis.orders.create', function ($user) {
    return $user->hasRole('admin');
});

You can also add a global gate check to your Botovis route middleware:

// config/botovis.php
'route' => [
    'middleware' => ['web', 'auth', 'can:use-botovis'],
],

Schema Filtering

Botovis automatically filters the database schema based on the current user's permissions:

  • A viewer with access to ['products', 'categories'] will only see those two tables in the AI's context
  • The AI literally cannot reference tables the user doesn't have access to — they don't exist in its schema
  • Relationships pointing to inaccessible tables are excluded
  • This filtering happens at the SecurityContext level before any LLM interaction

SecurityContext

The SecurityContext DTO is built for every request and carries:

SecurityContext {
    ?string $userId       // Authenticated user's ID
    ?string $userRole     // Resolved role name
    array $allowedTables  // Tables visible to this user
    array $permissions    // CRUD permissions per table
    array $metadata       // Additional context (extensible)
}

This object is passed through the entire request lifecycle — from schema filtering to tool execution to the agent loop. The BotovisAuthorizer builds this context using the buildContext() method.

Custom Authorizer

For complete control over authorization, implement the AuthorizerInterface:

use Botovis\Core\Contracts\AuthorizerInterface;
use Botovis\Core\DTO\SecurityContext;

class CustomAuthorizer implements AuthorizerInterface
{
    public function buildContext(): SecurityContext
    {
        $user = auth()->user();

        return new SecurityContext(
            userId: (string) $user->id,
            userRole: $this->determineRole($user),
            allowedTables: $this->getAllowedTables($user),
            permissions: $this->getPermissions($user),
            metadata: ['source' => 'custom'],
        );
    }

    public function authorize(SecurityContext $context, string $action, string $table): bool
    {
        // Your custom authorization logic
        return in_array($action, $context->permissions[$table] ?? []);
    }

    public function filterSchema(SecurityContext $context, array $schema): array
    {
        // Filter schema based on permissions
        return array_filter($schema, fn($table) =>
            in_array($table, $context->allowedTables)
        );
    }
}

Register it in a service provider:

$this->app->singleton(AuthorizerInterface::class, CustomAuthorizer::class);

Best Practices

  1. Start restrictive — Use the wildcard * role with minimal permissions, then add specific roles as needed
  2. Always require confirmation — Keep ['create', 'update', 'delete'] in the require_confirmation array in production
  3. Use the model whitelist — Only register models that should be accessible via the AI
  4. Audit access — Monitor which users are making which queries through your application logs
  5. Guard your routes — Add appropriate middleware (authentication, rate limiting, etc.)
  6. Test role resolution — Use botovis:discover to see what each role can access
  7. Separate environments — Consider different configurations for development vs. production

On this page