Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
max-parallel: 1
matrix:
python-version: [3.9]
python-version: [ 3.9 ]
steps:
- name: Checkout source
uses: actions/checkout@v2
Expand Down Expand Up @@ -138,7 +138,7 @@ jobs:
strategy:
max-parallel: 6
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ]
fail-fast: false
steps:
- name: Checkout source
Expand Down Expand Up @@ -180,7 +180,7 @@ jobs:
strategy:
max-parallel: 4
matrix:
go-version: ["1.22", "1.23", "1.24", "1.25"]
go-version: [ "1.22", "1.23", "1.24", "1.25" ]
fail-fast: false
steps:
- name: Checkout source
Expand All @@ -201,6 +201,10 @@ jobs:
- name: Setup Go and protoc
run: |
python do.py setup_ext ${{ matrix.go-version }}

- name: Set Toolchain to 1.25.0
run: echo "GOTOOLCHAIN=go1.25.0" >> $GITHUB_ENV

- name: Run artifact generation
run: |
python do.py generate go
Expand All @@ -221,9 +225,7 @@ jobs:
name: python_package
- name: Display structure of downloaded files
run: ls -R
- uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}

- name: Run go tests
run: |
python do.py testgo
Expand Down Expand Up @@ -260,7 +262,7 @@ jobs:
fail: true

publish_python_package:
needs: [python_tests, go_tests, proto_yml_generation_test]
needs: [ python_tests, go_tests, proto_yml_generation_test ]
runs-on: ubuntu-latest
steps:
- name: Checkout source
Expand Down
7 changes: 5 additions & 2 deletions do.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ def testgo():
def go_lint():
try:

version = "1.64.2"
version = "1.64.8"

pkg = "go install"
if on_linux() or on_macos():
Expand Down Expand Up @@ -471,7 +471,8 @@ def run(commands, capture_output=False):
cmd = cmd.encode("utf-8", errors="ignore")
subprocess.check_call(cmd, shell=True, stdout=fd)
return flush_output(fd, logfile)
except Exception:
except Exception as e:
print(f"Error: {e}")
flush_output(fd, logfile)
sys.exit(1)

Expand All @@ -484,6 +485,7 @@ def getstatusoutput(command):


def build(sdk="all", env_setup=None):
os.environ["GOTOOLCHAIN"] = "go1.25.0"
print("\nSTEP 1: Set up virtual environment")

if env_setup is not None and env_setup.lower() == "clean":
Expand Down Expand Up @@ -512,6 +514,7 @@ def build(sdk="all", env_setup=None):
)
init()
run([py() + " -m pip install ."])

print("\nSTEP 3: Generating Python and Go SDKs\n")
generate(sdk=sdk, cicd="True")
if sdk == "python" or sdk == "all":
Expand Down
13 changes: 11 additions & 2 deletions openapiart/bundler.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ def _read_file(self, base_dir, filename):
def _process_yaml_object(self, base_dir, yobject):
for key, value in yobject.items():
if (
key in ["openapi", "info", "servers"]
key in ["openapi", "info", "servers", "security"]
and key not in self._content.keys()
):
self._content[key] = value
Expand All @@ -536,9 +536,16 @@ def _process_yaml_object(self, base_dir, yobject):
self._content[key][sub_key] = value[sub_key]
elif key == "components":
if key not in self._content.keys():
self._content[key] = {"responses": {}, "schemas": {}}
self._content[key] = {
"responses": {},
"schemas": {},
"securitySchemes": {},
}
self._validate_names("^[+a-zA-Z0-9_]+$", "schemas", value)
self._validate_names("^[+a-zA-Z0-9_]+$", "responses", value)
self._validate_names(
"^[+a-zA-Z0-9_]+$", "securitySchemes", value
)
self._check_nested_components(value)
self._resolve_refs(base_dir, yobject)

Expand Down Expand Up @@ -571,6 +578,8 @@ def _validate_names(self, regex, components_key, components):
self._content["components"][components_key][key] = value

def _check_nested_components(self, components):
if "schemas" not in components:
return
objects = components["schemas"]
errors = []
for component_name, component_value in objects.items():
Expand Down
129 changes: 129 additions & 0 deletions openapiart/tests/security/security.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
security:
- bearerAuth: []
- apiKeyAuth: []

paths:
/config:
post:
tags: ["Security"]
operationId: set_config
description: Sets configuration resources.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Config"
responses:
"200":
x-field-uid: 1
description: "OK"
content:
application/octet-stream:
schema:
type: string
format: binary
default:
x-field-uid: 2
description: "Unexpected error"
content:
application/json:
schema:
$ref: "#/components/schemas/Error"

/status:
get:
tags: ["Security"]
operationId: get_status
description: Get status. Overrides root security to require oauth2 with a specific scope.
security:
- oauth2Auth:
- read:config
responses:
"200":
x-field-uid: 1
description: "OK"
content:
application/octet-stream:
schema:
type: string
format: binary
default:
x-field-uid: 2
description: "Unexpected error"
content:
application/json:
schema:
$ref: "#/components/schemas/Error"

/public:
get:
tags: ["Security"]
operationId: get_public
description: Public endpoint. Explicitly overrides root security with empty list.
security: []
responses:
"200":
x-field-uid: 1
description: "OK"
content:
application/octet-stream:
schema:
type: string
format: binary
default:
x-field-uid: 2
description: "Unexpected error"
content:
application/json:
schema:
$ref: "#/components/schemas/Error"

components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
apiKeyAuth:
type: apiKey
in: header
name: X-API-Key
oauth2Auth:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://example.com/oauth/authorize
tokenUrl: https://example.com/oauth/token
scopes:
read:config: Read configuration resources
write:config: Write configuration resources
openIdAuth:
type: openIdConnect
openIdConnectUrl: https://example.com/.well-known/openid-configuration
schemas:
Config:
description: Security test config object
type: object
properties:
name:
type: string
description: Name of the config
x-field-uid: 1
Error:
description: Error response generated while serving API request.
type: object
required:
- code
- errors
properties:
code:
description: Numeric status code based on underlying transport being used.
type: integer
x-field-uid: 1
errors:
description: List of error messages generated while serving API request.
type: array
items:
type: string
x-field-uid: 2
12 changes: 10 additions & 2 deletions openapiart/tests/test_error_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@ def create_openapi_artifacts(openapiart_class, sdk=None, file_name=None):


def str_compare(validte_str, entire_str, item):
return validte_str in entire_str and item in entire_str
normalized_entire = (
entire_str.replace("(", "").replace(")", "").replace("'", "")
)
normalized_validate = (
validte_str.replace("(", "").replace(")", "").replace("'", "")
)
return (
normalized_validate in normalized_entire and item in normalized_entire
)


def test_validate_response_default():
Expand Down Expand Up @@ -64,7 +72,7 @@ def test_error_for_missing_required():
file_name="./response/response_missing_required_in_error.yaml",
)
error_value = execinfo.value.args[0]
assert error_msg == error_value
assert str_compare(error_msg, error_value, "Error")


if __name__ == "__main__":
Expand Down
98 changes: 98 additions & 0 deletions openapiart/tests/test_security_definitions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import os
import yaml
import tempfile
import pytest
from openapiart.openapiart import OpenApiArt as openapiart_class


def create_openapi_artifacts(artifact_dir):
openapiart_class(
api_files=[
os.path.join(os.path.dirname(__file__), "./api/info.yaml"),
os.path.join(
os.path.dirname(__file__), "./security/security.yaml"
),
],
artifact_dir=artifact_dir,
extension_prefix="status",
proto_service="statusapi",
)


def test_security_definitions_preserved():
with tempfile.TemporaryDirectory() as artifact_dir:
create_openapi_artifacts(artifact_dir)

bundled_path = os.path.join(artifact_dir, "openapi.yaml")
with open(bundled_path) as f:
bundled = yaml.safe_load(f)

_assert_root_security(bundled)
_assert_security_schemes(bundled)
_assert_operation_level_security(bundled)


def _assert_root_security(bundled):
assert (
"security" in bundled
), "root-level 'security' key missing from bundled output"
security = bundled["security"]
assert isinstance(security, list)
assert {"bearerAuth": []} in security
assert {"apiKeyAuth": []} in security


def _assert_security_schemes(bundled):
assert "securitySchemes" in bundled.get(
"components", {}
), "'securitySchemes' missing from bundled components"
schemes = bundled["components"]["securitySchemes"]

# http bearer
assert "bearerAuth" in schemes
bearer = schemes["bearerAuth"]
assert bearer["type"] == "http"
assert bearer["scheme"] == "bearer"
assert bearer["bearerFormat"] == "JWT"

# apiKey
assert "apiKeyAuth" in schemes
apikey = schemes["apiKeyAuth"]
assert apikey["type"] == "apiKey"
assert apikey["in"] == "header"
assert apikey["name"] == "X-API-Key"

# oauth2 with authorizationCode flow
assert "oauth2Auth" in schemes
oauth2 = schemes["oauth2Auth"]
assert oauth2["type"] == "oauth2"
flow = oauth2["flows"]["authorizationCode"]
assert flow["authorizationUrl"] == "https://example.com/oauth/authorize"
assert flow["tokenUrl"] == "https://example.com/oauth/token"
assert "read:config" in flow["scopes"]
assert "write:config" in flow["scopes"]

# openIdConnect
assert "openIdAuth" in schemes
oidc = schemes["openIdAuth"]
assert oidc["type"] == "openIdConnect"
assert (
oidc["openIdConnectUrl"]
== "https://example.com/.well-known/openid-configuration"
)


def _assert_operation_level_security(bundled):
paths = bundled.get("paths", {})

# /status overrides root security with oauth2 + scope
status_security = paths["/status"]["get"]["security"]
assert isinstance(status_security, list)
assert len(status_security) == 1
assert "oauth2Auth" in status_security[0]
assert "read:config" in status_security[0]["oauth2Auth"]

# /public explicitly disables security with an empty list
public_security = paths["/public"]["get"]["security"]
assert isinstance(public_security, list)
assert len(public_security) == 0
4 changes: 3 additions & 1 deletion openapiart/tests/test_validate_x_field_pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ def create_openapi_artifacts(openapiart_class, sdk=None, file_name=None):


def str_compare(validte_str, entire_str):
return validte_str in entire_str
normalized_entire = entire_str.replace("(", "").replace(")", "")
normalized_validate = validte_str.replace("(", "").replace(")", "")
return normalized_validate in normalized_entire


def test_validate_pattern():
Expand Down
Loading