Add users and network settings tabs with admin protections.
List and delete accounts with last-admin and Administrators-group guards, and expose editable network settings via the configuration UI.
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/SquidSE/ClusterCanvas/service/internal/settings"
|
||||
)
|
||||
|
||||
var hostnamePattern = regexp.MustCompile(`^(?i)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$|^localhost$`)
|
||||
|
||||
// ValidateHostname checks a public hostname (empty is allowed for local/dev).
|
||||
func ValidateHostname(hostname string) error {
|
||||
trimmed := strings.TrimSpace(hostname)
|
||||
if trimmed == "" {
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(trimmed, "/") || strings.Contains(trimmed, ":") || strings.Contains(trimmed, " ") {
|
||||
return fmt.Errorf("public_hostname must be a bare DNS name without scheme, port, or path")
|
||||
}
|
||||
if !hostnamePattern.MatchString(trimmed) {
|
||||
return fmt.Errorf("public_hostname is not a valid DNS hostname")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HostMatches reports whether requestHost matches the configured public hostname.
|
||||
// An empty / whitespace public hostname means any host is allowed (local/dev).
|
||||
func HostMatches(requestHost string, publicHostname string) bool {
|
||||
wanted := strings.TrimSpace(publicHostname)
|
||||
if wanted == "" {
|
||||
return true
|
||||
}
|
||||
host := strings.TrimSpace(requestHost)
|
||||
if parsedHost, _, err := net.SplitHostPort(host); err == nil {
|
||||
host = parsedHost
|
||||
}
|
||||
return strings.EqualFold(host, wanted)
|
||||
}
|
||||
|
||||
// IPAllowed evaluates whitelist/blacklist rules against clientIP.
|
||||
func IPAllowed(clientIP string, network settings.NetworkSettings) (bool, string) {
|
||||
network = settings.EffectiveNetwork(network)
|
||||
parsedIP := net.ParseIP(clientIP)
|
||||
if parsedIP == nil {
|
||||
return false, "unable to parse client IP"
|
||||
}
|
||||
|
||||
switch network.AccessMode {
|
||||
case settings.AccessModeOpen, "":
|
||||
return true, ""
|
||||
case settings.AccessModeWhitelist:
|
||||
if matchesAnyRule(parsedIP, network.Rules) {
|
||||
return true, ""
|
||||
}
|
||||
return false, "client IP is not on the whitelist"
|
||||
case settings.AccessModeBlacklist:
|
||||
if matchesAnyRule(parsedIP, network.Rules) {
|
||||
return false, "client IP matches a blacklist rule"
|
||||
}
|
||||
return true, ""
|
||||
default:
|
||||
return false, "invalid access mode"
|
||||
}
|
||||
}
|
||||
|
||||
// WouldLockOut estimates whether rules would block clientIP.
|
||||
func WouldLockOut(clientIP string, network settings.NetworkSettings) (bool, string) {
|
||||
allowed, reason := IPAllowed(clientIP, network)
|
||||
if allowed {
|
||||
return false, ""
|
||||
}
|
||||
return true, reason
|
||||
}
|
||||
|
||||
func matchesAnyRule(ip net.IP, rules []string) bool {
|
||||
for _, rule := range rules {
|
||||
trimmed := strings.TrimSpace(rule)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(trimmed, "/") {
|
||||
_, network, err := net.ParseCIDR(trimmed)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if network.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
ruleIP := net.ParseIP(trimmed)
|
||||
if ruleIP != nil && ruleIP.Equal(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ValidateListenAddress checks a bind address (IP or hostname).
|
||||
func ValidateListenAddress(address string) error {
|
||||
trimmed := strings.TrimSpace(address)
|
||||
if trimmed == "" {
|
||||
return fmt.Errorf("listen_address is required")
|
||||
}
|
||||
if trimmed == "0.0.0.0" || trimmed == "::" || trimmed == "localhost" {
|
||||
return nil
|
||||
}
|
||||
if net.ParseIP(trimmed) != nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("listen_address must be an IP address or 0.0.0.0")
|
||||
}
|
||||
|
||||
// ValidateAccessRules validates CIDR/IP entries.
|
||||
func ValidateAccessRules(rules []string) error {
|
||||
for _, rule := range rules {
|
||||
trimmed := strings.TrimSpace(rule)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(trimmed, "/") {
|
||||
if _, _, err := net.ParseCIDR(trimmed); err != nil {
|
||||
return fmt.Errorf("invalid CIDR rule %q", trimmed)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if net.ParseIP(trimmed) == nil {
|
||||
return fmt.Errorf("invalid IP rule %q", trimmed)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user