-
Notifications
You must be signed in to change notification settings - Fork 0
refactor: zero-hardcode settings-driven rebuild #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| FROM python:3.11-slim | ||
| WORKDIR /app | ||
| COPY pyproject.toml README.md ./ | ||
| COPY matgraph ./matgraph | ||
| RUN pip install --no-cache-dir -e . | ||
| EXPOSE 8000 | ||
| ENV MATGRAPH_CACHE_DIR=/data/cache | ||
| CMD ["uvicorn","matgraph.graphql_app:app","--host","0.0.0.0","--port","8000"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| services: | ||
| api: | ||
| build: . | ||
| ports: ["8000:8000"] | ||
| environment: | ||
| - MP_API_KEY=${MP_API_KEY} | ||
| - MATGRAPH_CACHE_DIR=/data/cache | ||
| - MATGRAPH_GRAPHQL_DEFAULT_LIMIT=10 | ||
| volumes: | ||
| - matcache:/data/cache | ||
|
|
||
| volumes: | ||
| matcache: |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| from matgraph.graphql_app import app | ||
| __all__ = ["app"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,45 +1,88 @@ | ||
| import json | ||
| import secrets | ||
| import os | ||
| import hashlib | ||
| import time | ||
| from pathlib import Path | ||
| from typing import Optional | ||
|
|
||
| KEYS_FILE = Path.home() / ".matgraph_keys.json" | ||
| def _keys_file() -> Path: | ||
| from matgraph.settings import settings | ||
| return settings.auth_keys_file | ||
|
|
||
| def _prefix() -> str: | ||
| from matgraph.settings import settings | ||
| return settings.auth_key_prefix | ||
|
|
||
| def load_keys() -> dict: | ||
| if not KEYS_FILE.exists(): | ||
| f = _keys_file() | ||
| if not f.exists(): | ||
| return {} | ||
| with open(KEYS_FILE, "r") as f: | ||
| with open(f, "r") as fh: | ||
| try: | ||
| return json.load(f) | ||
| return json.load(fh) | ||
| except json.JSONDecodeError: | ||
| return {} | ||
|
|
||
| def save_keys(keys: dict): | ||
| with open(KEYS_FILE, "w") as f: | ||
| json.dump(keys, f, indent=4) | ||
| f = _keys_file() | ||
| f.parent.mkdir(parents=True, exist_ok=True) | ||
| with open(f, "w") as fh: | ||
| json.dump(keys, fh, indent=4) | ||
| try: | ||
| f.chmod(0o600) | ||
| except Exception: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| pass | ||
|
|
||
| def _hash_key(api_key: str) -> str: | ||
| return hashlib.sha256(api_key.encode()).hexdigest() | ||
|
|
||
| def generate_api_key(user_name: str) -> str: | ||
| """Generates a secure API key for a user and saves it.""" | ||
| def generate_api_key(user_name: str, ttl_days: Optional[int] = None, scopes: Optional[list] = None) -> str: | ||
| """Generates a secure API key, stores only hash. No plaintext.""" | ||
| from matgraph.settings import settings | ||
| if ttl_days is None: | ||
| ttl_days = settings.auth_default_ttl_days | ||
| if scopes is None: | ||
| scopes = ["read:predict","read:phonon","read:elastic"] | ||
| keys = load_keys() | ||
| new_key = "mg_" + secrets.token_urlsafe(24) | ||
| keys[new_key] = { | ||
| "user": user_name, | ||
| "active": True | ||
| } | ||
| raw = _prefix() + secrets.token_urlsafe(24) | ||
| h = _hash_key(raw) | ||
| expires_at = time.time() + ttl_days*86400 if ttl_days else None | ||
| keys[h] = {"user": user_name, "active": True, "scopes": scopes, "created_at": time.time(), "expires_at": expires_at, "prefix": raw[:8]+"..."} | ||
| save_keys(keys) | ||
| return new_key | ||
| return raw | ||
|
|
||
| def is_valid_key(api_key: str) -> bool: | ||
| """Checks if the API key is valid.""" | ||
| # Allow master key from env for dev purposes | ||
| master_key = os.environ.get("MATGRAPH_API_KEY") | ||
| if master_key and api_key == master_key: | ||
| def is_valid_key(api_key: str, required_scope: Optional[str] = None) -> bool: | ||
| master = os.environ.get("MATGRAPH_API_KEY") | ||
| if master and api_key == master: | ||
| return True | ||
|
|
||
| # support legacy plaintext keys file for migration | ||
| h = _hash_key(api_key) | ||
| keys = load_keys() | ||
| key_info = keys.get(api_key) | ||
| if key_info and key_info.get("active", False): | ||
| return True | ||
|
|
||
| return False | ||
| # legacy: if keys contain plaintext key directly, migrate check | ||
| if api_key in keys: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| info = keys[api_key] | ||
| else: | ||
| info = keys.get(h) | ||
| if not info or not info.get("active", False): | ||
| return False | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic |
||
| exp = info.get("expires_at") | ||
| if exp and time.time() > exp: | ||
| return False | ||
| if required_scope and required_scope not in info.get("scopes", []): | ||
| return False | ||
| return True | ||
|
|
||
| def revoke_key(api_key: str) -> bool: | ||
| h = _hash_key(api_key) | ||
| keys = load_keys() | ||
| # try hash or plaintext | ||
| target = h if h in keys else (api_key if api_key in keys else None) | ||
| if not target: | ||
| return False | ||
| keys[target]["active"] = False | ||
| save_keys(keys) | ||
| return True | ||
|
|
||
| def list_keys() -> dict: | ||
| return load_keys() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For production deployments, it's generally recommended to install packages in non-editable mode (
pip install --no-cache-dir .) to ensure a cleaner and more predictable build. Editable installs (-e .) are more suited for development. Consider switching this for a production-optimized Dockerfile if this image is intended for deployment.