@shortlink-org/portolan 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +387 -0
- package/catalog/enum_test.go +46 -0
- package/catalog/model.go +932 -0
- package/catalog/roundtrip_test.go +185 -0
- package/catalog/via_test.go +38 -0
- package/cli/portolan.mjs +368 -0
- package/go.mod +14 -0
- package/go.sum +14 -0
- package/index.html +13 -0
- package/internal/commands/cargo.go +154 -0
- package/internal/commands/commands.go +143 -0
- package/internal/commands/commands_test.go +323 -0
- package/internal/commands/gradle.go +77 -0
- package/internal/commands/justfile.go +82 -0
- package/internal/commands/makefile.go +106 -0
- package/internal/commands/maven.go +93 -0
- package/internal/commands/packagejson.go +159 -0
- package/internal/commands/pyproject.go +169 -0
- package/internal/commands/taskfile.go +101 -0
- package/internal/commands/testdata/estate/.cargo/config.toml +7 -0
- package/internal/commands/testdata/estate/Makefile +33 -0
- package/internal/commands/testdata/estate/Taskfile.yml +21 -0
- package/internal/commands/testdata/estate/build.gradle.kts +21 -0
- package/internal/commands/testdata/estate/justfile +20 -0
- package/internal/commands/testdata/estate/package.json +13 -0
- package/internal/commands/testdata/estate/pom.xml +29 -0
- package/internal/commands/testdata/estate/pyproject.toml +27 -0
- package/internal/commands/testdata/estate/xtask/src/main.rs +27 -0
- package/internal/commands/testdata/golden/commands.json +305 -0
- package/internal/gohttp/analyze.go +2500 -0
- package/internal/gohttp/endpoints.go +1067 -0
- package/internal/gohttp/roots.go +320 -0
- package/internal/gohttp/typed.go +143 -0
- package/internal/goscan/constants.go +85 -0
- package/internal/goscan/goscan_test.go +227 -0
- package/internal/goscan/names.go +52 -0
- package/internal/goscan/parse_test.go +11 -0
- package/internal/goscan/source.go +36 -0
- package/internal/goscan/tree.go +155 -0
- package/internal/goscan/types.go +99 -0
- package/internal/wsdl/ids.go +127 -0
- package/internal/wsdl/ids_test.go +21 -0
- package/internal/wsdl/model.go +62 -0
- package/internal/wsdl/parse.go +920 -0
- package/internal/wsdl/parse_test.go +133 -0
- package/package.json +117 -0
- package/plugin/describe.go +107 -0
- package/plugin/describe_test.go +114 -0
- package/plugin/protocol.go +117 -0
- package/plugin/schematest/schematest.go +126 -0
- package/plugins/README.md +1160 -0
- package/plugins/extract-adr/describe.go +19 -0
- package/plugins/extract-adr/describe_test.go +11 -0
- package/plugins/extract-adr/extract.go +153 -0
- package/plugins/extract-adr/extract_test.go +350 -0
- package/plugins/extract-adr/history.go +99 -0
- package/plugins/extract-adr/main.go +65 -0
- package/plugins/extract-adr/options.schema.json +26 -0
- package/plugins/extract-adr/parse.go +642 -0
- package/plugins/extract-adr/parse_test.go +405 -0
- package/plugins/extract-asyncapi/describe.go +19 -0
- package/plugins/extract-asyncapi/describe_test.go +11 -0
- package/plugins/extract-asyncapi/extract.go +315 -0
- package/plugins/extract-asyncapi/extract_test.go +197 -0
- package/plugins/extract-asyncapi/main.go +48 -0
- package/plugins/extract-asyncapi/options.schema.json +26 -0
- package/plugins/extract-asyncapi/spec.go +151 -0
- package/plugins/extract-celery/README.md +169 -0
- package/plugins/extract-celery/extract.py +346 -0
- package/plugins/extract-celery/extract_test.py +192 -0
- package/plugins/extract-celery/main.py +66 -0
- package/plugins/extract-celery/options.py +31 -0
- package/plugins/extract-celery/options.schema.json +31 -0
- package/plugins/extract-celery/producers.py +156 -0
- package/plugins/extract-celery/routes_test.py +106 -0
- package/plugins/extract-commands/describe.go +19 -0
- package/plugins/extract-commands/describe_test.go +11 -0
- package/plugins/extract-commands/extract.go +79 -0
- package/plugins/extract-commands/extract_test.go +92 -0
- package/plugins/extract-commands/main.go +55 -0
- package/plugins/extract-commands/options.schema.json +20 -0
- package/plugins/extract-csr/avro.go +258 -0
- package/plugins/extract-csr/describe.go +19 -0
- package/plugins/extract-csr/describe_test.go +11 -0
- package/plugins/extract-csr/extract.go +338 -0
- package/plugins/extract-csr/extract_test.go +374 -0
- package/plugins/extract-csr/jsonschema.go +343 -0
- package/plugins/extract-csr/lock.go +25 -0
- package/plugins/extract-csr/main.go +84 -0
- package/plugins/extract-csr/options.schema.json +43 -0
- package/plugins/extract-csr/subject.go +102 -0
- package/plugins/extract-django/README.md +386 -0
- package/plugins/extract-django/apps.py +100 -0
- package/plugins/extract-django/auth.py +302 -0
- package/plugins/extract-django/auth_test.py +188 -0
- package/plugins/extract-django/clients.py +121 -0
- package/plugins/extract-django/contracts.py +458 -0
- package/plugins/extract-django/database.py +119 -0
- package/plugins/extract-django/domain.py +318 -0
- package/plugins/extract-django/events.py +86 -0
- package/plugins/extract-django/extract.py +601 -0
- package/plugins/extract-django/extract_test.py +397 -0
- package/plugins/extract-django/flows.py +1278 -0
- package/plugins/extract-django/flows_test.py +338 -0
- package/plugins/extract-django/ids.py +42 -0
- package/plugins/extract-django/ids_test.py +30 -0
- package/plugins/extract-django/lifecycle.py +222 -0
- package/plugins/extract-django/lifecycle_test.py +76 -0
- package/plugins/extract-django/main.py +66 -0
- package/plugins/extract-django/openapi.py +229 -0
- package/plugins/extract-django/openapi_test.py +60 -0
- package/plugins/extract-django/operations.py +96 -0
- package/plugins/extract-django/options.py +61 -0
- package/plugins/extract-django/options.schema.json +126 -0
- package/plugins/extract-django/routing.py +214 -0
- package/plugins/extract-django/routing_test.py +198 -0
- package/plugins/extract-django/serializers.py +558 -0
- package/plugins/extract-django/store.py +434 -0
- package/plugins/extract-django/transport.py +242 -0
- package/plugins/extract-flows/describe.go +19 -0
- package/plugins/extract-flows/describe_test.go +11 -0
- package/plugins/extract-flows/extract.go +91 -0
- package/plugins/extract-flows/main.go +45 -0
- package/plugins/extract-flows/options.schema.json +16 -0
- package/plugins/extract-flows/parse.go +593 -0
- package/plugins/extract-flows/parse_test.go +204 -0
- package/plugins/extract-glossary/describe.go +19 -0
- package/plugins/extract-glossary/describe_test.go +11 -0
- package/plugins/extract-glossary/extract.go +115 -0
- package/plugins/extract-glossary/extract_test.go +220 -0
- package/plugins/extract-glossary/main.go +59 -0
- package/plugins/extract-glossary/options.schema.json +21 -0
- package/plugins/extract-glossary/parse.go +214 -0
- package/plugins/extract-glossary/parse_test.go +203 -0
- package/plugins/extract-go/aggregate.go +214 -0
- package/plugins/extract-go/client.go +409 -0
- package/plugins/extract-go/client_test.go +266 -0
- package/plugins/extract-go/describe.go +19 -0
- package/plugins/extract-go/describe_test.go +11 -0
- package/plugins/extract-go/enum.go +195 -0
- package/plugins/extract-go/enum_test.go +82 -0
- package/plugins/extract-go/event.go +99 -0
- package/plugins/extract-go/extract.go +191 -0
- package/plugins/extract-go/extract_test.go +261 -0
- package/plugins/extract-go/flow.go +1441 -0
- package/plugins/extract-go/flow_test.go +609 -0
- package/plugins/extract-go/httpclient.go +174 -0
- package/plugins/extract-go/httpclient_test.go +296 -0
- package/plugins/extract-go/ids.go +92 -0
- package/plugins/extract-go/layout.go +225 -0
- package/plugins/extract-go/layout_test.go +125 -0
- package/plugins/extract-go/lifecycle.go +324 -0
- package/plugins/extract-go/lifecycle_test.go +108 -0
- package/plugins/extract-go/main.go +79 -0
- package/plugins/extract-go/operation.go +157 -0
- package/plugins/extract-go/options.schema.json +66 -0
- package/plugins/extract-go/source.go +316 -0
- package/plugins/extract-go/transport.go +321 -0
- package/plugins/extract-go/transport_test.go +145 -0
- package/plugins/extract-go/wiring.go +434 -0
- package/plugins/extract-go-nats/describe.go +19 -0
- package/plugins/extract-go-nats/describe_test.go +11 -0
- package/plugins/extract-go-nats/extract.go +177 -0
- package/plugins/extract-go-nats/extract_test.go +389 -0
- package/plugins/extract-go-nats/index.go +379 -0
- package/plugins/extract-go-nats/main.go +42 -0
- package/plugins/extract-go-nats/options.schema.json +21 -0
- package/plugins/extract-go-nats/resolve.go +161 -0
- package/plugins/extract-go-nats/sites.go +224 -0
- package/plugins/extract-graphql/describe.go +19 -0
- package/plugins/extract-graphql/describe_test.go +11 -0
- package/plugins/extract-graphql/extract.go +433 -0
- package/plugins/extract-graphql/extract_test.go +256 -0
- package/plugins/extract-graphql/ids.go +49 -0
- package/plugins/extract-graphql/lex.go +237 -0
- package/plugins/extract-graphql/main.go +51 -0
- package/plugins/extract-graphql/options.schema.json +31 -0
- package/plugins/extract-graphql/parse.go +621 -0
- package/plugins/extract-graphql/parse_test.go +122 -0
- package/plugins/extract-http-clients/describe.go +19 -0
- package/plugins/extract-http-clients/describe_test.go +11 -0
- package/plugins/extract-http-clients/extract.go +705 -0
- package/plugins/extract-http-clients/extract_test.go +1263 -0
- package/plugins/extract-http-clients/main.go +42 -0
- package/plugins/extract-http-clients/options.schema.json +33 -0
- package/plugins/extract-java/README.md +190 -0
- package/plugins/extract-java/build/org/portolan/extract/Catalog.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$2.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Call.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Client.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Route.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Domain$Aggregate.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Domain.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Events$Found.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Events.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Binding.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Draft.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Frame.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Options.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Ids.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Json$Reader.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Json.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle$Move.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi$Operation.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi$Spec.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations$UseCase.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto$Rpc.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto$Service.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source$Project.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source$Unit.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Transport$Endpoint.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Transport.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml$Line.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml$Reader.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml.class +0 -0
- package/plugins/extract-java/options.schema.json +70 -0
- package/plugins/extract-openapi/describe.go +19 -0
- package/plugins/extract-openapi/describe_test.go +11 -0
- package/plugins/extract-openapi/discover.go +243 -0
- package/plugins/extract-openapi/extract.go +526 -0
- package/plugins/extract-openapi/extract_test.go +545 -0
- package/plugins/extract-openapi/main.go +75 -0
- package/plugins/extract-openapi/options.schema.json +62 -0
- package/plugins/extract-openapi/spec.go +350 -0
- package/plugins/extract-project/README.md +22 -0
- package/plugins/extract-project/describe.go +19 -0
- package/plugins/extract-project/describe_test.go +11 -0
- package/plugins/extract-project/extract.go +221 -0
- package/plugins/extract-project/extract_test.go +109 -0
- package/plugins/extract-project/main.go +41 -0
- package/plugins/extract-project/options.schema.json +48 -0
- package/plugins/extract-proto/ast.go +125 -0
- package/plugins/extract-proto/consumes.go +77 -0
- package/plugins/extract-proto/describe.go +19 -0
- package/plugins/extract-proto/extract.go +293 -0
- package/plugins/extract-proto/extract_test.go +459 -0
- package/plugins/extract-proto/ids.go +89 -0
- package/plugins/extract-proto/ids_test.go +57 -0
- package/plugins/extract-proto/lex.go +285 -0
- package/plugins/extract-proto/main.go +100 -0
- package/plugins/extract-proto/module.go +120 -0
- package/plugins/extract-proto/options.schema.json +51 -0
- package/plugins/extract-proto/parse.go +720 -0
- package/plugins/extract-proto/parse_test.go +307 -0
- package/plugins/extract-proto/provides.go +236 -0
- package/plugins/extract-proto/resolve.go +222 -0
- package/plugins/extract-proto/resolve_test.go +119 -0
- package/plugins/extract-python-kafka/README.md +36 -0
- package/plugins/extract-python-kafka/extract.py +211 -0
- package/plugins/extract-python-kafka/extract_test.py +68 -0
- package/plugins/extract-python-kafka/main.py +59 -0
- package/plugins/extract-python-kafka/options.py +29 -0
- package/plugins/extract-python-kafka/options.schema.json +31 -0
- package/plugins/extract-redis/README.md +11 -0
- package/plugins/extract-redis/describe.go +19 -0
- package/plugins/extract-redis/describe_test.go +11 -0
- package/plugins/extract-redis/extract.go +183 -0
- package/plugins/extract-redis/extract_test.go +168 -0
- package/plugins/extract-redis/keyspaces.go +469 -0
- package/plugins/extract-redis/main.go +44 -0
- package/plugins/extract-redis/options.schema.json +30 -0
- package/plugins/extract-river/describe.go +19 -0
- package/plugins/extract-river/describe_test.go +11 -0
- package/plugins/extract-river/extract.go +507 -0
- package/plugins/extract-river/extract_test.go +132 -0
- package/plugins/extract-river/main.go +42 -0
- package/plugins/extract-river/options.schema.json +21 -0
- package/plugins/extract-rust/Cargo.lock +133 -0
- package/plugins/extract-rust/Cargo.toml +23 -0
- package/plugins/extract-rust/README.md +204 -0
- package/plugins/extract-rust/options.schema.json +65 -0
- package/plugins/extract-rust/rustfmt.toml +1 -0
- package/plugins/extract-sql/ddl.go +893 -0
- package/plugins/extract-sql/ddl_test.go +401 -0
- package/plugins/extract-sql/describe.go +19 -0
- package/plugins/extract-sql/describe_test.go +11 -0
- package/plugins/extract-sql/layout.go +221 -0
- package/plugins/extract-sql/layout_test.go +55 -0
- package/plugins/extract-sql/lineage.go +107 -0
- package/plugins/extract-sql/main.go +141 -0
- package/plugins/extract-sql/maps.go +564 -0
- package/plugins/extract-sql/maps_java.go +117 -0
- package/plugins/extract-sql/maps_rust.go +333 -0
- package/plugins/extract-sql/maps_rust_test.go +70 -0
- package/plugins/extract-sql/maps_test.go +204 -0
- package/plugins/extract-sql/maps_ts.go +398 -0
- package/plugins/extract-sql/maps_ts_test.go +136 -0
- package/plugins/extract-sql/options.schema.json +53 -0
- package/plugins/extract-sql/projection.go +70 -0
- package/plugins/extract-sql/projection_test.go +50 -0
- package/plugins/extract-sql/store.go +420 -0
- package/plugins/extract-sql/store_test.go +233 -0
- package/plugins/extract-sql/view.go +295 -0
- package/plugins/extract-ts/README.md +306 -0
- package/plugins/extract-ts/ast.test.ts +178 -0
- package/plugins/extract-ts/ast.ts +648 -0
- package/plugins/extract-ts/clients.ts +185 -0
- package/plugins/extract-ts/domain.ts +164 -0
- package/plugins/extract-ts/extract.test.ts +208 -0
- package/plugins/extract-ts/extract.ts +200 -0
- package/plugins/extract-ts/flows.ts +783 -0
- package/plugins/extract-ts/graphql.test.ts +73 -0
- package/plugins/extract-ts/graphql.ts +181 -0
- package/plugins/extract-ts/ids.test.ts +21 -0
- package/plugins/extract-ts/ids.ts +52 -0
- package/plugins/extract-ts/lifecycle.ts +138 -0
- package/plugins/extract-ts/main.ts +48 -0
- package/plugins/extract-ts/openapi.test.ts +64 -0
- package/plugins/extract-ts/openapi.ts +99 -0
- package/plugins/extract-ts/operations.ts +93 -0
- package/plugins/extract-ts/options.schema.json +96 -0
- package/plugins/extract-ts/source.test.ts +174 -0
- package/plugins/extract-ts/source.ts +465 -0
- package/plugins/extract-ts/transport.ts +147 -0
- package/plugins/extract-ts/tsconfig.json +13 -0
- package/plugins/extract-ts/wiring.ts +190 -0
- package/plugins/extract-watermill/describe.go +19 -0
- package/plugins/extract-watermill/describe_test.go +11 -0
- package/plugins/extract-watermill/extract.go +1228 -0
- package/plugins/extract-watermill/extract_test.go +234 -0
- package/plugins/extract-watermill/main.go +41 -0
- package/plugins/extract-watermill/options.schema.json +21 -0
- package/plugins/extract-wsdl/describe.go +19 -0
- package/plugins/extract-wsdl/describe_test.go +11 -0
- package/plugins/extract-wsdl/extract.go +148 -0
- package/plugins/extract-wsdl/extract_test.go +45 -0
- package/plugins/extract-wsdl/main.go +53 -0
- package/plugins/extract-wsdl/options.schema.json +59 -0
- package/plugins/fetch-bsr/auth.go +114 -0
- package/plugins/fetch-bsr/bsr.go +240 -0
- package/plugins/fetch-bsr/cache.go +66 -0
- package/plugins/fetch-bsr/describe.go +19 -0
- package/plugins/fetch-bsr/fetch.go +192 -0
- package/plugins/fetch-bsr/fetch_test.go +465 -0
- package/plugins/fetch-bsr/lock.go +71 -0
- package/plugins/fetch-bsr/main.go +117 -0
- package/plugins/fetch-bsr/options.schema.json +42 -0
- package/plugins/fetch-csr/auth.go +95 -0
- package/plugins/fetch-csr/cache.go +69 -0
- package/plugins/fetch-csr/describe.go +19 -0
- package/plugins/fetch-csr/describe_test.go +11 -0
- package/plugins/fetch-csr/fetch.go +254 -0
- package/plugins/fetch-csr/fetch_test.go +494 -0
- package/plugins/fetch-csr/lock.go +95 -0
- package/plugins/fetch-csr/main.go +122 -0
- package/plugins/fetch-csr/options.schema.json +42 -0
- package/plugins/fetch-csr/registry.go +209 -0
- package/plugins/fetch-git/cache.go +65 -0
- package/plugins/fetch-git/describe.go +19 -0
- package/plugins/fetch-git/describe_test.go +11 -0
- package/plugins/fetch-git/fetch.go +181 -0
- package/plugins/fetch-git/fetch_test.go +332 -0
- package/plugins/fetch-git/git.go +169 -0
- package/plugins/fetch-git/lock.go +72 -0
- package/plugins/fetch-git/main.go +127 -0
- package/plugins/fetch-git/offline.go +39 -0
- package/plugins/fetch-git/options.schema.json +42 -0
- package/plugins/fetch-git/pin.go +90 -0
- package/plugins/fetch-git/pin_test.go +74 -0
- package/plugins/gen-backstage/describe.go +17 -0
- package/plugins/gen-backstage/main.go +22 -0
- package/plugins/gen-backstage/options.schema.json +30 -0
- package/plugins/gen-backstage/plugin.go +473 -0
- package/plugins/gen-backstage/plugin_test.go +145 -0
- package/plugins/gen-backstage.wasm +0 -0
- package/plugins/gen-markdown/adr.go +162 -0
- package/plugins/gen-markdown/aggregate.go +362 -0
- package/plugins/gen-markdown/canonical.go +93 -0
- package/plugins/gen-markdown/context.go +90 -0
- package/plugins/gen-markdown/coverage_test.go +89 -0
- package/plugins/gen-markdown/describe.go +19 -0
- package/plugins/gen-markdown/describe_test.go +11 -0
- package/plugins/gen-markdown/external.go +71 -0
- package/plugins/gen-markdown/flow.go +277 -0
- package/plugins/gen-markdown/glossary.go +76 -0
- package/plugins/gen-markdown/glossary_test.go +148 -0
- package/plugins/gen-markdown/llms.go +302 -0
- package/plugins/gen-markdown/main.go +22 -0
- package/plugins/gen-markdown/markdown.go +254 -0
- package/plugins/gen-markdown/markdown_test.go +100 -0
- package/plugins/gen-markdown/module.go +107 -0
- package/plugins/gen-markdown/options.schema.json +16 -0
- package/plugins/gen-markdown/plugin.go +39 -0
- package/plugins/gen-markdown/quality_test.go +176 -0
- package/plugins/gen-markdown/redis_test.go +38 -0
- package/plugins/gen-markdown/render.go +344 -0
- package/plugins/gen-markdown/render_test.go +186 -0
- package/plugins/gen-markdown/service.go +456 -0
- package/plugins/gen-markdown/source.go +177 -0
- package/plugins/gen-markdown/store.go +201 -0
- package/plugins/gen-markdown.wasm +0 -0
- package/plugins/gen-mermaid/describe.go +17 -0
- package/plugins/gen-mermaid/main.go +22 -0
- package/plugins/gen-mermaid/options.schema.json +10 -0
- package/plugins/gen-mermaid/plugin.go +104 -0
- package/plugins/gen-mermaid/plugin_test.go +33 -0
- package/plugins/gen-mermaid.wasm +0 -0
- package/plugins/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -0
- package/plugins/pyplugin/catalog.py +209 -0
- package/plugins/pyplugin/celery_conf.py +436 -0
- package/plugins/pyplugin/celery_tasks.py +76 -0
- package/plugins/pyplugin/kafka.py +570 -0
- package/plugins/pyplugin/names.py +38 -0
- package/plugins/pyplugin/protocol.py +79 -0
- package/plugins/pyplugin/source.py +285 -0
- package/plugins/verify-codeowners/describe.go +19 -0
- package/plugins/verify-codeowners/describe_test.go +11 -0
- package/plugins/verify-codeowners/main.go +75 -0
- package/plugins/verify-codeowners/match.go +85 -0
- package/plugins/verify-codeowners/match_test.go +47 -0
- package/plugins/verify-codeowners/options.schema.json +15 -0
- package/plugins/verify-codeowners/owners.go +164 -0
- package/plugins/verify-codeowners/owners_test.go +225 -0
- package/plugins/verify-codeowners/parse.go +90 -0
- package/plugins/verify-codeowners/parse_test.go +62 -0
- package/plugins/verify-otel/describe.go +19 -0
- package/plugins/verify-otel/describe_test.go +11 -0
- package/plugins/verify-otel/main.go +53 -0
- package/plugins/verify-otel/match.go +336 -0
- package/plugins/verify-otel/options.schema.json +30 -0
- package/plugins/verify-otel/otlp.go +200 -0
- package/plugins/verify-otel/verify.go +734 -0
- package/plugins/verify-otel/verify_test.go +460 -0
- package/portolan.json +1073 -0
- package/public/favicon.svg +12 -0
- package/schema/portolan.schema.json +2558 -0
- package/scripts/README.md +53 -0
- package/scripts/build-report.mjs +60 -0
- package/scripts/build-report.test.mjs +44 -0
- package/scripts/builtin-plugins.mjs +49 -0
- package/scripts/catalog-sources.mjs +67 -0
- package/scripts/diff.mjs +315 -0
- package/scripts/diff.test.mjs +151 -0
- package/scripts/forge-comment.mjs +144 -0
- package/scripts/forge-comment.test.mjs +151 -0
- package/scripts/forge-release.mjs +138 -0
- package/scripts/forge-release.test.mjs +106 -0
- package/scripts/forge.mjs +61 -0
- package/scripts/gen-likec4.mjs +854 -0
- package/scripts/gen-likec4.test.mjs +258 -0
- package/scripts/gen.mjs +603 -0
- package/scripts/generated-docs.test.mjs +66 -0
- package/scripts/local-api.mjs +1192 -0
- package/scripts/local-api.test.mjs +387 -0
- package/scripts/manifest.mjs +214 -0
- package/scripts/manifest.test.mjs +148 -0
- package/scripts/output-path.mjs +79 -0
- package/scripts/output-path.test.mjs +45 -0
- package/scripts/package-smoke.mjs +37 -0
- package/scripts/plugin-host.mjs +419 -0
- package/scripts/plugin-host.test.mjs +109 -0
- package/scripts/plugin-wasm-worker.mjs +34 -0
- package/scripts/run-builtin.mjs +81 -0
- package/scripts/schema.mjs +357 -0
- package/scripts/site-docs.mjs +295 -0
- package/scripts/site-docs.test.mjs +181 -0
- package/scripts/vendor-lock.mjs +58 -0
- package/scripts/vendor-lock.test.mjs +69 -0
- package/src/README.md +38 -0
- package/src/app/App.tsx +448 -0
- package/src/app/BranchPicker.test.ts +46 -0
- package/src/app/BranchPicker.tsx +153 -0
- package/src/app/Breadcrumbs.test.ts +38 -0
- package/src/app/Breadcrumbs.tsx +223 -0
- package/src/app/BuildStamp.tsx +146 -0
- package/src/app/CommandPalette.tsx +292 -0
- package/src/app/HashScroll.tsx +45 -0
- package/src/app/Sidebar.tsx +1742 -0
- package/src/app/TopBar.tsx +393 -0
- package/src/app/density.tsx +64 -0
- package/src/app/forge-access.test.ts +27 -0
- package/src/app/forge-access.tsx +80 -0
- package/src/app/panels.tsx +141 -0
- package/src/app/pins.tsx +242 -0
- package/src/app/query-client.ts +24 -0
- package/src/app/responsive.ts +55 -0
- package/src/app/shortcuts.test.ts +39 -0
- package/src/app/shortcuts.tsx +282 -0
- package/src/app/theme.tsx +51 -0
- package/src/app/title.test.ts +14 -0
- package/src/app/title.ts +30 -0
- package/src/app/toast.tsx +59 -0
- package/src/app/tree-keys.test.ts +80 -0
- package/src/app/tree-keys.ts +87 -0
- package/src/app/ui-store.ts +52 -0
- package/src/catalog-error.test.ts +105 -0
- package/src/catalog-profile.test.ts +59 -0
- package/src/catalog-profile.ts +101 -0
- package/src/catalog-stores.test.ts +431 -0
- package/src/catalog.test.ts +1215 -0
- package/src/catalog.ts +3125 -0
- package/src/chat/Cards.tsx +408 -0
- package/src/chat/ChatPanel.tsx +79 -0
- package/src/chat/ChatSettings.tsx +219 -0
- package/src/chat/Composer.tsx +99 -0
- package/src/chat/Conversation.tsx +281 -0
- package/src/chat/Header.tsx +46 -0
- package/src/chat/Message.tsx +49 -0
- package/src/chat/Notice.tsx +195 -0
- package/src/chat/Starter.tsx +67 -0
- package/src/chat/Steps.tsx +45 -0
- package/src/chat/Waiting.tsx +87 -0
- package/src/chat/flags.test.ts +59 -0
- package/src/chat/flags.ts +155 -0
- package/src/chat/linkify.test.ts +30 -0
- package/src/chat/linkify.ts +45 -0
- package/src/chat/prefs.ts +56 -0
- package/src/chat/prompt.test.ts +43 -0
- package/src/chat/prompt.ts +157 -0
- package/src/chat/store.ts +35 -0
- package/src/chat/tools.ts +58 -0
- package/src/chat/transport.ts +70 -0
- package/src/components/AdrRow.tsx +29 -0
- package/src/components/AnchorLink.tsx +67 -0
- package/src/components/ApiReference.tsx +134 -0
- package/src/components/AsyncApiReference.tsx +120 -0
- package/src/components/CatalogStamp.tsx +155 -0
- package/src/components/ChannelRows.tsx +132 -0
- package/src/components/CommandRows.tsx +139 -0
- package/src/components/DiagramSkeleton.tsx +71 -0
- package/src/components/EditorLink.tsx +85 -0
- package/src/components/FieldTree.tsx +305 -0
- package/src/components/FlowTrigger.tsx +34 -0
- package/src/components/Ident.tsx +89 -0
- package/src/components/Integrations.tsx +243 -0
- package/src/components/LifecycleDiagram.tsx +110 -0
- package/src/components/MachineDocs.tsx +42 -0
- package/src/components/Markdown.tsx +173 -0
- package/src/components/MarkdownTable.tsx +206 -0
- package/src/components/Mermaid.tsx +211 -0
- package/src/components/MethodRows.tsx +309 -0
- package/src/components/Overlay.tsx +125 -0
- package/src/components/PageHeader.tsx +189 -0
- package/src/components/RowActions.tsx +97 -0
- package/src/components/SchemaDocument.tsx +89 -0
- package/src/components/Select.tsx +126 -0
- package/src/components/ShapeRows.tsx +118 -0
- package/src/components/SourceDoc.tsx +82 -0
- package/src/components/SourcePreview.tsx +441 -0
- package/src/components/TabRow.tsx +182 -0
- package/src/components/TechIcon.tsx +34 -0
- package/src/components/Toc.tsx +132 -0
- package/src/components/WhatLinksHere.tsx +273 -0
- package/src/components/WsdlReference.tsx +84 -0
- package/src/components/ddd-icons.tsx +107 -0
- package/src/components/kind.tsx +157 -0
- package/src/components/logo.tsx +61 -0
- package/src/components/primitives.tsx +264 -0
- package/src/data.test.ts +18 -0
- package/src/data.ts +202 -0
- package/src/enrich.test.ts +1150 -0
- package/src/enrich.ts +672 -0
- package/src/er/ErCanvas.tsx +533 -0
- package/src/er/RedisSchema.tsx +74 -0
- package/src/er/RelationCard.tsx +267 -0
- package/src/er/StoreHeader.tsx +155 -0
- package/src/er/TableNode.tsx +84 -0
- package/src/er/ViewNode.tsx +83 -0
- package/src/er/layout.test.ts +115 -0
- package/src/er/layout.ts +77 -0
- package/src/er/lineage.test.ts +140 -0
- package/src/er/lineage.ts +107 -0
- package/src/er/markers.tsx +79 -0
- package/src/er/spec.test.ts +344 -0
- package/src/er/spec.ts +426 -0
- package/src/flow/ChainList.tsx +162 -0
- package/src/flow/FlowTable.tsx +123 -0
- package/src/flow/FlowToolbar.tsx +372 -0
- package/src/flow/StepDetail.tsx +380 -0
- package/src/flow/StepRail.tsx +428 -0
- package/src/flow/answers.test.ts +50 -0
- package/src/flow/answers.ts +58 -0
- package/src/flow/chain.test.ts +383 -0
- package/src/flow/chain.ts +353 -0
- package/src/flow/chapters.test.ts +185 -0
- package/src/flow/chapters.ts +225 -0
- package/src/flow/continues.test.ts +102 -0
- package/src/flow/continues.ts +65 -0
- package/src/flow/cross-context.test.ts +97 -0
- package/src/flow/cross-context.ts +35 -0
- package/src/flow/mermaid.test.ts +22 -0
- package/src/flow/mermaid.ts +141 -0
- package/src/flow/outline.test.ts +212 -0
- package/src/flow/outline.ts +170 -0
- package/src/flow/paths.test.ts +129 -0
- package/src/flow/paths.ts +147 -0
- package/src/flow/prefs.test.ts +80 -0
- package/src/flow/prefs.ts +104 -0
- package/src/graph/DependencyGraph.tsx +283 -0
- package/src/graph/DependencyNodes.tsx +162 -0
- package/src/graph/EdgeLabel.tsx +61 -0
- package/src/graph/FocusedEventGraph.tsx +211 -0
- package/src/graph/GraphToolbar.tsx +187 -0
- package/src/graph/RoutedEdge.tsx +80 -0
- package/src/graph/dependency-layout.test.ts +360 -0
- package/src/graph/dependency-layout.ts +444 -0
- package/src/graph/elk.test.ts +70 -0
- package/src/graph/elk.ts +287 -0
- package/src/graph/nodes.tsx +97 -0
- package/src/graph/theme.ts +81 -0
- package/src/graph/useElkFlow.ts +125 -0
- package/src/index.css +1547 -0
- package/src/language/InLanguage.tsx +40 -0
- package/src/language/TermCard.tsx +132 -0
- package/src/language/cards.test.ts +114 -0
- package/src/language/cards.ts +111 -0
- package/src/lib/admonition.test.ts +49 -0
- package/src/lib/admonition.ts +48 -0
- package/src/lib/adr.test.ts +131 -0
- package/src/lib/adr.ts +68 -0
- package/src/lib/api.test.ts +122 -0
- package/src/lib/api.ts +135 -0
- package/src/lib/backlinks.test.ts +330 -0
- package/src/lib/backlinks.ts +567 -0
- package/src/lib/branch-compare.ts +16 -0
- package/src/lib/build-info.test.ts +76 -0
- package/src/lib/build-info.ts +80 -0
- package/src/lib/catalog-diff.test.ts +275 -0
- package/src/lib/catalog-diff.ts +655 -0
- package/src/lib/clipboard.ts +43 -0
- package/src/lib/comparison-memory.test.ts +40 -0
- package/src/lib/comparison-memory.ts +39 -0
- package/src/lib/context-color.ts +27 -0
- package/src/lib/context-map.test.ts +148 -0
- package/src/lib/context-map.ts +399 -0
- package/src/lib/data-model.test.ts +245 -0
- package/src/lib/data-model.ts +399 -0
- package/src/lib/data-problems.test.ts +178 -0
- package/src/lib/data-problems.ts +314 -0
- package/src/lib/derive.test.ts +251 -0
- package/src/lib/derive.ts +515 -0
- package/src/lib/editor-link.test.ts +125 -0
- package/src/lib/editor-link.ts +96 -0
- package/src/lib/editor-prefs.ts +27 -0
- package/src/lib/event-graph.test.ts +237 -0
- package/src/lib/event-graph.ts +327 -0
- package/src/lib/export-canvas.ts +37 -0
- package/src/lib/export-file.test.ts +30 -0
- package/src/lib/export-file.ts +20 -0
- package/src/lib/flow-tree.test.ts +177 -0
- package/src/lib/flow-tree.ts +160 -0
- package/src/lib/forge-refs.test.ts +50 -0
- package/src/lib/forge-refs.ts +60 -0
- package/src/lib/format.test.ts +35 -0
- package/src/lib/format.ts +64 -0
- package/src/lib/github-catalog.test.ts +301 -0
- package/src/lib/github-catalog.ts +610 -0
- package/src/lib/integrations.test.ts +119 -0
- package/src/lib/integrations.ts +133 -0
- package/src/lib/kinds.test.ts +91 -0
- package/src/lib/kinds.ts +204 -0
- package/src/lib/lifecycle.test.ts +93 -0
- package/src/lib/lifecycle.ts +165 -0
- package/src/lib/local-api.ts +194 -0
- package/src/lib/motion-features.ts +11 -0
- package/src/lib/motion.test.ts +120 -0
- package/src/lib/motion.tsx +269 -0
- package/src/lib/palette-recent.test.ts +80 -0
- package/src/lib/palette-recent.ts +90 -0
- package/src/lib/palette.test.ts +365 -0
- package/src/lib/palette.ts +593 -0
- package/src/lib/pins.test.ts +139 -0
- package/src/lib/pins.ts +130 -0
- package/src/lib/problems.test.ts +112 -0
- package/src/lib/project-preview.ts +5 -0
- package/src/lib/proto-problems.test.ts +121 -0
- package/src/lib/proto-problems.ts +65 -0
- package/src/lib/queries.test.ts +113 -0
- package/src/lib/queries.ts +131 -0
- package/src/lib/registry.test.ts +219 -0
- package/src/lib/registry.ts +208 -0
- package/src/lib/scenarios.ts +547 -0
- package/src/lib/setup-info.test.ts +199 -0
- package/src/lib/setup-info.ts +415 -0
- package/src/lib/shape.test.ts +289 -0
- package/src/lib/shape.ts +454 -0
- package/src/lib/sidebar-prefs.test.ts +32 -0
- package/src/lib/sidebar-prefs.ts +75 -0
- package/src/lib/source-code.test.ts +159 -0
- package/src/lib/source-code.ts +286 -0
- package/src/lib/source-doc.test.ts +131 -0
- package/src/lib/source-doc.ts +103 -0
- package/src/lib/source-highlight.test.ts +29 -0
- package/src/lib/source-highlight.ts +24 -0
- package/src/lib/source-link.test.ts +153 -0
- package/src/lib/source-link.ts +201 -0
- package/src/lib/spec-files.test.ts +24 -0
- package/src/lib/spec-files.ts +23 -0
- package/src/lib/table-directive.test.ts +139 -0
- package/src/lib/table-directive.ts +106 -0
- package/src/lib/tech.test.ts +120 -0
- package/src/lib/tech.ts +421 -0
- package/src/lib/terms.test.ts +67 -0
- package/src/lib/terms.ts +179 -0
- package/src/lib/wire-problems.test.ts +263 -0
- package/src/lib/wire-problems.ts +281 -0
- package/src/likec4/C4View.tsx +55 -0
- package/src/likec4/CanvasBridge.tsx +187 -0
- package/src/likec4/FlowView.tsx +235 -0
- package/src/likec4/InteractiveView.tsx +144 -0
- package/src/likec4/canvas-viewport.test.ts +149 -0
- package/src/likec4/canvas-viewport.ts +135 -0
- package/src/likec4/flow-edges.test.ts +51 -0
- package/src/likec4/flow-edges.ts +89 -0
- package/src/likec4/frame-theme.test.ts +76 -0
- package/src/likec4/frame-theme.ts +101 -0
- package/src/likec4/highlight-css.test.ts +49 -0
- package/src/likec4/highlight-css.ts +52 -0
- package/src/likec4/ids.test.ts +151 -0
- package/src/likec4/ids.ts +115 -0
- package/src/likec4/levels.tsx +70 -0
- package/src/likec4/mapping.test.ts +66 -0
- package/src/likec4/mapping.ts +54 -0
- package/src/likec4/reserved.json +141 -0
- package/src/likec4/view-index.ts +78 -0
- package/src/main.tsx +13 -0
- package/src/map/ContextMapGraph.tsx +274 -0
- package/src/map/ContextNode.tsx +70 -0
- package/src/map/FloatingEdge.tsx +86 -0
- package/src/merge.test.ts +1038 -0
- package/src/merge.ts +1004 -0
- package/src/pages/AdrDetail.tsx +242 -0
- package/src/pages/AdrIndex.tsx +114 -0
- package/src/pages/AggregatePage.tsx +727 -0
- package/src/pages/BlockPage.tsx +478 -0
- package/src/pages/CatalogFailure.tsx +111 -0
- package/src/pages/Changes.tsx +313 -0
- package/src/pages/ContextMap.tsx +534 -0
- package/src/pages/ContextPage.tsx +346 -0
- package/src/pages/EnumPage.tsx +282 -0
- package/src/pages/EventPage.tsx +686 -0
- package/src/pages/ExternalPage.tsx +204 -0
- package/src/pages/FlowDetail.tsx +663 -0
- package/src/pages/FlowIndex.tsx +303 -0
- package/src/pages/GraphPage.tsx +217 -0
- package/src/pages/Language.tsx +257 -0
- package/src/pages/ModulePage.tsx +366 -0
- package/src/pages/NotFound.tsx +54 -0
- package/src/pages/Overview.tsx +344 -0
- package/src/pages/Problems.tsx +360 -0
- package/src/pages/RegistryIndex.tsx +118 -0
- package/src/pages/ServicePage.tsx +746 -0
- package/src/pages/Settings.tsx +975 -0
- package/src/pages/StorePage.tsx +114 -0
- package/src/peek/PeekCard.test.ts +35 -0
- package/src/peek/PeekCard.tsx +118 -0
- package/src/peek/PeekLayer.tsx +147 -0
- package/src/peek/model.test.ts +156 -0
- package/src/peek/model.ts +253 -0
- package/src/routes.test.ts +222 -0
- package/src/routes.ts +449 -0
- package/src/selection/DetailPanel.tsx +1407 -0
- package/src/selection/hash.test.ts +73 -0
- package/src/selection/hash.ts +58 -0
- package/src/selection/model.test.ts +199 -0
- package/src/selection/model.ts +473 -0
- package/src/selection/pages.test.ts +115 -0
- package/src/selection/pages.ts +268 -0
- package/src/selection/store.test.ts +62 -0
- package/src/selection/store.ts +46 -0
- package/src/selection/sync.tsx +99 -0
- package/src/table/ColumnsMenu.tsx +72 -0
- package/src/table/DataTable.tsx +780 -0
- package/src/table/Facets.tsx +54 -0
- package/src/table/GroupMenu.tsx +69 -0
- package/src/table/Toolbar.tsx +190 -0
- package/src/table/cells.tsx +117 -0
- package/src/table/compare.test.ts +117 -0
- package/src/table/compare.ts +196 -0
- package/src/table/export.test.ts +88 -0
- package/src/table/export.ts +66 -0
- package/src/table/facet-groups.test.ts +74 -0
- package/src/table/facet-groups.ts +56 -0
- package/src/table/grouping.test.ts +84 -0
- package/src/table/grouping.ts +82 -0
- package/src/table/infer.test.ts +57 -0
- package/src/table/infer.ts +50 -0
- package/src/table/persist.test.ts +76 -0
- package/src/table/persist.ts +112 -0
- package/src/table/sort-url.test.ts +75 -0
- package/src/table/sort-url.ts +52 -0
- package/src/table/types.ts +80 -0
- package/src/table/useTableState.ts +220 -0
- package/src/test-catalog.ts +7 -0
- package/src/testing/estate/data__catalog.json +2847 -0
- package/src/testing/estate/data__flows.json +1724 -0
- package/src/testing/estate/examples__auth__portolan__api.json +212 -0
- package/src/testing/estate/examples__auth__portolan__domain.json +1204 -0
- package/src/testing/estate/examples__auth__portolan__glossary.json +226 -0
- package/src/testing/estate/examples__auth__portolan__observed.json +757 -0
- package/src/testing/estate/examples__auth__portolan__stores.json +195 -0
- package/src/testing/estate/examples__shop__cart__portolan__api.json +234 -0
- package/src/testing/estate/examples__shop__cart__portolan__commands.json +86 -0
- package/src/testing/estate/examples__shop__cart__portolan__domain.json +966 -0
- package/src/testing/estate/examples__shop__cart__portolan__observed.json +498 -0
- package/src/testing/estate/examples__shop__cart__portolan__stores.json +208 -0
- package/src/testing/estate/examples__shop__oms__portolan__glossary.json +98 -0
- package/src/testing/estate/examples__shop__oms__portolan__proto.json +186 -0
- package/src/testing/estate.ts +49 -0
- package/src/trail/Trail.tsx +81 -0
- package/src/trail/model.test.ts +157 -0
- package/src/trail/model.ts +189 -0
- package/src/trail/record.tsx +32 -0
- package/src/trail/store.test.ts +66 -0
- package/src/trail/store.ts +102 -0
- package/tsconfig.json +35 -0
- package/vite.config.ts +171 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Who may call an endpoint, read off DRF's permission and authentication
|
|
2
|
+
classes and written into the inferred OpenAPI document as `security`.
|
|
3
|
+
|
|
4
|
+
DRF decides both per view, and the reader follows the same order the
|
|
5
|
+
framework does: the handler's own `@action(permission_classes=...)` or the
|
|
6
|
+
function's `@permission_classes(...)`, then a `get_permissions()` that a
|
|
7
|
+
`self.action` branch can decide, then the class attribute - its own or one a
|
|
8
|
+
local base class declares - and past all of those `REST_FRAMEWORK` in the
|
|
9
|
+
settings module, and past that DRF's own defaults, which are session or
|
|
10
|
+
basic authentication and `AllowAny`. `authentication_classes` is read the
|
|
11
|
+
same way.
|
|
12
|
+
|
|
13
|
+
What is written is OpenAPI's, not DRF's. An authentication class becomes a
|
|
14
|
+
security scheme - `SessionAuthentication` a cookie, `TokenAuthentication` an
|
|
15
|
+
`Authorization` header, `JWTAuthentication` a bearer token - and the
|
|
16
|
+
permission classes decide whether an operation requires one of them, accepts
|
|
17
|
+
one optionally, or takes nobody's name. A permission this reader does not
|
|
18
|
+
know - a project's own `IsOwner` - is not guessed at: the operation gets no
|
|
19
|
+
`security` of its own, its classes are still written under
|
|
20
|
+
`x-portolan-permissions`, and the diagnostic says which class it was.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import ast
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
28
|
+
|
|
29
|
+
from celery_conf import follow, settings_module_name
|
|
30
|
+
from serializers import resolve_symbol, selected_return
|
|
31
|
+
from source import Module, Project, assigned, const_str, dotted, keyword
|
|
32
|
+
|
|
33
|
+
# DRF's authentication classes and the common third-party ones, to the
|
|
34
|
+
# security scheme OpenAPI writes them as. The id is what the operations refer
|
|
35
|
+
# to, and it is the one drf-spectacular gives the same class, so a document a
|
|
36
|
+
# project generates at runtime and this one agree on the name.
|
|
37
|
+
AUTHENTICATION: Dict[str, Tuple[str, Dict[str, Any]]] = {
|
|
38
|
+
"SessionAuthentication": ("cookieAuth", {"type": "apiKey", "in": "cookie", "name": "sessionid"}),
|
|
39
|
+
"BasicAuthentication": ("basicAuth", {"type": "http", "scheme": "basic"}),
|
|
40
|
+
"TokenAuthentication": (
|
|
41
|
+
"tokenAuth",
|
|
42
|
+
{"type": "apiKey", "in": "header", "name": "Authorization", "description": "Token-based authentication with required prefix \"Token\"."},
|
|
43
|
+
),
|
|
44
|
+
"JWTAuthentication": ("jwtAuth", {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}),
|
|
45
|
+
"JWTStatelessUserAuthentication": ("jwtAuth", {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}),
|
|
46
|
+
"JSONWebTokenAuthentication": ("jwtAuth", {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}),
|
|
47
|
+
"OAuth2Authentication": (
|
|
48
|
+
"oauth2",
|
|
49
|
+
{"type": "oauth2", "flows": {}, "description": "OAuth 2.0 bearer token; the flows are configured at runtime and are not recoverable from source."},
|
|
50
|
+
),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# DRF's own permissions, by what they say about who may call.
|
|
54
|
+
PUBLIC = {"AllowAny"}
|
|
55
|
+
REQUIRED = {"IsAuthenticated", "IsAdminUser", "DjangoModelPermissions", "DjangoObjectPermissions"}
|
|
56
|
+
READ_OPTIONAL = {"IsAuthenticatedOrReadOnly", "DjangoModelPermissionsOrAnonReadOnly"}
|
|
57
|
+
SAFE_VERBS = {"GET", "HEAD", "OPTIONS"}
|
|
58
|
+
|
|
59
|
+
# What DRF applies when the settings say nothing.
|
|
60
|
+
DEFAULT_AUTHENTICATION = ["SessionAuthentication", "BasicAuthentication"]
|
|
61
|
+
DEFAULT_PERMISSION = ["AllowAny"]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class Security:
|
|
66
|
+
"""What one operation says about its caller."""
|
|
67
|
+
|
|
68
|
+
# The OpenAPI `security` requirement, or None when it could not be decided.
|
|
69
|
+
requirement: Optional[List[Dict[str, List[str]]]]
|
|
70
|
+
# The permission classes as the code names them, for `x-portolan-permissions`.
|
|
71
|
+
permissions: List[str] = field(default_factory=list)
|
|
72
|
+
# The schemes the requirement refers to, for `components.securitySchemes`.
|
|
73
|
+
schemes: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Registry:
|
|
77
|
+
def __init__(self, project: Project, settings: str, b) -> None:
|
|
78
|
+
self.project = project
|
|
79
|
+
self.b = b
|
|
80
|
+
self.default_authentication, self.default_permission = read_defaults(project, settings)
|
|
81
|
+
self.schemes: Dict[str, Dict[str, Any]] = {}
|
|
82
|
+
self._warned: set = set()
|
|
83
|
+
|
|
84
|
+
# --- what the document says at the top ---------------------------------
|
|
85
|
+
|
|
86
|
+
def default_security(self) -> Optional[List[Dict[str, List[str]]]]:
|
|
87
|
+
"""The document-level requirement: what an operation the settings
|
|
88
|
+
alone decide would carry, for a POST."""
|
|
89
|
+
decided = self.decide(self.default_authentication, self.default_permission, "POST", "")
|
|
90
|
+
return decided.requirement
|
|
91
|
+
|
|
92
|
+
# --- what one operation says -------------------------------------------
|
|
93
|
+
|
|
94
|
+
def security_for(self, endpoint) -> Security:
|
|
95
|
+
declared = declared_security(endpoint)
|
|
96
|
+
if declared is not None:
|
|
97
|
+
return self.schemes_of(declared)
|
|
98
|
+
authentication = self.classes_for(endpoint, "authentication_classes", "get_authenticators")
|
|
99
|
+
if authentication is None:
|
|
100
|
+
authentication = list(self.default_authentication)
|
|
101
|
+
permission = self.classes_for(endpoint, "permission_classes", "get_permissions")
|
|
102
|
+
if permission is None:
|
|
103
|
+
permission = list(self.default_permission)
|
|
104
|
+
if UNRESOLVED in authentication or UNRESOLVED in permission:
|
|
105
|
+
names = [name for name in permission if name is not UNRESOLVED]
|
|
106
|
+
self.warn(endpoint.route_source or endpoint.module.rel, "%s builds its permission or authentication classes in a way this reader cannot follow; the operation carries no security of its own" % endpoint.id)
|
|
107
|
+
return Security(None, names)
|
|
108
|
+
return self.decide(authentication, permission, endpoint.verb.upper(), endpoint.route_source or endpoint.module.rel)
|
|
109
|
+
|
|
110
|
+
def decide(self, authentication: List[str], permission: List[str], verb: str, where: str) -> Security:
|
|
111
|
+
schemes: Dict[str, Dict[str, Any]] = {}
|
|
112
|
+
for name in authentication:
|
|
113
|
+
scheme_id, scheme = self.scheme_of(name, where)
|
|
114
|
+
schemes[scheme_id] = scheme
|
|
115
|
+
self.schemes.update(schemes)
|
|
116
|
+
unknown = [name for name in permission if name not in PUBLIC and name not in REQUIRED and name not in READ_OPTIONAL]
|
|
117
|
+
if unknown:
|
|
118
|
+
for name in unknown:
|
|
119
|
+
self.warn(where, "%s is not a DRF permission this reader knows, so whether the endpoint needs a caller is not said; the class is kept under x-portolan-permissions" % name)
|
|
120
|
+
return Security(None, list(permission), schemes)
|
|
121
|
+
# DRF's permission list is a conjunction, and its authentication list
|
|
122
|
+
# is tried in order until one answers, so the requirement is one
|
|
123
|
+
# alternative per scheme - and an empty one when nobody is needed.
|
|
124
|
+
required = any(name in REQUIRED for name in permission) or (verb not in SAFE_VERBS and any(name in READ_OPTIONAL for name in permission))
|
|
125
|
+
requirement = [{scheme_id: []} for scheme_id in schemes]
|
|
126
|
+
if required and not requirement:
|
|
127
|
+
self.warn(where, "the endpoint requires an authenticated caller but declares no authentication class: nobody can call it")
|
|
128
|
+
if not required and requirement:
|
|
129
|
+
requirement.append({})
|
|
130
|
+
return Security(requirement, list(permission), schemes)
|
|
131
|
+
|
|
132
|
+
def schemes_of(self, declared: List[Dict[str, List[str]]]) -> Security:
|
|
133
|
+
"""A requirement the schema decorator spelled out, in OpenAPI's own
|
|
134
|
+
words; the schemes it names are the document's to declare."""
|
|
135
|
+
known = {scheme_id: scheme for _, (scheme_id, scheme) in AUTHENTICATION.items()}
|
|
136
|
+
schemes = {}
|
|
137
|
+
for alternative in declared:
|
|
138
|
+
for scheme_id in alternative:
|
|
139
|
+
if scheme_id in known:
|
|
140
|
+
schemes[scheme_id] = known[scheme_id]
|
|
141
|
+
self.schemes.update(schemes)
|
|
142
|
+
return Security(declared, [], schemes)
|
|
143
|
+
|
|
144
|
+
def scheme_of(self, name: str, where: str) -> Tuple[str, Dict[str, Any]]:
|
|
145
|
+
if name in AUTHENTICATION:
|
|
146
|
+
scheme_id, scheme = AUTHENTICATION[name]
|
|
147
|
+
return scheme_id, dict(scheme)
|
|
148
|
+
self.warn(where, "%s is not a DRF authentication class this reader knows; it is written as a security scheme of its own, and how the credential travels is not said" % name)
|
|
149
|
+
scheme_id = name[0].lower() + name[1:] if name else "customAuth"
|
|
150
|
+
return scheme_id, {
|
|
151
|
+
"type": "http",
|
|
152
|
+
"scheme": "unknown",
|
|
153
|
+
"description": "%s: a custom authentication class. How the credential travels is not recoverable from source." % name,
|
|
154
|
+
"x-portolan-inferred": True,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# --- where the classes come from ---------------------------------------
|
|
158
|
+
|
|
159
|
+
def classes_for(self, endpoint, attribute: str, selector: str) -> Optional[List[Any]]:
|
|
160
|
+
"""The classes in force for this endpoint, in DRF's order of
|
|
161
|
+
precedence, or None when nothing but the settings says."""
|
|
162
|
+
node = endpoint.node
|
|
163
|
+
module = endpoint.module
|
|
164
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
165
|
+
for decorator in node.decorator_list:
|
|
166
|
+
if not isinstance(decorator, ast.Call):
|
|
167
|
+
continue
|
|
168
|
+
last = dotted(decorator.func).split(".")[-1]
|
|
169
|
+
if last == attribute and decorator.args:
|
|
170
|
+
return class_names(decorator.args[0], module)
|
|
171
|
+
if last == "action" and keyword(decorator, attribute) is not None:
|
|
172
|
+
return class_names(keyword(decorator, attribute), module)
|
|
173
|
+
view = view_class(endpoint)
|
|
174
|
+
if view is None:
|
|
175
|
+
return None
|
|
176
|
+
chosen = next((m for m in view.body if isinstance(m, (ast.FunctionDef, ast.AsyncFunctionDef)) and m.name == selector), None)
|
|
177
|
+
if chosen is not None:
|
|
178
|
+
selected = selected_return(chosen.body, endpoint.action)
|
|
179
|
+
if selected is None:
|
|
180
|
+
return [UNRESOLVED]
|
|
181
|
+
return class_names(selected, module)
|
|
182
|
+
found = self.class_attribute(module, view, attribute, 0)
|
|
183
|
+
if found is None:
|
|
184
|
+
return None
|
|
185
|
+
value, owner = found
|
|
186
|
+
return class_names(value, owner)
|
|
187
|
+
|
|
188
|
+
def class_attribute(self, module: Module, node: ast.ClassDef, attribute: str, depth: int) -> Optional[Tuple[ast.AST, Module]]:
|
|
189
|
+
"""The attribute as this class or a local base of it assigns it."""
|
|
190
|
+
for name, value, _ in assigned(node):
|
|
191
|
+
if name == attribute:
|
|
192
|
+
return value, module
|
|
193
|
+
if depth > 5:
|
|
194
|
+
return None
|
|
195
|
+
for base in node.bases:
|
|
196
|
+
name = dotted(base)
|
|
197
|
+
if not name:
|
|
198
|
+
continue
|
|
199
|
+
target_module, target_name = resolve_symbol(self.project, module, name)
|
|
200
|
+
target = self.project.module(target_module) if target_module else None
|
|
201
|
+
if target is None:
|
|
202
|
+
continue
|
|
203
|
+
base_node = next((c for c in target.classes() if c.name == target_name.split(".")[-1]), None)
|
|
204
|
+
if base_node is None:
|
|
205
|
+
continue
|
|
206
|
+
found = self.class_attribute(target, base_node, attribute, depth + 1)
|
|
207
|
+
if found is not None:
|
|
208
|
+
return found
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
def warn(self, where: str, message: str) -> None:
|
|
212
|
+
if (where, message) in self._warned:
|
|
213
|
+
return
|
|
214
|
+
self._warned.add((where, message))
|
|
215
|
+
self.b.warn(where, message)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# A class expression syntax does not name, kept apart from every real name.
|
|
219
|
+
UNRESOLVED = object()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def view_class(endpoint) -> Optional[ast.ClassDef]:
|
|
223
|
+
if not endpoint.view:
|
|
224
|
+
return None
|
|
225
|
+
name = endpoint.view.split(".", 1)[0]
|
|
226
|
+
return next((node for node in endpoint.module.classes() if node.name == name), None)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def class_names(value: Optional[ast.AST], module: Module) -> List[Any]:
|
|
230
|
+
"""`[IsAuthenticated, perms.IsOwner()]` as the class names it lists; an
|
|
231
|
+
element that is not a name - `IsAuthenticated | ReadOnly` - is UNRESOLVED."""
|
|
232
|
+
value = follow(value, module)
|
|
233
|
+
if not isinstance(value, (ast.List, ast.Tuple)):
|
|
234
|
+
return [UNRESOLVED]
|
|
235
|
+
out: List[Any] = []
|
|
236
|
+
for element in value.elts:
|
|
237
|
+
target = element.func if isinstance(element, ast.Call) else element
|
|
238
|
+
name = dotted(target).split(".")[-1]
|
|
239
|
+
out.append(name if name else UNRESOLVED)
|
|
240
|
+
return out
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def declared_security(endpoint) -> Optional[List[Dict[str, List[str]]]]:
|
|
244
|
+
"""`extend_schema(auth=[...])` or `swagger_auto_schema(security=[...])`
|
|
245
|
+
on the handler: the requirement as the author wrote it, when it is
|
|
246
|
+
written as literals."""
|
|
247
|
+
node = endpoint.node
|
|
248
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
249
|
+
return None
|
|
250
|
+
for decorator in node.decorator_list:
|
|
251
|
+
if not isinstance(decorator, ast.Call):
|
|
252
|
+
continue
|
|
253
|
+
last = dotted(decorator.func).split(".")[-1]
|
|
254
|
+
value = keyword(decorator, "auth") if last == "extend_schema" else keyword(decorator, "security") if last == "swagger_auto_schema" else None
|
|
255
|
+
if value is None:
|
|
256
|
+
continue
|
|
257
|
+
if not isinstance(value, (ast.List, ast.Tuple)):
|
|
258
|
+
return None
|
|
259
|
+
out: List[Dict[str, List[str]]] = []
|
|
260
|
+
for element in value.elts:
|
|
261
|
+
if isinstance(element, ast.Dict):
|
|
262
|
+
alternative: Dict[str, List[str]] = {}
|
|
263
|
+
for key, scopes in zip(element.keys, element.values):
|
|
264
|
+
scheme_id = const_str(key)
|
|
265
|
+
if not scheme_id or not isinstance(scopes, (ast.List, ast.Tuple)):
|
|
266
|
+
return None
|
|
267
|
+
alternative[scheme_id] = [const_str(scope) for scope in scopes.elts]
|
|
268
|
+
out.append(alternative)
|
|
269
|
+
elif const_str(element):
|
|
270
|
+
out.append({const_str(element): []})
|
|
271
|
+
else:
|
|
272
|
+
return None
|
|
273
|
+
return out
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def read_defaults(project: Project, settings: str) -> Tuple[List[str], List[str]]:
|
|
278
|
+
"""`REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"]` and
|
|
279
|
+
`["DEFAULT_PERMISSION_CLASSES"]` off the settings module, as class names;
|
|
280
|
+
DRF's own defaults where the settings say nothing."""
|
|
281
|
+
authentication = list(DEFAULT_AUTHENTICATION)
|
|
282
|
+
permission = list(DEFAULT_PERMISSION)
|
|
283
|
+
module = project.module(settings or settings_module_name(project))
|
|
284
|
+
if module is None:
|
|
285
|
+
return authentication, permission
|
|
286
|
+
config: Optional[ast.AST] = None
|
|
287
|
+
for name, value, _ in assigned(module.tree):
|
|
288
|
+
if name == "REST_FRAMEWORK":
|
|
289
|
+
config = follow(value, module)
|
|
290
|
+
if not isinstance(config, ast.Dict):
|
|
291
|
+
return authentication, permission
|
|
292
|
+
for key, value in zip(config.keys, config.values):
|
|
293
|
+
setting = const_str(key) if key is not None else ""
|
|
294
|
+
value = follow(value, module)
|
|
295
|
+
if setting not in ("DEFAULT_AUTHENTICATION_CLASSES", "DEFAULT_PERMISSION_CLASSES") or not isinstance(value, (ast.List, ast.Tuple)):
|
|
296
|
+
continue
|
|
297
|
+
names = [const_str(element).split(".")[-1] for element in value.elts if const_str(element)]
|
|
298
|
+
if setting == "DEFAULT_AUTHENTICATION_CLASSES":
|
|
299
|
+
authentication = names
|
|
300
|
+
else:
|
|
301
|
+
permission = names
|
|
302
|
+
return authentication, permission
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import tempfile
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
8
|
+
sys.path.insert(1, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "pyplugin"))
|
|
9
|
+
|
|
10
|
+
import apps # noqa: E402
|
|
11
|
+
import auth # noqa: E402
|
|
12
|
+
import routing # noqa: E402
|
|
13
|
+
import serializers # noqa: E402
|
|
14
|
+
import transport # noqa: E402
|
|
15
|
+
from extract import openapi_document, routed_applications # noqa: E402
|
|
16
|
+
from protocol import Builder # noqa: E402
|
|
17
|
+
from source import Project # noqa: E402
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WhoMayCall(unittest.TestCase):
|
|
21
|
+
"""Each way DRF says who may call, once, and what OpenAPI it becomes."""
|
|
22
|
+
|
|
23
|
+
def setUp(self):
|
|
24
|
+
self.holder = tempfile.TemporaryDirectory(prefix="portolan-django-auth-")
|
|
25
|
+
root = self.holder.name
|
|
26
|
+
files = {
|
|
27
|
+
"manage.py": 'os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")\n',
|
|
28
|
+
"config/settings.py": '''
|
|
29
|
+
ROOT_URLCONF = "config.urls"
|
|
30
|
+
REST_FRAMEWORK = {
|
|
31
|
+
"DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.TokenAuthentication",),
|
|
32
|
+
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.AllowAny"],
|
|
33
|
+
}
|
|
34
|
+
''',
|
|
35
|
+
"config/urls.py": '''
|
|
36
|
+
from django.urls import include, path
|
|
37
|
+
urlpatterns = [path("", include("api.urls"))]
|
|
38
|
+
''',
|
|
39
|
+
"api/base.py": '''
|
|
40
|
+
from rest_framework.permissions import IsAuthenticated
|
|
41
|
+
from rest_framework.views import APIView
|
|
42
|
+
class Protected(APIView):
|
|
43
|
+
permission_classes = [IsAuthenticated]
|
|
44
|
+
''',
|
|
45
|
+
"api/views.py": '''
|
|
46
|
+
from drf_spectacular.utils import extend_schema
|
|
47
|
+
from rest_framework import viewsets
|
|
48
|
+
from rest_framework.decorators import api_view, authentication_classes, permission_classes
|
|
49
|
+
from rest_framework.permissions import AllowAny, IsAuthenticated
|
|
50
|
+
from rest_framework.views import APIView
|
|
51
|
+
from .base import Protected
|
|
52
|
+
from .perms import IsOwner
|
|
53
|
+
|
|
54
|
+
class Public(APIView):
|
|
55
|
+
def get(self, request):
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
class Admin(Protected):
|
|
59
|
+
def post(self, request):
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
class Mixed(viewsets.ViewSet):
|
|
63
|
+
def get_permissions(self):
|
|
64
|
+
if self.action == "list":
|
|
65
|
+
return [AllowAny()]
|
|
66
|
+
return [IsAuthenticated()]
|
|
67
|
+
def list(self, request):
|
|
68
|
+
return None
|
|
69
|
+
def create(self, request):
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
class Owned(APIView):
|
|
73
|
+
permission_classes = [IsOwner]
|
|
74
|
+
def get(self, request):
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
class Spelled(APIView):
|
|
78
|
+
@extend_schema(auth=[{"basicAuth": []}])
|
|
79
|
+
def get(self, request):
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
@api_view(["POST"])
|
|
83
|
+
@authentication_classes([])
|
|
84
|
+
@permission_classes([IsAuthenticated])
|
|
85
|
+
def hook(request):
|
|
86
|
+
return None
|
|
87
|
+
''',
|
|
88
|
+
"api/perms.py": '''
|
|
89
|
+
from rest_framework.permissions import BasePermission
|
|
90
|
+
class IsOwner(BasePermission):
|
|
91
|
+
pass
|
|
92
|
+
''',
|
|
93
|
+
"api/urls.py": '''
|
|
94
|
+
from django.urls import path
|
|
95
|
+
from rest_framework.routers import DefaultRouter
|
|
96
|
+
from .views import Admin, Owned, Public, Spelled, hook, Mixed
|
|
97
|
+
|
|
98
|
+
router = DefaultRouter()
|
|
99
|
+
router.register("mixed", Mixed, basename="mixed")
|
|
100
|
+
urlpatterns = [
|
|
101
|
+
path("public/", Public.as_view()),
|
|
102
|
+
path("admin/", Admin.as_view()),
|
|
103
|
+
path("owned/", Owned.as_view()),
|
|
104
|
+
path("spelled/", Spelled.as_view()),
|
|
105
|
+
path("hook/", hook),
|
|
106
|
+
] + router.urls
|
|
107
|
+
''',
|
|
108
|
+
}
|
|
109
|
+
for name, contents in files.items():
|
|
110
|
+
path = os.path.join(root, name)
|
|
111
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
112
|
+
with open(path, "w", encoding="utf-8") as handle:
|
|
113
|
+
handle.write(contents)
|
|
114
|
+
self.project = Project(root, root, lambda path: os.path.relpath(path, root).replace(os.sep, "/"))
|
|
115
|
+
self.b = Builder()
|
|
116
|
+
routes = routing.read(self.project)
|
|
117
|
+
endpoint_apps = routed_applications(self.project, apps.discover(self.project, []), routes)
|
|
118
|
+
endpoints = []
|
|
119
|
+
for app in endpoint_apps:
|
|
120
|
+
endpoints += [(app, endpoint) for endpoint in transport.read_endpoints(app, self.b, routes)]
|
|
121
|
+
registry = serializers.read(self.project, endpoint_apps)
|
|
122
|
+
self.registry = auth.Registry(self.project, "", self.b)
|
|
123
|
+
self.spec = openapi_document(endpoints, "Api", self.b, registry, "", self.registry)
|
|
124
|
+
|
|
125
|
+
def tearDown(self):
|
|
126
|
+
self.holder.cleanup()
|
|
127
|
+
|
|
128
|
+
def operation(self, path, verb):
|
|
129
|
+
return self.spec["paths"][path][verb]
|
|
130
|
+
|
|
131
|
+
def test_the_settings_decide_the_document_and_a_view_that_says_nothing(self):
|
|
132
|
+
self.assertEqual(self.registry.default_authentication, ["TokenAuthentication"])
|
|
133
|
+
self.assertEqual(self.spec["security"], [{"tokenAuth": []}, {}])
|
|
134
|
+
self.assertEqual(self.operation("/public/", "get")["security"], [{"tokenAuth": []}, {}])
|
|
135
|
+
self.assertEqual(self.operation("/public/", "get")["x-portolan-permissions"], ["AllowAny"])
|
|
136
|
+
self.assertEqual(self.spec["components"]["securitySchemes"]["tokenAuth"]["in"], "header")
|
|
137
|
+
|
|
138
|
+
def test_a_local_base_class_lends_its_permissions(self):
|
|
139
|
+
self.assertEqual(self.operation("/admin/", "post")["security"], [{"tokenAuth": []}])
|
|
140
|
+
self.assertEqual(self.operation("/admin/", "post")["x-portolan-permissions"], ["IsAuthenticated"])
|
|
141
|
+
|
|
142
|
+
def test_get_permissions_is_decided_per_action(self):
|
|
143
|
+
self.assertEqual(self.operation("/mixed/", "get")["security"], [{"tokenAuth": []}, {}])
|
|
144
|
+
self.assertEqual(self.operation("/mixed/", "post")["security"], [{"tokenAuth": []}])
|
|
145
|
+
|
|
146
|
+
def test_a_permission_the_reader_does_not_know_is_named_and_not_guessed(self):
|
|
147
|
+
owned = self.operation("/owned/", "get")
|
|
148
|
+
self.assertNotIn("security", owned)
|
|
149
|
+
self.assertEqual(owned["x-portolan-permissions"], ["IsOwner"])
|
|
150
|
+
self.assertTrue(any("IsOwner is not a DRF permission" in w.message for w in self.b.warnings))
|
|
151
|
+
|
|
152
|
+
def test_the_schema_decorator_spells_the_requirement_itself(self):
|
|
153
|
+
self.assertEqual(self.operation("/spelled/", "get")["security"], [{"basicAuth": []}])
|
|
154
|
+
self.assertEqual(self.spec["components"]["securitySchemes"]["basicAuth"], {"type": "http", "scheme": "basic"})
|
|
155
|
+
|
|
156
|
+
def test_a_function_view_reads_its_decorators_and_an_empty_list_is_a_fact(self):
|
|
157
|
+
hook = self.operation("/hook/", "post")
|
|
158
|
+
self.assertEqual(hook["security"], [])
|
|
159
|
+
self.assertEqual(hook["x-portolan-permissions"], ["IsAuthenticated"])
|
|
160
|
+
self.assertTrue(any("nobody can call it" in w.message for w in self.b.warnings))
|
|
161
|
+
|
|
162
|
+
def test_the_document_is_still_json(self):
|
|
163
|
+
json.dumps(self.spec)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class ReadOnly(unittest.TestCase):
|
|
167
|
+
def test_read_only_permissions_relax_safe_verbs_only(self):
|
|
168
|
+
registry = auth.Registry.__new__(auth.Registry)
|
|
169
|
+
registry.schemes = {}
|
|
170
|
+
registry._warned = set()
|
|
171
|
+
registry.b = Builder()
|
|
172
|
+
for verb, expected in (("GET", [{"cookieAuth": []}, {}]), ("POST", [{"cookieAuth": []}])):
|
|
173
|
+
decided = registry.decide(["SessionAuthentication"], ["IsAuthenticatedOrReadOnly"], verb, "")
|
|
174
|
+
self.assertEqual(decided.requirement, expected)
|
|
175
|
+
|
|
176
|
+
def test_a_custom_authentication_class_is_a_scheme_whose_transport_is_unknown(self):
|
|
177
|
+
registry = auth.Registry.__new__(auth.Registry)
|
|
178
|
+
registry.schemes = {}
|
|
179
|
+
registry._warned = set()
|
|
180
|
+
registry.b = Builder()
|
|
181
|
+
decided = registry.decide(["HmacAuthentication"], ["IsAuthenticated"], "POST", "api/views.py")
|
|
182
|
+
self.assertEqual(decided.requirement, [{"hmacAuthentication": []}])
|
|
183
|
+
self.assertEqual(decided.schemes["hmacAuthentication"]["scheme"], "unknown")
|
|
184
|
+
self.assertEqual(len(registry.b.warnings), 1)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
unittest.main()
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""A call to another service, read off the client that makes it.
|
|
2
|
+
|
|
3
|
+
`<app>/clients/<peer>/` holds the vendored document and the class that calls
|
|
4
|
+
it. The code names the verb and the route - `self._http.post("/v1/quotes")` -
|
|
5
|
+
and the document beside it says which operation answers there, so the call is
|
|
6
|
+
recorded under the id the callee's own extractor would give it. Without a
|
|
7
|
+
document there is no id to share: the call is still recorded, against the route
|
|
8
|
+
it names, and left unresolved.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import ast
|
|
14
|
+
import os
|
|
15
|
+
from dataclasses import dataclass, field as dc_field
|
|
16
|
+
from typing import Dict, List, Optional, Tuple
|
|
17
|
+
|
|
18
|
+
import openapi
|
|
19
|
+
from apps import App
|
|
20
|
+
from source import Module, const_str, dotted
|
|
21
|
+
|
|
22
|
+
VERBS = {"get", "post", "put", "patch", "delete", "head", "options", "request"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Call:
|
|
27
|
+
"""One method of a client, and what it reaches."""
|
|
28
|
+
|
|
29
|
+
id: str # "shop.v1.Pricing/GetQuote", or "POST /v1/quotes" when nothing names it
|
|
30
|
+
pkg: str # what the manifest's peers map is keyed by: the api id
|
|
31
|
+
source: str # the document, or the client module
|
|
32
|
+
label: str # what the arrow says: the operation, or the route
|
|
33
|
+
resolved: bool
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Client:
|
|
38
|
+
name: str # the class
|
|
39
|
+
module: Module
|
|
40
|
+
directory: str
|
|
41
|
+
spec: Optional[openapi.Spec] = None
|
|
42
|
+
calls: Dict[str, Call] = dc_field(default_factory=dict) # method -> call
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def path_of(node: ast.AST) -> str:
|
|
46
|
+
"""The route as the code writes it, with every hole spelled the same."""
|
|
47
|
+
literal = const_str(node)
|
|
48
|
+
if literal:
|
|
49
|
+
return literal
|
|
50
|
+
if isinstance(node, ast.JoinedStr):
|
|
51
|
+
out = ""
|
|
52
|
+
for part in node.values:
|
|
53
|
+
if isinstance(part, ast.Constant) and isinstance(part.value, str):
|
|
54
|
+
out += part.value
|
|
55
|
+
else:
|
|
56
|
+
out += "{}"
|
|
57
|
+
return out
|
|
58
|
+
return ""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def http_call(node: ast.Call) -> Optional[Tuple[str, str]]:
|
|
62
|
+
"""(verb, route) for a call that goes out over HTTP.
|
|
63
|
+
|
|
64
|
+
The route has to look like one. `get` is the name of an HTTP verb and also
|
|
65
|
+
the name of the method every mapping in Python has, and what tells the two
|
|
66
|
+
apart is that only one of them is handed a path."""
|
|
67
|
+
name = dotted(node.func)
|
|
68
|
+
verb = name.split(".")[-1]
|
|
69
|
+
if verb not in VERBS or not node.args:
|
|
70
|
+
return None
|
|
71
|
+
if verb == "request":
|
|
72
|
+
if len(node.args) < 2:
|
|
73
|
+
return None
|
|
74
|
+
route = path_of(node.args[1])
|
|
75
|
+
return (const_str(node.args[0]).upper(), route) if is_route(route) else None
|
|
76
|
+
route = path_of(node.args[0])
|
|
77
|
+
return (verb.upper(), route) if is_route(route) else None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def is_route(route: str) -> bool:
|
|
81
|
+
return route.startswith("/") or route.startswith("http://") or route.startswith("https://")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def read_clients(app: App, peers: Dict[str, str], rel, b) -> List[Client]:
|
|
85
|
+
out: List[Client] = []
|
|
86
|
+
for module in app.package("clients"):
|
|
87
|
+
directory = os.path.dirname(module.path)
|
|
88
|
+
spec_path = openapi.beside(directory)
|
|
89
|
+
spec = openapi.read(spec_path) if spec_path else None
|
|
90
|
+
for node in module.classes():
|
|
91
|
+
client = Client(name=node.name, module=module, directory=directory, spec=spec)
|
|
92
|
+
for method in node.body:
|
|
93
|
+
if not isinstance(method, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
94
|
+
continue
|
|
95
|
+
for child in ast.walk(method):
|
|
96
|
+
if not isinstance(child, ast.Call):
|
|
97
|
+
continue
|
|
98
|
+
found = http_call(child)
|
|
99
|
+
if found is None:
|
|
100
|
+
continue
|
|
101
|
+
verb, route = found
|
|
102
|
+
client.calls[method.name] = resolve(verb, route, spec, module, os.path.basename(directory), rel, b)
|
|
103
|
+
break
|
|
104
|
+
if client.calls:
|
|
105
|
+
out.append(client)
|
|
106
|
+
if spec is None and any(module.classes()):
|
|
107
|
+
b.warn(module.rel, "no OpenAPI document beside this client: its calls name a route, and nothing says which operation answers there")
|
|
108
|
+
for client in out:
|
|
109
|
+
for call in client.calls.values():
|
|
110
|
+
if call.resolved and call.pkg not in peers:
|
|
111
|
+
b.warn(call.id, "%s answers this call and the manifest names no peer for it: the step stays unresolved" % call.pkg)
|
|
112
|
+
return out
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def resolve(verb: str, route: str, spec: Optional[openapi.Spec], module: Module, fallback: str, rel, b) -> Call:
|
|
116
|
+
if spec is not None:
|
|
117
|
+
operation = spec.find(verb, route)
|
|
118
|
+
if operation is not None:
|
|
119
|
+
return Call(id=operation.call_id(spec.api), pkg=spec.api, source=rel(spec.path), label=operation.id, resolved=True)
|
|
120
|
+
b.warn(module.rel, "%s %s is not a route %s declares" % (verb, route, rel(spec.path)))
|
|
121
|
+
return Call(id="%s %s" % (verb, route), pkg=fallback, source=module.rel, label="%s %s" % (verb, route), resolved=False)
|