fix(authz): Enforce per-deployment scope on container and resource actions#121
Conversation
…tions Operators with read-only access to a deployment could previously perform write actions on its containers (SSH, lifecycle, resources) and see or mutate domains, SSL, backups, scheduler tasks, security events, traffic logs, and virtual hosts belonging to deployments they had no access to. Each protected action now checks the actor's access level for the target deployment, list endpoints filter results to the actor's accessible deployments, and global proxy sync is restricted to admins. WebSocket container exec rejects the session before signaling auth success when the actor lacks write access. API-key usage on the WebSocket path now updates last-used metadata for audit parity with HTTP requests.
Code Review SummaryThis PR implements robust per-deployment authorization across the API. It ensures that operators are restricted to resources (containers, backups, logs) belonging to deployments they have explicit access to. 🚀 Key Improvements
💡 Minor Suggestions
|
| if deploymentName == "" { | ||
| c.JSON(http.StatusBadRequest, gin.H{"error": "Deployment name required"}) | ||
| return false | ||
| } |
There was a problem hiding this comment.
Invoking a new process (docker inspect) on every authorization check is extremely inefficient and will lead to poor API performance. This should be replaced with a cache or an in-memory lookup from a previously populated map, similar to how listContainerDeploymentLabels is used in the stats module.
| } | |
| func (s *Server) getContainerDeployment(containerID string) (string, error) { | |
| // TODO: Implement caching for container-to-deployment mapping | |
| cmd := exec.Command("docker", "inspect", "--format", "{{ index .Config.Labels \""+composeProjectLabel+"\" }}", containerID) | |
| output, err := cmd.Output() | |
| if err != nil { | |
| return "", err | |
| } | |
| deploymentName := strings.TrimSpace(string(output)) | |
| if deploymentName == "<no value>" { | |
| return "", nil | |
| } | |
| return deploymentName, nil | |
| } |
| return | ||
| } | ||
|
|
||
| actor := auth.GetActorFromContext(c) |
There was a problem hiding this comment.
Filtering the slice in-place using backups[:0] is efficient, but ensure that the underlying backups slice isn't used elsewhere after this function as the backing array is modified.
| actor := auth.GetActorFromContext(c) | |
| actor := auth.GetActorFromContext(c) | |
| if actor != nil && actor.Role != auth.RoleAdmin { | |
| filtered := make([]models.Backup, 0) | |
| for _, b := range backups { | |
| if actor.CanAccessDeployment(b.DeploymentName, auth.AccessLevelRead) { | |
| filtered = append(filtered, b) | |
| } | |
| } | |
| backups = filtered | |
| } |
Operators with read-only access to a deployment could previously perform write actions on its containers (SSH, lifecycle, resources) and see or mutate domains, SSL, backups, scheduler tasks, security events, traffic logs, and virtual hosts belonging to deployments they had no access to. Each protected action now checks the actor's access level for the target deployment, list endpoints filter results to the actor's accessible deployments, and global proxy sync is restricted to admins. WebSocket container exec rejects the session before signaling auth success when the actor lacks write access. API-key usage on the WebSocket path now updates last-used metadata for audit parity with HTTP requests.
Resolves #74