From 95e6e90ee236a472357333cb00d75e78bc3cafe4 Mon Sep 17 00:00:00 2001 From: 65397 Date: Fri, 26 Jan 2024 16:32:52 +0000 Subject: [PATCH] NGINX Declarative API v4.1.0 (#36) * 20230125-01 API v4.1 development * 20230125-02 API v4.1 development * NGINX Declarative API v4.1 * USAGE updated --- .gitignore | 2 +- FEATURES.md | 56 +- README.md | 11 +- USAGE-v4.1.md | 257 +++ contrib/devportal/{ => redocly}/Dockerfile | 2 +- contrib/devportal/{ => redocly}/README.md | 0 .../{ => redocly}/src/requirements.txt | 0 contrib/devportal/{ => redocly}/src/server.py | 2 +- contrib/devportal/{ => redocly}/src/start.sh | 0 contrib/docker-compose/docker-compose.yaml | 2 +- .../v4.0/www.online-boutique.local.crt | 20 + .../v4.0/www.online-boutique.local.key | 28 + .../v4.0/www2.online-boutique.local.crt | 20 + .../v4.0/www2.online-boutique.local.key | 28 + .../v4.1/nap-policy-gitops.json | 23 + .../v4.1/nap-policy-xss-allowed.json | 39 + .../v4.1/nap-policy-xss-blocked.json | 23 + .../v4.1/www.online-boutique.local.chain | 20 + .../v4.1/www.online-boutique.local.crt | 20 + .../v4.1/www.online-boutique.local.key | 28 + .../v4.1/www2.online-boutique.local.chain | 20 + .../v4.1/www2.online-boutique.local.crt | 20 + .../v4.1/www2.online-boutique.local.key | 28 + ...NX Declarative API.postman_collection.json | 1989 ++++++++++++++++- contrib/postman/README.md | 16 +- etc/config.toml | 2 +- src/V4_1_CreateConfig.py | 792 +++++++ src/V4_1_NginxConfigDeclaration.py | 530 +++++ src/main.py | 52 + src/v4_1/APIGateway.py | 37 + src/v4_1/DeclarationPatcher.py | 238 ++ src/v4_1/DevPortal.py | 46 + src/v4_1/GitOps.py | 64 + src/v4_1/MiscUtils.py | 44 + src/v4_1/NAPUtils.py | 269 +++ src/v4_1/NIMUtils.py | 30 + src/v4_1/OpenAPIParser.py | 71 + templates/v4.1/apigateway.tmpl | 110 + templates/v4.1/auth/client/jwks.tmpl | 11 + templates/v4.1/auth/client/jwt.tmpl | 4 + templates/v4.1/auth/server/token.tmpl | 5 + templates/v4.1/configmap.tmpl | 13 + templates/v4.1/http.tmpl | 274 +++ templates/v4.1/logformat.tmpl | 12 + templates/v4.1/nginx-conf/mime.types | 97 + templates/v4.1/nginx-conf/nginx.conf | 40 + templates/v4.1/stream.tmpl | 66 + 47 files changed, 5419 insertions(+), 42 deletions(-) create mode 100644 USAGE-v4.1.md rename contrib/devportal/{ => redocly}/Dockerfile (96%) rename contrib/devportal/{ => redocly}/README.md (100%) rename contrib/devportal/{ => redocly}/src/requirements.txt (100%) rename contrib/devportal/{ => redocly}/src/server.py (98%) rename contrib/devportal/{ => redocly}/src/start.sh (100%) create mode 100644 contrib/gitops-examples/v4.0/www.online-boutique.local.crt create mode 100644 contrib/gitops-examples/v4.0/www.online-boutique.local.key create mode 100644 contrib/gitops-examples/v4.0/www2.online-boutique.local.crt create mode 100644 contrib/gitops-examples/v4.0/www2.online-boutique.local.key create mode 100644 contrib/gitops-examples/v4.1/nap-policy-gitops.json create mode 100644 contrib/gitops-examples/v4.1/nap-policy-xss-allowed.json create mode 100644 contrib/gitops-examples/v4.1/nap-policy-xss-blocked.json create mode 100644 contrib/gitops-examples/v4.1/www.online-boutique.local.chain create mode 100644 contrib/gitops-examples/v4.1/www.online-boutique.local.crt create mode 100644 contrib/gitops-examples/v4.1/www.online-boutique.local.key create mode 100644 contrib/gitops-examples/v4.1/www2.online-boutique.local.chain create mode 100644 contrib/gitops-examples/v4.1/www2.online-boutique.local.crt create mode 100644 contrib/gitops-examples/v4.1/www2.online-boutique.local.key create mode 100644 src/V4_1_CreateConfig.py create mode 100644 src/V4_1_NginxConfigDeclaration.py create mode 100644 src/v4_1/APIGateway.py create mode 100644 src/v4_1/DeclarationPatcher.py create mode 100644 src/v4_1/DevPortal.py create mode 100644 src/v4_1/GitOps.py create mode 100644 src/v4_1/MiscUtils.py create mode 100644 src/v4_1/NAPUtils.py create mode 100644 src/v4_1/NIMUtils.py create mode 100644 src/v4_1/OpenAPIParser.py create mode 100644 templates/v4.1/apigateway.tmpl create mode 100644 templates/v4.1/auth/client/jwks.tmpl create mode 100644 templates/v4.1/auth/client/jwt.tmpl create mode 100644 templates/v4.1/auth/server/token.tmpl create mode 100644 templates/v4.1/configmap.tmpl create mode 100644 templates/v4.1/http.tmpl create mode 100644 templates/v4.1/logformat.tmpl create mode 100644 templates/v4.1/nginx-conf/mime.types create mode 100644 templates/v4.1/nginx-conf/nginx.conf create mode 100644 templates/v4.1/stream.tmpl diff --git a/.gitignore b/.gitignore index 38f2524..2123d4a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,5 +21,5 @@ Thumbs.db /.idea/ /src/__pycache__/ /src/v3_1/__pycache__/ -/contrib/devportal/src/__pycache__/ +/contrib/devportal/redocly/src/__pycache__/ /venv/ diff --git a/FEATURES.md b/FEATURES.md index cb20192..baf0182 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -2,34 +2,40 @@ ### NGINX `http` and `stream` servers -| Feature | API v3.1 | API v4.0 | Notes | -|----------------------------|----------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Upstreams | CRUD | CRUD |
  • Snippets supported: static and from source of truth
  • | -| HTTP servers | CRUD | CRUD |
  • Snippets supported (`http`, `servers`, `locations`): static and from source of truth
  • | -| TCP/UDP servers | CRUD | CRUD |
  • Snippets supported (`streams`, `servers`): static and from source of truth
  • | -| TLS | CRUD | CRUD |
  • Certificates and keys can be dynamically fetched from source of truth
  • | -| mTLS | CRUD | CRUD |
  • Certificates and keys can be dynamically fetched from source of truth
  • | -| JWT client authentication | | X |
  • JWT key can be hardwired or fetched from source of truth
  • | -| Rate limiting | X | X | | -| Active healthchecks | X | X | | -| Cookie-based stickiness | X | X | | -| Maps | X | X | | -| NGINX Plus REST API access | X | X | | -| NGINX App Protect WAF | X | X |
  • Per-policy CRUD at `server` and `location` level
  • Support for dataplane-based bundle compilation
  • Security policies can be fetched from source of truth
  • | - +| Feature | API v3.1 | API v4.0 | API v4.1 | Notes | +|----------------------------|----------|----------|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Upstreams | CRUD | CRUD | CRUD |
  • Snippets supported: static and from source of truth
  • | +| HTTP servers | CRUD | CRUD | CRUD |
  • Snippets supported (`http`, `servers`, `locations`): static and from source of truth
  • | +| TCP/UDP servers | CRUD | CRUD | CRUD |
  • Snippets supported (`streams`, `servers`): static and from source of truth
  • | +| TLS | CRUD | CRUD | CRUD |
  • Certificates and keys can be dynamically fetched from source of truth
  • | +| mTLS | CRUD | CRUD | CRUD |
  • Certificates and keys can be dynamically fetched from source of truth
  • | +| JWT client authentication | | X | X |
  • JWT key can be hardwired or fetched from source of truth
  • | +| Upstream authentication | | | X |
  • Bearer token
  • HTTP header
  • | +| Rate limiting | X | X | X | | +| Active healthchecks | X | X | X | | +| Cookie-based stickiness | X | X | X | | +| Maps | X | X | X | | +| NGINX Plus REST API access | X | X | X | | +| NGINX App Protect WAF | X | X | X |
  • Per-policy CRUD at `server` and `location` level
  • Support for dataplane-based bundle compilation
  • Security policies can be fetched from source of truth
  • | ### API Gateway -| Feature | API v3.1 | API v4.0 | Notes | -|----------------------------------------------|----------|----------|----------------------------------------------------------| -| Configuration generation from OpenAPI schema | X | X | | -| HTTP methods enforcement | X | X | | -| per-URI rate limiting | X | X | | -| per-URI JWT authentication | X | X | JWT key can be hardwired or fetched from source of truth | - +| Feature | API v3.1 | API v4.0 | API v4.1 | Notes | +|----------------------------------------------|----------|----------|----------|---------------------------------------------------------------------------| +| Configuration generation from OpenAPI schema | X | X | X | | +| HTTP methods enforcement | X | X | X | | +| per-URI rate limiting | X | X | X | | +| per-URI JWT authentication | X | X | X |
  • Static JWT key
  • JWT fetched from URL
  • Bearer token
  • | ### API Gateway - Developer Portal -| Feature | API v3.1 | API v4.0 | Notes | -|-------------------------------------------------|----------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Developer Portal generation from OpenAPI schema | X | X |
  • Based on Redocly
  • | +| Feature | API v3.1 | API v4.0 | API v4.1 | Notes | +|-------------------------------------------------|----------|----------|----------|---------------------------| +| Developer Portal generation from OpenAPI schema | X | X | X |
  • Based on Redocly
  • | + +### Source of truth + +| Feature | API v3.1 | API v4.0 | API v4.1 | Notes | +|--------------------------------------|----------|----------|----------|-------| +| HTTP header-based authentication | | | X | | +| Bearer token authentication | | | X | | diff --git a/README.md b/README.md index 3022045..d7bbfc1 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ end - [X] POST to Generic REST API endpoint - [X] Output to NGINX Instance Manager 2.14+ imperative REST API -## Supported NGINX Plus features +## Supported features See the [features list](/FEATURES.md) @@ -140,7 +140,8 @@ See the [features list](/FEATURES.md) Usage details and JSON schema are available here: -- [API v4.0](/USAGE-v4.0.md) - latest +- [API v4.1](/USAGE-v4.1.md) - latest +- [API v4.0](/USAGE-v4.0.md) - [API v3.1](/USAGE-v3.1.md) A sample Postman collection and usage instructions can be found [here](/contrib/postman) @@ -159,14 +160,10 @@ Docker images can be built and run using: docker build --no-cache -t nginx-declarative-api -f ./Dockerfile . docker run --name nginx-declarative-api -d -p 5000:5000 nginx-declarative-api - cd contrib/devportal + cd contrib/redocly/devportal docker build --no-cache -t nginx-declarative-api-devportal . docker run --name devportal -d -p 5001:5000 nginx-declarative-api-devportal -Pre-built docker images are available on Docker Hub at -- https://hub.docker.com/repository/docker/fiorucci/nginx-declarative-api/general -- https://hub.docker.com/repository/docker/fiorucci/nginx-declarative-api-devportal/general - Configuration can be customized mounting `config.toml` as a volume `nginx-declarative-api` docker image as a volume to customize ## REST API documentation diff --git a/USAGE-v4.1.md b/USAGE-v4.1.md new file mode 100644 index 0000000..4b5e1be --- /dev/null +++ b/USAGE-v4.1.md @@ -0,0 +1,257 @@ +# Usage for API v4.1 + +Version 4.1 API requires: + +- NGINX Instance Manager 2.14+ +- NGINX Plus R30+ + +If NGINX App Protect declarations are used: +- NGINX App Protect Policy Compiler 4.2.0+ +- NGINX Plus instances running App Protect WAF 4.2.0+ using `precompiled_publication: true` in `/etc/nginx-agent/nginx-agent.conf` + +The JSON schema is self explanatory. See also the [sample Postman collection](/contrib/postman) + +- `.output.type` defines how NGINX configuration will be returned: + - *plaintext* - plaintext format + - *json* - JSON-wrapped, base64-encoded + - *configmap* - Kubernetes Configmap in YAML format. + - `.output.configmap.name` must be set to the ConfigMap name + - `.output.configmap.filename` must be set to the NGINX configuration filename + - `.output.configmap.namespace` the optional namespace for the ConfigMap + - *http* - NGINX configuration is POSTed to custom url + - `.output.http.url` the URL to POST the configuration to + - *nms* - NGINX configuration is published as a Staged Config to NGINX Instance Manager + - `.output.nms.url` the NGINX Instance Manager URL + - `.output.nms.username` the NGINX Instance Manager authentication username + - `.output.nms.password` the NGINX Instance Manager authentication password + - `.output.nms.instancegroup` the NGINX Instance Manager instance group to publish the configuration to + - `.output.nms.synctime` **optional**, used for GitOps autosync. When specified and the declaration includes HTTP(S) references to NGINX App Protect policies, TLS certificates/keys/chains, the HTTP(S) endpoints will be checked every `synctime` seconds and if external contents have changed, the updated configuration will automatically be published to NGINX Instance Manager + - `.output.nms.modules` an optional array of NGINX module names (ie. 'ngx_http_app_protect_module', 'ngx_http_js_module','ngx_stream_js_module') + - `.output.nms.certificates` an optional array of TLS certificates/keys/chains to be published + - `.output.nms.certificates[].type` the item type ('certificate', 'key', 'chain') + - `.output.nms.certificates[].name` the certificate/key/chain name with no path/extension (ie. 'test-application') + - `.output.nms.certificates[].contents` the content: this can be either base64-encoded or be a HTTP(S) URL that will be fetched dynamically from a source of truth + - `.output.nms.policies[]` an optional array of NGINX App Protect security policies + - `.output.nms.policies[].type` the policy type ('app_protect') + - `.output.nms.policies[].name` the policy name (ie. 'prod-policy') + - `.output.nms.policies[].active_tag` the policy tag to enable among all available versions (ie. 'v1') + - `.output.nms.policies[].versions[]` array with all available policy versions + - `.output.nms.policies[].versions[].tag` the policy version's tag name + - `.output.nms.policies[].versions[].displayName` the policy version's display name + - `.output.nms.policies[].versions[].description` the policy version's description + - `.output.nms.policies[].versions[].contents` this can be either base64-encoded or be a HTTP(S) URL that will be fetched dynamically from a source of truth +- `.declaration` describes the NGINX configuration to be created. + +### Locations ### + +Locations `.declaration.http.servers[].locations[].uri` match modifiers in `.declaration.http.servers[].locations[].urimatch` can be: + +- *prefix* - prefix URI matching +- *exact* - exact URI matching +- *regex* - case sensitive regex matching +- *iregex* - case insensitive regex matching +- *best* - case sensitive regex matching that halts any other location matching once a match is made + +### API Gateway ### + +Swagger files and OpenAPI schemas can be used to automatically configure NGINX as an API Gateway. Developer portal creation is supported through [Redocly](https://redocly.com/) + +Declaration path `.declaration.http.servers[].locations[].apigateway` defines the API Gateway configuration: + +- `openapi_schema` - the base64-encoded schema, or the schema URL. YAML and JSON are supported +- `api_gateway.enabled` - enable/disable API Gateway provisioning +- `api_gateway.strip_uri` - removes the `.declaration.http.servers[].locations[].uri` part of the URI before forwarding requests to the upstream +- `api_gateway.server_url` - the base URL of the upstream server +- `developer_portal.enabled` - enable/disable Developer portal provisioning +- `developer_portal.uri` - the trailing part of the Developer portal URI, this is appended to `.declaration.http.servers[].locations[].uri`. If omitted it defaults to `devportal.html` +- `authentication` - optional, used to enforce JWT authentication at the API Gateway level +- `authentication.client` - JWT authentication profile name +- `authentication.enforceOnPaths` - if set to `true` JWT authentication is enforced on all API endpoints listed under `authentication.paths`. if set to `false` JWT authentication is enforced on all API endpoints but those listed under `authentication.paths` +- `rate_limit` - optional, used to enforce rate limiting at the API Gateway level +- `rate_limit.enforceOnPaths` - if set to `true` rate limiting is enforced on all API endpoints listed under `rate_limit.paths`. if set to `false` rate limiting is enforced on all API endpoints but those listed under `rate_limit.paths` + +A sample API Gateway declaration to publish the `https://petstore.swagger.io` REST API and enforce: + +- REST API endpoint URIs +- HTTP Methods +- Rate limiting on `/user/login` and `/user/logout` +- JWT authentication on `/user/login` and `/usr/logout` + +is: + +```commandline +{ + "output": { + "type": "nms", + "nms": { + "url": "{{nim_host}}", + "username": "{{nim_username}}", + "password": "{{nim_password}}", + "instancegroup": "{{nim_instancegroup}}", + "synctime": 0, + "modules": [ + "ngx_http_js_module", + "ngx_stream_js_module" + ] + } + }, + "declaration": { + "http": { + "servers": [ + { + "name": "Petstore API", + "names": [ + "apigw.nginx.lab" + ], + "resolver": "8.8.8.8", + "listen": { + "address": "80" + }, + "log": { + "access": "/var/log/nginx/apigw.nginx.lab-access_log", + "error": "/var/log/nginx/apigw.nginx.lab-error_log" + }, + "locations": [ + { + "uri": "/petstore", + "urimatch": "prefix", + "apigateway": { + "openapi_schema": { + "content": "http://petstore.swagger.io/v2/swagger.json", + "authentication": [ + { + "profile": "Source of truth authentication profile using HTTP header token authentication" + } + ] + }, + "api_gateway": { + "enabled": true, + "strip_uri": true, + "server_url": "https://petstore.swagger.io/v2" + }, + "developer_portal": { + "enabled": false, + "uri": "/petstore-devportal.html" + }, + "authentication": { + "client": [ + { + "profile": "Petstore JWT Authentication" + } + ], + "enforceOnPaths": true, + "paths": [ + "/user/login", + "/user/logout" + ] + }, + "rate_limit": [ + { + "profile": "petstore_ratelimit", + "httpcode": 429, + "burst": 0, + "delay": 0, + "enforceOnPaths": true, + "paths": [ + "/user/login", + "/user/logout" + ] + } + ] + }, + "log": { + "access": "/var/log/nginx/petstore-access_log", + "error": "/var/log/nginx/petstore-error_log" + } + } + ] + } + ], + "rate_limit": [ + { + "name": "petstore_ratelimit", + "key": "$binary_remote_addr", + "size": "10m", + "rate": "2r/s" + } + ], + "authentication": { + "client": [ + { + "name": "Petstore JWT Authentication", + "type": "jwt", + "jwt": { + "realm": "Petstore Authentication", + "key": "{\"keys\": [{\"k\":\"ZmFudGFzdGljand0\",\"kty\":\"oct\",\"kid\":\"0001\"}]}", + "cachetime": 5 + } + } + ], + "server": [ + { + "name": "Source of truth authentication profile using HTTP header token authentication", + "type": "token", + "token": { + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEiLCJpc3MiOiJCYXNoIEpXVCBHZW5lcmF0b3IiLCJpYXQiOjE3MDI0ODEzNjcsImV4cCI6MTcwMjQ4MTM2OH0.eyJuYW1lIjoiQm9iIERldk9wcyIsInN1YiI6IkpXVCBzdWIgY2xhaW0iLCJpc3MiOiJKV1QgaXNzIGNsYWltIiwicm9sZXMiOlsiZGV2b3BzIl19.SKA_7MszAypMEtX5NDQ0TcUbVYx_Wt0hrtmuyTmrVKU", + "type": "header", + "location": "X-AUTH-TOKEN" + } + } + ] + } + } + } +} +``` + +It can be tested using: + +``` +curl -iH "Host: apigw.nginx.lab" http:///petstore/store/inventory +``` + +Authentication failed: + +``` +curl -i http://apigw.nginx.lab/petstore/user/login +``` + +Authentication Succeeded: + +``` +curl -i http://apigw.nginx.lab/petstore/user/login -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEiLCJpc3MiOiJCYXNoIEpXVCBHZW5lcmF0b3IiLCJpYXQiOjE3MDI0ODEzNjcsImV4cCI6MTcwMjQ4MTM2OH0.eyJuYW1lIjoiQm9iIERldk9wcyIsInN1YiI6IkpXVCBzdWIgY2xhaW0iLCJpc3MiOiJKV1QgaXNzIGNsYWltIiwicm9sZXMiOlsiZGV2b3BzIl19.SKA_7MszAypMEtX5NDQ0TcUbVYx_Wt0hrtmuyTmrVKU" +``` + +The API Developer portal can be accessed at: + + http:///petstore/petstore-devportal.html + +### Maps ### + +Map entries `.declaration.maps[].entries.keymatch` can be: + +- *exact* - exact variable matching +- *regex* - case sensitive regex matching +- *iregex* - case insensitive regex matching + +### Snippets ### + +Snippets for http, upstream, server and location can be specified as: +- base64-encoded content +- HTTP(S) URL of a source of truth to fetch snippet content from. Content on the source of truth must be plaintext, it will be automatically base64-encoded + +### Methods ### + +- `POST /v4.1/config/` - Publish a new declaration +- `PATCH /v4.1/config/{config_uid}` - Update an existing declaration + - Per-HTTP server CRUD + - Per-HTTP upstream CRUD + - Per-Stream server CRUD + - Per-Stream upstream CRUD + - Per-NGINX App Protect WAF policy CRUD +- `GET /v4.1/config/{config_uid}` - Retrieve an existing declaration +- `DELETE /v4.1/config/{config_uid}` - Delete an existing declaration + +### Usage Examples ### + +A sample Postman collection is available [here](/contrib/postman) \ No newline at end of file diff --git a/contrib/devportal/Dockerfile b/contrib/devportal/redocly/Dockerfile similarity index 96% rename from contrib/devportal/Dockerfile rename to contrib/devportal/redocly/Dockerfile index 8ea587b..5894a3f 100644 --- a/contrib/devportal/Dockerfile +++ b/contrib/devportal/redocly/Dockerfile @@ -2,7 +2,7 @@ FROM redocly/cli WORKDIR /deployment -COPY src/ src/ +COPY src src/ RUN apk update && \ apk add --update --no-cache bash python3 && \ diff --git a/contrib/devportal/README.md b/contrib/devportal/redocly/README.md similarity index 100% rename from contrib/devportal/README.md rename to contrib/devportal/redocly/README.md diff --git a/contrib/devportal/src/requirements.txt b/contrib/devportal/redocly/src/requirements.txt similarity index 100% rename from contrib/devportal/src/requirements.txt rename to contrib/devportal/redocly/src/requirements.txt diff --git a/contrib/devportal/src/server.py b/contrib/devportal/redocly/src/server.py similarity index 98% rename from contrib/devportal/src/server.py rename to contrib/devportal/redocly/src/server.py index 42a1eab..916ba56 100644 --- a/contrib/devportal/src/server.py +++ b/contrib/devportal/redocly/src/server.py @@ -10,7 +10,7 @@ from fastapi.responses import PlainTextResponse, Response, JSONResponse app = FastAPI( - title="Redoc connector", + title="Redocly connector", version="1.0.0", contact={"name": "GitHub", "url": "https://github.com/f5devcentral/NGINX-Declarative-API"} ) diff --git a/contrib/devportal/src/start.sh b/contrib/devportal/redocly/src/start.sh similarity index 100% rename from contrib/devportal/src/start.sh rename to contrib/devportal/redocly/src/start.sh diff --git a/contrib/docker-compose/docker-compose.yaml b/contrib/docker-compose/docker-compose.yaml index 4996313..273f8f0 100644 --- a/contrib/docker-compose/docker-compose.yaml +++ b/contrib/docker-compose/docker-compose.yaml @@ -20,7 +20,7 @@ services: #image: fiorucci/nginx-declarative-api-devportal:latest image: nginx-declarative-api-devportal build: - context: ../devportal/ + context: ../devportal/redocly dockerfile: Dockerfile user: "${USERID}:${USERGROUP}" container_name: "devportal" diff --git a/contrib/gitops-examples/v4.0/www.online-boutique.local.crt b/contrib/gitops-examples/v4.0/www.online-boutique.local.crt new file mode 100644 index 0000000..062f3d8 --- /dev/null +++ b/contrib/gitops-examples/v4.0/www.online-boutique.local.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDRjCCAi6gAwIBAgIUccT8pYiNoy0AQnkrUZrVCXhnm70wDQYJKoZIhvcNAQEL +BQAwSDEiMCAGCSqGSIb3DQEJARYTbXlAZW1haWxhZGRyZXNzLmNvbTEiMCAGA1UE +AwwZd3d3Lm9ubGluZS1ib3V0aXF1ZS5sb2NhbDAeFw0yMzAyMTQxNTA2NDRaFw0z +MzAyMTExNTA2NDRaMEgxIjAgBgkqhkiG9w0BCQEWE215QGVtYWlsYWRkcmVzcy5j +b20xIjAgBgNVBAMMGXd3dy5vbmxpbmUtYm91dGlxdWUubG9jYWwwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDasqlTnb5WlPHkjiLnjFX27QBm2BgDcA+x +Y37biaMp+I1bTUd57hDM1X+kBeLZc98xe7hc/HaO5+3ywanBRTMVaBUCKBduET/o +N+DdBAj8c64vov9a064sjXwYSeeh8lPqX52R5DYWxpHh2tvSg+a/3wwmIPoScXIu +R1Wq7RY//dsshIHQkxVa9VAPYjLnusQjH9zOzJGZOxQ/OlCdaUrShbraKg6oVHK1 ++2h3M2S7RFjXFr8mbpxSgez1w9oN1YI+RM/kLPgI+Kq+YN6MfMhYvQei537569N9 +wJ9bl/i34EbM5gDf8BxhJJbo+ypEC9h0H8FmSXbqXcb8gt/dQMfZAgMBAAGjKDAm +MCQGA1UdEQQdMBuCGXd3dy5vbmxpbmUtYm91dGlxdWUubG9jYWwwDQYJKoZIhvcN +AQELBQADggEBAKLCxBakmK43tbf+2pwgbRLYuOvz4A8GCh/EK2F1OKeuw0oIat7M +Nn5SxSgNFj45XswaRsR3/ObLovrYmXzZnV9IPNHIsB0/sBsyqEYMA55hJsb/8HEd ++bWv2UJdcAimhbLMi1bpgSlSCNMn/CWkX09e3IjRrYkZxT50/ahOBBfBshoY+jwe +sA1VDwZ4neXfrTUXJ4ixiTOfna2TpADLNxk6pncr6uiIpJSmLAq9jQwPBNlrpRN3 +TCyKKcadGdUPkALXm0+dMn1OHphy7ZQADMxPwxLySuoCK8kcslpGLg18zPXYQkoD +C1KAguWu/VU4EBT39aC5/9askw+zV4XE9hw= +-----END CERTIFICATE----- diff --git a/contrib/gitops-examples/v4.0/www.online-boutique.local.key b/contrib/gitops-examples/v4.0/www.online-boutique.local.key new file mode 100644 index 0000000..5a759e7 --- /dev/null +++ b/contrib/gitops-examples/v4.0/www.online-boutique.local.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDasqlTnb5WlPHk +jiLnjFX27QBm2BgDcA+xY37biaMp+I1bTUd57hDM1X+kBeLZc98xe7hc/HaO5+3y +wanBRTMVaBUCKBduET/oN+DdBAj8c64vov9a064sjXwYSeeh8lPqX52R5DYWxpHh +2tvSg+a/3wwmIPoScXIuR1Wq7RY//dsshIHQkxVa9VAPYjLnusQjH9zOzJGZOxQ/ +OlCdaUrShbraKg6oVHK1+2h3M2S7RFjXFr8mbpxSgez1w9oN1YI+RM/kLPgI+Kq+ +YN6MfMhYvQei537569N9wJ9bl/i34EbM5gDf8BxhJJbo+ypEC9h0H8FmSXbqXcb8 +gt/dQMfZAgMBAAECggEBAIcP85baQqSCE+mNBm1ts+FteOQK7LmiyW5J/hxjIktX +XyVV0qCwr9twtwV7RR/6eYq7155FpIqZHdDgXZAoxmWgA+lzDRVCve8SI2MbjNks +MBTnW0CinlyRfGQbsFvJpp2GM2/YOrdSJuIPIdce8rDodT5O+2HMgjGmiHepOCaH +4XS42Av9KDbcbo2rSfAhSulsXJetd9G7luD3dY3Lsp2TC6ihzwZOtGSHJjoY29uS +D+MJvAq2MzZxRnyE6vP88NY45rZug1YS7EHs0v6NAnI6/Ew6M3sAjss+qhBL/k6j +AMWAibn1oTgbN6ifXapXpynQOSU1IvEJqXNHA+YQMEkCgYEA/t7TihpRoJRcMzx6 +4XtKMMDS/ixv7QUmOnJ1AO1Je3hjJK+2U1wARYLHoVdlZYTb9GRRlH6UHPL3lh2m +ZWfDHRjKzQ+E4jlYFvQMAmAhbSqCAPtyFadQf7qLDKZAT/1WUj+Ynh0vjUUigqKj +VsukDJkwqSt3jCk2ECtddcTicCcCgYEA26rLSaDx/+bDYVlJW9nXt8/N2ZYqtBev +I+AV1qgVYjlskbcMch3apIVELiOEVfBaIGJDoq1N3e25cTvNNO/mA2rDSYLQuz7L +bwlVs6ix02/tFd1R0N5nGKMhziue1l3BOjADHzf/UkOc4UIJGkmUQHrVxpby6FMY +cT+HJ3+OB/8CgYAHtDqWuRbwkKt8zP415KEoehumnTdA5d/y8lTBE2seNVRh3oHX +YTeM4lggc2DYQbzYVVP19iGKAnojaoAGHq3SKlau/iIZKHyWLQhT5g38m8VUPEWF +jAot4jijyD63bEP4tn1pgh5W2dkiM8JWNE+gJd1Hr82sSe6dbIIlti7WDwKBgA3n +PX6nhSmhPXSH5jC+FP05VdoTuxgy947ZvAgeE1xoLgr6/vqqERCgrrQM429dCxdJ +oOZG+cq2JSqZkl9rX6+PrlSUxwlS7CVW/emlH7w2NVdQ7sC9kuDoUlduQ1tmC7jX +GRt8u9hFF0TanSDgz1VVcPpky3MQ71cboj5JwH+ZAoGAAcDSlHuOu2xnbdnaTdE1 +MK0q/Yt3EOCGp0FRBkResPhhoV/TXadk0sJJbWFnleRFDkmgsw2QAwVhKQHCRWOu +b4G8EnEXY/91IoRSevWtZTNf+/9+y5w4iOWI3rGJNpXCEgzN7Yc60z1sIsC/fu+Q +YxXNiLIYUB4Pk4pDNRNLMZo= +-----END PRIVATE KEY----- diff --git a/contrib/gitops-examples/v4.0/www2.online-boutique.local.crt b/contrib/gitops-examples/v4.0/www2.online-boutique.local.crt new file mode 100644 index 0000000..76e8157 --- /dev/null +++ b/contrib/gitops-examples/v4.0/www2.online-boutique.local.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDSTCCAjGgAwIBAgIUbzaZNPdYel80IQ81PCNfMllLnYgwDQYJKoZIhvcNAQEL +BQAwSTEiMCAGCSqGSIb3DQEJARYTbXlAZW1haWxhZGRyZXNzLmNvbTEjMCEGA1UE +Awwad3d3Mi5vbmxpbmUtYm91dGlxdWUubG9jYWwwHhcNMjMwMjE0MTUwNzMxWhcN +MzMwMjExMTUwNzMxWjBJMSIwIAYJKoZIhvcNAQkBFhNteUBlbWFpbGFkZHJlc3Mu +Y29tMSMwIQYDVQQDDBp3d3cyLm9ubGluZS1ib3V0aXF1ZS5sb2NhbDCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAN2OHfrfxO6hhg4dGMi0Q4+iwuuNGv2J +eh+t8g9A+dGB4zUkU0SEa2EFLjyAw2P7F1XAtiClHmS9nA0uTuj9nuw9Xk+wFUlm +ahMkdkLBRNNsH3O1gTYUQSOK7xm8FOMBnOYQqJOHILXsXgM2z8QHpX//wl15aGXf +9vPm4CQUqqsEGNWDfpOQ8jfhrA4T8W1WuAXzJAQmZfhW1qMWxoj1iTCk+Q6nH/8a +MWLWPjTes0bTSRXRTpuQCyeHCaqS8oPPshcd52/FXChnYbNAG1s9Q+35MrgDztL6 +dPkNiA9UaUY7pumNip93ZnsYwDmOPnDBPqfvy9Dk1zQuaAsJ1FXwRNsCAwEAAaMp +MCcwJQYDVR0RBB4wHIIad3d3Mi5vbmxpbmUtYm91dGlxdWUubG9jYWwwDQYJKoZI +hvcNAQELBQADggEBADT2SchC4VhWqCYRsw/3nqrVh0JQmD+/x9JjiNsY6fLnG6uW +8bs8/714qoghJff67H60B6NbrS3lpTZ3bmyotcGtwNNsY4QSFHRu/x4OyIrTfjKb +VIdhRM2Atwc1s1YA6c+2JBquBDhniqABKG9u+j1aa2ElXSalCj+Kozm8ma0yduVw +TX8zS6XZl57vSk/Qo/PZvbmbs8EMOwTUCLn6WQldAARCLughjd9LI9prNpBlYon6 +jmZ8oi59arK16cKe6i6tQ3QExT2kQsLlrK/jFw0xqrFnveKgUTeevViT4WZnqsCm +awrPhrbDn0F7HmFSrgenrW8BIuJ2crBHy0230Lc= +-----END CERTIFICATE----- diff --git a/contrib/gitops-examples/v4.0/www2.online-boutique.local.key b/contrib/gitops-examples/v4.0/www2.online-boutique.local.key new file mode 100644 index 0000000..b1e2db4 --- /dev/null +++ b/contrib/gitops-examples/v4.0/www2.online-boutique.local.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDdjh3638TuoYYO +HRjItEOPosLrjRr9iXofrfIPQPnRgeM1JFNEhGthBS48gMNj+xdVwLYgpR5kvZwN +Lk7o/Z7sPV5PsBVJZmoTJHZCwUTTbB9ztYE2FEEjiu8ZvBTjAZzmEKiThyC17F4D +Ns/EB6V//8JdeWhl3/bz5uAkFKqrBBjVg36TkPI34awOE/FtVrgF8yQEJmX4Vtaj +FsaI9YkwpPkOpx//GjFi1j403rNG00kV0U6bkAsnhwmqkvKDz7IXHedvxVwoZ2Gz +QBtbPUPt+TK4A87S+nT5DYgPVGlGO6bpjYqfd2Z7GMA5jj5wwT6n78vQ5Nc0LmgL +CdRV8ETbAgMBAAECggEBAJkAy010eWC+5YAbsgDxFHM+WNQo90m+RjtYegD0w1Ff +HNSXSHXZ6PnwhPS9i7IhNgU/d8dloG67zuf/YflfJQBKhTYNCTZOZtTfalhBdlF3 +quTSpO/+3tk32lVwYRBADeWH/ZMcT4ezO12G3can7TBPJSA+ds1b9QSNHZ9tMku0 +2J2pzT8C8V0vHn49oKO9D4Q+QwRvisT6FKNaOW/xKZjtUIvyjVvjENnBW7MQ5y5z +IHsUNkwGgZ2V2IbDouM4+A42J9iDsseM2euSsaDfum3vj9MPTqXE8zUUdX+/miMq +RJTXLxDdkPaCbQftQ7y/cIAFLDZS8Fvvsa5pX69jl1ECgYEA8sQ6S7kcyYgBLX4T +KVYPg1sFTgCofW5nTgCND4kiVvIDz/uk9vsfuMj7FMS3fcCfZorPdYEKa+Zwi4wZ +NMCSArtezbwFBeaOiZTpjh2k8CBrtumdcU/EfzQOvrVcSzVc27ditwRAmONeBXVw +GSvqSfjQKLDh/4Y191qOmgyIBDMCgYEA6aHjQr43XM811VQhTPBtLlBccD8hCS23 +NcujPMa85Vz+RES3OqHt2nH9NGuYrexGbrSt3ckjxnO+TdsS4f94bKhquXvv7AkX +7qr8KZEwHncyjcL3uTChbjWr2xJR4lSnS5PAbnWfyR48HXkHO9IVPe/IcYnvuGDN +AeMJkCgp1LkCgYEArvD/PXAxMX1js9/FeSU+Wp8t7G8G/BSiNxColkhxSYxveOJT +l3OSAXw7i1TTEbjMZX2kUH3j/6t48ObNhzk6PuO9Rq62Q/FISBbaU4JDSJNka9Rf +k7cy16Ow+HcDAmN6/g5iAZb74fD+4Rom5MzDsfiuMJR+179khlJortRW9AcCgYAD +4AKD9eG3MVykOCwBOa+l6AFQf0uN+msigkkn1egGKd+xxC4B0/O8/s0DVJGIuPWG +GosTtaVZQkwywGJ0yyb1Lmnuv6aAFLqH4+Ag1F6m8rUs8sHnGW5kBJHgJVKkXWEU ++NNlQaAv1seKeZpsHJTrnRGHCJGoTjq4QErFUFU5SQKBgEYMGdnPP4ZDAeezPCML +42RVHOAG9IsMl4mV7+fwcEHcmuvb8nNgKNfayjG/NbiQWtT4v7iQH4GHYzUobaqr +3xnQlec3jBrLEsWR8ItRWKxKO2GGLlbmQ99Kn3UGh+6FtXbxwB0hhkFfKXJzM10N +Y9ruUdyYpjGO/j9ROjoBepUH +-----END PRIVATE KEY----- diff --git a/contrib/gitops-examples/v4.1/nap-policy-gitops.json b/contrib/gitops-examples/v4.1/nap-policy-gitops.json new file mode 100644 index 0000000..1fbaaf8 --- /dev/null +++ b/contrib/gitops-examples/v4.1/nap-policy-gitops.json @@ -0,0 +1,23 @@ +{ + "policy": { + "name": "prod-policy", + "template": { + "name": "POLICY_TEMPLATE_NGINX_BASE" + }, + "applicationLanguage": "utf-8", + "enforcementMode": "blocking", + "signature-sets": [ + { + "name": "All Signatures", + "block": true, + "alarm": true + } + ], + "signatures": [ + { + "signatureId": 200001834, + "enabled": false + } + ] + } +} diff --git a/contrib/gitops-examples/v4.1/nap-policy-xss-allowed.json b/contrib/gitops-examples/v4.1/nap-policy-xss-allowed.json new file mode 100644 index 0000000..2fe91c8 --- /dev/null +++ b/contrib/gitops-examples/v4.1/nap-policy-xss-allowed.json @@ -0,0 +1,39 @@ +{ + "policy": { + "name": "prod-policy", + "template": { + "name": "POLICY_TEMPLATE_NGINX_BASE" + }, + "applicationLanguage": "utf-8", + "enforcementMode": "blocking", + "signature-sets": [ + { + "name": "All Signatures", + "block": true, + "alarm": true + } + ], + "signatures": [ + { + "signatureId": 200001834, + "enabled": false + }, + { + "signatureId": 200001475, + "enabled": false + }, + { + "signatureId": 200000098, + "enabled": false + }, + { + "signatureId": 200001088, + "enabled": false + }, + { + "signatureId": 200101609, + "enabled": false + } + ] + } +} diff --git a/contrib/gitops-examples/v4.1/nap-policy-xss-blocked.json b/contrib/gitops-examples/v4.1/nap-policy-xss-blocked.json new file mode 100644 index 0000000..1fbaaf8 --- /dev/null +++ b/contrib/gitops-examples/v4.1/nap-policy-xss-blocked.json @@ -0,0 +1,23 @@ +{ + "policy": { + "name": "prod-policy", + "template": { + "name": "POLICY_TEMPLATE_NGINX_BASE" + }, + "applicationLanguage": "utf-8", + "enforcementMode": "blocking", + "signature-sets": [ + { + "name": "All Signatures", + "block": true, + "alarm": true + } + ], + "signatures": [ + { + "signatureId": 200001834, + "enabled": false + } + ] + } +} diff --git a/contrib/gitops-examples/v4.1/www.online-boutique.local.chain b/contrib/gitops-examples/v4.1/www.online-boutique.local.chain new file mode 100644 index 0000000..062f3d8 --- /dev/null +++ b/contrib/gitops-examples/v4.1/www.online-boutique.local.chain @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDRjCCAi6gAwIBAgIUccT8pYiNoy0AQnkrUZrVCXhnm70wDQYJKoZIhvcNAQEL +BQAwSDEiMCAGCSqGSIb3DQEJARYTbXlAZW1haWxhZGRyZXNzLmNvbTEiMCAGA1UE +AwwZd3d3Lm9ubGluZS1ib3V0aXF1ZS5sb2NhbDAeFw0yMzAyMTQxNTA2NDRaFw0z +MzAyMTExNTA2NDRaMEgxIjAgBgkqhkiG9w0BCQEWE215QGVtYWlsYWRkcmVzcy5j +b20xIjAgBgNVBAMMGXd3dy5vbmxpbmUtYm91dGlxdWUubG9jYWwwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDasqlTnb5WlPHkjiLnjFX27QBm2BgDcA+x +Y37biaMp+I1bTUd57hDM1X+kBeLZc98xe7hc/HaO5+3ywanBRTMVaBUCKBduET/o +N+DdBAj8c64vov9a064sjXwYSeeh8lPqX52R5DYWxpHh2tvSg+a/3wwmIPoScXIu +R1Wq7RY//dsshIHQkxVa9VAPYjLnusQjH9zOzJGZOxQ/OlCdaUrShbraKg6oVHK1 ++2h3M2S7RFjXFr8mbpxSgez1w9oN1YI+RM/kLPgI+Kq+YN6MfMhYvQei537569N9 +wJ9bl/i34EbM5gDf8BxhJJbo+ypEC9h0H8FmSXbqXcb8gt/dQMfZAgMBAAGjKDAm +MCQGA1UdEQQdMBuCGXd3dy5vbmxpbmUtYm91dGlxdWUubG9jYWwwDQYJKoZIhvcN +AQELBQADggEBAKLCxBakmK43tbf+2pwgbRLYuOvz4A8GCh/EK2F1OKeuw0oIat7M +Nn5SxSgNFj45XswaRsR3/ObLovrYmXzZnV9IPNHIsB0/sBsyqEYMA55hJsb/8HEd ++bWv2UJdcAimhbLMi1bpgSlSCNMn/CWkX09e3IjRrYkZxT50/ahOBBfBshoY+jwe +sA1VDwZ4neXfrTUXJ4ixiTOfna2TpADLNxk6pncr6uiIpJSmLAq9jQwPBNlrpRN3 +TCyKKcadGdUPkALXm0+dMn1OHphy7ZQADMxPwxLySuoCK8kcslpGLg18zPXYQkoD +C1KAguWu/VU4EBT39aC5/9askw+zV4XE9hw= +-----END CERTIFICATE----- diff --git a/contrib/gitops-examples/v4.1/www.online-boutique.local.crt b/contrib/gitops-examples/v4.1/www.online-boutique.local.crt new file mode 100644 index 0000000..062f3d8 --- /dev/null +++ b/contrib/gitops-examples/v4.1/www.online-boutique.local.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDRjCCAi6gAwIBAgIUccT8pYiNoy0AQnkrUZrVCXhnm70wDQYJKoZIhvcNAQEL +BQAwSDEiMCAGCSqGSIb3DQEJARYTbXlAZW1haWxhZGRyZXNzLmNvbTEiMCAGA1UE +AwwZd3d3Lm9ubGluZS1ib3V0aXF1ZS5sb2NhbDAeFw0yMzAyMTQxNTA2NDRaFw0z +MzAyMTExNTA2NDRaMEgxIjAgBgkqhkiG9w0BCQEWE215QGVtYWlsYWRkcmVzcy5j +b20xIjAgBgNVBAMMGXd3dy5vbmxpbmUtYm91dGlxdWUubG9jYWwwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDasqlTnb5WlPHkjiLnjFX27QBm2BgDcA+x +Y37biaMp+I1bTUd57hDM1X+kBeLZc98xe7hc/HaO5+3ywanBRTMVaBUCKBduET/o +N+DdBAj8c64vov9a064sjXwYSeeh8lPqX52R5DYWxpHh2tvSg+a/3wwmIPoScXIu +R1Wq7RY//dsshIHQkxVa9VAPYjLnusQjH9zOzJGZOxQ/OlCdaUrShbraKg6oVHK1 ++2h3M2S7RFjXFr8mbpxSgez1w9oN1YI+RM/kLPgI+Kq+YN6MfMhYvQei537569N9 +wJ9bl/i34EbM5gDf8BxhJJbo+ypEC9h0H8FmSXbqXcb8gt/dQMfZAgMBAAGjKDAm +MCQGA1UdEQQdMBuCGXd3dy5vbmxpbmUtYm91dGlxdWUubG9jYWwwDQYJKoZIhvcN +AQELBQADggEBAKLCxBakmK43tbf+2pwgbRLYuOvz4A8GCh/EK2F1OKeuw0oIat7M +Nn5SxSgNFj45XswaRsR3/ObLovrYmXzZnV9IPNHIsB0/sBsyqEYMA55hJsb/8HEd ++bWv2UJdcAimhbLMi1bpgSlSCNMn/CWkX09e3IjRrYkZxT50/ahOBBfBshoY+jwe +sA1VDwZ4neXfrTUXJ4ixiTOfna2TpADLNxk6pncr6uiIpJSmLAq9jQwPBNlrpRN3 +TCyKKcadGdUPkALXm0+dMn1OHphy7ZQADMxPwxLySuoCK8kcslpGLg18zPXYQkoD +C1KAguWu/VU4EBT39aC5/9askw+zV4XE9hw= +-----END CERTIFICATE----- diff --git a/contrib/gitops-examples/v4.1/www.online-boutique.local.key b/contrib/gitops-examples/v4.1/www.online-boutique.local.key new file mode 100644 index 0000000..5a759e7 --- /dev/null +++ b/contrib/gitops-examples/v4.1/www.online-boutique.local.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDasqlTnb5WlPHk +jiLnjFX27QBm2BgDcA+xY37biaMp+I1bTUd57hDM1X+kBeLZc98xe7hc/HaO5+3y +wanBRTMVaBUCKBduET/oN+DdBAj8c64vov9a064sjXwYSeeh8lPqX52R5DYWxpHh +2tvSg+a/3wwmIPoScXIuR1Wq7RY//dsshIHQkxVa9VAPYjLnusQjH9zOzJGZOxQ/ +OlCdaUrShbraKg6oVHK1+2h3M2S7RFjXFr8mbpxSgez1w9oN1YI+RM/kLPgI+Kq+ +YN6MfMhYvQei537569N9wJ9bl/i34EbM5gDf8BxhJJbo+ypEC9h0H8FmSXbqXcb8 +gt/dQMfZAgMBAAECggEBAIcP85baQqSCE+mNBm1ts+FteOQK7LmiyW5J/hxjIktX +XyVV0qCwr9twtwV7RR/6eYq7155FpIqZHdDgXZAoxmWgA+lzDRVCve8SI2MbjNks +MBTnW0CinlyRfGQbsFvJpp2GM2/YOrdSJuIPIdce8rDodT5O+2HMgjGmiHepOCaH +4XS42Av9KDbcbo2rSfAhSulsXJetd9G7luD3dY3Lsp2TC6ihzwZOtGSHJjoY29uS +D+MJvAq2MzZxRnyE6vP88NY45rZug1YS7EHs0v6NAnI6/Ew6M3sAjss+qhBL/k6j +AMWAibn1oTgbN6ifXapXpynQOSU1IvEJqXNHA+YQMEkCgYEA/t7TihpRoJRcMzx6 +4XtKMMDS/ixv7QUmOnJ1AO1Je3hjJK+2U1wARYLHoVdlZYTb9GRRlH6UHPL3lh2m +ZWfDHRjKzQ+E4jlYFvQMAmAhbSqCAPtyFadQf7qLDKZAT/1WUj+Ynh0vjUUigqKj +VsukDJkwqSt3jCk2ECtddcTicCcCgYEA26rLSaDx/+bDYVlJW9nXt8/N2ZYqtBev +I+AV1qgVYjlskbcMch3apIVELiOEVfBaIGJDoq1N3e25cTvNNO/mA2rDSYLQuz7L +bwlVs6ix02/tFd1R0N5nGKMhziue1l3BOjADHzf/UkOc4UIJGkmUQHrVxpby6FMY +cT+HJ3+OB/8CgYAHtDqWuRbwkKt8zP415KEoehumnTdA5d/y8lTBE2seNVRh3oHX +YTeM4lggc2DYQbzYVVP19iGKAnojaoAGHq3SKlau/iIZKHyWLQhT5g38m8VUPEWF +jAot4jijyD63bEP4tn1pgh5W2dkiM8JWNE+gJd1Hr82sSe6dbIIlti7WDwKBgA3n +PX6nhSmhPXSH5jC+FP05VdoTuxgy947ZvAgeE1xoLgr6/vqqERCgrrQM429dCxdJ +oOZG+cq2JSqZkl9rX6+PrlSUxwlS7CVW/emlH7w2NVdQ7sC9kuDoUlduQ1tmC7jX +GRt8u9hFF0TanSDgz1VVcPpky3MQ71cboj5JwH+ZAoGAAcDSlHuOu2xnbdnaTdE1 +MK0q/Yt3EOCGp0FRBkResPhhoV/TXadk0sJJbWFnleRFDkmgsw2QAwVhKQHCRWOu +b4G8EnEXY/91IoRSevWtZTNf+/9+y5w4iOWI3rGJNpXCEgzN7Yc60z1sIsC/fu+Q +YxXNiLIYUB4Pk4pDNRNLMZo= +-----END PRIVATE KEY----- diff --git a/contrib/gitops-examples/v4.1/www2.online-boutique.local.chain b/contrib/gitops-examples/v4.1/www2.online-boutique.local.chain new file mode 100644 index 0000000..76e8157 --- /dev/null +++ b/contrib/gitops-examples/v4.1/www2.online-boutique.local.chain @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDSTCCAjGgAwIBAgIUbzaZNPdYel80IQ81PCNfMllLnYgwDQYJKoZIhvcNAQEL +BQAwSTEiMCAGCSqGSIb3DQEJARYTbXlAZW1haWxhZGRyZXNzLmNvbTEjMCEGA1UE +Awwad3d3Mi5vbmxpbmUtYm91dGlxdWUubG9jYWwwHhcNMjMwMjE0MTUwNzMxWhcN +MzMwMjExMTUwNzMxWjBJMSIwIAYJKoZIhvcNAQkBFhNteUBlbWFpbGFkZHJlc3Mu +Y29tMSMwIQYDVQQDDBp3d3cyLm9ubGluZS1ib3V0aXF1ZS5sb2NhbDCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAN2OHfrfxO6hhg4dGMi0Q4+iwuuNGv2J +eh+t8g9A+dGB4zUkU0SEa2EFLjyAw2P7F1XAtiClHmS9nA0uTuj9nuw9Xk+wFUlm +ahMkdkLBRNNsH3O1gTYUQSOK7xm8FOMBnOYQqJOHILXsXgM2z8QHpX//wl15aGXf +9vPm4CQUqqsEGNWDfpOQ8jfhrA4T8W1WuAXzJAQmZfhW1qMWxoj1iTCk+Q6nH/8a +MWLWPjTes0bTSRXRTpuQCyeHCaqS8oPPshcd52/FXChnYbNAG1s9Q+35MrgDztL6 +dPkNiA9UaUY7pumNip93ZnsYwDmOPnDBPqfvy9Dk1zQuaAsJ1FXwRNsCAwEAAaMp +MCcwJQYDVR0RBB4wHIIad3d3Mi5vbmxpbmUtYm91dGlxdWUubG9jYWwwDQYJKoZI +hvcNAQELBQADggEBADT2SchC4VhWqCYRsw/3nqrVh0JQmD+/x9JjiNsY6fLnG6uW +8bs8/714qoghJff67H60B6NbrS3lpTZ3bmyotcGtwNNsY4QSFHRu/x4OyIrTfjKb +VIdhRM2Atwc1s1YA6c+2JBquBDhniqABKG9u+j1aa2ElXSalCj+Kozm8ma0yduVw +TX8zS6XZl57vSk/Qo/PZvbmbs8EMOwTUCLn6WQldAARCLughjd9LI9prNpBlYon6 +jmZ8oi59arK16cKe6i6tQ3QExT2kQsLlrK/jFw0xqrFnveKgUTeevViT4WZnqsCm +awrPhrbDn0F7HmFSrgenrW8BIuJ2crBHy0230Lc= +-----END CERTIFICATE----- diff --git a/contrib/gitops-examples/v4.1/www2.online-boutique.local.crt b/contrib/gitops-examples/v4.1/www2.online-boutique.local.crt new file mode 100644 index 0000000..76e8157 --- /dev/null +++ b/contrib/gitops-examples/v4.1/www2.online-boutique.local.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDSTCCAjGgAwIBAgIUbzaZNPdYel80IQ81PCNfMllLnYgwDQYJKoZIhvcNAQEL +BQAwSTEiMCAGCSqGSIb3DQEJARYTbXlAZW1haWxhZGRyZXNzLmNvbTEjMCEGA1UE +Awwad3d3Mi5vbmxpbmUtYm91dGlxdWUubG9jYWwwHhcNMjMwMjE0MTUwNzMxWhcN +MzMwMjExMTUwNzMxWjBJMSIwIAYJKoZIhvcNAQkBFhNteUBlbWFpbGFkZHJlc3Mu +Y29tMSMwIQYDVQQDDBp3d3cyLm9ubGluZS1ib3V0aXF1ZS5sb2NhbDCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAN2OHfrfxO6hhg4dGMi0Q4+iwuuNGv2J +eh+t8g9A+dGB4zUkU0SEa2EFLjyAw2P7F1XAtiClHmS9nA0uTuj9nuw9Xk+wFUlm +ahMkdkLBRNNsH3O1gTYUQSOK7xm8FOMBnOYQqJOHILXsXgM2z8QHpX//wl15aGXf +9vPm4CQUqqsEGNWDfpOQ8jfhrA4T8W1WuAXzJAQmZfhW1qMWxoj1iTCk+Q6nH/8a +MWLWPjTes0bTSRXRTpuQCyeHCaqS8oPPshcd52/FXChnYbNAG1s9Q+35MrgDztL6 +dPkNiA9UaUY7pumNip93ZnsYwDmOPnDBPqfvy9Dk1zQuaAsJ1FXwRNsCAwEAAaMp +MCcwJQYDVR0RBB4wHIIad3d3Mi5vbmxpbmUtYm91dGlxdWUubG9jYWwwDQYJKoZI +hvcNAQELBQADggEBADT2SchC4VhWqCYRsw/3nqrVh0JQmD+/x9JjiNsY6fLnG6uW +8bs8/714qoghJff67H60B6NbrS3lpTZ3bmyotcGtwNNsY4QSFHRu/x4OyIrTfjKb +VIdhRM2Atwc1s1YA6c+2JBquBDhniqABKG9u+j1aa2ElXSalCj+Kozm8ma0yduVw +TX8zS6XZl57vSk/Qo/PZvbmbs8EMOwTUCLn6WQldAARCLughjd9LI9prNpBlYon6 +jmZ8oi59arK16cKe6i6tQ3QExT2kQsLlrK/jFw0xqrFnveKgUTeevViT4WZnqsCm +awrPhrbDn0F7HmFSrgenrW8BIuJ2crBHy0230Lc= +-----END CERTIFICATE----- diff --git a/contrib/gitops-examples/v4.1/www2.online-boutique.local.key b/contrib/gitops-examples/v4.1/www2.online-boutique.local.key new file mode 100644 index 0000000..b1e2db4 --- /dev/null +++ b/contrib/gitops-examples/v4.1/www2.online-boutique.local.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDdjh3638TuoYYO +HRjItEOPosLrjRr9iXofrfIPQPnRgeM1JFNEhGthBS48gMNj+xdVwLYgpR5kvZwN +Lk7o/Z7sPV5PsBVJZmoTJHZCwUTTbB9ztYE2FEEjiu8ZvBTjAZzmEKiThyC17F4D +Ns/EB6V//8JdeWhl3/bz5uAkFKqrBBjVg36TkPI34awOE/FtVrgF8yQEJmX4Vtaj +FsaI9YkwpPkOpx//GjFi1j403rNG00kV0U6bkAsnhwmqkvKDz7IXHedvxVwoZ2Gz +QBtbPUPt+TK4A87S+nT5DYgPVGlGO6bpjYqfd2Z7GMA5jj5wwT6n78vQ5Nc0LmgL +CdRV8ETbAgMBAAECggEBAJkAy010eWC+5YAbsgDxFHM+WNQo90m+RjtYegD0w1Ff +HNSXSHXZ6PnwhPS9i7IhNgU/d8dloG67zuf/YflfJQBKhTYNCTZOZtTfalhBdlF3 +quTSpO/+3tk32lVwYRBADeWH/ZMcT4ezO12G3can7TBPJSA+ds1b9QSNHZ9tMku0 +2J2pzT8C8V0vHn49oKO9D4Q+QwRvisT6FKNaOW/xKZjtUIvyjVvjENnBW7MQ5y5z +IHsUNkwGgZ2V2IbDouM4+A42J9iDsseM2euSsaDfum3vj9MPTqXE8zUUdX+/miMq +RJTXLxDdkPaCbQftQ7y/cIAFLDZS8Fvvsa5pX69jl1ECgYEA8sQ6S7kcyYgBLX4T +KVYPg1sFTgCofW5nTgCND4kiVvIDz/uk9vsfuMj7FMS3fcCfZorPdYEKa+Zwi4wZ +NMCSArtezbwFBeaOiZTpjh2k8CBrtumdcU/EfzQOvrVcSzVc27ditwRAmONeBXVw +GSvqSfjQKLDh/4Y191qOmgyIBDMCgYEA6aHjQr43XM811VQhTPBtLlBccD8hCS23 +NcujPMa85Vz+RES3OqHt2nH9NGuYrexGbrSt3ckjxnO+TdsS4f94bKhquXvv7AkX +7qr8KZEwHncyjcL3uTChbjWr2xJR4lSnS5PAbnWfyR48HXkHO9IVPe/IcYnvuGDN +AeMJkCgp1LkCgYEArvD/PXAxMX1js9/FeSU+Wp8t7G8G/BSiNxColkhxSYxveOJT +l3OSAXw7i1TTEbjMZX2kUH3j/6t48ObNhzk6PuO9Rq62Q/FISBbaU4JDSJNka9Rf +k7cy16Ow+HcDAmN6/g5iAZb74fD+4Rom5MzDsfiuMJR+179khlJortRW9AcCgYAD +4AKD9eG3MVykOCwBOa+l6AFQf0uN+msigkkn1egGKd+xxC4B0/O8/s0DVJGIuPWG +GosTtaVZQkwywGJ0yyb1Lmnuv6aAFLqH4+Ag1F6m8rUs8sHnGW5kBJHgJVKkXWEU ++NNlQaAv1seKeZpsHJTrnRGHCJGoTjq4QErFUFU5SQKBgEYMGdnPP4ZDAeezPCML +42RVHOAG9IsMl4mV7+fwcEHcmuvb8nNgKNfayjG/NbiQWtT4v7iQH4GHYzUobaqr +3xnQlec3jBrLEsWR8ItRWKxKO2GGLlbmQ99Kn3UGh+6FtXbxwB0hhkFfKXJzM10N +Y9ruUdyYpjGO/j9ROjoBepUH +-----END PRIVATE KEY----- diff --git a/contrib/postman/NGINX Declarative API.postman_collection.json b/contrib/postman/NGINX Declarative API.postman_collection.json index 89a1812..5d6ecc6 100644 --- a/contrib/postman/NGINX Declarative API.postman_collection.json +++ b/contrib/postman/NGINX Declarative API.postman_collection.json @@ -1,10 +1,10 @@ { "info": { - "_postman_id": "2e2d1a74-49b7-4f4f-8c46-decd87287609", + "_postman_id": "fc1e8cd7-6c83-4877-8095-50f28e9bb468", "name": "NGINX Declarative API", "description": "Declarative REST API and GitOps automation layer for NGINX Instance Manager", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "1667416" + "_exporter_id": "30973250" }, "item": [ { @@ -3806,6 +3806,1989 @@ ] } ] + }, + { + "name": "v4.1", + "item": [ + { + "name": "Configuration generation", + "item": [ + { + "name": "Basic - LB", + "item": [ + { + "name": "Basic - LB - plaintext output", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"plaintext\"\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"sample L4 service\",\n \"listen\": {\n \"address\": \"10053\",\n \"protocol\": \"udp\"\n },\n \"upstream\": \"l4_upstream\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"l4_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:53\"\n },\n {\n \"server\": \"10.0.0.2:53\"\n }\n ]\n }\n ]\n },\n \"http\": {\n \"servers\": [\n {\n \"name\": \"A sample HTTP service\",\n \"names\": [\n \"server_8080.nginx.lab\",\n \"server_8081.nginx.lab\"\n ],\n \"listen\": {\n \"address\": \"8080\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/access_log\",\n \"error\": \"/var/log/nginx/error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://test_upstream\",\n \"health_check\": {\n \"enabled\": true\n }\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\"\n },\n {\n \"server\": \"10.0.0.2:80\"\n }\n ]\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config" + ] + } + }, + "response": [] + }, + { + "name": "Basic - LB - json b64 encoded output", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"json\"\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"sample L4 service\",\n \"listen\": {\n \"address\": \"10053\",\n \"protocol\": \"udp\"\n },\n \"upstream\": \"l4_upstream\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"l4_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:53\"\n },\n {\n \"server\": \"10.0.0.2:53\"\n }\n ]\n }\n ]\n },\n \"http\": {\n \"servers\": [\n {\n \"name\": \"A sample HTTP service\",\n \"names\": [\n \"server_8080.nginx.lab\",\n \"server_8081.nginx.lab\"\n ],\n \"listen\": {\n \"address\": \"8080\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/access_log\",\n \"error\": \"/var/log/nginx/error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://test_upstream\",\n \"health_check\": {\n \"enabled\": true\n }\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\"\n },\n {\n \"server\": \"10.0.0.2:80\"\n }\n ]\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config" + ] + } + }, + "response": [] + }, + { + "name": "Basic - LB - ConfigMap output", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"configmap\",\n \"configmap\": {\n \"name\": \"nginx.test\",\n \"filename\": \"testservice.conf\",\n \"namespace\": \"test-namespace\"\n }\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"sample L4 service\",\n \"listen\": {\n \"address\": \"10053\",\n \"protocol\": \"udp\"\n },\n \"upstream\": \"l4_upstream\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"l4_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:53\"\n },\n {\n \"server\": \"10.0.0.2:53\"\n }\n ]\n }\n ]\n },\n \"http\": {\n \"servers\": [\n {\n \"name\": \"A sample HTTP service\",\n \"names\": [\n \"server_8080.nginx.lab\",\n \"server_8081.nginx.lab\"\n ],\n \"listen\": {\n \"address\": \"8080\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/access_log\",\n \"error\": \"/var/log/nginx/error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://test_upstream\",\n \"health_check\": {\n \"enabled\": true\n }\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\"\n },\n {\n \"server\": \"10.0.0.2:80\"\n }\n ]\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config" + ] + } + }, + "response": [] + }, + { + "name": "Basic - LB - HTTP output", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"http\",\n \"http\": {\n \"url\": \"http://192.168.2.19:8080/path/service\"\n }\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"sample L4 service\",\n \"listen\": {\n \"address\": \"10053\",\n \"protocol\": \"udp\"\n },\n \"upstream\": \"l4_upstream\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"l4_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:53\"\n },\n {\n \"server\": \"10.0.0.2:53\"\n }\n ]\n }\n ]\n },\n \"http\": {\n \"servers\": [\n {\n \"name\": \"A sample HTTP service\",\n \"names\": [\n \"server_8080.nginx.lab\",\n \"server_8081.nginx.lab\"\n ],\n \"listen\": {\n \"address\": \"8080\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/access_log\",\n \"error\": \"/var/log/nginx/error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://test_upstream\",\n \"health_check\": {\n \"enabled\": true\n }\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\"\n },\n {\n \"server\": \"10.0.0.2:80\"\n }\n ]\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Advanced", + "item": [ + { + "name": "Advanced LB - plaintext output", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"plaintext\"\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"sample_layer4_service\",\n \"listen\": {\n \"address\": \"10053\",\n \"protocol\": \"tcp\",\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"upstream\": \"l4_upstream\",\n \"snippet\": {\n \"content\": \"IyBUaGlzIGlzIGEgbDQgdXBzdHJlYW0gc25pcHBldCBjb21tZW50Cg==\"\n }\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"l4_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:53\"\n },\n {\n \"server\": \"10.0.0.2:53\"\n }\n ]\n }\n ]\n },\n \"http\": {\n \"servers\": [\n {\n \"name\": \"HTTP test application\",\n \"names\": [\n \"server_8080.nginx.lab\",\n \"server_8081.nginx.lab\"\n ],\n \"listen\": {\n \"address\": \"127.0.0.1:8080\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/access_log\",\n \"error\": \"/var/log/nginx/error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/test\",\n \"urimatch\": \"exact\",\n \"upstream\": \"http://test_upstream\",\n \"health_check\": {\n \"enabled\": true,\n \"uri\": \"/healthcheck\",\n \"interval\": 5,\n \"fails\": 3,\n \"passes\": 2\n },\n \"rate_limit\": {\n \"profile\": \"test_ratelimit\",\n \"httpcode\": 429,\n \"burst\": 10,\n \"delay\": 3\n },\n \"app_protect\": {\n \"enabled\": true,\n \"policy\": \"test_policy\",\n \"log\": {\n \"profile_name\": \"log_illegal\",\n \"enabled\": true,\n \"destination\": \"192.168.1.5:514\"\n }\n },\n \"snippet\": {\n \"content\": \"IyBUaGlzIGlzIGEgbG9jYXRpb24gc25pcHBldCBjb21tZW50Cg==\"\n }\n }\n ],\n \"app_protect\": {\n \"enabled\": true,\n \"policy\": \"test_policy\",\n \"log\": {\n \"profile_name\": \"log_blocked\",\n \"enabled\": true,\n \"destination\": \"192.168.1.5:514\"\n }\n },\n \"snippet\": {\n \"content\": \"IyBUaGlzIGlzIGEgc2VydmVyIHNuaXBwZXQgY29tbWVudAo=\"\n }\n },\n {\n \"name\": \"another HTTP test application\",\n \"names\": [\n \"server_443\"\n ],\n \"listen\": {\n \"address\": \"127.0.0.1:443\",\n \"http2\": true,\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"upstream\": \"http://test_upstream\"\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\",\n \"weight\": 5,\n \"max_fails\": 2,\n \"fail_timeout\": \"30s\",\n \"max_conns\": 3,\n \"slow_start\": \"30s\"\n },\n {\n \"server\": \"10.0.0.2:80\",\n \"backup\": true\n }\n ],\n \"sticky\": {\n \"cookie\": \"cookie_name\",\n \"expires\": \"1h\",\n \"domain\": \".testserver\",\n \"path\": \"/\"\n },\n \"snippet\": {\n \"content\": \"IyBUaGlzIGlzIGEgdXBzdHJlYW0gc25pcHBldCBjb21tZW50Cg==\"\n }\n }\n ],\n \"rate_limit\": [\n {\n \"name\": \"test_ratelimit\",\n \"key\": \"$binary_remote_addr\",\n \"size\": \"10m\",\n \"rate\": \"1r/s\"\n }\n ],\n \"maps\": [\n {\n \"match\": \"$host$request_uri\",\n \"variable\": \"$backend\",\n \"entries\": [\n {\n \"key\": \"www.test.lab/app1/\",\n \"keymatch\": \"iregex\",\n \"value\": \"upstream_1\"\n },\n {\n \"key\": \"(.*).test.lab/app2/\",\n \"keymatch\": \"regex\",\n \"value\": \"upstream_2\"\n }\n ]\n }\n ],\n \"nginx_plus_api\": {\n \"write\": true,\n \"listen\": \"127.0.0.1:8080\",\n \"allow_acl\": \"0.0.0.0/0\"\n },\n \"snippet\": {\n \"content\": \"IyBUaGlzIGlzIGEgSFRUUCBzbmlwcGV0IGNvbW1lbnQK\"\n }\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config" + ] + } + }, + "response": [] + }, + { + "name": "Advanced LB - json b64 encoded output", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"json\"\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"sample_layer4_service\",\n \"listen\": {\n \"address\": \"10053\",\n \"protocol\": \"tcp\",\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"upstream\": \"l4_upstream\",\n \"snippet\": \"IyBUaGlzIGlzIGEgbDQgdXBzdHJlYW0gc25pcHBldCBjb21tZW50Cg==\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"l4_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:53\"\n },\n {\n \"server\": \"10.0.0.2:53\"\n }\n ]\n }\n ]\n },\n \"http\": {\n \"servers\": [\n {\n \"name\": \"HTTP test application\",\n \"names\": [\n \"server_8080.nginx.lab\",\n \"server_8081.nginx.lab\"\n ],\n \"listen\": {\n \"address\": \"127.0.0.1:8080\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/access_log\",\n \"error\": \"/var/log/nginx/error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/test\",\n \"urimatch\": \"exact\",\n \"upstream\": \"http://test_upstream\",\n \"health_check\": {\n \"enabled\": true,\n \"uri\": \"/healthcheck\",\n \"interval\": 5,\n \"fails\": 3,\n \"passes\": 2\n },\n \"rate_limit\": {\n \"profile\": \"test_ratelimit\",\n \"httpcode\": 429,\n \"burst\": 10,\n \"delay\": 3\n },\n \"app_protect\": {\n \"enabled\": true,\n \"policy\": \"test_policy\",\n \"log\": {\n \"profile_name\": \"log_illegal\",\n \"enabled\": true,\n \"destination\": \"192.168.1.5:514\"\n }\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgbG9jYXRpb24gc25pcHBldCBjb21tZW50Cg==\"\n }\n ],\n \"app_protect\": {\n \"enabled\": true,\n \"policy\": \"test_policy\",\n \"log\": {\n \"profile_name\": \"log_blocked\",\n \"enabled\": true,\n \"destination\": \"192.168.1.5:514\"\n }\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgc2VydmVyIHNuaXBwZXQgY29tbWVudAo=\"\n },\n {\n \"name\": \"another HTTP test application\",\n \"names\": [\n \"server_443\"\n ],\n \"listen\": {\n \"address\": \"127.0.0.1:443\",\n \"http2\": true,\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"upstream\": \"http://test_upstream\"\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\",\n \"weight\": 5,\n \"max_fails\": 2,\n \"fail_timeout\": \"30s\",\n \"max_conns\": 3,\n \"slow_start\": \"30s\"\n },\n {\n \"server\": \"10.0.0.2:80\",\n \"backup\": true\n }\n ],\n \"sticky\": {\n \"cookie\": \"cookie_name\",\n \"expires\": \"1h\",\n \"domain\": \".testserver\",\n \"path\": \"/\"\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgdXBzdHJlYW0gc25pcHBldCBjb21tZW50Cg==\"\n }\n ],\n \"rate_limit\": [\n {\n \"name\": \"test_ratelimit\",\n \"key\": \"$binary_remote_addr\",\n \"size\": \"10m\",\n \"rate\": \"1r/s\"\n }\n ],\n \"maps\": [\n {\n \"match\": \"$host$request_uri\",\n \"variable\": \"$backend\",\n \"entries\": [\n {\n \"key\": \"www.test.lab/app1/\",\n \"keymatch\": \"iregex\",\n \"value\": \"upstream_1\"\n },\n {\n \"key\": \"(.*).test.lab/app2/\",\n \"keymatch\": \"regex\",\n \"value\": \"upstream_2\"\n }\n ]\n }\n ],\n \"nginx_plus_api\": {\n \"write\": true,\n \"listen\": \"127.0.0.1:8080\",\n \"allow_acl\": \"0.0.0.0/0\"\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgSFRUUCBzbmlwcGV0IGNvbW1lbnQK\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config" + ] + } + }, + "response": [] + }, + { + "name": "Advanced LB - ConfigMap output", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"configmap\",\n \"configmap\": {\n \"name\": \"nginx.test\",\n \"filename\": \"testservice.conf\",\n \"namespace\": \"test-namespace\"\n }\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"sample_layer4_service\",\n \"listen\": {\n \"address\": \"10053\",\n \"protocol\": \"tcp\",\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"upstream\": \"l4_upstream\",\n \"snippet\": \"IyBUaGlzIGlzIGEgbDQgdXBzdHJlYW0gc25pcHBldCBjb21tZW50Cg==\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"l4_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:53\"\n },\n {\n \"server\": \"10.0.0.2:53\"\n }\n ]\n }\n ]\n },\n \"http\": {\n \"servers\": [\n {\n \"name\": \"HTTP test application\",\n \"names\": [\n \"server_8080.nginx.lab\",\n \"server_8081.nginx.lab\"\n ],\n \"listen\": {\n \"address\": \"127.0.0.1:8080\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/access_log\",\n \"error\": \"/var/log/nginx/error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/test\",\n \"urimatch\": \"exact\",\n \"upstream\": \"http://test_upstream\",\n \"health_check\": {\n \"enabled\": true,\n \"uri\": \"/healthcheck\",\n \"interval\": 5,\n \"fails\": 3,\n \"passes\": 2\n },\n \"rate_limit\": {\n \"profile\": \"test_ratelimit\",\n \"httpcode\": 429,\n \"burst\": 10,\n \"delay\": 3\n },\n \"app_protect\": {\n \"enabled\": true,\n \"policy\": \"test_policy\",\n \"log\": {\n \"profile_name\": \"log_illegal\",\n \"enabled\": true,\n \"destination\": \"192.168.1.5:514\"\n }\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgbG9jYXRpb24gc25pcHBldCBjb21tZW50Cg==\"\n }\n ],\n \"app_protect\": {\n \"enabled\": true,\n \"policy\": \"test_policy\",\n \"log\": {\n \"profile_name\": \"log_blocked\",\n \"enabled\": true,\n \"destination\": \"192.168.1.5:514\"\n }\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgc2VydmVyIHNuaXBwZXQgY29tbWVudAo=\"\n },\n {\n \"name\": \"another HTTP test application\",\n \"names\": [\n \"server_443\"\n ],\n \"listen\": {\n \"address\": \"127.0.0.1:443\",\n \"http2\": true,\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"upstream\": \"http://test_upstream\"\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\",\n \"weight\": 5,\n \"max_fails\": 2,\n \"fail_timeout\": \"30s\",\n \"max_conns\": 3,\n \"slow_start\": \"30s\"\n },\n {\n \"server\": \"10.0.0.2:80\",\n \"backup\": true\n }\n ],\n \"sticky\": {\n \"cookie\": \"cookie_name\",\n \"expires\": \"1h\",\n \"domain\": \".testserver\",\n \"path\": \"/\"\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgdXBzdHJlYW0gc25pcHBldCBjb21tZW50Cg==\"\n }\n ],\n \"rate_limit\": [\n {\n \"name\": \"test_ratelimit\",\n \"key\": \"$binary_remote_addr\",\n \"size\": \"10m\",\n \"rate\": \"1r/s\"\n }\n ],\n \"maps\": [\n {\n \"match\": \"$host$request_uri\",\n \"variable\": \"$backend\",\n \"entries\": [\n {\n \"key\": \"www.test.lab/app1/\",\n \"keymatch\": \"iregex\",\n \"value\": \"upstream_1\"\n },\n {\n \"key\": \"(.*).test.lab/app2/\",\n \"keymatch\": \"regex\",\n \"value\": \"upstream_2\"\n }\n ]\n }\n ],\n \"nginx_plus_api\": {\n \"write\": true,\n \"listen\": \"127.0.0.1:8080\",\n \"allow_acl\": \"0.0.0.0/0\"\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgSFRUUCBzbmlwcGV0IGNvbW1lbnQK\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config" + ] + } + }, + "response": [] + }, + { + "name": "Advanced LB - HTTP output", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"http\",\n \"http\": {\n \"url\": \"http://192.168.1.19:8080/path/service\"\n }\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"sample_layer4_service\",\n \"listen\": {\n \"address\": \"10053\",\n \"protocol\": \"tcp\",\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"upstream\": \"l4_upstream\",\n \"snippet\": \"IyBUaGlzIGlzIGEgbDQgdXBzdHJlYW0gc25pcHBldCBjb21tZW50Cg==\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"l4_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:53\"\n },\n {\n \"server\": \"10.0.0.2:53\"\n }\n ]\n }\n ]\n },\n \"http\": {\n \"servers\": [\n {\n \"name\": \"HTTP test application\",\n \"names\": [\n \"server_8080.nginx.lab\",\n \"server_8081.nginx.lab\"\n ],\n \"listen\": {\n \"address\": \"127.0.0.1:8080\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/access_log\",\n \"error\": \"/var/log/nginx/error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/test\",\n \"urimatch\": \"exact\",\n \"upstream\": \"http://test_upstream\",\n \"health_check\": {\n \"enabled\": true,\n \"uri\": \"/healthcheck\",\n \"interval\": 5,\n \"fails\": 3,\n \"passes\": 2\n },\n \"rate_limit\": {\n \"profile\": \"test_ratelimit\",\n \"httpcode\": 429,\n \"burst\": 10,\n \"delay\": 3\n },\n \"app_protect\": {\n \"enabled\": true,\n \"policy\": \"test_policy\",\n \"log\": {\n \"profile_name\": \"log_illegal\",\n \"enabled\": true,\n \"destination\": \"192.168.1.5:514\"\n }\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgbG9jYXRpb24gc25pcHBldCBjb21tZW50Cg==\"\n }\n ],\n \"app_protect\": {\n \"enabled\": true,\n \"policy\": \"test_policy\",\n \"log\": {\n \"profile_name\": \"log_blocked\",\n \"enabled\": true,\n \"destination\": \"192.168.1.5:514\"\n }\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgc2VydmVyIHNuaXBwZXQgY29tbWVudAo=\"\n },\n {\n \"name\": \"another HTTP test application\",\n \"names\": [\n \"server_443\"\n ],\n \"listen\": {\n \"address\": \"127.0.0.1:443\",\n \"http2\": true,\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"upstream\": \"http://test_upstream\"\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\",\n \"weight\": 5,\n \"max_fails\": 2,\n \"fail_timeout\": \"30s\",\n \"max_conns\": 3,\n \"slow_start\": \"30s\"\n },\n {\n \"server\": \"10.0.0.2:80\",\n \"backup\": true\n }\n ],\n \"sticky\": {\n \"cookie\": \"cookie_name\",\n \"expires\": \"1h\",\n \"domain\": \".testserver\",\n \"path\": \"/\"\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgdXBzdHJlYW0gc25pcHBldCBjb21tZW50Cg==\"\n }\n ],\n \"rate_limit\": [\n {\n \"name\": \"test_ratelimit\",\n \"key\": \"$binary_remote_addr\",\n \"size\": \"10m\",\n \"rate\": \"1r/s\"\n }\n ],\n \"maps\": [\n {\n \"match\": \"$host$request_uri\",\n \"variable\": \"$backend\",\n \"entries\": [\n {\n \"key\": \"www.test.lab/app1/\",\n \"keymatch\": \"iregex\",\n \"value\": \"upstream_1\"\n },\n {\n \"key\": \"(.*).test.lab/app2/\",\n \"keymatch\": \"regex\",\n \"value\": \"upstream_2\"\n }\n ]\n }\n ],\n \"nginx_plus_api\": {\n \"write\": true,\n \"listen\": \"127.0.0.1:8080\",\n \"allow_acl\": \"0.0.0.0/0\"\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgSFRUUCBzbmlwcGV0IGNvbW1lbnQK\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config" + ] + } + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Declarative automation examples", + "item": [ + { + "name": "API Gateway", + "item": [ + { + "name": "Ergast API", + "item": [ + { + "name": "Ergast API Gateway and DevPortal", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Ergast API\",\n \"names\": [\n \"apigw.nginx.lab\"\n ],\n \"resolver\": \"8.8.8.8\",\n \"listen\": {\n \"address\": \"80\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/apigw.nginx.lab-access_log\",\n \"error\": \"/var/log/nginx/apigw.nginx.lab-error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/ergast\",\n \"urimatch\": \"prefix\",\n \"snippet\": {\n \"content\": \"IyBUZXN0IFNOSVBQRVQK\"\n },\n \"apigateway\": {\n \"openapi_schema\": {\n \"content\": \"https://raw.githubusercontent.com/adampax/ergast-f1-openapi-doc/e558eea18e176e4f78a8765ac7eccc804b5157ff/ergast-openapi-doc.yaml\"\n },\n \"api_gateway\": {\n \"enabled\": true,\n \"strip_uri\": true\n },\n \"developer_portal\": {\n \"enabled\": true,\n \"uri\": \"/ergast-devportal.html\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/ergast-access_log\",\n \"error\": \"/var/log/nginx/ergast-error_log\"\n }\n }\n }\n ]\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Petstore API", + "item": [ + { + "name": "Petstore API Gateway RateLimit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Petstore API\",\n \"names\": [\n \"apigw.nginx.lab\"\n ],\n \"resolver\": \"8.8.8.8\",\n \"listen\": {\n \"address\": \"80\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/apigw.nginx.lab-access_log\",\n \"error\": \"/var/log/nginx/apigw.nginx.lab-error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/petstore\",\n \"urimatch\": \"prefix\",\n \"apigateway\": {\n \"openapi_schema\": {\n \"content\": \"http://petstore.swagger.io/v2/swagger.json\"\n },\n \"api_gateway\": {\n \"enabled\": true,\n \"strip_uri\": true,\n \"server_url\": \"https://petstore.swagger.io/v2\"\n },\n \"developer_portal\": {\n \"enabled\": true,\n \"uri\": \"/petstore-devportal.html\"\n },\n \"rate_limit\": [\n {\n \"profile\": \"petstore_ratelimit\",\n \"httpcode\": 429,\n \"burst\": 0,\n \"delay\": 0,\n \"enforceOnPaths\": true,\n \"paths\": [\n \"/user/login\",\n \"/user/logout\"\n ]\n }\n ]\n },\n \"log\": {\n \"access\": \"/var/log/nginx/petstore-access_log\",\n \"error\": \"/var/log/nginx/petstore-error_log\"\n }\n }\n ]\n }\n ],\n \"rate_limit\": [\n {\n \"name\": \"petstore_ratelimit\",\n \"key\": \"$binary_remote_addr\",\n \"size\": \"10m\",\n \"rate\": \"2r/s\"\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config" + ] + } + }, + "response": [] + }, + { + "name": "Petstore API Gateway RateLimit + JWT Authentication", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Petstore API\",\n \"names\": [\n \"apigw.nginx.lab\"\n ],\n \"resolver\": \"8.8.8.8\",\n \"listen\": {\n \"address\": \"80\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/apigw.nginx.lab-access_log\",\n \"error\": \"/var/log/nginx/apigw.nginx.lab-error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/petstore\",\n \"urimatch\": \"prefix\",\n \"apigateway\": {\n \"openapi_schema\": {\n \"content\": \"http://petstore.swagger.io/v2/swagger.json\",\n \"authentication\": [\n {\n \"profile\": \"Source of truth authentication profile using HTTP header token authentication\"\n }\n ]\n },\n \"api_gateway\": {\n \"enabled\": true,\n \"strip_uri\": true,\n \"server_url\": \"https://petstore.swagger.io/v2\"\n },\n \"developer_portal\": {\n \"enabled\": true,\n \"uri\": \"/petstore-devportal.html\"\n },\n \"authentication\": {\n \"client\": [\n {\n \"profile\": \"Petstore JWT Authentication\"\n }\n ],\n \"enforceOnPaths\": true,\n \"paths\": [\n \"/user/login\",\n \"/user/logout\"\n ]\n },\n \"rate_limit\": [\n {\n \"profile\": \"petstore_ratelimit\",\n \"httpcode\": 429,\n \"burst\": 0,\n \"delay\": 0,\n \"enforceOnPaths\": true,\n \"paths\": [\n \"/user/login\",\n \"/user/logout\"\n ]\n }\n ]\n },\n \"log\": {\n \"access\": \"/var/log/nginx/petstore-access_log\",\n \"error\": \"/var/log/nginx/petstore-error_log\"\n }\n }\n ]\n }\n ],\n \"rate_limit\": [\n {\n \"name\": \"petstore_ratelimit\",\n \"key\": \"$binary_remote_addr\",\n \"size\": \"10m\",\n \"rate\": \"2r/s\"\n }\n ],\n \"authentication\": {\n \"client\": [\n {\n \"name\": \"Petstore JWT Authentication\",\n \"type\": \"jwt\",\n \"jwt\": {\n \"realm\": \"Petstore Authentication\",\n \"key\": \"{\\\"keys\\\": [{\\\"k\\\":\\\"ZmFudGFzdGljand0\\\",\\\"kty\\\":\\\"oct\\\",\\\"kid\\\":\\\"0001\\\"}]}\",\n \"cachetime\": 5\n }\n }\n ],\n \"server\": [\n {\n \"name\": \"Source of truth authentication profile using bearer token authentication\",\n \"type\": \"token\",\n \"token\": {\n \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEiLCJpc3MiOiJCYXNoIEpXVCBHZW5lcmF0b3IiLCJpYXQiOjE3MDI0ODEzNjcsImV4cCI6MTcwMjQ4MTM2OH0.eyJuYW1lIjoiQm9iIERldk9wcyIsInN1YiI6IkpXVCBzdWIgY2xhaW0iLCJpc3MiOiJKV1QgaXNzIGNsYWltIiwicm9sZXMiOlsiZGV2b3BzIl19.SKA_7MszAypMEtX5NDQ0TcUbVYx_Wt0hrtmuyTmrVKU\",\n \"type\": \"bearer\"\n }\n },\n {\n \"name\": \"Source of truth authentication profile using HTTP header token authentication\",\n \"type\": \"token\",\n \"token\": {\n \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEiLCJpc3MiOiJCYXNoIEpXVCBHZW5lcmF0b3IiLCJpYXQiOjE3MDI0ODEzNjcsImV4cCI6MTcwMjQ4MTM2OH0.eyJuYW1lIjoiQm9iIERldk9wcyIsInN1YiI6IkpXVCBzdWIgY2xhaW0iLCJpc3MiOiJKV1QgaXNzIGNsYWltIiwicm9sZXMiOlsiZGV2b3BzIl19.SKA_7MszAypMEtX5NDQ0TcUbVYx_Wt0hrtmuyTmrVKU\",\n \"type\": \"header\",\n \"location\": \"X-AUTH-TOKEN\"\n }\n }\n ]\n }\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Petstore & Ergast API Gateway all in one", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Petstore and Ergast API\",\n \"names\": [\n \"apigw.nginx.lab\"\n ],\n \"resolver\": \"192.168.2.13\",\n \"listen\": {\n \"address\": \"80\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/apigw.nginx.lab-access_log\",\n \"error\": \"/var/log/nginx/apigw.nginx.lab-error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/petstore\",\n \"urimatch\": \"prefix\",\n \"apigateway\": {\n \"openapi_schema\": {\n \"content\": \"http://petstore.swagger.io/v2/swagger.json\"\n },\n \"api_gateway\": {\n \"enabled\": true,\n \"strip_uri\": true,\n \"server_url\": \"https://petstore.swagger.io/v2\"\n },\n \"developer_portal\": {\n \"enabled\": true,\n \"uri\": \"/petstore-devportal.html\"\n },\n \"authentication\": {\n \"client\": [\n {\n \"profile\": \"Petstore JWT Authentication\"\n }\n ],\n \"enforceOnPaths\": true,\n \"paths\": [\n \"/user/login\",\n \"/user/logout\"\n ]\n },\n \"rate_limit\": [\n {\n \"profile\": \"petstore_ratelimit\",\n \"httpcode\": 429,\n \"burst\": 0,\n \"delay\": 0,\n \"enforceOnPaths\": true,\n \"paths\": [\n \"/user/login\",\n \"/user/logout\"\n ]\n }\n ],\n \"log\": {\n \"access\": \"/var/log/nginx/petstore-access_log\",\n \"error\": \"/var/log/nginx/petstore-error_log\"\n }\n }\n },\n {\n \"uri\": \"/ergast\",\n \"urimatch\": \"prefix\",\n \"snippet\": {\n \"content\": \"IyBUZXN0IFNOSVBQRVQK\"\n },\n \"apigateway\": {\n \"openapi_schema\": {\n \"content\": \"https://raw.githubusercontent.com/adampax/ergast-f1-openapi-doc/e558eea18e176e4f78a8765ac7eccc804b5157ff/ergast-openapi-doc.yaml\"\n },\n \"api_gateway\": {\n \"enabled\": true,\n \"strip_uri\": true\n },\n \"developer_portal\": {\n \"enabled\": true,\n \"uri\": \"/ergast-devportal.html\"\n },\n \"rate_limit\": [\n {\n \"profile\": \"ergast_ratelimit\",\n \"httpcode\": 429,\n \"burst\": 0,\n \"delay\": 0\n }\n ],\n \"log\": {\n \"access\": \"/var/log/nginx/ergast-access_log\",\n \"error\": \"/var/log/nginx/ergast-error_log\"\n }\n }\n }\n ]\n }\n ],\n \"rate_limit\": [\n {\n \"name\": \"ergast_ratelimit\",\n \"key\": \"$binary_remote_addr\",\n \"size\": \"10m\",\n \"rate\": \"1r/s\"\n },\n {\n \"name\": \"petstore_ratelimit\",\n \"key\": \"$binary_remote_addr\",\n \"size\": \"10m\",\n \"rate\": \"2r/s\"\n }\n ],\n \"authentication\": {\n \"client\": [\n {\n \"name\": \"Petstore JWT Authentication\",\n \"type\": \"jwt\",\n \"jwt\": {\n \"realm\": \"Petstore Authentication\",\n \"key\": \"{\\\"keys\\\": [{\\\"k\\\":\\\"ZmFudGFzdGljand0\\\",\\\"kty\\\":\\\"oct\\\",\\\"kid\\\":\\\"0001\\\"}]}\",\n \"cachetime\": 5\n }\n }\n ]\n }\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "CRUD automation", + "item": [ + { + "name": "Create initial NGINX configuration", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"HTTP test application\",\n \"names\": [\n \"patched_server.nginx.lab\"\n ],\n \"listen\": {\n \"address\": \"127.0.0.1:8080\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/access_log\",\n \"error\": \"/var/log/nginx/error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://test_upstream\"\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\"\n },\n {\n \"server\": \"10.0.0.2:80\"\n }\n ]\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config" + ] + } + }, + "response": [] + }, + { + "name": "Update HTTP upstream", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\"\n },\n {\n \"server\": \"10.0.0.2:80\"\n },\n {\n \"server\": \"10.0.0.3:80\"\n },\n {\n \"server\": \"10.0.0.4:80\"\n },\n {\n \"server\": \"10.0.0.5:80\"\n }\n ]\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}" + ] + } + }, + "response": [] + }, + { + "name": "Modify and add HTTP upstream", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\"\n },\n {\n \"server\": \"10.0.0.2:80\"\n }\n ]\n },\n {\n \"name\": \"test_upstream_added\",\n \"origin\": [\n {\n \"server\": \"192.168.1.1:80\"\n },\n {\n \"server\": \"192.168.1.2:80\"\n }\n ]\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}" + ] + } + }, + "response": [] + }, + { + "name": "Modify and add HTTP server and upstream", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"HTTP test application\",\n \"names\": [\n \"patched_server_v2.nginx.lab\"\n ],\n \"listen\": {\n \"address\": \"127.0.0.1:8080\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/access_v2_log\",\n \"error\": \"/var/log/nginx/error_v2_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://test_upstream_added\"\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream_added\",\n \"origin\": [\n {\n \"server\": \"192.168.1.100:80\",\n \"weight\": 5,\n \"max_fails\": 2,\n \"fail_timeout\": \"30s\",\n \"max_conns\": 3,\n \"slow_start\": \"30s\"\n },\n {\n \"server\": \"192.168.1.101:80\"\n },\n {\n \"server\": \"192.168.1.102:80\"\n },\n {\n \"server\": \"192.168.1.103:80\"\n }\n ]\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}" + ] + } + }, + "response": [] + }, + { + "name": "Remove HTTP server and upstream", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"HTTP test application\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream_added\"\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}" + ] + } + }, + "response": [] + }, + { + "name": "Modify Stream server and upstream", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"TCP_10053_service\",\n \"listen\": {\n \"address\": \"10053\",\n \"protocol\": \"tcp\"\n },\n \"upstream\": \"TCP_10053_upstream\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"TCP_10053_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:53\"\n },\n {\n \"server\": \"10.0.0.2:53\"\n },\n {\n \"server\": \"10.0.0.3:53\"\n },\n {\n \"server\": \"10.0.0.4:53\"\n }\n ]\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}" + ] + } + }, + "response": [] + }, + { + "name": "Add stream server and upstream", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"TCP_15432_service\",\n \"listen\": {\n \"address\": \"15432\",\n \"protocol\": \"tcp\"\n },\n \"upstream\": \"TCP_15432_upstream\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"TCP_15432_upstream\",\n \"origin\": [\n {\n \"server\": \"172.16.10.1:5432\"\n },\n {\n \"server\": \"172.16.10.1:5432\"\n }\n ]\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}" + ] + } + }, + "response": [] + }, + { + "name": "Remove stream server and upstream #1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"TCP_10053_service\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"TCP_10053_upstream\"\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}" + ] + } + }, + "response": [] + }, + { + "name": "Remove stream server and upstream #2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"TCP_15432_service\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"TCP_15432_upstream\"\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}" + ] + } + }, + "response": [] + }, + { + "name": "Get declaration status", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}/status", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}", + "status" + ] + } + }, + "response": [] + }, + { + "name": "Get declaration", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete declaration", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "GitOps autosync", + "item": [ + { + "name": "NGINX Plus and GitOps", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 5,\n \"modules\": [\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ],\n \"certificates\": [\n {\n \"type\": \"certificate\",\n \"name\": \"test_cert\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v4.1/www.online-boutique.local.crt\"\n }\n },\n {\n \"type\": \"key\",\n \"name\": \"test_key\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v4.1/www.online-boutique.local.key\"\n }\n }\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Online boutique HTTPS\",\n \"names\": [\n \"www.online-boutique.lan\"\n ],\n \"listen\": {\n \"address\": \"0.0.0.0:443\",\n \"http2\": true,\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"log\": {\n \"access\": \"/var/log/nginx/online_boutique_https_access_log\",\n \"error\": \"/var/log/nginx/online_boutique_https_error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://upstream_boutique\"\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"upstream_boutique\",\n \"origin\": [\n {\n \"server\": \"192.168.2.200:80\"\n }\n ]\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config" + ] + } + }, + "response": [] + }, + { + "name": "NGINX App Protect WAF and GitOps", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 5,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ],\n \"certificates\": [\n {\n \"type\": \"certificate\",\n \"name\": \"test_cert\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v2/www.online-boutique.local.crt\"\n }\n },\n {\n \"type\": \"key\",\n \"name\": \"test_key\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v2/www.online-boutique.local.key\"\n }\n }\n ],\n \"policies\": [\n {\n \"type\": \"app_protect\",\n \"name\": \"production-policy\",\n \"active_tag\": \"gitops\",\n \"versions\": [\n {\n \"tag\": \"gitops\",\n \"displayName\": \"Production Policy - GitOps\",\n \"description\": \"This is a production-ready policy - Managed by GitOps\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v2/nap-policy-gitops.json\"\n }\n }\n ]\n }\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Online boutique HTTPS\",\n \"names\": [\n \"www.online-boutique.lan\"\n ],\n \"listen\": {\n \"address\": \"0.0.0.0:443\",\n \"http2\": true,\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"log\": {\n \"access\": \"/var/log/nginx/online_boutique_https_access_log\",\n \"error\": \"/var/log/nginx/online_boutique_https_error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://upstream_boutique\"\n }\n ],\n \"app_protect\": {\n \"enabled\": true,\n \"policy\": \"production-policy\",\n \"log\": {\n \"profile_name\": \"secops_dashboard\",\n \"enabled\": true,\n \"destination\": \"127.0.0.1:514\"\n }\n }\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"upstream_boutique\",\n \"origin\": [\n {\n \"server\": \"192.168.1.200:80\"\n }\n ]\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config" + ] + } + }, + "response": [] + }, + { + "name": "Get declaration status", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}/status", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}", + "status" + ] + } + }, + "response": [] + }, + { + "name": "Get declaration", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete declaration", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Housekeeping - common endpoints", + "item": [ + { + "name": "Clean NGINX configuration", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config" + ] + } + }, + "response": [] + }, + { + "name": "Get declaration status", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}/status", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}", + "status" + ] + } + }, + "response": [] + }, + { + "name": "Get declaration", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete declaration", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "JWT Client Authentication", + "item": [ + { + "name": "JWT Client Authentication - local JWT key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": []\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Online boutique\",\n \"names\": [\n \"www.online-boutique.lan\"\n ],\n \"listen\": {\n \"address\": \"80\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/online-boutique.lan-access_log\",\n \"error\": \"/var/log/nginx/online-boutique.lan-error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://onlineboutique_upstream\",\n \"authentication\": {\n \"client\": [\n {\n \"profile\": \"online_boutique_jwt_authentication_local\"\n }\n ]\n }\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"onlineboutique_upstream\",\n \"origin\": [\n {\n \"server\": \"192.168.2.200:80\"\n }\n ]\n }\n ],\n \"authentication\": {\n \"client\": [\n {\n \"name\": \"online_boutique_jwt_authentication_local\",\n \"type\": \"jwt\",\n \"jwt\": {\n \"realm\": \"Online Boutique Authentication\",\n \"key\": \"{\\\"keys\\\": [{\\\"k\\\":\\\"ZmFudGFzdGljand0\\\",\\\"kty\\\":\\\"oct\\\",\\\"kid\\\":\\\"0001\\\"}]}\",\n \"cachetime\": 5\n }\n },\n {\n \"name\": \"online_boutique_jwt_authentication_key_from_url\",\n \"type\": \"jwt\",\n \"jwt\": {\n \"realm\": \"Online Boutique Authentication GitOps\",\n \"key\": \"http://192.168.2.19/jwks.json\",\n \"cachetime\": 5\n }\n }\n ]\n }\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config" + ] + } + }, + "response": [] + }, + { + "name": "Change to use JWT key stored on external URL", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": []\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Online boutique\",\n \"names\": [\n \"www.online-boutique.lan\"\n ],\n \"listen\": {\n \"address\": \"80\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/online-boutique.lan-access_log\",\n \"error\": \"/var/log/nginx/online-boutique.lan-error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://onlineboutique_upstream\",\n \"authentication\": {\n \"client\": [\n {\n \"profile\": \"online_boutique_jwt_authentication_key_from_url\"\n }\n ]\n }\n }\n ]\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "mTLS Client Authentication", + "item": [ + { + "name": "HTTPS server with mTLS, OCSP, SSL Stapling", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ],\n \"certificates\": [\n {\n \"type\": \"certificate\",\n \"name\": \"server_cert\",\n \"contents\": {\n \"content\": \"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUdSRENDQkN5Z0F3SUJBZ0lVTTNJQVZIRmxhSTVsY1d0TjZxOUVhcnlka0w4d0RRWUpLb1pJaHZjTkFRRUwKQlFBd1NURUxNQWtHQTFVRUJoTUNTVlF4RFRBTEJnTlZCQWdNQkVGemRHa3hFVEFQQmdOVkJBb01DRlJsYzNRZwpUR0ZpTVJnd0ZnWURWUVFEREE5MmJTMWliR0Z1YXk1bVppNXNZVzR3SGhjTk1qTXdOakE0TVRBd01qTTVXaGNOCk1qUXdOakEzTVRBd01qTTVXakJKTVFzd0NRWURWUVFHRXdKSlZERU5NQXNHQTFVRUNBd0VRWE4wYVRFUk1BOEcKQTFVRUNnd0lWR1Z6ZENCTVlXSXhHREFXQmdOVkJBTU1EM1p0TFdKc1lXNXJMbVptTG14aGJqQ0NBaUl3RFFZSgpLb1pJaHZjTkFRRUJCUUFEZ2dJUEFEQ0NBZ29DZ2dJQkFMRFQ2bnZleVZlNi9VZlk2aUtHVC9oV1A0cktDSGR0ClloZWU3RGVZR29QWGhGVjB6a3grVWExanBEZ21WUE1kVEJBdnoxODg5NzlEcHBqdmNYeFhsRmpnaUhjWDhpWVgKSXovSUVMc3dKRUNITWNsNkxmelA5eDVUY1gxTEdFblFOTWhHRzA2MjlxU2NCQmQyUUNiWlY0UWE1TkxlQnQ4cQpHQ2lXY3JiQnR3YlpiSGo1dk9aenJrdHBtRFBGS1V4bXR5b2dBQnNaTllnL0F3Y1l2RXdBOEQ0QTN0VEgxcGhvCkdYY3ZvZWpJelhRMUdmYys5azR3OFhHYWFQOGd2bTdOMXN2MnU2Yld4SHRGZHpWQk9udzJyaHUvWGYyY0N0dW4KUnIxSENKQXRRSDlkbDhzZks1czBSRlVuTlVYbFBiNTFBTjBjVFVGbEYrZlVUVmVON3dNMTdmeVZVY3IydTltSwo0UGdoWjkvMml0ZUpZV3hjK3k4V2NEQzBUV3hwZ2paVEw5Tk1GK2t6SXV2TjJOWFFybjcvSU5UQTMvNFlmWGRPCloxelpTdTlkclRMcG5DZHRpOWxuRHBKODd3bW41cVZSTlZiTlZRbldEeW5yZnoyTU1DY21jLzcvdkJFN2dDemQKNFJLWHJLdHloenlQSitycmh3NmpxYVA4QytaZGRvKzkvak9QVDFTSnUxZ21VbzFuZ2hBMWh2N0M5RUYrM2xQVApYSk5WV3dtYkdWK0p4cUdKSjJSa2toMlIrZTVIREdRY2hGWjJIcXBGTGVQN0trTHJBR2RkZFZQWEZhQ0RiU0R6ClJQd0I5WFlhakg5Zm5QWEtFT3ZpVEJhQVNjWUZwTXB5cm02UkxHUGRSVnE2RUNYVlB4MDdHdGFCaEVvVWIwK2YKVkZnNExtQkx4MldQQWdNQkFBR2pnZ0VpTUlJQkhqQUpCZ05WSFJNRUFqQUFNQkVHQ1dDR1NBR0crRUlCQVFRRQpBd0lHUURBekJnbGdoa2dCaHZoQ0FRMEVKaFlrVDNCbGJsTlRUQ0JIWlc1bGNtRjBaV1FnVTJWeWRtVnlJRU5sCmNuUnBabWxqWVhSbE1CMEdBMVVkRGdRV0JCVHZFZWJGK1JDV0JhcGVPWUdpQ0YyVHZxbExYekNCaEFZRFZSMGoKQkgwd2U0QVVFdW9Db3kvcmhMQmxzcm5KdXE2QzFJczQxbFNoVGFSTE1Fa3hDekFKQmdOVkJBWVRBa2xVTVEwdwpDd1lEVlFRSURBUkJjM1JwTVJFd0R3WURWUVFLREFoVVpYTjBJRXhoWWpFWU1CWUdBMVVFQXd3UGRtMHRZbXhoCmJtc3VabVl1YkdGdWdoUld4QjhCa3lmK1RkQXc2Q3dPZE1aT0k0NlZ2REFPQmdOVkhROEJBZjhFQkFNQ0JhQXcKRXdZRFZSMGxCQXd3Q2dZSUt3WUJCUVVIQXdFd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dJQkFHUDR6ZkdseTI1RwpneTBSeC9SSTNpNzJDVlIrSXY3SW5WTUVGWDZqRHRNV3hSblFtRGZsMWtTOVF1Y3hNb0tnOE9URStMcnlzdGJsClF4WGZiakZQekNoNHB1UGtGTmNBeG1mVmR4b20xR1lodWpoYTBQOUswUURZSDZycGlUaFdSQ2greUovQm1qZ2wKTlJabks4WGRqME85Ui9XKzJrTFRac2VFbS9hZHFVQ3dkYzNBWWlNWGh4QXkvQlh3bFRQeDMyMHZCcXYxZGFyVgp5ZlVoRlM1Rkg3enV2bGtGQ1p6M3lpOGYvYXMwbkRTUkFrY3dPRFQvN1diQlN4QTk3ZzJmRk1EMEI3WlUvbndGCmU4VnRzNDl3YmZ6QWJRMk40RUc2OEVhODE1VlFRM2N6YWthdjBCdkxHL2UwT0habGxYcUVhV1ZlWFJtSWFFOHcKWko5OEhUaDJMbUlFV2Jpdm94Kyt2UXd3bVhKTm1DRFVXNnVmcHdBOVdKQ0VhYmhxeXdGVzh1dFVENzRTVXE3SApEUDhNamtJZ0o3ekl2Tkd1RkFsSzd6c2xpV2pzeUN1OGVNamhvN2pVRFhGR1R0R0ZMUGtVa08vSysrSGVVRFg0Cm1OWDJ2aHI3NGRqRkNBTTEvOTYxWnB5NUFYUzZkd2g3MFlJL2dMdldSL0J1ejBnNEp6YUI2UFo4M1ErYm9QVHYKM1ZIS2xOWjlKQlhRTmtSc3N6U0dYWG5MYmtOTmNwVFg2cnAyZ1pUSS9NNDhGTnBxanAxOXRpQVg3bWN0cTl2SgpNejhvemhEcHZmSTlnMjFsNFZlRGdpbWEwTDVBc1pQbFdIQlZjcy9yL3dMU2YzWFVYZEs0UHpCQUdIRFBidXYrCnpKOVNqS0NFVll2bHRhMHlUUVBCSFJPa2Y2MG1sVmh6Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K\"\n }\n },\n {\n \"type\": \"key\",\n \"name\": \"server_key\",\n \"contents\": {\n \"content\": \"LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS0FJQkFBS0NBZ0VBc05QcWU5N0pWN3I5UjlqcUlvWlArRlkvaXNvSWQyMWlGNTdzTjVnYWc5ZUVWWFRPClRINVJyV09rT0NaVTh4MU1FQy9QWHp6M3YwT21tTzl4ZkZlVVdPQ0lkeGZ5SmhjalA4Z1F1ekFrUUljeHlYb3QKL00vM0hsTnhmVXNZU2RBMHlFWWJUcmIycEp3RUYzWkFKdGxYaEJyazB0NEczeW9ZS0paeXRzRzNCdGxzZVBtOAo1bk91UzJtWU04VXBUR2EzS2lBQUd4azFpRDhEQnhpOFRBRHdQZ0RlMU1mV21HZ1pkeStoNk1qTmREVVo5ejcyClRqRHhjWnBvL3lDK2JzM1d5L2E3cHRiRWUwVjNOVUU2ZkRhdUc3OWQvWndLMjZkR3ZVY0lrQzFBZjEyWHl4OHIKbXpSRVZTYzFSZVU5dm5VQTNSeE5RV1VYNTlSTlY0M3ZBelh0L0pWUnl2YTcyWXJnK0NGbjMvYUsxNGxoYkZ6NwpMeFp3TUxSTmJHbUNObE12MDB3WDZUTWk2ODNZMWRDdWZ2OGcxTURmL2hoOWQwNW5YTmxLNzEydE11bWNKMjJMCjJXY09rbnp2Q2FmbXBWRTFWczFWQ2RZUEtldC9QWXd3Snlaei92KzhFVHVBTE4zaEVwZXNxM0tIUEk4bjZ1dUgKRHFPcG8vd0w1bDEyajczK000OVBWSW03V0NaU2pXZUNFRFdHL3NMMFFYN2VVOU5jazFWYkNac1pYNG5Hb1lrbgpaR1NTSFpINTdrY01aQnlFVm5ZZXFrVXQ0L3NxUXVzQVoxMTFVOWNWb0lOdElQTkUvQUgxZGhxTWYxK2M5Y29RCjYrSk1Gb0JKeGdXa3luS3VicEVzWTkxRldyb1FKZFUvSFRzYTFvR0VTaFJ2VDU5VVdEZ3VZRXZIWlk4Q0F3RUEKQVFLQ0FnQVBUR1pQRFRsU004VlIvL3hSdkZrUzNUTm1LSkNPOUpHMkJYUGVZM1IzejUrTlhTdTBCb0craEk1aQpwVDVZUWtLZ2ErSi9GT0ZDVlBJRzdVQmVSNTE0Q3dVRGVMamtmci8zOXJFcjRNQmlMTkFyNUR3eVVUUEtGZUlOCnV2K0E4MWg5czBNTmpsck1ad3NibElsOFV2VjFZblpGb0J2c0Z0SThRTGZ3QTlaMzZ6dXRRNzRLR2h3TVBqaUMKMGgzK2xDeG9vcGdmd0JDWGx3d0dBeWZYVTRWMWQ5SFBpdktRQVFHakJDWDM0OWVTcEQxNDNLT21wQ2xmY01LQQp3QzU1bTZsbndCTUFIamlsaVo4RXBuNE8zUlEzSmxsVlpiaXl4RWdrZkE3TG1uNm9Ca3Jwc2VxdDVObThuRVhKCnBFbXhQcUl5Znc1WUNBMEhhNkM5WUhRN1RPRW9BbHBmWld4azAxSnpoVi9aK3FmVHM1YlMwQWNaTzFOVDRaeDgKWlF2eHQ0TDJINVcrK2R6RjhReTlidzQ2M3lKb1dydWxtNy9uQ3YvL1FpNGl0eHRnYyt0N2lwVXZzaUdTVktVWQpPelhCSXNWTUlnd0F6eUtTSEhPL21rMkEwVkgxaHB3emY2L0RzR2wxSjM4TU9pVGo4dEx1RWt3cFY4WGh5MnZwCkd0cXpsT21DS1hodlVDam9iZWlYSWJwSlIzeEM1NmliRjVadk0vQUdONzI5K0xKRFNwbHJtWVJRVHh1UTJWSE8KQWFXQ01SQWFBdUtCVnBxYTRjd25WRy9POEpkN2ZPSi9tMFlIN3FpRlJHREdvdVNOdHZJUUVtaXVkK3dRWjJ6dwpUcmFNVWk0SENtNEFPa0ZNVXBsRmt1ajA2ZHRqM2RIWUtPQkdMK25vaUp4WmJxb3kwUUtDQVFFQTFiZUl6WHh6CnRFRlp2OGRlOXljOWdCUUtNNUNIbHp6NUNMZXVkTitvemxxeDNCMW1PRStxbFkyaEd3RklIWVBJajFLYS83RlkKbExmNFpiUEJRMFhiNUo5VzQzSGIyTnEydXdRQ3ZiSXhVMW9zaGJVWlhZc2FUaE15azc2VzQ5YjU3UC9HdFE3NwpTbkVZTXNrTzRUQndyS3lBdVhDVHRtTk1Qa2J1NFBxT05PeVFQY3o3Yi92VEU1eERjMENMVS9oUXM3NWFHeCs1Citld2VjeEZNa0JKTVo2c2N5TzcySEdSNHZwTHduRXUvcU5uN2JmUElSaUx1T3BwTTdHNlUwQlBPL2todHJ5ZmQKV3U3MHJYZGJSdGRJUHlsQWxSOG9zczJqWWsrRHNPUnNESm9pbkk5WU1Va3dmdHdCNTRQbytGRGtGOHBzV202RQpSaklpenFBK0piWDlTd0tDQVFFQTA4Ly9oM0NabDg2M2xUZHNrU1JKRUZKc0RtdkZkUStzMWtlNUFwMjdnWTBXCmZJbEFGZFlRR3RORUVlTk9xS3EwdTFtS0lqWHFacWNTdU9DNzZIYTE5Tk9waHVoK1dwV0t2Ni9BTWtQSjE5SUIKQ3RqS0lkc2s0U2M3WG02MnNOV1pnQm5XT1Z3QVdzU0VzTHRac1NvWUJUVTJJS1pBOVJOWHhkSEQreGZ2SWJkNApZYngzTzk4WklNQzNlVFFiOW9jVHZab0RNWGdLaHRtTy9iMnlSeEVDSGpGRmxzYlhhc1RPeG5XOWZSVXJtdGVqCk9pdVlXaEZOM2R6dmpuVEdLY0xieWY0MWpHaUVUeFViUHVpei9ZMmk5NldCNVN6MW9zaGorRU1OaFhtRzZSYXUKQUIvelhwNldtSUJ2bDNpU0lzOGJRNkh3Qm1DTjc1R2VVVG1GUUlyaVRRS0NBUUVBbTkzWVN5MXA0VndNRGI5bApObElMRzM4Q0ZhdGlDRjR5cmpYd2FWSzVkWTVWeTFneHRmMzhSa2hkNkNrZUpGQjVsSFhGajVnVEo1dW84TnVSCnB2T3JOT2swNEhxb3dWWjZFSmtUT3JCY0l4TlFCMUFXS05BTHBrZUFDcHJreDFTQlFHVW0wZVFVUjYyRjNYd2YKZXdMdUdqRlJURzJiZlZpY1FZdFFLd3J4YmczZUFRU2ZtSU9MNVBDQmpPdlU4YS9YZzgvZlBZcjlBeFkrK3VMeAorTjB2bGlnSXZVN3lkYkNkRXpodGZVQU5qeU16cVhRemExdU1iWGNkaFEzOVFHaEIvZGhyRG1TL250Tko1YjEzCjk0bUpLbTkycDR0ckRrVEYxU3h5dWk5TjBqOFQ0U1QyU0RPOXg3ZkROOHRQdk5LYUYvUE01SU5YdXk1VGptajIKQ21EWlV3S0NBUUFOUVJYSFh1ZHRsWFR0ZEhOcHZiQ0l3ZStiRTJsZXd1VlkzMUlYZE5GWDhRRTROOHAzMDFaYwpwMTI2Rk5SR1A3QmhqTi9VOWpTOXliU2xOd0xyTUFxQTBJSHFQRUF6NE9tMnh3T3E0WTBPNFVoSmFubHpsdWYrCjR0cVhOU3hmY201UmtzeFIrSXpaSVRVQWJpalZxa0dvaWNUaVZDVDZjUVJzRDQxSStCMXhxYTV4eHo1YTA4SVoKeDVWemt5d3d5QkVYS3owSjZtNFdOQ1Q3Z2RSWEdCeGUwVXgrZStEZEFJWEQ2M2c1RElzVy9HbHRhVzcySytFSQpnaHZIZVUweExjMWRIWGd5V2hQMWN1ZXFqeHM4UVpHeUYzeENZQWJhOGRrM250S0l5S3NGaVBMSWRUZGdjMklQCkZ2SmtzeG5KN2RYUjdKODlkdXRLMDN6cHJrVEZYaXQ5QW9JQkFDcjhkb2ZCcFlFL1JuTlFwbVNET29DRm1sdTkKQlozN3h5K0puZ2FrQ2RSdHFyR1lDdkZMSnI2QnpGdXE0SHpsM0piTkRCM1BkYSs4Z2VNd2cxU1htTEhrRVFrTQpXV2ptNHpmU3hiTUtKamx3REdoeUlwSU9nQ2FQL1hyT2hxTGl4bnJ6UHFHZmM4R0FZTDE2Rm1PeGVqbVk5aERtCmNibkFqZlNwUjF1WEt2S2d6d1NLQ0VWdzc0VjJSRmRqQXBLVDl3bkpOQTZiWHQ5SXFkaS96d3BYbDQ0OVczdVMKNjRjVVpaK3luYnQ5QUlxbFNjMDdNRHl1TUtueExMbDFLeEJYenNxZlVsYWtlRGVoVmdGS05OOTNXQWJJc09ieAp1d1hTd0hXa1B6RGFHeE9wdzlSMHo2S2t2N25YZnBIYW1RWENBZEdsRjkyc1QwYW80Y3FuejFJSmJ2bz0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K\"\n }\n },\n {\n \"type\": \"certificate\",\n \"name\": \"cacert\",\n \"contents\": {\n \"content\": \"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZjekNDQTF1Z0F3SUJBZ0lVVnNRZkFaTW4vazNRTU9nc0RuVEdUaU9PbGJ3d0RRWUpLb1pJaHZjTkFRRUwKQlFBd1NURUxNQWtHQTFVRUJoTUNTVlF4RFRBTEJnTlZCQWdNQkVGemRHa3hFVEFQQmdOVkJBb01DRlJsYzNRZwpUR0ZpTVJnd0ZnWURWUVFEREE5MmJTMWliR0Z1YXk1bVppNXNZVzR3SGhjTk1qTXdOakE0TURrMU5EQTRXaGNOCk1qUXdOakEzTURrMU5EQTRXakJKTVFzd0NRWURWUVFHRXdKSlZERU5NQXNHQTFVRUNBd0VRWE4wYVRFUk1BOEcKQTFVRUNnd0lWR1Z6ZENCTVlXSXhHREFXQmdOVkJBTU1EM1p0TFdKc1lXNXJMbVptTG14aGJqQ0NBaUl3RFFZSgpLb1pJaHZjTkFRRUJCUUFEZ2dJUEFEQ0NBZ29DZ2dJQkFLMlQwWXpkcjB0dWQzaVJRNGNzaGNhRVJTRzVjTDE2CkhRblhoYWw4emlUL1VRQUNIUGdzZDYwcWlEaldvQTJXb0lGWFFpUHkzOG1vZGtWRlR4Qmt5U2VldndOOFJiLzEKOFhaMS8yS1RnVmRDcHkvNm11WE15bXZYODJad05CVkV3QnoxUk5kbklUSk44cVh3a0d4bHozbDBib1loRkFyUQpNdmkxcW1RaHpDa2Zpb041MVkrYlBXOXpTQlFQdXNrcXJYYzRqTTJ0VENNQ2pTcFlvd1hXM1ppRmc5WEJ1Z09aCjFmdWd1Zmw4K1FJYzNZSEFoL1Z1NloraXFEOGxQeGRKODlBeDZaazVtOGdkVG9JdUhBbUNWaHFpUXBGRjkzSTgKbkYrSnRuYnBaNTRJUTZBbWYrYiswakMxdmY4Kzg0WUppaEVzWExyaGMxZTRTZ2dwdzEvcWpDb21QblhGVjEzUwpsUG5kVlhVR0taa1ZKdXdZTjJyZElmd3YrdCs5MGhwUVBmNmFBTjRCamRxOXdkdkQzSXVnS2JYZG5CQ0FUTEY4ClYyRTFTSE9VZGdRY3duK1d1WDVVOGdPa3B2b2VFN0g1REJ6Rks1WTZ2SHZlaTRlNkp3RTRDK3FJL1BmbTgreTEKNEpsOFBSOW5JQmdGQ3hrZWpwa2tRQ0I5U0dvMVZidzZhWmdZd0VQNHh6YXFYYXV3L3F4c0oxNUkrRTBndEs1OApuWUtkM0hqelk5Slh6V0NVNTdXbmc2SzNvTTIzNXpyRzJnNm1FaHQ4SStDckVMUFNuZURjZU8zVlJkc2dlblBCCis4U1JxVU8vWG9LWHNEU3I5amoxdWluVzYwTG5MZ0Zmc3JQeGlQVlZlMFh1TFZESlhCSlNoRDZDeGRyMnBSOGQKS25SRDZrTFpZZEtMQWdNQkFBR2pVekJSTUIwR0ExVWREZ1FXQkJRUzZnS2pMK3VFc0dXeXVjbTZyb0xVaXpqVwpWREFmQmdOVkhTTUVHREFXZ0JRUzZnS2pMK3VFc0dXeXVjbTZyb0xVaXpqV1ZEQVBCZ05WSFJNQkFmOEVCVEFECkFRSC9NQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUNBUUJad3B4Z2Z4N2thZFhvRHNyT1hUVXJ6dEFPMkFQRVJNaTAKaTkyNk9DTGFPbVVYZW1uKytXSUU1K2tUSE0wcS8vbUZCTURzSmdZSFVLUlNvRGNsNmh4TnVFNUNzS2trRVFTSgpMTHZrWlB0S2J5NGlxMitLZ1JtdVZxbXJNVTBYQzZMZDl3WmttL2huUjNtT3V6bko4MGZmV1JDQ0xGWDEwY2EzCnc5TGM1d1JLTFBZZXQvcEs5SitOYWN3TFJRYTczVFovMUpQNW9BU3czVjNoYkxlLy9UeWpnOURqUlZGY3FYWnEKWWs2Mm5qSkhZVzh3WmlhZzc0QXU4dHE5OG5KandBV1ROMFV5L2w1Q2VpWnV5bzZlU0RHVDNJNm1BdGU1VXBvWAppNXBkYlZ6VDdOZC9IOEwwZHZNdVZ2N0FmakZlcU91cUZNNkkzTnlvbStLWENxNmJQdGxBWEkzeVFZc0t4ZlRkCkw3SnRaTmx6MGJ6eHJhcHI4RmpYcjhML1ZkeHQza00xMnJwb2kzL3hsckR6Q2Q2b2YrQ1MxelBocUdpOUhvcUoKZEU5VGhYMklTdkd2akVSYzVVNFRsNjJBNHNyeGJQbUt0eWx3dGNGVEJacUJiRGY3ZjBBc2cveWhndXdTcktsQQpBNkRWVXVCRFErdGpwZ0N0b0ZlOEhLVDJ6UFVlaEQ2ZjVNQkhmU2ZUZ1crTlhFSXNvVDNsampjY1hsYXhPcFJWCkNQNWxCczNmekxyYnBxbUlLaWZhdWlTNWM4TzlSUjhjQTVzeWlBOTBmbmJIdDlmdGxpRG9jcFRzNUtrbjk2NkIKZUxMM1dXVldCYUtvanJzY1RkVXJoalNnVVBmam5FTXpnVzR2eEc3d3BVNHR2ME4yaEtHUWc0bVhhcDV0SU5Pcwp4WktnZXRHUldnPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=\"\n }\n }\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Online boutique HTTPS\",\n \"names\": [\n \"vm-blank.ff.lan\"\n ],\n \"resolver\": \"192.168.1.13\",\n \"listen\": {\n \"address\": \"0.0.0.0:443\",\n \"http2\": true,\n \"tls\": {\n \"certificate\": \"server_cert\",\n \"key\": \"server_key\",\n \"trusted_ca_certificates\": \"cacert\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ],\n \"mtls\": {\n \"enabled\": \"on\",\n \"client_certificates\": \"cacert\"\n },\n \"ocsp\": {\n \"enabled\": \"on\",\n \"responder\": \"http://ocsp.k8s.ie.ff.lan\"\n },\n \"stapling\": {\n \"enabled\": true,\n \"verify\": true,\n \"responder\": \"http://ocsp.k8s.ie.ff.lan\"\n }\n }\n },\n \"log\": {\n \"access\": \"/var/log/nginx/vm-blank.ff.lan_access_log\",\n \"error\": \"/var/log/nginx/vm-blank.ff.lan_error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://origin_server\"\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"origin_server\",\n \"origin\": [\n {\n \"server\": \"192.168.1.200:80\"\n }\n ]\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "NGINX App Protect WAF", + "item": [ + { + "name": "Create initial NGINX configuration with NGINX App Protect WAF", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ],\n \"certificates\": [\n {\n \"type\": \"certificate\",\n \"name\": \"test_cert\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v4.1/www.online-boutique.local.crt\"\n }\n },\n {\n \"type\": \"key\",\n \"name\": \"test_key\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v4.1/www.online-boutique.local.key\"\n }\n }\n ],\n \"policies\": [\n {\n \"type\": \"app_protect\",\n \"name\": \"production-policy\",\n \"active_tag\": \"xss-blocked\",\n \"versions\": [\n {\n \"tag\": \"xss-blocked\",\n \"displayName\": \"Production Policy - XSS blocked\",\n \"description\": \"This is a production-ready policy - XSS blocked\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v4.1/nap-policy-xss-blocked.json\"\n }\n },\n {\n \"tag\": \"xss-allowed\",\n \"displayName\": \"Production Policy - XSS allowed\",\n \"description\": \"This is a production-ready policy - XSS allowed\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v4.1/nap-policy-xss-allowed.json\"\n }\n }\n ]\n }\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Online boutique HTTPS\",\n \"names\": [\n \"www.online-boutique.lan\"\n ],\n \"listen\": {\n \"address\": \"0.0.0.0:443\",\n \"http2\": true,\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"log\": {\n \"access\": \"/var/log/nginx/online_boutique_https_access_log\",\n \"error\": \"/var/log/nginx/online_boutique_https_error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://upstream_boutique\"\n }\n ],\n \"app_protect\": {\n \"enabled\": true,\n \"policy\": \"production-policy\",\n \"log\": {\n \"profile_name\": \"secops_dashboard\",\n \"enabled\": true,\n \"destination\": \"127.0.0.1:514\"\n }\n }\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"upstream_boutique\",\n \"origin\": [\n {\n \"server\": \"192.168.1.200:80\"\n }\n ]\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config" + ] + } + }, + "response": [] + }, + { + "name": "Change active NGINX App Protect policy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ],\n \"policies\": [\n {\n \"type\": \"app_protect\",\n \"name\": \"production-policy\",\n \"active_tag\": \"xss-allowed\",\n \"versions\": [\n {\n \"tag\": \"xss-blocked\",\n \"displayName\": \"Production Policy - XSS blocked\",\n \"description\": \"Production-ready policy - XSS blocked\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v4.1/nap-policy-xss-blocked.json\"\n }\n },\n {\n \"tag\": \"xss-allowed\",\n \"displayName\": \"Production Policy - XSS allowed\",\n \"description\": \"Production-ready policy - XSS allowed\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v4.1/nap-policy-xss-allowed.json\"\n }\n }\n ]\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}" + ] + } + }, + "response": [] + }, + { + "name": "Update TLS certificates", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ],\n \"certificates\": [\n {\n \"type\": \"certificate\",\n \"name\": \"test_cert\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v4.1/www2.online-boutique.local.crt\"\n }\n },\n {\n \"type\": \"key\",\n \"name\": \"test_key\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v4.1/www2.online-boutique.local.key\"\n }\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}" + ] + } + }, + "response": [] + }, + { + "name": "Disable NGINX App Protect WAF", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Online boutique HTTPS\",\n \"names\": [\n \"www.online-boutique.lan\"\n ],\n \"listen\": {\n \"address\": \"0.0.0.0:443\",\n \"http2\": true,\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"log\": {\n \"access\": \"/var/log/nginx/online_boutique_https_access_log\",\n \"error\": \"/var/log/nginx/online_boutique_https_error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://upstream_boutique\"\n }\n ]\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}" + ] + } + }, + "response": [] + }, + { + "name": "Get declaration status", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}/status", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}", + "status" + ] + } + }, + "response": [] + }, + { + "name": "Get declaration", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete declaration", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config", + "{{configUid}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Server-side and source of truth authentication", + "item": [ + { + "name": "Server-side and source of truth authentication", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var respData = JSON.parse(responseBody);", + "", + "tests[\"configUid is: \" +respData.configUid] = respData.configUid;", + "", + "pm.collectionVariables.set('configUid',respData.configUid);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ],\n \"certificates\": [\n {\n \"type\": \"certificate\",\n \"name\": \"test_cert\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v2/www.online-boutique.local.crt\",\n \"authentication\": [\n {\n \"profile\": \"Bearer token-based authentication profile\"\n }\n ]\n }\n },\n {\n \"type\": \"key\",\n \"name\": \"test_key\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v2/www.online-boutique.local.key\",\n \"authentication\": [\n {\n \"profile\": \"Bearer token-based authentication profile\"\n }\n ]\n }\n }\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Online boutique HTTPS\",\n \"names\": [\n \"www.online-boutique.lan\"\n ],\n \"listen\": {\n \"address\": \"0.0.0.0:443\",\n \"http2\": true,\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"log\": {\n \"access\": \"/var/log/nginx/online_boutique_https_access_log\",\n \"error\": \"/var/log/nginx/online_boutique_https_error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://upstream_boutique\",\n \"authentication\": {\n \"server\": [\n {\n \"profile\": \"Header-based authentication profile\"\n }\n ]\n }\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"upstream_boutique\",\n \"origin\": [\n {\n \"server\": \"192.168.2.200:80\"\n }\n ]\n }\n ],\n \"authentication\": {\n \"server\": [\n {\n \"name\": \"Bearer token-based authentication profile\",\n \"type\": \"token\",\n \"token\": {\n \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEiLCJpc3MiOiJCYXNoIEpXVCBHZW5lcmF0b3IiLCJpYXQiOjE3MDI0ODEzNjcsImV4cCI6MTcwMjQ4MTM2OH0.eyJuYW1lIjoiQm9iIERldk9wcyIsInN1YiI6IkpXVCBzdWIgY2xhaW0iLCJpc3MiOiJKV1QgaXNzIGNsYWltIiwicm9sZXMiOlsiZGV2b3BzIl19.SKA_7MszAypMEtX5NDQ0TcUbVYx_Wt0hrtmuyTmrVKU\",\n \"type\": \"bearer\"\n }\n },\n {\n \"name\": \"Header-based authentication profile\",\n \"type\": \"token\",\n \"token\": {\n \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEiLCJpc3MiOiJCYXNoIEpXVCBHZW5lcmF0b3IiLCJpYXQiOjE3MDI0ODEzNjcsImV4cCI6MTcwMjQ4MTM2OH0.eyJuYW1lIjoiQm9iIERldk9wcyIsInN1YiI6IkpXVCBzdWIgY2xhaW0iLCJpc3MiOiJKV1QgaXNzIGNsYWltIiwicm9sZXMiOlsiZGV2b3BzIl19.SKA_7MszAypMEtX5NDQ0TcUbVYx_Wt0hrtmuyTmrVKU\",\n \"type\": \"header\",\n \"location\": \"X-AUTH-TOKEN\"\n }\n }\n ]\n }\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config", + "protocol": "http", + "host": [ + "{{ncg_host}}" + ], + "port": "{{ncg_port}}", + "path": [ + "{{ngc_api_version}}", + "config" + ] + } + }, + "response": [] + } + ] + } + ] + } + ] } ], "event": [ @@ -3880,7 +5863,7 @@ }, { "key": "ngc_api_version", - "value": "v4.0", + "value": "v4.1", "type": "string" } ] diff --git a/contrib/postman/README.md b/contrib/postman/README.md index 867fcbb..7eaa52d 100644 --- a/contrib/postman/README.md +++ b/contrib/postman/README.md @@ -2,7 +2,7 @@ This collection contains: -API v4.0 - Latest +API v4.1 - Latest - `Configuration generation` - Declaration examples with output to plaintext, JSON, Kubernetes ConfigMap, HTTP POST - `Declarative automation examples` - Several examples and use cases - `API Gateway` - Sample API gateway requests for Swagger and OpenAPI schemas import @@ -11,7 +11,19 @@ API v4.0 - Latest - `Housekeeping - common endpoints` - Miscellaneous general purpose requests - `JWT Client Authentication` - JWT-based client authentication for HTTP - `mTLS Client Authentication` - mTLS client authentication for HTTP - - `NGINX App Protect WAF` - Sample requests for declarative configuration lifecycle management + - `NGINX App Protect WAF` - Sample requests for declarative configuration lifecycle management + - `Server-side and source of truth authentication` - Requests for authentication towards upstreams and source of truth + +API v4.0 +- `Configuration generation` - Declaration examples with output to plaintext, JSON, Kubernetes ConfigMap, HTTP POST +- `Declarative automation examples` - Several examples and use cases + - `API Gateway` - Sample API gateway requests for Swagger and OpenAPI schemas import + - `CRUD automation` - Sample requests for CRUD-based automation + - `GitOps autosync` - GitOps automation demo + - `Housekeeping - common endpoints` - Miscellaneous general purpose requests + - `JWT Client Authentication` - JWT-based client authentication for HTTP + - `mTLS Client Authentication` - mTLS client authentication for HTTP + - `NGINX App Protect WAF` - Sample requests for declarative configuration lifecycle management API v3.1 - Deprecated - `Configuration generation` - Declaration examples with output to plaintext, JSON, Kubernetes ConfigMap, HTTP POST diff --git a/etc/config.toml b/etc/config.toml index 03b8318..71e0ef1 100644 --- a/etc/config.toml +++ b/etc/config.toml @@ -3,7 +3,7 @@ # Main variables [main] banner = "NGINX Declarative API" -version = "4.0" +version = "4.1" url = "https://github.com/f5devcentral/NGINX-Declarative-API" # Templates diff --git a/src/V4_1_CreateConfig.py b/src/V4_1_CreateConfig.py new file mode 100644 index 0000000..eb4856f --- /dev/null +++ b/src/V4_1_CreateConfig.py @@ -0,0 +1,792 @@ +""" +Configuration creation based on jinja2 templates +""" + +import base64 +import json +import pickle +import time +import uuid +from datetime import datetime +from urllib.parse import urlparse + +import requests +import schedule +from fastapi.responses import Response, JSONResponse +from jinja2 import Environment, FileSystemLoader +from pydantic import ValidationError +from requests.packages.urllib3.exceptions import InsecureRequestWarning + +import v4_1.APIGateway +import v4_1.DevPortal +import v4_1.DeclarationPatcher +import v4_1.GitOps +import v4_1.MiscUtils + +# NGINX App Protect helper functions +import v4_1.NAPUtils +import v4_1.NIMUtils + +# NGINX Declarative API modules +from NcgConfig import NcgConfig +from NcgRedis import NcgRedis + +# pydantic models +from V4_1_NginxConfigDeclaration import * + +# Tolerates self-signed TLS certificates +requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + + +def getuniqueid(): + return uuid.uuid4() + + +def configautosync(configUid): + print("Autosyncing configuid [" + configUid + "]") + + declaration = '' + declFromRedis = NcgRedis.redis.get(f'ncg.declaration.{configUid}') + + if declFromRedis is not None: + declaration = pickle.loads(declFromRedis) + apiversion = NcgRedis.redis.get(f'ncg.apiversion.{configUid}').decode() + + createconfig(declaration=declaration, apiversion=apiversion, runfromautosync=True, configUid=configUid) + + +# Create the given declarative configuration +# Return a JSON string: +# { "status_code": nnn, "headers": {}, "message": {} } +def createconfig(declaration: ConfigDeclaration, apiversion: str, runfromautosync: bool = False, configUid: str = ""): + # Building NGINX configuration for the given declaration + + # NGINX configuration files for staged config + configFiles = {'files': [], 'rootDir': NcgConfig.config['nms']['config_dir']} + + # NGINX auxiliary files for staged config + auxFiles = {'files': [], 'rootDir': NcgConfig.config['nms']['config_dir']} + + try: + # Pydantic JSON validation + ConfigDeclaration(**declaration.model_dump()) + except ValidationError as e: + print(f'Invalid declaration {e}') + + d = declaration.model_dump() + decltype = d['output']['type'] + + j2_env = Environment(loader=FileSystemLoader(NcgConfig.config['templates']['root_dir'] + '/' + apiversion), + trim_blocks=True, extensions=["jinja2_base64_filters.Base64Filters"]) + j2_env.filters['regex_replace'] = v4_1.MiscUtils.regex_replace + + if 'http' in d['declaration']: + if 'snippet' in d['declaration']['http']: + status, snippet = v4_1.GitOps.getObjectFromRepo(object = d['declaration']['http']['snippet'], authProfiles = d['declaration']['http']['authentication']) + + if status != 200: + return {"status_code": 422, "message": {"status_code": status, "message": snippet}} + + d['declaration']['http']['snippet'] = snippet + + # Check HTTP upstreams validity + all_upstreams = [] + http = d['declaration']['http'] + + if 'upstreams' in http: + for i in range(len(http['upstreams'])): + + upstream = http['upstreams'][i] + + if upstream['snippet']: + status, snippet = v4_1.GitOps.getObjectFromRepo(object = upstream['snippet'], authProfiles = d['declaration']['http']['authentication']) + + if status != 200: + return {"status_code": 422, "message": {"status_code": status, "message": snippet}} + + d['declaration']['http']['upstreams'][i]['snippet'] = snippet + + all_upstreams.append(http['upstreams'][i]['name']) + + # Check HTTP rate_limit profiles validity + all_ratelimits = [] + http = d['declaration']['http'] + + d_rate_limit = v4_1.MiscUtils.getDictKey(d, 'declaration.http.rate_limit') + if d_rate_limit is not None: + for i in range(len(d_rate_limit)): + all_ratelimits.append(d_rate_limit[i]['name']) + + # Check authentication profiles validity and creates authentication config files + + # List of all auth client & server profile names + all_auth_client_profiles = [] + all_auth_server_profiles = [] + + d_auth_profiles = v4_1.MiscUtils.getDictKey(d, 'declaration.http.authentication') + if d_auth_profiles is not None: + if 'client' in d_auth_profiles: + # Render all client authentication profiles + + auth_client_profiles = d_auth_profiles['client'] + for i in range(len(auth_client_profiles)): + auth_profile = auth_client_profiles[i] + + match auth_profile['type']: + case 'jwt': + # Add the rendered authentication configuration snippet as a config file in the staged configuration - jwt template + templateName = NcgConfig.config['templates']['auth_client_root']+"/jwt.tmpl" + renderedClientAuthProfile = j2_env.get_template(templateName).render( + authprofile=auth_profile, ncgconfig=NcgConfig.config) + + b64renderedClientAuthProfile = base64.b64encode(bytes(renderedClientAuthProfile, 'utf-8')).decode('utf-8') + configFileName = NcgConfig.config['nms']['auth_client_dir'] + '/'+auth_profile['name'].replace(' ','_')+".conf" + authProfileConfigFile = {'contents': b64renderedClientAuthProfile, + 'name': configFileName } + + all_auth_client_profiles.append(auth_profile['name']) + auxFiles['files'].append(authProfileConfigFile) + + # Add the rendered authentication configuration snippet as a config file in the staged configuration - jwks template + templateName = NcgConfig.config['templates']['auth_client_root']+"/jwks.tmpl" + renderedClientAuthProfile = j2_env.get_template(templateName).render( + authprofile=auth_profile, ncgconfig=NcgConfig.config) + + b64renderedClientAuthProfile = base64.b64encode(bytes(renderedClientAuthProfile, 'utf-8')).decode('utf-8') + configFileName = NcgConfig.config['nms']['auth_client_dir'] + '/jwks_'+auth_profile['name'].replace(' ','_')+".conf" + authProfileConfigFile = {'contents': b64renderedClientAuthProfile, + 'name': configFileName } + + all_auth_client_profiles.append(auth_profile['name']) + auxFiles['files'].append(authProfileConfigFile) + + + if 'server' in d_auth_profiles: + # Render all server authentication profiles + + auth_server_profiles = d_auth_profiles['server'] + for i in range(len(auth_server_profiles)): + auth_profile = auth_server_profiles[i] + + match auth_profile['type']: + case 'token': + # Add the rendered authentication configuration snippet as a config file in the staged configuration - jwt template + templateName = NcgConfig.config['templates']['auth_server_root']+"/token.tmpl" + renderedServerAuthProfile = j2_env.get_template(templateName).render( + authprofile=auth_profile, ncgconfig=NcgConfig.config) + + b64renderedServerAuthProfile = base64.b64encode(bytes(renderedServerAuthProfile, 'utf-8')).decode('utf-8') + configFileName = NcgConfig.config['nms']['auth_server_dir'] + '/'+auth_profile['name'].replace(' ','_')+".conf" + authProfileConfigFile = {'contents': b64renderedServerAuthProfile, + 'name': configFileName } + + all_auth_server_profiles.append(auth_profile['name']) + auxFiles['files'].append(authProfileConfigFile) + + # Parse HTTP servers + d_servers = v4_1.MiscUtils.getDictKey(d, 'declaration.http.servers') + if d_servers is not None: + apiGatewaySnippet = '' + + for server in d_servers: + serverSnippet = '' + + if server['snippet']: + status, serverSnippet = v4_1.GitOps.getObjectFromRepo(object = server['snippet'], authProfiles = d['declaration']['http']['authentication'], base64Encode = False) + + if status != 200: + return {"status_code": 422, "message": {"status_code": status, "message": serverSnippet}} + + serverSnippet = serverSnippet['content'] + + for loc in server['locations']: + if loc['snippet']: + status, snippet = v4_1.GitOps.getObjectFromRepo(object = loc['snippet'], authProfiles = d['declaration']['http']['authentication']) + + if status != 200: + return {"status_code": 422, "message": {"status_code": status, "message": snippet}} + + loc['snippet'] = snippet + + # Location upstream name validity check + if 'upstream' in loc and loc['upstream'] and urlparse(loc['upstream']).netloc not in all_upstreams: + return {"status_code": 422, + "message": {"status_code": status, "message": + {"code": status, "content": f"invalid HTTP upstream [{loc['upstream']}]"}}} + + # Location client authentication name validity check + if 'authentication' in loc and loc['authentication']: + locAuthClientProfiles = loc['authentication']['client'] + + for authClientProfile in locAuthClientProfiles: + if authClientProfile['profile'] not in all_auth_client_profiles: + return {"status_code": 422, + "message": {"status_code": status, "message": + {"code": status, "content": f"invalid client authentication profile [{authClientProfile['profile']}] in location [{loc['uri']}]"}}} + + # Location server authentication name validity check + if 'authentication' in loc and loc['authentication']: + locAuthServerProfiles = loc['authentication']['server'] + + for authServerProfile in locAuthServerProfiles: + if authServerProfile['profile'] not in all_auth_server_profiles: + return {"status_code": 422, + "message": {"status_code": status, "message": + {"code": status, "content": f"invalid server authentication profile [{authServerProfile['profile']}] in location [{loc['uri']}]"}}} + + # API Gateway provisioning + if loc['apigateway'] and loc['apigateway']['api_gateway'] and loc['apigateway']['api_gateway']['enabled'] and loc['apigateway']['api_gateway']['enabled'] == True: + openApiAuthProfile = loc['apigateway']['openapi_schema']['authentication'] + if openApiAuthProfile and openApiAuthProfile[0]['profile'] not in all_auth_server_profiles: + return {"status_code": 422, + "message": {"status_code": status, "message": + {"code": status, + "content": f"invalid server authentication profile [{openApiAuthProfile[0]['profile']}] for OpenAPI schema [{loc['apigateway']['openapi_schema']['content']}]"}}} + + status, apiGatewayConfigDeclaration = v4_1.APIGateway.createAPIGateway(locationDeclaration = loc, authProfiles = d['declaration']['http']['authentication']) + else: + apiGatewayConfigDeclaration = '' + + # API Gateway Developer portal provisioning + if loc['apigateway'] and loc['apigateway']['developer_portal'] and 'enabled' in loc['apigateway']['developer_portal'] and loc['apigateway']['developer_portal']['enabled'] == True: + + status, devPortalHTML = v4_1.DevPortal.createDevPortal(locationDeclaration = loc, authProfiles = d['declaration']['http']['authentication']) + + if status != 200: + return {"status_code": 400, + "message": {"status_code": status, "message": + {"code": status, "content": f"Developer Portal creation failed for {loc['apigateway']['openapi_schema']['content']}"}}} + + ### Add optional API Developer portal HTML files + # devPortalHTML + newAuxFile = {'contents': devPortalHTML, 'name': NcgConfig.config['nms']['devportal_dir'] + + loc['apigateway']['developer_portal']['uri']} + auxFiles['files'].append(newAuxFile) + + ### / Add optional API Developer portal HTML files + + if loc['rate_limit'] is not None: + if 'profile' in loc['rate_limit'] and loc['rate_limit']['profile'] and loc['rate_limit'][ + 'profile'] not in all_ratelimits: + return {"status_code": 422, + "message": { + "status_code": status, + "message": + {"code": status, + "content": + f"invalid rate_limit profile [{loc['rate_limit']['profile']}]"}}} + + # API Gateway configuration template rendering + apiGatewaySnippet += j2_env.get_template(NcgConfig.config['templates']['apigwconf']).render( + declaration=apiGatewayConfigDeclaration, ncgconfig=NcgConfig.config)\ + if apiGatewayConfigDeclaration else '' + + server['snippet']['content'] = base64.b64encode(bytes(serverSnippet + apiGatewaySnippet, 'utf-8')).decode('utf-8') + + if 'layer4' in d['declaration']: + # Check Layer4/stream upstreams validity + all_upstreams = [] + + d_upstreams = v4_1.MiscUtils.getDictKey(d, 'declaration.layer4.upstreams') + if d_upstreams is not None: + for i in range(len(d_upstreams)): + all_upstreams.append(d_upstreams[i]['name']) + + d_servers = v4_1.MiscUtils.getDictKey(d, 'declaration.layer4.servers') + if d_servers is not None: + for server in d_servers: + + if server['snippet']: + status, snippet = v4_1.GitOps.getObjectFromRepo(object = server['snippet'], authProfiles = d['declaration']['http']['authentication']) + + if status != 200: + return {"status_code": 422, "message": {"status_code": status, "message": snippet}} + + server['snippet'] = snippet + + if 'upstream' in server and server['upstream'] and server['upstream'] not in all_upstreams: + return {"status_code": 422, + "message": { + "status_code": status, + "message": + {"code": status, "content": f"invalid Layer4 upstream {server['upstream']}"}}} + + # HTTP configuration template rendering + httpConf = j2_env.get_template(NcgConfig.config['templates']['httpconf']).render( + declaration=d['declaration']['http'], ncgconfig=NcgConfig.config) if 'http' in d['declaration'] else '' + + # Stream configuration template rendering + streamConf = j2_env.get_template(NcgConfig.config['templates']['streamconf']).render( + declaration=d['declaration']['layer4'], ncgconfig=NcgConfig.config) if 'layer4' in d['declaration'] else '' + + b64HttpConf = str(base64.b64encode(httpConf.encode("utf-8")), "utf-8") + b64StreamConf = str(base64.b64encode(streamConf.encode("utf-8")), "utf-8") + + if decltype.lower() == "plaintext": + # Plaintext output + return httpConf + streamConf + + elif decltype.lower() == "json" or decltype.lower() == 'http': + # JSON-wrapped b64-encoded output + payload = {"http_config": f"{b64HttpConf}", "stream_config": f"{b64StreamConf}"} + + if decltype.lower() == "json": + # JSON output + return {"status_code": 200, "message": {"status_code": 200, "message": payload}} + else: + # HTTP POST output + try: + r = requests.post(d['output']['http']['url'], data=json.dumps(payload), + headers={'Content-Type': 'application/json'}) + except: + headers = {'Content-Type': 'application/json'} + content = {'message': d['output']['http']['url'] + ' unreachable'} + + return {"status_code": 502, "message": {"status_code": 502, "message": content}, "headers": headers} + + r.headers.pop("Content-Length") if "Content-Length" in r.headers else '' + r.headers.pop("Server") if "Server" in r.headers else '' + r.headers.pop("Date") if "Date" in r.headers else '' + r.headers.pop("Content-Type") if "Content-Type" in r.headers else '' + + r.headers['Content-Type'] = 'application/json' + + return {"status_code": r.status_code, "message": {"code": r.status_code, "content": r.text}, + "headers": r.headers} + + elif decltype.lower() == 'configmap': + # Kubernetes ConfigMap output + cmHttp = j2_env.get_template(NcgConfig.config['templates']['configmap']).render(nginxconfig=httpConf, + name=d['output']['configmap'][ + 'name'] + '.http', + filename= + d['output']['configmap'][ + 'filename'] + '.http', + namespace= + d['output']['configmap'][ + 'namespace']) + cmStream = j2_env.get_template(NcgConfig.config['templates']['configmap']).render(nginxconfig=streamConf, + name=d['output']['configmap'][ + 'name'] + '.stream', + filename= + d['output']['configmap'][ + 'filename'] + '.stream', + namespace= + d['output']['configmap'][ + 'namespace']) + + return Response(content=cmHttp + '\n---\n' + cmStream, headers={'Content-Type': 'application/x-yaml'}) + + elif decltype.lower() == 'nms': + # NGINX Instance Manager Staged Configuration publish + + nmsUsername = v4_1.MiscUtils.getDictKey(d, 'output.nms.username') + nmsPassword = v4_1.MiscUtils.getDictKey(d, 'output.nms.password') + nmsInstanceGroup = v4_1.MiscUtils.getDictKey(d, 'output.nms.instancegroup') + nmsSynctime = v4_1.MiscUtils.getDictKey(d, 'output.nms.synctime') + + nmsUrlFromJson = v4_1.MiscUtils.getDictKey(d, 'output.nms.url') + urlCheck = urlparse(nmsUrlFromJson) + + if urlCheck.scheme not in ['http', 'https'] or urlCheck.scheme == "" or urlCheck.netloc == "": + return {"status_code": 400, + "message": {"status_code": 400, "message": {"code": 400, + "content": f"invalid NGINX Management Suite URL {nmsUrlFromJson}"}}, + "headers": {'Content-Type': 'application/json'}} + + nmsUrl = f"{urlCheck.scheme}://{urlCheck.netloc}" + + if nmsSynctime < 0: + return {"status_code": 400, + "message": {"status_code": 400, "message": {"code": 400, "content": "synctime must be >= 0"}}, + "headers": {'Content-Type': 'application/json'}} + + # Fetch NGINX App Protect WAF policies from source of truth if needed + d_policies = v4_1.MiscUtils.getDictKey(d, 'output.nms.policies') + if d_policies is not None: + for policy in d_policies: + if 'versions' in policy: + for policyVersion in policy['versions']: + status, content = v4_1.GitOps.getObjectFromRepo(object = policyVersion['contents'], authProfiles = d['declaration']['http']['authentication']) + + if status != 200: + return {"status_code": 422, "message": {"status_code": status, "message": content}} + + policyVersion['contents'] = content + + # Check TLS items validity + all_tls = {'certificate': {}, 'key': {}} + + d_certs = v4_1.MiscUtils.getDictKey(d, 'output.nms.certificates') + if d_certs is not None: + for i in range(len(d_certs)): + if d_certs[i]['name']: + all_tls[d_certs[i]['type']][d_certs[i]['name']] = True + + d_servers = v4_1.MiscUtils.getDictKey(d, 'declaration.http.servers') + if d_servers is not None: + for server in d_servers: + if server['listen'] is not None: + if 'tls' in server['listen']: + cert_name = v4_1.MiscUtils.getDictKey(server, 'listen.tls.certificate') + if cert_name and cert_name not in all_tls['certificate']: + return {"status_code": 422, + "message": { + "status_code": 422, + "message": {"code": 422, + "content": "invalid TLS certificate " + + cert_name + " for server" + str( + server['names'])} + }} + + cert_key = v4_1.MiscUtils.getDictKey(server, 'listen.tls.key') + if cert_key and cert_key not in all_tls['key']: + return {"status_code": 422, + "message": { + "status_code": 422, + "message": {"code": 422, + "content": "invalid TLS key " + cert_key + " for server" + str( + server['names'])} + }} + + trusted_cert_name = v4_1.MiscUtils.getDictKey(server, 'listen.tls.trusted_ca_certificates') + if trusted_cert_name and trusted_cert_name not in all_tls['certificate']: + return {"status_code": 422, + "message": { + "status_code": 422, + "message": {"code": 422, + "content": "invalid trusted CA certificate " + + trusted_cert_name + " for server" + str(server['names'])} + }} + + if v4_1.MiscUtils.getDictKey(server, 'listen.tls.mtls.enabled') in ['optional_no_ca'] \ + and 'ocsp' in server['listen']['tls']: + return {"status_code": 422, + "message": { + "status_code": 422, + "message": {"code": 422, + "content": "OCSP is incompatible with 'optional_no_ca' client " + "mTLS verification for server" + str( + server['names'])} + }} + + client_cert_name = v4_1.MiscUtils.getDictKey(server, 'listen.tls.mtls.client_certificates') + if client_cert_name and client_cert_name not in all_tls['certificate']: + return {"status_code": 422, + "message": { + "status_code": 422, + "message": {"code": 422, + "content": f"invalid mTLS client certificates [{client_cert_name}] for server {str(server['names'])}"} + }} + + # Add optional certificates specified under output.nms.certificates + extensions_map = {'certificate': '.crt', 'key': '.key'} + + d_certificates = v4_1.MiscUtils.getDictKey(d, 'output.nms.certificates') + if d_certificates is not None: + for c in d_certificates: + status, certContent = v4_1.GitOps.getObjectFromRepo(object = c['contents'], authProfiles = d['declaration']['http']['authentication']) + + if status != 200: + return {"status_code": 422, + "message": {"status_code": status, "message": {"code": status, "content": certContent}}} + + newAuxFile = {'contents': certContent['content'], 'name': NcgConfig.config['nms']['certs_dir'] + + '/' + c['name'] + extensions_map[c['type']]} + auxFiles['files'].append(newAuxFile) + + ### / Add optional certificates specified under output.nms.certificates + + # NGINX main configuration file through template + j2_env = Environment(loader=FileSystemLoader(NcgConfig.config['templates']['root_dir'] + '/' + apiversion), + trim_blocks=True, extensions=["jinja2_base64_filters.Base64Filters"]) + + nginxMainConf = j2_env.get_template(NcgConfig.config['templates']['nginxmain']).render( + nginxconf={'modules': v4_1.MiscUtils.getDictKey(d, 'output.nms.modules')}) + + # Base64-encoded NGINX main configuration (/etc/nginx/nginx.conf) + b64NginxMain = str(base64.urlsafe_b64encode(nginxMainConf.encode("utf-8")), "utf-8") + + # Base64-encoded NGINX mime.types (/etc/nginx/mime.types) + f = open(NcgConfig.config['templates']['root_dir'] + '/' + apiversion + '/' + NcgConfig.config['templates'][ + 'mimetypes'], 'r') + nginxMimeTypes = f.read() + f.close() + + b64NginxMimeTypes = str(base64.urlsafe_b64encode(nginxMimeTypes.encode("utf-8")), "utf-8") + filesMimeType = {'contents': b64NginxMimeTypes, 'name': NcgConfig.config['nms']['config_dir'] + '/mime.types'} + auxFiles['files'].append(filesMimeType) + + # Base64-encoded NGINX HTTP service configuration + filesNginxMain = {'contents': b64NginxMain, 'name': NcgConfig.config['nms']['config_dir'] + '/nginx.conf'} + filesHttpConf = {'contents': b64HttpConf, + 'name': NcgConfig.config['nms']['config_dir'] + '/' + NcgConfig.config['nms'][ + 'staged_config_http_filename']} + filesStreamConf = {'contents': b64StreamConf, + 'name': NcgConfig.config['nms']['config_dir'] + '/' + NcgConfig.config['nms'][ + 'staged_config_stream_filename']} + + # Append config files to staged configuration + configFiles['files'].append(filesNginxMain) + configFiles['files'].append(filesHttpConf) + configFiles['files'].append(filesStreamConf) + + # Staged config + baseStagedConfig = {'auxFiles': auxFiles, 'configFiles': configFiles} + stagedConfig = {'auxFiles': auxFiles, 'configFiles': configFiles, + 'updateTime': datetime.utcnow().isoformat()[:-3] + 'Z', + 'ignoreConflict': True, 'validateConfig': False} + + currentBaseStagedConfig = NcgRedis.redis.get(f'ncg.basestagedconfig.{configUid}').decode('utf-8') if NcgRedis.redis.get(f'ncg.basestagedconfig.{configUid}') else None + newBaseStagedConfig = json.dumps(baseStagedConfig) + + if currentBaseStagedConfig is not None and newBaseStagedConfig == currentBaseStagedConfig: + print(f'Declaration [{configUid}] not changed') + return {"status_code": 200, + "message": {"status_code": 200, "message": {"code": 200, "content": "no changes"}}} + else: + # Configuration objects have changed, publish to NIM needed + print(f'Declaration [{configUid}] changed, publishing to NMS') + + # Retrieve instance group uid + try: + ig = requests.get(url=f'{nmsUrl}/api/platform/v1/instance-groups', auth=(nmsUsername, nmsPassword), + verify=False) + except Exception as e: + return {"status_code": 400, + "message": {"status_code": 400, + "message": {"code": 400, "content": f"Can't connect to {nmsUrl}"}}} + + if ig.status_code != 200: + try: + return {"status_code": ig.status_code, + "message": {"status_code": ig.status_code, + "message": {"code": ig.status_code, "content": json.loads(ig.text)}}} + except: + return {"status_code": ig.status_code, + "message": {"status_code": ig.status_code, + "message": {"code": ig.status_code, "content": ig.text}}} + + # Get the instance group id + igUid = v4_1.NIMUtils.getNIMInstanceGroupUid(nmsUrl=nmsUrl, nmsUsername=nmsUsername, + nmsPassword=nmsPassword, instanceGroupName=nmsInstanceGroup) + + # Invalid instance group + if igUid is None: + return {"status_code": 404, + "message": {"status_code": 404, "message": {"code": 404, + "content": f"instance group {nmsInstanceGroup} not found"}}, + "headers": {'Content-Type': 'application/json'}} + + ### NGINX App Protect policies support - commits policies to control plane + + # Check NGINX App Protect WAF policies configuration sanity + status, description = v4_1.NAPUtils.checkDeclarationPolicies(d) + + if status != 200: + return {"status_code": 422, "message": {"status_code": status, "message": description}} + + # Provision NGINX App Protect WAF policies to NGINX Instance Manager + provisionedNapPolicies, activePolicyUids = v4_1.NAPUtils.provisionPolicies( + nmsUrl=nmsUrl, nmsUsername=nmsUsername, nmsPassword=nmsPassword, declaration=d) + + ### / NGINX App Protect policies support + + ### Publish staged config to instance group + r = requests.post(url=nmsUrl + f"/api/platform/v1/instance-groups/{igUid}/config", + data=json.dumps(stagedConfig), + headers={'Content-Type': 'application/json'}, + auth=(nmsUsername, nmsPassword), + verify=False) + + if r.status_code != 202: + # Configuration push failed + return {"status_code": r.status_code, + "message": {"status_code": r.status_code, "message": r.text}, + "headers": {'Content-Type': 'application/json'}} + + # Fetch the deployment status + publishResponse = json.loads(r.text) + + # Wait for either NIM success or failure after pushing a staged config + isPending = True + while isPending: + time.sleep(NcgConfig.config['nms']['staged_config_publish_waittime']) + deploymentCheck = requests.get(url=nmsUrl + publishResponse['links']['rel'], + auth=(nmsUsername, nmsPassword), + verify=False) + + checkJson = json.loads(deploymentCheck.text) + + if not checkJson['details']['pending']: + isPending = False + + if len(checkJson['details']['failure']) > 0: + # Staged config publish to NIM failed + jsonResponse = checkJson['details']['failure'][0] + deploymentCheck.status_code = 422 + else: + # Staged config publish to NIM succeeded + jsonResponse = json.loads(deploymentCheck.text) + + # if nmsSynctime > 0 and runfromautosync == False: + if runfromautosync == False: + # No configuration is found, generate one + configUid = str(getuniqueid()) + + # Stores the staged config to redis + # Redis keys: + # ncg.declaration.[configUid] = original config declaration + # ncg.declarationrendered.[configUid] = original config declaration - rendered + # ncg.basestagedconfig.[configUid] = base staged configuration + # ncg.apiversion.[configUid] = ncg API version + # ncg.status.[configUid] = latest status + + NcgRedis.redis.set(f'ncg.declaration.{configUid}', pickle.dumps(declaration)) + NcgRedis.redis.set(f'ncg.declarationrendered.{configUid}', json.dumps(d)) + NcgRedis.redis.set(f'ncg.basestagedconfig.{configUid}', json.dumps(baseStagedConfig)) + NcgRedis.redis.set(f'ncg.apiversion.{configUid}', apiversion) + + # Makes NGINX App Protect policies active + doWeHavePolicies = v4_1.NAPUtils.makePolicyActive(nmsUrl=nmsUrl, nmsUsername=nmsUsername, + nmsPassword=nmsPassword, + activePolicyUids=activePolicyUids, + instanceGroupUid=igUid) + + if doWeHavePolicies: + # Clean up NGINX App Protect WAF policies not used anymore + # and not defined in the declaration just pushed + time.sleep(NcgConfig.config['nms']['staged_config_publish_waittime']) + v4_1.NAPUtils.cleanPolicyLeftovers(nmsUrl=nmsUrl, nmsUsername=nmsUsername, + nmsPassword=nmsPassword, + currentPolicies=provisionedNapPolicies) + + # If deploying a new configuration in GitOps mode start autosync + if nmsSynctime == 0: + NcgRedis.declarationsList[configUid] = "static" + elif not runfromautosync: + # GitOps autosync + print(f'Starting autosync for configUid {configUid} every {nmsSynctime} seconds') + + job = schedule.every(nmsSynctime).seconds.do(lambda: configautosync(configUid)) + # Keep track of GitOps configs, key is the threaded job + NcgRedis.declarationsList[configUid] = job + + NcgRedis.redis.set(f'ncg.apiversion.{configUid}', apiversion) + + responseContent = {'code': deploymentCheck.status_code, 'content': jsonResponse, 'configUid': configUid} + + # Configuration push completed, update redis keys + if configUid != "": + NcgRedis.redis.set('ncg.status.' + configUid, json.dumps(responseContent)) + + # if nmsSynctime > 0: + # Updates status, declaration and basestagedconfig in redis + NcgRedis.redis.set('ncg.declaration.' + configUid, pickle.dumps(declaration)) + NcgRedis.redis.set('ncg.declarationrendered.' + configUid, json.dumps(d)) + NcgRedis.redis.set('ncg.basestagedconfig.' + configUid, json.dumps(baseStagedConfig)) + + return {"status_code": deploymentCheck.status_code, + "message": {"status_code": deploymentCheck.status_code, + "message": responseContent}, + "headers": {'Content-Type': 'application/json'} + } + + else: + return {"status_code": 422, "message": {"status_code": 422, "message": f"output type {decltype} unknown"}} + + +def patch_config(declaration: ConfigDeclaration, configUid: str, apiversion: str): + # Patch a declaration + if configUid not in NcgRedis.declarationsList: + return JSONResponse( + status_code=404, + content={'code': 404, 'details': {'message': f'declaration {configUid} not found'}}, + headers={'Content-Type': 'application/json'} + ) + + # The declaration sections to be patched + declarationToPatch = declaration.model_dump() + + # The currently applied declaration + status_code, currentDeclaration = get_declaration(configUid=configUid) + + # Handle policy updates + d_policies = v4_1.MiscUtils.getDictKey(declarationToPatch, 'output.nms.policies') + if d_policies is not None: + # NGINX App Protect WAF policy updates + for p in d_policies: + currentDeclaration = v4_1.DeclarationPatcher.patchNAPPolicies( + sourceDeclaration=currentDeclaration, patchedNAPPolicies=p) + + # Handle certificate updates + d_certificates = v4_1.MiscUtils.getDictKey(declarationToPatch, 'output.nms.certificates') + if d_certificates is not None: + # TLS certificate/key updates + for p in d_certificates: + currentDeclaration = v4_1.DeclarationPatcher.patchCertificates( + sourceDeclaration=currentDeclaration, patchedCertificates=p) + + # Handle declaration updates + if 'declaration' in declarationToPatch: + # HTTP + d_upstreams = v4_1.MiscUtils.getDictKey(declarationToPatch, 'declaration.http.upstreams') + if d_upstreams: + # HTTP upstream patch + for u in d_upstreams: + currentDeclaration = v4_1.DeclarationPatcher.patchHttpUpstream( + sourceDeclaration=currentDeclaration, patchedHttpUpstream=u) + + d_servers = v4_1.MiscUtils.getDictKey(declarationToPatch, 'declaration.http.servers') + if d_servers: + # HTTP servers patch + for s in d_servers: + currentDeclaration = v4_1.DeclarationPatcher.patchHttpServer( + sourceDeclaration=currentDeclaration, patchedHttpServer=s) + + # Stream / Layer4 + d_upstreams = v4_1.MiscUtils.getDictKey(declarationToPatch, 'declaration.layer4.upstreams') + if d_upstreams: + # Stream upstream patch + for u in d_upstreams: + currentDeclaration = v4_1.DeclarationPatcher.patchStreamUpstream( + sourceDeclaration=currentDeclaration, patchedStreamUpstream=u) + + d_servers = v4_1.MiscUtils.getDictKey(declarationToPatch, 'declaration.layer4.servers') + if d_servers: + # Stream servers patch + for s in d_servers: + currentDeclaration = v4_1.DeclarationPatcher.patchStreamServer( + sourceDeclaration=currentDeclaration, patchedStreamServer=s) + + # Apply the updated declaration + configDeclaration = ConfigDeclaration.model_validate_json(json.dumps(currentDeclaration)) + + r = createconfig(declaration=configDeclaration, apiversion=apiversion, + runfromautosync=True, configUid=configUid) + + # Return the updated declaration + message = r['message'] + + if r['status_code'] != 200: + currentDeclaration = {} + # message = f'declaration {configUid} update failed'; + + responseContent = {'code': r['status_code'], 'details': {'message': message}, + 'declaration': currentDeclaration, 'configUid': configUid} + + return JSONResponse( + status_code=r['status_code'], + content=responseContent, + headers={'Content-Type': 'application/json'} + ) + + +# Gets the given declaration. Returns status_code and body +def get_declaration(configUid: str): + cfg = NcgRedis.redis.get('ncg.declaration.' + configUid) + + if cfg is None: + return 404, "" + + return 200, pickle.loads(cfg).dict() diff --git a/src/V4_1_NginxConfigDeclaration.py b/src/V4_1_NginxConfigDeclaration.py new file mode 100644 index 0000000..df73430 --- /dev/null +++ b/src/V4_1_NginxConfigDeclaration.py @@ -0,0 +1,530 @@ +""" +JSON declaration format +""" + +from __future__ import annotations + +from typing import List, Optional + +from pydantic import BaseModel, Extra, model_validator + + +class OutputConfigMap(BaseModel, extra="forbid"): + name: str = "nginx-config" + namespace: Optional[str] = "" + filename: str = "nginx-config.conf" + + +class OutputHttp(BaseModel, extra="forbid"): + url: str = "" + + +class NmsCertificate(BaseModel, extra="forbid"): + type: str + name: str + contents: Optional[ObjectFromSourceOfTruth] = {} + + @model_validator(mode='after') + def check_type(self) -> 'NmsCertificate': + _type = self.type + + valid = ['certificate', 'key'] + if _type not in valid: + raise ValueError(f"Invalid certificate type [{_type}] must be one of {str(valid)}") + + return self + + +class NmsPolicyVersion(BaseModel, extra="forbid"): + tag: str = "" + displayName: Optional[str] = "" + description: Optional[str] = "" + contents: Optional[ObjectFromSourceOfTruth] = {} + + +class NmsPolicy(BaseModel, extra="forbid"): + type: str = "" + name: str = "" + active_tag: str = "" + versions: Optional[List[NmsPolicyVersion]] = [] + + @model_validator(mode='after') + def check_type(self) -> 'NmsPolicy': + _type = self.type + + valid = ['app_protect'] + if _type not in valid: + raise ValueError(f"Invalid policy type [{_type}] must be one of {str(valid)}") + + return self + + +class AppProtectLogProfile(BaseModel, extra="forbid"): + name: str + format: Optional[str] = "default" + format_string: Optional[str] = "" + type: Optional[str] = "blocked" + max_request_size: Optional[str] = "any" + max_message_size: Optional[str] = "5k" + + @model_validator(mode='after') + def check_type(self) -> 'AppProtectLogProfile': + _type, _format, _format_string = self.type, self.format, self.format_string + + valid = ['all', 'illegal', 'blocked'] + if _type not in valid: + raise ValueError(f"Invalid NGINX App Protect log type [{_type}] must be one of {str(valid)}") + + valid = ['default', 'grpc', 'arcsight', 'splunk', 'user-defined'] + if _format not in valid: + raise ValueError(f"Invalid NGINX App Protect log format [{_format}] must be one of {str(valid)}") + + if _format == 'user-defined' and _format_string == "": + raise ValueError(f"NGINX App Protect log format {_format} requires format_string") + + return self + + +class LogProfile(BaseModel, extra="forbid"): + type: str + app_protect: Optional[AppProtectLogProfile] = {} + + @model_validator(mode='after') + def check_type(self) -> 'LogProfile': + _type, app_protect = self.type, self.app_protect + + valid = ['app_protect'] + if _type not in valid: + raise ValueError(f"Invalid log profile type [{_type}] must be one of {str(valid)}") + + isError = False + if _type == 'app_protect': + if app_protect is None: + isError = True + + if isError: + raise ValueError(f"Invalid log profile data for type [{_type}]") + + return self + + +class OutputNMS(BaseModel, extra="forbid"): + url: str = "" + username: str = "" + password: str = "" + instancegroup: str = "" + synctime: Optional[int] = 0 + modules: Optional[List[str]] = [] + certificates: Optional[List[NmsCertificate]] = [] + policies: Optional[List[NmsPolicy]] = [] + log_profiles: Optional[List[LogProfile]] = [] + + +class Output(BaseModel, extra="forbid"): + type: str + configmap: Optional[OutputConfigMap] = {} + http: Optional[OutputHttp] = {} + nms: Optional[OutputNMS] = {} + + @model_validator(mode='after') + def check_type(self) -> 'Output': + _type, configmap, http, nms = self.type, self.configmap, self.http, self.nms + + valid = ['plaintext', 'json', 'configmap', 'http', 'nms'] + if _type not in valid: + raise ValueError(f"Invalid output type [{_type}] must be one of {str(valid)}") + + isError = False + + if _type == 'configmap' and not configmap: + isError = True + elif _type == 'http' and not http: + isError = True + elif _type == 'nms' and not nms: + isError = True + + if isError: + raise ValueError(f"Invalid output data for type [{_type}]") + + return self + + + +class OcspStapling(BaseModel, extra="forbid"): + enabled: Optional[bool] = False + verify: Optional[bool] = False + responder: Optional[str] = "" + + +class Ocsp(BaseModel, extra="forbid"): + enabled: Optional[str] = "off" + responder: Optional[str] = "" + + +class Mtls(BaseModel, extra="forbid"): + enabled: Optional[str] = "off" + client_certificates: str = "" + + @model_validator(mode='after') + def check_type(self) -> 'Mtls': + _enabled = self.enabled + + valid = ['on', 'off', 'optional', 'optional_no_ca'] + if _enabled not in valid: + raise ValueError(f"Invalid mTLS type [{_enabled}] must be one of {str(valid)}") + + return self + + +class Tls(BaseModel, extra="forbid"): + certificate: str = "" + key: str = "" + trusted_ca_certificates: str = "" + ciphers: Optional[str] = "" + protocols: Optional[List[str]] = [] + mtls: Optional[Mtls] = {} + ocsp: Optional[Ocsp] = {} + stapling: Optional[OcspStapling] = {} + + +class Listen(BaseModel, extra="forbid"): + address: Optional[str] = "" + http2: Optional[bool] = False + tls: Optional[Tls] = {} + + +class ListenL4(BaseModel, extra="forbid"): + address: Optional[str] = "" + protocol: Optional[str] = "tcp" + tls: Optional[Tls] = {} + + @model_validator(mode='after') + def check_type(self) -> 'ListenL4': + protocol, tls = self.protocol, self.tls + + valid = ['tcp', 'udp'] + if protocol not in valid: + raise ValueError(f"Invalid protocol [{protocol}] must be one of {str(valid)}") + + if protocol != 'tcp' and tls and tls.certificate: + raise ValueError("TLS termination over UDP is not supported") + + return self + + +class Log(BaseModel, extra="forbid"): + access: Optional[str] = "" + error: Optional[str] = "" + + +class RateLimit(BaseModel, extra="forbid"): + profile: Optional[str] = "" + httpcode: Optional[int] = 429 + burst: Optional[int] = 0 + delay: Optional[int] = 0 + + +class LocationAuthClient(BaseModel, extra="forbid"): + profile: Optional[str] = "" + + +class LocationAuthServer(BaseModel, extra="forbid"): + profile: Optional[str] = "" + + +class LocationAuth(BaseModel, extra="forbid"): + client: Optional[List[LocationAuthClient]] = [] + server: Optional[List[LocationAuthServer]] = [] + + +class RateLimitApiGw(BaseModel, extra="forbid"): + profile: Optional[str] = "" + httpcode: Optional[int] = 429 + burst: Optional[int] = 0 + delay: Optional[int] = 0 + enforceOnPaths: Optional[bool] = True + paths: Optional[List[str]] = [] + +class APIGatewayAuthentication(BaseModel, extra="forbid"): + client: Optional[List[LocationAuthClient]] = [] + enforceOnPaths: Optional[bool] = True + paths: Optional[List[str]] = [] + + +class AuthClientJWT(BaseModel, extra="forbid"): + realm: str = "JWT Authentication" + key: str = "" + cachetime: Optional[int] = 0 + jwt_type: str = "signed" + + @model_validator(mode='after') + def check_type(self) -> 'AuthClientJWT': + jwt_type, key = self.jwt_type, self.key + + if not key.strip() : + raise ValueError(f"Invalid: JWT key must not be empty") + + valid = ['signed', 'encrypted', 'nested'] + if jwt_type not in valid: + raise ValueError(f"Invalid JWT type [{jwt_type}] must be one of {str(valid)}") + + return self + +class AuthServerToken(BaseModel, extra="forbid"): + token: str = "" + type: Optional[str] = "bearer" + location: Optional[str] = "" + + @model_validator(mode='after') + def check_type(self) -> 'AuthServerToken': + location, type = self.location, self.type.lower() + + valid = ['bearer', 'header'] + if type not in valid: + raise ValueError(f"Invalid token type [{type}] must be one of {str(valid)}") + + if type in ['header'] and location == "": + raise ValueError(f"Empty location for [{type}] token") + + return self + + +class HealthCheck(BaseModel, extra="forbid"): + enabled: Optional[bool] = False + uri: Optional[str] = "/" + interval: Optional[int] = 5 + fails: Optional[int] = 1 + passes: Optional[int] = 1 + + +class AppProtectLog(BaseModel, extra="forbid"): + enabled: Optional[bool] = False + profile_name: Optional[str] = "" + destination: Optional[str] = "" + + +class AppProtect(BaseModel, extra="forbid"): + enabled: Optional[bool] = False + policy: str = "" + log: AppProtectLog = {} + + +class Location(BaseModel, extra="forbid"): + uri: str + urimatch: Optional[str] = "prefix" + upstream: Optional[str] = "" + log: Optional[Log] = {} + apigateway: Optional[APIGateway] = {} + caching: Optional[str] = "" + rate_limit: Optional[RateLimit] = {} + health_check: Optional[HealthCheck] = {} + app_protect: Optional[AppProtect] = {} + snippet: Optional[ObjectFromSourceOfTruth] = {} + authentication: Optional[LocationAuth] = {} + + @model_validator(mode='after') + def check_type(self) -> 'Location': + urimatch = self.urimatch + upstream = self.upstream + + valid = ['prefix', 'exact', 'regex', 'iregex', 'best'] + if urimatch not in valid: + raise ValueError(f"Invalid URI match type [{urimatch}] must be one of {str(valid)}") + + prefixes = ["http://", "https://"] + if upstream != "" and not upstream.lower().startswith(tuple(prefixes)): + raise ValueError(f"Upstream must start with one of {str(prefixes)}") + + return self + + +class ObjectFromSourceOfTruth(BaseModel, extra="forbid"): + content: str = "" + authentication: Optional[List[LocationAuthServer]] = [] + +class Server(BaseModel, extra="forbid"): + name: str + names: Optional[List[str]] = [] + resolver: Optional[str] = "" + listen: Optional[Listen] = {} + log: Optional[Log] = {} + locations: Optional[List[Location]] = [] + app_protect: Optional[AppProtect] = {} + snippet: Optional[ObjectFromSourceOfTruth] = {} + + +class L4Server(BaseModel, extra="forbid"): + name: str + listen: Optional[ListenL4] = {} + upstream: Optional[str] = "" + snippet: Optional[ObjectFromSourceOfTruth] = {} + + +class Sticky(BaseModel, extra="forbid"): + cookie: str = "" + expires: Optional[str] = "" + domain: Optional[str] = "" + path: Optional[str] = "" + + +class Origin(BaseModel, extra="forbid"): + server: str + weight: Optional[int] = 1 + max_fails: Optional[int] = 1 + fail_timeout: Optional[str] = "10s" + max_conns: Optional[int] = 0 + slow_start: Optional[str] = "0" + backup: Optional[bool] = False + + +class L4Origin(BaseModel, extra="forbid"): + server: str + weight: Optional[int] = 1 + max_fails: Optional[int] = 1 + fail_timeout: Optional[str] = "" + max_conns: Optional[int] = 0 + slow_start: Optional[str] = "" + backup: Optional[bool] = False + + +class Upstream(BaseModel, extra="forbid"): + name: str + origin: Optional[List[Origin]] = [] + sticky: Optional[Sticky] = {} + snippet: Optional[ObjectFromSourceOfTruth] = {} + + +class L4Upstream(BaseModel, extra="forbid"): + name: str + origin: Optional[List[L4Origin]] = [] + snippet: Optional[ObjectFromSourceOfTruth] = {} + + +class ValidItem(BaseModel, extra="forbid"): + codes: Optional[List[int]] = [200] + ttl: Optional[str] = 60 + + +class CachingItem(BaseModel, extra="forbid"): + name: str + key: str + size: Optional[str] = "10m" + valid: Optional[List[ValidItem]] = [] + + +class RateLimitItem(BaseModel, extra="forbid"): + name: str + key: str + size: Optional[str] = "" + rate: Optional[str] = "" + + +class NginxPlusApi(BaseModel, extra="forbid"): + write: Optional[bool] = False + listen: Optional[str] = "" + allow_acl: Optional[str] = "" + + +class MapEntry(BaseModel, extra="forbid"): + key: str + keymatch: str + value: str + + @model_validator(mode='after') + def check_type(self) -> 'MapEntry': + keymatch = self.keymatch + + valid = ['exact', 'regex', 'iregex'] + if keymatch not in valid: + raise ValueError(f"Invalid key match type [{keymatch}] must be one of {str(valid)}") + + return self + + +class Map(BaseModel, extra="forbid"): + match: str + variable: str + entries: Optional[List[MapEntry]] = [] + + +class Layer4(BaseModel, extra="forbid"): + servers: Optional[List[L4Server]] = [] + upstreams: Optional[List[L4Upstream]] = [] + + +class Authentication_Client(BaseModel, extra="forbid"): + name: str + type: str + + jwt: Optional[AuthClientJWT] = {} + + @model_validator(mode='after') + def check_type(self) -> 'Authentication_Client': + _type, name = self.type, self.name + + valid = ['jwt'] + if _type not in valid: + raise ValueError(f"Invalid client authentication type [{_type}] for profile [{name}] must be one of {str(valid)}") + + return self + + +class Authentication_Server(BaseModel, extra="forbid"): + name: str + type: str + + token: Optional[AuthServerToken] = {} + + @model_validator(mode='after') + def check_type(self) -> 'Authentication_Server': + _type, name = self.type, self.name + + valid = ['token'] + if _type not in valid: + raise ValueError(f"Invalid server authentication type [{_type}] for profile [{name}] must be one of {str(valid)}") + + return self + + +class Authentication(BaseModel, extra="forbid"): + client: Optional[List[Authentication_Client]] = [] + server: Optional[List[Authentication_Server]] = [] + + +class Http(BaseModel, extra="forbid"): + servers: Optional[List[Server]] = [] + upstreams: Optional[List[Upstream]] = [] + caching: Optional[List[CachingItem]] = [] + rate_limit: Optional[List[RateLimitItem]] = [] + nginx_plus_api: Optional[NginxPlusApi] = {} + maps: Optional[List[Map]] = [] + snippet: Optional[ObjectFromSourceOfTruth] = {} + authentication: Optional[Authentication] = {} + + +class Declaration(BaseModel, extra="forbid"): + layer4: Optional[Layer4] = {} + http: Optional[Http] = {} + + +class API_Gateway(BaseModel, extra="forbid"): + enabled: Optional[bool] = False + strip_uri: Optional[bool] = False + server_url: Optional[str] = "" + +class DeveloperPortal(BaseModel, extra="forbid"): + enabled: Optional[bool] = False + uri: Optional[str] = "/devportal.html" + +class APIGateway(BaseModel, extra="forbid"): + openapi_schema: Optional[ObjectFromSourceOfTruth] = {} + api_gateway: Optional[API_Gateway] = {} + developer_portal: Optional[DeveloperPortal] = {} + rate_limit: Optional[List[RateLimitApiGw]] = [] + authentication: Optional[APIGatewayAuthentication] = {} + log: Optional[Log] = {} + + +class ConfigDeclaration(BaseModel, extra="forbid"): + output: Output + declaration: Optional[Declaration] = {} diff --git a/src/main.py b/src/main.py index 03037b1..3cc0002 100644 --- a/src/main.py +++ b/src/main.py @@ -22,6 +22,9 @@ import V4_0_CreateConfig import V4_0_NginxConfigDeclaration +import V4_1_CreateConfig +import V4_1_NginxConfigDeclaration + cfg = NcgConfig.NcgConfig(configFile="../etc/config.toml") redis = NcgRedis.NcgRedis(host=cfg.config['redis']['host'], port=cfg.config['redis']['port']) @@ -83,6 +86,28 @@ def post_config_v4_0(d: V4_0_NginxConfigDeclaration.ConfigDeclaration, response: return JSONResponse(content=response, status_code=output['status_code'], headers=headers) +# Submit declaration using v4.1 API +@app.post("/v4.1/config", status_code=200, response_class=PlainTextResponse) +def post_config_v4_1(d: V4_1_NginxConfigDeclaration.ConfigDeclaration, response: Response): + output = V4_1_CreateConfig.createconfig(declaration=d, apiversion='v4.1') + + if type(output) in [Response, str]: + # ConfigMap or plaintext response + return output + + headers = output['message']['headers'] if 'headers' in output['message'] else {'Content-Type': 'application/json'} + + if 'message' in output: + if 'message' in output['message']: + response = output['message']['message'] + else: + response = output['message'] + else: + response = output + + return JSONResponse(content=response, status_code=output['status_code'], headers=headers) + + # Modify declaration using v3.1 API @app.patch("/v3.1/config/{configuid}", status_code=200, response_class=PlainTextResponse) def patch_config_v3_1(d: V3_1_NginxConfigDeclaration.ConfigDeclaration, response: Response, configuid: str): @@ -95,6 +120,12 @@ def patch_config_v4_0(d: V4_0_NginxConfigDeclaration.ConfigDeclaration, response return V4_0_CreateConfig.patch_config(declaration=d, configUid=configuid, apiversion='v4.0') +# Modify declaration using v4.1 API +@app.patch("/v4.1/config/{configuid}", status_code=200, response_class=PlainTextResponse) +def patch_config_v4_1(d: V4_1_NginxConfigDeclaration.ConfigDeclaration, response: Response, configuid: str): + return V4_1_CreateConfig.patch_config(declaration=d, configUid=configuid, apiversion='v4.1') + + # Get declaration - v3.1 API @app.get("/v3.1/config/{configuid}", status_code=200, response_class=PlainTextResponse) def get_config_declaration_v3_1(configuid: str): @@ -133,9 +164,29 @@ def get_config_declaration_v4_0(configuid: str): ) +# Get declaration - v4.1 API +@app.get("/v4.1/config/{configuid}", status_code=200, response_class=PlainTextResponse) +def get_config_declaration_v4_1(configuid: str): + status_code, content = V4_1_CreateConfig.get_declaration(configUid=configuid) + + if status_code == 404: + return JSONResponse( + status_code=404, + content={'code': 404, 'details': {'message': f'declaration {configuid} not found'}}, + headers={'Content-Type': 'application/json'} + ) + + return JSONResponse( + status_code=200, + content=content, + headers={'Content-Type': 'application/json'} + ) + + # Get declaration status @app.get("/v3.1/config/{configuid}/status", status_code=200, response_class=PlainTextResponse) @app.get("/v4.0/config/{configuid}/status", status_code=200, response_class=PlainTextResponse) +@app.get("/v4.1/config/{configuid}/status", status_code=200, response_class=PlainTextResponse) def get_config_status(configuid: str): status = redis.redis.get('ncg.status.' + configuid) @@ -156,6 +207,7 @@ def get_config_status(configuid: str): # Delete declaration @app.delete("/v3.1/config/{configuid}", status_code=200, response_class=PlainTextResponse) @app.delete("/v4.0/config/{configuid}", status_code=200, response_class=PlainTextResponse) +@app.delete("/v4.1/config/{configuid}", status_code=200, response_class=PlainTextResponse) def delete_config(configuid: str = ""): if configuid not in redis.declarationsList: return JSONResponse( diff --git a/src/v4_1/APIGateway.py b/src/v4_1/APIGateway.py new file mode 100644 index 0000000..0c87e87 --- /dev/null +++ b/src/v4_1/APIGateway.py @@ -0,0 +1,37 @@ +""" +API Gateway support functions +""" + +import json + +import v4_1.GitOps +import v4_1.MiscUtils +from v4_1.OpenAPIParser import OpenAPIParser + +# pydantic models +from V4_1_NginxConfigDeclaration import * + + +# Builds the declarative JSON for the API Gateway configuration +# Return a tuple: status, description. If status = 200 things were successful +def createAPIGateway(locationDeclaration: dict, authProfiles: Authentication={}): + apiGwDeclaration = {} + + if locationDeclaration['apigateway']['openapi_schema']: + status, apiSchemaString = v4_1.GitOps.getObjectFromRepo(object=locationDeclaration['apigateway']['openapi_schema'], + authProfiles = authProfiles['server'] if 'server' in authProfiles else {}, base64Encode=False) + + if v4_1.MiscUtils.yaml_or_json(apiSchemaString['content']) == 'yaml': + # YAML to JSON conversion + apiSchemaString['content'] = v4_1.MiscUtils.yaml_to_json(apiSchemaString['content']) + + apiSchema = OpenAPIParser(json.loads(apiSchemaString['content'])) + + apiGwDeclaration = {} + apiGwDeclaration['location'] = locationDeclaration + apiGwDeclaration['info'] = apiSchema.info() + apiGwDeclaration['servers'] = apiSchema.servers() + apiGwDeclaration['paths'] = apiSchema.paths() + apiGwDeclaration['version'] = apiSchema.version() + + return 200, apiGwDeclaration \ No newline at end of file diff --git a/src/v4_1/DeclarationPatcher.py b/src/v4_1/DeclarationPatcher.py new file mode 100644 index 0000000..6e27285 --- /dev/null +++ b/src/v4_1/DeclarationPatcher.py @@ -0,0 +1,238 @@ +""" +Declaration parsing functions +""" + + +# Returns the patched declaration based on the patchedHttpServer +def patchHttpServer(sourceDeclaration: dict, patchedHttpServer: dict): + allTargetServers = [] + + haveWePatched = False + + if 'declaration' not in sourceDeclaration: + sourceDeclaration['declaration'] = {} + + if 'http' not in sourceDeclaration['declaration']: + sourceDeclaration['declaration']['http'] = {} + + if 'servers' not in sourceDeclaration['declaration']['http']: + sourceDeclaration['declaration']['http']['servers'] = [] + + # HTTP server patch + for s in sourceDeclaration['declaration']['http']['servers']: + if s['name'] == patchedHttpServer['name']: + # Patching an existing HTTP server, 'name' is the key + if len(patchedHttpServer) > 1: + # Patching HTTP server specifying only 'name' (len == 1) means delete + # If further fields are specified HTTP server is patched + allTargetServers.append(patchedHttpServer) + + haveWePatched = True + else: + # Unmodified HTTP server + allTargetServers.append(s) + + if not haveWePatched: + # The HTTP server being patched is a new one, let's add it + allTargetServers.append(patchedHttpServer) + + sourceDeclaration['declaration']['http']['servers'] = allTargetServers + + return sourceDeclaration + + +# Returns the patched declaration based on the patchedHttpUpstream +def patchHttpUpstream(sourceDeclaration: dict, patchedHttpUpstream: dict): + allTargetUpstreams = [] + + haveWePatched = False + + if 'declaration' not in sourceDeclaration: + sourceDeclaration['declaration'] = {} + + if 'http' not in sourceDeclaration['declaration']: + sourceDeclaration['declaration']['http'] = {} + + if 'upstreams' not in sourceDeclaration['declaration']['http']: + sourceDeclaration['declaration']['http']['upstreams'] = [] + + # HTTP upstreams patch + for s in sourceDeclaration['declaration']['http']['upstreams']: + if s['name'] == patchedHttpUpstream['name']: + # Patching an existing HTTP upstream, 'name' is the key + if len(patchedHttpUpstream) > 1: + # Patching HTTP upstream specifying only 'name' (len == 1) means delete + # If further fields are specified HTTP upstream is patched + allTargetUpstreams.append(patchedHttpUpstream) + + haveWePatched = True + else: + # Unmodified HTTP upstream + allTargetUpstreams.append(s) + + if not haveWePatched: + # The HTTP upstream being patched is a new one, let's add it + allTargetUpstreams.append(patchedHttpUpstream) + + sourceDeclaration['declaration']['http']['upstreams'] = allTargetUpstreams + + return sourceDeclaration + + +# Returns the patched declaration based on the patchedStreamServer +def patchStreamServer(sourceDeclaration: dict, patchedStreamServer: dict): + allTargetServers = [] + + haveWePatched = False + + if 'declaration' not in sourceDeclaration: + sourceDeclaration['declaration'] = {} + + if 'layer4' not in sourceDeclaration['declaration']: + sourceDeclaration['declaration']['layer4'] = {} + + if 'servers' not in sourceDeclaration['declaration']['layer4']: + sourceDeclaration['declaration']['layer4']['servers'] = [] + + # HTTP server patch + for s in sourceDeclaration['declaration']['layer4']['servers']: + if s['name'] == patchedStreamServer['name']: + # Patching an existing Stream server, 'name' is the key + if len(patchedStreamServer) > 1: + # Patching Stream server specifying only 'name' (len == 1) means delete + # If further fields are specified HTTP server is patched + allTargetServers.append(patchedStreamServer) + + haveWePatched = True + else: + # Unmodified HTTP server + allTargetServers.append(s) + + if not haveWePatched: + # The Stream server being patched is a new one, let's add it + allTargetServers.append(patchedStreamServer) + + sourceDeclaration['declaration']['layer4']['servers'] = allTargetServers + + return sourceDeclaration + + +# Returns the patched declaration based on the patchedStreamUpstream +def patchStreamUpstream(sourceDeclaration: dict, patchedStreamUpstream: dict): + allTargetUpstreams = [] + + haveWePatched = False + + if 'declaration' not in sourceDeclaration: + sourceDeclaration['declaration'] = {} + + if 'layer4' not in sourceDeclaration['declaration']: + sourceDeclaration['declaration']['layer4'] = {} + + if 'upstreams' not in sourceDeclaration['declaration']['layer4']: + sourceDeclaration['declaration']['layer4']['upstreams'] = [] + + # HTTP upstreams patch + for s in sourceDeclaration['declaration']['layer4']['upstreams']: + if s['name'] == patchedStreamUpstream['name']: + # Patching an existing Stream upstream, 'name' is the key + if len(patchedStreamUpstream) > 1: + # Patching Stream upstream specifying only 'name' (len == 1) means delete + # If further fields are specified HTTP upstream is patched + allTargetUpstreams.append(patchedStreamUpstream) + + haveWePatched = True + else: + # Unmodified HTTP upstream + allTargetUpstreams.append(s) + + if not haveWePatched: + # The Stream upstream being patched is a new one, let's add it + allTargetUpstreams.append(patchedStreamUpstream) + + sourceDeclaration['declaration']['layer4']['upstreams'] = allTargetUpstreams + + return sourceDeclaration + + +# Returns the patched declaration based on the patchedNAPPolicies +def patchNAPPolicies(sourceDeclaration: dict, patchedNAPPolicies: dict): + allTargetPolicies = [] + + haveWePatched = False + + if 'output' not in sourceDeclaration: + return sourceDeclaration + + if 'nms' not in sourceDeclaration['output']: + return sourceDeclaration + + if 'policies' not in sourceDeclaration['output']['nms']: + return sourceDeclaration + + # NGINX App Protect WAF policies patch + for p in sourceDeclaration['output']['nms']['policies']: + if 'type' in p and p['type'] == 'app_protect' \ + and 'name' in p and p['name'] \ + and p['type'] == patchedNAPPolicies['type'] \ + and p['name'] == patchedNAPPolicies['name']: + + # Patching an existing NGINX App Protect WAF policy, 'name' is the key + if patchedNAPPolicies['versions'] and patchedNAPPolicies['active_tag']: + # Patching NAP policy specifying 'versions' and 'active_tag' means updating + # If 'versions' and 'active_tag' are missing then it's a deletion + allTargetPolicies.append(patchedNAPPolicies) + + haveWePatched = True + else: + # Unmodified HTTP upstream + allTargetPolicies.append(p) + + if not haveWePatched: + # The NAP policy being patched is a new one, let's add it + allTargetPolicies.append(patchedNAPPolicies) + + sourceDeclaration['output']['nms']['policies'] = allTargetPolicies + + return sourceDeclaration + + +# Returns the patched declaration based on patchedCertificates +def patchCertificates(sourceDeclaration: dict, patchedCertificates: dict): + allTargetCertificates = [] + + haveWePatched = False + + if 'output' not in sourceDeclaration: + return sourceDeclaration + + if 'nms' not in sourceDeclaration['output']: + return sourceDeclaration + + if 'certificates' not in sourceDeclaration['output']['nms']: + return sourceDeclaration + + # TLS certificates patch + for c in sourceDeclaration['output']['nms']['certificates']: + if 'type' in c and c['type'] in ['certificate', 'key', 'chain'] \ + and 'name' in c and c['name'] \ + and c['type'] == patchedCertificates['type'] \ + and c['name'] == patchedCertificates['name']: + + if 'contents' in c and c['contents']: + # Patching an existing TLS certificate/key/chain, 'name' is the key. + # If content is empty the certificate is deleted + allTargetCertificates.append(patchedCertificates) + + haveWePatched = True + else: + # Unmodified HTTP upstream + allTargetCertificates.append(c) + + if not haveWePatched: + # The TLS certificate/key/chain being patched is a new one, let's add it + allTargetCertificates.append(patchedCertificates) + + sourceDeclaration['output']['nms']['certificates'] = allTargetCertificates + + return sourceDeclaration diff --git a/src/v4_1/DevPortal.py b/src/v4_1/DevPortal.py new file mode 100644 index 0000000..7f508cc --- /dev/null +++ b/src/v4_1/DevPortal.py @@ -0,0 +1,46 @@ +""" +API Gateway Developer Portal support functions +""" + +import json +import requests +import base64 + +# NGINX Declarative API modules +from NcgConfig import NcgConfig +import v4_1.GitOps +import v4_1.MiscUtils + +# pydantic models +from V4_1_NginxConfigDeclaration import * + +def buildDevPortal(openapischema): + try: + response = requests.post(f"http://{NcgConfig.config['devportal']['host']}:" + f"{NcgConfig.config['devportal']['port']}{NcgConfig.config['devportal']['uri']}", + headers={'Content-Type': 'application/json'}, data=openapischema) + except Exception as e: + return 400, "" + + return response.status_code, json.loads(response.text) + + +# Builds the declarative JSON for the API Gateway configuration +# Return a tuple: status, description. If status = 200 things were successful +def createDevPortal(locationDeclaration: dict, authProfiles: Authentication={}): + if locationDeclaration['apigateway']['openapi_schema']: + status, apiSchemaString = v4_1.GitOps.getObjectFromRepo( + object = locationDeclaration['apigateway']['openapi_schema'], authProfiles = authProfiles['server'] if 'server' in authProfiles else {}, base64Encode = False) + + if v4_1.MiscUtils.yaml_or_json(apiSchemaString['content']) == 'yaml': + # YAML to JSON conversion + status, devportalJSON = buildDevPortal(openapischema = v4_1.MiscUtils.yaml_to_json(apiSchemaString['content'])) + else: + status, devportalJSON = buildDevPortal(openapischema = apiSchemaString['content']) + + if status == 200: + devportalHTML = base64.b64encode(bytes(devportalJSON['devportal'], 'utf-8')).decode('utf-8') + else: + devportalHTML = "" + + return status, devportalHTML \ No newline at end of file diff --git a/src/v4_1/GitOps.py b/src/v4_1/GitOps.py new file mode 100644 index 0000000..5a1be5b --- /dev/null +++ b/src/v4_1/GitOps.py @@ -0,0 +1,64 @@ +""" +GitOps support functions +""" + +import base64 +import requests + +from requests import ReadTimeout, HTTPError, Timeout, ConnectionError, ConnectTimeout +from typing import List + +# pydantic models +from V4_1_NginxConfigDeclaration import * + + +# Fetches a URL content +def __fetchfromsourceoftruth__(url, headers = {} ): + # Object is fetched from external repository + try: + reply = requests.get(url = url, headers = headers, verify=False) + except (ConnectTimeout, HTTPError, ReadTimeout, Timeout, ConnectionError): + return 408, "URL " + url + " unreachable" + + return reply.status_code, reply.text + + +# If content starts with http(s):// fetches the object and return it b64-encoded by default. +# base64Encode to be set to False to disable b64 encoding +# Returns the status original content otherwise. +# Return is a tuple: status_code, content +def getObjectFromRepo(object: ObjectFromSourceOfTruth, authProfiles: Authentication={}, base64Encode: bool=True): + status_code = 200 + response = object + + if object: + if object['content'].lower().startswith(("http://","https://")): + # Object is fetched from external repository + headers = {} + + # Set server authentication if needed + if authProfiles and 'server' in authProfiles: + for authP in authProfiles['server']: + if object['authentication'][0]['profile'] == authP['name']: + # Sets up authentication + if authP['type'].lower() == 'token': + authToken = authP['token']['token'] + authTokenType = authP['token']['type'] + authTokenLocation = authP['token']['location'] + + if authTokenType.lower() == 'bearer': + headers['Authorization'] = f"Bearer {authToken}" + elif authTokenType.lower() == 'header': + headers[authTokenLocation] = authToken + + status_code, fetchedContent = __fetchfromsourceoftruth__(url = object['content'], headers = headers) + + if status_code == 200: + if base64Encode == True: + fetchedContent = base64.b64encode(bytes(fetchedContent, 'utf-8')).decode('utf-8') + else: + fetchedContent = bytes(fetchedContent, 'utf-8').decode("utf-8") + + response['content'] = fetchedContent + + return status_code, response \ No newline at end of file diff --git a/src/v4_1/MiscUtils.py b/src/v4_1/MiscUtils.py new file mode 100644 index 0000000..a5ea547 --- /dev/null +++ b/src/v4_1/MiscUtils.py @@ -0,0 +1,44 @@ +""" +Support functions +""" + +import re +import json +import yaml + +def getDictKey(_dict: dict, key_lookup: str, separator='.'): + """ + Searches for a nested key in a dictionary and returns its value, or None if nothing was found. + key_lookup must be a string where each key is deparated by a given "separator" character, which by default is a dot + """ + keys = key_lookup.split(separator) + subdict = _dict + + for k in keys: + subdict = subdict[k] if k in subdict else None + if subdict is None: + return None + + return subdict + +""" +Jinja2 regexp filter +""" +def regex_replace(s, find, replace): + return re.sub(find, replace, s) + +""" +JSON/YAML detection +""" +def yaml_or_json(document: str): + try: + json.load(document) + return 'json' + except Exception: + return 'yaml' + +""" +YAML to JSON conversion +""" +def yaml_to_json(document: str): + return json.dumps(yaml.safe_load(document)) \ No newline at end of file diff --git a/src/v4_1/NAPUtils.py b/src/v4_1/NAPUtils.py new file mode 100644 index 0000000..bdfcea8 --- /dev/null +++ b/src/v4_1/NAPUtils.py @@ -0,0 +1,269 @@ +""" +NGINX App Protect support functions +""" + +import requests +import json + +import v4_1.GitOps + +from fastapi.responses import Response, JSONResponse + +available_log_profiles = ['log_all', 'log_blocked', 'log_illegal', 'secops_dashboard'] + + +# Define (create/update) a NGINX App Protect policy on NMS. +# If policyUid is not empty a the policy update is performed +# Returns a tuple {status_code,text}. status_code is 201 if successful +def __definePolicyOnNMS__(nmsUrl: str, nmsUsername: str, nmsPassword: str, policyName: str, policyDisplayName: str, + policyDescription: str, policyJson: str, policyUid: str = ""): + # policyBody holds the base64-encoded policy JSON definition + # Control plane-compiled policy bundles are supported. Create the NGINX App Protect policy on NMS + # POST https://{{nms_host}}/api/platform/v1/security/policies + # { + # "metadata": { + # "name": "prod-policy", + # "displayName": "Production Policy - blocking", + # "description": "Production-ready policy - blocking" + # }, + # "content": "" + # } + + policyCreationPayload = {'metadata': {}} + policyCreationPayload['metadata']['name'] = policyName + policyCreationPayload['metadata']['displayName'] = policyDisplayName + policyCreationPayload['metadata']['description'] = policyDescription + policyCreationPayload['content'] = policyJson + + if policyUid != "": + # Existing policy update + r = requests.put(url=f"{nmsUrl}/api/platform/v1/security/policies/{policyUid}", + data=json.dumps(policyCreationPayload), + headers={'Content-Type': 'application/json'}, + auth=(nmsUsername, nmsPassword), + verify=False) + else: + # New policy creation - first try to create it as a new revision for an existing policy + # The response code is 201 if successful and 404 if there is no policy with the given name + r = requests.post(url=f"{nmsUrl}/api/platform/v1/security/policies?isNewRevision=true", + data=json.dumps(policyCreationPayload), + headers={'Content-Type': 'application/json'}, + auth=(nmsUsername, nmsPassword), + verify=False) + + # Check if this is a new policy with no existing versions. If this is true create its initial version + if r.status_code == 404: + r = requests.post(url=f"{nmsUrl}/api/platform/v1/security/policies", + data=json.dumps(policyCreationPayload), + headers={'Content-Type': 'application/json'}, + auth=(nmsUsername, nmsPassword), + verify=False) + + return r + + +# Retrieve security policies information +def __getAllPolicies__(nmsUrl: str, nmsUsername: str, nmsPassword: str): + return requests.get(url=f'{nmsUrl}/api/platform/v1/security/policies', + auth=(nmsUsername, nmsPassword), verify=False) + + +# Delete security policy from control plane +def __deletePolicy__(nmsUrl: str, nmsUsername: str, nmsPassword: str, policyUid: str): + return requests.delete(url=f'{nmsUrl}/api/platform/v1/security/policies/{policyUid}', + auth=(nmsUsername, nmsPassword), verify=False) + + +# Check NAP policies names validity for the given declaration +# Return a tuple: status, description. If status = 200 checks were successful +def checkDeclarationPolicies(declaration: dict): + # NGINX App Protect policies check - duplicated policy names + + # all policy names as defined in the declaration + # { 'policyName': 'activeTag', ... } + allPolicyNames = {} + + if 'policies' not in declaration['output']['nms']: + return 200, "" + + for policy in declaration['output']['nms']['policies']: + # print(f"Found NAP Policy [{policy['name']}] active tag [{policy['active_tag']}]") + + if policy['name'] and policy['name'] in allPolicyNames: + return 422, f"Duplicated NGINX App Protect WAF policy [{policy['name']}]" + + allPolicyNames[policy['name']] = policy['active_tag'] + + # Check policy releases for non-univoque tags + allPolicyVersionTags = {} + for policyVersion in policy['versions']: + # print(f"--> Policy [{policy['name']}] tag [{policyVersion['tag']}]") + if policyVersion['tag'] and policyVersion['tag'] in allPolicyVersionTags: + return 422, f"Duplicated NGINX App Protect WAF policy tag [{policyVersion['tag']}] " \ + f"for policy [{policy['name']}]" + + allPolicyVersionTags[policyVersion['tag']] = "found" + + if policy['active_tag'] and policy['active_tag'] not in allPolicyVersionTags: + return 422, f"Invalid active tag [{policy['active_tag']}] for policy [{policy['name']}]" + + # Check policy names referenced by the declaration inside HTTP servers[]: they must be valid + if 'http' in declaration['declaration'] and 'servers' in declaration['declaration']['http']: + for httpServer in declaration['declaration']['http']['servers']: + if 'app_protect' in httpServer: + if 'policy' in httpServer['app_protect'] and httpServer['app_protect']['policy'] \ + and httpServer['app_protect']['policy'] not in allPolicyNames: + return 422, f"Unknown NGINX App Protect WAF policy [{httpServer['app_protect']['policy']}] " \ + f"referenced by HTTP server [{httpServer['name']}]" + + if 'log' in httpServer['app_protect'] \ + and 'profile_name' in httpServer['app_protect']['log'] \ + and httpServer['app_protect']['log']['profile_name'] \ + and httpServer['app_protect']['log']['profile_name'] \ + not in available_log_profiles: + return 422, f"Invalid NGINX App Protect WAF log profile " \ + f"[{httpServer['app_protect']['log']['profile_name']}] referenced by HTTP server " \ + f"[{httpServer['name']}]" + + # Check policy names referenced in HTTP servers[].locations[] + for location in httpServer['locations']: + if 'app_protect' in location: + if 'policy' in location['app_protect'] and location['app_protect']['policy'] \ + and location['app_protect']['policy'] not in allPolicyNames: + return 422, f"Unknown NGINX App Protect WAF policy [{location['app_protect']['policy']}] " \ + f"referenced by HTTP server [{httpServer['name']}] location [{location['uri']}]" + + if 'log' in httpServer['app_protect'] and httpServer['app_protect']['log'] \ + and httpServer['app_protect']['log']['profile_name'] \ + and httpServer['app_protect']['log']['profile_name'] \ + not in available_log_profiles: + return 422, f"Invalid NGINX App Protect WAF log profile " \ + f"[{httpServer['app_protect']['log']['profile_name']}] referenced by HTTP server " \ + f"[{httpServer['name']}] location [{location['uri']}]" + + return 200, "" + + +# For the given declaration creates/updates NGINX App Protect WAF policies on NGINX Management Suite +# making sure that they are in sync with what is defined in the JSON declaration +# Returns a tuple with two dictionaries: all_policy_names_and_versions, all_policy_active_names_and_uids +def provisionPolicies(nmsUrl: str, nmsUsername: str, nmsPassword: str, declaration: dict): + # NGINX App Protect policies - each policy supports multiple tagged versions + + # Policy names and all tag/uid pairs + # {'prod-policy': [{'tag': 'v1', 'uid': 'ebcf9c7e-0930-450d-8108-7cad30e59661'}, + # {'tag': 'v2', 'uid': 'd18c2eb7-814e-4e4d-90fc-54014eef199e'}], + # 'staging-policy': [{'tag': 'block', 'uid': '9794faa7-5b6c-4ce5-9e68-946f04766bb4'}, + # {'tag': 'xss-ok', 'uid': '7b4b850a-ff9e-42a0-85d0-850171474224'}]} + all_policy_names_and_versions = {} + + # Policy names and active tag uids + # { 'prod-policy': 'ebcf9c7e-0930-450d-8108-7cad30e59661', + # 'staging-policy': '7b4b850a-ff9e-42a0-85d0-850171474224' } + all_policy_active_names_and_uids = {} + + for p in declaration['output']['nms']['policies']: + policy_name = p['name'] + if policy_name: + policy_active_tag = p['active_tag'] + + # Iterates over all NGINX App Protect policies + if p['type'] == 'app_protect': + # Iterates over all policy versions + for policyVersion in p['versions']: + status, policyBody = v4_1.GitOps.getObjectFromRepo(policyVersion['contents']) + + if status != 200: + return JSONResponse( + status_code=422, + content={"code": status, + "details": policyBody['content']} + ) + + # Create the NGINX App Protect policy on NMS + r = __definePolicyOnNMS__( + nmsUrl=nmsUrl, nmsUsername=nmsUsername, nmsPassword=nmsPassword, + policyName=policy_name, + policyDisplayName=policyVersion['displayName'], + policyDescription=policyVersion['description'], + policyJson=policyBody['content'] + ) + + # Check for errors creating NGINX App Protect policy + if r.status_code != 201: + return JSONResponse( + status_code=r.status_code, + content={"code": r.status_code, "details": json.loads(r.text)} + ) + else: + # Policy was created, retrieve metadata.uid for each policy version + if policy_name not in all_policy_names_and_versions: + all_policy_names_and_versions[policy_name] = [] + + # Stores the policy version + uid = json.loads(r.text)['metadata']['uid'] + tag = policyVersion['tag'] + + if policy_active_tag == tag: + all_policy_active_names_and_uids[policy_name] = uid + + all_policy_names_and_versions[policy_name].append({'tag': tag, 'uid': uid}) + + return all_policy_names_and_versions, all_policy_active_names_and_uids + + +# Publish a NGINX App Protect WAF policy making it active +# activePolicyUids is a dict { "policy_name": "active_uid", [...] } +# Return True if at least one policy was enabled, False otherwise +def makePolicyActive(nmsUrl: str, nmsUsername: str, nmsPassword: str, activePolicyUids: dict, instanceGroupUid: str): + doWeHavePolicies = False + + for policyName in activePolicyUids: + body = { + "publications": [ + { + "policyContent": { + "name": f'{policyName}', + "uid": f'{activePolicyUids[policyName]}' + }, + "instanceGroups": [ + f'{instanceGroupUid}' + ] + } + ] + } + + doWeHavePolicies = True + r = requests.post(url=f'{nmsUrl}/api/platform/v1/security/publish', auth=(nmsUsername, nmsPassword), + data=json.dumps(body), headers={'Content-Type': 'application/json'}, verify=False) + + return doWeHavePolicies + + +# For the given declaration creates/updates NGINX App Protect WAF policies on NGINX Management Suite +# making sure that they are in sync with what is defined in the JSON declaration +# Returns a tuple: status, response payload +def cleanPolicyLeftovers(nmsUrl: str, nmsUsername: str, nmsPassword: str, currentPolicies: dict): + # Fetch all policies currently defined on the control plane + allNMSPolicies = __getAllPolicies__(nmsUrl=nmsUrl, nmsUsername=nmsUsername, nmsPassword=nmsPassword) + allNMSPoliciesJson = json.loads(allNMSPolicies.text) + + # Build a list of all uids for policies currently available on the control plane whose names match + # currentPolicies (policies that have just been pushed to data plane) + allUidsOnNMS = [] + for p in allNMSPoliciesJson['items']: + if p['metadata']['name'] in currentPolicies: + allUidsOnNMS.append(p['metadata']['uid']) + + allCurrentPoliciesUIDs = [] + for policyName in currentPolicies: + if policyName: + for tag in currentPolicies[policyName]: + allCurrentPoliciesUIDs.append(tag['uid']) + + uidsToRemove = list(set(allUidsOnNMS) - set(allCurrentPoliciesUIDs)) + + for uid in uidsToRemove: + __deletePolicy__(nmsUrl=nmsUrl, nmsUsername=nmsUsername, nmsPassword=nmsPassword, policyUid=uid) + + return diff --git a/src/v4_1/NIMUtils.py b/src/v4_1/NIMUtils.py new file mode 100644 index 0000000..8472ba7 --- /dev/null +++ b/src/v4_1/NIMUtils.py @@ -0,0 +1,30 @@ +""" +NGINX Instance Manager support functions +""" + +import requests +import json + +import v4_1.GitOps + +from fastapi.responses import Response, JSONResponse + + +# Fetch an instance group UID from NGINX Instance Manager +# Return None if not found +def getNIMInstanceGroupUid(nmsUrl: str, nmsUsername: str, nmsPassword: str, instanceGroupName: str): + # Retrieve instance group uid + ig = requests.get(url=f'{nmsUrl}/api/platform/v1/instance-groups', auth=(nmsUsername, nmsPassword), + verify=False) + + if ig.status_code != 200: + return None + + # Get the instance group id + igUid = None + igJson = json.loads(ig.text) + for i in igJson['items']: + if i['name'] == instanceGroupName: + igUid = i['uid'] + + return igUid diff --git a/src/v4_1/OpenAPIParser.py b/src/v4_1/OpenAPIParser.py new file mode 100644 index 0000000..0e4e355 --- /dev/null +++ b/src/v4_1/OpenAPIParser.py @@ -0,0 +1,71 @@ +""" +OpenAPI schema parser support functions +""" + +import json + +class OpenAPIParser: + httpMethods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH'] + + def __init__(self, openAPISchema): + self.openAPISchema = openAPISchema + + def version(self): + if 'openapi' in self.openAPISchema: + return self.openAPISchema['openapi'] + elif 'swagger' in self.openAPISchema: + return self.openAPISchema['swagger'] + + return None + + def info(self): + return self.openAPISchema['info'] + + def servers(self): + self.allServers = [] + + # Loop over OpenAPI schema servers + if 'servers' in self.openAPISchema: + for server in self.openAPISchema['servers']: + urlName = server['url'] + self.s = {} + self.s['url'] = urlName + + if 'description' in server: + self.s['description'] = server['description'] + + self.allServers.append(self.s) + + return self.allServers + + def paths(self): + self.allPaths = [] + + # Loop over OpenAPI schema paths + if 'paths' in self.openAPISchema: + for path in self.openAPISchema['paths'].keys(): + #print(f"- {path}") + self.p = {} + self.p['path'] = path + self.p['methods'] = [] + + # Loop over path HTTP methods found in schema + for method in self.openAPISchema['paths'][path].keys(): + methodInfo = self.openAPISchema['paths'][path][method] + + if method.upper() in self.httpMethods: + #print(f" - {method} - {methodInfo['description'] if 'description' in methodInfo else ''}") + self.m = {} + self.m['method'] = method + self.m['details'] = {} + + if 'description' in methodInfo and methodInfo['description']: + self.m['details']['description'] = methodInfo['description'] + if 'operationId' in methodInfo and methodInfo['operationId']: + self.m['details']['operationId'] = methodInfo['operationId'] + + self.p['methods'].append(self.m) + + self.allPaths.append(self.p) + + return self.allPaths \ No newline at end of file diff --git a/templates/v4.1/apigateway.tmpl b/templates/v4.1/apigateway.tmpl new file mode 100644 index 0000000..9a0bd2a --- /dev/null +++ b/templates/v4.1/apigateway.tmpl @@ -0,0 +1,110 @@ +{% if declaration.servers %} + {# --- OpenAPI schema contains server details --- #} + {% if declaration.servers[0].url.lower().startswith('http://') or declaration.servers[0].url.lower().startswith('https://') %} + {# --- OpenAPI schema contains a full server URL --- #} + {% set destination_server = declaration.servers[0].url %} + {% else %} + {# --- OpenAPI schema contains a server URI --- #} + {% set destination_server = declaration.location.apigateway.api_gateway.server_url + declaration.servers[0].url %} + {% endif %} +{% else %} + {# --- OpenAPI schema contains no server details --- #} + {% set destination_server = declaration.location.apigateway.api_gateway.server_url %} +{% endif %} + +# API Gateway: {{ declaration.info.title }} {{ declaration.info.version }} +# OpenAPI version: {{ declaration.version }} +# Base URI: {{ declaration.location.uri }} +# Strip base URI: {{ declaration.location.apigateway.api_gateway.strip_uri }} +# Destination server: {{ destination_server }} + +{% if declaration.paths -%} +{% for path in declaration.paths %} +location {% if '{' not in path.path %}={% else %}~{% endif %} {{ declaration.location.uri }}{{ path.path | regex_replace('{(.*?)}','(.*)') }} { + {% for method in path.methods -%} + # {{ method.method|upper }} - operationId: {{ method.details.operationId }} + {% endfor -%} + {% set method_names = path.methods|map(attribute='method')|list %} + + {% if declaration.location.apigateway.log.access %}access_log {{ declaration.location.apigateway.log.access }} main;{% endif %} + + {% if declaration.location.apigateway.log.error %}error_log {{ declaration.location.apigateway.log.error }};{% endif %} + + + limit_except {{ method_names|join(' ')|upper }} { deny all; } + + {# --- Rate limiting start --- #} + {%- for rl in declaration.location.apigateway.rate_limit -%} + {%- set enforceRL = namespace(toBeEnforced = False) -%} + {%- if rl.enforceOnPaths == False -%} + {%- set enforceRL.toBeEnforced = True -%} + {%- endif -%} + {%- for rlPath in rl.paths -%} + {%- if path.path == rlPath -%} + {%- if rl.enforceOnPaths == True -%} + {%- set enforceRL.toBeEnforced = True -%} + {%- else -%} + {%- set enforceRL.toBeEnforced = False -%} + {%- endif -%} + {%- endif -%} + {%- endfor -%} + + {%- if enforceRL.toBeEnforced == True -%} + {%- if rl.profile %}limit_req zone={{ rl.profile }}{% if rl.burst %} burst={{ rl.burst }}{% endif %}{% if rl.delay == 0 %} nodelay;{% else %} delay={{ rl.delay }};{% endif %}{% endif %} + + {% if rl.httpcode %}limit_req_status {{ rl.httpcode }};{% endif %} + {%- endif -%} + {%- endfor -%} + + {# --- Rate limiting end --- #} + + + {# --- Authentication start --- #} + {%- if declaration.location.apigateway.authentication -%} + {%- set enforceAuth = namespace(toBeEnforced = False) -%} + {%- if declaration.location.apigateway.authentication.enforceOnPaths == False -%} + {%- set enforceAuth.toBeEnforced = True -%} + {%- endif -%} + {%- for authPath in declaration.location.apigateway.authentication.paths -%} + {%- if path.path == authPath -%} + {%- if declaration.location.apigateway.authentication.enforceOnPaths == True -%} + {%- set enforceAuth.toBeEnforced = True -%} + {%- else -%} + {%- set enforceRL.toBeEnforced = False -%} + {%- endif -%} + {%- endif -%} + {%- endfor -%} + + {# --- Client authentication --- #} + {%- if enforceAuth.toBeEnforced == True -%} + {%- if declaration.location.apigateway.authentication and declaration.location.apigateway.authentication.client -%} + {%- for clientAuthProfile in declaration.location.apigateway.authentication.client -%} + include "{{ ncgconfig.nms.auth_client_dir }}/{{ clientAuthProfile.profile | replace(" ", "_") }}.conf"; + {% endfor -%} + {%- endif -%} + {%- endif -%} + + {%- endif %} + + {# --- Authentication end --- #} + + {% if declaration.location.apigateway.api_gateway.strip_uri -%} + rewrite ^{{ declaration.location.uri }}/(.*)$ /$1 break; + {% endif %} + + {% if declaration.location.apigateway.api_gateway.server_url -%} + proxy_set_header Host {{ declaration.location.apigateway.api_gateway.server_url.split('://')[1].split('/')[0] }}; + {% endif -%} + + proxy_pass {{ destination_server }}$uri; +} + +{% endfor %} + +{% if declaration.location.apigateway.developer_portal.enabled == True -%} +location = {{ declaration.location.uri }}{{ declaration.location.apigateway.developer_portal.uri }} { + rewrite ^{{ declaration.location.uri }}/(.*)$ /$1 break; + root {{ ncgconfig.nms.devportal_dir }}; +} +{% endif %} +{% endif %} diff --git a/templates/v4.1/auth/client/jwks.tmpl b/templates/v4.1/auth/client/jwks.tmpl new file mode 100644 index 0000000..aea3a35 --- /dev/null +++ b/templates/v4.1/auth/client/jwks.tmpl @@ -0,0 +1,11 @@ +location = /_auth/jwt/{{ authprofile.name | replace(" ", "_") }}/_jwks_uri { + internal; + + {% if authprofile.jwt.key.startswith('http://') or authprofile.jwt.key.startswith('https://') -%} + proxy_method GET; + proxy_pass {{ authprofile.jwt.key }}; + {% else -%} + return 200 '{{ authprofile.jwt.key }}'; + {%- endif %} + +} diff --git a/templates/v4.1/auth/client/jwt.tmpl b/templates/v4.1/auth/client/jwt.tmpl new file mode 100644 index 0000000..4aef347 --- /dev/null +++ b/templates/v4.1/auth/client/jwt.tmpl @@ -0,0 +1,4 @@ +auth_jwt "{{ authprofile.jwt.realm }}"; +auth_jwt_type {{ authprofile.jwt.jwt_type }}; +auth_jwt_key_request /_auth/jwt/{{ authprofile.name | replace(" ", "_") }}/_jwks_uri; +auth_jwt_key_cache {{ authprofile.jwt.cachetime }}; \ No newline at end of file diff --git a/templates/v4.1/auth/server/token.tmpl b/templates/v4.1/auth/server/token.tmpl new file mode 100644 index 0000000..862630b --- /dev/null +++ b/templates/v4.1/auth/server/token.tmpl @@ -0,0 +1,5 @@ +{% if authprofile.token.type == "bearer" %} +proxy_set_header Authorization "Bearer {{ authprofile.token.token }}"; +{% elif authprofile.token.type == "header" %} +proxy_set_header {{ authprofile.token.location }} "{{ authprofile.token.token }}"; +{% endif %} \ No newline at end of file diff --git a/templates/v4.1/configmap.tmpl b/templates/v4.1/configmap.tmpl new file mode 100644 index 0000000..29c3973 --- /dev/null +++ b/templates/v4.1/configmap.tmpl @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ name }} + {% if namespace -%} + namespace: {{ namespace }} + {% endif %} + +data: + {{ filename }}: |- +{% filter indent(width = 4) %} + {{ nginxconfig }} +{% endfilter %} \ No newline at end of file diff --git a/templates/v4.1/http.tmpl b/templates/v4.1/http.tmpl new file mode 100644 index 0000000..4fb3331 --- /dev/null +++ b/templates/v4.1/http.tmpl @@ -0,0 +1,274 @@ +# NGINX configuration file - HTTP servers - generated by https://github.com/f5devcentral/NGINX-Declarative-API + +{# --- Maps section --- #} + +{% if declaration.maps %} +{% for m in declaration.maps %} + +map {{ m.match }} {{ m.variable }} { + {% for e in m.entries %} + {%- if e.keymatch|lower == "exact" %}{% endif -%} + {%- if e.keymatch|lower == "regex" %} ~^ {%- endif -%} + {%- if e.keymatch|lower == "iregex" %} ~*^ {%- endif -%} + {{ e.key }} {{ e.value }}; + {% endfor -%} + +} +{% endfor %} +{% endif %} + +{# --- Snippets section --- #} +{% if declaration.snippet and declaration.snippet.content %}{{ declaration.snippet.content | b64decode }}{% endif %} + + +{# --- Upstreams section --- #} +{% if declaration.upstreams %} +{% for u in declaration.upstreams %} +{% if u.name %} +{% if u.origin %} +upstream {{ u.name }} { + zone {{ u.name }} 64k; + {% for o in u.origin -%} + server {{ o.server }}{% if o.weight %} weight={{ o.weight }}{% endif %}{% if o.max_fails %} max_fails={{ o.max_fails }}{% endif %}{% if o.fail_timeout %} fail_timeout={{ o.fail_timeout }}{% endif %}{% if o.max_conns %} max_conns={{ o.max_conns }}{% endif %}{% if o.slow_start %} slow_start={{ o.slow_start }}{% endif %}{% if o.backup and o.backup == True %} backup{% endif %}; + {% endfor %} + + {% if u.sticky and u.sticky.cookie and u.sticky.expires and u.sticky.domain and u.sticky.path -%} + sticky cookie {{ u.sticky.cookie }}{% if u.sticky.expires %} expires={{ u.sticky.expires }}{% endif %}{% if u.sticky.domain %} domain={{ u.sticky.domain }}{% endif %}{% if u.sticky.path %} path={{ u.sticky.path }}{% endif %}; + {% endif -%} + + {% if u.snippet and u.snippet.content %}{{ u.snippet.content | b64decode }}{% endif %} + +} +{% endif %} +{% endif %} +{% endfor %} +{% endif %} + +{# --- Rate limit section --- #} + +{% if declaration.rate_limit %} +{% for rl in declaration.rate_limit %} +limit_req_zone {{ rl.key }} zone={{ rl.name }}:{{ rl.size }} rate={{ rl.rate }}; +{% endfor %} +{% endif %} + +{# --- Server section for NGINX Plus API --- #} + +{% if declaration.nginx_plus_api %} +{% if declaration.nginx_plus_api.listen %} +server { + listen {{ declaration.nginx_plus_api.listen }}; + + location /api { + {% if declaration.nginx_plus_api.write == True %}api write=on;{% else %}api write=off;{% endif %} + + {% if declaration.nginx_plus_api.allow_acl -%} + allow {{ declaration.nginx_plus_api.allow_acl }}; + deny all; + {% else %} + allow all; + {% endif %} + + } + + location / { + root /usr/share/nginx/html; + index dashboard.html; + } +} +{% endif %} +{% endif %} + +{# --- Server section --- #} + +{% for s in declaration.servers %} +server { + # {{ s.name }} + {# --- Listen section start --- #} + {%- if s.listen -%} + {% if s.listen.address %} + + listen {{ s.listen.address }}{% if s.listen.tls and s.listen.tls.certificate %} ssl{% endif %}; + {% if s.listen.http2 and s.listen.http2 == True -%}http2 on;{% endif -%} + {%- endif %} + + {# --- TLS section start --- #} + {%- if s.listen.tls -%} + + {%- if s.listen.tls.certificate -%} + ssl_certificate {{ ncgconfig.nms.certs_dir }}/{{ s.listen.tls.certificate }}.crt; + {% endif -%} + {%- if s.listen.tls.key -%} + ssl_certificate_key {{ ncgconfig.nms.certs_dir }}/{{ s.listen.tls.key }}.key; + {% endif -%} + {% if s.listen.tls.ciphers -%} + ssl_ciphers {{ s.listen.tls.ciphers }}; + {% endif -%} + {% if s.listen.tls.protocols -%} + ssl_protocols{% for p in s.listen.tls.protocols %} {{ p }}{% endfor %}; + {% endif -%} + {% if s.listen.tls.trusted_ca_certificates -%} + ssl_trusted_certificate {{ ncgconfig.nms.certs_dir }}/{{ s.listen.tls.trusted_ca_certificates }}.crt; + {% endif %} + + {# --- mTLS section --- #} + {%- if s.listen.tls.mtls -%} + {%- if s.listen.tls.mtls.enabled|lower != "off" -%} + ssl_verify_client {{ s.listen.tls.mtls.enabled }}; + {% endif %} + {% if s.listen.tls.mtls.client_certificates -%} + ssl_client_certificate {{ ncgconfig.nms.certs_dir }}/{{ s.listen.tls.mtls.client_certificates }}.crt; + {% endif %} + {% endif %} + + {# --- OCSP section start --- #} + {%- if s.listen.tls.ocsp and s.listen.tls.ocsp.enabled|lower != "off" -%} + ssl_ocsp {{ s.listen.tls.ocsp.enabled }}; + {% if s.listen.tls.ocsp.responder -%} + ssl_ocsp_responder {{ s.listen.tls.ocsp.responder }}; + {% endif %} + {% endif %} + {# --- OCSP section end --- #} + + {# --- TLS stapling section start --- #} + {%- if s.listen.tls.stapling and s.listen.tls.stapling.enabled == True -%} + ssl_stapling on; + ssl_stapling_verify {% if s.listen.tls.stapling.verify == True %}on{% else %}off{% endif %}; + {% if s.listen.tls.stapling.responder -%} + ssl_stapling_responder {{ s.listen.tls.stapling.responder }}; + + {% endif -%} + {%- endif %} + {# --- TLS stapling section end --- #} + + {%- endif %} + {# --- TLS section end --- #} + + {%- endif %} + {# --- Listen section end --- #} + + {% if s.names -%} + server_name{% for svrname in s.names %} {{ svrname }}{% endfor -%}; + status_zone {{ s.names[0] }}; + proxy_set_header Host $host; + proxy_ssl_server_name on; + {% endif -%} + + {% if s.resolver -%} + resolver {{ s.resolver }}; + {% endif -%} + + {# --- Server NGINX App Protect WAF section start --- #} + + {% if s.app_protect -%} + {% if s.app_protect.enabled == True -%} + app_protect_enable on; + {% endif -%} + {% if s.app_protect.policy -%} + app_protect_policy_file {{ ncgconfig.nms.nap_policies_dir_pum }}/{{ s.app_protect.policy }}.tgz; + {% endif -%} + {% if s.app_protect.log -%} + {% if s.app_protect.log.enabled == True -%} + app_protect_security_log_enable on; + {% if s.app_protect.log.profile_name -%} + app_protect_security_log "{{ ncgconfig.nms.nap_logformats_dir_pum }}/{{ s.app_protect.log.profile_name }}.tgz" syslog:server={{ s.app_protect.log.destination }}; + {% endif -%} + {% endif %} + {% endif %} + {% endif %} + {# --- Server NGINX App Protect WAF section end --- #} + + {% if s.log.access %}access_log {{ s.log.access }} main;{% endif %} + + {% if s.log.error %}error_log {{ s.log.error }};{% endif %} + + + {% filter indent(width=4) %} +{% if s.snippet and s.snippet.content %}{{ s.snippet.content | b64decode }}{% endif %} + {% endfilter %} + + {# --- Server location section start --- #} + {% for loc in s.locations %} + + location + {%- if loc.urimatch -%} + {# location URI match types: prefix (default), exact (=), casesens_regex (~), caseinsens_regex (~*), best_nonregex (^~) #} + {%- if loc.urimatch|lower == "prefix" %} {% endif %} + {%- if loc.urimatch|lower == "exact" %} = {% endif %} + {%- if loc.urimatch|lower == "regex" %} ~ {% endif %} + {%- if loc.urimatch|lower == "iregex" %} ~* {% endif %} + {%- if loc.urimatch|lower == "best" %} ^~ {% endif %} + {%- endif -%} + {{ loc.uri }} { + {% if loc.authentication and loc.authentication.server and loc.authentication.server[0].profile -%} + include "{{ ncgconfig.nms.auth_server_dir }}/{{ loc.authentication.server[0].profile | replace(" ", "_") }}.conf"; + {% endif %} + + {% if loc.upstream %}proxy_pass {{ loc.upstream }};{% endif %} + + {% if loc.log.access %}access_log {{ loc.log.access }} main;{% endif %} + + {% if loc.log.error %}error_log {{ loc.log.error }};{% endif %} + + {# --- Active healthchecks --- #} + + {% if loc.health_check -%} + {% if loc.health_check.enabled == True -%} + health_check{% if loc.health_check.uri %} uri={{ loc.health_check.uri }}{% endif %}{% if loc.health_check.interval %} interval={{ loc.health_check.interval }}{% endif %}{% if loc.health_check.fails %} fails={{ loc.health_check.fails }}{% endif %}{% if loc.health_check.passes %} passes={{ loc.health_check.passes }}{% endif %}; + {% endif %} + {% endif %} + + {# --- Rate limiting --- #} + + {% if loc.rate_limit -%} + {% if loc.rate_limit.profile %}limit_req zone={{ loc.rate_limit.profile }}{% if loc.rate_limit.burst %} burst={{ loc.rate_limit.burst }}{% endif %}{% if loc.rate_limit.delay == 0 %} nodelay;{% else %} delay={{ loc.rate_limit.delay }};{% endif %} + + {% if loc.rate_limit.httpcode %}limit_req_status {{ loc.rate_limit.httpcode }};{% endif %}{% endif %} + {% endif %} + + {# --- Client authentication --- #} + {%- if loc.authentication and loc.authentication.client -%} + {%- for clientAuthProfile in loc.authentication.client -%} + include "{{ ncgconfig.nms.auth_client_dir }}/{{ clientAuthProfile.profile | replace(" ", "_") }}.conf"; + {% endfor -%} + {%- endif -%} + + {# --- Location NGINX App Protect WAF --- #} + + {% if loc.app_protect -%} + {% if loc.app_protect.enabled == True -%} + app_protect_enable on; + {% endif -%} + {% if loc.app_protect.policy -%} + app_protect_policy_file {{ ncgconfig.nms.nap_policies_dir_pum }}/{{ loc.app_protect.policy }}.tgz; + {% endif %} + {% if loc.app_protect.log -%} + {% if loc.app_protect.log.enabled == True -%} + app_protect_security_log_enable on; + {% if loc.app_protect.log.profile_name -%} + app_protect_security_log "{{ ncgconfig.nms.nap_logformats_dir_pum }}/{{ loc.app_protect.log.profile_name }}.tgz" syslog:server={{ loc.app_protect.log.destination }}; + {% endif %} + {% endif %} + {% endif %} + {% endif %} + + {# --- Location snipptes --- #} + + {% if loc.snippet and loc.snippet.content %}{{ loc.snippet.content | b64decode }}{% endif %} + + } + {% endfor %} + + {# --- JWT authentication JWKS endpoints --- #} + {%- if declaration.authentication and declaration.authentication.client -%} + {%- for clientAuthProfile in declaration.authentication.client -%} + {%- if clientAuthProfile.type == "jwt" -%} + include "{{ ncgconfig.nms.auth_client_dir }}/jwks_{{ clientAuthProfile.name | replace(" ", "_") }}.conf"; + {% endif -%} + {%- endfor -%} + {%- endif -%} + +{%- if s.listen.address -%} +{%- endif -%} +} +{% endfor -%} \ No newline at end of file diff --git a/templates/v4.1/logformat.tmpl b/templates/v4.1/logformat.tmpl new file mode 100644 index 0000000..db9863e --- /dev/null +++ b/templates/v4.1/logformat.tmpl @@ -0,0 +1,12 @@ +{ + "filter": { + "request_type": "{{ log.type }}" + }, + + "content": { + "format": "{{ log.format }}", + "format_string": "{{ log.format_string }}", + "max_request_size": "{{ log.max_request_size }}", + "max_message_size": "{{ log.max_message_size }}" + } +} diff --git a/templates/v4.1/nginx-conf/mime.types b/templates/v4.1/nginx-conf/mime.types new file mode 100644 index 0000000..d4e08df --- /dev/null +++ b/templates/v4.1/nginx-conf/mime.types @@ -0,0 +1,97 @@ +types { + text/html html htm shtml; + text/css css; + text/xml xml; + image/gif gif; + image/jpeg jpeg jpg; + application/javascript js; + application/atom+xml atom; + application/rss+xml rss; + + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + + image/png png; + image/svg+xml svg svgz; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/webp webp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + + font/woff woff; + font/woff2 woff2; + + application/java-archive jar war ear; + application/json json; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.apple.mpegurl m3u8; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/vnd.ms-excel xls; + application/vnd.ms-fontobject eot; + application/vnd.ms-powerpoint ppt; + application/vnd.oasis.opendocument.graphics odg; + application/vnd.oasis.opendocument.presentation odp; + application/vnd.oasis.opendocument.spreadsheet ods; + application/vnd.oasis.opendocument.text odt; + application/vnd.openxmlformats-officedocument.presentationml.presentation + pptx; + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + xlsx; + application/vnd.openxmlformats-officedocument.wordprocessingml.document + docx; + application/vnd.wap.wmlc wmlc; + application/wasm wasm; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/xspf+xml xspf; + application/zip zip; + + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream iso img; + application/octet-stream msi msp msm; + + audio/midi mid midi kar; + audio/mpeg mp3; + audio/ogg ogg; + audio/x-m4a m4a; + audio/x-realaudio ra; + + video/3gpp 3gpp 3gp; + video/mp2t ts; + video/mp4 mp4; + video/mpeg mpeg mpg; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-m4v m4v; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; +} \ No newline at end of file diff --git a/templates/v4.1/nginx-conf/nginx.conf b/templates/v4.1/nginx-conf/nginx.conf new file mode 100644 index 0000000..6ee69c9 --- /dev/null +++ b/templates/v4.1/nginx-conf/nginx.conf @@ -0,0 +1,40 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + +{% for m in nginxconf.modules %} +load_module modules/{{m}}.so; +{% endfor %} + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + keepalive_timeout 65; + #gzip on; + include /etc/nginx/conf.d/*.conf; +} + + +# TCP/UDP proxy and load balancing block +stream { + log_format stream-main '$remote_addr [$time_local] ' + '$protocol $status $bytes_sent $bytes_received ' + '$session_time "$ssl_preread_server_name"'; + #access_log /dev/stdout stream-main; + include /etc/nginx/stream-conf.d/*.conf; +} \ No newline at end of file diff --git a/templates/v4.1/stream.tmpl b/templates/v4.1/stream.tmpl new file mode 100644 index 0000000..3ae64f0 --- /dev/null +++ b/templates/v4.1/stream.tmpl @@ -0,0 +1,66 @@ +# NGINX configuration file - Stream servers - generated by https://github.com/f5devcentral/NGINX-Declarative-API + +{# --- Upstreams section --- #} + +{% if declaration.upstreams %} +{% for u in declaration.upstreams %} +{% if u.name %} +{% if u.origin %} +upstream {{ u.name }} { + zone {{ u.name }} 64k; + {% for o in u.origin -%} + server {{ o.server }}{% if o.weight %} weight={{ o.weight }}{% endif %}{% if o.max_fails %} max_fails={{ o.max_fails }}{% endif %}{% if o.fail_timeout %} fail_timeout={{ o.fail_timeout }}{% endif %}{% if o.max_conns %} max_conns={{ o.max_conns }}{% endif %}{% if o.slow_start %} slow_start={{ o.slow_start }}{% endif %}{% if o.backup and o.backup == True %} backup{% endif %}; + {% endfor %} + + {% if u.snippet and u.snippet.content %}{{ u.snippet.content }}{% endif %} + +} +{% endif %} +{% endif %} +{% endfor %} +{% endif %} + + +{# --- Stream server section --- #} + +{% for s in declaration.servers %} + {%- if s.listen %} + {% if s.listen.address %} + +server { + listen {{ s.listen.address }}{% if s.listen.protocol == "udp" %} {{ s.listen.protocol }}{% endif %}; + status_zone {{ s.name }}; + {% endif -%} + {% endif -%} + + + {# --- TLS section --- #} + {%- if s.listen.tls -%} + {%- if s.listen.tls.certificate -%} + ssl_certificate {{ ncgconfig.nms.certs_dir }}/{{ s.listen.tls.certificate }}.crt; + {% endif -%} + {%- if s.listen.tls.key -%} + ssl_certificate_key {{ ncgconfig.nms.certs_dir }}/{{ s.listen.tls.key }}.key; + {% endif -%} + {% if s.listen.tls.ciphers -%} + ssl_ciphers {{ s.listen.tls.ciphers }}; + {% endif -%} + {% if s.listen.tls.protocols -%} + ssl_protocols{% for p in s.listen.tls.protocols %} {{ p }}{% endfor %}; + {% endif %} + {% endif %} + + {% if s.upstream -%} + proxy_pass {{ s.upstream }}; + {% endif %} + + {% if s.snippet and s.snippet.content %}{{ s.snippet.content | b64decode }}{% endif %} + + {%- if s.listen %} + {%- if s.listen.address %} + +} + {% endif -%} + {% endif -%} + +{%- endfor %}