basyx commited on
Commit
878ab1e
·
verified ·
1 Parent(s): 849bb2d

Apply PR #7 security changes directly to main: app/security/cli.py

Browse files
Files changed (1) hide show
  1. app/security/cli.py +67 -0
app/security/cli.py CHANGED
@@ -3,12 +3,16 @@ from __future__ import annotations
3
  import argparse
4
  import asyncio
5
  import json
 
 
 
6
 
7
  from app.core.config import get_settings
8
  from app.security.database import SecurityDatabase
9
  from app.security.schemas import APIKeyCreate, APIKeyView
10
  from app.security.service import APIKeyService
11
  from app.security.tenancy import TenantService
 
12
 
13
 
14
  async def _create(arguments: argparse.Namespace) -> None:
@@ -33,6 +37,58 @@ async def _create(arguments: argparse.Namespace) -> None:
33
  await database.close()
34
 
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  def _bootstrap(arguments: argparse.Namespace) -> None:
37
  material = APIKeyService.generate_material(arguments.environment)
38
  print("Store the API key in your password manager. It will not be shown again.\n")
@@ -49,6 +105,15 @@ def main() -> None:
49
  "generate-bootstrap", help="Generate a key and hash-only bootstrap settings"
50
  )
51
  bootstrap.add_argument("--environment", choices=("live", "test"), default="live")
 
 
 
 
 
 
 
 
 
52
  create = commands.add_parser("create", help="Create a key directly in the configured database")
53
  create.add_argument("--name", required=True)
54
  create.add_argument("--environment", choices=("live", "test"), default="live")
@@ -57,6 +122,8 @@ def main() -> None:
57
  arguments = parser.parse_args()
58
  if arguments.command == "generate-bootstrap":
59
  _bootstrap(arguments)
 
 
60
  else:
61
  asyncio.run(_create(arguments))
62
 
 
3
  import argparse
4
  import asyncio
5
  import json
6
+ from uuid import uuid4
7
+
8
+ from sqlalchemy import select
9
 
10
  from app.core.config import get_settings
11
  from app.security.database import SecurityDatabase
12
  from app.security.schemas import APIKeyCreate, APIKeyView
13
  from app.security.service import APIKeyService
14
  from app.security.tenancy import TenantService
15
+ from app.security.models import User, Workspace, WorkspaceMembership
16
 
17
 
18
  async def _create(arguments: argparse.Namespace) -> None:
 
37
  await database.close()
38
 
39
 
40
+
41
+ async def _provision_studio_user(arguments: argparse.Namespace) -> None:
42
+ """Explicit operator-driven onboarding; never grant access from a browser request."""
43
+ settings = get_settings()
44
+ database = SecurityDatabase(settings.database_url, auto_migrate=settings.security_auto_migrate)
45
+ await database.initialize()
46
+ try:
47
+ async with database.session() as session:
48
+ existing = await session.scalar(select(User).where(User.subject == arguments.subject))
49
+ if existing is not None:
50
+ raise SystemExit("That identity subject is already provisioned; refusing to reassign it.")
51
+ workspace = await session.scalar(
52
+ select(Workspace).where(Workspace.slug == arguments.workspace_slug)
53
+ )
54
+ if workspace is not None:
55
+ raise SystemExit("Workspace slug already exists; refusing to attach an initial owner implicitly.")
56
+ user_id = str(uuid4())
57
+ workspace_id = str(uuid4())
58
+ user = User(
59
+ id=user_id,
60
+ subject=arguments.subject,
61
+ display_name=arguments.display_name,
62
+ metadata_json={"provisioned_by": "security-cli"},
63
+ )
64
+ workspace = Workspace(
65
+ id=workspace_id,
66
+ slug=arguments.workspace_slug,
67
+ name=arguments.workspace_name,
68
+ metadata_json={"provisioned_by": "security-cli"},
69
+ )
70
+ membership = WorkspaceMembership(
71
+ id=str(uuid4()),
72
+ workspace_id=workspace_id,
73
+ user_id=user_id,
74
+ role=arguments.role,
75
+ status="active",
76
+ )
77
+ session.add_all([user, workspace, membership])
78
+ await session.commit()
79
+ print(json.dumps({
80
+ "user_id": user_id,
81
+ "subject": arguments.subject,
82
+ "workspace_id": workspace_id,
83
+ "workspace_slug": arguments.workspace_slug,
84
+ "membership_id": membership.id,
85
+ "role": arguments.role,
86
+ "status": "active",
87
+ }, indent=2))
88
+ finally:
89
+ await database.close()
90
+
91
+
92
  def _bootstrap(arguments: argparse.Namespace) -> None:
93
  material = APIKeyService.generate_material(arguments.environment)
94
  print("Store the API key in your password manager. It will not be shown again.\n")
 
105
  "generate-bootstrap", help="Generate a key and hash-only bootstrap settings"
106
  )
107
  bootstrap.add_argument("--environment", choices=("live", "test"), default="live")
108
+ provision = commands.add_parser(
109
+ "provision-studio-user",
110
+ help="Explicitly provision a verified Supabase subject and initial workspace membership",
111
+ )
112
+ provision.add_argument("--subject", required=True, help="Exact Supabase auth user UUID/subject")
113
+ provision.add_argument("--display-name", required=True)
114
+ provision.add_argument("--workspace-slug", required=True)
115
+ provision.add_argument("--workspace-name", required=True)
116
+ provision.add_argument("--role", choices=("owner", "admin", "member"), default="owner")
117
  create = commands.add_parser("create", help="Create a key directly in the configured database")
118
  create.add_argument("--name", required=True)
119
  create.add_argument("--environment", choices=("live", "test"), default="live")
 
122
  arguments = parser.parse_args()
123
  if arguments.command == "generate-bootstrap":
124
  _bootstrap(arguments)
125
+ elif arguments.command == "provision-studio-user":
126
+ asyncio.run(_provision_studio_user(arguments))
127
  else:
128
  asyncio.run(_create(arguments))
129