Mostrando entradas con la etiqueta WSO2. Mostrar todas las entradas
Mostrando entradas con la etiqueta WSO2. Mostrar todas las entradas

viernes, 3 de marzo de 2023

WSO2 Secret: Creación básica de claves

WSO2 permite la creación de claves que son manejadas por su propio Secure Vault. Una buena herramienta que nos permite el manejo de claves de forma segura e implementar buenas practicas. 

Para este ejemplo hemos utilizado la imagen: docker.wso2.com/wso2mi:1.2.0

Clave estáticas

Por un lado, las configuramos en el fichero de configuración, deployment.toml, sin encriptar. Deben ir entre comillas dobles y corchetes.

[secrets]
server_secret = "[secret_1]"
synapse_secret = "[secret_2]"

Si quieres activar dicha implementación en un entorno con VM lo podemos hacer ejecutando el comando:

sh <MI_HOME>/bin/ciphertool.sh -Dconfigure

Tras ejecutarlo, tendremos que indicar la clave de nuestro keystore. Que por defecto es, wso2carbon. Tras realizar este paso, si volvemos al fichero de configuración, podremos ver cómo las variables han sido encriptadas.

  • Como usarlas en fichero de configuración
Para ello solo tendremos que hacer referencias a las mismas, a través del alias que le dimos.

[keystore.primary]
password = "$secret{server_secret}"

  • Como usarlas en el contexto de synapse

En este caso, lo realizaremos a través del mediator property y el alias que le dimos

<property expression="wso2:vault-lookup('synapse_secret')" name="secret"/>

Clave dinámicas

Este es el funcionamiento básico, que nos ayuda a gestionar claves privadas, pero el manejo y creación de nuevas claves require el reinicio del servidor. Lo cual puede ser un inconveniente en determinados entornos. Por lo que la creación dinámica de estas contraseñas puede ser un punto fuerte. 

Podemos indicar claves dinámicas a través de variables del entorno o variables del sistema. Para ello primero necesitaremos encriptar las contraseñas, luego setear la variable y posteriormente configurarla.

Para encriptar la variable, podemos utilizar el Micro Integrator CLI o directamente ejecutar el comando:

sh <MI_HOME>/bin/ciphertool.sh

Este comando primero nos pedirá la clave del keystore y posteriormente la contraseña a encriptar, dos veces. 

Una vez echo esto, seteamos la variable. Por ejemplo, para almacenarla en el entorno, ejecutamos:

export env_secret=<ENCRYPTED_VALUE>

Y por último solo nos quedará configurarla. Ejemplo:

[secrets]
env_secret = "$env{env_secret}"

Ya podremos utilizar como vimos anteriormente en nuestros ficheros de configuración o en el código synapse. Ejemplo:

<log level="custom">
   <property name="MSG" value="secretExample_v1_reader_api - GET - /reader/ - init"/>
   <property name="keystore_pwd" expression="wso2:vault-lookup('keystore_pwd')" />
   <property name="env_secret" expression="wso2:vault-lookup('env_secret')" />
</log>

Obteniendo la siguiente salida

wso2mi    | [2022-06-15 10:15:15,424]  INFO {LogMediator} - {api:secretExample_v1_reader_api} 
    MSG = secretExample_v1_reader_api - GET - /reader/ - init, 
    keystore_pwd = wso2carbon, env_secret = envpwd

También podemos configurarlas a través de ficheros o secrets de Docker, con las cuales podremos aumentar el dinamismo de la configuración y utilización de las claves. 


lunes, 6 de junio de 2022

WSO2 Identity Server: Como hacer un custom user store

En este post que es y para qué sirve un Custom User Store en el WSO2 Identity Server. Y además también veremos pequeñas diferencias de desplegarlo en una versión en el Identity Server 5.10, 5.11 o 5.12.

El IS es un producto que nos permite entre otras funciones gestionar los usuarios de distintos orígenes de forma centralizada. A esta fuente donde se almacenan los usuarios y sus roles, es un User Store. Por defecto, él IS nos permite configurar distintos User Stores en base a su origen (LDAP, BBDD, etc). Pero nosotros, también podemos crear nuestro propio tipo de User Store basando en uno de estos y personalizando determinados métodos. 

Como siempre, vamos a crear un ejemplo sencillo y paso a paso. En él, personalizaremos el User Store, para que verifique que la contraseña sea el texto resultante de la codificación del nombre del usuario. Y el algoritmo de codificación lo podremos configurar en el propio custom user store. 

Empezamos creando una clase que herede de alguno de los User Store por defecto. A partir de la versión 5.11 solo existen los User Store con prefijo UniqueID, que aseguran que cada usuario tenga un identificador único. 

public class CustomUserStore10 extends UniqueIDJDBCUserStoreManager {

  private static final Log LOG = LogFactory.getLog(CustomUserStore10.class);

  private final static String CUS_ALG_NAME = "algorithm";
  private final static String CUS_ALG_DFLT_VAL = "BASE64";
  private final static String CUS_ALG_DIS_NAME = "Algorithm";
  private final static String CUS_ALG_DESC = "Algorithm for encrypt username";
  private final static String CUS_ALG_PROP_DESC = CUS_ALG_DIS_NAME + "#" + CUS_ALG_DESC;

  public CustomUserStore10(final RealmConfiguration realmConfig,
      final Map<String, Object> properties, final ClaimManager claimManager,
      final ProfileConfigurationManager profileManager, final UserRealm realm,
      final Integer tenantId) throws UserStoreException {
    super(realmConfig, properties, claimManager, profileManager, realm, tenantId);
    this.realmConfig = realmConfig;
  }

  @Override
  protected AuthenticationResult doAuthenticateWithUserName(final String userName,
      final Object credential) throws UserStoreException {
      String algorithm = realmConfig.getUserStoreProperty(CUS_ALG_NAME);
      String sCredential = (String) credential;
      String myPwdEncripted = null;
      if ("BASE64".equals(algorithm)) {
        myPwdEncripted = Base64.getEncoder().encodeToString(userName.getBytes());
      }
      if (sCredential.equalsIgnoreCase(myPwdEncripted)) {
        User user = getUser(getUserIDFromUserName(userName), userName);
        authResult = new AuthenticationResult(AuthenticationResult.AuthenticationStatus.SUCCESS);
        authResult.setAuthenticatedUser(user);
      } else {
        authResult = new AuthenticationResult(AuthenticationResult.AuthenticationStatus.FAIL);
        authResult.setFailureReason(new FailureReason("Password change required."));
      }
    return authResult;
  }

  @Override
  public Properties getDefaultUserStoreProperties() {
    Properties properties = super.getDefaultUserStoreProperties();
    List<Property> mandatories =
        Stream.of(properties.getMandatoryProperties()).collect(Collectors.toList());
    // create new configuration property
    Property apiURL = new Property(CUS_ALG_NAME, CUS_ALG_DFLT_VAL, CUS_ALG_PROP_DESC, null);
    // add to mandatory properties
    mandatories.add(apiURL);

    properties.setMandatoryProperties(mandatories.stream().toArray(Property[]::new));
    return properties;
  }
}

Para el ejemplo hemos extendido de la clase UniqueIDJDBCUserStoreManager, y para ello hemos creado un esquema de BBDD con el formato necesario para ser utilizado por WSO2. Este scripts se encuentra dentro de la propia imagen del producto y contiene de distintos tipos, en función de la BBDD donde queramos crearlo. 

En el método getDefaultUserStoreProperties podemos añadir nuevos parámetros de configuración, los cuales se mostrarán en la UI de creación del User Store. Y el método doAuthenticateWithUserName será el que sobrescribiremos para modificar la lógica por defecto cuando se logea un usuario. 

Si queremos hacer tareas más complejas y no vemos correctamente cuál es el método a sobrescribir, siempre podemos bajar el nivel de debug del paquete donde se encuentre la clase que extendemos y verificar cuales son los métodos que se invocan. 

El siguiente paso será crear una clase que permita cargar nuestra clase de forma dinámica con OSGI. Para ello la clase debe tener los siguientes elementos:

  • Anotación @Component asociada a la clase. Permitirá crear la configuración de forma automática, a través del maven-bundle-plugin, que debe ser incluida en nuestro JAR. 
  • Anotación @Activate asociada a un método. El cual registrará nuestra clase dentro del contexto de WSO2. 
@Component(name = "custom.user.store.manager.dscomponent", service = CustomUserStore10DSC.class,
    immediate = true)
public class CustomUserStore10DSC {
  @Activate
  protected void activate(final ComponentContext ctxt) {
    UserStoreManager customUSManager = new CustomUserStore10();
    ctxt.getBundleContext().registerService(UserStoreManager.class.getName(), customUSManager,
        null);
  }
}

Ahora toca definir el fichero pom.xml dónde principalmente configuraremos el plugin maven-bundle-plugin. 

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>es.home.example.wso2is</groupId>
	<artifactId>custom-user-store-10</artifactId>
	<version>1.0.0</version>
	<packaging>bundle</packaging>
	<dependencies>
		<dependency>
			<groupId>org.wso2.carbon</groupId>
			<artifactId>org.wso2.carbon.user.core</artifactId>
			<version>4.6.0</version>
			<scope>provided</scope>
</dependency> <dependency> <groupId>org.wso2.carbon</groupId> <artifactId>org.wso2.carbon.utils</artifactId> <version>4.6.0</version>
			<scope>provided</scope>
</dependency> <dependency> <groupId>org.wso2.carbon</groupId> <artifactId>org.wso2.carbon.user.api</artifactId> <version>4.6.0</version>
			<scope>provided</scope>
</dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.24</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.felix</groupId> <artifactId>org.apache.felix.scr.ds-annotations</artifactId> <scope>provided</scope> <version>1.2.10</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <version>3.2.0</version> <extensions>true</extensions> <configuration> <instructions> <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName> <Bundle-Name>${project.artifactId}</Bundle-Name> <Private-Package>es.home.example.wso2is.userstore.internal</Private-Package> <Export-Package>!es.home.example.wso2is.userstore.internal, es.home.example.wso2is.userstore.*</Export-Package> <_dsannotations>*</_dsannotations> <Import-Package>org.osgi.service.component.*;version="${osgi.service.component.imp.pkg.version.range}", org.wso2.carbon.user.api; version="${carbon.user.api.imp.pkg.version.range}", org.wso2.carbon.user.core.*; version="${carbon.kernel.package.import.version.range}", org.wso2.carbon.utils; version="${carbon.kernel.package.import.version.range}", org.apache.commons.logging; version="${import.package.version.commons.logging}", org.osgi.framework; version="${osgi.framework.imp.pkg.version.range}", javax.servlet; version=2.4.0, javax.servlet.http; version=2.4.0, *;resolution:=optional</Import-Package> </instructions> </configuration> </plugin> </plugins> </build> <properties> <carbon.user.api.imp.pkg.version.range>[1.0, 2.0.0)</carbon.user.api.imp.pkg.version.range> <carbon.kernel.package.import.version.range>[4.5.0, 5.0.0)</carbon.kernel.package.import.version.range> <import.package.version.commons.logging>[1.2.0,2.0.0)</import.package.version.commons.logging> <osgi.framework.imp.pkg.version.range>[1.7.0, 2.0.0)</osgi.framework.imp.pkg.version.range> <osgi.service.component.imp.pkg.version.range>[1.2.0, 2.0.0)</osgi.service.component.imp.pkg.version.range> </properties>
</project>

Por último, solo quedaría desplegarlo en la carpeta ${WSO2_HOME}/repository/components/dropins/ y arrancar el WSO2 IS. En la carpeta src/test/resources hay un docker-compose del código fuente, que os puede ayudar a desplegarlo. Una vez arrancado, crearemos el User Store desde la opción del menú lateral 'add User Store'. 


Este sería el funcionamiento para el IS 5.10. Y en la siguiente versión, aunque los elementos explicados anteriormente se mantienen iguales, la configuración es distinta. 

Para ello, deberemos configurar todos los User Stores disponibles en el IS, los de por defecto y los que creemos nosotros, en el fichero de configuración deployment.toml. 

[user_store_mgt]
allowed_user_stores=["org.wso2.carbon.user.core.jdbc.UniqueIDJDBCUserStoreManager", 
    "org.wso2.carbon.user.core.ldap.UniqueIDActiveDirectoryUserStoreManager", 
    "org.wso2.carbon.user.core.ldap.UniqueIDReadOnlyLDAPUserStoreManager", 
    "org.wso2.carbon.user.core.ldap.UniqueIDReadWriteLDAPUserStoreManager", 
    "es.home.example.wso2is.userstore.CustomUserStore10"]

Y esto ha sido todos, si quieres ver todo el código puedes hacerlo aquí. 

lunes, 11 de abril de 2022

WSO2 MI: Envío y Recepción con ActiveMQ

Buenas, hoy veremos un ejemplo completo de como interacturar desde WSO2 con una cola ActiveMQ. Mostraremos como configurar el Micro Integrator, MI, escribir y leer de la cola y consultar cuantos registros tiene. 

Para el ejemplo utilizaremos las siguientes versiones:

  • Micro Integrator 4.0.0
  • Active MQ 5.8.14. 

    1. Docker Compose

Por un lado debemos incluir las librerías y dependencias de ActiveMQ en el docker compose base que tenemos:

  • activemq-broker, activemq-client y activemq-openwire-legacy 5.14.0
  • geronimo-j2ee-management_1_1_spec-1.0.1 y geronimo-jms_1.1_spec-1.1.1
  • hawtbu-1.11
He incluir la instancia de ActiveMQ:

  activemq:
    image: rmohr/activemq:5.14.5
    mem_limit: 1G
    hostname: activemq
    container_name: activemq
    ports:
      - 8161:8161   #UI
      - 61616:61616 #JMS
    networks:
      wso2-net:
        ipv4_address: 172.16.238.20

Si no configuramos nada más, cuando intentemos conectarnos con ActiveMQ, recibiremos el siguiente error:

Unexpected error during sending message out org.apache.axis2.AxisFault: 
The system cannot infer the transport information from the jms:/.....

Por tanto deberemos modificar el fichero de configuración deployment.toml para añadir:

[[transport.jms.sender]]
name = "myQueueSender"
parameter.initial_naming_factory = "org.apache.activemq.jndi.ActiveMQInitialContextFactory"
parameter.provider_url = "tcp://activemq:61616"
parameter.connection_factory_name = "QueueConnectionFactory"
parameter.connection_factory_type = "queue"
parameter.cache_level = "producer"

Y para poder leer de una cola necesitamos configurar un JMS Listener también en el fichero de configuración deployment.toml

[[transport.jms.listener]]
name = "myQueueConnectionFactory"
parameter.initial_naming_factory = "org.apache.activemq.jndi.ActiveMQInitialContextFactory"
parameter.provider_url = "failover:tcp://activemq:61616"
parameter.connection_factory_name = "QueueConnectionFactory"
parameter.connection_factory_type = "queue"
parameter.cache_level = "consumer"

[[transport.jms.listener]]
name = "default"
parameter.initial_naming_factory = "org.apache.activemq.jndi.ActiveMQInitialContextFactory"
parameter.provider_url = "failover:tcp://activemq:61616"
parameter.connection_factory_name = "QueueConnectionFactory"
parameter.connection_factory_type = "queue"

En esta configuración es muy importante indicar la configuración de failover. Puesto que de lo contrario si se cayese el nodo de ActiveMQ, no se podría volver a conectar con el mismo a menos que se reiniciase el MI. 

    2. Envío de mensajes

Ahora procederemos a crear el primer recurso de una API, con el cual enviaremos información al activeMQ. Pero previamente transformaremos el mensaje, simplemente por añadir una pequeña lógica al ejemplo.

<resource methods="POST" uri-template="/activeMQ/">
    <inSequence>
        <payloadFactory media-type="xml">
            <format>
                <m:getQuote xmlns:m="http://services.samples">
                    <m:request>
                        <m:book>
                            <m:isbn>$1</m:isbn>
                            <m:release>$2</m:release>
                        </m:book>
                    </m:request>
                </m:getQuote>
            </format>
            <args>
                <arg expression="$.isbn" evaluator="json"/>
                <arg expression="$.year" evaluator="json"/>
            </args>
        </payloadFactory>
        <property name="OUT_ONLY" value="true"/>
        <call>
        <endpoint>
            <address uri="jms:/myqueue?transport.jms.ConnectionFactoryJNDIName=QueueConnectionFactory
                   &amp;java.naming.provider.url=failover:(tcp://activemq:61616)
                   &amp;java.naming.factory.initial=org.apache.activemq.jndi.ActiveMQInitialContextFactory
                   &amp;transport.jms.DestinationType=queue&amp;redeliveryPolicy.redeliveryDelay=30000
                   &amp;maximumRedeliveries=10" />
        </endpoint>
        </call>
        <respond />
    </inSequence>
    <outSequence/>
    <faultSequence/>
</resource>

En este código hay dos apartados interesantes:

  • La configuración de la propiedad OUT_ONLY que nos permite indicarle al MI que no se quede a la espera de una respuesta por parte del ActiveMQ. 
  • La configuración de la invocación al ActiveMQ, donde aparte de los valores básicos para la configuración,  podemos incluir la política de reintentos a través de las propiedades de redelivery o aumentar la resiliencia con la configuración de failover, donde podemos incluir varios nodos de ActiveMQ.
Si hubiésemos configurado con valores más específicos, no necesitaríamos un address endpoint con tanto detalle. Solo necesitaríamos indicar la cola y el transport sender a utilizar. 

<address uri="jms:/myqueue?transport.jms.ConnectionFactory=myCustomSender" />

    3. Lectura de mensajes en la cola

Desde la versión 5.8 ActiveMQ incluye una API a través de la cual podemos enviar y consultar mensajes. Además con la ayuda de Jolokia pone a disposición de los usuarios una API de gestión. Esta  API será la que utilicemos para ver cuantos mensajes hay disponibles en la cola.   

La lógica de nuestro recurso será sencillo: 
    - Creamos una cabecera de autorización básica para la invocación de la API de activeMQ.
    - En la llamada a la API indicamos el broker y la cola de la cual deseamos los datos. Los cuales obtenemos desde nuestro propio recurso.
    - Leemos la respuesta y en caso de ser correcta en base al valor del campo status. (a tener en cuenta que siempre devuelve HTTP Status Code a 200 y Content-Type a text/plain). Leemos el campo value y lo devolvemos.  

<resource methods="GET" uri-template="/activeMQ/{broker}/{queue}/count">
    <inSequence>
        <property name="Authorization" expression="fn:concat('Basic ', base64Encode('admin:admin'))" scope="transport"/>  
        <send>
            <endpoint xmlns="http://ws.apache.org/ns/synapse" name="mock_v1_activemqCount_httpEp">
                <http method="GET" uri-template="http://activemq:8161/api/jolokia/read/org.apache.activemq:type=Broker,brokerName={uri.var.broker},destinationType=Queue,destinationName={uri.var.queue}/QueueSize" />
            </endpoint>
        </send>
    </inSequence>
    <outSequence>
        <property name="ContentType" scope="axis2" type="STRING" value="application/json"/>
        <property name="messageType" scope="axis2" type="STRING" value="application/json"/>
        <switch source="json-eval($.status)">
            <case regex="200">
                <payloadFactory media-type="json">
                    <format> { "count": $1 } </format>
                    <args> <arg evaluator="json" expression="json-eval($.value)"/> </args>
                </payloadFactory>
            </case>
            <default>
                <property name="HTTP_SC" value="500" scope="axis2" />
                <payloadFactory media-type="json">
                    <format>{ "msg": "Unexpected error"}</format>
                    <args />
                </payloadFactory>
            </default>
        </switch>
        <respond/>
    </outSequence>
    <faultSequence/>
</resource>

    4. Lectura de mensajes 

Por último crearemos un Proxy Service que nos permitirá la lectura de esa misma cola. En este caso es importante también devolverle una respuesta a la cola, puesto que de lo contrarío reenviaría el mensaje a la cola de Dead Letter Channel, ActiveMQ.DLQ. 

<proxy xmlns="http://ws.apache.org/ns/synapse" name="activemqExample_v1_consumer_pr" transports="jms" statistics="disable" trace="disable" startOnLoad="true">
    <target>
        <inSequence>
            <log level="full">
                <property name="MSG" value="Receiving messages from Queue" />
            </log>
            <property action="set" name="OUT_ONLY" value="true"/>
            <drop />
        </inSequence>
        <faultSequence />
    </target>
    <parameter name="redeliveryPolicy.maximumRedeliveries">1</parameter>
    <parameter name="transport.jms.DestinationType">queue</parameter>
    <parameter name="transport.jms.SessionTransacted">true</parameter>
    <parameter name="transport.jms.Destination">myqueue</parameter>
    <parameter name="redeliveryPolicy.redeliveryDelay">2000</parameter>
    <parameter name="transport.jms.CacheLevel">consumer</parameter>
    <description />
</proxy>

Ahora podremos realizar la siguiente invocación y comprobar como por un lado enviamos el mensaje y lo leemos por el otro. 

curl --request POST 'https://localhost:8253/mock/activeMQ' \
--header 'Content-Type: application/json' \
--data-raw '{
    "author":"Alfred Bester",
    "name":"the stars my destination",
    "isbn":"1-85798-814-0",
    "year": 1956
}'

Con esto hemos visto un completo ejemplo sobre que podemos hacer para leer y escribir en una cola de ActiveMQ con WSO2. Si nos adentramos un poco más en la configuración podremos realizar tareas más complejas. Como siempre, todo el código lo podemos ver aquí.

sábado, 19 de marzo de 2022

WSO2 Micro Integrator : Arrancar y Desplegar

Hoy vamos a ver un post básico sobre cómo lanzar y desplegar una aplicación en un WSO2 Micro Integrator, MI. La idea es crear un ejemplo básico sobre el cual podremos construir ejemplos más complejos en un futuros posts. 

El Micro Integrator es la evolución del Enterprise Integrator de WSO2. Es la solución que plantean para acercar el mundo de los Enterprise Service Bus hacía los microservicios. Permitiendo el despliegue de integraciones con contenedores en un tiempo mínimo, una huella de memoria baja y de forma ágil. 

Para el ejemplo utilizaremos:

  • Micro Integrator 4.0.0
Lo primero será crear nuestra aplicación que para este ejemplo será un simple mock. Si no tienes mucha experiencia con WSO2 lo mejor es utilizar el Integration Studio. El cual es un eclipse embebido pero con múltiples cualidades:
  • Motor gráfico que te ayuda en la creación de integraciones. 
  • Templates para realizar determinadas acciones. 
  • Guías paso a paso para realizar la configuración de determinados componentes.
  • Permite seleccionar los artefactos a desplegar de forma sencilla. 

Mi opinión personal sobre el Integration Studio es que ha mejorado mucho con los años y que si no tienes experiencia te puede ayudar muchísimo. Pero si tienes experiencia y un buen arquetipo Maven, que puedes hacerte tú mismo, con un simple editor de texto puedes conseguir lo mismo de forma más rápida y ágil. 

Es por eso que cuando pongamos código relativo a integraciones WSO2 lo haremos siempre mostrando el código XML y no los gráficos propios del Integration Studio. 

Con el vamos a crear una simple API que tenga dos recursos que nos permitan devolver siempre el mismo resultado:

<?xml version="1.0" encoding="UTF-8"?>
<api context="/book" name="basicExample_v1_book_api" xmlns="http://ws.apache.org/ns/synapse">
    <resource methods="GET">
        <inSequence/>
        <outSequence/>
        <faultSequence/>
    </resource>
    <resource methods="GET" uri-template="/mock/">
        <inSequence>
            <log level="custom">
                <property name="MSG" value="basicExample_v1_book_api - get - /mock/ - init"/>
            </log>
            <payloadFactory media-type="json">
                <format>
		    [{
		        "id" : 1, "author":"Frank Herbert", "name":"Dune"
		    },{
		        "id" : 2, "author":"Alfred Bester", "name":"The starts my destination"
		    },{
		        "id" : 3, "author":"Frederik Pohl","name":"Gateway"
		    }]
		</format>
                <args/>
            </payloadFactory>
            <log level="custom">
                <property name="MSG" value="basicExample_v1_book_api - get - /mock/ - end"/>
            </log>
            <respond/>
        </inSequence>
        <outSequence/>
        <faultSequence/>
    </resource>
    <resource methods="GET" uri-template="/mock/{id}">
        <inSequence>
            <log level="custom">
                <property name="MSG" value="basicExample_v1_book_api - get - /mock/{id} - init"/>
            </log>
            <payloadFactory media-type="json">
                <format>
		    { "id" : $1, "author":"Frank Herbert", "name":"Dune"}
		</format>
                <args>
                    <arg evaluator="xml" expression="$ctx:uri.var.id"/>
                </args>
            </payloadFactory>
            <log level="custom">
                <property name="MSG" value="basicExample_v1_book_api - get - /mock/{id} - end"/>
            </log>
            <respond/>
        </inSequence>
        <outSequence/>
        <faultSequence/>
    </resource>
</api>

Esta API, la incluiremos en un CAR que es el formato de paquete para las integraciones de WSO2. 

A continuación veremos cómo desplegarlo en una imagen docker. Pero para poder bajarnos la imagen del MI, necesitaremos una cuenta de WSO2, gratuita, y logarnos en su docker hub. 

docker login docker.wso2.com -u youraccount@example.com

Esta imagen la incluiremos en un docker-compose básico en el cual podremos incluirle otras dependencias en futuros posts. 

version: '3.7'

networks:
  wso2-net:
    ipam:
      driver: default
      config:
        - subnet: "172.16.238.0/24"

services:
  wso2mi:
    mem_limit: 1G
    image: wso2/wso2mi:4.0.0
    hostname: wso2mi
    container_name: wso2mi
    ports:
      - 8290:8290
      - 8253:8253
      - 9164:9164
    networks:
      wso2-net:
        ipv4_address: 172.16.238.10
    environment:
      JAVA_OPTS: "-DenableManagementApi=true -Duser.timezone=Europe/Madrid"
    volumes:
       - ./conf/deployment.toml:/home/wso2carbon/wso2mi-4.0.0/conf/deployment.toml

Un par de apuntes sobre este fichero:

  • Con el parámetro enableManagementApi podemos habilitar la API de gestión.
  • El fichero deployment.toml es el fichero de configuración del Micro Integrator. Nosotros para este caso no hemos hecho ningún cambio sobre el fichero de por defecto, pero es normal modificarlo y por eso lo hemos añadido en un volumen. 

Ahora con este docker-compose veremos distintas formas de poder desplegar el CAR. 

1. Configurar un volumen donde se encuentren el desplegable

Nos permite indicar en el momento del arranque las integraciones que queremos desplegar. 

    volumes:
       - ./conf/deployment.toml:/home/wso2carbon/wso2mi-4.0.0/conf/deployment.toml
       - ./apps/wso2-basic-example_1.0.0.car:/home/wso2carbon/wso2mi-4.0.0/repository/deployment/server/carbonapps/wso2-basic-example_1.0.0.car

2. Arrancar el MI y pasarle el desplegable

Similar al paso anterior, pero nos permite desplegar aplicaciones con el MI ya en marcha. Esto es especialmente util cuando estamos desarrollando. 

docker cp wso2-basic-example_1.0.0.car wso2mi:/home/wso2carbon/wso2mi-4.0.0/repository/deployment/server/carbonapps/

3. Crear un Dockerfile con el CAR. 

Esta opción será la ideal cuando tengamos nuestra integración estable y la querremos desplegar en entornos productivos. 

    Primero construimos el Dockerfile

FROM wso2/wso2mi:4.0.0
COPY wso2-basic-example_1.0.0.car /home/wso2carbon/wso2mi-4.0.0/repository/deployment/server/carbonapps/

    Luego construimos la imagen

docker build -t deesebc/book-api-mi .

    Y por último arrancamos un contenedor

docker run -it -p 8290:8290 -p 8253:8253 -p 9164:9164 --name micro-integrator deesebc/book-api-mi

4. API Management

A pesar de estar indicado en la documentación como una posibilidad, a 03/2022 es solo posible como actualización de pago del MI. 

Una vez que lo hemos arrancado ya podremos consultar nuestra API

curl --location --request GET 'http://localhost:8290/book/mock'
curl --location --request GET 'https://localhost:8253/book/mock'

El otro puerto habilitado, 9164, será el que nos da acceso a través de HTTPS a la API de gestión. 

Espero que haya sido de utilidad, y como siempre si queréis el código completo podéis verlo aquí. 

jueves, 4 de febrero de 2021

WSO2 y Redis

Ya estuvimos hablando en un post anterior de WSO2 y el cache mediator. Hoy veremos una solución alternativa pero similar. En este caso utilizaremos Redis, que nos permitirá cachear la respuesta de un backend. 

Redis es un almacén de estructura de datos de valores de clave en memoria rápido y de código abierto. Básicamente una base de datos nosql de tipo clave y valor, muy popular entre los desarrolladores. Redis, permite el uso de diferentes tipos de datos: List, Sets, Hashes, etc. 

Nosotros nos basaremos únicamente en los pares clave-valor, los hashes. Y veremos su uso a través de un ejemplo, como siempre. En este ejemplo llamaremos a un backend y almacenaremos su respuesta asociado al identificador del objeto. Lo cual nos permitirá que al volver a llamar con el mismo identificador, devolver la respuesta anteriormente obtenida. 

Para empezar, usaremos un WSO2 EI 6.5.0. Con la versión standalone no es posible conectarse a Redis, para conseguirlo debemos hacer dos cosas:

  • Añadir la librería Jedis (2.10.2) en la carpeta ${WSO2_HOME}/lib y arrancar el servidor. 
  • Añadir el conector de Redis a través de la interfaz gráfica.  

El conector se puede obtener desde el WSO2 Store, y hay distintas versiones en función del producto que utilicemos. Hay que recordar que una vez después de añadirlo es necesario habilitarlo.


La forma más sencilla de utilizar Redis es como siempre utilizar docker, y para ello usaremos el siguiente comando:

docker run -p 6379:6379 --name redis -d redis

El uso del conector de Redis es similar al de otros conectores. Básicamente son custom mediators, que admiten múltiples propiedades y los cuales tienen nombres identificativos de las operaciones que realizan. 

Vamos a crear una API con varios recursos. En el primer recurso realizaremos los siguientes pasos:
  • Buscamos el identificador en Redis a través de la operación hExists. En la cual tenemos que incluir el volumen donde esta almacenado y su clave. 
  • Si existe la clave, utilizaremos la operación hGet con los mismos valores para recuperar el valor almacenado. 
  • Si no existe la clave, procederemos a llamar al backend. Posteriormente almacenaremos el valor a través de la operación hSet, en la cual indicaremos el volumen, la clave y el valor asociado. 
Además debemos tener en cuenta distintos aspectos del conector a tener en cuenta:
  • La primera vez que utilicemos el conector debemos invocar la operación init, la cual nos posibilitará la conexión a Redis. 
  • El resultado de la operación hExists se almacena en una propiedad de contexto denominada redisResponse. 
  • La operación hGet devuelve un valor true o false en base a si encuentra o no el resultado.
  • Debemos almacenar y restaurar el payload al utilizar la operación hSet para evitar devolver el resultado de la misma al cliente. 
<resource methods="GET" uri-template="/{bookId}">
    <inSequence>
        <property name="bookId" expression="$ctx:uri.var.bookId"/>
        <redis.init>
            <redisHost>localhost</redisHost>
            <redisPort>6379</redisPort>
            <redisTimeout>500</redisTimeout>
        </redis.init>
        <redis.hExists>
            <redisKey>Library</redisKey>
            <redisField>{$ctx:bookId}</redisField>
        </redis.hExists>
        <filter source="$ctx:redisResponse" regex="true">
            <then>
                <redis.hGet>
                    <redisKey>Library</redisKey>
                    <redisField>{$ctx:bookId}</redisField>
                </redis.hGet>
            </then>
            <else>
                <call>
                    <endpoint>
                        <http method="GET" uri-template="http://localhost:57001/book/{uri.var.bookId}"/>
                    </endpoint>
                </call>
                <enrich>
                    <source type="body" clone="false"/>
                    <target type="property" property="ORIGINAL_PAYLOAD"/>
                </enrich>
                <redis.hSet>
                    <redisKey>Library</redisKey>
                    <redisField>{$ctx:bookId}</redisField>
                    <redisValue>{$ctx:ORIGINAL_PAYLOAD}</redisValue>
                </redis.hSet>
                <enrich>
                    <source type="property" clone="false" property="ORIGINAL_PAYLOAD"/>
                    <target type="body"/>
                </enrich>
            </else>
        </filter>
        <property name="ContentType" scope="axis2" type="STRING" value="application/json"/>
        <respond/>
    </inSequence>
</resource>

A diferencia del cache mediator, con el conector de Redis podremos borrar un valor almacenado. Y esto es gracias a que el conector contiene múltiples operaciones. 

<resource methods="DELETE" uri-template="/{bookId}">
    <inSequence>
        <property name="bookId" expression="$ctx:uri.var.bookId"/>
        <redis.init>
            <redisHost>localhost</redisHost>
            <redisPort>6379</redisPort>
            <redisTimeout>500</redisTimeout>
        </redis.init>
        <redis.hDel>
            <redisKey>Library</redisKey>
            <redisFields>{$ctx:bookId}</redisFields>
        </redis.hDel>
        <respond/>
    </inSequence>
    <outSequence/>
    <faultSequence/>
</resource>

Este es un ejemplo muy básico, en nuestra integración podemos realizar diferentes llamadas y lógicas más complicadas para luego almacenar ese valor en base a un identificador. Esto nos puede llevar a mejorar mucho el rendimiento de nuestras integraciones. 

lunes, 11 de enero de 2021

WSO2 Mediator Tutorial: Como usar la cache

Hoy vamos a volver a post sobre WSO2 y sus mediadores. Veremos como funciona el Cache Mediator, el cual puede ser muy util aunque no es muy conocido. 

La idea es sencilla, a través de este mediador, WSO2 cacheará la respuesta de un backend determinado en base a la petición que vayamos a hacer. ¿Como lo hace? Obtiene un valor hash asociado al mensaje almacenado en el flujo. Si existe ejecutará la secuencia configurada en el parámetro onCacheHit, el cual es opcional, obtiene la respuesta almacenada en el cache y envía la respuesta. Si no existe, se realiza la llamada al backend y se almacena la respuesta para su posible uso futuro. 

Vamos a ver un ejemplo sencillo de su uso. Vamos a crear un recurso de una API que llame a un backend, hecho con Wiremock, y antes del cual configuraremos el Cache Mediator para su uso. En el mediador configuraremos varias cosas aparte de las que vienen por defecto. Como son que la cache se mantenga durante 20 segundos, que solo cachee las llamadas de métodos POST y que como mucho almacene 10 elementos en la cache. 

<resource methods="POST" url-mapping="/sample1">
    <inSequence>
        <property name="REST_URL_POSTFIX" scope="axis2" action="remove"/>
        <cache collector="false" timeout="20">
            <onCacheHit>
                <log level="custom">
                    <property name="testing" value="Cache Hit"/>
                </log>
                <respond/>
            </onCacheHit>
            <protocol type="HTTP">
                <methods>POST</methods>
                <headersToExcludeInHash/>
                <responseCodes>.*</responseCodes>
                <enableCacheControl>false</enableCacheControl>
                <includeAgeHeader>false</includeAgeHeader>
                <hashGenerator>org.wso2.carbon.mediator.cache.digest.DOMHASHGenerator</hashGenerator>
            </protocol>
            <implementation maxSize="10"/>
        </cache>
        <send>
            <endpoint>
                <http method="POST" uri-template="http://localhost:57001/sample1"/>
            </endpoint>
        </send>
    </inSequence>
    <outSequence>
        <cache collector="true"/>
        <respond/>
    </outSequence>
</resource>

A continuación realizaremos una invocación a la API y la primera vez veremos como el Wiremock es invocado, y cuando volvemos a llamar una segunda vez no. 

curl --location --request POST 'http://localhost:8280/cache/sample1' \
--header 'Content-Type: application/json' \
--data-raw '{}'

Como veis en el ejemplo, el uso de cache mediator se divide en dos partes. Por un lado la configuración de la propia cache antes de llamar al backend y por otro lado una nueva invocación a la misma pero para indicar que nos devuelva el valor asociado a la misma si es que hay. En esta segunda invocación no es necesario configurar nada, solo indicar a true el parámetro collector. 

El cache mediator también puede ser utilizado para llamadas al backend a través del call mediator. En el caso de que decidamos que queremos usarlo así, tendremos que indicar a Synapse despues de la llamada al backend, que nos encontramos tratando con un mensaje de respuesta. Esto lo haremos a través de la propiedad RESPONSE. Sin esa propiedad, en este ejemplo, el cache mediator no funcionará. 

<resource methods="POST" url-mapping="/sample2">
    <inSequence>
        <property name="REST_URL_POSTFIX" scope="axis2" action="remove"/>
        <cache collector="false" timeout="20">
            <protocol type="HTTP">
                <methods>POST</methods>
                <headersToExcludeInHash/>
                <responseCodes>.*</responseCodes>
                <enableCacheControl>false</enableCacheControl>
                <includeAgeHeader>false</includeAgeHeader>
                <hashGenerator>org.wso2.carbon.mediator.cache.digest.DOMHASHGenerator</hashGenerator>
            </protocol>
            <implementation maxSize="10"/>
        </cache>
        <call>
            <endpoint>
                <http method="POST" uri-template="http://localhost:57001/sample1"/>
            </endpoint>
        </call>
        <property name="RESPONSE" value="true" scope="default" type="STRING"/>
        <cache collector="true"/>
        <respond/>
    </inSequence>
    <outSequence />
</resource>

Como veis es muy sencillo de usar y no requiere de apenas configuración. Por ponerle alguna pega, podriamos decir que no cuenta de un método que permita borrar la cache para un determinado mensaje, por lo que en caso de que la respuesta no valiese ya, solo podría ser cambiada una vez que se dicha cache se caducase.