diff --git a/sdks/apigw-manager/Makefile b/sdks/apigw-manager/Makefile index 091f4a5..b11e057 100644 --- a/sdks/apigw-manager/Makefile +++ b/sdks/apigw-manager/Makefile @@ -22,7 +22,7 @@ poetry.lock: pyproject.toml .PHONY: requirements requirements: poetry.lock - poetry export -f requirements.txt --without-hashes --extras demo --with dev --output requirements_tox.txt + poetry export -f requirements.txt --without-hashes --extras demo --extras drf --with dev --output requirements_tox.txt README.rst: README.md m2r --overwrite README.md diff --git a/sdks/apigw-manager/poetry.lock b/sdks/apigw-manager/poetry.lock index 9731634..f60c6ab 100644 --- a/sdks/apigw-manager/poetry.lock +++ b/sdks/apigw-manager/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "appnope" @@ -85,6 +85,37 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] testing = ["pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] +[[package]] +name = "backports-zoneinfo" +version = "0.2.1" +description = "Backport of the standard library zoneinfo module" +optional = true +python-versions = ">=3.6" +files = [ + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, + {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, +] + +[package.dependencies] +importlib-resources = {version = "*", markers = "python_version < \"3.7\""} + +[package.extras] +tzdata = ["tzdata"] + [[package]] name = "bkapi-bk-apigateway" version = "1.0.11" @@ -438,6 +469,21 @@ develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.dev0)", "py docs = ["furo (>=2021.8.17b43,<2021.9.dev0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"] +[[package]] +name = "djangorestframework" +version = "3.15.1" +description = "Web APIs for Django, made easy." +optional = true +python-versions = ">=3.6" +files = [ + {file = "djangorestframework-3.15.1-py3-none-any.whl", hash = "sha256:3ccc0475bce968608cf30d07fb17d8e52d1d7fc8bfe779c905463200750cbca6"}, + {file = "djangorestframework-3.15.1.tar.gz", hash = "sha256:f88fad74183dfc7144b2756d0d2ac716ea5b4c7c9840995ac3bfd8ec034333c1"}, +] + +[package.dependencies] +"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} +django = ">=3.0" + [[package]] name = "docutils" version = "0.17.1" @@ -449,6 +495,30 @@ files = [ {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, ] +[[package]] +name = "drf-spectacular" +version = "0.27.1" +description = "Sane and flexible OpenAPI 3 schema generation for Django REST framework" +optional = true +python-versions = ">=3.6" +files = [ + {file = "drf-spectacular-0.27.1.tar.gz", hash = "sha256:452e0cff3c12ee057b897508a077562967b9e62717992eeec10e62dbbc7b5a33"}, + {file = "drf_spectacular-0.27.1-py3-none-any.whl", hash = "sha256:0a4cada4b7136a0bf17233476c066c511a048bc6a485ae2140326ac7ba4003b2"}, +] + +[package.dependencies] +Django = ">=2.2" +djangorestframework = ">=3.10.3" +inflection = ">=0.3.1" +jsonschema = ">=2.6.0" +PyYAML = ">=5.1" +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} +uritemplate = ">=2.0.0" + +[package.extras] +offline = ["drf-spectacular-sidecar"] +sidecar = ["drf-spectacular-sidecar"] + [[package]] name = "faker" version = "14.2.1" @@ -571,6 +641,17 @@ zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] testing = ["pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] +[[package]] +name = "inflection" +version = "0.5.1" +description = "A port of Ruby on Rails inflector to Python" +optional = true +python-versions = ">=3.5" +files = [ + {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, + {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, +] + [[package]] name = "iniconfig" version = "1.1.1" @@ -661,6 +742,28 @@ files = [ test = ["pytest", "pytest-asyncio", "pytest-trio", "testpath", "trio"] trio = ["async_generator", "trio"] +[[package]] +name = "jsonschema" +version = "3.2.0" +description = "An implementation of JSON Schema validation for Python" +optional = true +python-versions = "*" +files = [ + {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, + {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, +] + +[package.dependencies] +attrs = ">=17.4.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +pyrsistent = ">=0.14.0" +setuptools = "*" +six = ">=1.11.0" + +[package.extras] +format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] +format-nongpl = ["idna", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "webcolors"] + [[package]] name = "keyring" version = "23.0.1" @@ -1056,6 +1159,36 @@ files = [ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] +[[package]] +name = "pyrsistent" +version = "0.18.0" +description = "Persistent/Functional/Immutable data structures" +optional = true +python-versions = ">=3.6" +files = [ + {file = "pyrsistent-0.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-win32.whl", hash = "sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-win32.whl", hash = "sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-win_amd64.whl", hash = "sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2"}, + {file = "pyrsistent-0.18.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427"}, + {file = "pyrsistent-0.18.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef"}, + {file = "pyrsistent-0.18.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c"}, + {file = "pyrsistent-0.18.0-cp38-cp38-win32.whl", hash = "sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78"}, + {file = "pyrsistent-0.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b"}, + {file = "pyrsistent-0.18.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4"}, + {file = "pyrsistent-0.18.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680"}, + {file = "pyrsistent-0.18.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426"}, + {file = "pyrsistent-0.18.0-cp39-cp39-win32.whl", hash = "sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b"}, + {file = "pyrsistent-0.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea"}, + {file = "pyrsistent-0.18.0.tar.gz", hash = "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b"}, +] + [[package]] name = "pytest" version = "7.0.1" @@ -1576,6 +1709,17 @@ files = [ {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, ] +[[package]] +name = "uritemplate" +version = "4.1.1" +description = "Implementation of RFC 6570 URI Templates" +optional = true +python-versions = ">=3.6" +files = [ + {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, + {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, +] + [[package]] name = "urllib3" version = "1.26.6" @@ -1673,9 +1817,10 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=4.6)", "pytest-black ( cryptography = ["cryptography", "pyjwt"] demo = ["Django", "PyMySQL", "django-environ", "pyjwt"] django = ["Django", "pyjwt"] +drf = ["Django", "cryptography", "djangorestframework", "drf-spectacular", "pyjwt"] kubernetes = ["kubernetes"] [metadata] lock-version = "2.0" python-versions = "^3.6.1" -content-hash = "999bc9cab42810b11d9f453632a077ff7e599096d8aecb9d63ffe219e9026e1b" +content-hash = "a916ad81d2f43468caac4860eef1e735d1d93864617c848510bbfb4a28c126cd" diff --git a/sdks/apigw-manager/pyproject.toml b/sdks/apigw-manager/pyproject.toml index cccfdda..a0510fe 100644 --- a/sdks/apigw-manager/pyproject.toml +++ b/sdks/apigw-manager/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "apigw-manager" -version = "3.0.5" +version = "3.1.0" description = "The SDK for managing blueking gateway resource." readme = "README.md" authors = ["blueking "] @@ -29,9 +29,14 @@ cryptography = { version = ">=3.1.1", optional = true } packaging = { version = ">=20.4" } PyMySQL = { version = "^1.0.2", optional = true } kubernetes = { version = "*", optional = true } +# if want to upgrade this package, should upgrade python version first; 3.15.2 is not compatible with python 3.6/3.7 +djangorestframework = { version = "<=3.15.1", optional = true } +# if want to upgrade this package, should upgrade python version first; 0.27.2 is not compatible with python 3.6 +drf-spectacular = { version = "<=0.27.1", optional = true } [tool.poetry.extras] cryptography = ["cryptography", "pyjwt"] +drf = ["django", "cryptography", "pyjwt", "djangorestframework", "drf-spectacular"] django = ["django", "pyjwt"] demo = ["django-environ", "django", "PyMySQL", "pyjwt"] kubernetes = ["kubernetes"] diff --git a/sdks/apigw-manager/requirements_tox.txt b/sdks/apigw-manager/requirements_tox.txt index 0f686a7..5a2ef51 100644 --- a/sdks/apigw-manager/requirements_tox.txt +++ b/sdks/apigw-manager/requirements_tox.txt @@ -4,6 +4,7 @@ atomicwrites==1.4.0 ; python_full_version >= "3.6.1" and python_full_version < " attrs==21.2.0 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" backcall==0.2.0 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" backports-entry-points-selectable==1.1.0 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" +backports-zoneinfo==0.2.1 ; python_full_version >= "3.6.1" and python_version < "3.9" bkapi-bk-apigateway==1.0.11 ; python_full_version >= "3.6.1" and python_version < "4.0" bkapi-client-core==1.2.0 ; python_full_version >= "3.6.1" and python_version < "4.0" bleach==3.3.0 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" @@ -21,7 +22,9 @@ decorator==5.0.9 ; python_full_version >= "3.6.1" and python_full_version < "4.0 distlib==0.3.2 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" django-environ==0.8.1 ; python_full_version >= "3.6.1" and python_version < "4" django==3.2.12 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" +djangorestframework==3.15.1 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" docutils==0.17.1 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" +drf-spectacular==0.27.1 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" faker==14.2.1 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" filelock==3.0.12 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" future==0.18.2 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" @@ -30,11 +33,13 @@ identify==2.2.11 ; python_full_version >= "3.6.1" and python_full_version < "4.0 idna==3.2 ; python_full_version >= "3.6.1" and python_version < "4.0" importlib-metadata==4.6.1 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" importlib-resources==5.2.0 ; python_full_version >= "3.6.1" and python_version < "3.7" +inflection==0.5.1 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" iniconfig==1.1.1 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" ipython-genutils==0.2.0 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" ipython==7.16.1 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" jedi==0.17.2 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" jeepney==0.7.0 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" and sys_platform == "linux" +jsonschema==3.2.0 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" keyring==23.0.1 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" kubernetes==24.2.0 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" m2r==0.2.1 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" @@ -61,6 +66,7 @@ pygments==2.9.0 ; python_full_version >= "3.6.1" and python_full_version < "4.0. pyjwt==2.1.0 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" pymysql==1.0.2 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" pyparsing==2.4.7 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" +pyrsistent==0.18.0 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" pytest-cov==4.0.0 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" pytest-django==4.5.2 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" pytest-mock==3.6.1 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" @@ -77,7 +83,6 @@ requests==2.26.0 ; python_full_version >= "3.6.1" and python_version < "4.0" rfc3986==1.5.0 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" rich==12.0.1 ; python_version >= "3.7" and python_full_version < "3.8.0" rsa==4.9 ; python_full_version >= "3.6.1" and python_version < "4" -ruff==0.1.6 ; python_version >= "3.7" and python_full_version < "3.8.0" secretstorage==3.3.1 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" and sys_platform == "linux" setuptools==59.6.0 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" six==1.16.0 ; python_full_version >= "3.6.1" and python_version < "4.0" @@ -92,6 +97,7 @@ typed-ast==1.4.3 ; python_full_version >= "3.6.1" and python_version < "3.8" types-pymysql==1.1.0.1 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" types-pyyaml==6.0.12.9 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" typing-extensions==3.10.0.0 ; python_full_version >= "3.6.1" and python_version < "4.0" +uritemplate==4.1.1 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" urllib3==1.26.6 ; python_full_version >= "3.6.1" and python_version < "4" virtualenv==20.5.0 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" wcwidth==0.2.5 ; python_full_version >= "3.6.1" and python_full_version < "4.0.0" diff --git a/sdks/apigw-manager/src/apigw_manager/__init__.py b/sdks/apigw-manager/src/apigw_manager/__init__.py index bc79700..ce92420 100644 --- a/sdks/apigw-manager/src/apigw_manager/__init__.py +++ b/sdks/apigw-manager/src/apigw_manager/__init__.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- -""" - * TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. - * Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. - * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://opensource.org/licenses/MIT - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. -""" +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. diff --git a/sdks/apigw-manager/src/apigw_manager/drf/__init__.py b/sdks/apigw-manager/src/apigw_manager/drf/__init__.py new file mode 100644 index 0000000..ce92420 --- /dev/null +++ b/sdks/apigw-manager/src/apigw_manager/drf/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. diff --git a/sdks/apigw-manager/src/apigw_manager/drf/apps.py b/sdks/apigw-manager/src/apigw_manager/drf/apps.py new file mode 100644 index 0000000..f3b18c4 --- /dev/null +++ b/sdks/apigw-manager/src/apigw_manager/drf/apps.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +from django.apps import AppConfig + + +class DrfConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apigw_manager.drf" + + def ready(self): + # init the scheme + from . import scheme # noqa diff --git a/sdks/apigw-manager/src/apigw_manager/drf/authentication.py b/sdks/apigw-manager/src/apigw_manager/drf/authentication.py new file mode 100644 index 0000000..b7aa063 --- /dev/null +++ b/sdks/apigw-manager/src/apigw_manager/drf/authentication.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +import logging +from collections import namedtuple +from typing import ClassVar, Type + +from apigw_manager.apigw.providers import CachePublicKeyProvider, PublicKeyProvider +from apigw_manager.apigw.utils import get_configuration +from django.conf import settings +from django.contrib import auth +from django.utils.module_loading import import_string +from rest_framework.authentication import BaseAuthentication + +logger = logging.getLogger(__name__) + +App = namedtuple("App", ["bk_app_code", "verified"]) + + +class ApiGatewayJWTAuthentication(BaseAuthentication): + """the authentication inherit from BaseAuthentication of rest_framework + it will authenticate the request by the jwt token in the request header + + 1. get the public_key from the PublicKeyProvider(like CachePublicKeyProvider, will fetch the public key from db) + 2. verify the jwt token by the public_key + 3. get the app and user from jwt, and verify + """ + + JWT_KEY_NAME = "HTTP_X_BKAPI_JWT" + ALGORITHM = "RS512" + PUBLIC_KEY_PROVIDER_CLS: ClassVar[Type[PublicKeyProvider]] = CachePublicKeyProvider + + def __init__(self): + configuration = get_configuration() + jwt_provider_cls = import_string( + configuration.jwt_provider_cls or "apigw_manager.apigw.providers.DefaultJWTProvider" + ) + algorithm = getattr(settings, "APIGW_JWT_ALGORITHM", self.ALGORITHM) + allow_invalid_jwt_token = getattr(settings, "APIGW_ALLOW_INVALID_JWT_TOKEN", False) + + self.provider = jwt_provider_cls( + jwt_key_name=self.JWT_KEY_NAME, + default_gateway_name=configuration.gateway_name, + algorithm=algorithm, + allow_invalid_jwt_token=allow_invalid_jwt_token, + public_key_provider=self.PUBLIC_KEY_PROVIDER_CLS(default_gateway_name=configuration.gateway_name), + ) + + def authenticate(self, request): + """it will get jwt from the provider + then: + 1. assign request.app with jwt.app + 2. do the authenticate with jwt.user + """ + jwt = self.provider.provide(request) + if not jwt: + logger.error("[ApiGatewayJWTAuthentication] can not found jwt in request header") + return None + + request.jwt = jwt + request._dont_enforce_csrf_checks = True + + jwt_app = (jwt.payload.get("app") or {}).copy() + jwt_app.setdefault("bk_app_code", jwt_app.pop("app_code", None)) + + request.app = self.make_app(**jwt_app) + + jwt_user = (jwt.payload.get("user") or {}).copy() + jwt_user.setdefault("bk_username", jwt_user.pop("username", None)) + + user = self.get_user(request, gateway_name=jwt.gateway_name, **jwt_user) + logger.debug("[ApiGatewayJWTAuthentication] authenticate user: %s", user) + return (user, None) + + def authenticate_header(self, request): + return self.JWT_KEY_NAME + + def make_app(self, bk_app_code=None, verified=False, **jwt_app) -> App: + return App( + bk_app_code=bk_app_code, + verified=verified, + ) + + def get_user( + self, + request, + gateway_name=None, + bk_username=None, + verified=False, + **credentials, + ): + # 传递 gateway_name 参数的用途: + # 1. 来明确标识这个请求来自于网关 + # 2. 用户已经过认证,后端无需再认证 + # 3. 避免非预期调用激活对应后端使得用户认证被绕过 + return auth.authenticate( + request, + gateway_name=gateway_name, + bk_username=bk_username, + verified=verified, + **credentials, + ) diff --git a/sdks/apigw-manager/src/apigw_manager/drf/management/commands/data/definition.yaml b/sdks/apigw-manager/src/apigw_manager/drf/management/commands/data/definition.yaml new file mode 100644 index 0000000..aa30381 --- /dev/null +++ b/sdks/apigw-manager/src/apigw_manager/drf/management/commands/data/definition.yaml @@ -0,0 +1,61 @@ +# reference: https://github.com/TencentBlueKing/bkpaas-python-sdk/tree/master/sdks/apigw-manager +spec_version: 2 + +release: + version: "{{ settings.BK_APIGW_RELEASE_VERSION }}" + title: "{{ settings.BK_APIGW_RELEASE_TITLE }}" + comment: "{{ settings.BK_APIGW_RELEASE_COMMENT }}" + +apigateway: + description: "{{ settings.BK_APIGW_DESCRIPTION }}" + description_en: "{{ settings.BK_APIGW_DESCRIPTION_EN }}" + is_public: {{ settings.BK_APIGW_IS_PUBLIC }} + api_type: {{ settings.BK_APIGW_IS_OFFICIAL }} + maintainers: + {% if settings.BK_APIGW_MAINTAINERS %} + {% for maintainer in settings.BK_APIGW_MAINTAINERS %} + - "{{ maintainer }}" + {% endfor %} + {% else %} + - "admin" + {% endif %} + +stages: + - name: "{{ settings.BK_APIGW_DEFAULT_STAGE_NAME }}" + description: "{{ settings.BK_APIGW_DEFAULT_STAGE_DESCRIPTION }}" + description_en: "{{ settings.BK_APIGW_DEFAULT_STAGE_DESCRIPTION_EN }}" + {% if settings.BK_APIGW_DEFAULT_STAGE_BACKEND_SUBPATH %} + vars: + api_sub_path: {{ settings.BK_APIGW_DEFAULT_STAGE_BACKEND_SUBPATH }} + {% else %} + vars: {} + {% endif %} + backends: + - name: "default" + config: + timeout: 60 + loadbalance: "roundrobin" + hosts: + # 网关调用后端服务的默认域名或IP,不包含Path,比如:http://api.example.com + - host: "{{ settings.BK_APIGW_DEFAULT_STAGE_BACKEND_HOST }}" + weight: 100 + # TODO: support plugin_configs + {% if settings.BK_APIGW_DEFAULT_STAGE_PLUGIN_CONFIGS %} + plugin_configs: + {% for plugin_config in settings.BK_APIGW_DEFAULT_STAGE_PLUGIN_CONFIGS %} + - type: {{ plugin_config.type }} + yaml: |- + {{ plugin_config.yaml }} + {% endfor %} + {% endif %} + +{% if settings.BK_APIGW_GRANT_PERMISSION_DIMENSION_GATEWAY_APP_CODES %} +grant_permissions: + {% for app_code in settings.BK_APIGW_GRANT_PERMISSION_DIMENSION_GATEWAY_APP_CODES %} + - bk_app_code: "{{ app_code }}" + grant_dimension: "gateway" + {% endfor %} +{% endif %} + +related_apps: + - "{{ settings.BK_APP_CODE }}" \ No newline at end of file diff --git a/sdks/apigw-manager/src/apigw_manager/drf/management/commands/generate_definition_yaml.py b/sdks/apigw-manager/src/apigw_manager/drf/management/commands/generate_definition_yaml.py new file mode 100644 index 0000000..2d11435 --- /dev/null +++ b/sdks/apigw-manager/src/apigw_manager/drf/management/commands/generate_definition_yaml.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +""" +this command will generate the definition.yaml + +it will copy a template definition.yaml from apigw_manager into the project. And if the version of apigw_manager changes, +the template definition.yaml will be updated. +""" + +import shutil + +from django.conf import settings +from django.core.management.base import BaseCommand +from pathlib import Path + + +class Command(BaseCommand): + def handle(self, *args, **kwargs): + current_dir = Path(__file__).resolve().parent + source_file = current_dir / "data" / "definition.yaml" + + define_dir = Path(settings.BASE_DIR) + definition_path = define_dir / "definition.yaml" + + self.stdout.write(f"will generate {definition_path} from {source_file}") + shutil.copyfile(source_file, definition_path) + + self.stdout.write(f"generated {definition_path} from {source_file} success") diff --git a/sdks/apigw-manager/src/apigw_manager/drf/management/commands/generate_resources_yaml.py b/sdks/apigw-manager/src/apigw_manager/drf/management/commands/generate_resources_yaml.py new file mode 100644 index 0000000..c0cd8aa --- /dev/null +++ b/sdks/apigw-manager/src/apigw_manager/drf/management/commands/generate_resources_yaml.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +""" +this command will generate the resources.yaml from the drf_spectacular config of the apis under project + +if only want part of the apis: +1. add the `tags` in `@extend_schema` of each method in the views.py +2. call this command with tag, e.g. `python manage.py generate_resource_yaml.py --tag=foo --tag=bar` +""" + +from typing import List + +from django.conf import settings +from django.core.management.base import BaseCommand +from drf_spectacular.management.commands.spectacular import SchemaValidationError +from drf_spectacular.renderers import OpenApiYamlRenderer +from drf_spectacular.settings import spectacular_settings +from drf_spectacular.validation import validate_schema +from pathlib import Path + + +def post_process_only_keep_the_apis_with_specified_tags(tags: List) -> callable: + def only_keep_the_apis_with_specified_tags(result, generator, request, public): + paths = result.get("paths", None) + if not paths: + return result + + api_to_delete = [] + for uri, methods in paths.items(): + for method, info in methods.items(): + if not set(tags).intersection(info.get("tags", [])): + api_to_delete.append((uri, method)) + + for uri, method in api_to_delete: + del paths[uri][method] + + result["paths"] = {k: v for k, v in paths.items() if v} + return result + + return only_keep_the_apis_with_specified_tags + + +def post_process_inject_method_and_path(result, generator, request, public): + paths = result.get("paths", None) + if not paths: + return result + sub_path = settings.BK_APIGW_DEFAULT_STAGE_BACKEND_SUBPATH + + for uri, methods in paths.items(): + for method, info in methods.items(): + info["x-bk-apigateway-resource"]["backend"]["method"] = method + if not sub_path: + info["x-bk-apigateway-resource"]["backend"]["path"] = uri + else: + info["x-bk-apigateway-resource"]["backend"]["path"] = f"{sub_path}{uri}" + + return result + + +class Command(BaseCommand): + def add_arguments(self, parser): + parser.add_argument( + "--tag", + nargs="*", + help="if set only generate the specified tags api to resources.yaml", + ) + + def handle(self, *args, **kwargs): + define_dir = Path(settings.BASE_DIR) + resources_path = define_dir / "resources.yaml" + self.stdout.write(f"will generate {resources_path}") + + tags = kwargs.get("tag") + if tags: + self.stdout.write(f"get tags, will only use the apis with tags: {tags}") + spectacular_settings.POSTPROCESSING_HOOKS.append(post_process_only_keep_the_apis_with_specified_tags(tags)) + else: + self.stdout.write("no argument --tag, will use all apis under the project") + + self.stdout.write(f"process the project sub_path={settings.BK_APIGW_DEFAULT_STAGE_BACKEND_SUBPATH}") + spectacular_settings.POSTPROCESSING_HOOKS.append(post_process_inject_method_and_path) + + generator = spectacular_settings.DEFAULT_GENERATOR_CLASS() + renderer = OpenApiYamlRenderer() + schema = generator.get_schema(request=None, public=True) + + self.stdout.write("validate schema of all apis") + try: + validate_schema(schema) + self.stdout.write("schema validated") + except Exception as e: + self.stdout.write(f"schema validation failed: {str(e)}") + raise SchemaValidationError(e) + + self.stdout.write(f"render resources.yaml to {resources_path}") + output = renderer.render(schema, renderer_context={}) + with open(resources_path, "wb") as f: + f.write(output) diff --git a/sdks/apigw-manager/src/apigw_manager/drf/management/commands/sync_drf_apigateway.py b/sdks/apigw-manager/src/apigw_manager/drf/management/commands/sync_drf_apigateway.py new file mode 100644 index 0000000..cdeebe1 --- /dev/null +++ b/sdks/apigw-manager/src/apigw_manager/drf/management/commands/sync_drf_apigateway.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +""" +this command will sync the apis of current project to bk-apigateway + +the related files for syncing: +- config/settings.py (the settings values for the gateway) +- definition.yaml (generated by command `generate_definition_yaml`, the template, will be rendered with the settings) +- resources.yaml (generated by command `generate_resource_yaml`, the api definitions of the gateway) + +reference: https://github.com/TencentBlueKing/bkpaas-python-sdk/blob/master/sdks/apigw-manager/docs/sync-apigateway-with-django.md + +the steps of syncing +1. generate definition.yaml: `python manage.py generate_definition_yaml` +2. generate resources.yaml: `python manage.py generate_resources_yaml` +3. do the sync +""" + +from pathlib import Path +from django.conf import settings +from django.core.management import call_command +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + def handle(self, *args, **kwargs): + gateway_name = settings.BK_APIGW_NAME + + file_dir = Path(settings.BASE_DIR) + definition_file_path = file_dir / "definition.yaml" + resources_file_path = file_dir / "resources.yaml" + + self.stdout.write(f"call sync_apigw_config with definition: {definition_file_path}") + call_command( + "sync_apigw_config", + f"--gateway-name={gateway_name}", + f"--file={definition_file_path}", + ) + + self.stdout.write(f"call sync_apigw_stage with definition: {definition_file_path}") + call_command( + "sync_apigw_stage", + f"--gateway-name={gateway_name}", + f"--file={definition_file_path}", + ) + self.stdout.write(f"call sync_apigw_resources with resources: {resources_file_path}") + call_command( + "sync_apigw_resources", + f"--gateway-name={gateway_name}", + "--delete", + f"--file={resources_file_path}", + ) + + self.stdout.write( + f"call create_version_and_release_apigw with definition: {definition_file_path}, stage: {settings.BK_APIGW_DEFAULT_STAGE_NAME}" + ) + call_command( + "create_version_and_release_apigw", + f"--gateway-name={gateway_name}", + f"--file={definition_file_path}", + f"--stage={settings.BK_APIGW_DEFAULT_STAGE_NAME}", + ) + + self.stdout.write(f"call grant_apigw_permissions with definition: {definition_file_path}") + call_command( + "grant_apigw_permissions", + f"--gateway-name={gateway_name}", + f"--file={definition_file_path}", + ) + + self.stdout.write(f"call fetch_apigw_public_key {gateway_name}") + call_command("fetch_apigw_public_key", f"--gateway-name={gateway_name}") diff --git a/sdks/apigw-manager/src/apigw_manager/drf/migrations/__init__.py b/sdks/apigw-manager/src/apigw_manager/drf/migrations/__init__.py new file mode 100644 index 0000000..ce92420 --- /dev/null +++ b/sdks/apigw-manager/src/apigw_manager/drf/migrations/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. diff --git a/sdks/apigw-manager/src/apigw_manager/drf/permission.py b/sdks/apigw-manager/src/apigw_manager/drf/permission.py new file mode 100644 index 0000000..82a612e --- /dev/null +++ b/sdks/apigw-manager/src/apigw_manager/drf/permission.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +import logging + +from rest_framework import permissions + +logger = logging.getLogger(__name__) + + +class ApiGatewayPermission(permissions.BasePermission): + """this permission is used to check if the request is from apigateway + + if the view has `FROM_APIGW_EXEMPT` attribute, then the permission will be exempted + otherwise: + 1. check if the request has jwt, if not, return False + 2. check if the view has `app_verified_required` attribute, if yes, check if the request has app and app is verified + 3. check if the view has `user_verified_required` attribute, if yes, check if the request has user and user is authenticated + """ + + def has_permission(self, request, view): + exempt = getattr(view, "FROM_APIGW_EXEMPT", False) + if exempt: + return True + + if not hasattr(request, "jwt"): + logger.error( + "can not found jwt in request, " + "make sure ApiGatewayJWTAuthentication is config in REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES and incoming jwt is valid" + ) + return False + + logger.debug("request.jwt.payload: %s", request.jwt.payload) + + if getattr(view, "app_verified_required", False): + if not hasattr(request, "app"): + logger.error( + "can not found app in request, " + "make sure ApiGatewayJWTAppMiddleware is config in REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES and incoming jwt is valid" + ) + return False + + if not request.app.verified: + logger.error( + "found app in request but the app is not verified, " + "make sure ApiGatewayJWTAppMiddleware is config in REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES and incoming jwt is valid" + ) + return False + + if getattr(view, "user_verified_required", False): + if not hasattr(request, "user"): + logger.error( + "can not found user in request, " + "make sure ApiGatewayJWTAuthentication is config in REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES and incoming jwt is valid" + ) + return False + + if not request.user.is_authenticated: + logger.error( + "found user in request but the user is not authenticated, " + "make sure ApiGatewayJWTAuthentication is config in REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES and incoming jwt is valid" + ) + return False + + return True diff --git a/sdks/apigw-manager/src/apigw_manager/drf/scheme.py b/sdks/apigw-manager/src/apigw_manager/drf/scheme.py new file mode 100644 index 0000000..fab4991 --- /dev/null +++ b/sdks/apigw-manager/src/apigw_manager/drf/scheme.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +from drf_spectacular.extensions import OpenApiAuthenticationExtension + + +class ApiGatewayJWTAuthenticationScheme(OpenApiAuthenticationExtension): + target_class = "apigw_manager.drf.authentication.ApiGatewayJWTAuthentication" # full import path OR class ref + name = "ApiGatewayJWTAuthentication" # name used in the schema + + def get_security_definition(self, auto_schema): + return { + "type": "apiKey", + "in": "header", + "name": "X-BKAPI-JWT", + } diff --git a/sdks/apigw-manager/src/apigw_manager/drf/utils.py b/sdks/apigw-manager/src/apigw_manager/drf/utils.py new file mode 100644 index 0000000..c573e22 --- /dev/null +++ b/sdks/apigw-manager/src/apigw_manager/drf/utils.py @@ -0,0 +1,237 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云 - 蓝鲸 PaaS 平台 (BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +import os +import random +import string +import sys + +from typing import Dict, List, Optional + + +def gen_apigateway_resource_config( + is_public: bool = True, + allow_apply_permission: bool = True, + user_verified_required: bool = False, + app_verified_required: bool = True, + resource_permission_required: bool = True, + description_en: str = "", + plugin_configs: Optional[List[Dict]] = None, + match_subpath: bool = False, +) -> Dict[str, Dict[str, any]]: + """用于辅助生成 bk-apigateway 的资源配置 + Args: + is_public (bool, optional): 是否公开,不公开在文档中心/应用申请网关权限资源列表中不可见。默认 True + allow_apply_permission (bool, optional): 是否允许申请权限,不允许的话在应用申请网关权限资源列表中不可见。默认 True + user_verified_required (bool, optional): 是否开启用户认证 默认 False + app_verified_required (bool, optional): 是否开启应用认证。默认 True + resource_permission_required (bool, optional): 是否校验资源权限,是的话将会校验应用是否有调用这个资源的权限,前置条件:开启应用认证。默认 True + description_en (str, optional): 资源的英文描述。默认 "" + plugin_configs (Optional[List[Dict]], optional): 插件配置,类型为 List[Dict], 用于声明作用在这个资源上的插件,可以参考官方文档。默认 None. + match_subpath (bool, optional): 匹配所有子路径,默认为 False. 默认 False + Returns: + Dict[str, Dict[str, any]]: _description_ + """ + + # resource_permission_required need app_verified_required + if not app_verified_required: + resource_permission_required = False + + if not plugin_configs: + plugin_configs = [] + + return { + "x-bk-apigateway-resource": { + "isPublic": is_public, + "matchSubpath": match_subpath, + "backend": { + "name": "default", + # filled by post process + "method": "", + # filled by post process + "path": "", + "matchSubpath": match_subpath, + "timeout": 0, + }, + "pluginConfigs": plugin_configs, + "allowApplyPermission": allow_apply_permission, + "authConfig": { + "userVerifiedRequired": user_verified_required, + "appVerifiedRequired": app_verified_required, + "resourcePermissionRequired": resource_permission_required, + }, + "descriptionEn": description_en, + } + } + + +def get_logging_config_dict( + log_level: str, + is_local: bool, + log_dir: str, + app_code: str, +): + """用户生成蓝鲸 PaaS 运行时的 Django Logging 配置 + 来源于蓝鲸开发框架,以获取最大的兼容性 reference: https://github.com/TencentBlueKing/blueapps/blob/master/blueapps/conf/log.py + + Args: + log_level (str): 日志级别 + is_local (bool): 是否是本地开发,本地开发日志格式为文本格式,线上环境为 json 格式 + log_dir (str): 日志文件所在目录 + app_code (str): 应用 app_code + + Returns: + logging config dict + """ + log_class = "concurrent_log_handler.ConcurrentRotatingFileHandler" + + if is_local: + log_name_prefix = os.getenv("BKPAAS_LOG_NAME_PREFIX", app_code) + logging_format = { + "format": ( + "%(levelname)s [%(asctime)s] %(pathname)s " + "%(lineno)d %(funcName)s %(process)d %(thread)d " + "\n \t %(message)s \n" + ), + "datefmt": "%Y-%m-%d %H:%M:%S", + } + else: + rand_str = "".join(random.sample(string.ascii_letters + string.digits, 4)) + log_name_prefix = "{}-{}".format(os.getenv("BKPAAS_PROCESS_TYPE", "web"), rand_str) + + logging_format = { + "()": "pythonjsonlogger.jsonlogger.JsonFormatter", + "fmt": ( + "%(levelname)s %(asctime)s %(pathname)s %(lineno)d " "%(funcName)s %(process)d %(thread)d %(message)s" + ), + } + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + return { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": logging_format, + "simple": {"format": "%(levelname)s %(message)s"}, + }, + "handlers": { + "null": {"level": "DEBUG", "class": "logging.NullHandler"}, + "console": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "formatter": "simple", + }, + "root": { + "class": log_class, + "formatter": "verbose", + "filename": os.path.join(log_dir, "%s-django.log" % log_name_prefix), + "maxBytes": 1024 * 1024 * 10, + "backupCount": 5, + }, + "component": { + "class": log_class, + "formatter": "verbose", + "filename": os.path.join(log_dir, "%s-component.log" % log_name_prefix), + "maxBytes": 1024 * 1024 * 10, + "backupCount": 5, + }, + "mysql": { + "class": log_class, + "formatter": "verbose", + "filename": os.path.join(log_dir, "%s-mysql.log" % log_name_prefix), + "maxBytes": 1024 * 1024 * 10, + "backupCount": 5, + }, + "celery": { + "class": log_class, + "formatter": "verbose", + "filename": os.path.join(log_dir, "%s-celery.log" % log_name_prefix), + "maxBytes": 1024 * 1024 * 10, + "backupCount": 5, + }, + "apigw_manager": { + "class": log_class, + "formatter": "verbose", + "filename": os.path.join(log_dir, "%s-apigw_manager.log" % log_name_prefix), + "maxBytes": 1024 * 1024 * 10, + "backupCount": 5, + }, + }, + "loggers": { + "django": {"handlers": ["null"], "level": "INFO", "propagate": True}, + "django.server": { + "handlers": ["console"], + "level": log_level, + "propagate": True, + }, + "django.request": { + "handlers": ["root"], + "level": "ERROR", + "propagate": True, + }, + "django.db.backends": { + "handlers": ["mysql"], + "level": log_level, + "propagate": True, + }, + # the root logger ,用于整个 project 的 logger + "root": {"handlers": ["root"], "level": log_level, "propagate": True}, + # 组件调用日志 + "component": { + "handlers": ["component"], + "level": log_level, + "propagate": True, + }, + "celery": {"handlers": ["celery"], "level": log_level, "propagate": True}, + # other loggers... + # 普通 app 日志 + "app": {"handlers": ["root"], "level": log_level, "propagate": True}, + # 框架 + "apigw_manager": { + "handlers": ["apigw_manager"], + "level": log_level, + "propagate": True, + }, + }, + } + + +def get_default_database_config_dict(settings_module): + """用户生成蓝鲸 PaaS 运行时的 Django Database 配置 + 由于蓝鲸 PaaS 内外版本差异,数据库相关的环境变量有所不同,所以需要通过这个函数做版本差异兼容。 + 来源于蓝鲸开发框架,以获取最大的兼容性 reference: https://github.com/TencentBlueKing/blueapps/blob/master/blueapps/conf/database.py + + Args: + django settings locals() 配置 + + Returns: + database config dict + """ + if os.getenv("GCS_MYSQL_NAME") and os.getenv("MYSQL_NAME"): + db_prefix = settings_module.get("DB_PREFIX", "") + if not db_prefix: + raise EnvironmentError("no DB_PREFIX config while multiple " "databases found in environment") + elif os.getenv("GCS_MYSQL_NAME"): + db_prefix = "GCS_MYSQL" + elif os.getenv("MYSQL_NAME"): + db_prefix = "MYSQL" + else: + # 对应非 GCS_MYSQL 或 MYSQL 开头的情况,需开发者自行配置 + sys.stderr.write("DB_PREFIX config is not 'GCS_MYSQL' or 'MYSQL_NAME'\n") + return {} + return { + "ENGINE": "django.db.backends.mysql", + "NAME": os.environ["%s_NAME" % db_prefix], + "USER": os.environ["%s_USER" % db_prefix], + "PASSWORD": os.environ["%s_PASSWORD" % db_prefix], + "HOST": os.environ["%s_HOST" % db_prefix], + "PORT": os.environ["%s_PORT" % db_prefix], + "OPTIONS": {"isolation_level": "repeatable read"}, + } diff --git a/sdks/apigw-manager/src/apigw_manager/plugin/__init__.py b/sdks/apigw-manager/src/apigw_manager/plugin/__init__.py new file mode 100644 index 0000000..ce92420 --- /dev/null +++ b/sdks/apigw-manager/src/apigw_manager/plugin/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. diff --git a/sdks/apigw-manager/src/apigw_manager/plugin/config.py b/sdks/apigw-manager/src/apigw_manager/plugin/config.py new file mode 100644 index 0000000..7e0cb5e --- /dev/null +++ b/sdks/apigw-manager/src/apigw_manager/plugin/config.py @@ -0,0 +1,267 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云 - 蓝鲸 PaaS 平台 (BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +import ipaddress +from typing import Dict, List, Optional, Tuple + +from .utils import literal_unicode, yaml_dump, yaml_text_indent + + +def build_bk_header_rewrite(set: Dict[str, str], remove: List[str]) -> Dict[str, str]: + """generate bk-header-rewrite plugin config + + Args: + set (Dict[str, str]): the key-value pair to set header + remove (List[str]): the key list to remove header + + Raises: + ValueError: key {} can not contain ':' + + Returns: + { + "type": "bk-header-rewrite", + "yaml": "set:\n - key: key1\n value: value1\n - key: key2\n value: value2\nremove:\n - key1\n" + } + """ + set_data = [] + for k, v in set.items(): + if ":" in k: + raise ValueError(f"key {k} can not contain ':'") + set_data.append({"key": k, "value": v}) + + remove_data = [] + for k in remove: + if ":" in k: + raise ValueError(f"key {k} can not contain ':'") + remove_data.append({"key": k}) + + return { + "type": "bk-header-rewrite", + "yaml": yaml_dump( + { + "set": set_data, + "remove": remove_data, + } + ), + } + + +def build_bk_cors( + allow_origins: str = "*", + allow_origins_by_regex: Optional[List[str]] = None, + allow_methods: str = "*", + allow_headers: str = "*", + expose_headers: str = "*", + max_age: int = 86400, + allow_credential: bool = False, +): + """generate bk-cors plugin config + + Args: + allow_origins (str, optional): 允许跨域访问的 Origin. Defaults to "*". + - 格式为 scheme://host:port,示例如 https://example.com:8081。如果你有多个 Origin,请使用 , 分隔。 + - 当 allow_credential 为 false 时,可以使用 * 来表示允许所有 Origin 通过。 + - 你也可以在启用了 allow_credential 后使用 ** 强制允许所有 Origin 均通过,但请注意这样存在安全隐患。 + - allow_origins、allow_origins_by_regex 只能一个有效 + allow_origins_by_regex (Optional[List[str]], optional): 使用正则表示的允许跨域访问的 Origin. Defaults to None. + - 示例如 '^https://.*\\.example\\.com:8081$',此正则允许 https://a.example.com:8081, https://b.example.com:8081。 + - allow_origins、allow_origins_by_regex 只能一个有效。 + allow_methods (str, optional): 允许跨域访问的 Method. Defaults to "*". + - 比如:GET,POST 等。如果你有多个 Method,请使用 , 分割。 + - 当 allow_credential 为 false 时,可以使用 * 来表示允许所有 Method 通过。 + - 你也可以在启用了 allow_credential 后使用 ** 强制允许所有 Method 都通过,但请注意这样存在安全隐患。 + allow_headers (str, optional): 允许跨域访问时请求方携带哪些非 CORS 规范 以外的 Header. Defaults to "*". + - 如果你有多个 Header,请使用 , 分割。 + - 当 allow_credential 为 false 时,可以使用 * 来表示允许所有 Header 通过。 + - 你也可以在启用了 allow_credential 后使用 ** 强制允许所有 Header 都通过,但请注意这样存在安全隐患。 + expose_headers (str, optional): 允许跨域访问时响应方携带哪些非 CORS 规范 以外的 Header. Defaults to "*". + - 如果你有多个 Header,请使用 , 分割。 + - 当 allow_credential 为 false 时,可以使用 * 来表示允许任意 Header。 + - 你也可以在启用了 allow_credential 后使用 ** 强制允许任意 Header,但请注意这样存在安全隐患 + max_age (int, optional): 浏览器缓存 CORS 结果的最大时间,单位为秒。Defaults to 86400. + - 在这个时间范围内,浏览器会复用上一次的检查结果,-1 表示不缓存。请注意各个浏览器允许的最大时间不同。 + allow_credential (bool, optional): 是否允许跨域访问的请求方携带凭据(如 Cookie 等). Defaults to False. + - 根据 CORS 规范,如果设置该选项为 true,那么将不能在其他属性中使用 *。 + + Raises: + ValueError: allow_credential can not be True when allow_origins/allow_methods/allow_headers is '*' + ValueError: allow_origins and allow_origins_by_regex can not be set at the same time + + Returns: + { + "type": "bk-cors", + "yaml": "allow_origins: '*'\nallow_origins_by_regex: null\nallow_methods: '*'\nallow_headers: '*'\nexpose_headers: '*'\nmax_age: 86400\nallow_credential: false" + } + """ + if allow_credential and (allow_origins == "*" or allow_methods == "*" or allow_headers == "*"): + raise ValueError("allow_credential can not be True when allow_origins/allow_methods/allow_headers is '*'") + + if allow_origins and allow_origins_by_regex: + raise ValueError("allow_origins and allow_origins_by_regex can not be set at the same time") + + if allow_methods != "*" and allow_methods != "**": + methods = allow_methods.split(",") + for m in methods: + if m not in ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "TRACE", "CONNECT"]: + raise ValueError(f"invalid method {m}") + + return { + "type": "bk-cors", + "yaml": yaml_dump( + { + "allow_origins": allow_origins, + "allow_origins_by_regex": allow_origins_by_regex, + "allow_methods": allow_methods, + "allow_headers": allow_headers, + "expose_headers": expose_headers, + "max_age": max_age, + "allow_credential": allow_credential, + } + ), + } + + +def build_bk_ip_restriction( + whitelist: Optional[List[str]] = None, + blacklist: Optional[List[str]] = None, +) -> Dict[str, str]: + """generate bk-ip-restriction plugin config + + Args: + whitelist (Optional[List[str]], optional): 白名单,ip 列表或 cidr 列表。Defaults to None. + blacklist (Optional[List[str]], optional): 黑名单,ip 列表或 cidr 列表。Defaults to None. + + Raises: + ValueError: whitelist and blacklist can not be set at the same time + ValueError: whitelist or blacklist should be set + Returns: + { + "type": "bk-ip-restriction", + "yaml": "whitelist: |- 127.0.0.1" + } + """ + if whitelist and blacklist: + raise ValueError("whitelist and blacklist can not be set at the same time") + + if not (whitelist or blacklist): + raise ValueError("whitelist or blacklist should be set") + + if not whitelist: + whitelist = [] + if not blacklist: + blacklist = [] + + # validate the ips + if whitelist: + [ipaddress.ip_interface(ip) for ip in whitelist] + return { + "type": "bk-ip-restriction", + "yaml": yaml_dump( + { + "whitelist": literal_unicode("\n".join(whitelist)), + }, + ), + } + + if blacklist: + [ipaddress.ip_interface(ip) for ip in blacklist] + return { + "type": "bk-ip-restriction", + "yaml": yaml_dump( + { + "blacklist": literal_unicode("\n".join(blacklist)), + }, + ), + } + + return {} + + +VALID_PERIODS = [1, 60, 3600, 86400] + + +def build_bk_rate_limit( + default_period: int, default_tokens: int, specific_app_limits: Optional[List[Tuple[str, int, int]]] +) -> Dict[str, str]: + """generate bk-rate-limit plugin config + + Args: + default_period (int): 默认的限流周期,单位为秒。有效值为 1 (seconds)/ 60 (minute)/ 3600 (hour)/ 86400 (day) + default_tokens (int): 默认的限流令牌数 + specific_app_limits (Optional[List[Tuple[str, int, int]]]): 特殊应用的限流配置,格式为 [(app_code, period, tokens), ...] + + Raises: + ValueError: default_period should be 1 (seconds)/ 60 (minute)/ 3600 (hour)/ 86400 (day) + ValueError: period of {app_code} should be 1 (seconds)/ 60 (minute)/ 3600 (hour)/ 86400 (day) + + Returns: + { + "type": "bk-rate-limit", + "yaml": "rates:\n __default:\n - period: 1\n tokens: 10\n app_code:\n - period: 1\n tokens: 10" + } + """ + if default_period not in VALID_PERIODS: + raise ValueError("default_period should be 1 (seconds)/ 60 (minute)/ 3600 (hour)/ 86400 (day)") + + rates = { + "__default": [ + { + "period": default_period, + "tokens": default_tokens, + } + ] + } + if specific_app_limits: + for app_code, period, tokens in specific_app_limits: + if period not in VALID_PERIODS: + raise ValueError(f"period of {app_code} should be 1 (seconds)/ 60 (minute)/ 3600 (hour)/ 86400 (day)") + rates[app_code] = [ + { + "period": period, + "tokens": tokens, + } + ] + + return { + "type": "bk-rate-limit", + "yaml": yaml_dump( + { + "rates": rates, + } + ), + } + + +def build_stage_plugin_config_for_definition_yaml( + plugin_configs: List[Dict[str, str]], indent: int = 10 +) -> List[Dict[str, str]]: + """generate stage plugin config for definition yaml, it would include with indent + + Args: + plugin_configs (List[Dict[str, str]]): the plugin config list + + Returns: + { + "type": "stage", + "yaml": "pre: |- set:\n - key: key1\n value: value1\n - key: key2\n value: value2\nremove:\n - key1\n" + } + """ + indented_plugin_configs: List[Dict[str, str]] = [] + for plugin_config in plugin_configs: + _type = plugin_config["type"] + yaml = plugin_config["yaml"] + indented_yaml = yaml_text_indent(yaml, indent) + + indented_plugin_configs.append( + { + "type": _type, + "yaml": indented_yaml, + } + ) + return indented_plugin_configs diff --git a/sdks/apigw-manager/src/apigw_manager/plugin/utils.py b/sdks/apigw-manager/src/apigw_manager/plugin/utils.py new file mode 100644 index 0000000..0205ed9 --- /dev/null +++ b/sdks/apigw-manager/src/apigw_manager/plugin/utils.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云 - 蓝鲸 PaaS 平台 (BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + + +from typing import Any + +import yaml + +# NOTE: +# not literal_unicode: yaml: '127.0.0.1\n192.168.1.1' +# with literal_unicode: yaml: |- +# 127.0.0.1 +# 192.168.1.1 + + +class literal_unicode(str): + pass + + +def literal_unicode_representer(dumper, data): + return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|") + + +yaml.add_representer(literal_unicode, literal_unicode_representer) + + +def yaml_dump(data: Any) -> str: + return yaml.dump(data) + + +def yaml_text_indent(text: str, indent: int) -> str: + """indent the yaml text, for render yaml text in another `yaml:|-` + - yaml: |- + a + b + c + + TO: + - yaml: |- + a + b + c + + Args: + text (str): the yaml text + indent (int): the indent number + + Returns: + str: the indented yaml text + """ + lines = text.splitlines() + first_line = lines[0] + + indent_spaces = " " * indent + other_lines = [f"{indent_spaces}{line}" for line in lines[1:]] + return "\n".join([first_line] + other_lines) diff --git a/sdks/apigw-manager/tests/apigw_manager/__init__.py b/sdks/apigw-manager/tests/apigw_manager/__init__.py index bc79700..ce92420 100644 --- a/sdks/apigw-manager/tests/apigw_manager/__init__.py +++ b/sdks/apigw-manager/tests/apigw_manager/__init__.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- -""" - * TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. - * Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. - * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://opensource.org/licenses/MIT - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. -""" +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. diff --git a/sdks/apigw-manager/tests/apigw_manager/drf/__init__.py b/sdks/apigw-manager/tests/apigw_manager/drf/__init__.py new file mode 100644 index 0000000..ce92420 --- /dev/null +++ b/sdks/apigw-manager/tests/apigw_manager/drf/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. diff --git a/sdks/apigw-manager/tests/apigw_manager/drf/management/__init__.py b/sdks/apigw-manager/tests/apigw_manager/drf/management/__init__.py new file mode 100644 index 0000000..ce92420 --- /dev/null +++ b/sdks/apigw-manager/tests/apigw_manager/drf/management/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. diff --git a/sdks/apigw-manager/tests/apigw_manager/drf/management/commands/__init__.py b/sdks/apigw-manager/tests/apigw_manager/drf/management/commands/__init__.py new file mode 100644 index 0000000..ce92420 --- /dev/null +++ b/sdks/apigw-manager/tests/apigw_manager/drf/management/commands/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. diff --git a/sdks/apigw-manager/tests/apigw_manager/drf/management/commands/test_generate_resources_yaml.py b/sdks/apigw-manager/tests/apigw_manager/drf/management/commands/test_generate_resources_yaml.py new file mode 100644 index 0000000..133511f --- /dev/null +++ b/sdks/apigw-manager/tests/apigw_manager/drf/management/commands/test_generate_resources_yaml.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +from unittest.mock import Mock + +import pytest +from apigw_manager.drf.management.commands.generate_resources_yaml import ( + post_process_inject_method_and_path, + post_process_only_keep_the_apis_with_specified_tags, +) + + +@pytest.fixture() +def django_settings_subpath_empty(settings): + settings.BK_APIGW_DEFAULT_STAGE_BACKEND_SUBPATH = "" + + +@pytest.fixture() +def django_settings_subpath(settings): + settings.BK_APIGW_DEFAULT_STAGE_BACKEND_SUBPATH = "/mock" + + +class TestPostProcess: + def test_only_keep_the_apis_with_specified_tags(self): + tags = ["open"] + + f = post_process_only_keep_the_apis_with_specified_tags(tags) + + # no paths + result1 = {"hello": "world"} + data1 = f(result1, Mock(), Mock(), Mock()) + + assert data1 == result1 + + # with paths, no tag hit + result2 = { + "paths": { + "/api/v1/xxx": {"get": {"tags": ["close"]}}, + } + } + data2 = f(result2, Mock(), Mock(), Mock()) + assert data2 == {"paths": {}} + + # with paths, tag hit + result3 = { + "paths": { + "/api/v1/xxx": {"get": {"tags": ["open"]}, "post": {"tags": ["close"]}}, + "/api/v1/yyy": {"get": {"tags": ["close"]}, "post": {"tags": ["close"]}}, + } + } + data3 = f(result3, Mock(), Mock(), Mock()) + assert data3 == { + "paths": { + "/api/v1/xxx": {"get": {"tags": ["open"]}}, + } + } + + def test_post_process_inject_method_and_path_no_subpath(self, django_settings_subpath_empty): + f = post_process_inject_method_and_path + + # no paths + result1 = {"hello": "world"} + data1 = f(result1, Mock(), Mock(), Mock()) + + assert data1 == result1 + + # with paths + result2 = { + "paths": { + "/api/v1/xxx": {"get": {"tags": ["open"], "x-bk-apigateway-resource": {"backend": {}}}}, + } + } + data2 = f(result2, Mock(), Mock(), Mock()) + assert data2 == { + "paths": { + "/api/v1/xxx": { + "get": { + "tags": ["open"], + "x-bk-apigateway-resource": {"backend": {"method": "get", "path": "/api/v1/xxx"}}, + } + }, + } + } + + def test_post_process_inject_method_and_path_with_subpath(self, django_settings_subpath): + f = post_process_inject_method_and_path + + # no paths + result1 = {"hello": "world"} + data1 = f(result1, Mock(), Mock(), Mock()) + + assert data1 == result1 + + # with paths + result2 = { + "paths": { + "/api/v1/xxx": {"get": {"tags": ["open"], "x-bk-apigateway-resource": {"backend": {}}}}, + } + } + data2 = f(result2, Mock(), Mock(), Mock()) + assert data2 == { + "paths": { + "/api/v1/xxx": { + "get": { + "tags": ["open"], + "x-bk-apigateway-resource": {"backend": {"method": "get", "path": "/mock/api/v1/xxx"}}, + } + }, + } + } diff --git a/sdks/apigw-manager/tests/apigw_manager/drf/test_authentication.py b/sdks/apigw-manager/tests/apigw_manager/drf/test_authentication.py new file mode 100644 index 0000000..846eef3 --- /dev/null +++ b/sdks/apigw-manager/tests/apigw_manager/drf/test_authentication.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +from typing import Optional +from unittest.mock import Mock + +import pytest +from apigw_manager.apigw.providers import PublicKeyProvider +from apigw_manager.drf.authentication import ApiGatewayJWTAuthentication + + +class MockEmptyProvider(PublicKeyProvider): + def provide(self, gateway_name: str, jwt_issuer: Optional[str] = None) -> Optional[str]: + return None + + +class MockJwtProvider(PublicKeyProvider): + def provide(self, gateway_name: str, jwt_issuer: Optional[str] = None) -> Optional[str]: + jwt = Mock() + jwt.payload = { + "app": {"app_code": "mock_app", "verified": True}, + "user": {"username": "mock_user"}, + } + jwt.gateway_name = "mock_gateway" + + return jwt + + +@pytest.fixture() +def mock_request(mocker): + return mocker.MagicMock() + + +class TestApiGatewayJWTAuthentication: + def test_authenticate_header(self): + authentication = ApiGatewayJWTAuthentication() + header = authentication.authenticate_header(Mock()) + + assert header == "HTTP_X_BKAPI_JWT" + + def test_make_app(self): + authentication = ApiGatewayJWTAuthentication() + app = authentication.make_app(bk_app_code="my_app", verified=True) + + assert app.bk_app_code == "my_app" + assert app.verified + + @pytest.fixture(autouse=True) + def _patch_authenticate(self, mocker): + self.authenticate_function = mocker.patch("django.contrib.auth.authenticate") + + def test_get_user(self): + self.authenticate_function.return_value = "my_user" + user = ApiGatewayJWTAuthentication().get_user(Mock()) + assert user == "my_user" + + def test_authenticate_invalid_provider(self): + authentication = ApiGatewayJWTAuthentication() + authentication.provider = MockEmptyProvider("mock") + + assert authentication.authenticate(Mock()) is None + + def test_authenticate(self, mock_request): + self.authenticate_function.return_value = "my_user" + + authentication = ApiGatewayJWTAuthentication() + authentication.provider = MockJwtProvider("mock") + + user, arg2 = authentication.authenticate(mock_request) + assert arg2 is None + assert user == "my_user" + + assert mock_request.app == authentication.make_app(bk_app_code="mock_app", verified=True) diff --git a/sdks/apigw-manager/tests/apigw_manager/drf/test_permission.py b/sdks/apigw-manager/tests/apigw_manager/drf/test_permission.py new file mode 100644 index 0000000..2ba6e68 --- /dev/null +++ b/sdks/apigw-manager/tests/apigw_manager/drf/test_permission.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +import pytest +from apigw_manager.drf.permission import ApiGatewayPermission + + +class Request: + pass + + +class App: + verified = False + + +class User: + is_authenticated = False + + +@pytest.fixture() +def empty_request(mocker): + return Request() + + +@pytest.fixture() +def jwt_request(mocker): + # request = mocker.MagicMock() + request = Request() + request.jwt = mocker.MagicMock() + request.jwt.payload = {} + return request + + +@pytest.fixture() +def exempt_view(mocker): + view = mocker.MagicMock() + view.FROM_APIGW_EXEMPT = True + return view + + +@pytest.fixture() +def view(mocker): + view = mocker.MagicMock() + view.FROM_APIGW_EXEMPT = False + return view + + +class TestApiGatewayPermission: + def test_has_permission_exempt_view(self, empty_request, exempt_view): + permission = ApiGatewayPermission() + assert permission.has_permission(empty_request, exempt_view) is True + + def test_has_permission_missing_jwt(self, empty_request, view): + permission = ApiGatewayPermission() + assert permission.has_permission(empty_request, view) is False + + def test_has_permission_no_app(self, jwt_request, view): + permission = ApiGatewayPermission() + view.app_verified_required = True + assert permission.has_permission(jwt_request, view) is False + + def test_has_permission_app_verified_false(self, jwt_request, view): + permission = ApiGatewayPermission() + view.app_verified_required = True + jwt_request.app = App() + jwt_request.app.verified = False + assert permission.has_permission(jwt_request, view) is False + + def test_has_permission_no_user(self, jwt_request, view): + permission = ApiGatewayPermission() + view.app_verified_required = False + view.user_verified_required = True + assert permission.has_permission(jwt_request, view) is False + + def test_has_permission_user_not_authenticated(self, jwt_request, view): + permission = ApiGatewayPermission() + view.app_verified_required = False + view.user_verified_required = True + + jwt_request.user = User() + jwt_request.user.is_authenticated = False + assert permission.has_permission(jwt_request, view) is False + + def test_pass(self, jwt_request, view): + permission = ApiGatewayPermission() + view.app_verified_required = True + jwt_request.app = App() + jwt_request.app.verified = True + + view.user_verified_required = True + jwt_request.user = User() + jwt_request.user.is_authenticated = True + assert permission.has_permission(jwt_request, view) is True diff --git a/sdks/apigw-manager/tests/apigw_manager/drf/test_scheme.py b/sdks/apigw-manager/tests/apigw_manager/drf/test_scheme.py new file mode 100644 index 0000000..853dd02 --- /dev/null +++ b/sdks/apigw-manager/tests/apigw_manager/drf/test_scheme.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + + +from apigw_manager.drf.scheme import ApiGatewayJWTAuthenticationScheme + + +class TestApiGatewayJWTAuthenticationScheme: + def test_get_security_definition(self): + scheme = ApiGatewayJWTAuthenticationScheme(target=None) + assert scheme.get_security_definition(None) == { + "type": "apiKey", + "in": "header", + "name": "X-BKAPI-JWT", + } diff --git a/sdks/apigw-manager/tests/apigw_manager/drf/test_utils.py b/sdks/apigw-manager/tests/apigw_manager/drf/test_utils.py new file mode 100644 index 0000000..e08ea47 --- /dev/null +++ b/sdks/apigw-manager/tests/apigw_manager/drf/test_utils.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +import os +from unittest import mock + +from apigw_manager.drf.utils import ( + gen_apigateway_resource_config, + get_default_database_config_dict, + get_logging_config_dict, +) + + +class TestGenApigatewayResourceConfig: + def test_gen(self): + data = gen_apigateway_resource_config( + is_public=False, + allow_apply_permission=False, + user_verified_required=True, + app_verified_required=True, + resource_permission_required=True, + description_en="this is a test", + match_subpath=False, + ) + assert data == { + "x-bk-apigateway-resource": { + "isPublic": False, + "matchSubpath": False, + "backend": { + "name": "default", + "method": "", + "path": "", + "matchSubpath": False, + "timeout": 0, + }, + "pluginConfigs": [], + "allowApplyPermission": False, + "authConfig": { + "userVerifiedRequired": True, + "appVerifiedRequired": True, + "resourcePermissionRequired": True, + }, + "descriptionEn": "this is a test", + } + } + + +class TestGetLoggingConfigDict: + def test_get(self): + data = get_logging_config_dict( + log_level="DEBUG", + is_local=True, + log_dir="/tmp", + app_code="test", + ) + + expected_format = ( + "%(levelname)s [%(asctime)s] %(pathname)s " + "%(lineno)d %(funcName)s %(process)d %(thread)d " + "\n \t %(message)s \n" + ) + assert data["formatters"]["verbose"]["format"] == expected_format + + +class TestGetDefaultDatabaseConfigDict: + @mock.patch.dict( + os.environ, + { + "GCS_MYSQL_NAME": "a", + "GCS_MYSQL_USER": "b", + "GCS_MYSQL_PASSWORD": "c", + "GCS_MYSQL_HOST": "d", + "GCS_MYSQL_PORT": "e", + }, + ) + def test_get(self): + settings_module = {"DB_PREFIX": "GCS_MYSQL"} + data = get_default_database_config_dict(settings_module) + assert data == { + "ENGINE": "django.db.backends.mysql", + "NAME": "a", + "USER": "b", + "PASSWORD": "c", + "HOST": "d", + "PORT": "e", + "OPTIONS": {"isolation_level": "repeatable read"}, + } diff --git a/sdks/apigw-manager/tests/apigw_manager/plugin/__init__.py b/sdks/apigw-manager/tests/apigw_manager/plugin/__init__.py new file mode 100644 index 0000000..ce92420 --- /dev/null +++ b/sdks/apigw-manager/tests/apigw_manager/plugin/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. diff --git a/sdks/apigw-manager/tests/apigw_manager/plugin/test_config.py b/sdks/apigw-manager/tests/apigw_manager/plugin/test_config.py new file mode 100644 index 0000000..b3b606e --- /dev/null +++ b/sdks/apigw-manager/tests/apigw_manager/plugin/test_config.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +import pytest +from apigw_manager.plugin.config import ( + build_bk_cors, + build_bk_header_rewrite, + build_bk_ip_restriction, + build_bk_rate_limit, + build_stage_plugin_config_for_definition_yaml, +) + + +class TestBuildPluginConfig: + @pytest.mark.parametrize( + "set, remove, will_error, expected", + [ + ( + {"key1": "value1", "key2": "value2"}, + ["key1"], + False, + { + "type": "bk-header-rewrite", + "yaml": "remove:\n- key: key1\nset:\n- key: key1\n value: value1\n- key: key2\n value: value2\n", + }, + ), + ( + {"key:1": "value1", "key2": "value2"}, + ["key1"], + True, + None, + ), + ( + {"key1": "value1", "key2": "value2"}, + ["key:1"], + True, + None, + ), + ], + ) + def test_build_bk_header_rewrite(self, set, remove, will_error, expected): + if will_error: + with pytest.raises(ValueError): + build_bk_header_rewrite(set, remove) + return + + assert build_bk_header_rewrite(set, remove) == expected + + @pytest.mark.parametrize( + "data, will_error, expected", + [ + ( + {}, + False, + { + "type": "bk-cors", + "yaml": "allow_credential: false\n" + "allow_headers: '*'\n" + "allow_methods: '*'\n" + "allow_origins: '*'\n" + "allow_origins_by_regex: null\n" + "expose_headers: '*'\n" + "max_age: 86400\n", + }, + ), + ({"allow_credential": True, "allow_origins": "*"}, True, None), + ({"allow_credential": True, "allow_methods": "*"}, True, None), + ({"allow_credential": True, "allow_headers": "*"}, True, None), + # allow origins and allow_origins_by_regex + ({"allow_origins": "*", "allow_origins_by_regex": "*"}, True, None), + ({"allow_methods": "NOT_VALID"}, True, None), + ], + ) + def test_build_bk_cors(self, data, will_error, expected): + if will_error: + with pytest.raises(ValueError): + build_bk_cors(**data) + return + + assert build_bk_cors(**data) == expected + + @pytest.mark.parametrize( + "whitelist, blacklist, will_error, expected", + [ + ( + ["127.0.0.1"], + None, + False, + {"type": "bk-ip-restriction", "yaml": "whitelist: |-\n 127.0.0.1\n"}, + ), + ( + ["127.0.0.1/8"], + None, + False, + {"type": "bk-ip-restriction", "yaml": "whitelist: |-\n 127.0.0.1/8\n"}, + ), + ( + None, + ["127.0.0.1"], + False, + {"type": "bk-ip-restriction", "yaml": "blacklist: |-\n 127.0.0.1\n"}, + ), + ( + ["127.0.0.1"], + ["127.0.0.1"], + True, + None, + ), + ( + None, + None, + True, + None, + ), + ( + ["invalid_ip"], + None, + True, + None, + ), + ], + ) + def test_build_bk_ip_restriction(self, whitelist, blacklist, will_error, expected): + if will_error: + with pytest.raises(ValueError): + build_bk_ip_restriction(whitelist, blacklist) + return + + assert build_bk_ip_restriction(whitelist, blacklist) == expected + + @pytest.mark.parametrize( + "default_period, default_tokens, specific_app_limits, will_error, expected", + [ + ( + 1, + 1000, + [("demo", 60, 1000)], + False, + { + "type": "bk-rate-limit", + "yaml": "rates:\n" + " __default:\n" + " - period: 1\n" + " tokens: 1000\n" + " demo:\n" + " - period: 60\n" + " tokens: 1000\n", + }, + ), + ( + 20, + 1000, + None, + True, + None, + ), + ( + 1, + 1000, + [("demo", 20, 1000)], + True, + None, + ), + ], + ) + def test_build_bk_rate_limit(self, default_period, default_tokens, specific_app_limits, will_error, expected): + if will_error: + with pytest.raises(ValueError): + build_bk_rate_limit(default_period, default_tokens, specific_app_limits) + return + + assert build_bk_rate_limit(default_period, default_tokens, specific_app_limits) == expected + + +class TestBuildStagePluginConfigForDefinitionYaml: + def test_build_stage_plugin_config_for_definition_yaml(self): + yaml_str = """a +b +c""" + plugin_configs = [{"type": "bk-rate-limit", "yaml": yaml_str}] + result = build_stage_plugin_config_for_definition_yaml(plugin_configs, indent=2) + + expected_yaml_str = """a + b + c""" + + assert result == [{"type": "bk-rate-limit", "yaml": expected_yaml_str}] diff --git a/sdks/apigw-manager/tests/apigw_manager/plugin/test_utils.py b/sdks/apigw-manager/tests/apigw_manager/plugin/test_utils.py new file mode 100644 index 0000000..00a73cd --- /dev/null +++ b/sdks/apigw-manager/tests/apigw_manager/plugin/test_utils.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available. +# Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://opensource.org/licenses/MIT +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +from apigw_manager.plugin.utils import literal_unicode, yaml_dump, yaml_text_indent + + +class TestYamlDump: + def test_yaml_dump(self): + data = {"yaml": ["a", "b"]} + result = yaml_dump(data) + assert "yaml:\n- a\n- b\n" == result + + def test_yaml_dump_str(self): + data = {"yaml": "127.0.0.1\n192.168.1.1"} + result = yaml_dump(data) + expected = """yaml: '127.0.0.1 + + 192.168.1.1' +""" + assert expected == result + + def test_yaml_dump_literal_unicode(self): + data = {"yaml": literal_unicode("127.0.0.1\n192.168.1.1")} + + result = yaml_dump(data) + expected = """yaml: |- + 127.0.0.1 + 192.168.1.1 +""" + assert expected == result + + +class TestYamlTextIndent: + def test_yaml_text_indent(self): + yaml_text = """a +b +c +""" + result = yaml_text_indent(yaml_text, 2) + expected = """a + b + c""" + assert expected == result