Skip to content

Commit 7657a19

Browse files
authored
Merge pull request #121 from flatrun/fix/issue-74-authz-scope
fix(authz): Enforce per-deployment scope on container and resource actions
2 parents 0577298 + 48b23c0 commit 7657a19

15 files changed

Lines changed: 985 additions & 41 deletions

internal/api/authz.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package api
2+
3+
import (
4+
"net/http"
5+
"os/exec"
6+
"strings"
7+
8+
"github.com/flatrun/agent/internal/auth"
9+
"github.com/gin-gonic/gin"
10+
)
11+
12+
const composeProjectLabel = "com.docker.compose.project"
13+
14+
func (s *Server) requireDeploymentAccess(c *gin.Context, deploymentName, level string) bool {
15+
if deploymentName == "" {
16+
c.JSON(http.StatusBadRequest, gin.H{"error": "Deployment name required"})
17+
return false
18+
}
19+
20+
actor := auth.GetActorFromContext(c)
21+
// Nil actor is allowed for direct handler tests; production routes set an actor via auth middleware
22+
// or explicit anonymous-admin context when auth is disabled.
23+
if actor == nil {
24+
return true
25+
}
26+
if actor.Role == auth.RoleAdmin {
27+
return true
28+
}
29+
30+
if !actor.CanAccessDeployment(deploymentName, level) {
31+
c.JSON(http.StatusForbidden, gin.H{"error": "No access to this deployment"})
32+
return false
33+
}
34+
35+
return true
36+
}
37+
38+
func (s *Server) requireContainerAccess(c *gin.Context, containerID, level string) bool {
39+
if containerID == "" {
40+
c.JSON(http.StatusBadRequest, gin.H{"error": "Container ID required"})
41+
return false
42+
}
43+
44+
actor := auth.GetActorFromContext(c)
45+
// Nil actor is allowed for direct handler tests; production routes set an actor via auth middleware
46+
// or explicit anonymous-admin context when auth is disabled.
47+
if actor == nil {
48+
return true
49+
}
50+
if actor.Role == auth.RoleAdmin {
51+
// Admins can see missing-container errors; non-admins below get a non-enumerating 403.
52+
if _, err := containerDeploymentName(containerID); err != nil {
53+
c.JSON(http.StatusNotFound, gin.H{"error": "Container not found"})
54+
return false
55+
}
56+
return true
57+
}
58+
59+
deploymentName, err := containerDeploymentName(containerID)
60+
if err != nil || deploymentName == "" {
61+
c.JSON(http.StatusForbidden, gin.H{"error": "No access to this container"})
62+
return false
63+
}
64+
65+
if !actor.CanAccessDeployment(deploymentName, level) {
66+
c.JSON(http.StatusForbidden, gin.H{"error": "No access to this container"})
67+
return false
68+
}
69+
70+
return true
71+
}
72+
73+
func (s *Server) actorCanAccessContainer(c *gin.Context, containerID, level string) bool {
74+
actor := auth.GetActorFromContext(c)
75+
if actor == nil || actor.Role == auth.RoleAdmin {
76+
return true
77+
}
78+
79+
deploymentName, err := containerDeploymentName(containerID)
80+
if err != nil || deploymentName == "" {
81+
return false
82+
}
83+
84+
return actor.CanAccessDeployment(deploymentName, level)
85+
}
86+
87+
func containerDeploymentName(containerID string) (string, error) {
88+
cmd := exec.Command("docker", "inspect", "--format", "{{ index .Config.Labels \""+composeProjectLabel+"\" }}", containerID)
89+
output, err := cmd.Output()
90+
if err != nil {
91+
return "", err
92+
}
93+
94+
deploymentName := strings.TrimSpace(string(output))
95+
if deploymentName == "<no value>" {
96+
return "", nil
97+
}
98+
99+
return deploymentName, nil
100+
}

0 commit comments

Comments
 (0)