Skip to content

Commit

Permalink
NGINX Declarative API 4.2.3 (#44)
Browse files Browse the repository at this point in the history
* 20240311-01 Commit

* 20240311-02 Commit - added regexp checks for names

* 20240311-02 Commit - added regexp checks for names

* 20240327 - Refactored API Gateway auxfiles

* 20240327-01 commit
Refactored API Gateway configuration files
Regexp check for object descriptions
  • Loading branch information
fabriziofiorucci authored Mar 27, 2024
1 parent 327de21 commit 912854e
Show file tree
Hide file tree
Showing 35 changed files with 236 additions and 2,049 deletions.
39 changes: 39 additions & 0 deletions contrib/gitops-examples/v4.2/nap-policy-xss-allowed.json
Original file line number Diff line number Diff line change
@@ -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
}
]
}
}
23 changes: 23 additions & 0 deletions contrib/gitops-examples/v4.2/nap-policy-xss-blocked.json
Original file line number Diff line number Diff line change
@@ -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
}
]
}
}
92 changes: 73 additions & 19 deletions contrib/postman/NGINX Declarative API.postman_collection.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions etc/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ uri = "/v1/devportal"
[nms]
config_dir = '/etc/nginx'
certs_dir = '/etc/nginx/ssl'
apigw_dir = '/etc/nginx/apigateway'
devportal_dir = '/etc/nginx/devportal'
auth_client_dir = '/etc/nginx/authn/client'
auth_server_dir = '/etc/nginx/authn/server'
Expand Down
24 changes: 13 additions & 11 deletions src/V4_2_CreateConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pickle
import time
import uuid
import hashlib
from datetime import datetime
from urllib.parse import urlparse

Expand Down Expand Up @@ -274,8 +275,6 @@ def createconfig(declaration: ConfigDeclaration, apiversion: str, runfromautosyn
# Parse HTTP servers
d_servers = v4_2.MiscUtils.getDictKey(d, 'declaration.http.servers')
if d_servers is not None:
apiGatewaySnippet = ''

for server in d_servers:
serverSnippet = ''

Expand Down Expand Up @@ -377,8 +376,16 @@ def createconfig(declaration: ConfigDeclaration, apiversion: str, runfromautosyn
"content": f"invalid server authentication profile [{openApiAuthProfile[0]['profile']}] for OpenAPI schema [{loc['apigateway']['openapi_schema']['content']}]"}}}

status, apiGatewayConfigDeclaration = v4_2.APIGateway.createAPIGateway(locationDeclaration = loc, authProfiles = d['declaration']['http']['authentication'])
else:
apiGatewayConfigDeclaration = ''

# API Gateway configuration template rendering
if apiGatewayConfigDeclaration:
apiGatewaySnippet = j2_env.get_template(NcgConfig.config['templates']['apigwconf']).render(
declaration=apiGatewayConfigDeclaration, ncgconfig=NcgConfig.config)
apiGatewaySnippetb64 = base64.b64encode(bytes(apiGatewaySnippet, 'utf-8')).decode('utf-8')

newAuxFile = {'contents': apiGatewaySnippetb64, 'name': NcgConfig.config['nms']['apigw_dir'] +
loc['uri'] + ".conf" }
auxFiles['files'].append(newAuxFile)

# 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:
Expand Down Expand Up @@ -409,12 +416,7 @@ def createconfig(declaration: ConfigDeclaration, apiversion: str, runfromautosyn
"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')
server['snippet']['content'] = base64.b64encode(bytes(serverSnippet, 'utf-8')).decode('utf-8')

if 'layer4' in d['declaration']:
# Check Layer4/stream upstreams validity
Expand Down Expand Up @@ -922,4 +924,4 @@ def get_declaration(configUid: str):
if cfg is None:
return 404, ""

return 200, pickle.loads(cfg).dict()
return 200, pickle.loads(cfg).dict()
82 changes: 78 additions & 4 deletions src/V4_2_NginxConfigDeclaration.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""
JSON declaration format
JSON declaration structure
"""

from __future__ import annotations

from typing import List, Optional

from pydantic import BaseModel, Extra, model_validator

import re

# Regexp to check names
alphanumRegexp = '^[a-zA-Z0-9\ \-\_]+$'

class OutputConfigMap(BaseModel, extra="forbid"):
name: str = "nginx-config"
Expand Down Expand Up @@ -290,7 +292,7 @@ class AuthClientJWT(BaseModel, extra="forbid"):
def check_type(self) -> 'AuthClientJWT':
jwt_type, key = self.jwt_type, self.key

if not key.strip() :
if not key.strip():
raise ValueError(f"Invalid: JWT key must not be empty")

valid = ['signed', 'encrypted', 'nested']
Expand Down Expand Up @@ -478,13 +480,31 @@ class Server(BaseModel, extra="forbid"):
authentication: Optional[LocationAuth] = {}
authorization: Optional[AuthorizationProfileReference] = {}

@model_validator(mode='after')
def check_type(self) -> 'Server':
name = self.name

if not re.search(alphanumRegexp,name):
raise ValueError(f"Invalid name [{name}] should match regexp {alphanumRegexp}")

return self


class L4Server(BaseModel, extra="forbid"):
name: str
listen: Optional[ListenL4] = {}
upstream: Optional[str] = ""
snippet: Optional[ObjectFromSourceOfTruth] = {}

@model_validator(mode='after')
def check_type(self) -> 'L4Server':
name = self.name

if not re.search(alphanumRegexp,name):
raise ValueError(f"Invalid name [{name}] should match regexp {alphanumRegexp}")

return self


class Sticky(BaseModel, extra="forbid"):
cookie: str = ""
Expand Down Expand Up @@ -519,12 +539,30 @@ class Upstream(BaseModel, extra="forbid"):
sticky: Optional[Sticky] = {}
snippet: Optional[ObjectFromSourceOfTruth] = {}

@model_validator(mode='after')
def check_type(self) -> 'Upstream':
name = self.name

if not re.search(alphanumRegexp,name):
raise ValueError(f"Invalid name [{name}] should match regexp {alphanumRegexp}")

return self


class L4Upstream(BaseModel, extra="forbid"):
name: str
origin: Optional[List[L4Origin]] = []
snippet: Optional[ObjectFromSourceOfTruth] = {}

@model_validator(mode='after')
def check_type(self) -> 'L4Upstream':
name = self.name

if not re.search(alphanumRegexp,name):
raise ValueError(f"Invalid name [{name}] should match regexp {alphanumRegexp}")

return self


class ValidItem(BaseModel, extra="forbid"):
codes: Optional[List[int]] = [200]
Expand All @@ -537,13 +575,31 @@ class CachingItem(BaseModel, extra="forbid"):
size: Optional[str] = "10m"
valid: Optional[List[ValidItem]] = []

@model_validator(mode='after')
def check_type(self) -> 'CachingItem':
name = self.name

if not re.search(alphanumRegexp,name):
raise ValueError(f"Invalid name [{name}] should match regexp {alphanumRegexp}")

return self


class RateLimitItem(BaseModel, extra="forbid"):
name: str
key: str
size: Optional[str] = ""
rate: Optional[str] = ""

@model_validator(mode='after')
def check_type(self) -> 'RateLimitItem':
name = self.name

if not re.search(alphanumRegexp,name):
raise ValueError(f"Invalid name [{name}] should match regexp {alphanumRegexp}")

return self


class NginxPlusApi(BaseModel, extra="forbid"):
write: Optional[bool] = False
Expand Down Expand Up @@ -593,6 +649,9 @@ def check_type(self) -> 'Authentication_Client':
if _type not in valid:
raise ValueError(f"Invalid client authentication type [{_type}] for profile [{name}] must be one of {str(valid)}")

if not re.search(alphanumRegexp,name):
raise ValueError(f"Invalid name [{name}] should match regexp {alphanumRegexp}")

return self


Expand All @@ -610,6 +669,9 @@ def check_type(self) -> 'Authentication_Server':
if _type not in valid:
raise ValueError(f"Invalid server authentication type [{_type}] for profile [{name}] must be one of {str(valid)}")

if not re.search(alphanumRegexp,name):
raise ValueError(f"Invalid name [{name}] should match regexp {alphanumRegexp}")

return self


Expand All @@ -632,12 +694,24 @@ def check_type(self) -> 'Authorization':
if _type not in valid:
raise ValueError(f"Invalid authorization type [{_type}] for profile [{name}] must be one of {str(valid)}")

if not re.search(alphanumRegexp,name):
raise ValueError(f"Invalid name [{name}] should match regexp {alphanumRegexp}")

return self

class NjsFile(BaseModel, extra="forbid"):
name: str
file: ObjectFromSourceOfTruth

@model_validator(mode='after')
def check_type(self) -> 'NjsFile':
name = self.name

if not re.search(alphanumRegexp,name):
raise ValueError(f"Invalid name [{name}] should match regexp {alphanumRegexp}")

return self


class Http(BaseModel, extra="forbid"):
servers: Optional[List[Server]] = []
Expand Down
34 changes: 0 additions & 34 deletions src/v4_0/APIGateway.py
Original file line number Diff line number Diff line change
@@ -1,34 +0,0 @@
"""
API Gateway support functions
"""

import json
import base64

import v4_0.GitOps
import v4_0.MiscUtils
from v4_0.OpenAPIParser import OpenAPIParser


# Builds the declarative JSON for the API Gateway configuration
# Return a tuple: status, description. If status = 200 things were successful
def createAPIGateway(locationDeclaration: dict):
apiGwDeclaration = {}

if locationDeclaration['apigateway']['openapi_schema']:
status, apiSchemaString = v4_0.GitOps.getObjectFromRepo(content=locationDeclaration['apigateway']['openapi_schema'], base64Encode=False)

if v4_0.MiscUtils.yaml_or_json(apiSchemaString) == 'yaml':
# YAML to JSON conversion
apiSchemaString = v4_0.MiscUtils.yaml_to_json(apiSchemaString)

apiSchema = OpenAPIParser(json.loads(apiSchemaString))

apiGwDeclaration = {}
apiGwDeclaration['location'] = locationDeclaration
apiGwDeclaration['info'] = apiSchema.info()
apiGwDeclaration['servers'] = apiSchema.servers()
apiGwDeclaration['paths'] = apiSchema.paths()
apiGwDeclaration['version'] = apiSchema.version()

return 200, apiGwDeclaration
Loading

0 comments on commit 912854e

Please sign in to comment.