rasa2 commited on
Commit
7d5e8cb
·
1 Parent(s): 970a5a0

Add UI TOKEN handling

Browse files
Files changed (1) hide show
  1. app.py +43 -198
app.py CHANGED
@@ -9,22 +9,15 @@ from fastapi.responses import HTMLResponse, RedirectResponse
9
  from pydantic import BaseModel
10
 
11
 
12
- # ---------------------------------------------------------------------
13
- # Configuration
14
- # ---------------------------------------------------------------------
15
-
16
  BROKER_TOKEN = os.environ.get("BROKER_TOKEN")
 
17
 
18
  if not BROKER_TOKEN:
19
- raise RuntimeError(
20
- "Missing BROKER_TOKEN. Add it in Hugging Face Space "
21
- "Settings → Variables and secrets → New secret."
22
- )
23
 
 
 
24
 
25
- # ---------------------------------------------------------------------
26
- # Data model
27
- # ---------------------------------------------------------------------
28
 
29
  class JobStatus(str, Enum):
30
  queued = "queued"
@@ -40,20 +33,11 @@ class Job(BaseModel):
40
  result: Optional[str] = None
41
 
42
 
43
- # ---------------------------------------------------------------------
44
- # FastAPI app
45
- # ---------------------------------------------------------------------
46
-
47
  app = FastAPI(title="Fury Broker")
48
 
49
- # In-memory job storage.
50
- # Note: jobs disappear if the Space restarts.
51
  jobs: dict[str, Job] = {}
52
 
53
 
54
- # Only commands listed here can be executed by Fury.
55
- # The browser/Space user submits the logical command name.
56
- # The Fury worker receives the shell command.
57
  ALLOWED_COMMANDS = {
58
  "hostname": "hostname",
59
  "whoami": "whoami",
@@ -64,31 +48,15 @@ ALLOWED_COMMANDS = {
64
  }
65
 
66
 
67
- # ---------------------------------------------------------------------
68
- # Security
69
- # ---------------------------------------------------------------------
70
-
71
  def verify_broker_token(x_broker_token: Optional[str]) -> None:
72
- """
73
- Broker-level authentication.
74
-
75
- Important:
76
- - Hugging Face private Spaces use:
77
- Authorization: Bearer HF_TOKEN
78
-
79
- - This app uses:
80
- X-Broker-Token: BROKER_TOKEN
81
-
82
- This avoids conflict between Hugging Face authentication and
83
- the app's own authentication.
84
- """
85
  if x_broker_token != BROKER_TOKEN:
86
  raise HTTPException(status_code=401, detail="Unauthorized")
87
 
88
 
89
- # ---------------------------------------------------------------------
90
- # Health and diagnostics
91
- # ---------------------------------------------------------------------
 
92
 
93
  @app.get("/health")
94
  def health():
@@ -99,41 +67,17 @@ def health():
99
  }
100
 
101
 
102
- @app.get("/api/commands")
103
- def list_commands(x_broker_token: Optional[str] = Header(default=None)):
104
- verify_broker_token(x_broker_token)
105
-
106
- return {
107
- "commands": [
108
- {
109
- "name": name,
110
- "shell_command": shell_command,
111
- }
112
- for name, shell_command in ALLOWED_COMMANDS.items()
113
- ]
114
- }
115
-
116
-
117
- # ---------------------------------------------------------------------
118
- # Browser UI
119
- # ---------------------------------------------------------------------
120
-
121
  @app.get("/", response_class=HTMLResponse)
122
  def home():
123
  rows = ""
124
 
125
  for job in reversed(list(jobs.values())):
126
- safe_id = escape(job.id)
127
- safe_command = escape(job.command)
128
- safe_status = escape(job.status.value)
129
- safe_result = escape(job.result or "")
130
-
131
  rows += f"""
132
  <tr>
133
- <td><code>{safe_id}</code></td>
134
- <td>{safe_command}</td>
135
- <td>{safe_status}</td>
136
- <td><pre>{safe_result}</pre></td>
137
  </tr>
138
  """
139
 
@@ -154,13 +98,6 @@ def home():
154
  max-width: 1200px;
155
  line-height: 1.45;
156
  }}
157
- h1 {{
158
- margin-bottom: 0.2rem;
159
- }}
160
- .subtitle {{
161
- color: #555;
162
- margin-bottom: 2rem;
163
- }}
164
  form {{
165
  margin: 1.5rem 0;
166
  padding: 1rem;
@@ -168,13 +105,9 @@ def home():
168
  border-radius: 8px;
169
  background: #fafafa;
170
  }}
171
- select {{
172
  padding: 6px;
173
- min-width: 260px;
174
- }}
175
- button {{
176
- padding: 6px 12px;
177
- cursor: pointer;
178
  }}
179
  table {{
180
  border-collapse: collapse;
@@ -197,37 +130,34 @@ def home():
197
  overflow: auto;
198
  margin: 0;
199
  }}
200
- code {{
201
- font-size: 0.9em;
202
- }}
203
- .note {{
204
- color: #666;
205
- font-size: 0.95rem;
206
- }}
207
  </style>
208
  </head>
209
  <body>
210
  <h1>Fury Broker</h1>
211
- <div class="subtitle">
212
- Hugging Face Space broker for running approved commands on Fury.
213
- </div>
214
 
215
  <p>
216
- This Space does not SSH into Fury. Instead, the Fury worker polls this Space
217
- for queued jobs, executes only predefined commands, and posts the result back.
 
218
  </p>
219
 
220
  <form method="post" action="/submit">
221
- <label for="command"><strong>Command:</strong></label>
222
- <select id="command" name="command">
223
- {options}
224
- </select>
225
- <button type="submit">Submit job</button>
 
 
 
 
 
 
 
226
  </form>
227
 
228
- <p class="note">
229
- Refresh the page after submitting a job. The worker should pick it up within
230
- a few seconds if it is running on Fury.
231
  </p>
232
 
233
  <h2>Jobs</h2>
@@ -247,14 +177,12 @@ def home():
247
 
248
 
249
  @app.post("/submit")
250
- def submit_from_ui(command: str = Form(...)):
251
- """
252
- Browser form endpoint.
 
 
253
 
254
- This endpoint is intentionally not protected by X-Broker-Token because
255
- the Space itself is private. If you later make the Space public, protect
256
- this endpoint too.
257
- """
258
  if command not in ALLOWED_COMMANDS:
259
  raise HTTPException(status_code=400, detail="Command is not allowed")
260
 
@@ -269,26 +197,11 @@ def submit_from_ui(command: str = Form(...)):
269
  return RedirectResponse("/", status_code=303)
270
 
271
 
272
- # ---------------------------------------------------------------------
273
- # API: submit job
274
- # ---------------------------------------------------------------------
275
-
276
  @app.post("/api/jobs")
277
  def submit_job(
278
  command: str = Form(...),
279
  x_broker_token: Optional[str] = Header(default=None),
280
  ):
281
- """
282
- Submit a job via API.
283
-
284
- Example:
285
-
286
- curl -X POST \\
287
- -H "Authorization: Bearer $HF_TOKEN" \\
288
- -H "X-Broker-Token: $BROKER_TOKEN" \\
289
- -F "command=hostname" \\
290
- https://rasa2-fury.hf.space/api/jobs
291
- """
292
  verify_broker_token(x_broker_token)
293
 
294
  if command not in ALLOWED_COMMANDS:
@@ -307,45 +220,8 @@ def submit_job(
307
  return job
308
 
309
 
310
- # Also allow query-param style:
311
- # POST /api/jobs/query?command=hostname
312
- @app.post("/api/jobs/query")
313
- def submit_job_query(
314
- command: str,
315
- x_broker_token: Optional[str] = Header(default=None),
316
- ):
317
- verify_broker_token(x_broker_token)
318
-
319
- if command not in ALLOWED_COMMANDS:
320
- raise HTTPException(status_code=400, detail="Command is not allowed")
321
-
322
- job_id = str(uuid.uuid4())
323
-
324
- job = Job(
325
- id=job_id,
326
- command=command,
327
- status=JobStatus.queued,
328
- )
329
-
330
- jobs[job_id] = job
331
-
332
- return job
333
-
334
-
335
- # ---------------------------------------------------------------------
336
- # API: worker polling
337
- # ---------------------------------------------------------------------
338
-
339
  @app.get("/api/next-job")
340
  def get_next_job(x_broker_token: Optional[str] = Header(default=None)):
341
- """
342
- Called by the Fury worker.
343
-
344
- The worker asks:
345
- "Do you have a job for me?"
346
-
347
- If a queued job exists, this endpoint marks it as running and returns it.
348
- """
349
  verify_broker_token(x_broker_token)
350
 
351
  for job in jobs.values():
@@ -361,10 +237,6 @@ def get_next_job(x_broker_token: Optional[str] = Header(default=None)):
361
  return {"id": None}
362
 
363
 
364
- # ---------------------------------------------------------------------
365
- # API: worker posts result
366
- # ---------------------------------------------------------------------
367
-
368
  @app.post("/api/jobs/{job_id}/result")
369
  def post_result(
370
  job_id: str,
@@ -372,9 +244,6 @@ def post_result(
372
  success: bool = Form(...),
373
  x_broker_token: Optional[str] = Header(default=None),
374
  ):
375
- """
376
- Called by the Fury worker after it executes a command.
377
- """
378
  verify_broker_token(x_broker_token)
379
 
380
  if job_id not in jobs:
@@ -387,43 +256,19 @@ def post_result(
387
  return job
388
 
389
 
390
- # ---------------------------------------------------------------------
391
- # API: inspect jobs
392
- # ---------------------------------------------------------------------
393
-
394
  @app.get("/api/jobs")
395
  def list_jobs(x_broker_token: Optional[str] = Header(default=None)):
396
  verify_broker_token(x_broker_token)
397
-
398
- return {
399
- "jobs": list(jobs.values())
400
- }
401
 
402
 
403
- @app.get("/api/jobs/{job_id}")
404
- def get_job(
405
- job_id: str,
406
- x_broker_token: Optional[str] = Header(default=None),
407
- ):
408
- verify_broker_token(x_broker_token)
409
-
410
- if job_id not in jobs:
411
- raise HTTPException(status_code=404, detail="Job not found")
412
-
413
- return jobs[job_id]
414
-
415
-
416
- # ---------------------------------------------------------------------
417
- # API: clear jobs
418
- # ---------------------------------------------------------------------
419
-
420
- @app.post("/api/clear")
421
- def clear_jobs(x_broker_token: Optional[str] = Header(default=None)):
422
  verify_broker_token(x_broker_token)
423
 
424
- jobs.clear()
425
-
426
  return {
427
- "status": "cleared",
428
- "jobs_count": len(jobs),
 
 
429
  }
 
9
  from pydantic import BaseModel
10
 
11
 
 
 
 
 
12
  BROKER_TOKEN = os.environ.get("BROKER_TOKEN")
13
+ UI_TOKEN = os.environ.get("UI_TOKEN")
14
 
15
  if not BROKER_TOKEN:
16
+ raise RuntimeError("Missing BROKER_TOKEN")
 
 
 
17
 
18
+ if not UI_TOKEN:
19
+ raise RuntimeError("Missing UI_TOKEN")
20
 
 
 
 
21
 
22
  class JobStatus(str, Enum):
23
  queued = "queued"
 
33
  result: Optional[str] = None
34
 
35
 
 
 
 
 
36
  app = FastAPI(title="Fury Broker")
37
 
 
 
38
  jobs: dict[str, Job] = {}
39
 
40
 
 
 
 
41
  ALLOWED_COMMANDS = {
42
  "hostname": "hostname",
43
  "whoami": "whoami",
 
48
  }
49
 
50
 
 
 
 
 
51
  def verify_broker_token(x_broker_token: Optional[str]) -> None:
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  if x_broker_token != BROKER_TOKEN:
53
  raise HTTPException(status_code=401, detail="Unauthorized")
54
 
55
 
56
+ def verify_ui_token(ui_token: Optional[str]) -> None:
57
+ if ui_token != UI_TOKEN:
58
+ raise HTTPException(status_code=401, detail="Invalid UI token")
59
+
60
 
61
  @app.get("/health")
62
  def health():
 
67
  }
68
 
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  @app.get("/", response_class=HTMLResponse)
71
  def home():
72
  rows = ""
73
 
74
  for job in reversed(list(jobs.values())):
 
 
 
 
 
75
  rows += f"""
76
  <tr>
77
+ <td><code>{escape(job.id)}</code></td>
78
+ <td>{escape(job.command)}</td>
79
+ <td>{escape(job.status.value)}</td>
80
+ <td><pre>{escape(job.result or "")}</pre></td>
81
  </tr>
82
  """
83
 
 
98
  max-width: 1200px;
99
  line-height: 1.45;
100
  }}
 
 
 
 
 
 
 
101
  form {{
102
  margin: 1.5rem 0;
103
  padding: 1rem;
 
105
  border-radius: 8px;
106
  background: #fafafa;
107
  }}
108
+ input, select, button {{
109
  padding: 6px;
110
+ margin: 4px;
 
 
 
 
111
  }}
112
  table {{
113
  border-collapse: collapse;
 
130
  overflow: auto;
131
  margin: 0;
132
  }}
 
 
 
 
 
 
 
133
  </style>
134
  </head>
135
  <body>
136
  <h1>Fury Broker</h1>
 
 
 
137
 
138
  <p>
139
+ Submit an approved command from Hugging Face Spaces.
140
+ The worker running on Fury will pick it up, execute it on Fury,
141
+ and return the result here.
142
  </p>
143
 
144
  <form method="post" action="/submit">
145
+ <div>
146
+ <label><strong>UI token:</strong></label>
147
+ <input type="password" name="ui_token" placeholder="Enter UI_TOKEN" required>
148
+ </div>
149
+
150
+ <div>
151
+ <label><strong>Command:</strong></label>
152
+ <select name="command">
153
+ {options}
154
+ </select>
155
+ <button type="submit">Submit job</button>
156
+ </div>
157
  </form>
158
 
159
+ <p>
160
+ Refresh the page after a few seconds to see updated results.
 
161
  </p>
162
 
163
  <h2>Jobs</h2>
 
177
 
178
 
179
  @app.post("/submit")
180
+ def submit_from_ui(
181
+ command: str = Form(...),
182
+ ui_token: str = Form(...),
183
+ ):
184
+ verify_ui_token(ui_token)
185
 
 
 
 
 
186
  if command not in ALLOWED_COMMANDS:
187
  raise HTTPException(status_code=400, detail="Command is not allowed")
188
 
 
197
  return RedirectResponse("/", status_code=303)
198
 
199
 
 
 
 
 
200
  @app.post("/api/jobs")
201
  def submit_job(
202
  command: str = Form(...),
203
  x_broker_token: Optional[str] = Header(default=None),
204
  ):
 
 
 
 
 
 
 
 
 
 
 
205
  verify_broker_token(x_broker_token)
206
 
207
  if command not in ALLOWED_COMMANDS:
 
220
  return job
221
 
222
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  @app.get("/api/next-job")
224
  def get_next_job(x_broker_token: Optional[str] = Header(default=None)):
 
 
 
 
 
 
 
 
225
  verify_broker_token(x_broker_token)
226
 
227
  for job in jobs.values():
 
237
  return {"id": None}
238
 
239
 
 
 
 
 
240
  @app.post("/api/jobs/{job_id}/result")
241
  def post_result(
242
  job_id: str,
 
244
  success: bool = Form(...),
245
  x_broker_token: Optional[str] = Header(default=None),
246
  ):
 
 
 
247
  verify_broker_token(x_broker_token)
248
 
249
  if job_id not in jobs:
 
256
  return job
257
 
258
 
 
 
 
 
259
  @app.get("/api/jobs")
260
  def list_jobs(x_broker_token: Optional[str] = Header(default=None)):
261
  verify_broker_token(x_broker_token)
262
+ return {"jobs": list(jobs.values())}
 
 
 
263
 
264
 
265
+ @app.get("/api/commands")
266
+ def list_commands(x_broker_token: Optional[str] = Header(default=None)):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  verify_broker_token(x_broker_token)
268
 
 
 
269
  return {
270
+ "commands": [
271
+ {"name": name, "shell_command": cmd}
272
+ for name, cmd in ALLOWED_COMMANDS.items()
273
+ ]
274
  }