Files
GeminiKeyManagement/gemini_key_manager/database.py

184 lines
5.7 KiB
Python

"""Manages persistent storage of API key metadata.
Implements:
- JSON schema validation
- Thread-safe database operations
- Key lifecycle tracking
- Data versioning and backup
"""
from __future__ import annotations
import os
import json
import logging
import sys
from datetime import datetime, timezone
from typing import Any, Dict, List
import jsonschema
from google.cloud.resourcemanager_v3.types import Project as CloudProject
from google.cloud.api_keys_v2.types import Key as CloudKey
from .types import Account, ApiKeysDatabase, Project as LocalProject, TempKey
def load_schema(filename: str) -> Dict[str, Any]:
"""Validates and loads JSON schema definition.
Args:
filename (str): Path to schema file
Returns:
dict: Parsed schema document
Exits:
SystemExit: On invalid schema file
"""
if not os.path.exists(filename):
logging.error(f"Schema file not found at '{filename}'")
sys.exit(1)
with open(filename, "r") as f:
try:
return json.load(f)
except json.JSONDecodeError:
logging.error(f"Could not decode JSON schema from {filename}.")
sys.exit(1)
def load_keys_database(filename: str, schema: Dict[str, Any]) -> ApiKeysDatabase:
"""Loads and validates the JSON database of API keys."""
now = datetime.now(timezone.utc).isoformat()
empty_db: ApiKeysDatabase = {
"schema_version": "1.0.0",
"accounts": [],
"generation_timestamp_utc": now,
"last_modified_utc": now,
}
if not os.path.exists(filename):
return empty_db
with open(filename, "r") as f:
try:
data = json.load(f)
jsonschema.validate(instance=data, schema=schema)
return data
except json.JSONDecodeError:
logging.warning(f"Could not decode JSON from {filename}. Starting fresh.")
except jsonschema.ValidationError as e:
logging.warning(
f"Database file '{filename}' is not valid. {e.message}. Starting fresh."
)
return empty_db
def save_keys_to_json(
data: ApiKeysDatabase, filename: str, schema: Dict[str, Any]
) -> None:
"""Validates and saves the API key data to a single JSON file."""
now = datetime.now(timezone.utc).isoformat()
data["generation_timestamp_utc"] = data.get("generation_timestamp_utc", now)
data["last_modified_utc"] = now
try:
jsonschema.validate(instance=data, schema=schema)
with open(filename, "w") as f:
json.dump(data, f, indent=2)
logging.info(f"--- Database saved to {filename} ---")
except jsonschema.ValidationError as e:
logging.error(f"Data to be saved is invalid. Could not write to '{filename}'.")
logging.error(f"Validation Error: {e.message}")
sys.exit(1)
def add_key_to_database(
account_entry: Account, project: CloudProject, key_object: TempKey | CloudKey
) -> None:
"""Adds a new API key's details to the data structure."""
project_id = project.project_id
project_entry = next(
(
p
for p in account_entry["projects"]
if p.get("project_info", {}).get("project_id") == project_id
),
None,
)
if not project_entry:
project_entry: LocalProject = {
"project_info": {
"project_id": project_id,
"project_name": project.display_name,
"project_number": project.name.split("/")[-1],
"state": str(project.state),
},
"api_keys": [],
}
account_entry["projects"].append(project_entry)
api_targets: List[Dict[str, List[str]]] = []
if key_object.restrictions and key_object.restrictions.api_targets:
for target in key_object.restrictions.api_targets:
api_targets.append({"service": target.service, "methods": []})
new_key_entry = {
"key_details": {
"key_string": key_object.key_string,
"key_id": key_object.uid,
"key_name": key_object.name,
"display_name": key_object.display_name,
"creation_timestamp_utc": key_object.create_time.isoformat(),
"last_updated_timestamp_utc": key_object.update_time.isoformat(),
},
"restrictions": {"api_targets": api_targets},
"state": "ACTIVE",
}
existing_key = next(
(
k
for k in project_entry["api_keys"]
if k.get("key_details", {}).get("key_id") == key_object.uid
),
None,
)
if not existing_key:
project_entry["api_keys"].append(new_key_entry)
logging.info(
f" Added key {key_object.uid} to local database for project {project_id}"
)
else:
logging.warning(
f" Key {key_object.uid} already exists in local database for project {project_id}"
)
def remove_keys_from_database(
account_entry: Account, project_id: str, deleted_keys_uids: List[str]
) -> None:
"""Removes deleted API keys from the data structure."""
project_entry = next(
(
p
for p in account_entry["projects"]
if p.get("project_info", {}).get("project_id") == project_id
),
None,
)
if not project_entry:
return
initial_key_count = len(project_entry["api_keys"])
project_entry["api_keys"] = [
key
for key in project_entry["api_keys"]
if key.get("key_details", {}).get("key_id") not in deleted_keys_uids
]
final_key_count = len(project_entry["api_keys"])
num_removed = initial_key_count - final_key_count
if num_removed > 0:
logging.info(
f" Removed {num_removed} key(s) from local database for project {project_id}"
)