Skip to content

Commit 6555928

Browse files
authored
Merge pull request #20 from usnavy13/dev
Dev
2 parents 038df52 + 6bc5103 commit 6555928

10 files changed

Lines changed: 273 additions & 45 deletions

File tree

Dockerfile

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ RUN apt-get update && apt-get install -y \
4141
&& rm -rf /var/lib/apt/lists/* \
4242
&& apt-get clean
4343

44-
# Create non-root user for security and add to docker group with correct GID
45-
RUN groupadd -r appuser && useradd -r -g appuser appuser && \
46-
groupadd -g 113 docker && usermod -aG docker appuser
44+
# Create non-root user with explicit UID 1000 for consistent volume permissions
45+
# The docker group GID 988 matches the host's docker group for socket access
46+
RUN groupadd -r -g 1000 appuser && useradd -r -u 1000 -g appuser appuser && \
47+
groupadd -g 988 docker && usermod -aG docker appuser
4748

4849
# Set working directory
4950
WORKDIR /app
@@ -58,9 +59,9 @@ COPY dashboard/ ./dashboard/
5859

5960
COPY .env.example .
6061

61-
# Create necessary directories
62+
# Create necessary directories with correct ownership
6263
RUN mkdir -p /app/logs /app/data /app/ssl && \
63-
chown -R appuser:appuser /app
64+
chown -R 1000:1000 /app
6465

6566
# Switch to non-root user
6667
USER appuser

dashboard/static/css/style.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,11 @@ td {
323323
color: var(--accent-red);
324324
}
325325

326+
.badge-warning {
327+
background: rgba(251, 191, 36, 0.15);
328+
color: #fbbf24;
329+
}
330+
326331
/* Buttons */
327332
.btn {
328333
padding: 0.5rem 1rem;

dashboard/static/js/app.js

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -258,10 +258,10 @@ function populateApiKeyDropdown() {
258258
select.innerHTML =
259259
'<option value="">All API Keys</option>' +
260260
state.apiKeyList
261-
.map(
262-
(k) =>
263-
`<option value="${k.key_hash}">${k.name} (${k.key_prefix}...)</option>`,
264-
)
261+
.map((k) => {
262+
const envIndicator = k.source === "environment" ? " [ENV]" : "";
263+
return `<option value="${k.key_hash}">${k.name}${envIndicator} (${k.key_prefix}...)</option>`;
264+
})
265265
.join("");
266266
}
267267

@@ -642,25 +642,20 @@ function renderKeysTable() {
642642

643643
if (state.keys.length === 0) {
644644
tbody.innerHTML =
645-
'<tr><td colspan="7" style="text-align: center; color: var(--text-muted); padding: 3rem;">No managed API keys found.</td></tr>';
645+
'<tr><td colspan="7" style="text-align: center; color: var(--text-muted); padding: 3rem;">No API keys found.</td></tr>';
646646
return;
647647
}
648648

649649
tbody.innerHTML = state.keys
650-
.map(
651-
(key) => `
652-
<tr>
653-
<td>${key.name || "Unnamed"}</td>
654-
<td><code>${key.key_prefix || "---"}...</code></td>
655-
<td>
656-
<span class="badge ${key.enabled ? "badge-success" : "badge-danger"}">
657-
${key.enabled ? "Active" : "Disabled"}
658-
</span>
659-
</td>
660-
<td>${new Date(key.created_at).toLocaleDateString()}</td>
661-
<td>${key.usage_count || 0}</td>
662-
<td>${formatRateLimits(key.rate_limits)}</td>
663-
<td class="actions-cell">
650+
.map((key) => {
651+
const isEnvKey = key.source === "environment";
652+
const statusBadge = isEnvKey
653+
? '<span class="badge badge-warning">Environment</span>'
654+
: `<span class="badge ${key.enabled ? "badge-success" : "badge-danger"}">${key.enabled ? "Active" : "Disabled"}</span>`;
655+
656+
const actionsHtml = isEnvKey
657+
? '<span class="text-muted" style="font-size: 0.75rem;">Read-only</span>'
658+
: `
664659
<button class="btn btn-sm" data-action="edit-key" data-hash="${key.key_hash}" title="Edit">
665660
<i data-lucide="edit-2" style="width: 14px;"></i>
666661
</button>
@@ -669,11 +664,19 @@ function renderKeysTable() {
669664
</button>
670665
<button class="btn btn-sm btn-danger" data-action="revoke-key" data-hash="${key.key_hash}" title="Revoke">
671666
<i data-lucide="trash-2" style="width: 14px;"></i>
672-
</button>
673-
</td>
674-
</tr>
675-
`,
676-
)
667+
</button>`;
668+
669+
return `
670+
<tr${isEnvKey ? ' style="background: rgba(251, 191, 36, 0.05);"' : ""}>
671+
<td>${key.name || "Unnamed"}</td>
672+
<td><code>${key.key_prefix || "---"}...</code></td>
673+
<td>${statusBadge}</td>
674+
<td>${new Date(key.created_at).toLocaleDateString()}</td>
675+
<td>${key.usage_count || 0}</td>
676+
<td>${formatRateLimits(key.rate_limits)}</td>
677+
<td class="actions-cell">${actionsHtml}</td>
678+
</tr>`;
679+
})
677680
.join("");
678681

679682
initLucide();

docker-compose.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ services:
3636
volumes:
3737
- /var/run/docker.sock:/var/run/docker.sock
3838
- ./logs:/app/logs
39-
- ./data:/app/data
39+
- app-data:/app/data
4040
- ${SSL_CERTS_PATH:-./ssl}:/app/ssl
4141
- ./dashboard:/app/dashboard
4242
- ./src:/app/src
@@ -139,6 +139,8 @@ volumes:
139139
driver: local
140140
minio-data:
141141
driver: local
142+
app-data:
143+
driver: local
142144

143145
networks:
144146
code-interpreter-network:

src/api/admin.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class ApiKeyResponse(BaseModel):
4747
metadata: Dict[str, str]
4848
last_used_at: Optional[datetime] = None
4949
usage_count: int
50+
source: str = "managed" # "managed" or "environment"
5051

5152

5253
# --- Dependencies ---
@@ -70,9 +71,9 @@ async def verify_master_key(x_api_key: str = Header(...)):
7071

7172
@router.get("/keys", response_model=List[ApiKeyResponse])
7273
async def list_keys(_: str = Depends(verify_master_key)):
73-
"""List all managed API keys."""
74+
"""List all API keys including environment keys (read-only)."""
7475
manager = await get_api_key_manager()
75-
records = await manager.list_keys()
76+
records = await manager.list_keys(include_env_keys=True)
7677

7778
return [
7879
ApiKeyResponse(
@@ -85,6 +86,7 @@ async def list_keys(_: str = Depends(verify_master_key)):
8586
metadata=r.metadata,
8687
last_used_at=r.last_used_at,
8788
usage_count=r.usage_count,
89+
source=r.source,
8890
)
8991
for r in records
9092
]
@@ -121,6 +123,7 @@ async def create_key(data: ApiKeyCreate, _: str = Depends(verify_master_key)):
121123
metadata=record.metadata,
122124
last_used_at=record.last_used_at,
123125
usage_count=record.usage_count,
126+
source=record.source,
124127
),
125128
}
126129

@@ -132,6 +135,14 @@ async def update_key(
132135
"""Update an API key."""
133136
manager = await get_api_key_manager()
134137

138+
# Check if this is an env key (not allowed to modify)
139+
record = await manager.get_key(key_hash)
140+
if record and record.source == "environment":
141+
raise HTTPException(
142+
status_code=403,
143+
detail="Environment keys cannot be modified. Update the API_KEY environment variable instead.",
144+
)
145+
135146
rate_limits = None
136147
if data.rate_limits:
137148
rate_limits = RateLimitsModel(
@@ -156,6 +167,15 @@ async def update_key(
156167
async def revoke_key(key_hash: str, _: str = Depends(verify_master_key)):
157168
"""Revoke an API key."""
158169
manager = await get_api_key_manager()
170+
171+
# Check if this is an env key (not allowed to revoke)
172+
record = await manager.get_key(key_hash)
173+
if record and record.source == "environment":
174+
raise HTTPException(
175+
status_code=403,
176+
detail="Environment keys cannot be revoked. Remove the API_KEY environment variable instead.",
177+
)
178+
159179
success = await manager.revoke_key(key_hash)
160180

161181
if not success:

src/api/dashboard_metrics.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ class ApiKeyFilterOption(BaseModel):
109109
name: str
110110
key_prefix: str
111111
usage_count: int
112+
source: str = "managed" # "managed" or "environment"
112113

113114

114115
# --- Endpoints ---
@@ -228,10 +229,10 @@ async def get_activity_heatmap(
228229

229230
@router.get("/api-keys", response_model=List[ApiKeyFilterOption])
230231
async def get_api_keys_for_filter(_: str = Depends(verify_master_key)):
231-
"""Get list of API keys for filter dropdown."""
232-
# Get API keys from manager (with names)
232+
"""Get list of API keys for filter dropdown (includes env keys)."""
233+
# Get API keys from manager (with names), including env keys
233234
manager = await get_api_key_manager()
234-
managed_keys = await manager.list_keys()
235+
managed_keys = await manager.list_keys(include_env_keys=True)
235236

236237
# Build lookup by key_hash
237238
key_lookup = {k.key_hash: k for k in managed_keys}
@@ -240,19 +241,36 @@ async def get_api_keys_for_filter(_: str = Depends(verify_master_key)):
240241
sqlite_keys = await sqlite_metrics_service.get_api_keys_list()
241242

242243
result = []
244+
seen_hashes = set()
245+
243246
for sk in sqlite_keys:
244247
key_hash = sk["key_hash"]
245248
managed = key_lookup.get(key_hash)
249+
seen_hashes.add(key_hash)
246250

247251
result.append(
248252
ApiKeyFilterOption(
249253
key_hash=key_hash,
250254
name=managed.name if managed else f"Key {key_hash[:8]}",
251255
key_prefix=managed.key_prefix if managed else key_hash[:12],
252256
usage_count=sk["usage_count"],
257+
source=managed.source if managed else "managed",
253258
)
254259
)
255260

261+
# Add env keys that might not have SQLite usage yet
262+
for key in managed_keys:
263+
if key.source == "environment" and key.key_hash not in seen_hashes:
264+
result.append(
265+
ApiKeyFilterOption(
266+
key_hash=key.key_hash,
267+
name=key.name,
268+
key_prefix=key.key_prefix,
269+
usage_count=key.usage_count,
270+
source="environment",
271+
)
272+
)
273+
256274
return result
257275

258276

src/middleware/security.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,11 @@ async def _authenticate_request(self, request: Request, scope: dict):
215215
scope["state"]["api_key_hash"] = result.key_hash
216216
scope["state"]["is_env_key"] = result.is_env_key
217217

218-
# Record usage for Redis-managed keys (not env var keys)
219-
if not result.is_env_key and result.key_hash:
220-
await auth_service.record_usage(result.key_hash, is_env_key=False)
218+
# Record usage for all keys (both managed and env keys)
219+
if result.key_hash:
220+
await auth_service.record_usage(
221+
result.key_hash, is_env_key=result.is_env_key
222+
)
221223

222224
def _extract_api_key(self, request: Request) -> Optional[str]:
223225
"""Extract API key from request headers."""

src/models/api_key.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ class ApiKeyRecord:
6767
metadata: Dict[str, str] = field(default_factory=dict)
6868
last_used_at: Optional[datetime] = None
6969
usage_count: int = 0
70+
source: str = (
71+
"managed" # "managed" for Redis-managed, "environment" for env var keys
72+
)
7073

7174
def to_redis_hash(self) -> Dict[str, str]:
7275
"""Convert to Redis hash format (all string values)."""
@@ -104,6 +107,7 @@ def to_redis_hash(self) -> Dict[str, str]:
104107
"metadata": json.dumps(self.metadata),
105108
"last_used_at": self.last_used_at.isoformat() if self.last_used_at else "",
106109
"usage_count": str(self.usage_count),
110+
"source": self.source,
107111
}
108112

109113
@classmethod
@@ -167,6 +171,7 @@ def from_redis_hash(cls, data: Dict[bytes, bytes]) -> "ApiKeyRecord":
167171
metadata=metadata,
168172
last_used_at=last_used_at,
169173
usage_count=int(decoded.get("usage_count", "0")),
174+
source=decoded.get("source", "managed"),
170175
)
171176

172177
def to_display_dict(self) -> Dict[str, Any]:
@@ -185,6 +190,7 @@ def to_display_dict(self) -> Dict[str, Any]:
185190
"daily": self.rate_limits.daily,
186191
"monthly": self.rate_limits.monthly,
187192
},
193+
"source": self.source,
188194
}
189195

190196

0 commit comments

Comments
 (0)