geronimo.cli.keys_cmd

CLI commands for API key management.

  1"""CLI commands for API key management."""
  2
  3import typer
  4from rich.table import Table
  5
  6from geronimo.serving.auth.keys import APIKeyManager
  7from geronimo.cli.utils import console, success, error, warning, dim
  8
  9keys_app = typer.Typer(
 10    name="keys",
 11    help="Manage Service to Service API keys for endpoint authentication",
 12    no_args_is_help=True,
 13)
 14
 15
 16@keys_app.command("create")
 17def create_key(
 18    name: str = typer.Option(..., "--name", "-n", help="Name for the API key"),
 19    scopes: str = typer.Option(
 20        "predict",
 21        "--scopes",
 22        "-s",
 23        help="Comma-separated list of scopes",
 24    ),
 25    keys_file: str = typer.Option(
 26        ".geronimo/keys.json",
 27        "--keys-file",
 28        "-f",
 29        help="Path to keys file",
 30    ),
 31) -> None:
 32    """Create a new API key.
 33
 34    The raw key is only displayed once - save it securely!
 35    """
 36    manager = APIKeyManager(keys_file)
 37    scope_list = [s.strip() for s in scopes.split(",")]
 38
 39    raw_key, api_key = manager.create_key(name=name, scopes=scope_list)
 40
 41    console.print("\n[bold green]✓ API key created successfully![/bold green]\n")
 42    console.print(f"  Name: [cyan]{api_key.name}[/cyan]")
 43    console.print(f"  ID: [dim]{api_key.key_id}[/dim]")
 44    console.print(f"  Scopes: [yellow]{', '.join(api_key.scopes)}[/yellow]")
 45    console.print()
 46    console.print("[bold yellow]⚠ Save this key - it won't be shown again:[/bold yellow]")
 47    console.print(f"\n  [bold]{raw_key}[/bold]\n")
 48
 49
 50@keys_app.command("list")
 51def list_keys(
 52    keys_file: str = typer.Option(
 53        ".geronimo/keys.json",
 54        "--keys-file",
 55        "-f",
 56        help="Path to keys file",
 57    ),
 58) -> None:
 59    """List all API keys."""
 60    manager = APIKeyManager(keys_file)
 61    keys = manager.list_keys()
 62
 63    if not keys:
 64        dim("No API keys found.")
 65        return
 66
 67    table = Table(title="API Keys")
 68    table.add_column("ID", style="dim")
 69    table.add_column("Name", style="cyan")
 70    table.add_column("Scopes", style="yellow")
 71    table.add_column("Created", style="dim")
 72    table.add_column("Status")
 73
 74    for key in keys:
 75        status = "[green]active[/green]" if key.enabled else "[red]revoked[/red]"
 76        if key.expires_at:
 77            status += f" [dim](expires {key.expires_at.date()})[/dim]"
 78
 79        table.add_row(
 80            key.key_id,
 81            key.name,
 82            ", ".join(key.scopes),
 83            key.created_at.strftime("%Y-%m-%d"),
 84            status,
 85        )
 86
 87    console.print(table)
 88
 89
 90@keys_app.command("revoke")
 91def revoke_key(
 92    key_id: str = typer.Argument(..., help="ID of the key to revoke"),
 93    keys_file: str = typer.Option(
 94        ".geronimo/keys.json",
 95        "--keys-file",
 96        "-f",
 97        help="Path to keys file",
 98    ),
 99) -> None:
100    """Revoke an API key (disable but keep record)."""
101    manager = APIKeyManager(keys_file)
102
103    if manager.revoke(key_id):
104        success(f"Key {key_id} revoked")
105    else:
106        error(f"Key {key_id} not found", exit_code=1)
107
108
109@keys_app.command("delete")
110def delete_key(
111    key_id: str = typer.Argument(..., help="ID of the key to delete"),
112    keys_file: str = typer.Option(
113        ".geronimo/keys.json",
114        "--keys-file",
115        "-f",
116        help="Path to keys file",
117    ),
118    force: bool = typer.Option(
119        False,
120        "--force",
121        "-y",  # Changed from -f to avoid conflict with --keys-file
122        help="Skip confirmation",
123    ),
124) -> None:
125    """Permanently delete an API key."""
126    manager = APIKeyManager(keys_file)
127
128    key = manager.get_key(key_id)
129    if not key:
130        error(f"Key {key_id} not found", exit_code=1)
131
132    if not force:
133        confirm = typer.confirm(f"Permanently delete key '{key.name}' ({key_id})?")
134        if not confirm:
135            raise typer.Abort()
136
137    manager.delete(key_id)
138    success(f"Key {key_id} deleted")
139
140
141@keys_app.command("sync")
142def sync_keys(
143    keys_file: str = typer.Option(
144        ".geronimo/keys.json",
145        "--keys-file",
146        "-f",
147        help="Path to keys file",
148    ),
149    key_ids: str = typer.Option(
150        None,
151        "--key-ids",
152        "-k",
153        help="Comma-separated key IDs to sync (default: all)",
154    ),
155    interactive: bool = typer.Option(
156        False,
157        "--interactive",
158        "-i",
159        help="Interactively select keys to sync",
160    ),
161) -> None:
162    """Sync local API keys to Geronimo Cloud.
163    
164    Uploads your local API keys to Geronimo Cloud so they can be used
165    for authenticating requests to cloud-deployed endpoints.
166    
167    Cloud-managed keys (created via dashboard) take precedence and
168    won't be overwritten by synced keys.
169    """
170    from geronimo.deploy_cloud.client import GeronimoCloudClient
171    
172    manager = APIKeyManager(keys_file)
173    all_keys = manager.list_keys()
174    
175    if not all_keys:
176        dim("No local API keys found.")
177        return
178    
179    # Filter keys based on options
180    keys_to_sync = all_keys
181    
182    if key_ids:
183        # Filter to specified key IDs
184        requested_ids = {k.strip() for k in key_ids.split(",")}
185        keys_to_sync = [k for k in all_keys if k.key_id in requested_ids]
186        
187        # Warn about missing keys
188        found_ids = {k.key_id for k in keys_to_sync}
189        missing_ids = requested_ids - found_ids
190        if missing_ids:
191            warning(f"Keys not found: {', '.join(missing_ids)}")
192        
193        if not keys_to_sync:
194            error("No matching keys found", exit_code=1)
195    
196    elif interactive:
197        # Interactive selection
198        console.print("\n[bold]Select keys to sync:[/bold]\n")
199        keys_to_sync = []
200        
201        for key in all_keys:
202            status = "[green]active[/green]" if key.enabled else "[red]revoked[/red]"
203            console.print(f"  [dim]{key.key_id}[/dim] - [cyan]{key.name}[/cyan] ({status})")
204            
205            if typer.confirm("    Sync this key?", default=True):
206                keys_to_sync.append(key)
207        
208        console.print()
209        
210        if not keys_to_sync:
211            dim("No keys selected.")
212            return
213    
214    # Convert to dicts for API
215    keys_data = [key.to_dict() for key in keys_to_sync]
216    
217    # Sync to cloud
218    try:
219        client = GeronimoCloudClient()
220        result = client.sync_keys(keys_data)
221        
222        synced = result.get("synced", 0)
223        skipped = result.get("skipped", 0)
224        
225        console.print(f"\n[bold green]✓ Keys synced to Geronimo Cloud[/bold green]")
226        console.print(f"  Synced: [green]{synced}[/green]")
227        if skipped:
228            console.print(f"  Skipped: [yellow]{skipped}[/yellow] (cloud-managed keys take precedence)")
229        console.print()
230        
231    except RuntimeError as e:
232        error(str(e), exit_code=1)
233    except Exception as e:
234        error(f"Failed to sync keys: {e}", exit_code=1)
keys_app = <typer.main.Typer object>
@keys_app.command('create')
def create_key( name: str = <typer.models.OptionInfo object>, scopes: str = <typer.models.OptionInfo object>, keys_file: str = <typer.models.OptionInfo object>) -> None:
17@keys_app.command("create")
18def create_key(
19    name: str = typer.Option(..., "--name", "-n", help="Name for the API key"),
20    scopes: str = typer.Option(
21        "predict",
22        "--scopes",
23        "-s",
24        help="Comma-separated list of scopes",
25    ),
26    keys_file: str = typer.Option(
27        ".geronimo/keys.json",
28        "--keys-file",
29        "-f",
30        help="Path to keys file",
31    ),
32) -> None:
33    """Create a new API key.
34
35    The raw key is only displayed once - save it securely!
36    """
37    manager = APIKeyManager(keys_file)
38    scope_list = [s.strip() for s in scopes.split(",")]
39
40    raw_key, api_key = manager.create_key(name=name, scopes=scope_list)
41
42    console.print("\n[bold green]✓ API key created successfully![/bold green]\n")
43    console.print(f"  Name: [cyan]{api_key.name}[/cyan]")
44    console.print(f"  ID: [dim]{api_key.key_id}[/dim]")
45    console.print(f"  Scopes: [yellow]{', '.join(api_key.scopes)}[/yellow]")
46    console.print()
47    console.print("[bold yellow]⚠ Save this key - it won't be shown again:[/bold yellow]")
48    console.print(f"\n  [bold]{raw_key}[/bold]\n")

Create a new API key.

The raw key is only displayed once - save it securely!

@keys_app.command('list')
def list_keys(keys_file: str = <typer.models.OptionInfo object>) -> None:
51@keys_app.command("list")
52def list_keys(
53    keys_file: str = typer.Option(
54        ".geronimo/keys.json",
55        "--keys-file",
56        "-f",
57        help="Path to keys file",
58    ),
59) -> None:
60    """List all API keys."""
61    manager = APIKeyManager(keys_file)
62    keys = manager.list_keys()
63
64    if not keys:
65        dim("No API keys found.")
66        return
67
68    table = Table(title="API Keys")
69    table.add_column("ID", style="dim")
70    table.add_column("Name", style="cyan")
71    table.add_column("Scopes", style="yellow")
72    table.add_column("Created", style="dim")
73    table.add_column("Status")
74
75    for key in keys:
76        status = "[green]active[/green]" if key.enabled else "[red]revoked[/red]"
77        if key.expires_at:
78            status += f" [dim](expires {key.expires_at.date()})[/dim]"
79
80        table.add_row(
81            key.key_id,
82            key.name,
83            ", ".join(key.scopes),
84            key.created_at.strftime("%Y-%m-%d"),
85            status,
86        )
87
88    console.print(table)

List all API keys.

@keys_app.command('revoke')
def revoke_key( key_id: str = <typer.models.ArgumentInfo object>, keys_file: str = <typer.models.OptionInfo object>) -> None:
 91@keys_app.command("revoke")
 92def revoke_key(
 93    key_id: str = typer.Argument(..., help="ID of the key to revoke"),
 94    keys_file: str = typer.Option(
 95        ".geronimo/keys.json",
 96        "--keys-file",
 97        "-f",
 98        help="Path to keys file",
 99    ),
100) -> None:
101    """Revoke an API key (disable but keep record)."""
102    manager = APIKeyManager(keys_file)
103
104    if manager.revoke(key_id):
105        success(f"Key {key_id} revoked")
106    else:
107        error(f"Key {key_id} not found", exit_code=1)

Revoke an API key (disable but keep record).

@keys_app.command('delete')
def delete_key( key_id: str = <typer.models.ArgumentInfo object>, keys_file: str = <typer.models.OptionInfo object>, force: bool = <typer.models.OptionInfo object>) -> None:
110@keys_app.command("delete")
111def delete_key(
112    key_id: str = typer.Argument(..., help="ID of the key to delete"),
113    keys_file: str = typer.Option(
114        ".geronimo/keys.json",
115        "--keys-file",
116        "-f",
117        help="Path to keys file",
118    ),
119    force: bool = typer.Option(
120        False,
121        "--force",
122        "-y",  # Changed from -f to avoid conflict with --keys-file
123        help="Skip confirmation",
124    ),
125) -> None:
126    """Permanently delete an API key."""
127    manager = APIKeyManager(keys_file)
128
129    key = manager.get_key(key_id)
130    if not key:
131        error(f"Key {key_id} not found", exit_code=1)
132
133    if not force:
134        confirm = typer.confirm(f"Permanently delete key '{key.name}' ({key_id})?")
135        if not confirm:
136            raise typer.Abort()
137
138    manager.delete(key_id)
139    success(f"Key {key_id} deleted")

Permanently delete an API key.

@keys_app.command('sync')
def sync_keys( keys_file: str = <typer.models.OptionInfo object>, key_ids: str = <typer.models.OptionInfo object>, interactive: bool = <typer.models.OptionInfo object>) -> None:
142@keys_app.command("sync")
143def sync_keys(
144    keys_file: str = typer.Option(
145        ".geronimo/keys.json",
146        "--keys-file",
147        "-f",
148        help="Path to keys file",
149    ),
150    key_ids: str = typer.Option(
151        None,
152        "--key-ids",
153        "-k",
154        help="Comma-separated key IDs to sync (default: all)",
155    ),
156    interactive: bool = typer.Option(
157        False,
158        "--interactive",
159        "-i",
160        help="Interactively select keys to sync",
161    ),
162) -> None:
163    """Sync local API keys to Geronimo Cloud.
164    
165    Uploads your local API keys to Geronimo Cloud so they can be used
166    for authenticating requests to cloud-deployed endpoints.
167    
168    Cloud-managed keys (created via dashboard) take precedence and
169    won't be overwritten by synced keys.
170    """
171    from geronimo.deploy_cloud.client import GeronimoCloudClient
172    
173    manager = APIKeyManager(keys_file)
174    all_keys = manager.list_keys()
175    
176    if not all_keys:
177        dim("No local API keys found.")
178        return
179    
180    # Filter keys based on options
181    keys_to_sync = all_keys
182    
183    if key_ids:
184        # Filter to specified key IDs
185        requested_ids = {k.strip() for k in key_ids.split(",")}
186        keys_to_sync = [k for k in all_keys if k.key_id in requested_ids]
187        
188        # Warn about missing keys
189        found_ids = {k.key_id for k in keys_to_sync}
190        missing_ids = requested_ids - found_ids
191        if missing_ids:
192            warning(f"Keys not found: {', '.join(missing_ids)}")
193        
194        if not keys_to_sync:
195            error("No matching keys found", exit_code=1)
196    
197    elif interactive:
198        # Interactive selection
199        console.print("\n[bold]Select keys to sync:[/bold]\n")
200        keys_to_sync = []
201        
202        for key in all_keys:
203            status = "[green]active[/green]" if key.enabled else "[red]revoked[/red]"
204            console.print(f"  [dim]{key.key_id}[/dim] - [cyan]{key.name}[/cyan] ({status})")
205            
206            if typer.confirm("    Sync this key?", default=True):
207                keys_to_sync.append(key)
208        
209        console.print()
210        
211        if not keys_to_sync:
212            dim("No keys selected.")
213            return
214    
215    # Convert to dicts for API
216    keys_data = [key.to_dict() for key in keys_to_sync]
217    
218    # Sync to cloud
219    try:
220        client = GeronimoCloudClient()
221        result = client.sync_keys(keys_data)
222        
223        synced = result.get("synced", 0)
224        skipped = result.get("skipped", 0)
225        
226        console.print(f"\n[bold green]✓ Keys synced to Geronimo Cloud[/bold green]")
227        console.print(f"  Synced: [green]{synced}[/green]")
228        if skipped:
229            console.print(f"  Skipped: [yellow]{skipped}[/yellow] (cloud-managed keys take precedence)")
230        console.print()
231        
232    except RuntimeError as e:
233        error(str(e), exit_code=1)
234    except Exception as e:
235        error(f"Failed to sync keys: {e}", exit_code=1)

Sync local API keys to Geronimo Cloud.

Uploads your local API keys to Geronimo Cloud so they can be used for authenticating requests to cloud-deployed endpoints.

Cloud-managed keys (created via dashboard) take precedence and won't be overwritten by synced keys.