You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
88 lines
3.0 KiB
88 lines
3.0 KiB
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"teraclone/internal/service"
|
|
)
|
|
|
|
func TestWorkspaceIdentityIsPerLoginAndNotAnAuthenticationToken(t *testing.T) {
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/api/v1/auth/logout" {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
if r.URL.Path != "/api/v1/auth/login" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
var credentials map[string]string
|
|
_ = json.NewDecoder(r.Body).Decode(&credentials)
|
|
role := "administrator"
|
|
if credentials["username"] == "operator" {
|
|
role = "port_operator"
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"session_token": "private-backend-token", "actor": "test-" + credentials["username"], "role": role})
|
|
}))
|
|
defer backend.Close()
|
|
app, err := service.NewAppService(filepath.Join(t.TempDir(), "frontend.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer app.Close()
|
|
handler, err := NewDeviceHandler(app, backend.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mux := http.NewServeMux()
|
|
handler.RegisterRoutes(mux)
|
|
identity := regexp.MustCompile(`data-workspace-id="([a-f0-9]{64})"`)
|
|
seen := make(map[string]bool)
|
|
for _, username := range []string{"admin", "operator", "operator"} {
|
|
request := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(url.Values{"username": {username}, "password": {"test"}}.Encode()))
|
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
response := httptest.NewRecorder()
|
|
mux.ServeHTTP(response, request)
|
|
if response.Code != http.StatusSeeOther || len(response.Result().Cookies()) != 1 {
|
|
t.Fatalf("login failed: %d", response.Code)
|
|
}
|
|
cookie := response.Result().Cookies()[0]
|
|
var previousID string
|
|
for _, path := range []string{"/", "/port-status", "/"} {
|
|
pageRequest := httptest.NewRequest(http.MethodGet, path, nil)
|
|
pageRequest.AddCookie(cookie)
|
|
page := httptest.NewRecorder()
|
|
mux.ServeHTTP(page, pageRequest)
|
|
body := page.Body.String()
|
|
match := identity.FindStringSubmatch(body)
|
|
if page.Code != http.StatusOK || len(match) != 2 {
|
|
t.Fatalf("missing public workspace identity: status=%d", page.Code)
|
|
}
|
|
id := match[1]
|
|
if previousID != "" && id != previousID {
|
|
t.Fatal("workspace identity changed during page navigation/refresh")
|
|
}
|
|
if id == cookie.Value || strings.Contains(body, cookie.Value) || strings.Contains(body, "private-backend-token") {
|
|
t.Fatal("authentication token leaked into workspace HTML")
|
|
}
|
|
if !strings.Contains(body, `data-current-user="`+username+`"`) || page.Header().Get("Cache-Control") != "no-store" {
|
|
t.Fatal("missing workspace user or cache prevention")
|
|
}
|
|
previousID = id
|
|
}
|
|
if seen[previousID] {
|
|
t.Fatal("workspace identity reused after a new login")
|
|
}
|
|
seen[previousID] = true
|
|
logoutRequest := httptest.NewRequest(http.MethodPost, "/logout", nil)
|
|
logoutRequest.AddCookie(cookie)
|
|
mux.ServeHTTP(httptest.NewRecorder(), logoutRequest)
|
|
}
|
|
}
|
|
|