inou/api/auth.go

168 lines
5.2 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 session cookie (for portal-proxied requests)
if accessorID == "" {
if cookie, err := r.Cookie("session"); err == nil && cookie.Value != "" {
if d := lib.DossierGetBySessionToken(cookie.Value); d != nil {
accessorID = d.DossierID
}
}
}
// 3. Check token query parameter
if accessorID == "" {
if token := r.URL.Query().Get("token"); token != "" {
// Token is a dossier_id directly (for testing/internal use)
accessorID = token
}
}
// 4. 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)
}