- 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>
132 lines
4.0 KiB
Go
132 lines
4.0 KiB
Go
package auth
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/qdm12/gosettings"
|
|
"github.com/qdm12/gosettings/validate"
|
|
)
|
|
|
|
type Settings struct {
|
|
// Roles is a list of roles with their associated authentication
|
|
// and routes.
|
|
Roles []Role
|
|
}
|
|
|
|
func (s *Settings) SetDefaults() {
|
|
s.Roles = gosettings.DefaultSlice(s.Roles, []Role{{ // TODO v3.41.0 leave empty
|
|
Name: "public",
|
|
Auth: "none",
|
|
Routes: []string{
|
|
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",
|
|
http.MethodGet + " /v1/openvpn/status",
|
|
http.MethodPut + " /v1/openvpn/status",
|
|
http.MethodGet + " /v1/openvpn/portforwarded",
|
|
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",
|
|
},
|
|
}})
|
|
}
|
|
|
|
func (s Settings) Validate() (err error) {
|
|
for i, role := range s.Roles {
|
|
err = role.validate()
|
|
if err != nil {
|
|
return fmt.Errorf("role %s (%d of %d): %w",
|
|
role.Name, i+1, len(s.Roles), err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
AuthNone = "none"
|
|
AuthAPIKey = "apikey"
|
|
AuthBasic = "basic"
|
|
)
|
|
|
|
// Role contains the role name, authentication method name and
|
|
// routes that the role can access.
|
|
type Role struct {
|
|
// Name is the role name and is only used for documentation
|
|
// and in the authentication middleware debug logs.
|
|
Name string
|
|
// Auth is the authentication method to use, which can be 'none' or 'apikey'.
|
|
Auth string
|
|
// APIKey is the API key to use when using the 'apikey' authentication.
|
|
APIKey string
|
|
// Username for HTTP Basic authentication method.
|
|
Username string
|
|
// Password for HTTP Basic authentication method.
|
|
Password string
|
|
// Routes is a list of routes that the role can access in the format
|
|
// "HTTP_METHOD PATH", for example "GET /v1/vpn/status"
|
|
Routes []string
|
|
}
|
|
|
|
var (
|
|
ErrMethodNotSupported = errors.New("authentication method not supported")
|
|
ErrAPIKeyEmpty = errors.New("api key is empty")
|
|
ErrBasicUsernameEmpty = errors.New("username is empty")
|
|
ErrBasicPasswordEmpty = errors.New("password is empty")
|
|
ErrRouteNotSupported = errors.New("route not supported by the control server")
|
|
)
|
|
|
|
func (r Role) validate() (err error) {
|
|
err = validate.IsOneOf(r.Auth, AuthNone, AuthAPIKey, AuthBasic)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %s", ErrMethodNotSupported, r.Auth)
|
|
}
|
|
|
|
switch {
|
|
case r.Auth == AuthAPIKey && r.APIKey == "":
|
|
return fmt.Errorf("for role %s: %w", r.Name, ErrAPIKeyEmpty)
|
|
case r.Auth == AuthBasic && r.Username == "":
|
|
return fmt.Errorf("for role %s: %w", r.Name, ErrBasicUsernameEmpty)
|
|
case r.Auth == AuthBasic && r.Password == "":
|
|
return fmt.Errorf("for role %s: %w", r.Name, ErrBasicPasswordEmpty)
|
|
}
|
|
|
|
for i, route := range r.Routes {
|
|
_, ok := validRoutes[route]
|
|
if !ok {
|
|
return fmt.Errorf("route %d of %d: %w: %s",
|
|
i+1, len(r.Routes), ErrRouteNotSupported, route)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// WARNING: do not mutate programmatically.
|
|
var validRoutes = map[string]struct{}{ //nolint:gochecknoglobals
|
|
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": {},
|
|
http.MethodGet + " /v1/vpn/settings": {},
|
|
http.MethodPut + " /v1/vpn/settings": {},
|
|
http.MethodGet + " /v1/openvpn/status": {},
|
|
http.MethodPut + " /v1/openvpn/status": {},
|
|
http.MethodGet + " /v1/openvpn/portforwarded": {},
|
|
http.MethodGet + " /v1/openvpn/settings": {},
|
|
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": {},
|
|
}
|