Secure API Platform resources with security expressions, voters, and operation-level access control
/plugin marketplace add MakFly/superpowers-symfony/plugin install makfly-superpowers-symfony@MakFly/superpowers-symfonyThis skill inherits all available tools. When active, it can use any tool Claude has access to.
<?php
// src/Entity/Post.php
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
#[ApiResource(
operations: [
// Public read access
new GetCollection(),
new Get(),
// Authenticated users can create
new Post(
security: "is_granted('ROLE_USER')",
securityMessage: 'You must be logged in to create posts.',
),
// Only owner or admin can update
new Put(
security: "is_granted('ROLE_ADMIN') or object.getAuthor() == user",
securityMessage: 'You can only edit your own posts.',
),
new Patch(
security: "is_granted('ROLE_ADMIN') or object.getAuthor() == user",
),
// Only admin can delete
new Delete(
security: "is_granted('ROLE_ADMIN')",
securityMessage: 'Only administrators can delete posts.',
),
],
)]
class Post
{
// ...
}
#[ApiResource(
operations: [
new Get(
security: "is_granted('POST_VIEW', object)",
),
new Put(
security: "is_granted('POST_EDIT', object)",
securityMessage: 'You cannot edit this post.',
),
new Delete(
security: "is_granted('POST_DELETE', object)",
),
],
)]
class Post { /* ... */ }
Check security after input is processed:
#[ApiResource(
operations: [
new Post(
// Check before processing
security: "is_granted('ROLE_USER')",
// Check after input is bound to object
securityPostDenormalize: "is_granted('POST_CREATE', object)",
securityPostDenormalizeMessage: 'You cannot create this type of post.',
),
],
)]
class Post { /* ... */ }
Useful when security depends on the input data itself.
// User roles
security: "is_granted('ROLE_USER')"
security: "is_granted('ROLE_ADMIN')"
// Current user
security: "user == object.getOwner()"
security: "object.getAuthor().getId() == user.getId()"
// Object properties
security: "object.isPublished() or object.getAuthor() == user"
security: "object.getStatus() == 'draft' and object.getAuthor() == user"
// Voters
security: "is_granted('EDIT', object)"
security: "is_granted('VIEW', object)"
// Combined conditions
security: "is_granted('ROLE_ADMIN') or (is_granted('ROLE_USER') and object.getAuthor() == user)"
// Request data (for POST/PUT)
security: "is_granted('ROLE_ADMIN') or request.get('category') != 'restricted'"
<?php
// src/Doctrine/CurrentUserExtension.php
namespace App\Doctrine;
use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use App\Entity\Post;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bundle\SecurityBundle\Security;
final class CurrentUserExtension implements QueryCollectionExtensionInterface
{
public function __construct(
private Security $security,
) {}
public function applyToCollection(
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
?Operation $operation = null,
array $context = []
): void {
// Only filter Post resources
if ($resourceClass !== Post::class) {
return;
}
// Admins see everything
if ($this->security->isGranted('ROLE_ADMIN')) {
return;
}
$user = $this->security->getUser();
$alias = $queryBuilder->getRootAliases()[0];
if ($user) {
// Authenticated: see published + own drafts
$queryBuilder
->andWhere(sprintf(
'%s.isPublished = true OR %s.author = :currentUser',
$alias,
$alias
))
->setParameter('currentUser', $user);
} else {
// Anonymous: only published
$queryBuilder
->andWhere(sprintf('%s.isPublished = true', $alias));
}
}
}
use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface;
final class CurrentUserExtension implements
QueryCollectionExtensionInterface,
QueryItemExtensionInterface
{
public function applyToItem(
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
array $identifiers,
?Operation $operation = null,
array $context = []
): void {
// Same logic as collection
$this->addWhere($queryBuilder, $resourceClass);
}
public function applyToCollection(/* ... */): void
{
$this->addWhere($queryBuilder, $resourceClass);
}
private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void
{
// Shared filtering logic
}
}
Hide fields based on permissions:
<?php
// src/Entity/User.php
use Symfony\Component\Serializer\Attribute\Groups;
class User
{
#[Groups(['user:read', 'admin:read'])]
private ?int $id = null;
#[Groups(['user:read', 'admin:read'])]
private string $name;
// Only visible to admins and the user themselves
#[Groups(['user:owner', 'admin:read'])]
private string $email;
// Only visible to admins
#[Groups(['admin:read'])]
private array $roles;
// Never exposed
private string $password;
}
With context builder for dynamic groups:
<?php
// src/Serializer/UserContextBuilder.php
final class UserContextBuilder implements SerializerContextBuilderInterface
{
public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array
{
$context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
if ($this->security->isGranted('ROLE_ADMIN')) {
$context['groups'][] = 'admin:read';
}
// Check if viewing own profile
$resourceId = $request->attributes->get('id');
$currentUser = $this->security->getUser();
if ($currentUser && $currentUser->getId() == $resourceId) {
$context['groups'][] = 'user:owner';
}
return $context;
}
}
# config/packages/security.yaml
security:
firewalls:
api:
pattern: ^/api
stateless: true
jwt: ~
access_control:
- { path: ^/api/login, roles: PUBLIC_ACCESS }
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
use Symfony\Component\RateLimiter\Attribute\RateLimit;
#[ApiResource(
operations: [
new Post(
security: "is_granted('ROLE_USER')",
),
],
)]
#[RateLimit(limit: 10, interval: '1 minute')]
class Comment { /* ... */ }
Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.
Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.