Como restringir endpoints REST por capability en PHP en WordPress

Contents

Introducción

Este artículo explica en detalle cómo restringir endpoints del REST API de WordPress mediante capacidades (capabilities) usando PHP. Verás las diferencias entre usar permission_callback al registrar rutas nuevas y cómo modificar endpoints ya existentes del núcleo o de plugins para forzar comprobaciones de capacidad. Incluiré ejemplos prácticos listos para usar en un plugin o en functions.php de tu tema (mejor en un plugin), y también recomendaciones de seguridad y pruebas.

Conceptos clave

Antes de entrar en el código conviene repasar conceptos básicos:

  • REST endpoint: una ruta registrada en el REST API (por ejemplo /wp/v2/posts).
  • permission_callback: función que se ejecuta cuando se solicita un endpoint. Debe devolver true (permitir) o un WP_Error (o false) para denegar.
  • Capabilities: permisos del sistema de roles de WordPress (por ejemplo: edit_posts, delete_posts, manage_options). Se comprueban con current_user_can( capability, args ).
  • Hooks relevantes: register_rest_route() para endpoints nuevos y el filtro rest_endpoints para modificar endpoints ya registrados.

Por qué usar permission_callback y no filtrar la respuesta

  • permission_callback evita que el endpoint llegue a ejecutarse, por lo que no se gaste CPU en generar respuestas para usuarios no autorizados.
  • Es la forma recomendada por la API REST de WP para controlar acceso por petición.
  • Si un endpoint ya existe (core o plugin) y no tiene permission_callback o su callback no encaja con tus necesidades, puedes modificarlo con el filtro rest_endpoints antes de que se sirva.

Ejemplo 1 — Registrar un endpoint personalizado restringido por capability

Ejemplo: crear un endpoint que devuelva datos administrativos solo si el usuario tiene la capability manage_options (normalmente administradores).

 GET,
        callback            => function( WP_REST_Request request ) {
            // Código para recuperar las estadísticas
            return rest_ensure_response( array(
                users => count_users(),
                site_url => get_site_url(),
            ) )
        },
        permission_callback => function( WP_REST_Request request ) {
            // Permitir solo a usuarios con manage_options
            if ( ! current_user_can( manage_options ) ) {
                return new WP_Error( rest_forbidden, __( No autorizado. ), array( status => 403 ) )
            }
            return true
        },
    ) )
} )
?>

Este ejemplo es claro: si el current_user no tiene la capability, el resultado será un error 403. Usar WP_Error permite devolver un mensaje y código HTTP correcto.

Ejemplo 2 — Restringir endpoints existentes (core o plugins) usando rest_endpoints

Si necesitas cambiar el comportamiento de endpoints ya registrados (por ejemplo /wp/v2/posts), el filtro rest_endpoints te permite interceptar y ajustar los permission_callback. Debes engancharte con prioridad alta o al menos tras el registro de las rutas.

[d] )
    if ( isset( endpoints[/wp/v2/posts] ) ) {
        foreach ( endpoints[/wp/v2/posts] as route ) {
            // Reemplazamos o envolvemos el permission_callback existente
            route[permission_callback] = function( request ) {
                // Solo usuarios con capacidad edit_posts podrán usar la colección posts
                if ( ! current_user_can( edit_posts ) ) {
                    return new WP_Error( rest_forbidden, __( Acceso restringido a /wp/v2/posts ), array( status => 403 ) )
                }
                return true
            }
        }
    }

    // Ejemplo para rutas con ID:
    if ( isset( endpoints[/wp/v2/posts/(?P[d] )] ) ) {
        foreach ( endpoints[/wp/v2/posts/(?P[d] )] as route ) {
            route[permission_callback] = function( request ) {
                post_id = (int) request[id]
                // Permitir si puede editar el post concreto
                if ( current_user_can( edit_post, post_id )  current_user_can( edit_others_posts ) ) {
                    return true
                }
                return new WP_Error( rest_forbidden, __( No tienes permiso para este post. ), array( status => 403 ) )
            }
        }
    }

    return endpoints
} )
?>

Notas:

  • Las claves del array endpoints corresponden a las rutas registradas. Algunas rutas tienen parámetros (regex) y aparecen con esa notación.
  • Al sobrescribir permission_callback te aseguras de imponer la política que necesites, pero debes ser cuidadoso con rutas utilizadas por otros plugins o por el core.

Ejemplo 3 — Comprobar capabilities dinámicas (por parámetro)

Puede que quieras que el endpoint reciba una capability requerida en su propia definición o que varíe según parámetros. Aquí un ejemplo donde se dice qué capability se requiere según un parámetro del request:

[d] ), array(
        methods => GET,
        callback => function( request ) {
            id = (int) request[resource_id]
            // Devolver los datos del recurso
            return rest_ensure_response( array( id => id, foo => bar ) )
        },
        permission_callback => function( request ) {
            resource_id = (int) request[resource_id]

            // Ejemplo: si el recurso es privado requerimos read_private_posts, si es público lo permitimos
            is_private = get_post_meta( resource_id, _is_private, true )
            if ( is_private ) {
                if ( ! current_user_can( read_private_posts ) ) {
                    return new WP_Error( rest_forbidden, __( No autorizado para recursos privados. ), array( status => 403 ) )
                }
            }
            return true
        }
    ) )
} )
?>

Buenas prácticas y consideraciones

  • Usa capability en lugar de rol: comprobar capabilities es más flexible (roles pueden cambiar).
  • Devuelve WP_Error con códigos HTTP claros: facilita debugeo y control en clientes front-end.
  • No confíes en front-end JS para seguridad: las comprobaciones deben hacerse en servidor.
  • Caching y performance: permission_callback se ejecuta por petición. Las comprobaciones con current_user_can son baratas, pero evita consultas innecesarias si puedes.
  • Autenticación: current_user_can depende del usuario autenticado. Para peticiones desde apps externas, asegúrate que la autenticación (cookie, Application Passwords, OAuth) esté funcionando.
  • Orden de carga: rest_endpoints se debe usar con cuidado en plugins que se cargan antes o después del registro de endpoints objetivo. Hooks en rest_api_init o filtros pueden necesitar prioridade ajustada.

Errores comunes

  1. No devolver un WP_Error en el permission_callback: si devuelves false WordPress puede responder con un 401 o 403 inesperado WP_Error permite personalizar el mensaje y el status.
  2. Modificar rutas del core sin probar a fondo: podrías bloquear funciones necesarias para el editor o el front.
  3. Asumir que current_user_can admite cualquier string: algunas capabilities requieren mapa meta (por ejemplo capabilities dinámicas tipo edit_post con ID).

Comprobaciones más específicas: capabilities con argumentos (ej. edit_post)

Para capacidades que aceptan argumentos, pasa el ID correcto a current_user_can. Ejemplo para permitir edición sólo si el usuario puede editar ese post:

 403 ) )
}
?>

Cómo probar tus cambios

  • Usa Insomnia, Postman o curl y autentica con cookies (via navegador) o Application Passwords para representar distintos usuarios.
  • Prueba usuarios con y sin la capability esperada para confirmar el comportamiento.
  • Revisar respuestas HTTP: 200 (ok), 401/403 (no autorizado) según el caso.

Enlaces útiles

Resumen

La forma correcta de restringir endpoints por capability en WordPress es usar permission_callback cuando registras nuevas rutas y, para endpoints ya existentes del core o de terceros, usar el filtro rest_endpoints para sobreescribir o envolver permission_callback. Utiliza current_user_can para comprobar capabilities (incluyendo las que aceptan argumentos, como edit_post) y devuelve WP_Error con códigos HTTP correctos para un comportamiento claro en el cliente. Todo esto mantiene tu API segura y eficiente.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *