feat/PRWLR-3999 Implement providers endpoints (#21)

* feat(Backend): PRWLR-3989 add RLS to postgresql db and base models

* feat(API): PRWLR-3989 add TenantMiddleware

* chore(API, Backend): PRWLR-3989 create new db user without RLS bypass on migrations

* chore(Backend): PRWLR-3989 fix RLS bypasser for POST requests

* fix(Backend): PRWLR-3989 fix user permissions when migrating new models

* chore(Backend): PRWLR-3989 add testing view for RLS manual tests

* feat(API): PRWLR-3989 add tenant_id to API logging

* chore(API, Backend): PRWLR-3989 add TODOs

* test(API): PRWLR-3989 add new middleware unit tests

* chore(API): PRWLR-3989 refactor RLS code

* fix(tests): PRWLR-3989 fix testing db connector

* chore: PRWLR-3989 add references to JIRA tickets

* fix: PRWLR-3989 remove bypass logic and fix serializers

* fix: PRWLR-3989 improve drop SQL query for RLS models

* feat(Backend): PRWLR-3989 add specific permissions on each model

* fix(Backend): PRWLR-3989 fix database routing and grant select perms

* fix(test): PRWLR-3989 fix routing issues with unit tests

* chore: PRWLR-3989 remove references to JIRA tickets

* feat(Backend): PRWLR-3999 add Provider model

* feat: PRWLR-3999 add providers view logic

* fix: PRWLR-3999 fix unique index fields

* feat(API): PRWLR-3999 add custom exception handlers

* feat(API): PRWLR-3999 add /providers/{provider_id}/connection endpoint

* test(API): PRWLR-3999 add base unit tests for providers

* fix(API, Backend): PRWLR-3999 fix bugs after rebase

* chore(API, docker): PRWLR-3999 add dev feats for demo

* feat(API): PRWLR-3999 add drf-spectacular-jsonapi and improve docs

* test(API): PRWLR-3999 add providers unit tests

* chore(Backend): PRWLR-3989 adjust privileges for user on table tenant

* chore: PRWLR-3999 rename model custom validation error

* chore: PRWLR-3999 remove Test references

* chore: PRWLR-3999 update API v1 spec

* fix: PRWLR-3999 apply requested changes to filter and models

* feat: PRWLR-3999 add validation to PATCH /providers payload

* fix: PRWLR-3999 fix providers enum description

* chore: add more providers fixtures

* fix: PRWLR-3999 make providers.alias optional
This commit is contained in:
Víctor Fernández Poyatos
2024-08-06 18:39:08 +02:00
committed by GitHub
parent e1a12bcb14
commit 4371ad1535
19 changed files with 1888 additions and 481 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ WORKDIR /home/prowler/backend
FROM build as dev
USER 0
RUN apk --no-cache add curl=8.8.0-r0 vim=9.1.0414-r0
RUN apk --no-cache add curl=8.9.0-r0 vim=9.1.0414-r0
USER prowler
Generated
+174 -142
View File
@@ -27,13 +27,13 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
[[package]]
name = "astroid"
version = "3.2.3"
version = "3.2.4"
description = "An abstract syntax tree for Python with inference support."
optional = false
python-versions = ">=3.8.0"
files = [
{file = "astroid-3.2.3-py3-none-any.whl", hash = "sha256:3eae9ea67c11c858cdd2c91337d2e816bd019ac897ca07d7b346ac10105fceb3"},
{file = "astroid-3.2.3.tar.gz", hash = "sha256:7099b5a60985529d8d46858befa103b82d0d05a5a5e8b816b5303ed96075e1d9"},
{file = "astroid-3.2.4-py3-none-any.whl", hash = "sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25"},
{file = "astroid-3.2.4.tar.gz", hash = "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a"},
]
[[package]]
@@ -358,43 +358,38 @@ toml = ["tomli"]
[[package]]
name = "cryptography"
version = "42.0.8"
version = "43.0.0"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = ">=3.7"
files = [
{file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"},
{file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"},
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"},
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"},
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"},
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"},
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"},
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"},
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"},
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"},
{file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"},
{file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"},
{file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"},
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"},
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"},
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"},
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"},
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"},
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"},
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"},
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"},
{file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"},
{file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"},
{file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"},
{file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"},
{file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"},
{file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"},
{file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"},
{file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"},
{file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"},
{file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"},
{file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"},
{file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"},
{file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"},
{file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"},
{file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"},
{file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"},
{file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"},
{file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"},
{file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"},
{file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"},
{file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"},
{file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"},
]
[package.dependencies]
@@ -407,7 +402,7 @@ nox = ["nox"]
pep8test = ["check-sdist", "click", "mypy", "ruff"]
sdist = ["build"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test-randomorder = ["pytest-randomly"]
[[package]]
@@ -581,6 +576,20 @@ conda = ["pyyaml"]
pipenv = ["pipenv"]
poetry = ["poetry"]
[[package]]
name = "drf-extensions"
version = "0.7.1"
description = "Extensions for Django REST Framework"
optional = false
python-versions = "*"
files = [
{file = "drf-extensions-0.7.1.tar.gz", hash = "sha256:90abfc11a2221e8daf4cd54457e41ed38cd71134678de9622e806193db027db1"},
{file = "drf_extensions-0.7.1-py2.py3-none-any.whl", hash = "sha256:007910437e64aa1d5ad6fc47266a4ac4280e31761e6458eb30fcac7494ac7d4e"},
]
[package.dependencies]
djangorestframework = ">=3.9.3"
[[package]]
name = "drf-spectacular"
version = "0.27.2"
@@ -604,6 +613,24 @@ uritemplate = ">=2.0.0"
offline = ["drf-spectacular-sidecar"]
sidecar = ["drf-spectacular-sidecar"]
[[package]]
name = "drf-spectacular-jsonapi"
version = "0.4.1"
description = "open api 3 schema generator for drf-json-api package based on drf-spectacular package."
optional = false
python-versions = ">=3.7"
files = [
{file = "drf-spectacular-jsonapi-0.4.1.tar.gz", hash = "sha256:e6af0583a698e04c3bc7260f0768d9cbeb8dd6aac7ad5c58221dd58db948d6e7"},
{file = "drf_spectacular_jsonapi-0.4.1-py3-none-any.whl", hash = "sha256:793d8e8cafbb72c727563c5dcb92a120ce83fd8a60f9848d6b5da39422ca143e"},
]
[package.dependencies]
Django = ">=3.2"
djangorestframework = ">=3.13"
djangorestframework-jsonapi = ">=6.0.0"
drf-extensions = ">=0.7.1"
drf-spectacular = ">=0.25.0"
[[package]]
name = "execnet"
version = "2.1.1"
@@ -1509,110 +1536,114 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "rpds-py"
version = "0.19.0"
version = "0.19.1"
description = "Python bindings to Rust's persistent data structures (rpds)"
optional = false
python-versions = ">=3.8"
files = [
{file = "rpds_py-0.19.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:fb37bd599f031f1a6fb9e58ec62864ccf3ad549cf14bac527dbfa97123edcca4"},
{file = "rpds_py-0.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3384d278df99ec2c6acf701d067147320b864ef6727405d6470838476e44d9e8"},
{file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54548e0be3ac117595408fd4ca0ac9278fde89829b0b518be92863b17ff67a2"},
{file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8eb488ef928cdbc05a27245e52de73c0d7c72a34240ef4d9893fdf65a8c1a955"},
{file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5da93debdfe27b2bfc69eefb592e1831d957b9535e0943a0ee8b97996de21b5"},
{file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:79e205c70afddd41f6ee79a8656aec738492a550247a7af697d5bd1aee14f766"},
{file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:959179efb3e4a27610e8d54d667c02a9feaa86bbabaf63efa7faa4dfa780d4f1"},
{file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a6e605bb9edcf010f54f8b6a590dd23a4b40a8cb141255eec2a03db249bc915b"},
{file = "rpds_py-0.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9133d75dc119a61d1a0ded38fb9ba40a00ef41697cc07adb6ae098c875195a3f"},
{file = "rpds_py-0.19.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd36b712d35e757e28bf2f40a71e8f8a2d43c8b026d881aa0c617b450d6865c9"},
{file = "rpds_py-0.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354f3a91718489912f2e0fc331c24eaaf6a4565c080e00fbedb6015857c00582"},
{file = "rpds_py-0.19.0-cp310-none-win32.whl", hash = "sha256:ebcbf356bf5c51afc3290e491d3722b26aaf5b6af3c1c7f6a1b757828a46e336"},
{file = "rpds_py-0.19.0-cp310-none-win_amd64.whl", hash = "sha256:75a6076289b2df6c8ecb9d13ff79ae0cad1d5fb40af377a5021016d58cd691ec"},
{file = "rpds_py-0.19.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6d45080095e585f8c5097897313def60caa2046da202cdb17a01f147fb263b81"},
{file = "rpds_py-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5c9581019c96f865483d031691a5ff1cc455feb4d84fc6920a5ffc48a794d8a"},
{file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1540d807364c84516417115c38f0119dfec5ea5c0dd9a25332dea60b1d26fc4d"},
{file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e65489222b410f79711dc3d2d5003d2757e30874096b2008d50329ea4d0f88c"},
{file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9da6f400eeb8c36f72ef6646ea530d6d175a4f77ff2ed8dfd6352842274c1d8b"},
{file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37f46bb11858717e0efa7893c0f7055c43b44c103e40e69442db5061cb26ed34"},
{file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:071d4adc734de562bd11d43bd134330fb6249769b2f66b9310dab7460f4bf714"},
{file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9625367c8955e4319049113ea4f8fee0c6c1145192d57946c6ffcd8fe8bf48dd"},
{file = "rpds_py-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e19509145275d46bc4d1e16af0b57a12d227c8253655a46bbd5ec317e941279d"},
{file = "rpds_py-0.19.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d438e4c020d8c39961deaf58f6913b1bf8832d9b6f62ec35bd93e97807e9cbc"},
{file = "rpds_py-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90bf55d9d139e5d127193170f38c584ed3c79e16638890d2e36f23aa1630b952"},
{file = "rpds_py-0.19.0-cp311-none-win32.whl", hash = "sha256:8d6ad132b1bc13d05ffe5b85e7a01a3998bf3a6302ba594b28d61b8c2cf13aaf"},
{file = "rpds_py-0.19.0-cp311-none-win_amd64.whl", hash = "sha256:7ec72df7354e6b7f6eb2a17fa6901350018c3a9ad78e48d7b2b54d0412539a67"},
{file = "rpds_py-0.19.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:5095a7c838a8647c32aa37c3a460d2c48debff7fc26e1136aee60100a8cd8f68"},
{file = "rpds_py-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f2f78ef14077e08856e788fa482107aa602636c16c25bdf59c22ea525a785e9"},
{file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7cc6cb44f8636fbf4a934ca72f3e786ba3c9f9ba4f4d74611e7da80684e48d2"},
{file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf902878b4af334a09de7a45badbff0389e7cf8dc2e4dcf5f07125d0b7c2656d"},
{file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:688aa6b8aa724db1596514751ffb767766e02e5c4a87486ab36b8e1ebc1aedac"},
{file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57dbc9167d48e355e2569346b5aa4077f29bf86389c924df25c0a8b9124461fb"},
{file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4cf5a9497874822341c2ebe0d5850fed392034caadc0bad134ab6822c0925b"},
{file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a790d235b9d39c70a466200d506bb33a98e2ee374a9b4eec7a8ac64c2c261fa"},
{file = "rpds_py-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d16089dfa58719c98a1c06f2daceba6d8e3fb9b5d7931af4a990a3c486241cb"},
{file = "rpds_py-0.19.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bc9128e74fe94650367fe23f37074f121b9f796cabbd2f928f13e9661837296d"},
{file = "rpds_py-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c8f77e661ffd96ff104bebf7d0f3255b02aa5d5b28326f5408d6284c4a8b3248"},
{file = "rpds_py-0.19.0-cp312-none-win32.whl", hash = "sha256:5f83689a38e76969327e9b682be5521d87a0c9e5a2e187d2bc6be4765f0d4600"},
{file = "rpds_py-0.19.0-cp312-none-win_amd64.whl", hash = "sha256:06925c50f86da0596b9c3c64c3837b2481337b83ef3519e5db2701df695453a4"},
{file = "rpds_py-0.19.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:52e466bea6f8f3a44b1234570244b1cff45150f59a4acae3fcc5fd700c2993ca"},
{file = "rpds_py-0.19.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e21cc693045fda7f745c790cb687958161ce172ffe3c5719ca1764e752237d16"},
{file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b31f059878eb1f5da8b2fd82480cc18bed8dcd7fb8fe68370e2e6285fa86da6"},
{file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dd46f309e953927dd018567d6a9e2fb84783963650171f6c5fe7e5c41fd5666"},
{file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34a01a4490e170376cd79258b7f755fa13b1a6c3667e872c8e35051ae857a92b"},
{file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcf426a8c38eb57f7bf28932e68425ba86def6e756a5b8cb4731d8e62e4e0223"},
{file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f68eea5df6347d3f1378ce992d86b2af16ad7ff4dcb4a19ccdc23dea901b87fb"},
{file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dab8d921b55a28287733263c0e4c7db11b3ee22aee158a4de09f13c93283c62d"},
{file = "rpds_py-0.19.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6fe87efd7f47266dfc42fe76dae89060038f1d9cb911f89ae7e5084148d1cc08"},
{file = "rpds_py-0.19.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:535d4b52524a961d220875688159277f0e9eeeda0ac45e766092bfb54437543f"},
{file = "rpds_py-0.19.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8b1a94b8afc154fbe36978a511a1f155f9bd97664e4f1f7a374d72e180ceb0ae"},
{file = "rpds_py-0.19.0-cp38-none-win32.whl", hash = "sha256:7c98298a15d6b90c8f6e3caa6457f4f022423caa5fa1a1ca7a5e9e512bdb77a4"},
{file = "rpds_py-0.19.0-cp38-none-win_amd64.whl", hash = "sha256:b0da31853ab6e58a11db3205729133ce0df26e6804e93079dee095be3d681dc1"},
{file = "rpds_py-0.19.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5039e3cef7b3e7a060de468a4a60a60a1f31786da94c6cb054e7a3c75906111c"},
{file = "rpds_py-0.19.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab1932ca6cb8c7499a4d87cb21ccc0d3326f172cfb6a64021a889b591bb3045c"},
{file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2afd2164a1e85226fcb6a1da77a5c8896c18bfe08e82e8ceced5181c42d2179"},
{file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1c30841f5040de47a0046c243fc1b44ddc87d1b12435a43b8edff7e7cb1e0d0"},
{file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f757f359f30ec7dcebca662a6bd46d1098f8b9fb1fcd661a9e13f2e8ce343ba1"},
{file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15e65395a59d2e0e96caf8ee5389ffb4604e980479c32742936ddd7ade914b22"},
{file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb0f6eb3a320f24b94d177e62f4074ff438f2ad9d27e75a46221904ef21a7b05"},
{file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b228e693a2559888790936e20f5f88b6e9f8162c681830eda303bad7517b4d5a"},
{file = "rpds_py-0.19.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2575efaa5d949c9f4e2cdbe7d805d02122c16065bfb8d95c129372d65a291a0b"},
{file = "rpds_py-0.19.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5c872814b77a4e84afa293a1bee08c14daed1068b2bb1cc312edbf020bbbca2b"},
{file = "rpds_py-0.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:850720e1b383df199b8433a20e02b25b72f0fded28bc03c5bd79e2ce7ef050be"},
{file = "rpds_py-0.19.0-cp39-none-win32.whl", hash = "sha256:ce84a7efa5af9f54c0aa7692c45861c1667080814286cacb9958c07fc50294fb"},
{file = "rpds_py-0.19.0-cp39-none-win_amd64.whl", hash = "sha256:1c26da90b8d06227d7769f34915913911222d24ce08c0ab2d60b354e2d9c7aff"},
{file = "rpds_py-0.19.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:75969cf900d7be665ccb1622a9aba225cf386bbc9c3bcfeeab9f62b5048f4a07"},
{file = "rpds_py-0.19.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8445f23f13339da640d1be8e44e5baf4af97e396882ebbf1692aecd67f67c479"},
{file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5a7c1062ef8aea3eda149f08120f10795835fc1c8bc6ad948fb9652a113ca55"},
{file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:462b0c18fbb48fdbf980914a02ee38c423a25fcc4cf40f66bacc95a2d2d73bc8"},
{file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3208f9aea18991ac7f2b39721e947bbd752a1abbe79ad90d9b6a84a74d44409b"},
{file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3444fe52b82f122d8a99bf66777aed6b858d392b12f4c317da19f8234db4533"},
{file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb4bac7185a9f0168d38c01d7a00addece9822a52870eee26b8d5b61409213"},
{file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6b130bd4163c93798a6b9bb96be64a7c43e1cec81126ffa7ffaa106e1fc5cef5"},
{file = "rpds_py-0.19.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:a707b158b4410aefb6b054715545bbb21aaa5d5d0080217290131c49c2124a6e"},
{file = "rpds_py-0.19.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dc9ac4659456bde7c567107556ab065801622396b435a3ff213daef27b495388"},
{file = "rpds_py-0.19.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:81ea573aa46d3b6b3d890cd3c0ad82105985e6058a4baed03cf92518081eec8c"},
{file = "rpds_py-0.19.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f148c3f47f7f29a79c38cc5d020edcb5ca780020fab94dbc21f9af95c463581"},
{file = "rpds_py-0.19.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0906357f90784a66e89ae3eadc2654f36c580a7d65cf63e6a616e4aec3a81be"},
{file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f629ecc2db6a4736b5ba95a8347b0089240d69ad14ac364f557d52ad68cf94b0"},
{file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6feacd1d178c30e5bc37184526e56740342fd2aa6371a28367bad7908d454fc"},
{file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae8b6068ee374fdfab63689be0963333aa83b0815ead5d8648389a8ded593378"},
{file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d57546bad81e0da13263e4c9ce30e96dcbe720dbff5ada08d2600a3502e526"},
{file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b6683a37338818646af718c9ca2a07f89787551057fae57c4ec0446dc6224b"},
{file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e8481b946792415adc07410420d6fc65a352b45d347b78fec45d8f8f0d7496f0"},
{file = "rpds_py-0.19.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:bec35eb20792ea64c3c57891bc3ca0bedb2884fbac2c8249d9b731447ecde4fa"},
{file = "rpds_py-0.19.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:aa5476c3e3a402c37779e95f7b4048db2cb5b0ed0b9d006983965e93f40fe05a"},
{file = "rpds_py-0.19.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:19d02c45f2507b489fd4df7b827940f1420480b3e2e471e952af4d44a1ea8e34"},
{file = "rpds_py-0.19.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a3e2fd14c5d49ee1da322672375963f19f32b3d5953f0615b175ff7b9d38daed"},
{file = "rpds_py-0.19.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:93a91c2640645303e874eada51f4f33351b84b351a689d470f8108d0e0694210"},
{file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5b9fc03bf76a94065299d4a2ecd8dfbae4ae8e2e8098bbfa6ab6413ca267709"},
{file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a4b07cdf3f84310c08c1de2c12ddadbb7a77568bcb16e95489f9c81074322ed"},
{file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba0ed0dc6763d8bd6e5de5cf0d746d28e706a10b615ea382ac0ab17bb7388633"},
{file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:474bc83233abdcf2124ed3f66230a1c8435896046caa4b0b5ab6013c640803cc"},
{file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329c719d31362355a96b435f4653e3b4b061fcc9eba9f91dd40804ca637d914e"},
{file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef9101f3f7b59043a34f1dccbb385ca760467590951952d6701df0da9893ca0c"},
{file = "rpds_py-0.19.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:0121803b0f424ee2109d6e1f27db45b166ebaa4b32ff47d6aa225642636cd834"},
{file = "rpds_py-0.19.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:8344127403dea42f5970adccf6c5957a71a47f522171fafaf4c6ddb41b61703a"},
{file = "rpds_py-0.19.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:443cec402ddd650bb2b885113e1dcedb22b1175c6be223b14246a714b61cd521"},
{file = "rpds_py-0.19.0.tar.gz", hash = "sha256:4fdc9afadbeb393b4bbbad75481e0ea78e4469f2e1d713a90811700830b553a9"},
{file = "rpds_py-0.19.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:aaf71f95b21f9dc708123335df22e5a2fef6307e3e6f9ed773b2e0938cc4d491"},
{file = "rpds_py-0.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca0dda0c5715efe2ab35bb83f813f681ebcd2840d8b1b92bfc6fe3ab382fae4a"},
{file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81db2e7282cc0487f500d4db203edc57da81acde9e35f061d69ed983228ffe3b"},
{file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1a8dfa125b60ec00c7c9baef945bb04abf8ac772d8ebefd79dae2a5f316d7850"},
{file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:271accf41b02687cef26367c775ab220372ee0f4925591c6796e7c148c50cab5"},
{file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9bc4161bd3b970cd6a6fcda70583ad4afd10f2750609fb1f3ca9505050d4ef3"},
{file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0cf2a0dbb5987da4bd92a7ca727eadb225581dd9681365beba9accbe5308f7d"},
{file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b5e28e56143750808c1c79c70a16519e9bc0a68b623197b96292b21b62d6055c"},
{file = "rpds_py-0.19.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c7af6f7b80f687b33a4cdb0a785a5d4de1fb027a44c9a049d8eb67d5bfe8a687"},
{file = "rpds_py-0.19.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e429fc517a1c5e2a70d576077231538a98d59a45dfc552d1ac45a132844e6dfb"},
{file = "rpds_py-0.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d2dbd8f4990d4788cb122f63bf000357533f34860d269c1a8e90ae362090ff3a"},
{file = "rpds_py-0.19.1-cp310-none-win32.whl", hash = "sha256:e0f9d268b19e8f61bf42a1da48276bcd05f7ab5560311f541d22557f8227b866"},
{file = "rpds_py-0.19.1-cp310-none-win_amd64.whl", hash = "sha256:df7c841813f6265e636fe548a49664c77af31ddfa0085515326342a751a6ba51"},
{file = "rpds_py-0.19.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:902cf4739458852fe917104365ec0efbea7d29a15e4276c96a8d33e6ed8ec137"},
{file = "rpds_py-0.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f3d73022990ab0c8b172cce57c69fd9a89c24fd473a5e79cbce92df87e3d9c48"},
{file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3837c63dd6918a24de6c526277910e3766d8c2b1627c500b155f3eecad8fad65"},
{file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cdb7eb3cf3deb3dd9e7b8749323b5d970052711f9e1e9f36364163627f96da58"},
{file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26ab43b6d65d25b1a333c8d1b1c2f8399385ff683a35ab5e274ba7b8bb7dc61c"},
{file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75130df05aae7a7ac171b3b5b24714cffeabd054ad2ebc18870b3aa4526eba23"},
{file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c34f751bf67cab69638564eee34023909380ba3e0d8ee7f6fe473079bf93f09b"},
{file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2671cb47e50a97f419a02cd1e0c339b31de017b033186358db92f4d8e2e17d8"},
{file = "rpds_py-0.19.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c73254c256081704dba0a333457e2fb815364018788f9b501efe7c5e0ada401"},
{file = "rpds_py-0.19.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4383beb4a29935b8fa28aca8fa84c956bf545cb0c46307b091b8d312a9150e6a"},
{file = "rpds_py-0.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dbceedcf4a9329cc665452db1aaf0845b85c666e4885b92ee0cddb1dbf7e052a"},
{file = "rpds_py-0.19.1-cp311-none-win32.whl", hash = "sha256:f0a6d4a93d2a05daec7cb885157c97bbb0be4da739d6f9dfb02e101eb40921cd"},
{file = "rpds_py-0.19.1-cp311-none-win_amd64.whl", hash = "sha256:c149a652aeac4902ecff2dd93c3b2681c608bd5208c793c4a99404b3e1afc87c"},
{file = "rpds_py-0.19.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:56313be667a837ff1ea3508cebb1ef6681d418fa2913a0635386cf29cff35165"},
{file = "rpds_py-0.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d1d7539043b2b31307f2c6c72957a97c839a88b2629a348ebabe5aa8b626d6b"},
{file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1dc59a5e7bc7f44bd0c048681f5e05356e479c50be4f2c1a7089103f1621d5"},
{file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8f78398e67a7227aefa95f876481485403eb974b29e9dc38b307bb6eb2315ea"},
{file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef07a0a1d254eeb16455d839cef6e8c2ed127f47f014bbda64a58b5482b6c836"},
{file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8124101e92c56827bebef084ff106e8ea11c743256149a95b9fd860d3a4f331f"},
{file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08ce9c95a0b093b7aec75676b356a27879901488abc27e9d029273d280438505"},
{file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b02dd77a2de6e49078c8937aadabe933ceac04b41c5dde5eca13a69f3cf144e"},
{file = "rpds_py-0.19.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4dd02e29c8cbed21a1875330b07246b71121a1c08e29f0ee3db5b4cfe16980c4"},
{file = "rpds_py-0.19.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9c7042488165f7251dc7894cd533a875d2875af6d3b0e09eda9c4b334627ad1c"},
{file = "rpds_py-0.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f809a17cc78bd331e137caa25262b507225854073fd319e987bd216bed911b7c"},
{file = "rpds_py-0.19.1-cp312-none-win32.whl", hash = "sha256:3ddab996807c6b4227967fe1587febade4e48ac47bb0e2d3e7858bc621b1cace"},
{file = "rpds_py-0.19.1-cp312-none-win_amd64.whl", hash = "sha256:32e0db3d6e4f45601b58e4ac75c6f24afbf99818c647cc2066f3e4b192dabb1f"},
{file = "rpds_py-0.19.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:747251e428406b05fc86fee3904ee19550c4d2d19258cef274e2151f31ae9d38"},
{file = "rpds_py-0.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dc733d35f861f8d78abfaf54035461e10423422999b360966bf1c443cbc42705"},
{file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbda75f245caecff8faa7e32ee94dfaa8312a3367397975527f29654cd17a6ed"},
{file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd04d8cab16cab5b0a9ffc7d10f0779cf1120ab16c3925404428f74a0a43205a"},
{file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2d66eb41ffca6cc3c91d8387509d27ba73ad28371ef90255c50cb51f8953301"},
{file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdf4890cda3b59170009d012fca3294c00140e7f2abe1910e6a730809d0f3f9b"},
{file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1fa67ef839bad3815124f5f57e48cd50ff392f4911a9f3cf449d66fa3df62a5"},
{file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b82c9514c6d74b89a370c4060bdb80d2299bc6857e462e4a215b4ef7aa7b090e"},
{file = "rpds_py-0.19.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c7b07959866a6afb019abb9564d8a55046feb7a84506c74a6f197cbcdf8a208e"},
{file = "rpds_py-0.19.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4f580ae79d0b861dfd912494ab9d477bea535bfb4756a2269130b6607a21802e"},
{file = "rpds_py-0.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c6d20c8896c00775e6f62d8373aba32956aa0b850d02b5ec493f486c88e12859"},
{file = "rpds_py-0.19.1-cp313-none-win32.whl", hash = "sha256:afedc35fe4b9e30ab240b208bb9dc8938cb4afe9187589e8d8d085e1aacb8309"},
{file = "rpds_py-0.19.1-cp313-none-win_amd64.whl", hash = "sha256:1d4af2eb520d759f48f1073ad3caef997d1bfd910dc34e41261a595d3f038a94"},
{file = "rpds_py-0.19.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:34bca66e2e3eabc8a19e9afe0d3e77789733c702c7c43cd008e953d5d1463fde"},
{file = "rpds_py-0.19.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:24f8ae92c7fae7c28d0fae9b52829235df83f34847aa8160a47eb229d9666c7b"},
{file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71157f9db7f6bc6599a852852f3389343bea34315b4e6f109e5cbc97c1fb2963"},
{file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d494887d40dc4dd0d5a71e9d07324e5c09c4383d93942d391727e7a40ff810b"},
{file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b3661e6d4ba63a094138032c1356d557de5b3ea6fd3cca62a195f623e381c76"},
{file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97fbb77eaeb97591efdc654b8b5f3ccc066406ccfb3175b41382f221ecc216e8"},
{file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cc4bc73e53af8e7a42c8fd7923bbe35babacfa7394ae9240b3430b5dcf16b2a"},
{file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:35af5e4d5448fa179fd7fff0bba0fba51f876cd55212f96c8bbcecc5c684ae5c"},
{file = "rpds_py-0.19.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3511f6baf8438326e351097cecd137eb45c5f019944fe0fd0ae2fea2fd26be39"},
{file = "rpds_py-0.19.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:57863d16187995c10fe9cf911b897ed443ac68189179541734502353af33e693"},
{file = "rpds_py-0.19.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9e318e6786b1e750a62f90c6f7fa8b542102bdcf97c7c4de2a48b50b61bd36ec"},
{file = "rpds_py-0.19.1-cp38-none-win32.whl", hash = "sha256:53dbc35808c6faa2ce3e48571f8f74ef70802218554884787b86a30947842a14"},
{file = "rpds_py-0.19.1-cp38-none-win_amd64.whl", hash = "sha256:8df1c283e57c9cb4d271fdc1875f4a58a143a2d1698eb0d6b7c0d7d5f49c53a1"},
{file = "rpds_py-0.19.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e76c902d229a3aa9d5ceb813e1cbcc69bf5bda44c80d574ff1ac1fa3136dea71"},
{file = "rpds_py-0.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de1f7cd5b6b351e1afd7568bdab94934d656abe273d66cda0ceea43bbc02a0c2"},
{file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24fc5a84777cb61692d17988989690d6f34f7f95968ac81398d67c0d0994a897"},
{file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:74129d5ffc4cde992d89d345f7f7d6758320e5d44a369d74d83493429dad2de5"},
{file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e360188b72f8080fefa3adfdcf3618604cc8173651c9754f189fece068d2a45"},
{file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13e6d4840897d4e4e6b2aa1443e3a8eca92b0402182aafc5f4ca1f5e24f9270a"},
{file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f09529d2332264a902688031a83c19de8fda5eb5881e44233286b9c9ec91856d"},
{file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0d4b52811dcbc1aba08fd88d475f75b4f6db0984ba12275d9bed1a04b2cae9b5"},
{file = "rpds_py-0.19.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd635c2c4043222d80d80ca1ac4530a633102a9f2ad12252183bcf338c1b9474"},
{file = "rpds_py-0.19.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f35b34a5184d5e0cc360b61664c1c06e866aab077b5a7c538a3e20c8fcdbf90b"},
{file = "rpds_py-0.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d4ec0046facab83012d821b33cead742a35b54575c4edfb7ed7445f63441835f"},
{file = "rpds_py-0.19.1-cp39-none-win32.whl", hash = "sha256:f5b8353ea1a4d7dfb59a7f45c04df66ecfd363bb5b35f33b11ea579111d4655f"},
{file = "rpds_py-0.19.1-cp39-none-win_amd64.whl", hash = "sha256:1fb93d3486f793d54a094e2bfd9cd97031f63fcb5bc18faeb3dd4b49a1c06523"},
{file = "rpds_py-0.19.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7d5c7e32f3ee42f77d8ff1a10384b5cdcc2d37035e2e3320ded909aa192d32c3"},
{file = "rpds_py-0.19.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:89cc8921a4a5028d6dd388c399fcd2eef232e7040345af3d5b16c04b91cf3c7e"},
{file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca34e913d27401bda2a6f390d0614049f5a95b3b11cd8eff80fe4ec340a1208"},
{file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5953391af1405f968eb5701ebbb577ebc5ced8d0041406f9052638bafe52209d"},
{file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:840e18c38098221ea6201f091fc5d4de6128961d2930fbbc96806fb43f69aec1"},
{file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d8b735c4d162dc7d86a9cf3d717f14b6c73637a1f9cd57fe7e61002d9cb1972"},
{file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce757c7c90d35719b38fa3d4ca55654a76a40716ee299b0865f2de21c146801c"},
{file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9421b23c85f361a133aa7c5e8ec757668f70343f4ed8fdb5a4a14abd5437244"},
{file = "rpds_py-0.19.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3b823be829407393d84ee56dc849dbe3b31b6a326f388e171555b262e8456cc1"},
{file = "rpds_py-0.19.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:5e58b61dcbb483a442c6239c3836696b79f2cd8e7eec11e12155d3f6f2d886d1"},
{file = "rpds_py-0.19.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39d67896f7235b2c886fb1ee77b1491b77049dcef6fbf0f401e7b4cbed86bbd4"},
{file = "rpds_py-0.19.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8b32cd4ab6db50c875001ba4f5a6b30c0f42151aa1fbf9c2e7e3674893fb1dc4"},
{file = "rpds_py-0.19.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1c32e41de995f39b6b315d66c27dea3ef7f7c937c06caab4c6a79a5e09e2c415"},
{file = "rpds_py-0.19.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1a129c02b42d46758c87faeea21a9f574e1c858b9f358b6dd0bbd71d17713175"},
{file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:346557f5b1d8fd9966059b7a748fd79ac59f5752cd0e9498d6a40e3ac1c1875f"},
{file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31e450840f2f27699d014cfc8865cc747184286b26d945bcea6042bb6aa4d26e"},
{file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01227f8b3e6c8961490d869aa65c99653df80d2f0a7fde8c64ebddab2b9b02fd"},
{file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69084fd29bfeff14816666c93a466e85414fe6b7d236cfc108a9c11afa6f7301"},
{file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d2b88efe65544a7d5121b0c3b003ebba92bfede2ea3577ce548b69c5235185"},
{file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ea961a674172ed2235d990d7edf85d15d8dfa23ab8575e48306371c070cda67"},
{file = "rpds_py-0.19.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:5beffdbe766cfe4fb04f30644d822a1080b5359df7db3a63d30fa928375b2720"},
{file = "rpds_py-0.19.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:720f3108fb1bfa32e51db58b832898372eb5891e8472a8093008010911e324c5"},
{file = "rpds_py-0.19.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:c2087dbb76a87ec2c619253e021e4fb20d1a72580feeaa6892b0b3d955175a71"},
{file = "rpds_py-0.19.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ddd50f18ebc05ec29a0d9271e9dbe93997536da3546677f8ca00b76d477680c"},
{file = "rpds_py-0.19.1.tar.gz", hash = "sha256:31dd5794837f00b46f4096aa8ccaa5972f73a938982e32ed817bb520c465e520"},
]
[[package]]
@@ -1772,18 +1803,19 @@ typing-extensions = ">=4.7.1"
[[package]]
name = "setuptools"
version = "70.3.0"
version = "72.1.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"},
{file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"},
{file = "setuptools-72.1.0-py3-none-any.whl", hash = "sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1"},
{file = "setuptools-72.1.0.tar.gz", hash = "sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec"},
]
[package.extras]
core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "shellingham"
@@ -1928,4 +1960,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "17cd0c83d5aa272809743691d6b91ae3e7c3c98df8c7ce389aabc32f996b69d0"
content-hash = "bcabe584efc23c940efe1296b50b42e031f0e38c84d845b324d71070be96b361"
+1
View File
@@ -19,6 +19,7 @@ django-guid = "3.5.0"
djangorestframework = "3.15.2"
djangorestframework-jsonapi = "7.0.2"
drf-spectacular = "0.27.2"
drf-spectacular-jsonapi = "0.4.1"
gunicorn = "22.0.0"
psycopg2-binary = "2.9.9"
python = "^3.12"
+32
View File
@@ -0,0 +1,32 @@
from django.core.exceptions import ValidationError as django_validation_error
from rest_framework_json_api.exceptions import exception_handler
from rest_framework_json_api.serializers import ValidationError
class ModelValidationError(ValidationError):
def __init__(
self,
detail: str | None = None,
code: str | None = None,
pointer: str | None = None,
status_code: int = 400,
):
super().__init__(
detail=[
{
"detail": detail,
"status": str(status_code),
"source": {"pointer": pointer},
"code": code,
}
]
)
def custom_exception_handler(exc, context):
if isinstance(exc, django_validation_error):
if hasattr(exc, "error_dict"):
exc = ValidationError(exc.message_dict)
else:
exc = ValidationError(detail=exc.messages[0], code=exc.code)
return exception_handler(exc, context)
+15 -13
View File
@@ -1,6 +1,7 @@
from django_filters.rest_framework import FilterSet
from django_filters.rest_framework import FilterSet, BooleanFilter
from rest_framework_json_api.django_filters.backends import DjangoFilterBackend
from api.models import Provider
from api.rls import Tenant
@@ -13,24 +14,25 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
return None
class BaseFilter(FilterSet):
class TenantFilter(FilterSet):
class Meta:
model = None
model = Tenant
fields = {
"name": ["exact", "icontains"],
"inserted_at": ["exact", "gte", "lte"],
"updated_at": ["exact", "gte", "lte"],
}
class TenantFilter(BaseFilter):
class Meta(BaseFilter.Meta):
model = Tenant
class ProviderFilter(FilterSet):
connected = BooleanFilter()
class Meta:
model = Provider
fields = {
**BaseFilter.Meta.fields,
"name": ["exact", "icontains"],
"provider": ["exact"],
"provider_id": ["exact", "icontains"],
"alias": ["exact", "icontains"],
"inserted_at": ["exact", "gte", "lte"],
"updated_at": ["exact", "gte", "lte"],
}
class ProviderFilter(BaseFilter):
# TODO implement proper filter
pass
+109
View File
@@ -0,0 +1,109 @@
[
{
"model": "api.provider",
"pk": "37b065f8-26b0-4218-a665-0b23d07b27d9",
"fields": {
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-08-01T17:20:27.050Z",
"updated_at": "2024-08-01T17:20:27.050Z",
"provider": "gcp",
"provider_id": "a12322-test321",
"alias": "gcp_testing_2",
"connected": null,
"connection_last_checked_at": null,
"metadata": {}
}
},
{
"model": "api.provider",
"pk": "8851db6b-42e5-4533-aa9e-30a32d67e875",
"fields": {
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-08-01T17:19:42.453Z",
"updated_at": "2024-08-01T17:19:42.453Z",
"provider": "gcp",
"provider_id": "a12345-test123",
"alias": "gcp_testing_1",
"connected": null,
"connection_last_checked_at": null,
"metadata": {}
}
},
{
"model": "api.provider",
"pk": "b85601a8-4b45-4194-8135-03fb980ef428",
"fields": {
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-08-01T17:19:09.556Z",
"updated_at": "2024-08-01T17:19:09.556Z",
"provider": "aws",
"provider_id": "123456789020",
"alias": "aws_testing_2",
"connected": null,
"connection_last_checked_at": null,
"metadata": {}
}
},
{
"model": "api.provider",
"pk": "baa7b895-8bac-4f47-b010-4226d132856e",
"fields": {
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-08-01T17:20:16.962Z",
"updated_at": "2024-08-01T17:20:16.962Z",
"provider": "gcp",
"provider_id": "a12322-test123",
"alias": "gcp_testing_3",
"connected": null,
"connection_last_checked_at": null,
"metadata": {}
}
},
{
"model": "api.provider",
"pk": "d7c7ea89-d9af-423b-a364-1290dcad5a01",
"fields": {
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-08-01T17:18:58.132Z",
"updated_at": "2024-08-01T17:18:58.132Z",
"provider": "aws",
"provider_id": "123456789015",
"alias": "aws_testing_1",
"connected": null,
"connection_last_checked_at": null,
"metadata": {}
}
},
{
"model": "api.provider",
"pk": "1b59e032-3eb6-4694-93a5-df84cd9b3ce2",
"fields": {
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-08-06T16:03:26.176Z",
"updated_at": "2024-08-06T16:03:26.176Z",
"provider": "azure",
"provider_id": "8851db6b-42e5-4533-aa9e-30a32d67e875",
"alias": "azure_testing",
"connected": null,
"connection_last_checked_at": null,
"metadata": {},
"scanner_args": {}
}
},
{
"model": "api.provider",
"pk": "26e55a24-cb2c-4cef-ac87-6f91fddb2c97",
"fields": {
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-08-06T16:03:07.037Z",
"updated_at": "2024-08-06T16:03:07.037Z",
"provider": "kubernetes",
"provider_id": "kubernetes-test-12345",
"alias": "k8s_testing",
"connected": null,
"connection_last_checked_at": null,
"metadata": {},
"scanner_args": {}
}
}
]
@@ -1,8 +1,12 @@
import uuid
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import api.rls
DB_NAME = settings.DATABASES["default"]["NAME"]
DB_USER_NAME = settings.DATABASES["default"]["USER"]
DB_USER_PASSWORD = settings.DATABASES["default"]["PASSWORD"]
@@ -61,4 +65,80 @@ class Migration(migrations.Migration):
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE tenant TO {DB_USER_NAME};
"""
),
migrations.CreateModel(
name="Provider",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("inserted_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"provider",
models.CharField(
choices=[
("aws", "Aws"),
("azure", "Azure"),
("gcp", "Gcp"),
("kubernetes", "Kubernetes"),
],
default="aws",
max_length=10,
),
),
(
"provider_id",
models.CharField(
max_length=63,
validators=[django.core.validators.MinLengthValidator(3)],
),
),
(
"alias",
models.CharField(
blank=True,
null=True,
max_length=100,
validators=[django.core.validators.MinLengthValidator(3)],
),
),
("connected", models.BooleanField(blank=True, null=True)),
(
"connection_last_checked_at",
models.DateTimeField(blank=True, null=True),
),
("metadata", models.JSONField(default=dict)),
("scanner_args", models.JSONField(default=dict)),
(
"tenant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
),
),
],
options={
"abstract": False,
},
),
migrations.AddConstraint(
model_name="provider",
constraint=api.rls.RowLevelSecurityConstraint(
"tenant_id",
name="rls_on_provider",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
),
migrations.AddConstraint(
model_name="provider",
constraint=models.UniqueConstraint(
fields=("tenant_id", "provider", "provider_id"),
name="unique_provider_ids",
),
),
]
@@ -1,46 +0,0 @@
# Generated by Django 5.0.6 on 2024-08-01 15:11
import api.rls
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="Test",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
(
"tenant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
),
),
],
options={
"db_table": "test",
},
),
migrations.AddConstraint(
model_name="test",
constraint=api.rls.RowLevelSecurityConstraint(
"tenant_id",
name="rls_on_test",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
),
]
+85 -6
View File
@@ -1,17 +1,96 @@
import re
from uuid import uuid4, UUID
from django.core.validators import MinLengthValidator
from django.db import models
from django.utils.translation import gettext_lazy as _
from api.rls import RowLevelSecurityProtectedModel, RowLevelSecurityConstraint
from api.exceptions import ModelValidationError
from api.rls import RowLevelSecurityConstraint
from api.rls import RowLevelSecurityProtectedModel
class Test(RowLevelSecurityProtectedModel):
name = models.CharField(max_length=100)
class Provider(RowLevelSecurityProtectedModel):
class ProviderChoices(models.TextChoices):
AWS = "aws", _("AWS")
AZURE = "azure", _("Azure")
GCP = "gcp", _("GCP")
KUBERNETES = "kubernetes", _("Kubernetes")
class Meta:
db_table = "test"
@staticmethod
def validate_aws_provider_id(value):
if not re.match(r"^\d{12}$", value):
raise ModelValidationError(
detail="AWS provider ID must be exactly 12 digits.",
code="aws-provider-id",
pointer="/data/attributes/provider_id",
)
@staticmethod
def validate_azure_provider_id(value):
try:
val = UUID(value, version=4)
if str(val) != value:
raise ValueError
except ValueError:
raise ModelValidationError(
detail="Azure provider ID must be a valid UUID.",
code="azure-provider-id",
pointer="/data/attributes/provider_id",
)
@staticmethod
def validate_gcp_provider_id(value):
if not re.match(r"^[a-z][a-z0-9-]{5,29}$", value):
raise ModelValidationError(
detail="GCP provider ID must be 6 to 30 characters, start with a letter, and contain only lowercase "
"letters, numbers, and hyphens.",
code="gcp-provider-id",
pointer="/data/attributes/provider_id",
)
@staticmethod
def validate_kubernetes_provider_id(value):
if not re.match(r"^[a-z0-9]([-a-z0-9]{1,61}[a-z0-9])?$", value):
raise ModelValidationError(
detail="K8s provider ID must be up to 63 characters, start and end with a lowercase letter or number, "
"and contain only lowercase alphanumeric characters and hyphens.",
code="kubernetes-provider-id",
pointer="/data/attributes/provider_id",
)
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
updated_at = models.DateTimeField(auto_now=True, editable=False)
provider = models.CharField(
max_length=10, choices=ProviderChoices.choices, default=ProviderChoices.AWS
)
provider_id = models.CharField(max_length=63, validators=[MinLengthValidator(3)])
alias = models.CharField(
blank=True, null=True, max_length=100, validators=[MinLengthValidator(3)]
)
connected = models.BooleanField(null=True, blank=True)
connection_last_checked_at = models.DateTimeField(null=True, blank=True)
metadata = models.JSONField(default=dict, blank=True)
scanner_args = models.JSONField(default=dict, blank=True)
def clean(self):
super().clean()
getattr(self, f"validate_{self.provider}_provider_id")(self.provider_id)
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
class Meta(RowLevelSecurityProtectedModel.Meta):
constraints = [
models.UniqueConstraint(
fields=("tenant_id", "provider", "provider_id"),
name="unique_provider_ids",
),
RowLevelSecurityConstraint(
field="tenant_id",
name="rls_on_%(class)s",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
)
),
]
+6
View File
@@ -2,6 +2,8 @@ from typing import Any
from uuid import uuid4
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import DEFAULT_DB_ALIAS
from django.db import models
from django.db.backends.ddl_references import Statement, Table
@@ -108,6 +110,10 @@ class RowLevelSecurityConstraint(models.BaseConstraint):
path, _, kwargs = super().deconstruct()
return (path, (self.target_field,), kwargs)
def validate(self, model, instance, exclude=None, using=DEFAULT_DB_ALIAS): # noqa: F841
if not hasattr(instance, "tenant_id"):
raise ValidationError(f"{model.__name__} does not have a tenant_id field.")
class RowLevelSecurityProtectedModel(models.Model):
tenant = models.ForeignKey("Tenant", on_delete=models.CASCADE)
+77 -11
View File
@@ -1,14 +1,39 @@
from drf_spectacular.utils import extend_schema_field
from rest_framework_json_api import serializers
from rest_framework_json_api.serializers import ValidationError
from api.models import Test
from api.models import Provider
from api.rls import Tenant
# Base
class BaseSerializerV1(serializers.ModelSerializer):
def get_root_meta(self, _resource, _many):
return {"version": "v1"}
class BaseWriteSerializer(BaseSerializerV1):
def validate(self, data):
if hasattr(self, "initial_data"):
initial_data = set(self.initial_data.keys()) - {"id", "type"}
unknown_keys = initial_data - set(self.fields.keys())
if unknown_keys:
raise ValidationError(f"Invalid fields: {unknown_keys}")
return data
class RLSSerializer(BaseSerializerV1):
def create(self, validated_data):
tenant_id = self.context.get("tenant_id")
validated_data["tenant_id"] = tenant_id
return super().create(validated_data)
# Tenants
class TenantSerializer(BaseSerializerV1):
"""
Serializer for the Tenant model.
@@ -19,16 +44,57 @@ class TenantSerializer(BaseSerializerV1):
fields = "__all__"
class TestSerializer(BaseSerializerV1):
tenant = serializers.PrimaryKeyRelatedField(
queryset=Tenant.objects.all(), write_only=True
)
# Providers
class ProviderSerializer(RLSSerializer):
"""
Serializer for the Provider model.
"""
connection = serializers.SerializerMethodField(read_only=True)
class Meta:
model = Test
fields = "__all__"
model = Provider
fields = [
"id",
"inserted_at",
"updated_at",
"provider",
"provider_id",
"alias",
"connection",
"scanner_args",
]
extra_kwargs = {
"id": {"read_only": True},
"inserted_at": {"read_only": True},
"updated_at": {"read_only": True},
"connection": {"read_only": True},
}
def to_representation(self, instance):
representation = super().to_representation(instance)
representation.pop("tenant", None)
return representation
@extend_schema_field(
{
"type": "object",
"properties": {
"connected": {"type": "boolean"},
"last_checked_at": {"type": "string", "format": "date-time"},
},
}
)
def get_connection(self, obj):
return {
"connected": obj.connected,
"last_checked_at": obj.connection_last_checked_at,
}
class ProviderUpdateSerializer(BaseWriteSerializer):
"""
Serializer for updating the Provider model.
Only allows "alias" and "scanner_args" fields to be updated.
"""
class Meta:
model = Provider
fields = ["alias", "scanner_args"]
+857 -30
View File
@@ -7,30 +7,132 @@ info:
This file is auto-generated.
paths:
/api/v1/schema:
/api/v1/providers:
get:
operationId: schema_retrieve
description: |-
OpenApi3 schema for this API. Format can be selected via content negotiation.
- YAML: application/vnd.oai.openapi
- JSON: application/vnd.oai.openapi+json
operationId: providers_list
description: Retrieve a list of all providers with options for filtering by
various criteria.
summary: List all providers
parameters:
- in: query
name: format
name: fields[Provider]
schema:
type: string
enum:
- json
- yaml
type: array
items:
type: string
enum:
- inserted_at
- updated_at
- provider
- provider_id
- alias
- connection
- metadata
description: endpoint return only specific fields in the response on a per-type
basis by including a fields[TYPE] query parameter.
explode: false
- in: query
name: lang
name: filter[alias]
schema:
type: string
- in: query
name: filter[alias__icontains]
schema:
type: string
- in: query
name: filter[connected]
schema:
type: boolean
- in: query
name: filter[inserted_at]
schema:
type: string
format: date-time
- in: query
name: filter[inserted_at__gte]
schema:
type: string
format: date-time
- in: query
name: filter[inserted_at__lte]
schema:
type: string
format: date-time
- in: query
name: filter[provider]
schema:
type: string
enum:
- en
- aws
- azure
- gcp
- kubernetes
description: |-
* `aws` - Aws
* `azure` - Azure
* `gcp` - Gcp
* `kubernetes` - Kubernetes
- in: query
name: filter[provider_id]
schema:
type: string
- name: filter[search]
required: false
in: query
description: A search term.
schema:
type: string
- in: query
name: filter[updated_at]
schema:
type: string
format: date-time
- in: query
name: filter[updated_at__gte]
schema:
type: string
format: date-time
- in: query
name: filter[updated_at__lte]
schema:
type: string
format: date-time
- name: page[number]
required: false
in: query
description: A page number within the paginated result set.
schema:
type: integer
- name: page[size]
required: false
in: query
description: Number of results to return per page.
schema:
type: integer
- name: sort
required: false
in: query
description: '[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)'
schema:
type: array
items:
type: string
enum:
- provider
- -provider
- provider_id
- -provider_id
- alias
- -alias
- connected
- -connected
- inserted_at
- -inserted_at
- updated_at
- -updated_at
explode: false
tags:
- schema
- Provider
security:
- cookieAuth: []
- basicAuth: []
@@ -38,24 +140,749 @@ paths:
responses:
'200':
content:
application/vnd.oai.openapi:
application/vnd.api+json:
schema:
type: object
additionalProperties: {}
application/yaml:
schema:
type: object
additionalProperties: {}
application/vnd.oai.openapi+json:
schema:
type: object
additionalProperties: {}
application/json:
schema:
type: object
additionalProperties: {}
$ref: '#/components/schemas/PaginatedProviderList'
description: ''
post:
operationId: providers_create
description: Add a new provider to the system by providing the required provider
details.
summary: Create a new provider
tags:
- Provider
requestBody:
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/ProviderRequest'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/ProviderRequest'
multipart/form-data:
schema:
$ref: '#/components/schemas/ProviderRequest'
required: true
security:
- cookieAuth: []
- basicAuth: []
- {}
responses:
'201':
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/ProviderResponse'
description: ''
/api/v1/providers/{id}:
get:
operationId: providers_retrieve
description: Fetch detailed information about a specific provider by their ID.
summary: Retrieve data from a provider
parameters:
- in: query
name: fields[Provider]
schema:
type: array
items:
type: string
enum:
- inserted_at
- updated_at
- provider
- provider_id
- alias
- connection
- metadata
description: endpoint return only specific fields in the response on a per-type
basis by including a fields[TYPE] query parameter.
explode: false
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this provider.
required: true
tags:
- Provider
security:
- cookieAuth: []
- basicAuth: []
- {}
responses:
'200':
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/ProviderResponse'
description: ''
patch:
operationId: providers_partial_update
description: Update certain fields of an existing provider's information without
affecting other fields.
summary: Partially update a provider
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this provider.
required: true
tags:
- Provider
requestBody:
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/PatchedProviderRequest'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/PatchedProviderRequest'
multipart/form-data:
schema:
$ref: '#/components/schemas/PatchedProviderRequest'
required: true
security:
- cookieAuth: []
- basicAuth: []
- {}
responses:
'200':
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/ProviderResponse'
description: ''
delete:
operationId: providers_destroy
description: Remove a provider from the system by their ID.
summary: Delete a provider
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this provider.
required: true
tags:
- Provider
security:
- cookieAuth: []
- basicAuth: []
- {}
responses:
'202':
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/SerializerMetaclassResponse'
description: ''
/api/v1/providers/{id}/connection:
post:
operationId: providers_connection_create
description: Try to verify connection. For instance, Role & Credentials are
set correctly
summary: Check connection
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this provider.
required: true
tags:
- Provider
security:
- cookieAuth: []
- basicAuth: []
- {}
responses:
'202':
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/SerializerMetaclassResponse'
description: ''
/api/v1/tenants:
get:
operationId: tenants_list
description: Retrieve a list of all tenants with options for filtering by various
criteria.
summary: List all tenants
parameters:
- in: query
name: fields[Tenant]
schema:
type: array
items:
type: string
enum:
- inserted_at
- updated_at
- name
description: endpoint return only specific fields in the response on a per-type
basis by including a fields[TYPE] query parameter.
explode: false
- in: query
name: filter[inserted_at]
schema:
type: string
format: date-time
- in: query
name: filter[inserted_at__gte]
schema:
type: string
format: date-time
- in: query
name: filter[inserted_at__lte]
schema:
type: string
format: date-time
- in: query
name: filter[name]
schema:
type: string
- in: query
name: filter[name__icontains]
schema:
type: string
- name: filter[search]
required: false
in: query
description: A search term.
schema:
type: string
- in: query
name: filter[updated_at]
schema:
type: string
format: date-time
- in: query
name: filter[updated_at__gte]
schema:
type: string
format: date-time
- in: query
name: filter[updated_at__lte]
schema:
type: string
format: date-time
- name: page[number]
required: false
in: query
description: A page number within the paginated result set.
schema:
type: integer
- name: page[size]
required: false
in: query
description: Number of results to return per page.
schema:
type: integer
- name: sort
required: false
in: query
description: '[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)'
schema:
type: array
items:
type: string
enum:
- name
- -name
- inserted_at
- -inserted_at
- updated_at
- -updated_at
explode: false
tags:
- Tenant
security:
- cookieAuth: []
- basicAuth: []
- {}
responses:
'200':
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/PaginatedTenantList'
description: ''
post:
operationId: tenants_create
description: Add a new tenant to the system by providing the required tenant
details.
summary: Create a new tenant
tags:
- Tenant
requestBody:
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/TenantRequest'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/TenantRequest'
multipart/form-data:
schema:
$ref: '#/components/schemas/TenantRequest'
required: true
security:
- cookieAuth: []
- basicAuth: []
- {}
responses:
'201':
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/TenantResponse'
description: ''
/api/v1/tenants/{id}:
get:
operationId: tenants_retrieve
description: Fetch detailed information about a specific tenant by their ID.
summary: Retrieve data from a tenant
parameters:
- in: query
name: fields[Tenant]
schema:
type: array
items:
type: string
enum:
- inserted_at
- updated_at
- name
description: endpoint return only specific fields in the response on a per-type
basis by including a fields[TYPE] query parameter.
explode: false
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this tenant.
required: true
tags:
- Tenant
security:
- cookieAuth: []
- basicAuth: []
- {}
responses:
'200':
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/TenantResponse'
description: ''
patch:
operationId: tenants_partial_update
description: Update certain fields of an existing tenant's information without
affecting other fields.
summary: Partially update a tenant
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this tenant.
required: true
tags:
- Tenant
requestBody:
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/PatchedTenantRequest'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/PatchedTenantRequest'
multipart/form-data:
schema:
$ref: '#/components/schemas/PatchedTenantRequest'
required: true
security:
- cookieAuth: []
- basicAuth: []
- {}
responses:
'200':
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/TenantResponse'
description: ''
delete:
operationId: tenants_destroy
description: Remove a tenant from the system by their ID.
summary: Delete a tenant
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this tenant.
required: true
tags:
- Tenant
security:
- cookieAuth: []
- basicAuth: []
- {}
responses:
'204':
description: No response body
components:
schemas:
PaginatedProviderList:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Provider'
required:
- data
PaginatedTenantList:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Tenant'
required:
- data
PatchedProviderRequest:
type: object
properties:
data:
type: object
required:
- type
- id
additionalProperties: false
properties:
type:
type: string
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common attributes
and relationships.
enum:
- Provider
id:
type: string
format: uuid
attributes:
type: object
properties:
inserted_at:
type: string
format: date-time
readOnly: true
updated_at:
type: string
format: date-time
readOnly: true
provider:
enum:
- aws
- azure
- gcp
- kubernetes
type: string
description: |-
* `aws` - Aws
* `azure` - Azure
* `gcp` - Gcp
* `kubernetes` - Kubernetes
provider_id:
type: string
minLength: 3
maxLength: 63
alias:
type: string
minLength: 3
maxLength: 100
connection:
type: object
properties:
connected:
type: boolean
last_checked_at:
type: string
format: date-time
readOnly: true
metadata: {}
required:
- provider_id
- alias
required:
- data
PatchedTenantRequest:
type: object
properties:
data:
type: object
required:
- type
- id
additionalProperties: false
properties:
type:
type: string
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common attributes
and relationships.
enum:
- Tenant
id:
type: string
format: uuid
attributes:
type: object
properties:
inserted_at:
type: string
format: date-time
readOnly: true
updated_at:
type: string
format: date-time
readOnly: true
name:
type: string
minLength: 1
maxLength: 100
required:
- name
required:
- data
Provider:
type: object
required:
- type
- id
additionalProperties: false
properties:
type:
allOf:
- $ref: '#/components/schemas/ProviderTypeEnum'
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common attributes
and relationships.
id:
type: string
format: uuid
attributes:
type: object
properties:
inserted_at:
type: string
format: date-time
readOnly: true
updated_at:
type: string
format: date-time
readOnly: true
provider:
enum:
- aws
- azure
- gcp
- kubernetes
type: string
description: |-
* `aws` - Aws
* `azure` - Azure
* `gcp` - Gcp
* `kubernetes` - Kubernetes
provider_id:
type: string
maxLength: 63
minLength: 3
alias:
type: string
maxLength: 100
minLength: 3
connection:
type: object
properties:
connected:
type: boolean
last_checked_at:
type: string
format: date-time
readOnly: true
metadata: {}
required:
- provider_id
- alias
ProviderRequest:
type: object
properties:
data:
type: object
required:
- type
additionalProperties: false
properties:
type:
type: string
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common attributes
and relationships.
enum:
- Provider
attributes:
type: object
properties:
inserted_at:
type: string
format: date-time
readOnly: true
updated_at:
type: string
format: date-time
readOnly: true
provider:
enum:
- aws
- azure
- gcp
- kubernetes
type: string
description: |-
* `aws` - Aws
* `azure` - Azure
* `gcp` - Gcp
* `kubernetes` - Kubernetes
provider_id:
type: string
minLength: 3
maxLength: 63
alias:
type: string
minLength: 3
maxLength: 100
connection:
type: object
properties:
connected:
type: boolean
last_checked_at:
type: string
format: date-time
readOnly: true
metadata: {}
required:
- provider_id
- alias
required:
- data
ProviderResponse:
type: object
properties:
data:
$ref: '#/components/schemas/Provider'
required:
- data
ProviderTypeEnum:
type: string
enum:
- Provider
SerializerMetaclassResponse:
type: object
properties:
data:
$ref: '#/components/schemas/Provider'
required:
- data
Tenant:
type: object
required:
- type
- id
additionalProperties: false
properties:
type:
allOf:
- $ref: '#/components/schemas/TenantTypeEnum'
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common attributes
and relationships.
id:
type: string
format: uuid
attributes:
type: object
properties:
inserted_at:
type: string
format: date-time
readOnly: true
updated_at:
type: string
format: date-time
readOnly: true
name:
type: string
maxLength: 100
required:
- name
TenantRequest:
type: object
properties:
data:
type: object
required:
- type
additionalProperties: false
properties:
type:
type: string
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common attributes
and relationships.
enum:
- Tenant
attributes:
type: object
properties:
inserted_at:
type: string
format: date-time
readOnly: true
updated_at:
type: string
format: date-time
readOnly: true
name:
type: string
minLength: 1
maxLength: 100
required:
- name
required:
- data
TenantResponse:
type: object
properties:
data:
$ref: '#/components/schemas/Tenant'
required:
- data
TenantTypeEnum:
type: string
enum:
- Tenant
securitySchemes:
basicAuth:
type: http
+1 -2
View File
@@ -4,7 +4,6 @@ from django.db.migrations.recorder import MigrationRecorder
from django.db.utils import ConnectionRouter
from api.db_router import MainRouter
from api.models import Test
from api.rls import Tenant
from config.django.base import DATABASE_ROUTERS as PROD_DATABASE_ROUTERS
@@ -17,7 +16,7 @@ class TestMainDatabaseRouter:
yield ConnectionRouter()
settings.DATABASE_ROUTERS = testing_routers
@pytest.mark.parametrize("api_model", [Tenant, Test])
@pytest.mark.parametrize("api_model", [Tenant])
def test_router_api_models(self, api_model, router):
assert router.db_for_read(api_model) == MainRouter.default_db
assert router.db_for_write(api_model) == MainRouter.default_db
+346 -2
View File
@@ -1,10 +1,15 @@
from datetime import datetime
import pytest
from django.urls import reverse
from rest_framework import status
from api.models import Provider
from api.rls import Tenant
API_JSON_CONTENT_TYPE = "application/vnd.api+json"
# TODO Change to 401 when authentication/authorization is implemented
NO_TENANT_HTTP_STATUS = status.HTTP_403_FORBIDDEN
@pytest.mark.django_db
@@ -50,8 +55,7 @@ class TestTenantViewSet:
assert response.status_code == status.HTTP_200_OK
assert response.json()["data"]["attributes"]["name"] == tenant1.name
def test_tenants_invalid_retrieve(self, client, tenants):
tenant1, _ = tenants
def test_tenants_invalid_retrieve(self, client):
response = client.get(reverse("tenant-detail", kwargs={"pk": "random_id"}))
assert response.status_code == status.HTTP_404_NOT_FOUND
@@ -177,3 +181,343 @@ class TestTenantViewSet:
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 2
assert response.json()["data"][0]["attributes"]["name"] == tenant2.name
@pytest.mark.django_db
class TestProviderViewSet:
@pytest.fixture
def providers(self, get_tenant):
tenant = get_tenant
provider1 = Provider.objects.create(
provider="aws",
provider_id="123456789012",
alias="aws_testing_1",
tenant_id=tenant.id,
)
provider2 = Provider.objects.create(
provider="aws",
provider_id="123456789013",
alias="aws_testing_2",
tenant_id=tenant.id,
)
provider3 = Provider.objects.create(
provider="gcp",
provider_id="a12322-test321",
alias="gcp_testing",
tenant_id=tenant.id,
)
provider4 = Provider.objects.create(
provider="kubernetes",
provider_id="kubernetes-test-12345",
alias="k8s_testing",
tenant_id=tenant.id,
)
provider5 = Provider.objects.create(
provider="azure",
provider_id="37b065f8-26b0-4218-a665-0b23d07b27d9",
alias="azure_testing",
tenant_id=tenant.id,
)
return provider1, provider2, provider3, provider4, provider5
def test_providers_rls(self, client):
response = client.get(reverse("provider-list"))
assert response.status_code == NO_TENANT_HTTP_STATUS
def test_providers_list(self, client, providers, tenant_header):
response = client.get(reverse("provider-list"), headers=tenant_header)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == len(providers)
def test_providers_retrieve(self, client, providers, tenant_header):
provider1, *_ = providers
response = client.get(
reverse("provider-detail", kwargs={"pk": provider1.id}),
headers=tenant_header,
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["data"]["attributes"]["provider"] == provider1.provider
assert (
response.json()["data"]["attributes"]["provider_id"]
== provider1.provider_id
)
assert response.json()["data"]["attributes"]["alias"] == provider1.alias
def test_providers_invalid_retrieve(self, client, tenant_header):
response = client.get(
reverse("provider-detail", kwargs={"pk": "random_id"}),
headers=tenant_header,
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.parametrize(
"provider_json_payload",
(
[
{"provider": "aws", "provider_id": "111111111111", "alias": "test"},
{"provider": "gcp", "provider_id": "a12322-test54321", "alias": "test"},
{
"provider": "kubernetes",
"provider_id": "kubernetes-test-123456789",
"alias": "test",
},
{
"provider": "azure",
"provider_id": "8851db6b-42e5-4533-aa9e-30a32d67e875",
"alias": "test",
},
]
),
)
def test_providers_create_valid(self, client, tenant_header, provider_json_payload):
response = client.post(
reverse("provider-list"),
data=provider_json_payload,
format="json",
headers=tenant_header,
)
assert response.status_code == status.HTTP_201_CREATED
assert Provider.objects.count() == 1
assert Provider.objects.get().provider == provider_json_payload["provider"]
assert (
Provider.objects.get().provider_id == provider_json_payload["provider_id"]
)
assert Provider.objects.get().alias == provider_json_payload["alias"]
@pytest.mark.parametrize(
"provider_json_payload, error_code",
(
[
(
{"provider": "aws", "provider_id": "1", "alias": "test"},
"min_length",
),
(
{
"provider": "aws",
"provider_id": "1111111111111",
"alias": "test",
},
"aws-provider-id",
),
(
{"provider": "aws", "provider_id": "aaaaaaaaaaaa", "alias": "test"},
"aws-provider-id",
),
(
{"provider": "gcp", "provider_id": "1234asdf", "alias": "test"},
"gcp-provider-id",
),
(
{
"provider": "kubernetes",
"provider_id": "-1234asdf",
"alias": "test",
},
"kubernetes-provider-id",
),
(
{
"provider": "azure",
"provider_id": "8851db6b-42e5-4533-aa9e-30a32d67e87",
"alias": "test",
},
"azure-provider-id",
),
]
),
)
def test_providers_invalid_create(
self, client, tenant_header, provider_json_payload, error_code
):
response = client.post(
reverse("provider-list"),
data=provider_json_payload,
format="json",
headers=tenant_header,
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json()["errors"][0]["code"] == error_code
assert (
response.json()["errors"][0]["source"]["pointer"]
== "/data/attributes/provider_id"
)
def test_providers_partial_update(self, client, providers, tenant_header):
provider1, *_ = providers
new_alias = "This is the new name"
payload = {
"data": {
"type": "Provider",
"id": provider1.id,
"attributes": {"alias": new_alias},
},
}
response = client.patch(
reverse("provider-detail", kwargs={"pk": provider1.id}),
data=payload,
content_type=API_JSON_CONTENT_TYPE,
headers=tenant_header,
)
assert response.status_code == status.HTTP_200_OK
provider1.refresh_from_db()
assert provider1.alias == new_alias
def test_providers_partial_update_invalid_content_type(
self, client, providers, tenant_header
):
provider1, *_ = providers
response = client.patch(
reverse("provider-detail", kwargs={"pk": provider1.id}),
data={},
headers=tenant_header,
)
assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
def test_providers_partial_update_invalid_content(
self, client, providers, tenant_header
):
provider1, *_ = providers
new_name = "This is the new name"
payload = {"alias": new_name}
response = client.patch(
reverse("provider-detail", kwargs={"pk": provider1.id}),
data=payload,
content_type=API_JSON_CONTENT_TYPE,
headers=tenant_header,
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
@pytest.mark.parametrize(
"attribute_key, attribute_value",
[
("provider", "aws"),
("provider_id", "123456789012"),
],
)
def test_providers_partial_update_invalid_fields(
self, client, providers, tenant_header, attribute_key, attribute_value
):
provider1, *_ = providers
payload = {
"data": {
"type": "Provider",
"id": provider1.id,
"attributes": {attribute_key: attribute_value},
},
}
response = client.patch(
reverse("provider-detail", kwargs={"pk": provider1.id}),
data=payload,
content_type=API_JSON_CONTENT_TYPE,
headers=tenant_header,
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_providers_delete(self, client, providers, tenant_header):
provider1, *_ = providers
response = client.delete(
reverse("provider-detail", kwargs={"pk": provider1.id}),
headers=tenant_header,
)
assert response.status_code == status.HTTP_202_ACCEPTED
assert "Content-Location" in response.headers
assert Provider.objects.count() == len(providers) - 1
# TODO Assert a task is returned when they are implemented
def test_providers_delete_invalid(self, client, tenant_header):
response = client.delete(
reverse("provider-detail", kwargs={"pk": "random_id"}),
headers=tenant_header,
)
assert response.status_code == status.HTTP_404_NOT_FOUND
def test_providers_connection(self, client, providers, tenant_header):
provider1, *_ = providers
assert provider1.connected is None
assert provider1.connection_last_checked_at is None
response = client.post(
reverse("provider-connection", kwargs={"pk": provider1.id}),
headers=tenant_header,
)
assert response.status_code == status.HTTP_202_ACCEPTED
assert "Content-Location" in response.headers
# TODO Assert a task is returned when they are implemented
provider1.refresh_from_db()
assert provider1.connected is True
assert isinstance(provider1.connection_last_checked_at, datetime)
def test_providers_connection_invalid_provider(
self, client, providers, tenant_header
):
response = client.post(
reverse("provider-connection", kwargs={"pk": "random_id"}),
headers=tenant_header,
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.parametrize(
"filter_name, filter_value",
(
[
("provider", "aws"),
("provider_id", "12345"),
("alias", "test"),
("search", "test"),
("inserted_at", "2024-01-01 00:00:00"),
("updated_at", "2024-01-01 00:00:00"),
]
),
)
def test_providers_filters(self, client, tenant_header, filter_name, filter_value):
response = client.get(
reverse("provider-list"),
{f"filter[{filter_name}]": filter_value},
headers=tenant_header,
)
assert response.status_code == status.HTTP_200_OK
@pytest.mark.parametrize(
"filter_name",
(
[
"provider", # Valid filter, invalid value
"invalid",
]
),
)
def test_providers_filters_invalid(self, client, tenant_header, filter_name):
response = client.get(
reverse("provider-list"),
{f"filter[{filter_name}]": "whatever"},
headers=tenant_header,
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
@pytest.mark.parametrize(
"sort_field",
(
[
"provider",
"provider_id",
"alias",
"connected",
"inserted_at",
"updated_at",
]
),
)
def test_providers_sort(self, client, tenant_header, sort_field):
response = client.get(
reverse("provider-list"), {"sort": sort_field}, headers=tenant_header
)
assert response.status_code == status.HTTP_200_OK
def test_providers_sort_invalid(self, client, tenant_header):
response = client.get(
reverse("provider-list"), {"sort": "invalid"}, headers=tenant_header
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
+1 -2
View File
@@ -2,7 +2,7 @@ from django.urls import path, include
from drf_spectacular.views import SpectacularRedocView
from rest_framework import routers
from api.views.v1.views import SchemaView, TenantViewSet, ProviderViewSet, TestView
from api.views.v1.views import SchemaView, TenantViewSet, ProviderViewSet
router = routers.DefaultRouter(trailing_slash=False)
@@ -13,5 +13,4 @@ urlpatterns = [
path("", include(router.urls)),
path("schema", SchemaView.as_view(), name="schema"),
path("docs", SpectacularRedocView.as_view(url_name="schema"), name="docs"),
path("test", TestView.as_view(), name="test"),
]
+16
View File
@@ -1,3 +1,4 @@
from rest_framework.exceptions import NotAuthenticated
from rest_framework.filters import SearchFilter
from rest_framework_json_api import filters
from rest_framework_json_api.views import ModelViewSet
@@ -21,3 +22,18 @@ class BaseViewSet(ModelViewSet):
def get_queryset(self):
raise NotImplementedError
class BaseRLSViewSet(BaseViewSet):
def initial(self, request, *args, **kwargs):
super().initial(request, *args, **kwargs)
if "X-Tenant-ID" not in request.headers:
# This will return a 403 until we implement authentication/authorization
# https://www.django-rest-framework.org/api-guide/authentication/#unauthorized-and-forbidden-responses
raise NotAuthenticated("X-Tenant-ID header is required")
def get_serializer_context(self):
context = super().get_serializer_context()
tenant_id = self.request.headers.get("X-Tenant-ID")
context["tenant_id"] = tenant_id
return context
+63 -223
View File
@@ -1,17 +1,23 @@
from django.conf import settings as django_settings
from django.urls import reverse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
from drf_spectacular.settings import spectacular_settings
from drf_spectacular.utils import extend_schema, extend_schema_view
from drf_spectacular.views import SpectacularAPIView
from rest_framework import status, viewsets
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.generics import get_object_or_404
from rest_framework_json_api.views import Response
from api.filters import ProviderFilter
from api.filters import TenantFilter
from rest_framework.generics import ListCreateAPIView
from api.models import Test
from api.models import Provider
from api.rls import Tenant
from api.serializers import TenantSerializer, TestSerializer
from api.serializers import ProviderSerializer, ProviderUpdateSerializer
from api.serializers import TenantSerializer
from api.views.base_views import BaseRLSViewSet
from api.views.base_views import BaseViewSet
CACHE_DECORATOR = cache_control(
@@ -86,240 +92,74 @@ class TenantViewSet(BaseViewSet):
partial_update=extend_schema(
summary="Partially update a provider",
description="Update certain fields of an existing provider's information without affecting other fields.",
request=ProviderUpdateSerializer,
responses={200: ProviderSerializer},
),
destroy=extend_schema(
summary="Delete a provider",
description="Remove a provider from the system by their ID.",
# TODO Update with task response when implemented
responses={202: ProviderSerializer},
),
)
@method_decorator(CACHE_DECORATOR, name="list")
@method_decorator(CACHE_DECORATOR, name="retrieve")
class ProviderViewSet(viewsets.ViewSet):
content_location_header = {
"Content-Location": "https://api.prowler.com/api/v1/tasks/5234"
}
mock_links_data = {
"first": "https://api.prowler.com/api/v1/providers?page%5Bnumber%5D=1",
"last": "https://api.prowler.com/api/v1/providers?page%5Bnumber%5D=1",
"next": None,
"prev": None,
}
mock_provider_data = [
{
"id": "5fd8f121-269e-4715-84cf-f92373f15dfa",
"type": "providers",
"attributes": {
"provider": "aws",
"provider_id": "1234567890",
"alias": "mock_aws_connected",
"connection": {
"connected": True,
"last_checked_at": "2024-07-17T09:55:14.191475Z",
},
"scanner_args": {
"only_logs": True,
"excluded_checks": [
"awslambda_function_no_secrets_in_code",
"cloudwatch_log_group_no_secrets_in_logs",
],
"aws_retries_max_attempts": 5,
},
"last_resource_count": 1234,
"inserted_at": "2024-07-17T09:55:14.191475Z",
"updated_at": "2024-07-17T09:55:14.191475Z",
"created_by": {
"object": "user",
"id": "eea048ab-7cb3-47eb-9e5e-dce591ade41f",
},
},
},
{
"id": "16aaeb4e-d3cd-4bb6-86f8-6c39cf93821e",
"type": "providers",
"attributes": {
"provider": "aws",
"provider_id": "1234567891",
"alias": "mock_aws_not_connected",
"connection": {
"connected": False,
"last_checked_at": "2024-07-17T09:55:18.987425Z",
},
"scanner_args": {
"only_logs": True,
"excluded_checks": [
"awslambda_function_no_secrets_in_code",
"cloudwatch_log_group_no_secrets_in_logs",
],
"aws_retries_max_attempts": 5,
},
"last_resource_count": 0,
"inserted_at": "2024-07-17T09:55:18.987425Z",
"updated_at": "2024-07-17T09:55:18.987425Z",
"created_by": {
"object": "user",
"id": "a8f5e964-5964-4aaf-9176-844e2c3b0716",
},
},
},
{
"id": "63f16b03-7849-4054-b40b-300e331f46f0",
"type": "providers",
"attributes": {
"provider": "gcp",
"provider_id": "1234567895",
"alias": "mock_gcp",
"connection": {
"connected": True,
"last_checked_at": "2024-07-17T09:55:18.987425Z",
},
"scanner_args": {
"only_logs": True,
"excluded_checks": [
"apikeys_key_exists",
"cloudsql_instance_public_ip",
],
"excluded_services": ["kms"],
},
"last_resource_count": 1111,
"inserted_at": "2024-07-17T09:55:18.987425Z",
"updated_at": "2024-07-17T09:55:18.987425Z",
"created_by": {
"object": "user",
"id": "eea048ab-7cb3-47eb-9e5e-dce591ade41f",
},
},
},
class ProviderViewSet(BaseRLSViewSet):
queryset = Provider.objects.all()
serializer_class = ProviderSerializer
http_method_names = ["get", "post", "patch", "delete"]
filterset_class = ProviderFilter
search_fields = ["provider", "provider_id", "alias"]
ordering = ["inserted_at"]
ordering_fields = [
"provider",
"provider_id",
"alias",
"connected",
"inserted_at",
"updated_at",
]
mock_meta_data = {"version": "v1"}
http_method_names = ["get", "post", "patch", "delete"]
resource_name = False
def list(self, request, *args, **kwargs):
mock_data = {
"links": self.mock_links_data,
"data": self.mock_provider_data,
"meta": {
"pagination": {
"page": 1,
"pages": 1,
"count": len(self.mock_provider_data),
},
**self.mock_meta_data,
},
}
return Response(mock_data, status=status.HTTP_200_OK)
def retrieve(self, request, pk=None, *args, **kwargs):
provider = next(
(item for item in self.mock_provider_data if item["id"] == pk), None
)
if provider is not None:
return Response({**provider, **self.mock_meta_data})
return Response(
[{"detail": "Not found.", "status": "404", "code": "not_found"}],
status=status.HTTP_404_NOT_FOUND,
)
def create(self, request, *args, **kwargs):
return Response(
{"data": self.mock_provider_data[0], "meta": self.mock_meta_data},
status.HTTP_201_CREATED,
)
def get_queryset(self):
return Provider.objects.all()
def partial_update(self, request, *args, **kwargs):
return Response(
{"data": self.mock_provider_data[0], "meta": self.mock_meta_data},
status.HTTP_200_OK,
)
def destroy(self, request, *args, **kwargs):
task_mock = {
"data": {
"type": "tasks",
"id": "5234",
"attributes": {
"status": "pending",
},
"links": {"self": "/tasks/5234"},
}
}
return Response(
task_mock,
status=status.HTTP_202_ACCEPTED,
headers=self.content_location_header,
)
instance = self.get_object()
serializer = ProviderUpdateSerializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
read_serializer = ProviderSerializer(instance)
return Response(data=read_serializer.data, status=status.HTTP_200_OK)
@extend_schema(
summary="Check connection",
description="Try to verify connection. For instance, Role & Credentials are set correctly",
request=None,
# TODO Update with task response when implemented
responses={202: ProviderSerializer},
)
@action(detail=False, methods=["post"], url_path="check_connection")
def check_connection(self, request):
connection_data_mock = {
"meta": {"version": "v1"},
"data": {
"id": "63f16b03-7849-4054-b40b-300e331f46f0",
"type": "providers",
"attributes": {
"provider": "gcp",
"provider_id": "1234567895",
"alias": "mock_gcp",
"connection": {
"connected": True,
"last_checked_at": "2024-07-17T09:55:18.987425Z",
},
"scanner_args": {
"only_logs": True,
"excluded_checks": [
"apikeys_key_exists",
"cloudsql_instance_public_ip",
],
"excluded_services": ["kms"],
},
"last_resource_count": 1111,
"inserted_at": "2024-07-17T09:55:18.987425Z",
"updated_at": "2024-07-17T09:55:18.987425Z",
"created_by": {
"object": "user",
"id": "eea048ab-7cb3-47eb-9e5e-dce591ade41f",
},
},
},
"relationships": {
"tenant": {
"links": {
"self": "http://api.prowler.com/api/v1/providers/41f1d90e-5e3b-4eb0-b565-893f277d96c1",
"related": "http://api.prowler.com/api/v1/tenants/22f9341e-4e13-44a8-996a-11f370696c54",
},
"data": {"type": "tenants", "id": "uuid"},
},
"credential": {
"links": {
"self": "http://api.prowler.com/api/v1/providers/41f1d90e-5e3b-4eb0-b565-893f277d96c1",
"related": "http://api.prowler.com/api/v1/credentials/22f9341e-4e13-44a8-996a-11f370696c54",
},
"data": {"type": "credentials", "id": "uuid"},
},
"schedules": {
"links": {
"self": "http://api.prowler.com/api/v1/providers/41f1d90e-5e3b-4eb0-b565-893f277d96c1",
"related": "http://api.prowler.com/api/v1/schedules/22f9341e-4e13-44a8-996a-11f370696c54",
},
"data": {
"type": "schedules",
"id": "ee15292e-687f-41cf-8652-30a8b7cded35",
},
},
},
@action(detail=True, methods=["post"], url_name="connection")
def connection(self, request, pk=None):
# TODO Connection check to third parties
# TODO Returning provider data as a placeholder for tasks. We will do something similar for the created task
provider = get_object_or_404(Provider, pk=pk)
provider.connected = True
provider.connection_last_checked_at = timezone.now()
provider.save()
serializer = ProviderSerializer(provider)
return Response(
data=serializer.data,
status=status.HTTP_202_ACCEPTED,
headers={"Content-Location": reverse("provider-detail", kwargs={"pk": pk})},
)
def destroy(self, request, *args, **kwargs):
response = super().destroy(request, *args, **kwargs)
# TODO Background task to delete provider. For now, it will delete the provider from the system
# Same as /connection endpoint
response.status_code = status.HTTP_202_ACCEPTED
response.headers = {
"Content-Location": "/api/v1/tasks/5234",
**response.headers,
}
return Response(connection_data_mock, status=status.HTTP_200_OK)
class TestView(ListCreateAPIView):
serializer_class = TestSerializer
queryset = Test.objects.all()
return response
+7 -3
View File
@@ -57,10 +57,10 @@ TEMPLATES = [
]
REST_FRAMEWORK = {
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_SCHEMA_CLASS": "drf_spectacular_jsonapi.schemas.openapi.JsonApiAutoSchema",
"PAGE_SIZE": 10,
"EXCEPTION_HANDLER": "rest_framework_json_api.exceptions.exception_handler",
"DEFAULT_PAGINATION_CLASS": "rest_framework_json_api.pagination.JsonApiPageNumberPagination",
"EXCEPTION_HANDLER": "api.exceptions.custom_exception_handler",
"DEFAULT_PAGINATION_CLASS": "drf_spectacular_jsonapi.schemas.pagination.JsonApiPageNumberPagination",
"DEFAULT_PARSER_CLASSES": (
"rest_framework_json_api.parsers.JSONParser",
"rest_framework.parsers.FormParser",
@@ -84,6 +84,10 @@ REST_FRAMEWORK = {
SPECTACULAR_SETTINGS = {
"SERVE_INCLUDE_SCHEMA": False,
"COMPONENT_SPLIT_REQUEST": True,
"PREPROCESSING_HOOKS": [
"drf_spectacular_jsonapi.hooks.fix_nested_path_parameters",
],
}
WSGI_APPLICATION = "config.wsgi.application"
+17
View File
@@ -2,7 +2,24 @@ import logging
import pytest
from api.rls import Tenant
@pytest.fixture(autouse=True)
def disable_logging():
logging.disable(logging.CRITICAL)
@pytest.fixture
def get_tenant():
tenant = Tenant.objects.create(
name="Tenant One",
inserted_at="2023-01-01T00:00:00Z",
updated_at="2023-01-02T00:00:00Z",
)
return tenant
@pytest.fixture
def tenant_header(get_tenant):
return {"X-Tenant-ID": str(get_tenant.id)}