Files
netbird/management/internals/modules/reverseproxy/service/target_access.go
T
Maycon Santos cd22a9d4f1 [management] Configure per-target HTTP access actions (#8116)
Let HTTP targets inherit service authentication, bypass it, or block access. Validate and persist the action, expose it through the API, and deliver it to the proxy. Private services reject bypass.

Validation: service/manager, store, and management-to-proxy integration race tests passed; scoped lint passed.
2026-10-12 12:56:17 +02:00

116 lines
3.4 KiB
Go

package service
import (
"fmt"
"strings"
"unicode"
"unicode/utf8"
"github.com/netbirdio/netbird/shared/management/proto"
)
// TargetAccessAction controls authentication for a selected HTTP target.
type TargetAccessAction string
const (
TargetAccessActionInherit TargetAccessAction = "inherit"
TargetAccessActionBypass TargetAccessAction = "bypass"
TargetAccessActionBlock TargetAccessAction = "block"
)
func (t *Target) effectiveAccessAction() TargetAccessAction {
if t.AccessAction == "" {
return TargetAccessActionInherit
}
return t.AccessAction
}
func targetAccessActionToProto(action TargetAccessAction) proto.TargetAccessAction {
switch action {
case TargetAccessActionInherit:
return proto.TargetAccessAction_TARGET_ACCESS_ACTION_INHERIT
case TargetAccessActionBypass:
return proto.TargetAccessAction_TARGET_ACCESS_ACTION_BYPASS
case TargetAccessActionBlock:
return proto.TargetAccessAction_TARGET_ACCESS_ACTION_BLOCK
default:
return proto.TargetAccessAction_TARGET_ACCESS_ACTION_BLOCK
}
}
// HasTargetAccessControl reports whether any target overrides service authentication.
func (s *Service) HasTargetAccessControl() bool {
for _, target := range s.Targets {
if target != nil && target.effectiveAccessAction() != TargetAccessActionInherit {
return true
}
}
return false
}
func validateTargetAccessAction(idx int, target *Target, private bool) error {
action, err := validatedTargetAccessAction(idx, target)
if err != nil {
return err
}
if action == TargetAccessActionBypass {
if private {
return fmt.Errorf("target %d: bypass access_action is not supported for private services", idx)
}
if target.Options.AgentNetwork {
return fmt.Errorf("target %d: bypass access_action is not supported for Agent Network targets", idx)
}
}
if action == TargetAccessActionInherit {
return nil
}
return validateAccessActionPath(idx, target.Path)
}
func validatedTargetAccessAction(idx int, target *Target) (TargetAccessAction, error) {
if target.AccessActionProvided && target.AccessAction == "" {
return "", fmt.Errorf("target %d: unknown access_action %q", idx, target.AccessAction)
}
action := target.effectiveAccessAction()
switch action {
case TargetAccessActionInherit, TargetAccessActionBypass, TargetAccessActionBlock:
return action, nil
default:
return "", fmt.Errorf("target %d: unknown access_action %q", idx, target.AccessAction)
}
}
func normalizedTargetPath(configured *string) string {
if configured == nil || *configured == "" {
return "/"
}
return *configured
}
func validateAccessActionPath(idx int, configured *string) error {
if configured == nil {
return nil
}
value := *configured
if value == "" {
return nil
}
if !strings.HasPrefix(value, "/") {
return fmt.Errorf("target %d: access_action path %q must start with /", idx, value)
}
if !utf8.ValidString(value) || strings.ContainsAny(value, "%\\?#;") || strings.IndexFunc(value, unicode.IsControl) >= 0 {
return fmt.Errorf("target %d: access_action path %q contains invalid characters", idx, value)
}
segments := strings.Split(strings.TrimPrefix(value, "/"), "/")
for i, segment := range segments {
if segment == "." || segment == ".." {
return fmt.Errorf("target %d: access_action path %q is not canonical", idx, value)
}
if segment == "" && value != "/" && i != len(segments)-1 {
return fmt.Errorf("target %d: access_action path %q is not canonical", idx, value)
}
}
return nil
}