Como crear un sistema de permisos granular por metacapabilities en PHP en WordPress

Contents

Introducción

Este artículo explica con todo lujo de detalles cómo crear un sistema de permisos granular en WordPress usando metacapabilities. Verás qué son las metacapabilities, cómo mapearlas a capacidades primitivas mediante el filtro map_meta_cap, cómo registrar roles y capacidades, cómo integrar comprobaciones en el REST API y en AJAX, y qué medidas de seguridad y rendimiento debes considerar. Incluyo ejemplos completos en PHP listos para insertar en tu plugin o tema.

Conceptos clave

Qué es una metacapability

En WordPress existen dos tipos de capacidades: las llamadas capabilities primitivas (por ejemplo, edit_posts, publish_posts) que representan permisos atómicos, y las metacapabilities (por ejemplo, edit_post, delete_post) que dependen del contexto del objeto (post, user, term…). Las metacapabilities se mapean a primitivas según la lógica de negocio (por ejemplo, si el usuario es el autor, permitir edit_post con la primitiva edit_posts o una capacidad específica como edit_others_posts).

Por qué usar metacapabilities

  • Permiten lógica contextual (dueño, estado, grupo, meta del post).
  • Fácil implementación de controles finos: un usuario X puede editar posts del tipo Y sólo si….
  • Se integran con la API interna de current_user_can() y con checks automáticos de WordPress.

Fácil vs granular

Las capacidades primitivas sirven para roles simples las metacapabilities permiten reglas complejas (grupos, miembros, atributos de metadatos, expiraciones, etc.). Para sistemas multiusuario o contenido sensible, las metacapabilities son la mejor opción.

Arquitectura propuesta

Ejemplo práctico: construiremos un sistema de permisos para un Custom Post Type llamado document, donde:

  • Cada documento tiene un autor (post_author).
  • Además puede tener una lista de colaboradores guardada en post meta: _document_collaborators (array de IDs de usuario).
  • Soportaremos roles administrativos: document_manager (capacidad manage_documents) y usuarios con capacidades granulares como edit_document, read_document, delete_document.

1) Registrar el Custom Post Type y capacidades primitivas

Cuando registres el CPT deberás declarar el parámetro capability_type y las capacidades personalizadas. Ejemplo de registro:

 edit_document,
        read_post          => read_document,
        delete_post        => delete_document,
        edit_posts         => edit_documents,
        edit_others_posts  => edit_others_documents,
        publish_posts      => publish_documents,
        read_private_posts => read_private_documents,
    )

    args = array(
        label               => Documents,
        public              => true,
        supports            => array(title, editor, author),
        capability_type     => array(document, documents),
        map_meta_cap        => true, // Crucial para usar map_meta_cap
        capabilities        => caps,
    )

    register_post_type(document, args)
})
?>

2) Crear roles y asignar capacidades

Crea un rol específico para gestores de documentos y asigna las capacidades primitivas necesarias.

 true,
        manage_documents => true,
        // capacidades específicas
        edit_documents => true,
        edit_others_documents => true,
        publish_documents => true,
        read_private_documents => true,
    ))

    // Opcional: otorgar capacidades a administrador
    admin = get_role(administrator)
    if (admin) {
        admin->add_cap(manage_documents)
        admin->add_cap(edit_documents)
        admin->add_cap(edit_others_documents)
    }
})
?>

3) Mapear metacapabilities usando map_meta_cap

El paso esencial: enganchar una función al filtro map_meta_cap. Esa función recibirá la metacapability solicitada, el ID del usuario, y argumentos adicionales (p. ej. ID del post). Debes retornar un array de capacidades primitivas requeridas para autorizar la operación. Aquí implementaremos reglas:

  • El autor del documento puede leer/editar su documento si tiene edit_documents o read_documents según el caso.
  • Los colaboradores listados en post meta _document_collaborators pueden editar si el post está en determinado estado.
  • Los managers (capacidad manage_documents) pueden todo.
post_type !== document) {
        return array(do_not_allow)
    }

    // Si el usuario es administrador o tiene manage_documents, dejar que WP valide con esa primitiva
    if (user_can(user_id, manage_documents)) {
        return array(manage_documents)
    }

    // Propietario del documento
    if ((int)post->post_author === (int)user_id) {
        switch (cap) {
            case read_document:
                return array(read_documents)
            case edit_document:
                return array(edit_documents)
            case delete_document:
                return array(delete_documents)
        }
    }

    // Colaboradores almacenados en post meta (array de IDs)
    collabs = get_post_meta(post_id, _document_collaborators, true)
    if (!empty(collabs)  is_array(collabs)  in_array((int)user_id, array_map(intval, collabs), true)) {
        // Los colaboradores pueden leer y editar, pero no eliminar, por ejemplo
        if (cap === read_document) {
            return array(read_documents)
        } elseif (cap === edit_document) {
            return array(edit_documents)
        } else {
            return array(do_not_allow)
        }
    }

    // Por defecto, usuario necesita capacidades de others para editar/eliminar si no es autor
    if (cap === edit_document) {
        return array(edit_others_documents)
    } elseif (cap === delete_document) {
        return array(delete_others_documents)
    } elseif (cap === read_document) {
        // Si el post es público, permitir leer mediante capability read
        if (post->post_status === publish) {
            return array(read)
        }
        return array(do_not_allow)
    }

    return array(do_not_allow)
}, 10, 4)
?>

Notas sobre la implementación

  • Usamos map_meta_cap para convertir una sola metacapability en una o más capacidades primitivas.
  • Retornar do_not_allow es la forma correcta de denegar.
  • Evitar llamadas costosas (consultas pesadas) dentro del hook cachea si es necesario.

4) Añadir/gestionar colaboradores en la UI (ejemplo simple)

Puedes usar campos personalizados en la pantalla de edición para añadir IDs de colaboradores, o crear una metabox para seleccionar usuarios. Aquí un ejemplo mínimo para guardar un array de IDs.


5) Verificaciones en REST API y endpoints personalizados

Si expones el CPT en el REST API o creas endpoints personalizados, debes usar la misma lógica de permisos en la propiedad permission_callback de tus rutas o al definir el endpoint del CPT. Ejemplo de endpoint REST personalizado que usa current_user_can con la metacapability:

d ), array(
        methods => GET,
        callback => function(request) {
            id = (int) request[id]
            post = get_post(id)
            if (! post) {
                return new WP_Error(not_found, Documento no encontrado, array(status => 404))
            }
            return rest_ensure_response(array(
                id => post->ID,
                title => post->post_title,
            ))
        },
        permission_callback => function(request) {
            id = (int) request[id]
            return current_user_can(read_document, id)
        }
    ))
})
?>

6) Comprobaciones en frontend y en AJAX

En el frontend usa current_user_can(edit_document, post_id) antes de mostrar botones u operaciones sensibles. En AJAX asegúrate de sanitizar inputs y verificar nonces y capacidades en handlers.


7) Casos avanzados: grupos, expiraciones y capacidades temporales

Para escenarios más complejos (grupos de usuarios, permisos temporales, roles jerárquicos) sigue estas recomendaciones:

  • Almacena relaciones en tablas o meta bien indexadas. Evita serializaciones grandes en opciones.
  • Cuando verifiques pertenencia a grupos, usa transients para cachear comprobaciones costosas por unos segundos/minutos.
  • Para permisos temporales, añade metadatos con fecha de expiración en map_meta_cap comprueba fecha y devuelve do_not_allow si expiró.
  • Si necesitas capacidades calculadas a gran escala, considera almacenar capacidades agregadas por usuario (sincronizadas) para evitar cálculos on-the-fly en cada request.

8) Seguridad y buenas prácticas

  1. Principio de menor privilegio: otorga sólo las capacidades mínimas necesarias a roles.
  2. Evitar la escalada: no confíes en entradas del cliente siempre verifica capacidades en el servidor (REST/AJAX/Servidor).
  3. Saneamiento y validación: sanitiza IDs, cadenas y metas antes de guardarlas o procesarlas.
  4. Usa nonces en formularios y AJAX para evitar CSRF.
  5. Evita consultas dentro de map_meta_cap que sean muy costosas cachea resultados o precómputalos.
  6. Registro de auditoría: para acciones críticas (borrado, cambios de permisos), registra quién lo hizo y cuándo.

9) Rendimiento y caching

map_meta_cap se llama con frecuencia (cada current_user_can). Para evitar sobrecargar la base de datos:

  • Cachea listas de colaboradores por post en un transient o en un objeto cache persistente con expiración corta.
  • Si haces checks de pertenencia a grupos, precarga la información del usuario una vez por request.
  • Usa object cache (Redis/Memcached) en entornos con alto tráfico.

10) Tests y validación

Implementa tests unitarios con WP_UnitTestCase para validar las reglas de map_meta_cap: casos de autor, colaborador, manager, usuario anónimo y expiraciones. Ejemplo de test que debes implementar en tu suite de pruebas (esquema):

factory->user->create()
        wp_set_current_user(user_id)

        post_id = this->factory->post->create(array(
            post_type => document,
            post_author => user_id,
        ))

        this->assertTrue(current_user_can(edit_document, post_id))
    }
}
?>

Tabla resumen: metacapabilities y primitivas (ejemplo)

Metacapability Posible mapping Comentario
edit_document edit_documents / edit_others_documents / do_not_allow Autor usa edit_documents no autor requiere edit_others_documents o derecho especial
read_document read / read_documents / do_not_allow Documento publicado => read privado => read_documents o permiso especial
delete_document delete_documents / delete_others_documents / do_not_allow Similar a editar pero habitualmente más restrictivo

Consejos finales

Implementar un sistema de permisos granular con metacapabilities te dará control y flexibilidad sobre quién puede hacer qué en tu aplicación WordPress. Las reglas deben ser explícitas, auditables y optimizadas. Empieza por modelar claramente los actores (autor, colaborador, manager), define capacidades primitivas coherentes y documenta el comportamiento de map_meta_cap. Siempre prueba exhaustivamente y aplica caching cuando detectes cuellos de botella.

Recursos útiles



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 *