Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions components/backend/handlers/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,27 @@ func isValidKubernetesName(name string) bool {
return kubernetesNameRegex.MatchString(name)
}

// isValidRBACSubject validates RBAC subject identifiers (user/group names in RoleBindings)
// RBAC subjects can contain colons (e.g., system:authenticated, system:serviceaccount:ns:name)
// Unlike Kubernetes resource names, RBAC subjects follow a different format (RFC 1123 subdomain)
// Returns false if:
// - name is empty (prevents empty string injection)
// - name exceeds 253 characters (max length for DNS subdomain)
// - name contains control characters (prevents injection attacks)
func isValidRBACSubject(name string) bool {
// Max length for RBAC subjects is 253 chars (same as DNS subdomain)
if len(name) == 0 || len(name) > 253 {
return false
}
// Reject control characters and newlines for security
for _, r := range name {
if r < 32 || r == 127 {
return false
}
}
return true
}

// ContentListItem represents a content list item for file browsing
type ContentListItem struct {
Name string `json:"name"`
Expand Down
6 changes: 3 additions & 3 deletions components/backend/handlers/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,9 @@ func AddProjectPermission(c *gin.Context) {
return
}

// Validate subject name is a valid Kubernetes resource name
if !isValidKubernetesName(req.SubjectName) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid userName format. Must be a valid Kubernetes resource name."})
// Validate subject name is a valid RBAC subject identifier
if !isValidRBACSubject(req.SubjectName) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid subject name format"})
return
}

Expand Down
28 changes: 14 additions & 14 deletions components/backend/handlers/permissions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -738,17 +738,11 @@
Context("Input Validation", func() {
It("Should reject userNames with invalid characters", func() {
invalidUserNames := []string{
"user@domain.com", // @ not allowed in k8s resource names
"user/name", // / not allowed
"user\\name", // \ not allowed
"user:name", // : not allowed
"user name", // space not allowed
"user.name", // . not allowed in k8s resource names
"User-Name", // uppercase not allowed
"user-name-", // can't end with dash
"-user-name", // can't start with dash
"user..name", // consecutive dots not allowed
strings.Repeat("a", 64), // too long (>63 chars)
"user\nname", // newline not allowed (control character)
"user\tname", // tab not allowed (control character)
"user\x00name", // null byte not allowed (control character)
"user\x1fname", // control character not allowed
strings.Repeat("a", 254), // too long (>253 chars)
}

for _, userName := range invalidUserNames {
Expand All @@ -770,7 +764,7 @@

httpUtils.AssertHTTPStatus(http.StatusBadRequest)
httpUtils.AssertJSONContains(map[string]interface{}{
"error": "Invalid userName format. Must be a valid Kubernetes resource name.",
"error": "Invalid subject name format",
})

logger.Log("Correctly rejected invalid userName: %s", userName)
Expand All @@ -782,8 +776,14 @@
"user-name",
"user123",
"123user",
"a", // single character
strings.Repeat("a", 63), // exactly 63 chars (max allowed)
"a", // single character
"system:authenticated", // RBAC group for all authenticated users
"system:unauthenticated", // RBAC group for anonymous access
"system:serviceaccount:test-project:default", // service account reference
"user@domain.com", // email addresses are valid RBAC subjects
"user.name", // dots are valid in RBAC subjects
"User-Name", // uppercase is valid in RBAC subjects
strings.Repeat("a", 253), // exactly 253 chars (max allowed for RBAC subjects)
}

for _, userName := range validUserNames {
Expand Down Expand Up @@ -811,7 +811,7 @@
})

Context("Error Handling", func() {
It("Should handle Kubernetes API errors gracefully", func() {

Check notice on line 814 in components/backend/handlers/permissions_test.go

View workflow job for this annotation

GitHub Actions / Ambient Code Backend Unit Tests

It 01/29/26 05:12:26.788
// Test with a fake client that will return errors for create operations
// This would require modifying the fake client to return errors,
// which is more complex - for now we test the basic error paths
Expand All @@ -828,7 +828,7 @@
httpUtils.AssertErrorMessage("Invalid or missing token")
})

It("Should handle missing auth token", func() {

Check notice on line 831 in components/backend/handlers/permissions_test.go

View workflow job for this annotation

GitHub Actions / Ambient Code Backend Unit Tests

It 01/29/26 05:12:26.788
requestBody := map[string]interface{}{
"subjectType": "user",
"subjectName": "test-user",
Expand All @@ -849,7 +849,7 @@
})

Context("Resource Label Verification", func() {
It("Should create resources with proper ambient-code labels", func() {

Check notice on line 852 in components/backend/handlers/permissions_test.go

View workflow job for this annotation

GitHub Actions / Ambient Code Backend Unit Tests

It 01/29/26 05:12:26.788
requestBody := map[string]interface{}{
"subjectType": "user",
"subjectName": "labeled-user",
Expand Down
Loading