159 lines
4.9 KiB
Go
159 lines
4.9 KiB
Go
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 := lib.SystemAccessorID
|
|
if ctx != nil && !ctx.IsSystem {
|
|
accessorID = ctx.AccessorID
|
|
}
|
|
if !lib.CheckAccess(accessorID, dossierID, dossierID, lib.PermRead) {
|
|
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 := lib.SystemAccessorID
|
|
if ctx != nil && !ctx.IsSystem {
|
|
accessorID = ctx.AccessorID
|
|
}
|
|
perm := lib.PermRead
|
|
switch op {
|
|
case 'w': perm = lib.PermWrite
|
|
case 'd': perm = lib.PermDelete
|
|
case 'm': perm = lib.PermManage
|
|
}
|
|
if !lib.CheckAccess(accessorID, dossierID, entryID, perm) {
|
|
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 := lib.SystemAccessorID
|
|
if ctx != nil && !ctx.IsSystem {
|
|
accessorID = ctx.AccessorID
|
|
}
|
|
if !lib.CheckAccess(accessorID, dossierID, dossierID, lib.PermManage) {
|
|
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)
|
|
}
|