File size: 3,946 Bytes
2e39262
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
"""Manage API keys for external partner access to the URAAS dashboard API
(e.g. Africa PID Alliance / DOCiD reading papers/analytics endpoints
server-to-server, instead of using the browser session-cookie login).

The plaintext key is shown exactly once, at creation — it is never stored;
only its SHA-256 hash is, so a stolen database dump can't be used to
authenticate as a partner. If a key is lost, revoke it and create a new one.

Usage:
    python scripts/manage_api_keys.py create --name "Africa PID Alliance / DOCiD"
    python scripts/manage_api_keys.py list
    python scripts/manage_api_keys.py revoke --prefix uraas_live_AbCd
"""

import argparse
import hashlib
import os
import secrets
import sys
from datetime import datetime

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from uraas.database import ApiKey, SessionLocal

_PREFIX = "uraas_live_"


def _hash(key: str) -> str:
    return hashlib.sha256(key.encode("utf-8")).hexdigest()


def cmd_create(args):
    raw = _PREFIX + secrets.token_urlsafe(32)
    session = SessionLocal()
    try:
        row = ApiKey(
            name=args.name,
            key_hash=_hash(raw),
            key_prefix=raw[: len(_PREFIX) + 8],
            scope="read",
            created_by=args.created_by or "cli",
        )
        session.add(row)
        session.commit()
        print("Created API key — this is the ONLY time the full value is shown:\n")
        print(f"    {raw}\n")
        print(f"Name:   {row.name}")
        print(f"Prefix: {row.key_prefix}  (safe to log/reference later)")
        print(f"Scope:  {row.scope}")
        print("\nGive the partner this header format:")
        print(f'    X-API-Key: {raw}')
    finally:
        session.close()


def cmd_list(args):
    session = SessionLocal()
    try:
        rows = session.query(ApiKey).order_by(ApiKey.created_at.desc()).all()
        if not rows:
            print("No API keys yet.")
            return
        for r in rows:
            status = "REVOKED" if r.revoked else "active"
            last_used = r.last_used_at.isoformat() if r.last_used_at else "never"
            print(f"[{r.id}] {r.key_prefix}...  {r.name!r}  scope={r.scope}  "
                  f"status={status}  created={r.created_at}  last_used={last_used}")
    finally:
        session.close()


def cmd_revoke(args):
    session = SessionLocal()
    try:
        q = session.query(ApiKey)
        row = None
        if args.id:
            row = q.filter(ApiKey.id == args.id).first()
        elif args.prefix:
            row = q.filter(ApiKey.key_prefix == args.prefix).first()
        if not row:
            print("No matching key found.")
            return
        row.revoked = True
        row.revoked_at = datetime.utcnow()
        session.commit()
        print(f"Revoked key [{row.id}] {row.key_prefix}... ({row.name!r})")
    finally:
        session.close()


def main():
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    sub = parser.add_subparsers(dest="command", required=True)

    p_create = sub.add_parser("create", help="Issue a new partner API key")
    p_create.add_argument("--name", required=True, help="Partner label, e.g. 'Africa PID Alliance / DOCiD'")
    p_create.add_argument("--created-by", default=None)
    p_create.set_defaults(func=cmd_create)

    p_list = sub.add_parser("list", help="List all keys (never shows the full value)")
    p_list.set_defaults(func=cmd_list)

    p_revoke = sub.add_parser("revoke", help="Revoke a key")
    p_revoke.add_argument("--id", type=int, default=None)
    p_revoke.add_argument("--prefix", default=None)
    p_revoke.set_defaults(func=cmd_revoke)

    args = parser.parse_args()
    if args.command == "revoke" and not (args.id or args.prefix):
        parser.error("revoke requires --id or --prefix")
    args.func(args)


if __name__ == "__main__":
    main()