- Parse toml configuration file, see https://github.com/qdm12/gluetun-wiki/blob/main/setup/advanced/control-server.md#authentication - Retro-compatible with existing AND documented routes, until after v3.41 release - Log a warning if an unprotected-by-default route is accessed unprotected - Authentication methods: none, apikey, basic - `genkey` command to generate API keys - move log middleware to internal/server/middlewares/log Co-authored-by: Joe Jose <45399349+joejose97@users.noreply.github.com>
112 lines
3.6 KiB
Go
112 lines
3.6 KiB
Go
package auth
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
)
|
|
|
|
func New(settings Settings, debugLogger DebugLogger) (
|
|
middleware func(http.Handler) http.Handler,
|
|
err error) {
|
|
routeToRoles, err := settingsToLookupMap(settings)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting settings to lookup maps: %w", err)
|
|
}
|
|
|
|
//nolint:goconst
|
|
return func(handler http.Handler) http.Handler {
|
|
return &authHandler{
|
|
childHandler: handler,
|
|
routeToRoles: routeToRoles,
|
|
unprotectedRoutes: map[string]struct{}{
|
|
http.MethodGet + " /openvpn/actions/restart": {},
|
|
http.MethodGet + " /unbound/actions/restart": {},
|
|
http.MethodGet + " /updater/restart": {},
|
|
http.MethodGet + " /v1/version": {},
|
|
http.MethodGet + " /v1/vpn/status": {},
|
|
http.MethodPut + " /v1/vpn/status": {},
|
|
// GET /v1/vpn/settings is protected by default
|
|
// PUT /v1/vpn/settings is protected by default
|
|
http.MethodGet + " /v1/openvpn/status": {},
|
|
http.MethodPut + " /v1/openvpn/status": {},
|
|
http.MethodGet + " /v1/openvpn/portforwarded": {},
|
|
// GET /v1/openvpn/settings is protected by default
|
|
http.MethodGet + " /v1/dns/status": {},
|
|
http.MethodPut + " /v1/dns/status": {},
|
|
http.MethodGet + " /v1/updater/status": {},
|
|
http.MethodPut + " /v1/updater/status": {},
|
|
http.MethodGet + " /v1/publicip/ip": {},
|
|
},
|
|
logger: debugLogger,
|
|
}
|
|
}, nil
|
|
}
|
|
|
|
type authHandler struct {
|
|
childHandler http.Handler
|
|
routeToRoles map[string][]internalRole
|
|
unprotectedRoutes map[string]struct{} // TODO v3.41.0 remove
|
|
logger DebugLogger
|
|
}
|
|
|
|
func (h *authHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
|
route := request.Method + " " + request.URL.Path
|
|
roles := h.routeToRoles[route]
|
|
if len(roles) == 0 {
|
|
h.logger.Debugf("no authentication role defined for route %s", route)
|
|
http.Error(writer, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
responseHeader := make(http.Header, 0)
|
|
for _, role := range roles {
|
|
if !role.checker.isAuthorized(responseHeader, request) {
|
|
continue
|
|
}
|
|
|
|
h.warnIfUnprotectedByDefault(role, route) // TODO v3.41.0 remove
|
|
|
|
h.logger.Debugf("access to route %s authorized for role %s", route, role.name)
|
|
h.childHandler.ServeHTTP(writer, request)
|
|
return
|
|
}
|
|
|
|
// Flush out response headers if all roles failed to authenticate
|
|
for headerKey, headerValues := range responseHeader {
|
|
for _, headerValue := range headerValues {
|
|
writer.Header().Add(headerKey, headerValue)
|
|
}
|
|
}
|
|
|
|
allRoleNames := make([]string, len(roles))
|
|
for i, role := range roles {
|
|
allRoleNames[i] = role.name
|
|
}
|
|
h.logger.Debugf("access to route %s unauthorized after checking for roles %s",
|
|
route, andStrings(allRoleNames))
|
|
http.Error(writer, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
|
}
|
|
|
|
func (h *authHandler) warnIfUnprotectedByDefault(role internalRole, route string) {
|
|
// TODO v3.41.0 remove
|
|
if role.name != "public" {
|
|
// custom role name, allow none authentication to be specified
|
|
return
|
|
}
|
|
_, isNoneChecker := role.checker.(*noneMethod)
|
|
if !isNoneChecker {
|
|
// not the none authentication method
|
|
return
|
|
}
|
|
_, isUnprotectedByDefault := h.unprotectedRoutes[route]
|
|
if !isUnprotectedByDefault {
|
|
// route is not unprotected by default, so this is a user decision
|
|
return
|
|
}
|
|
h.logger.Warnf("route %s is unprotected by default, "+
|
|
"please set up authentication following the documentation at "+
|
|
"https://github.com/qdm12/gluetun-wiki/setup/advanced/control-server.md#authentication "+
|
|
"since this will become no longer publicly accessible after release v3.40.",
|
|
route)
|
|
}
|