package main import ( "net/http" "strings" "inou/lib" ) // ============================================================================ // RBAC Authentication/Authorization Helpers // ============================================================================ // These functions extract AccessContext from requests and provide middleware // for enforcing RBAC at the API boundary. // ============================================================================ // getAccessContext extracts AccessContext from the request. // Checks in order: // 1. Bearer token in Authorization header // 2. token query parameter // 3. dossier query parameter (deprecated, for backward compatibility) // // Returns nil if no valid context can be established. func getAccessContext(r *http.Request) *lib.AccessContext { var accessorID string // 1. Check Authorization header for Bearer token auth := r.Header.Get("Authorization") if strings.HasPrefix(auth, "Bearer ") { tokenStr := strings.TrimPrefix(auth, "Bearer ") // Try OAuth access token first (encrypted token from lib.TokenCreate) if parsed, err := lib.TokenParse(tokenStr); err == nil { accessorID = parsed.DossierID } else if d := lib.DossierGetBySessionToken(tokenStr); d != nil { // Fallback to session token lookup accessorID = d.DossierID } } // 2. Check token query parameter if accessorID == "" { if token := r.URL.Query().Get("token"); token != "" { // Token is a dossier_id directly for MCP bridge accessorID = token } } // 3. Fallback: dossier parameter (deprecated) if accessorID == "" { if dossier := r.URL.Query().Get("dossier"); dossier != "" { accessorID = dossier } } if accessorID == "" { return nil } return &lib.AccessContext{ AccessorID: accessorID, IsSystem: false, } } // getAccessContextOrFail extracts AccessContext and returns 401 if not found. // Returns nil and writes error response if no valid context. func getAccessContextOrFail(w http.ResponseWriter, r *http.Request) *lib.AccessContext { ctx := getAccessContext(r) if ctx == nil { http.Error(w, "Unauthorized: no valid authentication", http.StatusUnauthorized) return nil } return ctx } // requireDossierAccess checks if the accessor can read the specified dossier. // Returns true if allowed, false and writes 403 if denied. func requireDossierAccess(w http.ResponseWriter, ctx *lib.AccessContext, dossierID string) bool { accessorID := "" if ctx != nil && !ctx.IsSystem { accessorID = ctx.AccessorID } if err := lib.CheckAccess(accessorID, dossierID, "", 'r'); err != nil { http.Error(w, "Forbidden: access denied to this dossier", http.StatusForbidden) return false } return true } // requireEntryAccess checks if the accessor can perform op on the entry. // Returns true if allowed, false and writes 403 if denied. func requireEntryAccess(w http.ResponseWriter, ctx *lib.AccessContext, dossierID, entryID string, op rune) bool { accessorID := "" if ctx != nil && !ctx.IsSystem { accessorID = ctx.AccessorID } if err := lib.CheckAccess(accessorID, dossierID, entryID, op); err != nil { http.Error(w, "Forbidden: access denied", http.StatusForbidden) return false } return true } // requireManageAccess checks if the accessor can manage permissions for a dossier. // Returns true if allowed, false and writes 403 if denied. func requireManageAccess(w http.ResponseWriter, ctx *lib.AccessContext, dossierID string) bool { accessorID := "" if ctx != nil && !ctx.IsSystem { accessorID = ctx.AccessorID } if err := lib.CheckAccess(accessorID, dossierID, "", 'm'); err != nil { http.Error(w, "Forbidden: manage permission required", http.StatusForbidden) return false } return true } // getTargetDossier extracts and validates the target dossier from the request. // Returns the dossier ID if valid and accessible, empty string otherwise. func getTargetDossier(w http.ResponseWriter, r *http.Request, ctx *lib.AccessContext) string { dossierID := r.URL.Query().Get("dossier") if dossierID == "" { http.Error(w, "dossier parameter required", http.StatusBadRequest) return "" } if !requireDossierAccess(w, ctx, dossierID) { return "" } return dossierID } // systemContextForLocalhost returns SystemContext if request is from localhost, // used for internal API calls that should bypass RBAC. func systemContextForLocalhost(r *http.Request) *lib.AccessContext { if isLocalhost(r) { return lib.SystemContext } return nil } // getAccessContextOrSystem returns SystemContext for localhost requests, // otherwise extracts user context. Returns nil and writes error if neither. func getAccessContextOrSystem(w http.ResponseWriter, r *http.Request) *lib.AccessContext { // Localhost gets system context if isLocalhost(r) { return lib.SystemContext } // Otherwise require user auth return getAccessContextOrFail(w, r) }