La seguridad de tu aplicación supera la simple concesión o denegación de acceso a nivel superficial. Como desarrollador, necesitas implementar autorización de grano fino (FGA) para gestionar permisos a un nivel más detallado y granular.

FGA te permite establecer controles de acceso detallados que especifican quién puede hacer qué y bajo qué condiciones.

En este tutorial, aprenderás cómo implementar autorización de grano fino en Java y Spring Boot utilizando Permit.io.

Aquí está el código fuente (recuerda dármelo una estrella ⭐).

Espero que haya disfrutado de mi anterior blog sobre la construcción de una aplicación de video conferencia personalizada con Stream y Next.js. Estos blogs reflejan mi viaje en la creación de DevTools Academy, una plataforma diseñada para ayudar a los desarrolladores a descubrir herramientas de desarrollo impresionantes.

Este tutorial es otro esfuerzo para presentarte a una herramienta de desarrollo muy útil que exploré recientemente.

Tabla de contenidos:

¿Qué es Permitir?

Permit.io es una solución de autorización a nivel de aplicación completa, de cascada y lista para usar que le permite implementar una capa de autorización segura, flexible en solo minutos, para que pueda centrarse en lo que más importa.

Prerequisitos

Para comprender completamente el tutorial, necesita tener un conocimiento básico de Java y Spring Boot. También necesitará lo siguiente:

  • Permit.io: Una herramienta de desarrollo que simplifica la implementación de FGA.

  • Spring Boot Starter Web: Proporciona componentes básicos para la construcción de aplicaciones web, incluyendo API RESTful.

  • Gradle: Una herramienta de construcción para la gestión de dependencias.

  • JDK 11 o posterior: La versión del Kit de Desarrollo de Java necesaria para compilar y ejecutar su aplicación Spring Boot.

  • Postman o cURL: Herramientas para probar sus puntos finales de API.

¿Qué es la Autorización Finamente Granular?

La autorización finamente granular ofrece un control de acceso a los recursos determinando quién puede acceder a ellos, en qué grado y bajo condiciones específicas.

Contrariamente a la autorización de granularidad coarse (que maneja el acceso basado en categorías como roles de usuario como “admin” o “usuario“), la autorización de granularidad fina le ofrece la flexibilidad para definir el acceso a un nivel muy detallado, para recursos o acciones específicos e incluso atributos.

En Autorización de Granularidad Fina existen 3 tipos de modelos de política para la gestión de autorización; Control de Acceso Basado en Roles (RBAC), Control de Acceso Basado en Atributos (ABAC), y Control de Acceso Basado en Relaciones (ReBAC).

Vamos a echar un vistazo a cada una de estas abordadas y ver cómo puede implementarlas en su aplicación.

Control de Acceso Basado en Roles (RBAC)

RBAC es un enfoque de seguridad que controla el acceso a recursos basado en los roles de los usuarios dentro de una organización. Este modelo simplifica las permisos organizando a los usuarios en roles y gestionando el control de acceso de acuerdo con estos roles definidos.

Conceptos clave en RBAC:

Usuarios: Personas que usan el sistema, como empleados o clientes.

Roles: Un conjunto de permisos o privilegios de acceso asignado a un grupo de usuarios basado en sus responsabilidades o tareas, como admin, gerente o cliente.

Permisos: Los derechos concedidos a los usuarios para interactuar con los recursos, como leer, escribir o eliminar.

Control de Acceso Basado en Atributos (ABAC)

ABAC es un modelo de control de acceso versátil y adaptable que decide quién puede o no puede acceder a los recursos basado en atributos, como detalles del usuario. El modelo ABAC te permite definir una autorización detallada basada en atributos del usuario.

Conceptos clave en ABAC:

Atributos: Características o propiedades utilizadas para tomar decisiones de control de acceso. Los atributos generalmente se categorizan en:

  • Atributos del usuario: Información sobre el usuario (por ejemplo, rol, departamento, título laboral, edad, etc.).

  • Atributos del recurso: Características del recurso (por ejemplo, tipo de archivo, nivel de clasificación de datos, fecha de creación, propietario).

  • Atributos de acción: La acción que el usuario intenta realizar (por ejemplo, leer, escribir, eliminar, aprobar).

  • Atributos ambientales: Información contextual sobre la solicitud de acceso (por ejemplo, hora del día, ubicación, tipo de dispositivo, dirección IP).

Control de Acceso Basado en Relaciones (ReBAC)

ReBAC es un sistema de control de acceso que concede permisos para acceder a recursos basado en las relaciones entre entidades dentro de un sistema. El enfoque destaca la definición y la gestión de control de acceso mediante la representación de cómo los usuarios se relacionan con los recursos y otras entidades, como organizaciones o grupos.

Conceptos clave de ReBAC:

Entidades: Usuarios, recursos (como archivos y documentos), y otras entidades, como grupos o unidades organizativas.

Relaciones: Las conexiones que especifican la relación entre dos entidades. Un usuario podría ser el “propietario” de un documento o un “miembro” de un equipo, por ejemplo.

Políticas: Reglas que usan relaciones para determinar los derechos de acceso. Un usuario puede acceder a un recurso o ejecutar una acción en él si tiene una relación particular con él.

Cómo Implementar Autorización Finamente Granular

Ahora que tienes un entendimiento básico de RBAC, ABAC, y ReBAC, veamos cómo podemos implementar estos modelos en una aplicación de comercio electrónico.

Implementando Control de Acceso Basado en Roles

Paso 1: Vaya a Permit.io, y luego cree una cuenta y su espacio de trabajo.

Por defecto, debería ver un proyecto que incluye dos entornos: Desarrollo y Producción.

💡
Note: You need to define and test your policies in the development environment before deploying them to production.

Paso 2: Cree un recurso llamado Productos. Para crear el recurso, abra la pestaña Política en la barra lateral izquierda y luego abra la pestaña Recursos en la parte superior. Después, haga clic en el botón Crear un Recurso y cree un recurso llamado Productos con acciones leer, crear, actualizar y eliminar.

Paso 3: Cree otro recurso llamado Reviews con acciones leer, crear, actualizar y eliminar.

Paso 4: Abra la pestaña Editor de Política. Verá que se crearon 3 roles llamados admin, editor y visor.

  • El rol de administrador tiene permiso para crear, eliminar, leer o actualizar un producto o una reseña.

  • El rol editor tiene permiso para crear, leer o actualizar un producto o una reseña pero no para eliminar ninguna.

  • El rol viewer tiene permiso para crear y leer un producto o una reseña pero no para eliminar ni actualizar ninguno.

Implementando Control de Acceso Basado en Atributos

Paso 1: Abre la pestaña Recursos, luego haz clic en el botón Agregar Atributos.

  • Agrega un atributo llamado vendedor

  • Agrega un atributo llamado cliente

Paso 2: Abre la pestaña de Reglas ABAC, luego crea un nuevo Conjunto de Recursos ABAC llamado Productos Propios que depende del recurso Productos. Después, agrega una condición que otorgue permisos solo al usuario que creó un producto basado en el atributo vendedor.

Paso 3: Crea otro Conjunto de Recursos ABAC llamado Reseñas Propias que depende del recurso Reseñas.

Implementando Control de Acceso Basado en Relaciones

Paso 1: Abre la pestaña de Recursos y edita el recurso Productos. Agrega el rol vendedor en la sección de opciones ReBAC. Luego establece productos como padre de reseñas en la sección de relaciones.

Paso 2: Edita el recurso de Revisión agregando el rol de cliente en la sección de opciones de ReBAC, como se muestra abajo:

Paso 3: Vaya a la pestaña de Política Editor y agregue:

  • rol vendedor permiso para actualizar y eliminar sus propios productos.

  • rol cliente permiso para actualizar y eliminar sus propias reseñas sobre los productos.

Cómo implementar FGA en Java y SpringBoot

Ahora que hemos definido políticas de RBAC, ABAC y ReBAC en la interfaz web de Permit.io, vamos a aprender cómo aplicarlas en una aplicación de Sistema de Gestión de Comercio Electrónico utilizando la API de Permit.io.

Van a aparecer muchas líneas de código, así que asegúrate de leer las extensas comentarios que he dejado en cada bloque de código. Estos te ayudarán a comprender mejor lo que ocurre en este código.

Paso 1: Configuración de la Aplicación de Comercio Electrónico

Para configurar la aplicación de comercio electrónico y git clone el código fuente.

git clone https://github.com/tyaga001/java-spring-fine-grained-auth.git
💡
Then open the code in your Java IDE. I used JetBrains for all my work.

Instalación del paquete SDK Permit

Para instalar el SDK de Permit, agregas el SDK bajo el bloque de dependencias en el archivo build.graddle.

## Dependencies

To set up the necessary dependencies for your Spring Boot project, include the following in your `build.gradle` file:

```groovy
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

    // Agregue esta línea para instalar el SDK de Java de Permit.io en su proyecto
    implementation 'io.permit:permit-sdk-java:2.0.0'
}

Inicializando el SDK Permit

Puede inicializar el Cliente SDK de Permit usando el código de abajo:

package com.boostmytool.store.config;

import io.permit.sdk.Permit;
import io.permit.sdk.PermitConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration  // Marca esta clase como una clase de configuración para Spring IoC
public class PermitClientConfig {

    @Value("${permit.api-key}")  // Inyecta la clave de API de Permit de las propiedades de la aplicación
    private String apiKey;

    @Value("${permit.pdp-url}")  // Inyecta la URL de PDP (Policy Decision Point) de Permit de las propiedades de la aplicación
    private String pdpUrl;

    /**
     * Crea una instancia de cliente de Permit con configuración personalizada
     * @return Instancia del cliente de Permit
     */
    @Bean
    public Permit permit() {
        return new Permit(
                new PermitConfig.Builder(apiKey)  // Inicializar PermitConfig con la clave de API
                        .withPdpAddress(pdpUrl)   // Establecer la dirección del PDP
                        .withDebugMode(true)      // Activar el modo de depuración para un registro detallado
                        .build()                  // Construir el objeto PermitConfig
        );
    }
}

Sincronizando Usuarios con el SDK

Para comenzar a aplicar permisos, primero debe sincronizar un usuario con Permit, y luego asignarles un rol.

En el código de abajo, la clase UserService proporciona métodos para el inicio de sesión de usuarios, registro, asignación de roles y autorización, con manejo de excepciones para posibles errores al interactuar con la API de Permit.

package com.boostmytool.store.service;

import com.boostmytool.store.exception.ForbiddenAccessException;
import com.boostmytool.store.exception.UnauthorizedException;
import io.permit.sdk.Permit;
import io.permit.sdk.api.PermitApiError;
import io.permit.sdk.api.PermitContextError;
import io.permit.sdk.enforcement.Resource;
import io.permit.sdk.enforcement.User;
import org.springframework.stereotype.Service;

import java.io.IOException;

@Service  // Marca esta clase como un servicio de Spring, haciéndola una candidata para el escaneo de componentes
public class UserService {
    private final Permit permit;

    // Inyección de constructor para el SDK de Permit
    public UserService(Permit permit) {
        this.permit = permit;
    }

    /**
     * Simula el inicio de sesión de un usuario creando y devolviendo un objeto de usuario de Permit.
     * 
     * @param key Clave única del usuario
     * @return Objeto de usuario
     */
    public Object login(String key) {
        return new User.Builder(key).build();
    }

    /**
     * Manipula el registro de usuarios creando y sincronizando un nuevo usuario de Permit.
     * 
     * @param key Clave única del usuario
     * @return Usuario creado y sincronizado
     */
    public User signup(String key) {
        var user = new User.Builder(key).build();
        try {
            permit.api.users.sync(user);  // Sincroniza el nuevo usuario con el servicio de Permit
        } catch (PermitContextError | PermitApiError | IOException e) {
            throw new RuntimeException("Failed to create user", e);  // Maneja excepciones durante la creación de usuarios
        }
        return user;
    }

    /**
     * Asigna un rol al usuario dentro del entorno "default".
     * 
     * @param user Objeto de usuario al que se le asignará el rol
     * @param role Rol a asignar
     */
    public void assignRole(User user, String role) {
        try {
            permit.api.users.assignRole(user.getKey(), role, "default");  // Asigna el rol en el entorno "default"
        } catch (PermitApiError | PermitContextError | IOException e) {
            throw new RuntimeException("Failed to assign role to user", e);  // Maneja excepciones durante la asignación de roles
        }
    }

    /**
     * Comprueba si el usuario está autorizado para realizar una acción específica sobre un recurso.
     * 
     * @param user Objeto de usuario solicitando la autorización
     * @param action Acción que se debe autorizar
     * @param resource Recurso sobre el que se realizará la acción
     * @throws UnauthorizedException Si el usuario no está logueado
     * @throws ForbiddenAccessException Si el usuario se le niega el acceso
     */
    public void authorize(User user, String action, Resource resource) {
        if (user == null) {
            throw new UnauthorizedException("Not logged in");  // Lanza excepción si el usuario no está logueado
        }
        try {
            var permitted = permit.check(user, action, resource);  // Realiza la comprobación de autorización
            if (!permitted) {
                throw new ForbiddenAccessException("Access denied");  // Lanza excepción si se niega el acceso
            }
        } catch (PermitApiError | IOException e) {
            throw new RuntimeException("Failed to authorize user", e);  // Maneja excepciones durante la autorización
        }
    }
}

En el código de abajo, la clase UserController expone puntos finales de API REST para el registro de usuarios y la asignación de roles. Interactúa con la clase UserService para manejar la lógica de negocio relacionada con los usuarios y proporciona respuestas HTTP apropiadas.

package com.boostmytool.store.controllers;

import com.boostmytool.store.exception.UnauthorizedException;
import com.boostmytool.store.service.UserService;
import io.permit.sdk.enforcement.User;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController  // Indica que esta clase maneja solicitudes HTTP y devuelve respuestas en formato JSON
@RequestMapping("/api/users")  // Ruta de base URL para todas las operaciones relacionadas con los usuarios
public class UserController {
    private final UserService userService;

    // Inyección de constructor de UserService, que contiene la lógica de negocio para las operaciones de usuario
    public UserController(UserService userService) {
        this.userService = userService;
    }

    /**
     * Maneja solicitudes de registro de usuarios.
     * Punto final: POST /api/users/signup
     * 
     * @param key Clave única para el nuevo usuario
     * @return Objeto User creado
     */
    @PostMapping("/signup")
    public User signup(@RequestBody String key) {
        return userService.signup(key);  // Llama al método signup en UserService para crear un nuevo usuario
    }

    /**
     * Maneja la asignación de un rol al usuario conectado.
     * Punto final: POST /api/users/assign-role
     * 
     * @param request Solicitud HTTP, utilizada para recuperar el usuario actual
     * @param role Rol a asignar al usuario actual
     */
    @PostMapping("/assign-role")
    public void assignRole(HttpServletRequest request, @RequestBody String role) {
        // Recupera el usuario actual de los atributos de la solicitud
        User currentUser = (User) request.getAttribute("user");

        // Lanza una excepción si el usuario no está conectado
        if (currentUser == null) {
            throw new UnauthorizedException("Not logged in");
        }

        // Asigna el rol especificado al usuario actual
        userService.assignRole(currentUser, role);
    }
}

Creando Punto de Aplicación de Política de RBAC, ABAC y ReBAC

En el código de abajo, la clase ProductService gestiona las operaciones CRUD para productos y reseñas, manejando permisos y roles a través de la API Permit.

Cada operación incluye chequeos de autorización de usuario, con manejo apropiado de excepciones para errores de la API Permit y escenarios de recurso no encontrado.

package com.boostmytool.store.service;

import com.boostmytool.store.exception.ResourceNotFoundException;
import com.boostmytool.store.model.Product;
import com.boostmytool.store.model.Review;
import io.permit.sdk.Permit;
import io.permit.sdk.api.PermitApiError;
import io.permit.sdk.api.PermitContextError;
import io.permit.sdk.enforcement.Resource;
import io.permit.sdk.enforcement.User;
import io.permit.sdk.openapi.models.RelationshipTupleCreate;
import io.permit.sdk.openapi.models.ResourceInstanceCreate;
import io.permit.sdk.openapi.models.RoleAssignmentCreate;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

@Service  // Marca esta clase como un servicio de Spring
public class ProductService {

    private final List<Product> products = new ArrayList<>();  // Lista en memoria para almacenar productos
    private final AtomicInteger productIdCounter = new AtomicInteger();  // Contador para generar identificadores únicos de productos
    private final AtomicInteger reviewIdCounter = new AtomicInteger();   // Contador para generar identificadores únicos de reseñas

    // Constructores para instancias de recursos de permisos (producto y reseña)
    private final Resource.Builder productResourceBuilder = new Resource.Builder("product");
    private final Resource.Builder reviewResourceBuilder = new Resource.Builder("review");

    private final UserService userService;  // Servicio para manejar operaciones relacionadas con usuarios
    private final Permit permit;  // Instancia de SDK de Permiso para manejar autorización y gestión de recursos

    // Constructor para inyectar dependencias
    public ProductService(UserService userService, Permit permit) {
        this.userService = userService;
        this.permit = permit;
    }

    // Método para autorizar a un usuario para una acción específica en un recurso
    private void authorize(User user, String action, Resource resource) {
        userService.authorize(user, action, resource);
    }

    // Autoriza a un usuario a realizar una acción en un producto específico
    private void authorize(User user, String action, Product product) {
        var attributes = new HashMap<String, Object>();
        attributes.put("vendor", product.getVendor());  // Agrega el atributo de proveedor al producto
        userService.authorize(user, action, productResourceBuilder.withKey(product.getId().toString()).withAttributes(attributes).build());
    }

    // Autoriza a un usuario a realizar una acción en una reseña específica
    private void authorize(User user, String action, Review review) {
        var attributes = new HashMap<String, Object>();
        attributes.put("customer", review.getCustomer());  // Agrega el atributo de cliente a la reseña
        userService.authorize(user, action, reviewResourceBuilder.withKey(review.getId().toString()).withAttributes(attributes).build());
    }

    // Obtiene un producto por su ID, lanza una excepción si no se encuentra
    private Product getProductById(int id) {
        return products.stream().filter(product -> product.getId().equals(id))
                .findFirst().orElseThrow(() -> new ResourceNotFoundException("Product with id " + id + " not found"));
    }

    // Obtiene todos los productos, verifica si el usuario está autorizado para "leer" productos
    public List<Product> getAllProducts(User user) {
        authorize(user, "read", productResourceBuilder.build());  // El usuario debe tener permiso de "lectura"
        return new ArrayList<>(products);  // Devuelve una copia de la lista de productos
    }

    // Obtiene un producto por su ID, verifica si el usuario está autorizado para "leer" el producto
    public Product getProduct(User user, int id) {
        authorize(user, "read", productResourceBuilder.build());
        return getProductById(id);
    }

    // Agrega un nuevo producto, autoriza al usuario y crea instancias de recursos y asignaciones de roles en Permit
    public Product addProduct(User user, String content) {
        authorize(user, "create", productResourceBuilder.build());  // Verifica si el usuario puede crear un producto
        Product product = new Product(productIdCounter.incrementAndGet(), user.getKey(), content);  // Crea un nuevo producto

        try {
            // Crea una instancia de recurso en Permit y asigna el rol "proveedor" al usuario para este producto
            permit.api.resourceInstances.create(new ResourceInstanceCreate(product.getId().toString(), "product").withTenant("default"));
            permit.api.roleAssignments.assign(new RoleAssignmentCreate("vendor", user.getKey()).withResourceInstance("product:" + product.getId()).withTenant("default"));
        } catch (IOException | PermitApiError | PermitContextError e) {
            throw new RuntimeException("Failed to create resource instance or role assignment: " + e.getMessage());
        }

        products.add(product);  // Agrega el producto a la lista en memoria
        return product;
    }

    // Actualiza el contenido de un producto, verifica si el usuario está autorizado para "actualizar" el producto
    public Product updateProduct(User user, int id, String content) {
        Product product = getProductById(id);  // Obtiene el producto por su ID
        authorize(user, "update", product);  // Verifica si el usuario puede actualizar el producto
        product.setContent(content);  // Actualiza el contenido del producto
        return product;
    }

    // Elimina un producto, verifica si el usuario está autorizado para "eliminar" el producto
    public void deleteProduct(User user, int id) {
        boolean isDeleted = products.removeIf(product -> {
            if (product.getId().equals(id)) {
                authorize(user, "delete", product);  // Verifica si el usuario puede eliminar el producto
                return true;
            } else {
                return false;
            }
        });

        if (!isDeleted) {
            throw new ResourceNotFoundException("Product with id " + id + " not found");
        }

        try {
            permit.api.resourceInstances.delete("product:" + id);  // Elimina la instancia de recurso del producto de Permit
        } catch (IOException | PermitApiError | PermitContextError e) {
            throw new RuntimeException(e);
        }
    }

    // Agrega una reseña a un producto, crea una instancia de recurso y una relación en Permit
    public Review addReview(User user, int productId, String content) {
        authorize(user, "create", reviewResourceBuilder.build());  // Verifica si el usuario puede crear una reseña
        Product product = getProductById(productId);  // Obtiene el producto por su ID
        Review review = new Review(reviewIdCounter.incrementAndGet(), user.getKey(), content);  // Crea una nueva reseña

        try {
            // Crea una instancia de recurso para la reseña y establece la relación con el producto
            permit.api.resourceInstances.create(new ResourceInstanceCreate(review.getId().toString(), "review").withTenant("default"));
            permit.api.relationshipTuples.create(new RelationshipTupleCreate("product:" + productId, "parent", "review:" + review.getId()));
        } catch (IOException | PermitApiError | PermitContextError e) {
            throw new RuntimeException(e);
        }

        product.addReview(review);  // Agrega la reseña al producto
        return review;
    }

    // Actualiza el contenido de una reseña, verifica si el usuario está autorizado para "actualizar" la reseña
    public Review updateReview(User user, int productId, int reviewId, String content) {
        Product product = getProductById(productId);  // Obtiene el producto por su ID
        Review review = product.getReviews().stream().filter(c -> c.getId().equals(reviewId))
                .findFirst().orElseThrow(() -> new ResourceNotFoundException("Review with id " + reviewId + " not found"));

        authorize(user, "update", review);  // Verifica si el usuario puede actualizar la reseña
        review.setContent(content);  // Actualiza el contenido de la reseña
        return review;
    }

    // Elimina una reseña, verifica si el usuario está autorizado para "eliminar" la reseña
    public void deleteReview(User user, int productId, int reviewId) {
        Product product = getProductById(productId);  // Obtiene el producto por su ID
        boolean isDeleted = product.getReviews().removeIf(review -> {
            if (review.getId().equals(reviewId)) {
                authorize(user, "delete", review);  // Verifica si el usuario puede eliminar la reseña
                return true;
            } else {
                return false;
            }
        });

        if (!isDeleted) {
            throw new ResourceNotFoundException("Review with id " + reviewId + " not found");
        }

        try {
            permit.api.resourceInstances.delete("review:" + reviewId);  // Elimina la instancia de recurso de la reseña de Permit
        } catch (IOException | PermitApiError | PermitContextError e) {
            throw new RuntimeException(e);
        }
    }
}

En el código que se muestra a continuación, la clase ProductController maneja solicitudes HTTP relacionadas con productos y sus reseñas. Expose puntos finales para la gestión de productos (como creando, actualizando, eliminando y recuperando) y para la gestión de reseñas de productos.

package com.boostmytool.store.controllers;

import com.boostmytool.store.model.Product;
import com.boostmytool.store.model.Review;
import com.boostmytool.store.service.ProductService;
import io.permit.sdk.enforcement.User;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController  // Indica que esta clase es un controlador REST de Spring
@RequestMapping("/api/products")  // URL base para todos los endpoints en este controlador
public class ProductController {

    private final ProductService productService;  // Instancia de ProductService para manejar operaciones relacionadas con productos

    @Autowired  // Autoconfigura el bean de ProductService automáticamente
    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    // Solicitud GET para recuperar todos los productos
    @GetMapping
    public List<Product> getAllProducts(HttpServletRequest request) {
        User currentUser = (User) request.getAttribute("user");  // Obtiene el usuario autenticado de la solicitud
        return productService.getAllProducts(currentUser);  // Llama a ProductService para obtener todos los productos para el usuario
    }

    // Solicitud GET para recuperar un producto por su ID
    @GetMapping("/{id}")
    public Product getProductById(HttpServletRequest request, @PathVariable("id") int id) {
        User currentUser = (User) request.getAttribute("user");  // Obtiene el usuario autenticado de la solicitud
        return productService.getProduct(currentUser, id);  // Llama a ProductService para obtener el producto por ID para el usuario
    }

    // Solicitud POST para agregar un nuevo producto
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)  // Establece el estado de respuesta a 201 (Creado)
    public Product addProduct(HttpServletRequest request, @RequestBody String content) {
        User currentUser = (User) request.getAttribute("user");  // Obtiene el usuario autenticado de la solicitud
        return productService.addProduct(currentUser, content);  // Llama a ProductService para agregar un nuevo producto
    }

    // Solicitud PUT para actualizar un producto existente por su ID
    @PutMapping("/{id}")
    public Product updateProduct(HttpServletRequest request, @PathVariable("id") int id, @RequestBody String content) {
        User currentUser = (User) request.getAttribute("user");  // Obtiene el usuario autenticado de la solicitud
        return productService.updateProduct(currentUser, id, content);  // Llama a ProductService para actualizar el producto por ID
    }

    // Solicitud DELETE para eliminar un producto por su ID
    @DeleteMapping("/{id}")
    public String deleteProduct(HttpServletRequest request, @PathVariable("id") int id) {
        User currentUser = (User) request.getAttribute("user");  // Obtiene el usuario autenticado de la solicitud
        productService.deleteProduct(currentUser, id);  // Llama a ProductService para eliminar el producto por ID
        return "Deleted product with id " + id;  // Devuelve un mensaje de éxito después de la eliminación
    }

    // Solicitud POST para agregar una nueva reseña a un producto por ID de producto
    @PostMapping("/{id}/review")
    public Review addReview(HttpServletRequest request, @PathVariable("id") int id, @RequestBody String content) {
        User currentUser = (User) request.getAttribute("user");  // Obtiene el usuario autenticado de la solicitud
        return productService.addReview(currentUser, id, content);  // Llama a ProductService para agregar una reseña al producto
    }

    // Solicitud PUT para actualizar una reseña existente por ID de producto y reseña
    @PutMapping("/{id}/review/{reviewId}")
    public Review updateReview(HttpServletRequest request, @PathVariable("id") int id, @PathVariable("reviewId") int reviewId, @RequestBody String content) {
        User currentUser = (User) request.getAttribute("user");  // Obtiene el usuario autenticado de la solicitud
        return productService.updateReview(currentUser, id, reviewId, content);  // Llama a ProductService para actualizar la reseña
    }

    // Solicitud DELETE para eliminar una reseña por ID de producto y reseña
    @DeleteMapping("/{id}/review/{reviewId}")
    public String deleteReview(HttpServletRequest request, @PathVariable("id") int id, @PathVariable("reviewId") int reviewId) {
        User currentUser = (User) request.getAttribute("user");  // Obtiene el usuario autenticado de la solicitud
        productService.deleteReview(currentUser, id, reviewId);  // Llama a ProductService para eliminar la reseña
        return "Deleted review with id " + reviewId + " from product " + id;  // Devuelve un mensaje de éxito después de la eliminación
    }
}

Paso 2: Obtén tu clave de API del entorno

En el panel de instrumentos de la interfaz de usuario, copia la API Key del entorno activo.

A continuación, agrega la API key del entorno y la PDP URL en el archivo application.yaml.

permit:
  pdpUrl: 'http://localhost:7766'
  apiKey: "Your Permit environment API Key"

Paso 3: Implementar Punto de Decisión de Política (PDP)

El Punto de Decisión de Política (PDP) se implementa en tu VPC y es responsable de evaluar tus solicitudes de autorización. El PDP garantizará cero latencia, excelente rendimiento, alta disponibilidad y mejor seguridad.

Use el comando siguiente para extraer el contenedor de Permit.io PDP de Docker Hub.

docker pull permitio/pdp-v2:latest

A continuación, ejecute el contenedor.

docker run -it -p 7766:7000 --env PDP_DEBUG=True --env PDP_API_KEY=<YOUR_API_KEY> permitio/pdp-v2:latest

Paso 4: Ejecutar la aplicación

Puede ejecutar la aplicación usando el siguiente comando de Gradle:

./gradlew bootRun

Ver y crear productos

Vamos a interactuar con los puntos finales de la aplicación usando REQBIN.

Primero, cree un nuevo usuario usando el punto final /api/users/signup.

curl -X POST "http://localhost:8080/api/users/signup" -H "Content-Type: application/json" -d 'johndoe'

Debería ser capaz de ver al usuario en su proyecto Permit, en Directorio > Todos los inquilinos.

Inicialmente, el usuario no tiene roles, por lo que no puede hacer mucho. Por ejemplo, intentar listar los productos resultará en una respuesta 403 Prohibido, como se muestra a continuación. El código de error 403 significa que el usuario no tiene permisos para acceder a la recurso solicitado, que en este caso son los productos. Puede aprender más sobre la diferencia entre los códigos de error 401 y 403 aquí.

Para que el usuario vea una lista de productos, asígnale un rol de visualización usando el comando a continuación:

curl -X POST "http://localhost:8080/api/users/assign-role" \
-H "Authorization: Bearer johndoe" \
-H "Content-Type: application/json" \
-d 'viewer'

Debería ver que el usuario johndoe se asignó el rol de visualización, como se muestra a continuación:

Como un visualizador puede crear un producto, use el comando a continuación para crear un producto con el usuario johndoe.

curl -X POST "http://localhost:8080/api/products" -H "Authorization: Bearer johndoe" -H "Content-Type: application/json" -d 'MacBook'

Debería ver que se creó un nuevo producto con ID 1 y que el usuario johndoe se agregó como proveedor.

Agregando reseñas a productos

Para agregar reseñas a productos, cree otro usuario llamado jane.

curl -X POST "http://localhost:8080/api/users/signup" -H "Content-Type: application/json" -d 'jane'

Para que el usuario agregue una reseña a los productos, asígnale un rol de visualización usando el comando a continuación:

curl -X POST "http://localhost:8080/api/users/assign-role" \
-H "Authorization: Bearer jane" \
-H "Content-Type: application/json" \
-d 'viewer'

A continuación, puede agregar una reseña al producto agregado por johndoe usando el comando a continuación:

curl -X POST "http://localhost:8080/api/products/1/review" -H "Authorization: Bearer jane" -H "Content-Type: application/json" -d 'The product was in good quality'

¡Felicitaciones! Ha completado el proyecto de este tutorial.

Pasos siguientes

Ahora que ha aprendido cómo implementar una autorización de alta granularidad en sus aplicaciones Java y Spring Boot usando Permit.io, tal vez quiera explorar más adelante.

Aquí tienes algunos recursos valiosos:

Antes de que terminemos

Espero que haya encontrado este tutorial informativo.

Aquí tienes algunos de mis otros posts de blog recientes que podrías disfrutar:

Para más tutoriales sobre herramientas de desarrollo impresionantes, asegúrate de revisar mi blog DTA.

Sígueme en Twitter para obtener actualizaciones en vivo sobre mis otros proyectos.

¡Feliz codificación.