mirror of
https://github.com/safing/portbase
synced 2025-09-01 01:59:48 +00:00
363 lines
9.7 KiB
Go
363 lines
9.7 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/tevino/abool"
|
|
|
|
"github.com/safing/portbase/modules"
|
|
|
|
"github.com/safing/portbase/log"
|
|
"github.com/safing/portbase/rng"
|
|
)
|
|
|
|
// Permission defines an API requests permission.
|
|
type Permission int8
|
|
|
|
const (
|
|
// NotFound declares that the operation does not exist.
|
|
NotFound Permission = -2
|
|
|
|
// Require declares that the operation requires permission to be processed,
|
|
// but anyone can execute the operation.
|
|
Require Permission = -1
|
|
|
|
// NotSupported declares that the operation is not supported.
|
|
NotSupported Permission = 0
|
|
|
|
// PermitAnyone declares that anyone can execute the operation without any
|
|
// authentication.
|
|
PermitAnyone Permission = 1
|
|
|
|
// PermitUser declares that the operation may be executed by authenticated
|
|
// third party applications that are categorized as representing a simple
|
|
// user and is limited in access.
|
|
PermitUser Permission = 2
|
|
|
|
// PermitAdmin declares that the operation may be executed by authenticated
|
|
// third party applications that are categorized as representing an
|
|
// administrator and has broad in access.
|
|
PermitAdmin Permission = 3
|
|
|
|
// PermitSelf declares that the operation may only be executed by the
|
|
// software itself and its own (first party) components.
|
|
PermitSelf Permission = 4
|
|
)
|
|
|
|
// AuthenticatorFunc is a function that can be set as the authenticator for the
|
|
// API endpoint. If none is set, all requests will have full access.
|
|
// The returned AuthToken represents the permissions that the request has.
|
|
type AuthenticatorFunc func(r *http.Request, s *http.Server) (*AuthToken, error)
|
|
|
|
// AuthToken represents either a set of required or granted permissions.
|
|
// All attributes must be set when the struct is built and must not be changed
|
|
// later. Functions may be called at any time.
|
|
// The Write permission implicitly also includes reading.
|
|
type AuthToken struct {
|
|
Read Permission
|
|
Write Permission
|
|
|
|
validUntil time.Time
|
|
validLock sync.Mutex
|
|
}
|
|
|
|
// Expired returns whether the token has expired.
|
|
func (token *AuthToken) Expired() bool {
|
|
token.validLock.Lock()
|
|
defer token.validLock.Unlock()
|
|
|
|
return time.Now().After(token.validUntil)
|
|
}
|
|
|
|
// Refresh refreshes the validity of the token with the given TTL.
|
|
func (token *AuthToken) Refresh(ttl time.Duration) {
|
|
token.validLock.Lock()
|
|
defer token.validLock.Unlock()
|
|
|
|
token.validUntil = time.Now().Add(ttl)
|
|
}
|
|
|
|
// AuthenticatedHandler defines the handler interface to specify custom
|
|
// permission for an API handler. The returned permission is the required
|
|
// permission for the request to proceed.
|
|
type AuthenticatedHandler interface {
|
|
ReadPermission(*http.Request) Permission
|
|
WritePermission(*http.Request) Permission
|
|
}
|
|
|
|
const (
|
|
cookieName = "Portmaster-API-Token"
|
|
cookieTTL = 5 * time.Minute
|
|
)
|
|
|
|
var (
|
|
authFnSet = abool.New()
|
|
authFn AuthenticatorFunc
|
|
|
|
authTokens = make(map[string]*AuthToken)
|
|
authTokensLock sync.Mutex
|
|
|
|
// ErrAPIAccessDeniedMessage should be wrapped by errors returned by
|
|
// AuthenticatorFunc in order to signify a blocked request, including a error
|
|
// message for the user. This is an empty message on purpose, as to allow the
|
|
// function to define the full text of the error shown to the user.
|
|
ErrAPIAccessDeniedMessage = errors.New("")
|
|
)
|
|
|
|
// SetAuthenticator sets an authenticator function for the API endpoint. If none is set, all requests will be permitted.
|
|
func SetAuthenticator(fn AuthenticatorFunc) error {
|
|
if module.Online() {
|
|
return ErrAuthenticationImmutable
|
|
}
|
|
|
|
if !authFnSet.SetToIf(false, true) {
|
|
return ErrAuthenticationAlreadySet
|
|
}
|
|
|
|
authFn = fn
|
|
return nil
|
|
}
|
|
|
|
func authMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
token := authenticateRequest(w, r, next)
|
|
if token != nil {
|
|
if _, apiRequest := getAPIContext(r); apiRequest != nil {
|
|
apiRequest.AuthToken = token
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
}
|
|
})
|
|
}
|
|
|
|
func authenticateRequest(w http.ResponseWriter, r *http.Request, targetHandler http.Handler) *AuthToken {
|
|
tracer := log.Tracer(r.Context())
|
|
|
|
// Check if authenticator is set.
|
|
if !authFnSet.IsSet() {
|
|
// Return highest available permissions for the request.
|
|
return &AuthToken{
|
|
Read: PermitSelf,
|
|
Write: PermitSelf,
|
|
}
|
|
}
|
|
|
|
// Check if request is read only.
|
|
readRequest := isReadMethod(r.Method)
|
|
|
|
// Get required permission for target handler.
|
|
requiredPermission := PermitSelf
|
|
if authdHandler, ok := targetHandler.(AuthenticatedHandler); ok {
|
|
if readRequest {
|
|
requiredPermission = authdHandler.ReadPermission(r)
|
|
} else {
|
|
requiredPermission = authdHandler.WritePermission(r)
|
|
}
|
|
}
|
|
|
|
// Check if we need to do any authentication at all.
|
|
switch requiredPermission {
|
|
case NotFound:
|
|
// Not found.
|
|
tracer.Trace("api: authenticated handler reported: not found")
|
|
http.Error(w, "Not found.", http.StatusNotFound)
|
|
return nil
|
|
case NotSupported:
|
|
// A read or write permission can be marked as not supported.
|
|
tracer.Trace("api: authenticated handler reported: not supported")
|
|
http.Error(w, "Method not allowed.", http.StatusMethodNotAllowed)
|
|
return nil
|
|
case PermitAnyone:
|
|
// Don't process permissions, as we don't need them.
|
|
tracer.Tracef("api: granted %s access to public handler", r.RemoteAddr)
|
|
return &AuthToken{
|
|
Read: PermitAnyone,
|
|
Write: PermitAnyone,
|
|
}
|
|
case Require:
|
|
// Continue processing permissions, but treat as PermitAnyone.
|
|
requiredPermission = PermitAnyone
|
|
}
|
|
|
|
// The required permission must match the request permission values after
|
|
// handling the specials.
|
|
if requiredPermission < PermitAnyone || requiredPermission > PermitSelf {
|
|
tracer.Warningf(
|
|
"api: handler returned invalid permission: %s (%d)",
|
|
requiredPermission,
|
|
requiredPermission,
|
|
)
|
|
http.Error(w, "Internal server error during authentication.", http.StatusInternalServerError)
|
|
return nil
|
|
}
|
|
|
|
// Check for an existing auth token.
|
|
token := checkAuthToken(r)
|
|
|
|
// Get auth token from authenticator if none was in the request.
|
|
if token == nil {
|
|
var err error
|
|
token, err = authFn(r, server)
|
|
if err != nil {
|
|
// Check for internal error.
|
|
if !errors.Is(err, ErrAPIAccessDeniedMessage) {
|
|
tracer.Errorf("api: authenticator failed: %s", err)
|
|
http.Error(w, "Internal server error during authentication.", http.StatusInternalServerError)
|
|
return nil
|
|
}
|
|
|
|
// If authentication failed and we require authentication, return an
|
|
// authentication error.
|
|
if requiredPermission != PermitAnyone {
|
|
// Return authentication error.
|
|
tracer.Warningf("api: denying api access to %s", r.RemoteAddr)
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
return nil
|
|
}
|
|
|
|
token = &AuthToken{
|
|
Read: PermitAnyone,
|
|
Write: PermitAnyone,
|
|
}
|
|
}
|
|
|
|
// Apply auth token to request.
|
|
err = applyAuthToken(w, token)
|
|
if err != nil {
|
|
tracer.Warningf("api: failed to create auth token: %s", err)
|
|
}
|
|
}
|
|
|
|
// Get effective permission for request.
|
|
var requestPermission Permission
|
|
if readRequest {
|
|
requestPermission = token.Read
|
|
} else {
|
|
requestPermission = token.Write
|
|
}
|
|
|
|
// Check for valid request permission.
|
|
if requestPermission < PermitAnyone || requestPermission > PermitSelf {
|
|
tracer.Warningf(
|
|
"api: authenticator returned invalid permission: %s (%d)",
|
|
requestPermission,
|
|
requestPermission,
|
|
)
|
|
http.Error(w, "Internal server error during authentication.", http.StatusInternalServerError)
|
|
return nil
|
|
}
|
|
|
|
// Check permission.
|
|
if requestPermission < requiredPermission {
|
|
http.Error(w, "Insufficient permissions.", http.StatusForbidden)
|
|
return nil
|
|
}
|
|
|
|
tracer.Tracef("api: granted %s access to authenticated handler", r.RemoteAddr)
|
|
|
|
// Make a copy of the AuthToken in order mitigate the handler poisoning the
|
|
// token, as changes would apply to future requests.
|
|
return &AuthToken{
|
|
Read: token.Read,
|
|
Write: token.Write,
|
|
}
|
|
}
|
|
|
|
func checkAuthToken(r *http.Request) *AuthToken {
|
|
// Get auth token from request.
|
|
c, err := r.Cookie(cookieName)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
// Check if auth token is registered.
|
|
authTokensLock.Lock()
|
|
token, ok := authTokens[c.Value]
|
|
authTokensLock.Unlock()
|
|
if !ok {
|
|
log.Tracer(r.Context()).Tracef("api: provided auth token %s is unknown", c.Value)
|
|
return nil
|
|
}
|
|
|
|
// Check if token is still valid.
|
|
if token.Expired() {
|
|
log.Tracer(r.Context()).Tracef("api: provided auth token %s has expired", c.Value)
|
|
return nil
|
|
}
|
|
|
|
// Refresh token and return.
|
|
token.Refresh(cookieTTL)
|
|
log.Tracer(r.Context()).Tracef("api: auth token %s is valid, refreshing", c.Value)
|
|
return token
|
|
}
|
|
|
|
func applyAuthToken(w http.ResponseWriter, token *AuthToken) error {
|
|
// Generate new token secret.
|
|
secret, err := rng.Bytes(32) // 256 bit
|
|
if err != nil {
|
|
return err
|
|
}
|
|
secretHex := base64.RawURLEncoding.EncodeToString(secret)
|
|
|
|
// Set token cookie in response.
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: cookieName,
|
|
Value: secretHex,
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
SameSite: http.SameSiteStrictMode,
|
|
})
|
|
|
|
// Set token TTL.
|
|
token.Refresh(cookieTTL)
|
|
|
|
// Save token.
|
|
authTokensLock.Lock()
|
|
defer authTokensLock.Unlock()
|
|
authTokens[secretHex] = token
|
|
|
|
return nil
|
|
}
|
|
|
|
func cleanAuthTokens(_ context.Context, _ *modules.Task) error {
|
|
authTokensLock.Lock()
|
|
defer authTokensLock.Unlock()
|
|
|
|
for secret, token := range authTokens {
|
|
if token.Expired() {
|
|
delete(authTokens, secret)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func isReadMethod(method string) bool {
|
|
return method == http.MethodGet || method == http.MethodHead
|
|
}
|
|
|
|
func (p Permission) String() string {
|
|
switch p {
|
|
case NotSupported:
|
|
return "NotSupported"
|
|
case Require:
|
|
return "Require"
|
|
case PermitAnyone:
|
|
return "PermitAnyone"
|
|
case PermitUser:
|
|
return "PermitUser"
|
|
case PermitAdmin:
|
|
return "PermitAdmin"
|
|
case PermitSelf:
|
|
return "PermitSelf"
|
|
case NotFound:
|
|
return "NotFound"
|
|
default:
|
|
return "Unknown"
|
|
}
|
|
}
|