attocode-code-intel 0.1.0__py3-none-any.whl
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.
- attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
- attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
- attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
- attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
- attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
- attocode_intel/GUIDELINES.md +432 -0
- attocode_intel/__init__.py +52 -0
- attocode_intel/_internal/__init__.py +1 -0
- attocode_intel/_internal/errors/__init__.py +126 -0
- attocode_intel/_internal/integrations/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
- attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
- attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
- attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
- attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
- attocode_intel/_internal/integrations/context/cas.py +466 -0
- attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
- attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
- attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
- attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
- attocode_intel/_internal/integrations/context/cross_references.py +616 -0
- attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
- attocode_intel/_internal/integrations/context/embeddings.py +282 -0
- attocode_intel/_internal/integrations/context/frecency.py +427 -0
- attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
- attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
- attocode_intel/_internal/integrations/context/hydration.py +90 -0
- attocode_intel/_internal/integrations/context/index_repair.py +87 -0
- attocode_intel/_internal/integrations/context/index_store.py +586 -0
- attocode_intel/_internal/integrations/context/memory_store.py +602 -0
- attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
- attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
- attocode_intel/_internal/integrations/context/query_history.py +417 -0
- attocode_intel/_internal/integrations/context/reranker.py +351 -0
- attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
- attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
- attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
- attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
- attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
- attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
- attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
- attocode_intel/_internal/integrations/context/vector_store.py +795 -0
- attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
- attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
- attocode_intel/_internal/integrations/lsp/client.py +1140 -0
- attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
- attocode_intel/_internal/integrations/mcp/client.py +253 -0
- attocode_intel/_internal/integrations/security/__init__.py +1 -0
- attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
- attocode_intel/_internal/integrations/security/dataflow.py +522 -0
- attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
- attocode_intel/_internal/integrations/security/matcher.py +44 -0
- attocode_intel/_internal/integrations/security/patterns.py +724 -0
- attocode_intel/_internal/integrations/security/scanner.py +351 -0
- attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
- attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
- attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
- attocode_intel/_internal/tricks/__init__.py +1 -0
- attocode_intel/_internal/tricks/recursive_context.py +357 -0
- attocode_intel/_internal/types/__init__.py +1 -0
- attocode_intel/_internal/types/events.py +289 -0
- attocode_intel/_internal/types/severity.py +15 -0
- attocode_intel/_shared.py +341 -0
- attocode_intel/analysis_coverage.py +56 -0
- attocode_intel/api/__init__.py +1 -0
- attocode_intel/api/access.py +32 -0
- attocode_intel/api/app.py +247 -0
- attocode_intel/api/auth/__init__.py +128 -0
- attocode_intel/api/auth/api_keys.py +61 -0
- attocode_intel/api/auth/context.py +34 -0
- attocode_intel/api/auth/jwt.py +66 -0
- attocode_intel/api/auth/oauth.py +146 -0
- attocode_intel/api/auth/passwords.py +15 -0
- attocode_intel/api/deps.py +351 -0
- attocode_intel/api/knowledge_bridge.py +13 -0
- attocode_intel/api/mcp_transport.py +51 -0
- attocode_intel/api/middleware.py +411 -0
- attocode_intel/api/models.py +698 -0
- attocode_intel/api/providers/__init__.py +1 -0
- attocode_intel/api/providers/db_provider.py +1106 -0
- attocode_intel/api/providers/local_provider.py +476 -0
- attocode_intel/api/providers/remote_provider.py +740 -0
- attocode_intel/api/repository_source.py +21 -0
- attocode_intel/api/routes/__init__.py +1 -0
- attocode_intel/api/routes/_pin_helper.py +158 -0
- attocode_intel/api/routes/activity.py +148 -0
- attocode_intel/api/routes/adr.py +102 -0
- attocode_intel/api/routes/analysis.py +515 -0
- attocode_intel/api/routes/api_keys.py +170 -0
- attocode_intel/api/routes/auth.py +428 -0
- attocode_intel/api/routes/branches.py +262 -0
- attocode_intel/api/routes/cross_repo_search.py +198 -0
- attocode_intel/api/routes/embeddings.py +386 -0
- attocode_intel/api/routes/files.py +132 -0
- attocode_intel/api/routes/files_v2.py +388 -0
- attocode_intel/api/routes/gc.py +322 -0
- attocode_intel/api/routes/git_v2.py +433 -0
- attocode_intel/api/routes/graph.py +174 -0
- attocode_intel/api/routes/graph_viz.py +160 -0
- attocode_intel/api/routes/health.py +62 -0
- attocode_intel/api/routes/history.py +175 -0
- attocode_intel/api/routes/jobs.py +84 -0
- attocode_intel/api/routes/learning.py +205 -0
- attocode_intel/api/routes/lsp.py +272 -0
- attocode_intel/api/routes/notify.py +623 -0
- attocode_intel/api/routes/operations.py +27 -0
- attocode_intel/api/routes/orgs.py +924 -0
- attocode_intel/api/routes/preferences.py +100 -0
- attocode_intel/api/routes/presence.py +42 -0
- attocode_intel/api/routes/projects.py +116 -0
- attocode_intel/api/routes/repos.py +47 -0
- attocode_intel/api/routes/search.py +180 -0
- attocode_intel/api/routes/snapshots.py +614 -0
- attocode_intel/api/routes/webhooks.py +313 -0
- attocode_intel/api/routes/websocket.py +116 -0
- attocode_intel/api/static/graph.html +239 -0
- attocode_intel/api/utils.py +96 -0
- attocode_intel/artifacts/__init__.py +45 -0
- attocode_intel/artifacts/hashing.py +230 -0
- attocode_intel/artifacts/provenance.py +190 -0
- attocode_intel/artifacts/retrieval_pin.py +159 -0
- attocode_intel/audit.py +59 -0
- attocode_intel/bug_finder.py +206 -0
- attocode_intel/bundle.py +80 -0
- attocode_intel/catalog.py +242 -0
- attocode_intel/change_review.py +75 -0
- attocode_intel/cli.py +3062 -0
- attocode_intel/community.py +63 -0
- attocode_intel/confidence/__init__.py +144 -0
- attocode_intel/confidence/jev.py +234 -0
- attocode_intel/confidence/llm.py +330 -0
- attocode_intel/confidence/redact.py +44 -0
- attocode_intel/confidence/settings.py +74 -0
- attocode_intel/config.py +256 -0
- attocode_intel/crypto.py +31 -0
- attocode_intel/db/__init__.py +7 -0
- attocode_intel/db/base.py +33 -0
- attocode_intel/db/engine.py +61 -0
- attocode_intel/db/models.py +529 -0
- attocode_intel/entrypoint.py +185 -0
- attocode_intel/focused_evidence.py +130 -0
- attocode_intel/freshness.py +154 -0
- attocode_intel/gateway.py +734 -0
- attocode_intel/git/__init__.py +7 -0
- attocode_intel/git/credentials.py +75 -0
- attocode_intel/git/manager.py +493 -0
- attocode_intel/git/models.py +96 -0
- attocode_intel/git/storage.py +98 -0
- attocode_intel/graph_query_parser.py +761 -0
- attocode_intel/helpers.py +712 -0
- attocode_intel/indexing/__init__.py +29 -0
- attocode_intel/indexing/debouncer.py +136 -0
- attocode_intel/indexing/delta_indexer.py +151 -0
- attocode_intel/indexing/full_indexer.py +323 -0
- attocode_intel/indexing/incremental.py +240 -0
- attocode_intel/indexing/parser.py +403 -0
- attocode_intel/indexing/summarizer.py +264 -0
- attocode_intel/installer.py +1896 -0
- attocode_intel/knowledge.py +269 -0
- attocode_intel/local_knowledge.py +77 -0
- attocode_intel/migrations/alembic.ini +36 -0
- attocode_intel/migrations/env.py +71 -0
- attocode_intel/migrations/script.py.mako +27 -0
- attocode_intel/migrations/versions/001_initial_schema.py +219 -0
- attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
- attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
- attocode_intel/migrations/versions/004_audit_events.py +38 -0
- attocode_intel/migrations/versions/005_user_preferences.py +30 -0
- attocode_intel/migrations/versions/006_google_oauth.py +27 -0
- attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
- attocode_intel/migrations/versions/008_symbol_references.py +37 -0
- attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
- attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
- attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
- attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
- attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
- attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
- attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
- attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
- attocode_intel/migrations/versions/017_provenance_table.py +102 -0
- attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
- attocode_intel/migrations/versions/019_symref_caller.py +42 -0
- attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
- attocode_intel/onboarding.py +258 -0
- attocode_intel/output.py +178 -0
- attocode_intel/precision.py +133 -0
- attocode_intel/presence.py +160 -0
- attocode_intel/probe.py +110 -0
- attocode_intel/project_dir.py +97 -0
- attocode_intel/pubsub.py +113 -0
- attocode_intel/query_ranking.py +242 -0
- attocode_intel/readiness.py +972 -0
- attocode_intel/redis.py +51 -0
- attocode_intel/reference_pages.py +60 -0
- attocode_intel/remote.py +131 -0
- attocode_intel/repo_ranker.py +246 -0
- attocode_intel/request_context.py +82 -0
- attocode_intel/rules/__init__.py +28 -0
- attocode_intel/rules/ci.py +320 -0
- attocode_intel/rules/combinators.py +293 -0
- attocode_intel/rules/enricher.py +127 -0
- attocode_intel/rules/evolution.py +452 -0
- attocode_intel/rules/executor.py +531 -0
- attocode_intel/rules/filters/__init__.py +5 -0
- attocode_intel/rules/filters/pipeline.py +151 -0
- attocode_intel/rules/formatter.py +198 -0
- attocode_intel/rules/hygiene.py +242 -0
- attocode_intel/rules/importers/__init__.py +1 -0
- attocode_intel/rules/importers/semgrep.py +382 -0
- attocode_intel/rules/loader.py +292 -0
- attocode_intel/rules/marketplace.py +375 -0
- attocode_intel/rules/metavar.py +181 -0
- attocode_intel/rules/model.py +317 -0
- attocode_intel/rules/packs/__init__.py +1 -0
- attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
- attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
- attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
- attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
- attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
- attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
- attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
- attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
- attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
- attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
- attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
- attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
- attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
- attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
- attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
- attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
- attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
- attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
- attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
- attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
- attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
- attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
- attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
- attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
- attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
- attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
- attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
- attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
- attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
- attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
- attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
- attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
- attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/pack_loader.py +244 -0
- attocode_intel/rules/plugins/__init__.py +1 -0
- attocode_intel/rules/plugins/plugin_loader.py +132 -0
- attocode_intel/rules/profiling.py +332 -0
- attocode_intel/rules/registry.py +213 -0
- attocode_intel/rules/sarif.py +187 -0
- attocode_intel/rules/synthesis.py +420 -0
- attocode_intel/rules/testing.py +326 -0
- attocode_intel/server.py +734 -0
- attocode_intel/service.py +3416 -0
- attocode_intel/sharing.py +57 -0
- attocode_intel/storage/__init__.py +6 -0
- attocode_intel/storage/branch_overlay.py +352 -0
- attocode_intel/storage/content_store.py +220 -0
- attocode_intel/storage/dependency_store.py +143 -0
- attocode_intel/storage/diff_engine.py +187 -0
- attocode_intel/storage/embedding_store.py +594 -0
- attocode_intel/storage/provenance_store.py +137 -0
- attocode_intel/storage/security_scanner_db.py +164 -0
- attocode_intel/storage/symbol_store.py +160 -0
- attocode_intel/structural_evidence.py +144 -0
- attocode_intel/symbol_inspection.py +141 -0
- attocode_intel/symbol_links.py +167 -0
- attocode_intel/telemetry.py +42 -0
- attocode_intel/test_ranking.py +73 -0
- attocode_intel/testing/__init__.py +43 -0
- attocode_intel/testing/fixtures.py +122 -0
- attocode_intel/testing/helpers.py +75 -0
- attocode_intel/testing/mocks.py +99 -0
- attocode_intel/tools/__init__.py +2 -0
- attocode_intel/tools/adr_tools.py +628 -0
- attocode_intel/tools/analysis_tools.py +382 -0
- attocode_intel/tools/composite_tools.py +695 -0
- attocode_intel/tools/cross_mode_tools.py +173 -0
- attocode_intel/tools/dead_code_tools.py +582 -0
- attocode_intel/tools/distill_tools.py +353 -0
- attocode_intel/tools/frecency_tools.py +220 -0
- attocode_intel/tools/fuzzy_tools.py +255 -0
- attocode_intel/tools/history_tools.py +486 -0
- attocode_intel/tools/learning_tools.py +222 -0
- attocode_intel/tools/lsp_tools.py +218 -0
- attocode_intel/tools/maintenance_tools.py +1662 -0
- attocode_intel/tools/navigation_tools.py +220 -0
- attocode_intel/tools/overlay_tools.py +540 -0
- attocode_intel/tools/pin_store.py +432 -0
- attocode_intel/tools/pin_tools.py +266 -0
- attocode_intel/tools/query_constraints_tools.py +262 -0
- attocode_intel/tools/query_history_tools.py +158 -0
- attocode_intel/tools/readiness_tools.py +44 -0
- attocode_intel/tools/rule_tools.py +992 -0
- attocode_intel/tools/search_tools.py +770 -0
- attocode_intel/tools/snapshot_tools.py +624 -0
- attocode_intel/workers/__init__.py +1 -0
- attocode_intel/workers/job_utils.py +111 -0
- attocode_intel/workers/jobs.py +573 -0
- attocode_intel/workers/run.py +30 -0
- attocode_intel/workers/settings.py +61 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Authentication package — unified auth resolution for local and service modes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from fastapi import Header, HTTPException
|
|
9
|
+
|
|
10
|
+
from attocode_intel.api.deps import get_config
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from attocode_intel.api.auth.context import AuthContext
|
|
14
|
+
|
|
15
|
+
# In-memory revocation cache (jti → expiry_monotonic)
|
|
16
|
+
_revocation_cache: dict[str, float] = {}
|
|
17
|
+
_REVOCATION_CACHE_TTL = 30 # seconds
|
|
18
|
+
_revocation_cache_refreshed_at: float = 0.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def verify_api_key(authorization: str | None = Header(None)) -> None:
|
|
22
|
+
"""Legacy API key verification (local mode).
|
|
23
|
+
|
|
24
|
+
If ATTOCODE_API_KEY is not set, all requests are allowed (local mode).
|
|
25
|
+
"""
|
|
26
|
+
config = get_config()
|
|
27
|
+
if not config.api_key:
|
|
28
|
+
return
|
|
29
|
+
if not authorization:
|
|
30
|
+
raise HTTPException(status_code=401, detail="Authorization header required")
|
|
31
|
+
token = authorization.removeprefix("Bearer ").strip()
|
|
32
|
+
if token != config.api_key:
|
|
33
|
+
raise HTTPException(status_code=401, detail="Invalid API key")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def verify_auth(authorization: str | None = Header(None)) -> None:
|
|
37
|
+
"""Router-level auth guard — delegates to resolve_auth, discards AuthContext."""
|
|
38
|
+
await resolve_auth(authorization)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def _is_token_revoked(jti: str) -> bool:
|
|
42
|
+
"""Check if a JWT's jti is in the revocation blocklist.
|
|
43
|
+
|
|
44
|
+
Uses a 30-second in-memory cache to avoid per-request DB hits.
|
|
45
|
+
"""
|
|
46
|
+
global _revocation_cache_refreshed_at
|
|
47
|
+
|
|
48
|
+
now = time.monotonic()
|
|
49
|
+
|
|
50
|
+
# Refresh cache if stale
|
|
51
|
+
if now - _revocation_cache_refreshed_at > _REVOCATION_CACHE_TTL:
|
|
52
|
+
try:
|
|
53
|
+
from attocode_intel.db.engine import get_session
|
|
54
|
+
from attocode_intel.db.models import RevokedToken
|
|
55
|
+
|
|
56
|
+
from sqlalchemy import select
|
|
57
|
+
|
|
58
|
+
async for session in get_session():
|
|
59
|
+
result = await session.execute(select(RevokedToken.jti))
|
|
60
|
+
_revocation_cache.clear()
|
|
61
|
+
for row in result:
|
|
62
|
+
_revocation_cache[row[0]] = now + _REVOCATION_CACHE_TTL
|
|
63
|
+
_revocation_cache_refreshed_at = now
|
|
64
|
+
break
|
|
65
|
+
except Exception:
|
|
66
|
+
pass # If DB is unavailable, use stale cache
|
|
67
|
+
|
|
68
|
+
return jti in _revocation_cache
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def resolve_auth(authorization: str | None = Header(None)) -> AuthContext:
|
|
72
|
+
"""Unified auth resolver — delegates to legacy or service path based on config.
|
|
73
|
+
|
|
74
|
+
In local mode: verifies static API key, returns a stub AuthContext.
|
|
75
|
+
In service mode: tries JWT first, then service API key resolution.
|
|
76
|
+
"""
|
|
77
|
+
from attocode_intel.api.auth.context import AuthContext
|
|
78
|
+
|
|
79
|
+
config = get_config()
|
|
80
|
+
|
|
81
|
+
if not config.is_service_mode:
|
|
82
|
+
# Local mode — delegate to legacy check
|
|
83
|
+
await verify_api_key(authorization)
|
|
84
|
+
return AuthContext(auth_method="legacy")
|
|
85
|
+
|
|
86
|
+
# Service mode — try JWT, then API key
|
|
87
|
+
if not authorization:
|
|
88
|
+
raise HTTPException(status_code=401, detail="Authorization header required")
|
|
89
|
+
|
|
90
|
+
token = authorization.removeprefix("Bearer ").strip()
|
|
91
|
+
if not token:
|
|
92
|
+
raise HTTPException(status_code=401, detail="Authorization header required")
|
|
93
|
+
|
|
94
|
+
# Try JWT first
|
|
95
|
+
from attocode_intel.api.auth.jwt import decode_token
|
|
96
|
+
|
|
97
|
+
payload = decode_token(token)
|
|
98
|
+
if payload is not None:
|
|
99
|
+
if payload.get("type", "access") != "access":
|
|
100
|
+
raise HTTPException(status_code=401, detail="Access token required")
|
|
101
|
+
import uuid
|
|
102
|
+
|
|
103
|
+
# Check revocation blocklist
|
|
104
|
+
jti = payload.get("jti")
|
|
105
|
+
if jti and await _is_token_revoked(jti):
|
|
106
|
+
raise HTTPException(status_code=401, detail="Token has been revoked")
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
user_id = uuid.UUID(payload["sub"])
|
|
110
|
+
org_id = uuid.UUID(payload["org"]) if payload.get("org") else None
|
|
111
|
+
except (KeyError, TypeError, ValueError, AttributeError) as exc:
|
|
112
|
+
raise HTTPException(status_code=401, detail="Invalid token identity") from exc
|
|
113
|
+
return AuthContext(
|
|
114
|
+
user_id=user_id,
|
|
115
|
+
org_id=org_id,
|
|
116
|
+
scopes=payload.get("scopes", []),
|
|
117
|
+
auth_method="jwt",
|
|
118
|
+
plan=payload.get("plan", "free"),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Try API key
|
|
122
|
+
from attocode_intel.api.auth.api_keys import resolve_api_key
|
|
123
|
+
|
|
124
|
+
ctx = await resolve_api_key(token)
|
|
125
|
+
if ctx is not None:
|
|
126
|
+
return ctx
|
|
127
|
+
|
|
128
|
+
raise HTTPException(status_code=401, detail="Invalid credentials")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Service API key generation and resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import secrets
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from attocode_intel.api.auth.context import AuthContext
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def generate_api_key() -> tuple[str, str, str]:
|
|
15
|
+
"""Generate a new API key.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
(plaintext_key, key_hash, key_prefix) — show plaintext once, store hash.
|
|
19
|
+
"""
|
|
20
|
+
plaintext = f"aci_{secrets.token_urlsafe(32)}"
|
|
21
|
+
key_hash = hashlib.sha256(plaintext.encode()).hexdigest()
|
|
22
|
+
key_prefix = plaintext[:12]
|
|
23
|
+
return plaintext, key_hash, key_prefix
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def resolve_api_key(token: str) -> AuthContext | None:
|
|
27
|
+
"""Look up a service API key by its SHA-256 hash. Returns AuthContext or None."""
|
|
28
|
+
import hashlib
|
|
29
|
+
|
|
30
|
+
from sqlalchemy import select
|
|
31
|
+
|
|
32
|
+
from attocode_intel.api.auth.context import AuthContext
|
|
33
|
+
from attocode_intel.db.engine import get_session
|
|
34
|
+
from attocode_intel.db.models import ApiKey
|
|
35
|
+
|
|
36
|
+
key_hash = hashlib.sha256(token.encode()).hexdigest()
|
|
37
|
+
|
|
38
|
+
async for session in get_session():
|
|
39
|
+
result = await session.execute(
|
|
40
|
+
select(ApiKey).where(ApiKey.key_hash == key_hash)
|
|
41
|
+
)
|
|
42
|
+
api_key = result.scalar_one_or_none()
|
|
43
|
+
if api_key is None:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
# Check expiry
|
|
47
|
+
if api_key.expires_at and api_key.expires_at < datetime.now(timezone.utc):
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
# Update last_used_at
|
|
51
|
+
api_key.last_used_at = datetime.now(timezone.utc)
|
|
52
|
+
await session.commit()
|
|
53
|
+
|
|
54
|
+
return AuthContext(
|
|
55
|
+
user_id=api_key.user_id,
|
|
56
|
+
org_id=api_key.org_id,
|
|
57
|
+
scopes=list(api_key.scopes) if api_key.scopes else [],
|
|
58
|
+
auth_method="api_key",
|
|
59
|
+
plan="free",
|
|
60
|
+
)
|
|
61
|
+
return None
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Authentication context — carries identity info through request lifecycle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(slots=True)
|
|
10
|
+
class AuthContext:
|
|
11
|
+
"""Identity and authorization context for an authenticated request."""
|
|
12
|
+
|
|
13
|
+
user_id: uuid.UUID | None = None
|
|
14
|
+
org_id: uuid.UUID | None = None
|
|
15
|
+
scopes: list[str] = field(default_factory=list)
|
|
16
|
+
auth_method: str = "legacy" # legacy|jwt|api_key
|
|
17
|
+
plan: str = "free" # free|team|enterprise
|
|
18
|
+
|
|
19
|
+
def has_scope(self, scope: str) -> bool:
|
|
20
|
+
"""Check if the context has a specific scope."""
|
|
21
|
+
if not self.scopes:
|
|
22
|
+
return True # Empty scopes = full access (legacy mode)
|
|
23
|
+
return scope in self.scopes
|
|
24
|
+
|
|
25
|
+
def require_scope(self, scope: str) -> None:
|
|
26
|
+
"""Raise if the required scope is missing."""
|
|
27
|
+
if not self.has_scope(scope):
|
|
28
|
+
from fastapi import HTTPException
|
|
29
|
+
|
|
30
|
+
raise HTTPException(status_code=403, detail=f"Missing scope: {scope}")
|
|
31
|
+
|
|
32
|
+
def require_role(self, org_id: uuid.UUID, min_role: str) -> None:
|
|
33
|
+
"""Placeholder for role checks — actual check done in route handlers."""
|
|
34
|
+
pass
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""JWT token creation and validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import uuid
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_access_token(
|
|
13
|
+
user_id: uuid.UUID,
|
|
14
|
+
org_id: uuid.UUID | None = None,
|
|
15
|
+
scopes: list[str] | None = None,
|
|
16
|
+
plan: str = "free",
|
|
17
|
+
expires_minutes: int = 60,
|
|
18
|
+
) -> str:
|
|
19
|
+
"""Create a signed JWT access token."""
|
|
20
|
+
from jose import jwt
|
|
21
|
+
|
|
22
|
+
from attocode_intel.api.deps import get_config
|
|
23
|
+
|
|
24
|
+
config = get_config()
|
|
25
|
+
now = datetime.now(timezone.utc)
|
|
26
|
+
payload = {
|
|
27
|
+
"sub": str(user_id),
|
|
28
|
+
"type": "access",
|
|
29
|
+
"scopes": scopes or [],
|
|
30
|
+
"plan": plan,
|
|
31
|
+
"iat": now,
|
|
32
|
+
"exp": now + timedelta(minutes=expires_minutes),
|
|
33
|
+
}
|
|
34
|
+
if org_id:
|
|
35
|
+
payload["org"] = str(org_id)
|
|
36
|
+
return jwt.encode(payload, config.secret_key, algorithm="HS256")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def create_refresh_token(user_id: uuid.UUID, expires_days: int = 30) -> str:
|
|
40
|
+
"""Create a signed JWT refresh token."""
|
|
41
|
+
from jose import jwt
|
|
42
|
+
|
|
43
|
+
from attocode_intel.api.deps import get_config
|
|
44
|
+
|
|
45
|
+
config = get_config()
|
|
46
|
+
now = datetime.now(timezone.utc)
|
|
47
|
+
payload = {
|
|
48
|
+
"sub": str(user_id),
|
|
49
|
+
"type": "refresh",
|
|
50
|
+
"iat": now,
|
|
51
|
+
"exp": now + timedelta(days=expires_days),
|
|
52
|
+
}
|
|
53
|
+
return jwt.encode(payload, config.secret_key, algorithm="HS256")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def decode_token(token: str) -> dict | None:
|
|
57
|
+
"""Decode and validate a JWT token. Returns payload dict or None if invalid."""
|
|
58
|
+
from jose import JWTError, jwt
|
|
59
|
+
|
|
60
|
+
from attocode_intel.api.deps import get_config
|
|
61
|
+
|
|
62
|
+
config = get_config()
|
|
63
|
+
try:
|
|
64
|
+
return jwt.decode(token, config.secret_key, algorithms=["HS256"])
|
|
65
|
+
except JWTError:
|
|
66
|
+
return None
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""OAuth 2.0 authorization code flows (GitHub + Google)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
GITHUB_AUTHORIZE_URL = "https://github.com/login/oauth/authorize"
|
|
14
|
+
GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token"
|
|
15
|
+
GITHUB_USER_URL = "https://api.github.com/user"
|
|
16
|
+
|
|
17
|
+
GOOGLE_AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth"
|
|
18
|
+
GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
|
|
19
|
+
GOOGLE_USERINFO_URL = "https://openidconnect.googleapis.com/v1/userinfo"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def get_github_auth_url(client_id: str, redirect_uri: str, state: str) -> str:
|
|
23
|
+
"""Build the GitHub OAuth authorization URL."""
|
|
24
|
+
import urllib.parse
|
|
25
|
+
|
|
26
|
+
params = {
|
|
27
|
+
"client_id": client_id,
|
|
28
|
+
"redirect_uri": redirect_uri,
|
|
29
|
+
"scope": "user:email",
|
|
30
|
+
"state": state,
|
|
31
|
+
}
|
|
32
|
+
return f"{GITHUB_AUTHORIZE_URL}?{urllib.parse.urlencode(params)}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def exchange_github_code(
|
|
36
|
+
code: str,
|
|
37
|
+
client_id: str,
|
|
38
|
+
client_secret: str,
|
|
39
|
+
) -> dict:
|
|
40
|
+
"""Exchange authorization code for access token and fetch user info.
|
|
41
|
+
|
|
42
|
+
Returns dict with keys: github_id, email, name, avatar_url.
|
|
43
|
+
"""
|
|
44
|
+
import httpx
|
|
45
|
+
|
|
46
|
+
async with httpx.AsyncClient() as client:
|
|
47
|
+
# Exchange code for token
|
|
48
|
+
token_resp = await client.post(
|
|
49
|
+
GITHUB_TOKEN_URL,
|
|
50
|
+
data={
|
|
51
|
+
"client_id": client_id,
|
|
52
|
+
"client_secret": client_secret,
|
|
53
|
+
"code": code,
|
|
54
|
+
},
|
|
55
|
+
headers={"Accept": "application/json"},
|
|
56
|
+
)
|
|
57
|
+
token_resp.raise_for_status()
|
|
58
|
+
token_data = token_resp.json()
|
|
59
|
+
access_token = token_data["access_token"]
|
|
60
|
+
|
|
61
|
+
# Fetch user profile
|
|
62
|
+
user_resp = await client.get(
|
|
63
|
+
GITHUB_USER_URL,
|
|
64
|
+
headers={"Authorization": f"Bearer {access_token}"},
|
|
65
|
+
)
|
|
66
|
+
user_resp.raise_for_status()
|
|
67
|
+
user_data = user_resp.json()
|
|
68
|
+
|
|
69
|
+
# Fetch email if not public
|
|
70
|
+
email = user_data.get("email")
|
|
71
|
+
if not email:
|
|
72
|
+
email_resp = await client.get(
|
|
73
|
+
"https://api.github.com/user/emails",
|
|
74
|
+
headers={"Authorization": f"Bearer {access_token}"},
|
|
75
|
+
)
|
|
76
|
+
email_resp.raise_for_status()
|
|
77
|
+
emails = email_resp.json()
|
|
78
|
+
primary = next((e for e in emails if e.get("primary")), None)
|
|
79
|
+
email = primary["email"] if primary else emails[0]["email"]
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
"github_id": user_data["id"],
|
|
83
|
+
"email": email,
|
|
84
|
+
"name": user_data.get("name") or user_data.get("login", ""),
|
|
85
|
+
"avatar_url": user_data.get("avatar_url", ""),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def get_google_auth_url(client_id: str, redirect_uri: str, state: str) -> str:
|
|
90
|
+
"""Build the Google OAuth authorization URL."""
|
|
91
|
+
import urllib.parse
|
|
92
|
+
|
|
93
|
+
params = {
|
|
94
|
+
"client_id": client_id,
|
|
95
|
+
"redirect_uri": redirect_uri,
|
|
96
|
+
"scope": "openid email profile",
|
|
97
|
+
"state": state,
|
|
98
|
+
"response_type": "code",
|
|
99
|
+
"access_type": "offline",
|
|
100
|
+
"prompt": "consent",
|
|
101
|
+
}
|
|
102
|
+
return f"{GOOGLE_AUTHORIZE_URL}?{urllib.parse.urlencode(params)}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def exchange_google_code(
|
|
106
|
+
code: str,
|
|
107
|
+
client_id: str,
|
|
108
|
+
client_secret: str,
|
|
109
|
+
redirect_uri: str,
|
|
110
|
+
) -> dict:
|
|
111
|
+
"""Exchange authorization code for access token and fetch user info.
|
|
112
|
+
|
|
113
|
+
Returns dict with keys: google_id, email, name, avatar_url.
|
|
114
|
+
"""
|
|
115
|
+
import httpx
|
|
116
|
+
|
|
117
|
+
async with httpx.AsyncClient() as client:
|
|
118
|
+
# Exchange code for token (Google requires form-encoded, not JSON)
|
|
119
|
+
token_resp = await client.post(
|
|
120
|
+
GOOGLE_TOKEN_URL,
|
|
121
|
+
data={
|
|
122
|
+
"code": code,
|
|
123
|
+
"client_id": client_id,
|
|
124
|
+
"client_secret": client_secret,
|
|
125
|
+
"redirect_uri": redirect_uri,
|
|
126
|
+
"grant_type": "authorization_code",
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
token_resp.raise_for_status()
|
|
130
|
+
token_data = token_resp.json()
|
|
131
|
+
access_token = token_data["access_token"]
|
|
132
|
+
|
|
133
|
+
# Fetch user profile
|
|
134
|
+
user_resp = await client.get(
|
|
135
|
+
GOOGLE_USERINFO_URL,
|
|
136
|
+
headers={"Authorization": f"Bearer {access_token}"},
|
|
137
|
+
)
|
|
138
|
+
user_resp.raise_for_status()
|
|
139
|
+
user_data = user_resp.json()
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
"google_id": user_data["sub"],
|
|
143
|
+
"email": user_data["email"],
|
|
144
|
+
"name": user_data.get("name", ""),
|
|
145
|
+
"avatar_url": user_data.get("picture", ""),
|
|
146
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Password hashing and verification via bcrypt."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import bcrypt
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def hash_password(password: str) -> str:
|
|
9
|
+
"""Hash a plaintext password with bcrypt."""
|
|
10
|
+
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def verify_password(plain: str, hashed: str) -> bool:
|
|
14
|
+
"""Verify a plaintext password against a bcrypt hash."""
|
|
15
|
+
return bcrypt.checkpw(plain.encode(), hashed.encode())
|