feat: API key edit, protected mode, system terminal and files#128
Merged
Conversation
Creating an API key while authenticated via a token that does not carry a user identity (such as a legacy admin JWT) failed with a 401 that the frontend interpreted as a session expiry and logged the user out. The handler now falls back to the first admin user when the actor is authenticated as admin but has no attached user record, and refuses with 403 (not 401) for non-admin paths without a user. This stops the spurious sign-out and keeps the legitimate auth failure paths intact. The API key response also stops shipping the Go zero timestamp for keys that never expire or have never been used. Those fields are now emitted as null, so clients can render them correctly instead of treating a key created today as already expired in the year 1.
API keys are now editable after creation through a new PUT endpoint that mirrors the same authorisation rules as create. Name, description, role, permissions, deployment scope, and expiry are all modifiable without revoking and recreating the key. A non-admin actor cannot elevate a key to admin or grant permissions it does not itself hold. Each entry in a key's deployment scope now carries an explicit access level (read, write, or admin) rather than being a plain membership flag. The auth middleware treats the entry as a cap on the user's level, so a key scoped to "deployments:write" cannot perform admin actions even if the owning user has admin access to that deployment. Existing keys keep working. The wire format accepts both the new object shape and the legacy array form: array entries are interpreted as admin level for backward compatibility, and serialized rows in the database with the old shape are upgraded transparently on read. Closes #123
Protected mode lets an admin lock a deployment so destructive actions (env edits, compose changes, redeploys, file deletes, quick actions) are refused with a 423 Locked response while the mode is on. The admin can further deny shell sessions outright and ship a list of command patterns that are blocked inside the terminal and quick actions. Each refusal carries the rule that triggered it. A host-level terminal endpoint backs the new System Terminal page in the UI. Commands flow over a WebSocket, are authorised by a new system:write permission, and pass through the same command-pattern filter as the deployment terminal so global rules apply uniformly. A new system:files permission and a parallel set of endpoints under /api/system/files expose a filesystem manager rooted at a configurable path. Operators can list, read, write, mkdir, touch, delete, and chmod files outside any deployment, with traversal protection and a setuid/setgid/sticky guard on the mode. Deployment file management gains the same chmod and file-create endpoints so both contexts share the same model. Closes #117 Closes #122
Code Review SummaryThis PR introduces significant security and administrative features, including editable API keys with granular deployment access, a 'Protected Mode' for deployments to prevent accidental destructive actions, and system-wide file and terminal management. The core architectural change is moving from simple list-based deployment access to a level-based map. 🚀 Key Improvements
💡 Minor Suggestions
🚨 Critical Issues
|
Comment on lines
+133
to
+167
| } | ||
|
|
||
| command := parts[0] | ||
| args := parts[1:] | ||
| if command == "ll" { | ||
| command = "ls" | ||
| args = append([]string{"-la"}, args...) | ||
| raw = strings.Join(append([]string{command}, args...), " ") | ||
| } | ||
| if command == "la" { | ||
| command = "ls" | ||
| args = append([]string{"-A"}, args...) | ||
| raw = strings.Join(append([]string{command}, args...), " ") | ||
| } | ||
|
|
||
| if command == "cd" { | ||
| target := session.cwd | ||
| if len(args) > 0 { | ||
| target = resolveSystemTerminalPath(session.cwd, args[0]) | ||
| } | ||
| info, err := os.Stat(target) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| if !info.IsDir() { | ||
| return "", fmt.Errorf("not a directory: %s", target) | ||
| } | ||
| session.cwd = target | ||
| return "", nil | ||
| } | ||
|
|
||
| ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | ||
| defer cancel() | ||
|
|
||
| cmd := exec.CommandContext(ctx, "sh", "-lc", raw) |
There was a problem hiding this comment.
The system terminal executes commands using sh -lc. If the input raw is not properly sanitized, an attacker might be able to inject commands despite the protectedCommandBlocked check (e.g., using command substitution or redirection).
Suggested change
| } | |
| command := parts[0] | |
| args := parts[1:] | |
| if command == "ll" { | |
| command = "ls" | |
| args = append([]string{"-la"}, args...) | |
| raw = strings.Join(append([]string{command}, args...), " ") | |
| } | |
| if command == "la" { | |
| command = "ls" | |
| args = append([]string{"-A"}, args...) | |
| raw = strings.Join(append([]string{command}, args...), " ") | |
| } | |
| if command == "cd" { | |
| target := session.cwd | |
| if len(args) > 0 { | |
| target = resolveSystemTerminalPath(session.cwd, args[0]) | |
| } | |
| info, err := os.Stat(target) | |
| if err != nil { | |
| return "", err | |
| } | |
| if !info.IsDir() { | |
| return "", fmt.Errorf("not a directory: %s", target) | |
| } | |
| session.cwd = target | |
| return "", nil | |
| } | |
| ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | |
| defer cancel() | |
| cmd := exec.CommandContext(ctx, "sh", "-lc", raw) | |
| // Consider using a more restricted shell or ensuring 'raw' does not contain malicious shell meta-characters | |
| cmd := exec.CommandContext(ctx, "sh", "-lc", "--", raw) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #117
Closes #122
Closes #123