feat(server/auth): HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE option (JSON encoded)

- For example: `{"auth":"basic","username":"me","password":"pass"}`
- For example`{"auth":"apiKey","apikey":"xyz"}`
- For example`{"auth":"none"}` (I don't recommend)
This commit is contained in:
Quentin McGaw
2025-11-13 18:24:34 +00:00
parent f11f142bee
commit 3fac02a82a
5 changed files with 146 additions and 31 deletions

View File

@@ -1,12 +1,16 @@
package auth
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"slices"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree"
)
type Settings struct {
@@ -15,6 +19,50 @@ type Settings struct {
Roles []Role
}
// SetDefaultRole sets a default role to apply to all routes without a
// previously user-defined role assigned to. Note the role argument
// routes are ignored. This should be called BEFORE calling [Settings.SetDefaults].
func (s *Settings) SetDefaultRole(jsonRole string) error {
var role Role
decoder := json.NewDecoder(bytes.NewBufferString(jsonRole))
decoder.DisallowUnknownFields()
err := decoder.Decode(&role)
if err != nil {
return fmt.Errorf("decoding default role: %w", err)
}
if role.Auth == "" {
return nil // no default role to set
}
err = role.Validate()
if err != nil {
return fmt.Errorf("validating default role: %w", err)
}
authenticatedRoutes := make(map[string]struct{}, len(validRoutes))
for _, role := range s.Roles {
for _, route := range role.Routes {
authenticatedRoutes[route] = struct{}{}
}
}
if len(authenticatedRoutes) == len(validRoutes) {
return nil
}
unauthenticatedRoutes := make([]string, 0, len(validRoutes))
for route := range validRoutes {
_, authenticated := authenticatedRoutes[route]
if !authenticated {
unauthenticatedRoutes = append(unauthenticatedRoutes, route)
}
}
slices.Sort(unauthenticatedRoutes)
role.Routes = unauthenticatedRoutes
s.Roles = append(s.Roles, role)
return nil
}
func (s *Settings) SetDefaults() {
s.Roles = gosettings.DefaultSlice(s.Roles, []Role{{ // TODO v3.41.0 leave empty
Name: "public",
@@ -42,7 +90,7 @@ func (s *Settings) SetDefaults() {
func (s Settings) Validate() (err error) {
for i, role := range s.Roles {
err = role.validate()
err = role.Validate()
if err != nil {
return fmt.Errorf("role %s (%d of %d): %w",
role.Name, i+1, len(s.Roles), err)
@@ -63,18 +111,18 @@ const (
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
Name string `json:"name"`
// Auth is the authentication method to use, which can be 'none', 'basic' or 'apikey'.
Auth string `json:"auth"`
// APIKey is the API key to use when using the 'apikey' authentication.
APIKey string
APIKey string `json:"apikey"`
// Username for HTTP Basic authentication method.
Username string
Username string `json:"username"`
// Password for HTTP Basic authentication method.
Password string
Password string `json:"password"`
// 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
Routes []string `json:"-"`
}
var (
@@ -85,7 +133,7 @@ var (
ErrRouteNotSupported = errors.New("route not supported by the control server")
)
func (r Role) validate() (err error) {
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)
@@ -134,3 +182,20 @@ var validRoutes = map[string]struct{}{ //nolint:gochecknoglobals
http.MethodGet + " /v1/publicip/ip": {},
http.MethodGet + " /v1/portforward": {},
}
func (r Role) ToLinesNode() (node *gotree.Node) {
node = gotree.New("Role " + r.Name)
node.Appendf("Authentication method: %s", r.Auth)
switch r.Auth {
case AuthNone:
case AuthBasic:
node.Appendf("Username: %s", r.Username)
node.Appendf("Password: %s", gosettings.ObfuscateKey(r.Password))
case AuthAPIKey:
node.Appendf("API key: %s", gosettings.ObfuscateKey(r.APIKey))
default:
panic("missing code for authentication method: " + r.Auth)
}
node.Appendf("Number of routes covered: %d", len(r.Routes))
return node
}