Block guest accounts from web UI login

The web UI now validates the MCIAS token after login and rejects
accounts with the guest role before setting the session cookie.
This is defense-in-depth alongside the env:restricted MCIAS tag.

The webserver.New() constructor takes a new ValidateFunc parameter
that inspects token roles post-authentication. MCIAS login does not
return roles, so this requires an extra ValidateToken round-trip at
login time (result is cached for 30s).

Security: guest role accounts are denied web UI access

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 23:02:22 -07:00
parent 3d36c58d0d
commit 9d7043a594
7 changed files with 160 additions and 18 deletions

View File

@@ -183,15 +183,35 @@ func setupTestEnv(t *testing.T) *testEnv {
if username == "admin" && password == "secret" {
return "test-token-12345", 3600, nil
}
if username == "guest" && password == "secret" {
return "test-token-guest", 3600, nil
}
if username == "user" && password == "secret" {
return "test-token-user", 3600, nil
}
return "", 0, fmt.Errorf("invalid credentials")
}
validateFn := func(token string) ([]string, error) {
switch token {
case "test-token-12345":
return []string{"admin"}, nil
case "test-token-guest":
return []string{"guest"}, nil
case "test-token-user":
return []string{"user"}, nil
default:
return nil, fmt.Errorf("invalid token")
}
}
srv, err := New(
mcrv1.NewRegistryServiceClient(conn),
mcrv1.NewPolicyServiceClient(conn),
mcrv1.NewAuditServiceClient(conn),
mcrv1.NewAdminServiceClient(conn),
loginFn,
validateFn,
csrfKey,
)
if err != nil {
@@ -543,6 +563,59 @@ func TestTruncate(t *testing.T) {
}
}
func TestLoginDeniesGuest(t *testing.T) {
env := setupTestEnv(t)
defer env.close()
// Get CSRF token.
getReq := httptest.NewRequest(http.MethodGet, "/login", nil)
getRec := httptest.NewRecorder()
env.server.Handler().ServeHTTP(getRec, getReq)
var csrfCookie *http.Cookie
for _, c := range getRec.Result().Cookies() {
if c.Name == "csrf_token" {
csrfCookie = c
break
}
}
if csrfCookie == nil {
t.Fatal("no csrf_token cookie")
}
parts := strings.SplitN(csrfCookie.Value, ".", 2)
csrfToken := parts[0]
form := url.Values{
"username": {"guest"},
"password": {"secret"},
"_csrf": {csrfToken},
}
postReq := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
postReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
postReq.AddCookie(csrfCookie)
postRec := httptest.NewRecorder()
env.server.Handler().ServeHTTP(postRec, postReq)
if postRec.Code != http.StatusOK {
t.Fatalf("POST /login as guest: status %d, want %d", postRec.Code, http.StatusOK)
}
body := postRec.Body.String()
if !strings.Contains(body, "Guest accounts are not permitted") {
t.Error("response does not contain guest denial message")
}
// Verify no session cookie was set.
for _, c := range postRec.Result().Cookies() {
if c.Name == "mcr_session" {
t.Error("session cookie should not be set for guest login")
}
}
}
func TestLoginSuccessSetsCookie(t *testing.T) {
env := setupTestEnv(t)
defer env.close()