Skip to content

Commit 912854e

Browse files
NGINX Declarative API 4.2.3 (#44)
* 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
1 parent 327de21 commit 912854e

35 files changed

+236
-2049
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"policy": {
3+
"name": "prod-policy",
4+
"template": {
5+
"name": "POLICY_TEMPLATE_NGINX_BASE"
6+
},
7+
"applicationLanguage": "utf-8",
8+
"enforcementMode": "blocking",
9+
"signature-sets": [
10+
{
11+
"name": "All Signatures",
12+
"block": true,
13+
"alarm": true
14+
}
15+
],
16+
"signatures": [
17+
{
18+
"signatureId": 200001834,
19+
"enabled": false
20+
},
21+
{
22+
"signatureId": 200001475,
23+
"enabled": false
24+
},
25+
{
26+
"signatureId": 200000098,
27+
"enabled": false
28+
},
29+
{
30+
"signatureId": 200001088,
31+
"enabled": false
32+
},
33+
{
34+
"signatureId": 200101609,
35+
"enabled": false
36+
}
37+
]
38+
}
39+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"policy": {
3+
"name": "prod-policy",
4+
"template": {
5+
"name": "POLICY_TEMPLATE_NGINX_BASE"
6+
},
7+
"applicationLanguage": "utf-8",
8+
"enforcementMode": "blocking",
9+
"signature-sets": [
10+
{
11+
"name": "All Signatures",
12+
"block": true,
13+
"alarm": true
14+
}
15+
],
16+
"signatures": [
17+
{
18+
"signatureId": 200001834,
19+
"enabled": false
20+
}
21+
]
22+
}
23+
}

contrib/postman/NGINX Declarative API.postman_collection.json

Lines changed: 73 additions & 19 deletions
Large diffs are not rendered by default.

etc/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ uri = "/v1/devportal"
4242
[nms]
4343
config_dir = '/etc/nginx'
4444
certs_dir = '/etc/nginx/ssl'
45+
apigw_dir = '/etc/nginx/apigateway'
4546
devportal_dir = '/etc/nginx/devportal'
4647
auth_client_dir = '/etc/nginx/authn/client'
4748
auth_server_dir = '/etc/nginx/authn/server'

src/V4_2_CreateConfig.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import pickle
88
import time
99
import uuid
10+
import hashlib
1011
from datetime import datetime
1112
from urllib.parse import urlparse
1213

@@ -274,8 +275,6 @@ def createconfig(declaration: ConfigDeclaration, apiversion: str, runfromautosyn
274275
# Parse HTTP servers
275276
d_servers = v4_2.MiscUtils.getDictKey(d, 'declaration.http.servers')
276277
if d_servers is not None:
277-
apiGatewaySnippet = ''
278-
279278
for server in d_servers:
280279
serverSnippet = ''
281280

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

379378
status, apiGatewayConfigDeclaration = v4_2.APIGateway.createAPIGateway(locationDeclaration = loc, authProfiles = d['declaration']['http']['authentication'])
380-
else:
381-
apiGatewayConfigDeclaration = ''
379+
380+
# API Gateway configuration template rendering
381+
if apiGatewayConfigDeclaration:
382+
apiGatewaySnippet = j2_env.get_template(NcgConfig.config['templates']['apigwconf']).render(
383+
declaration=apiGatewayConfigDeclaration, ncgconfig=NcgConfig.config)
384+
apiGatewaySnippetb64 = base64.b64encode(bytes(apiGatewaySnippet, 'utf-8')).decode('utf-8')
385+
386+
newAuxFile = {'contents': apiGatewaySnippetb64, 'name': NcgConfig.config['nms']['apigw_dir'] +
387+
loc['uri'] + ".conf" }
388+
auxFiles['files'].append(newAuxFile)
382389

383390
# API Gateway Developer portal provisioning
384391
if loc['apigateway'] and loc['apigateway']['developer_portal'] and 'enabled' in loc['apigateway']['developer_portal'] and loc['apigateway']['developer_portal']['enabled'] == True:
@@ -409,12 +416,7 @@ def createconfig(declaration: ConfigDeclaration, apiversion: str, runfromautosyn
409416
"content":
410417
f"invalid rate_limit profile [{loc['rate_limit']['profile']}]"}}}
411418

412-
# API Gateway configuration template rendering
413-
apiGatewaySnippet += j2_env.get_template(NcgConfig.config['templates']['apigwconf']).render(
414-
declaration=apiGatewayConfigDeclaration, ncgconfig=NcgConfig.config)\
415-
if apiGatewayConfigDeclaration else ''
416-
417-
server['snippet']['content'] = base64.b64encode(bytes(serverSnippet + apiGatewaySnippet, 'utf-8')).decode('utf-8')
419+
server['snippet']['content'] = base64.b64encode(bytes(serverSnippet, 'utf-8')).decode('utf-8')
418420

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

925-
return 200, pickle.loads(cfg).dict()
927+
return 200, pickle.loads(cfg).dict()

src/V4_2_NginxConfigDeclaration.py

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
"""
2-
JSON declaration format
2+
JSON declaration structure
33
"""
44

55
from __future__ import annotations
6-
76
from typing import List, Optional
8-
97
from pydantic import BaseModel, Extra, model_validator
108

9+
import re
10+
11+
# Regexp to check names
12+
alphanumRegexp = '^[a-zA-Z0-9\ \-\_]+$'
1113

1214
class OutputConfigMap(BaseModel, extra="forbid"):
1315
name: str = "nginx-config"
@@ -290,7 +292,7 @@ class AuthClientJWT(BaseModel, extra="forbid"):
290292
def check_type(self) -> 'AuthClientJWT':
291293
jwt_type, key = self.jwt_type, self.key
292294

293-
if not key.strip() :
295+
if not key.strip():
294296
raise ValueError(f"Invalid: JWT key must not be empty")
295297

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

483+
@model_validator(mode='after')
484+
def check_type(self) -> 'Server':
485+
name = self.name
486+
487+
if not re.search(alphanumRegexp,name):
488+
raise ValueError(f"Invalid name [{name}] should match regexp {alphanumRegexp}")
489+
490+
return self
491+
481492

482493
class L4Server(BaseModel, extra="forbid"):
483494
name: str
484495
listen: Optional[ListenL4] = {}
485496
upstream: Optional[str] = ""
486497
snippet: Optional[ObjectFromSourceOfTruth] = {}
487498

499+
@model_validator(mode='after')
500+
def check_type(self) -> 'L4Server':
501+
name = self.name
502+
503+
if not re.search(alphanumRegexp,name):
504+
raise ValueError(f"Invalid name [{name}] should match regexp {alphanumRegexp}")
505+
506+
return self
507+
488508

489509
class Sticky(BaseModel, extra="forbid"):
490510
cookie: str = ""
@@ -519,12 +539,30 @@ class Upstream(BaseModel, extra="forbid"):
519539
sticky: Optional[Sticky] = {}
520540
snippet: Optional[ObjectFromSourceOfTruth] = {}
521541

542+
@model_validator(mode='after')
543+
def check_type(self) -> 'Upstream':
544+
name = self.name
545+
546+
if not re.search(alphanumRegexp,name):
547+
raise ValueError(f"Invalid name [{name}] should match regexp {alphanumRegexp}")
548+
549+
return self
550+
522551

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

557+
@model_validator(mode='after')
558+
def check_type(self) -> 'L4Upstream':
559+
name = self.name
560+
561+
if not re.search(alphanumRegexp,name):
562+
raise ValueError(f"Invalid name [{name}] should match regexp {alphanumRegexp}")
563+
564+
return self
565+
528566

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

578+
@model_validator(mode='after')
579+
def check_type(self) -> 'CachingItem':
580+
name = self.name
581+
582+
if not re.search(alphanumRegexp,name):
583+
raise ValueError(f"Invalid name [{name}] should match regexp {alphanumRegexp}")
584+
585+
return self
586+
540587

541588
class RateLimitItem(BaseModel, extra="forbid"):
542589
name: str
543590
key: str
544591
size: Optional[str] = ""
545592
rate: Optional[str] = ""
546593

594+
@model_validator(mode='after')
595+
def check_type(self) -> 'RateLimitItem':
596+
name = self.name
597+
598+
if not re.search(alphanumRegexp,name):
599+
raise ValueError(f"Invalid name [{name}] should match regexp {alphanumRegexp}")
600+
601+
return self
602+
547603

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

652+
if not re.search(alphanumRegexp,name):
653+
raise ValueError(f"Invalid name [{name}] should match regexp {alphanumRegexp}")
654+
596655
return self
597656

598657

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

672+
if not re.search(alphanumRegexp,name):
673+
raise ValueError(f"Invalid name [{name}] should match regexp {alphanumRegexp}")
674+
613675
return self
614676

615677

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

697+
if not re.search(alphanumRegexp,name):
698+
raise ValueError(f"Invalid name [{name}] should match regexp {alphanumRegexp}")
699+
635700
return self
636701

637702
class NjsFile(BaseModel, extra="forbid"):
638703
name: str
639704
file: ObjectFromSourceOfTruth
640705

706+
@model_validator(mode='after')
707+
def check_type(self) -> 'NjsFile':
708+
name = self.name
709+
710+
if not re.search(alphanumRegexp,name):
711+
raise ValueError(f"Invalid name [{name}] should match regexp {alphanumRegexp}")
712+
713+
return self
714+
641715

642716
class Http(BaseModel, extra="forbid"):
643717
servers: Optional[List[Server]] = []

src/v4_0/APIGateway.py

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +0,0 @@
1-
"""
2-
API Gateway support functions
3-
"""
4-
5-
import json
6-
import base64
7-
8-
import v4_0.GitOps
9-
import v4_0.MiscUtils
10-
from v4_0.OpenAPIParser import OpenAPIParser
11-
12-
13-
# Builds the declarative JSON for the API Gateway configuration
14-
# Return a tuple: status, description. If status = 200 things were successful
15-
def createAPIGateway(locationDeclaration: dict):
16-
apiGwDeclaration = {}
17-
18-
if locationDeclaration['apigateway']['openapi_schema']:
19-
status, apiSchemaString = v4_0.GitOps.getObjectFromRepo(content=locationDeclaration['apigateway']['openapi_schema'], base64Encode=False)
20-
21-
if v4_0.MiscUtils.yaml_or_json(apiSchemaString) == 'yaml':
22-
# YAML to JSON conversion
23-
apiSchemaString = v4_0.MiscUtils.yaml_to_json(apiSchemaString)
24-
25-
apiSchema = OpenAPIParser(json.loads(apiSchemaString))
26-
27-
apiGwDeclaration = {}
28-
apiGwDeclaration['location'] = locationDeclaration
29-
apiGwDeclaration['info'] = apiSchema.info()
30-
apiGwDeclaration['servers'] = apiSchema.servers()
31-
apiGwDeclaration['paths'] = apiSchema.paths()
32-
apiGwDeclaration['version'] = apiSchema.version()
33-
34-
return 200, apiGwDeclaration

0 commit comments

Comments
 (0)