Depois de criar seus Resources
php artisan make:filament-resource Produto --panel=app
php artisan db:seed --class=Database\\Seeders\\ShieldPermissionsSeeder
php artisan db:seed --class=Database\\Seeders\\PapeisSeeder
Pacote novo com Resource: a policy precisa ser registrada
O Laravel descobre policy por convenção só para App\Models\*. Um resource de pacote — a trilha de auditoria, os logs de e-mail, as filas — tem modelo em namespace de vendor, e a App\Policies\XPolicy que você escrever para ele não é consultada por nada até alguém chamar Gate::policy(). A permissão aparece na tela de papéis, e não decide.
Foi assim que o kit ficou por várias versões, e a auditoria de aderência ao Blueprint (v0.21) pegou: oito telas do /infra e do /admin abriam com a permissão revogada. A correção é App\Support\PoliciesDeVendor, um mapa modelo => policy registrado no boot. Ao instalar um pacote com resource, acrescente a linha lá — e confira duas coisas no resource do pacote:
$shouldSkipAuthorization = truedesliga a policy inteira (o do Composer Release tinha; o kit o subclassifica comfalsee com a página apontando para a subclasse);canAccess()sobrescrito sem&& parent::canAccess()desliga a policy só para o índice.
tests/Kit/PermissoesDeResourcesTest.php reprova resource novo sem policy registrada e resource que abre com ViewAny revogada — com o nome do resource.
Os dois, nesta ordem, sempre. O primeiro roda shield:generate --all em cada painel e escreve as policies; o segundo recorta a matriz pelo painel em que o Resource está registrado e devolve as permissões aos papéis. Só o primeiro cria a permission e não a entrega a ninguém — a tela continua em 403 para quem não é master_global. Os dois são idempotentes: rodar de novo é operação normal.
Page, Widget e Action novos
Resource é o caso fácil: os dois seeders resolvem. As outras três famílias exigem uma linha de código, porque os defaults do Filament são permissivos — o vendor diz isso em comentário, em Pages/Concerns/CanAuthorizeAccess.php (canAccess() retorna true), em Widget.php (canView() retorna true) e em Actions/Concerns/CanBeAuthorized.php (autorização default null, liberada).
O Shield gera View:{Page} e View:{Widget} por descoberta, o PapeisSeeder entrega aos papéis do painel e a tela de papéis mostra o checkbox — mas nada disso faz a permissão ser consultada. Sem o trait, desmarcar o checkbox não muda nada.
// Page de painel nova
use App\Filament\Concerns\ExigePermissaoDaTela;
class MinhaPage extends Page
{
use ExigePermissaoDaTela;
// Regra local (flag de config, tenancy) vai NO HOOK, nunca sobrescrevendo canAccess():
protected static function regraLocalDeAcesso(): bool
{
return (bool) config('kit.minha_flag');
}
}
// Widget novo
use App\Filament\Concerns\ExigePermissaoDoWidget;
class MeuWidget extends StatsOverviewWidget
{
use ExigePermissaoDoWidget;
// Checagem de fonte opcional vai NO HOOK, nunca sobrescrevendo canView():
protected static function fonteDeDadosDisponivel(): bool
{
return (bool) rescue(fn (): bool => Schema::hasTable('minha_tabela'), false);
}
}
⚠️ Sobrescrever
canAccess()/canView()na classe desliga a permissão em silêncio. Método de classe vence método de trait, sem erro e sem aviso. É por isso que os dois concerns publicam um hook para a regra local, e por issotests/Kit/PermissoesDeTelasTest.phpePermissoesDeWidgetsTest.phptêm um caso que percorre TODAS as classes e reprova a que não consulta.
Action é declaração explícita, porque o Shield não descobre Action nenhuma:
| A Action é de… | A permissão nasce em | E na Action |
|---|---|---|
| Resource (tabela, header, RelationManager) | config('filament-shield.resources.manage') no Resource daquele painel | ->authorize('MinhaAcao:MeuModel') |
| Page | config('filament-shield.custom_permissions') e PapeisSeeder::paineisDasPermissoesCustomizadas() | ->authorize('MinhaAcao:MeuModel') |
A segunda linha tem duas metades porque custom_permissions não conhece painel: sem o mapa do seeder, a chave nova cai em admin, infra, admin_app e panel_user. Chave sem entrada no mapa não vai para papel nenhum (fail-closed) e o caso CT-19 de tests/Kit/PermissoesDeAcoesTest.php fica vermelho nomeando a chave.
⚠️ Em RelationManager, nem a Action NATIVA está coberta.
AttachAction,DetachAction,AssociateActioneDissociateActionsó checamisReadOnly()— o comentário está nogetDefaultActionAuthorizationResponse()do vendor. No kit, o vínculotenant_userque aAttachActioncria é exatamente o queUser::canAccessTenant()consulta para liberar/app/{slug}, então as duas levam->authorize().
Page e Widget de vendor ficam fora: são classes de pacote, sem ponto de extensão. A permissão delas existe no banco e no checkbox, e não é consultada — a barreira é canAccessPanel() mais os gates nomeados de KitServiceProvider (ver-logs, command-center:access, viewPulse, ver-ai-tasks).
RelationManager o Shield não enxerga. A descoberta dele cobre apenas Resources, Pages e Widgets, então nenhuma permission é gerada e a autorização recai na policy do model relacionado. Se esse model já tem Resource em algum painel, não há nada a fazer. Se não tem, crie a policy à mão (
php artisan make:policy) e declare as chaves emconfig('filament-shield.custom_permissions')antes de rodar os seeders — do contrário o RelationManager fica aberto a qualquer um que consiga abrir o Resource pai.
Adicione os dois traits do kit ao que foi gerado:
// No Resource — badge de contagem animado no menu:
use App\Filament\Concerns\BadgeContagemNavegacao;
class ProdutoResource extends Resource
{
use BadgeContagemNavegacao;
}
// Na List page — lembra a largura das colunas escolhida pelo usuário:
use Asmit\ResizedColumn\HasResizableColumn;
class ListProdutos extends ListRecords
{
use HasResizableColumn;
}
Badges de contagem
Todos os Resources do kit já têm badge no menu (Usuários, Agentes de IA, Execuções de IA). A contagem sai de getEloquentQuery(), nunca de Model::count(): a query do resource carrega os escopos que valem para aquele painel, e contar direto no model mostraria um número que a listagem não confirma. Zero não vira badge — um “0” cinza em todo item só polui.
Resources de plugins de terceiros (Auditoria, Logins, Filas, Pacotes do Composer, Comandos, Papéis do Shield, Onboarding) ficam sem badge: getNavigationBadge() é um método estático do resource, e o Filament não oferece API para sobrescrevê-lo de fora — a ResourceConfiguration do painel só permite trocar o slug. Dar badge a eles exigiria estender cada resource de vendor e impedir o plugin de registrar o seu, o que quebra a cada atualização do pacote. Se algum for importante no seu projeto, o caminho é esse — resource por resource, conscientemente.