Artículo
· 15 sep, 2023 Lectura de 42 min

IAM (InterSystems API Manager) - de cero a cien

Este artículo ha sido actualizado con respecto al original en inglés (más antiguo). Los ejemplos que se presentan aquí han sido validados con la última versión de IAM, actualmente la 3.2.1.0-4, y la versión más reciente de InterSystems IRIS, la 2023.2, que es la primera en que ya no se instala por defecto un servidor web.

Este artículo contiene los materiales, ejemplos y ejercicios necesarios para aprender los conceptos básicos de IAM.

Todos los recursos están disponibles en este git: https://github.com/grongierisc/iam-training.

Las soluciones están en esta rama.

alt
En el artículo se tratarán los siguientes temas:

1. Introducción

alt

1.1. ¿Qué es IAM?

IAM son las siglas de InterSystems API Manager, basado en la Kong Enterpise Edition.

Esto significa que concede acceso, además de a la edición open source de Kong, a:

  • Portal del administrador
  • Portal para desarrolladores
  • Plugins avanzados
    • Oauth2
    • Caching
    • ...

alt

1.2. ¿Qué es una Gestión de APIs?

La gestión de APIs es el proceso de creación y publicación de Interfaces de Programación de Aplicaciones (API) web, y de hacer cumplir sus políticas de uso, controlar el acceso, apoyar a la comunidad de suscriptores, recopilar y analizar estadísticas de uso e informar sobre el rendimiento. Los componentes de la gestión de APIs proporcionan mecanismos y herramientas para apoyar a la comunidad de desarrolladores y suscriptores.

alt

1.3. Portal del IAM

Kong e IAM están diseñados como API first, lo que significa que todo lo que se hace en Kong o IAM puede hacerse por medio de llamadas a REST o al Portal de administración.

En este artículo todos los ejemplos y ejercicios se presentarán de dos formas:

Portal IAM API REST
alt alt

1.4.Flujo de este artículo

El objetivo de este artículo es utilizar IAM como proxy de una API REST de IRIS.

La definición de esta API REST se puede encontrar aquí :

http://localhost:52773/swagger-ui/index.html#/

o aquí:

https://github.com/grongierisc/iam-training/blob/training/misc/spec.yml

Empezar este artículo desde la rama principal.

Al final del artículo, se debería tener el mismo resultado que con la rama de formación.

2. Instalación

alt

2.1. ¿Qué se necesita instalar?

2.2. Cómo funciona IAM con IRIS

Al iniciar Kong/IAM, el contenedor verifica la licencia de Kong/IAM con una llamada CURL.

El endpoint de esta llamada es una API REST en el contenedor de IRIS.

Para vuestra información: la licencia de Kong está incorporada en la de IRIS.

alt

2.3. Configuración

Clonar este repositorio con git clone.


git clone https://github.com/grongierisc/iam-training

Ejecutar la API REST inicial:


docker-compose up

Probarla:

http://localhost:52773/swagger-ui/index.html#/

Inicio de sesión/Contraseña: SuperUser/SYS

2.4. Instalación de IAM

2.4.1. Imagen de IRIS

Primero hay que cambiar de la Edición Community a una versión con licencia.

Para ello, hay que configurar el acceso al Registro de Contenedores de InterSystems para descargar las imágenes con acceso limitado de IRIS.

Echad un vistazo a Lanzamiento del Registro de Contenedores de InterSystems en la Comunidad de Desarrolladores.

docker login -u="user" -p="token" containers.intersystems.com
  • Obtener la imagen de InterSystems IRIS:
docker pull containers.intersystems.com/intersystems/irishealth:latest-cd

2.4.2. Imagen de IAM

En la página de distribución de software del Centro de Soporte Internacional (WRC), ir a:

  • Components
    Download archivo IAM-3.2.1.0-4.tar.gz (versión más reciente a día de hoy), descomprimir y extraer del tar y después cargar la imagen:
docker load -i iam_image.tar

2.4.3. Actualización del archivo Docker

Cambiar la Edición Commmunity de IRIS por una versión con licencia.

  • containers.intersystems.com/intersystems/irishealth:latest-cd
  • añadir iris.key en el directorio key

Editar el dockerfile para añadir esta parte encima de él

ARG IMAGE=containers.intersystems.com/intersystems/irishealth:latest-cd
# Frist stage
FROM $IMAGE as iris-iam
COPY key/iris.key /usr/irissys/mgr/iris.key
COPY iris-iam.script /tmp/iris-iam.script
RUN iris start IRIS \
&& iris session IRIS < /tmp/iris-iam.script \
&& iris stop IRIS quietly

# Second stage
FROM iris-iam

Esta parte creará un dockerfile multi-stage.

  • la primera etapa consiste en habilitar a IRIS para que proporcione la licencia de IAM.
  • la segunda etapa consiste en generar la API REST

Crear un nuevo archivo iris-iam.script para generar una nueva imagen de IRIS que habilite el endpoint y el usuario de IAM.

zn "%SYS"
write "Create web application ...",!
set webName = "/api/iam"
set webProperties("Enabled") = 1
set status = ##class(Security.Applications).Modify(webName, .webProperties)
write:'status $system.Status.DisplayError(status)
write "Web application "_webName_" was updated!",!

set userProperties("Enabled") = 1
set userName = "IAM"
Do ##class(Security.Users).Modify(userName,.userProperties)
write "User "_userName_" was updated!",!
halt

2.4.4. Actualización del archivo docker-compose

Actualizar el archivo docker-compose para incluir:
* iris
* argumento para obtener la password durante la reconstrucción de la imagen
* db
* base de datos de Postgres para IAM
* iam-migration
* inicia la base de datos
* iam
* instancia actual de IAM
* un volumen para datos persistentes

Añadir esta parte a la sección de build: del servicio iris: de nuestro docker-compose.yml:

 args:
     - IRIS_PASSWORD=${IRIS_PASSWORD}

Hemos de incorporar también el servicio de webgateway ya que, al contrario que las Community Edition, el resto de versiones de IRIS no instalan un servidor web. Así, añadiremos tras el bloque de iris:, lo siguiente:

  # web gateway container
  webgateway:
    image: ${ISC_WG_IMAGE}
    init: true
    container_name: webgateway
    hostname: webgateway
    ports:
    - "8882:80"
    - "8883:443"
    environment:
    - ISC_DATA_DIRECTORY=/webgateway-shared/durable
    - ISC_CSP_CONF_FILE=/webgateway-shared/CSP.conf
    - ISC_CSP_INI_FILE=/webgateway-shared/CSP.ini
    volumes:
    - type: bind
      source: ./webgateway
      target: /webgateway-shared

IMPORTANTE: Fijémonos en que esto significa que todas las peticiones a las APIs REst de IRIS deberán hacerse ahora a través del host: webgateway, y puerto 80 (si son dentro de la red virtual del docker-compose) o del puerto 8882 si vienen del exterior.

Y por último añadir esta parte al final del archivo** *docker-compose.yml** :

  iam-migrations:
    image: ${ISC_IAM_IMAGE}
    command: kong migrations bootstrap up
    depends_on:
      - db
    environment:
      KONG_DATABASE: postgres
      KONG_PG_DATABASE: ${KONG_PG_DATABASE:-iam}
      KONG_PG_HOST: db
      KONG_PG_PASSWORD: ${KONG_PG_PASSWORD:-iam}
      KONG_PG_USER: ${KONG_PG_USER:-iam}
      KONG_CASSANDRA_CONTACT_POINTS: db
      KONG_PLUGINS: bundled,jwt-crafter
      ISC_IRIS_URL: IAM:${IRIS_PASSWORD}@webgateway:80/api/iam/license
    restart: on-failure
    links:
      - db:db
  iam:
    image: ${ISC_IAM_IMAGE}
    depends_on:
      - db
    environment:
      KONG_ADMIN_ACCESS_LOG: /dev/stdout
      KONG_ADMIN_ERROR_LOG: /dev/stderr
      KONG_ADMIN_LISTEN: '0.0.0.0:8001'
      KONG_ANONYMOUS_REPORTS: 'off'
      KONG_CASSANDRA_CONTACT_POINTS: db
      KONG_DATABASE: postgres
      KONG_PG_DATABASE: ${KONG_PG_DATABASE:-iam}
      KONG_PG_HOST: db
      KONG_PG_PASSWORD: ${KONG_PG_PASSWORD:-iam}
      KONG_PG_USER: ${KONG_PG_USER:-iam}
      KONG_PROXY_ACCESS_LOG: /dev/stdout
      KONG_PROXY_ERROR_LOG: /dev/stderr
      KONG_PORTAL: 'on'
      KONG_PORTAL_GUI_PROTOCOL: http
      KONG_PORTAL_GUI_HOST: '127.0.0.1:8003'
      KONG_ADMIN_GUI_URL: http://localhost:8002
      KONG_PLUGINS: bundled
      ISC_IRIS_URL: IAM:${IRIS_PASSWORD}@webgateway:80/api/iam/license
    volumes: 
      - ./iam:/iam
    links:
      - db:db
    ports:
      - target: 8000
        published: 8000
        protocol: tcp
      - target: 8001
        published: 8001
        protocol: tcp
      - target: 8002
        published: 8002
        protocol: tcp
      - target: 8003
        published: 8003
        protocol: tcp
      - target: 8004
        published: 8004
        protocol: tcp
      - target: 8443
        published: 8443
        protocol: tcp
      - target: 8444
        published: 8444
        protocol: tcp
      - target: 8445
        published: 8445
        protocol: tcp
    restart: on-failure
  db:
    image: postgres:14.5
    environment:
      POSTGRES_DB: ${KONG_PG_DATABASE:-iam}
      POSTGRES_PASSWORD: ${KONG_PG_PASSWORD:-iam}
      POSTGRES_USER: ${KONG_PG_USER:-iam}
    volumes:
      - 'pgdata14:/var/lib/postgresql/data'
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "${KONG_PG_USER:-iam}"]
      interval: 30s
      timeout: 30s
      retries: 3
    restart: on-failure
    stdin_open: true
    tty: true
volumes:
  pgdata14:

Por cierto, esta es la definición de los puertos de Kong:

Puerto Protocolo Descripción
:8000 HTTP Toma el tráfico HTTP recibido de los Consumidores y lo reenvía a los Servicios upstream.
:8443 HTTPS Toma el tráfico HTTPS recibido de los Consumidores y lo reenvía a los Servicios upstream.
:8001 HTTP Admin API. Escucha las llamadas desde la línea de comendo sobre HTTP.
:8444 HTTPS Admin API. Escucha las llamadas desde la línea de comendo sobre HTTPS.
:8002 HTTP Kong Manager (GUI). Escucha tráfico HTTP.
:8445 HTTPS Kong Manager (GUI). Escucha tráfico HTTPS.
:8003 HTTP Portal para desarrolladores. Escucha tráfico HTTP, suponiendo que el Portal para desarrolladores está habilitado.
:8446 HTTPS Portal para desarrolladores. Escucha tráfico HTTPS, suponiendo que el Portal para desarrolladores está habilitado.
:8004 HTTP Portal para desarrolladores/tráfico de archivos por medio de HTTP, suponiendo que el Portal para desarrolladores esté habilitado.
:8447 HTTPS Portal para desarrolladores/tráfico de archivos por medio de HTTPS, suponiendo que el Portal para desarrolladores esté habilitado.

2.4.5. Añadir variables de entorno en un archivo .env

Como vemos, por facilidad de uso y por seguridad, no incluimos el nombre/tag:version de la imagen de IAM que queremos utilizar, ni la del webgateway ni la password por defecto que queremos para nuestro usuario administrador en IRIS. Esta información la pasaremos como variables de entorno en el momento de la reconstrucción de la imagen de IRIS y del arranque de los contenedores.
Para ello simplemente hemos de crear un fichero llamado: .env, en el directorio donde se encuentre nuestro docker-compose.yml, que deberá contener lo siguiente:

ISC_IAM_IMAGE=intersystems/iam:3.2.1.0-4
ISC_WG_IMAGE=containers.intersystems.com/intersystems/webgateway:latest-cd
IRIS_PASSWORD=SYS

2.4.6. Prueba


docker-compose -f "docker-compose.yml" up -d --build

3. Primer Servicio/Ruta

alt

¿Recordáis cómo funciona Kong/IAM?

alt

En este caso, vamos a crear:

  • un servicio
    • para nuestra API CRUD
  • una ruta
    • para acceder a este servicio

3.1. Cómo crear un servicio

Recordad que para acceder al portal del IAM debéis acceder por el puerto 8002, con:

http://localhost:8002/overview

ATENCIÓN: Hay un error en la imagen, el host debería ser el webgateway, por donde han de entrar todas las peticiones web a IRIS, y el puerto el 80, al estar el IAM y el webgateway en la misma red interna de docker

Portal IAM API REST

foo



# Create service
curl -i -X POST \
--url http://localhost:8001/services/ \
--data 'name=crud' \
--data 'url=http://webgateway:80/crud/'

Lo que vemos aquí es que para crear un servicio simplemente necesitamos su URL y que podemos hacerlo tanto utilizando el portal como vía llamadas rest directamete a la API REst del IAM.

3.2. Cómo crear una ruta

Portal IAM API REST

foo


# Create route

curl -i -X POST \
--url http://localhost:8001/services/crud/routes \
--data 'name=crud-route' \
--data 'paths=/persons/all' \
--data 'strip_path=false'

Lo que vemos aquí es que para crear un servicio necesitamos basicamente el nombre del servicio.

3.3. Prueba

API original API vía IAM

# No IAM
# Versiones pre-2023.2 
curl –i --location --request GET 'http://localhost:52773/crud/persons/all' \
--header 'Authorization: Basic U3VwZXJVc2VyOlNZUw=='
# Versiones 2023.2+
curl –i --location --request GET 'http://webgateway:8882/crud/persons/all' \
--header 'Authorization: Basic U3VwZXJVc2VyOlNZUw=='

# IAM
curl –i --location --request GET 'http://localhost:8000/persons/all' \
--header 'Authorization: Basic U3VwZXJVc2VyOlNZUw=='

Qué vemos aquí:

  • Nada nuevo en las opciones sin IAM.
  • En el lado de Kong:
    • Cambiamos el puerto
    • El path viene a ser la ruta
    • Aún necesitamos realizar la autenticación

4. Avanza más con plugins

Para continuar, intentaremos que el IAM se auto-autentique en el endpoint de IRIS.

Para ello, utilizaremos el plugin request-transformer.

alt

4.1. Añadir un plugin al servicio

Portal IAM API REST

foo


# Create plugin
curl -i -X POST \
--url http://localhost:8001/services/crud/plugins \
--data 'name=request-transformer' \
--data 'config.add.headers=Authorization:Basic U3VwZXJVc2VyOlNZUw==' \
--data 'config.replace.headers=Authorization:Basic U3VwZXJVc2VyOlNZUw=='

4.2. Prueba

API original API via IAM

# Legacy


curl –i -v --location --request \
    GET 'http://localhost:8882/crud/persons/all'

# IAM
curl –i --location --request \
     GET 'http://localhost:8000/persons/all' 

Qué vemos aquí:

  • El error 401 en la API original
  • Obtuvimos los datos sin realizar la autenticación

5.Añadir nuestra propia autenticación

Lo que queremos lograr aquí es añadir nuestra propia autenticación sin ninguna interrupción de la API original.

alt

5.1. Añadir consumidores

Portal IAM API REST

foo


# Add consumer anonymous
curl -i -X POST \
--url http://localhost:8001/consumers/ \
--data "username=anonymous" \
--data "custom_id=anonymous"

foo


# Add consumer user
curl -i -X POST \
--url http://localhost:8001/consumers/ \
--data "username=user" \
--data "custom_id=user"

5.2. Añadir el plugin de autenticación básica

Al mismo tiempo que añadimos el plugin de autenticación a la ruta crud-route, indicamos que se asocie cualquier petición no autenticada con el usuario anonymous que acabamos de crear (utilizamos el uuid que nos ha generado al crearlo en el paso anterior).

Portal IAM API REST

foo


# Enable basic auth for service
curl -i -X POST http://localhost:8001/routes/crud-route/plugins \
--data "name=basic-auth" \
--data "config.anonymous=5cc8dee4-066d-492e-b2f8-bd77eb0a4c86" \
--data "config.hide_credentials=false"

Donde:

  • config.anonymous = Identificador único universal (UUID) del consumidor anonymous
    (Atención: sustitúyelo por el uuid generado por tu instalación para ese usuario)

5.3. Añadir el plugin ACL

ACL es un plugin para control de tráfico, que nos va a permitir controlar qué usuarios pueden acceder a los servicios.

Portal IAM API REST

foo


# Habilitar ACL para una ruta
curl -i -X POST http://localhost:8001/routes/crud-route/plugins \
--data "name=acl" \
--data "config.allow=user"

# Habilitar ACL globalmente
curl -i -X POST http://localhost:8001/plugins \
--data "name=acl" --data "config.allow=user"

Como véis podemos decidir habilitar el plugin globalmente (para todos los servicios, rutas y consumidores) o con un alcance más limitado (en el ejemplo para la ruta: crud-route; con la que estamos trabajando).

5.4. Configurar el USER con ACL y credenciales

Cada plugin requiere su configuración. En el caso de ACL necesitamos que los usuarios estén asociados a grupos sobre los que luego podamos aplicar políticas de acceso. Así que vamos a asociar al usuario: user; a un grupo, también llamado: user, que está configurado en el plugin como que tiene permiso de acceso. Para el plugin de autenticación básica lo que hacemos es asignar una password al usuario: user .
Si utilizamos el portal, debemos ir a Consumers y pinchar en la opción Credentials. Allí nos aparecerán los plugins habilitados para el consumidor y que necesiten alguna configuración relacionada con el acceso.

Portal IAM API REST

foo


# Add consumer group
curl -i -X POST \
--url http://localhost:8001/consumers/user/acls \
--data "group=user"
# Add consumer credentials
curl -i -X POST http://localhost:8001/consumers/user/basic-auth \
--data "username=user" \
--data "password=user"

5.5. Prueba

API original:


# Legacy
curl –i --location --request GET 'http://localhost:8882/crud/persons/all' \
--header 'Authorization:Basic dXNlcjp1c2Vy'

API de IAM:


# IAM
curl –i --location --request GET 'http://localhost:8000/persons/all' \
--header 'Authorization:Basic dXNlcjp1c2Vy'

donde la cadena dXNlcjp1c2Vy es user:user codificada en Base64.

6. Ejercicio. Limitación del número de peticiones por unidad de tiempo (rate-limiting)

  1. Habilitar un usuario no autenticado
  2. Limitar la tasa a 2 llamadas por minuto para los usuarios no autenticados

6.1. Solución

  1. Habilitar a un usuario no autenticado. Añadimos al usuario anonymous como parte del grupo: user del plugin ACL. Recordemos que este grupo era el que tiene permitido el acceso a la ruta crud-route (config.alow=user)
Portal IAM API REST

foo


# Add consumer group

curl -i -X POST \
--url http://localhost:8001/consumers/anonymous/acls \
--data "group=user"

  1. Limitar la tasa a 2 llamadas por minuto para el usuario anonymous (recordemos que el plugin de autenticación básica hará que todo usuario no autenticado se identifique como anonymous).
Portal IAM API REST

foo


# Add rate limit consumer
curl -i -X POST \
--url http://localhost:8001/consumers/anonymous/plugins \
--data "name=rate-limiting" \
--data "config.limit_by=consumer" \
--data "config.minute=2"

7. Portal para desarrolladores

alt

7.1. Descripción breve

El Portal para desarrolladores de Kong proporciona:

  • una sola fuente confiable para todos los desarrolladores
  • gestion intuitiva de contenidos para la documentación
  • incorporación simplificada de desarrolladores
  • control de acceso basado en roles (RBAC)

alt

7.2. Activación

Portal IAM API REST

foo


curl -X PATCH http://localhost:8001/workspaces/default \
    --data "config.portal=true"

7.3. Añadir la primera especificación

Portal IAM API REST

foo

foo


curl -X POST http://localhost:8001/default/files \
    -F "path=specs/iam-training.yml" \
    -F "contents=@misc/spec.yml"

7.4. Prueba

http://localhost:8003/default/documentation/iam-training

¿Qué ocurre?
¿Cómo solucionarlo?

La especificación sólo tiene configurado un servidor para acceso seguro (https, sobre puerto 8443), que nosotros no hemos configurado. La forma de rodear este problema es simplemente añadiendo otro servidor más a nuestra especificación. Busquemos en el fichero la sección servers: y añadamos la línea:


    - url: http://localhost:8000/

7.5. Ejercicio

  1. Añadir el plugin CORS en la ruta

7.5.1. Solución

Portal IAM API REST

foo


# Enable CORS

curl -i -X POST http://localhost:8001/routes/crud-route/plugins \
--data "name=cors"

8. Portal para desarrolladores, parte dos, Autenticación

8.1. Habilitar la autenticación básica

Portal IAM Configuración de la sesión (JSON)

foo


{
    "cookie_secure": false,
    "cookie_name": "portal_session",
    "secret": "SYS",
    "storage": "kong"
}

Ahora la autenticación está habilitada en el Portal para desarrolladores.

8.2. Limitar el acceso

Por defecto, el usuario no autenticado tiene acceso a todo.

Podemos crear un rol para limitar algunos accesos.

Por ejemplo, vamos a restringir el acceso a nuestra documentación de la API CRUD.

8.2.1. Crear un rol

Desde el Portal IAM:

foo

o vía API REst:


# Enable role
curl -i -X POST http://localhost:8001/default/developers/roles \
--data "name=dev"

8.2.2. Añadir un rol a la especificación

Desde el Portal IAM:

foo

o vía API Rest:


# Enable role

curl 'http://localhost:8001/default/files/specs/iam-training.yml' -X PATCH -H 'Accept: application/json, text/plain, */*'  --compressed -H 'Content-Type: application/json;charset=utf-8' -H 'Origin: http://localhost:8002' -H 'Referer: http://localhost:8002/default/portal/permissions/' --data-raw $'{"contents":"x-headmatter:\\n  readable_by:\\n    - dev\\nswagger: \'2.0\'\\ninfo:\\n  title: InterSystems IRIS REST CRUD demo\\n  description: Demo of a simple rest API on IRIS\\n  version: \'0.1\'\\n  contact:\\n    email: apiteam@swagger.io\\n  license:\\n    name: Apache 2.0\\n    url: \'http://www.apache.org/licenses/LICENSE-2.0.html\'\\nhost: \'localhost:8000\'\\nbasePath: /\\nschemes:\\n  - http\\nsecurityDefinitions:\\n  basicAuth:\\n    type: basic\\nsecurity:\\n  - basicAuth: []\\npaths:\\n  /:\\n    get:\\n      description: \' PersonsREST general information \'\\n      summary: \' Server Info \'\\n      operationId: GetInfo\\n      x-ISC_CORS: true\\n      x-ISC_ServiceMethod: GetInfo\\n      responses:\\n        \'200\':\\n          description: (Expected Result)\\n          schema:\\n            type: object\\n            properties:\\n              version:\\n                type: string\\n        default:\\n          description: (Unexpected Error)\\n  /persons/all:\\n    get:\\n      description: \' Retreive all the records of Sample.Person \'\\n      summary: \' Get all records of Person class \'\\n      operationId: GetAllPersons\\n      x-ISC_ServiceMethod: GetAllPersons\\n      responses:\\n        \'200\':\\n          description: (Expected Result)\\n          schema:\\n            type: array\\n            items:\\n              $ref: \'#/definitions/Person\'\\n        default:\\n          description: (Unexpected Error)\\n  \'/persons/{id}\':\\n    get:\\n      description: \' Return one record fo Sample.Person \'\\n      summary: \' GET method to return JSON for a given person id\'\\n      operationId: GetPerson\\n      x-ISC_ServiceMethod: GetPerson\\n      parameters:\\n        - name: id\\n          in: path\\n          required: true\\n          type: string\\n      responses:\\n        \'200\':\\n          description: (Expected Result)\\n          schema:\\n            $ref: \'#/definitions/Person\'\\n        default:\\n          description: (Unexpected Error)\\n    put:\\n      description: \' Update a record in Sample.Person with id \'\\n      summary: \' Update a person with id\'\\n      operationId: UpdatePerson\\n      x-ISC_ServiceMethod: UpdatePerson\\n      parameters:\\n        - name: id\\n          in: path\\n          required: true\\n          type: string\\n        - name: payloadBody\\n          in: body\\n          description: Request body contents\\n          required: false\\n          schema:\\n            type: string\\n      responses:\\n        \'200\':\\n          description: (Expected Result)\\n        default:\\n          description: (Unexpected Error)\\n    delete:\\n      description: \' Delete a record with id in Sample.Person \'\\n      summary: \' Delete a person with id\'\\n      operationId: DeletePerson\\n      x-ISC_ServiceMethod: DeletePerson\\n      parameters:\\n        - name: id\\n          in: path\\n          required: true\\n          type: string\\n      responses:\\n        \'200\':\\n          description: (Expected Result)\\n        default:\\n          description: (Unexpected Error)\\n  /persons/:\\n    post:\\n      description: \' Creates a new Sample.Person record \'\\n      summary: \' Create a person\'\\n      operationId: CreatePerson\\n      x-ISC_ServiceMethod: CreatePerson\\n      parameters:\\n        - name: payloadBody\\n          in: body\\n          description: Request body contents\\n          required: false\\n          schema:\\n            type: string\\n      responses:\\n        \'200\':\\n          description: (Expected Result)\\n        default:\\n          description: (Unexpected Error)\\ndefinitions:\\n  Person:\\n    type: object\\n    properties:\\n      Name:\\n        type: string\\n      Title:\\n        type: string\\n      Company:\\n        type: string\\n      Phone:\\n        type: string\\n      DOB:\\n        type: string\\n        format: date-time\\n"}'


Lo importante aquí es esta parte:

x-headmatter:
  readable_by:
    - dev

Consultad esta documentación: readable_by attribute

8.2.3. Prueba

8.2.3.1. Registrar a un nuevo desarrollador

video

8.2.3.2. Aprobar a este desarrollador

video

8.2.3.3. Añadir un rol para este desarrollador

video


curl 'http://localhost:8001/default/developers/dev@dev.com' -X PATCH \
  --compressed -H 'Content-Type: application/json;charset=utf-8' -H 'Cache-Control: no-cache' \
  -H 'Origin: http://localhost:8002' -H 'DNT: 1' -H 'Connection: keep-alive' \
  -H 'Referer: http://localhost:8002/default/portal/permissions/dev/update' \
  -H 'Pragma: no-cache' --data-raw '{"roles":["dev"]}'

8.3. Añadir Oauth2 para el desarrollador

En esta parte añadiremos una autenticación Oauth2 para que los desarrolladores puedan utilizar de forma segura nuestra API CRUD.

Este flujo proporcionará un auto-registro a los desarrolladores y les dará acceso a la API CRUD.

8.3.1. Primero: eliminar la autenticación básica

Para ello, reemplazaremos nuestra autenticación básica por un bearToken.

Primero hay que desactivar nuestra autenticación/acl básica.

Desde el Portal IAM:

foo

o vía API REst:


# Disable ACL Plugin

curl 'http://localhost:8001/default/routes/afefe836-b9be-49a8-927a-1324a8597a9c/plugins/3f2e605e-9cb6-454a-83ec-d1b1929b1d30' \
-X PATCH --compressed -H 'Content-Type: application/json;charset=utf-8' -H 'Cache-Control: no-cache' \
-H 'Origin: http://localhost:8002' -H 'DNT: 1' -H 'Connection: keep-alive' \
-H 'Referer: http://localhost:8002/default/plugins/acl/31fe4e9e-d965-4afc-97ee-9448a4db9125/update' \
-H 'Pragma: no-cache' \
--data-raw '{"enabled":false,"name":"acl","route":{"id":"afefe836-b9be-49a8-927a-1324a8597a9c"},"config":{"hide_groups_header":false,"allow":["user","dev","crud"]}}'

donde afefe836-b9be-49a8-927a-1324a8597a9c es el ID de la ruta y 3f2e605e-9cb6-454a-83ec-d1b1929b1d30 es el ID del plugin ACL.

8.3.2. Segundo: añadir el plugin Oauth2

Portal IAM API REST

foo


# Create application-registration plugin

curl -i -X POST \
--url http://localhost:8001/services/crud/plugins \
--data 'name=oauth2' \
--data 'config.auth_header_name=authorization' \
--data 'config.enable_client_credentials=true' 

8.3.3. Enlazar el servicio y la documentación

Desde el Portal IAM:

foo

a través del API REst:


curl 'http://localhost:8001/default/document_objects' --compressed -H 'Content-Type: application/json;charset=utf-8' -H 'Cache-Control: no-cache' -H 'Origin: http://localhost:8002' -H 'DNT: 1' -H 'Connection: keep-alive' -H 'Referer: http://localhost:8002/default/services/create-documentation' -H 'Pragma: no-cache' --data-raw '{"service":{"id":"7bcef2e6-117c-487a-aab2-c7e57a0bf61a"},"path":"specs/iam-training.yml"}'

8.3.3.1. Prueba

Desde el portal para desarrolladores, iniciar sesión como dev@dev.com y crear una nueva aplicación.

alt

Esto dará un client_id y un client_secret.

Estos se pueden utilizar en el Portal para desarrolladores de Swagger.

Registrar esta aplicación en el servicio CRUD:

alt

Obtener el token:


curl --insecure -X POST https://localhost:8443/persons/oauth2/token \
  --data "grant_type=client_credentials" \
  --data "client_id=2TXNvDqjeVMHydJbjv9t96lWTXOKAtU8" \
  --data "client_secret=V6Vma6AtIvl04UYssz6gAxPc92eCF4KR"

Usar el token:


curl --insecure -X GET https://localhost:8443/persons/all \
  --header "authorization: Bearer u5guWaYR3BjZ1KdwuBSC6C7udCYxj5vK"

9. Portal de administración seguro

ATENCIÓN: El contenido de este apartado se ha modificado con respecto al contenido del mismo apartado en el artículo original ya que áquel no funcionaba en la nueva versión de Kong.

9.1. Crear un administrador

La forma más sencilla de asegurar nuestro API manager es iniciar el IAM (Kong) con un usuario con el role super-admin. Para ello, en la primera instanciación, antes de que se haya generado la BBDD asociada al IAM, deberemos haber incluido en nuestro docker-compose:

Bajo el servicio iam-migrations:

      KONG_PASSWORD: kong

y, bajo el servicio iam:

      KONG_PASSWORD: kong
      KONG_ENFORCE_RBAC: 'on'
      KONG_ADMIN_GUI_AUTH: 'basic-auth'
      KONG_ADMIN_GUI_SESSION_CONF: '{"secret":"${IRIS_PASSWORD}","storage":"kong","cookie_secure":false}'

Esto hará que el IAM se inicie con una configuración de autenticación básica. Por defecto tendremos disponible un usuario: kong_admin; con role super-admin, que nos permitirá crear cualquier otro tipo de usuario, incluido usuarios administradores.

Una vez en el portal, podremos "Invitar" a usuarios administradores. Para hacerlo:

  • Ir a Teams
  • Hacer clic en Invite admin
    • Especificar el email
    • Especificar el Username
    • Marcar el rol que deseemos, super-admin, admin, read-only,...
    • Invitar
  • Ir a Invited Admin
  • Hacer clic en View
  • Hacer clic en Generate link

Con ese link podremos activar el nuevo usuario administrador. Simplemente tenemos que pegarlo en el browser y darle a enter. Nos debería aparecer un mensaje indicando que el usuario se ha activado correctamente.

9.2. Usar la API del Administrador de Kong con el RBAC

Con un usuario de tipo RBAC ya no podemos utilizar la API de administración del IAM (Kong):


curl -s -X GET \
  --url http://localhost:8001/routes

Se recibe este error:

{"message":"Invalid credentials. Token or User credentials required"}

Necesitamos pasar las credenciales (token) en la llamada. Si no tenemos el token de nuestro usuario administrador, podemos regenerarlo. Entramos en el portal con su usuario y password, vamos al Profile, opción Editar y, una vez dentro, hacemos click en Regenerar Token. Lo podemos copiar y utilizar en nuestra llamada con curl.


curl -s -X GET \
  --url http://localhost:8001/routes \
  --header "Kong-Admin-Token: eBI8jb4V4kHlYWFhdQDVqNydVKHgIjxz"

10. Plugins

Apartado obsoleto. Actualmente varias de los contenidos de este apartado no funcionan con las últimas versiones de Kong y por cómo está descrito no permite ajuste sin cambiarlo radicalmente. Aparece en la traducción actual por mantener la integridad del artículo original.

alt

Kong incluye plugins de alta calidad.

Pero, ¿qué ocurre si necesitamos un plugin que no está incluído? ¿O si queremos plugins creados por la comunidad?

En este apartado hablaremos sobre los plugins creado por la comunidad y cómo importarlos.

A continuación, veremos cómo crear nuestro propio plugin.

10.1. Importar un plugin no incluido en Kong

En esta parte, utilizaremos el plugin jwt-crafter.

Este plugin añade la posibilidad de generar un token JWT dentro del propio IAM (Kong), eliminando la necesidad de que un servicio upstream sea el que genere el token.

Este es el plugin:

https://github.com/grongierisc/kong-plugin-jwt-crafter

Para instalar este plugin, como estamos utilizando la versión docker, necesitamos crear una nueva imagen que integre el plugin.

10.1.1. Generar una nueva imagen Docker para Kong/IAM con el plugin de la comunidad

  1. Crear una carpeta llamada IAM en la raíz de este git.
  2. Crear un dockerfile en esta nueva carpeta
  3. Crear una carpeta llamada plugins
    1. Aquí es donde añadiremos todos nuestros plugins externos
  4. Actualizar el archivo docker-compose para habilitar el nuevo plugin

En la carpeta plugins, clonar nuestro plugin externo con git clone.


git clone https://github.com/grongierisc/kong-plugin-jwt-crafter

El dockerfile debería verse así:

FROM intersystems/iam:3.2.1.0-4

USER root
COPY ./plugins /custom/plugins

RUN yum -y install unzip gzip gcc
RUN luarocks install luaossl OPENSSL_DIR=/usr/local/kong/ CRYPTO_INCDIR=/usr/local/kong/include

RUN cd /custom/plugins/kong-plugin-jwt-crafter && luarocks make

USER kong

¿Qué vemos en este dockerfile?

Básicamente, una vez tenemos los componentes de SO necesarios, para instalar un plugin externo, debemos ir a su carpeta raíz (donde está el rockspec) y llamar a luarocks make. Y eso es todo. Ya está instalado el plugin.

Para la parte de docker-compose:

  1. Editar la etiqueta imagen de IAM
    (puedes incluirlo directamente, y el build posterior la creará con este nombre, o cambiar el fichero .env e indicar el nombre de la imagen custom en la variable ISC_IAM_IMAGE)
    intersystems/iam:3.2.1.0-4 -> intersystems/iam-custom:3.2.1.0-4

  2. Añadir un build context en el servicio iam

    build: 
      context: iam
      dockerfile: dockerfile
  1. Habilitar el plugin en las variables de entorno
KONG_PLUGINS: 'bundled,jwt-crafter'

Ahora generamos nuestra nueva imagen IAM :


docker-compose build iam


este comando por defecto le da a la nueva imagen el nombre que aparezca en el campo* image:* del servicio. Ese nombre será el que hayamos indicado en la variable ISC_IAM_IMAGE (definida en nuestro archivo .env)

10.1.2. Prueba


docker-compose up -d

En plugin -> new, al final de la lista se ve el plugin jwt-crafter.

alt

10.1.2.1. Uso

  1. Crear un nuevo servicio:
Portal IAM API REST

foo


# Create service

curl -i -X POST \
--url http://localhost:8001/services/ \
--data 'name=crud-persons' \
--data 'url=http://webgateway:80/crud/persons/'

  1. Crear una ruta
Portal IAM API REST

foo


# Create route

curl -i -X POST \
--url http://localhost:8001/services/crud-persons/routes \
--data 'name=crud-route-jwt' \
--data 'paths=/crud/persons/*' \
--data 'strip_path=true'

  1. Reutilizar nuestra autenticación automática
Portal IAM API REST

foo


# Create plugin
curl -i -X POST \
--url http://localhost:8001/services/crud-persons/plugins \
--data 'name=request-transformer' \
--data 'config.add.headers=Authorization:Basic U3VwZXJVc2VyOlNZUw==' \
--data 'config.replace.headers=Authorization:Basic U3VwZXJVc2VyOlNZUw=='

Ya estamos listos, este es el uso real de jwt-crafter.

# Add acl to route
curl -i -X POST http://localhost:8001/routes/crud-route-jwt/plugins \
    --data "name=acl"  \
    --data "config.whitelist=test" \
    --data "config.hide_groups_header=false"

# Create service
curl -i -X POST \
  --url http://localhost:8001/services/ \
  --data 'name=jwt-login' \
  --data 'url=http://neverinvoked/'

# Create route
curl -i -X POST \
  --url http://localhost:8001/services/jwt-login/routes \
  --data 'name=jwt-login-route' \
  --data 'paths=/jwt/log-in'

# Enable basic auth for service
curl -i -X POST http://localhost:8001/routes/jwt-login-route/plugins \
    --data "name=basic-auth"  \
    --data "config.hide_credentials=false"

# Enable basic auth for service
curl -i -X POST http://localhost:8001/routes/jwt-login-route/plugins \
    --data "name=jwt-crafter"  \
    --data "config.expires_in=86400"

# Add consumer
curl -i -X POST \
   --url http://localhost:8001/consumers/ \
   --data "username=test"

# Add consumer group
curl -i -X POST \
   --url http://localhost:8001/consumers/test/acls \
   --data "group=test"

# Add consumer credentials
curl -i -X POST http://localhost:8001/consumers/test/basic-auth \
    --data "username=test" \
    --data "password=test"
curl -i -X POST http://localhost:8001/consumers/test/jwt \
    --data "key=test" \
    --data "algorithm=HS256"

# JWT plugins
curl -i -X POST http://localhost:8001/routes/crud-route-jwt/plugins \
    --data "name=jwt" 

Prueba

# test:test is base64 encoded
curl -H 'Authorization: basic dGVzdDp0ZXN0' localhost:8000/jwt/log-in
curl --location --request GET 'http://localhost:8000/crud/persons/all' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW0iOiJ0ZXN0Iiwic3ViIjoiODJiNjcwZDgtNmY2OC00NDE5LWJiMmMtMmYxZjMxNTViN2E2Iiwicm9sIjpbInRlc3QiXSwiZXhwIjoxNjE2MjUyMTIwLCJpc3MiOiJ0ZXN0In0.g2jFqe0hDPumy8_gG7J3nYsuZ8KUz9SgZOecdBDhfns'

10.2. Crear un nuevo plugin

Este no es el lugar para aprender Lua.

Pero os daré algunos consejos sobre cómo reiniciar rápidamente IAM para probar nuestro nuevo desarrollo.

10.2.1. Estructura de los archivos

kong-plugin-helloworld
├── kong
│   └── plugins
│       └── helloworld
│           ├── handler.lua
│           └── schema.lua
└── kong-plugin-helloworld-0.1.0-1.rockspec

Por costumbre, los plugins de kong deben tener el prefijo kong-plugin.

En nuestro ejemplo, el nombre del plugin es helloworld.

Hay tres archivos obligatorios:

  • handler.lua: es el núcleo de vuestro plugin. Es una interfaz para implementar, en la que cada función se ejecutará en el momento deseado del ciclo de vida de una solicitud/conexión.
  • schema.lua: vuestro plugin probablemente debe retener algún tipo de configuración introducida por el usuario. Este módulo contiene el esquema de esa configuración y define las reglas que se aplican sobre él, para que el usuario solo pueda introducir valores de configuración válidos.
  • *.rockspec: Rockspec: es un archivo de especificación de paquetes. Un script declarativo de Lua, con reglas sobre cómo generar y empaquetar rocks *.rockspec, un archivo Lua que contiene algunas tablas.

10.2.1.1. handler.lua

La interfaz de plugins permite anular cualquiera de los siguientes métodos en vuestro archivo handler.lua, para implementar una lógica personalizada en varios puntos de acceso durante la ejecución del ciclo de vida de Kong:

Nombre de la función Fase Descripción
:init_worker() init_worker Se ejecuta cuando inicia cada proceso en el worker de Nginx.
:certificate() ssl_certificate Se ejecuta durante la fase de entrega del certificado SSL durante el establecimiento de la comunicación (handshake) con SSL.
:rewrite() rewrite Se ejecuta para cada solicitud que es recibida de un cliente como controlador de la fase de reescritura. NOTA: en esta fase no se identificaron ni el Servicio ni el Consumidor, por lo que este controlador solamente se ejecutará si el complemento se configuró como un complemento global.
:access() access Se ejecuta para cada solicitud proveniente de un cliente y antes de que esta sea reenviada al servicio de las fases preliminares.
:response() access Reemplaza tanto a header_filter() como a body_filter(). Se ejecuta después de que todas las respuestas hayan sido recibidas del servicio de las fases preliminares, pero antes de enviar cualquier parte de las mismas al cliente.
:header_filter() header_filter Se ejecuta cuando todas las cabeceras de bytes fueron recibidas en el servicio de las fases preliminares.
:body_filter() body_filter Se ejecuta para cada fragmento recibido que proviene del contenido de la respuesta desde el servicio de las fases preliminares. Dado que la respuesta se transmite de vuelta al cliente, puede exceder el tamaño del búfer y sería transmitido por fragmentos, por lo tanto, se puede llamar a este método varias veces si la respuesta es grande. Consulte la documentación del módulo lua-nginx para obtener más información.
:log() log Se ejecuta cuando el último byte de la respuesta es enviado al cliente.
10.2.1.1.1. Ejemplo
local BasePlugin = require "kong.plugins.base_plugin"

local HelloWorldHandler = BasePlugin:extend()

function HelloWorldHandler:new()
  HelloWorldHandler.super.new(self, "helloworld")
end

function HelloWorldHandler:access(conf)
  HelloWorldHandler.super.access(self)
  if conf.say_hello then
    ngx.log(ngx.ERR, "============ Hello World! ============")
    ngx.header["Hello-World"] = "Hello World!!!"
  else
    ngx.log(ngx.ERR, "============ Bye World! ============")
    ngx.header["Hello-World"] = "Bye World!!!"
  end
end

return HelloWorldHandler

10.2.1.2. schema.lua

El archivo de configuración se ve en el portal.

return {
    no_consumer = true,
    fields = {
      say_hello = { type = "boolean", default = true },
      say_hello_body = { type = "boolean", default = true }
    }
  }

10.2.1.3. *.rockspec

package = "kong-plugin-helloworld"  -- hint: rename, must match the info in the filename of this rockspec!
                                  -- as a convention; stick to the prefix: `kong-plugin-`
version = "0.1.0-1"               -- hint: renumber, must match the info in the filename of this rockspec!
-- The version '0.1.0' is the source code version, the trailing '1' is the version of this rockspec.
-- whenever the source version changes, the rockspec should be reset to 1. The rockspec version is only
-- updated (incremented) when this file changes, but the source remains the same.

-- TODO: This is the name to set in the Kong configuration `plugins` setting.
-- Here we extract it from the package name.
local pluginName = package:match("^kong%-plugin%-(.+)$")  -- "myPlugin"

supported_platforms = {"linux", "macosx"}
source = {
  url = "https://github.com/grongierisc/iam-training",
  branch = "master",
--  tag = "0.1.0"
-- hint: "tag" could be used to match tag in the repository
}

description = {
  summary = "This a demo helloworld for Kong plugin",
  homepage = "https://github.com/grongierisc/iam-training",
  license = "Apache 2.0"
}

dependencies = {
   "lua >= 5.1"
   -- other dependencies should appear here
}

build = {
  type = "builtin",
  modules = {
    ["kong.plugins."..pluginName..".handler"] = "kong/plugins/"..pluginName.."/handler.lua",
    ["kong.plugins."..pluginName..".schema"] = "kong/plugins/"..pluginName.."/schema.lua",
  }
}

10.2.2. Creación

Vamos a hacer lo mismo que aquí: 11.1.1. Generar una nueva imagen Docker para Kong/IAM con el plugin externo

Pero adaptado a nuestro plugin:

Dockerfile:

FROM intersystems/iam:1.5.0.9-4

USER root
COPY ./plugins /custom/plugins

RUN cd /custom/plugins/kong-plugin-jwt-crafter && luarocks make
RUN cd /custom/plugins/kong-plugin-helloworld && luarocks make

#USER kong #Stay with root use, we will see why later

Habilitar el plugin en las variables de entorno

KONG_PLUGINS: 'bundled,jwt-crafter,helloworld'

Ahora generamos nuestra nueva imagen IAM:


docker-compose build iam

Y después docker-compose upy probadlo.

10.2.3. Consejos

Para ejecutar el contenedor de IAM en el "modo depuración", para pararlo/reiniciarlo fácilmente, modificar el dockerfile para añadir/eliminar el plugin y así sucesivamente.

Se puede detener el servicio de IAM:


docker-compose stop iam

E iniciarlo en modo ejecución con un intérprete:


docker-compose run -p 8000:8000 -p 8001:8001 -p 8002:8002 iam sh

En el contenedor:


./docker-entrypoint.sh kong

Feliz programación :)

11. CI/CD

alt

Estamos cerca del final de este artículo.

Para terminar, hablaremos sobre DevOps/CI/CD. El objetivo de este capítulo es dar algunas ideas sobre cómo implementar CI/CD para IAM/Kong.

Como IAM es API first, la idea es definir todas las llamadas a REST y después reproducirlas en cada entorno.

La forma más sencilla de definir las llamadas a REST es con Postman y su mejor amigo Newman (la versión de línea de comandos de Postman).

11.1. Crear una colección en Postman

Una cosa útil de Postman es su capacidad para ejecutar un script antes y después de una llamada a REST.

En la mayoría de los casos utilizaremos esta funcionalidad.

11.1.1. ¿Está funcionando IAM?

Nuestro primer script verificará si IAM está activo y funcionando.

alt

var iam_url = pm.environment.get("iam_url");
var iam_config_port = pm.environment.get("iam_config_port");
var url = "http://" + iam_url + ":" + iam_config_port + "/";
SenReq(20);
async function SenReq(maxRequest) {
    var next_request = "end request";
    const result = await SendRequest(maxRequest);
    console.log("result:",result);
    if(result == -1)
    {
        console.error("IAM starting .... failed !!!!");
    }
}

function SendRequest(maxRequest) {
    return new Promise(resolve => {
        pm.sendRequest(url,
            function (err) {
                if (err) {
                    if (maxRequest > 1) {
                        setTimeout(function () {}, 5000);
                        console.warn("IAM not started...retry..next retry in 5 sec");
                        SendRequest(maxRequest - 1);
                    } else {
                        console.error("IAM starting .... failed");
                        resolve(-1);
                    }
                } else {
                    console.log("IAM starting .... ok");
                    resolve(1);
                }

            }
        );
    });
}

11.1.2. Eliminar los datos antiguos


var iam_url=pm.environment.get("iam_url"); var iam_config_port=pm.environment.get("iam_config_port"); pm.sendRequest("http://"+iam_url+":"+iam_config_port+"/plugins", function (err, res) { if (err) { console.log("ERROR : ",err); } else { var body_json=res.json(); if(body_json.data) { for( i=0; i < body_json.data.length; i++) { // Example with a full fledged SDK Request route_id = body_json.data[i].id; const delete_route = { url: "http://"+iam_url+":"+iam_config_port+"/plugins/" + route_id, method: 'DELETE', }; pm.sendRequest(delete_route, function(err, res){ console.log(err ? err : res); }); } } } });

Hacer lo mismo para las rutas, los servicios y los consumidores.

Seguir este orden es importante porque no es posible eliminar servicios sobre los que hay rutas definidas.

11.1.3. Crear un servicio/ruta

Las rutas dependen de los servicios. En este tipo de casos podemos utilizar la función Test de Postman para recuperar los datos:

alt

Pantalla Script

foo


var id = pm.response.json().id;
var name = pm.response.json().name;
pm.globals.set("service_crud_id", id);
pm.globals.set("service_crud_name", name);

Aquí guardamos el ID y el nombre de los nuevos servicios de la respuesta.

Entonces podremos utilizarlos en la creación de la siguiente ruta:

Pantalla Script

foo


service_crud_name = pm.globals.get("service_crud_name");

Aquí recuperamos la variable global "service_crud_name".

Después, podemos utilizarla en la llamada real.

Pantalla Script

foo


{
    "paths": [
        "/persons/*"
    ],
    "protocols": [
        "http"
    ],
    "name": "crud-persons",
    "strip_path": false,
    "service": {
        "name": "{{service_crud_name}}"
        }
}

11.1.3.1. Consejos

  • paylaod puede ser json o form-data
    • form-data:

alt

  • json:

alt

Una forma sencilla de obtener el formato json es ir al portal del administrador/View/Copy json:

alt

11.2. Ejecutarlo con Newman


docker run  --rm -v "`pwd`/ci/":"/etc/newman" \
 --network="iam-training_default" \
 -t postman/newman run "DevOps_IAM.postman_collection.json" \
 --environment="DevOps_IAM.postman_environment.json"

Comentarios (0)1
Inicie sesión o regístrese para continuar