2018-01-28 14:16:52 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"regexp"
|
|
|
|
"strings"
|
|
|
|
|
2019-11-03 17:50:44 +00:00
|
|
|
"github.com/Luzifer/go_helpers/v2/str"
|
2018-01-28 14:16:52 +00:00
|
|
|
)
|
|
|
|
|
2023-07-28 14:57:34 +00:00
|
|
|
const (
|
|
|
|
groupAnonymous = "@_anonymous"
|
|
|
|
groupAuthenticated = "@_authenticated"
|
|
|
|
)
|
2018-01-28 14:16:52 +00:00
|
|
|
|
2023-07-28 14:57:34 +00:00
|
|
|
type (
|
|
|
|
acl struct {
|
|
|
|
RuleSets []aclRuleSet `yaml:"rule_sets"`
|
2018-01-28 14:16:52 +00:00
|
|
|
}
|
|
|
|
|
2023-07-28 14:57:34 +00:00
|
|
|
aclRule struct {
|
|
|
|
Field string `yaml:"field"`
|
|
|
|
Invert bool `yaml:"invert"`
|
|
|
|
IsPresent *bool `yaml:"present"`
|
|
|
|
MatchRegex *string `yaml:"regexp"`
|
|
|
|
MatchString *string `yaml:"equals"`
|
2018-01-28 14:16:52 +00:00
|
|
|
}
|
|
|
|
|
2023-07-28 14:57:34 +00:00
|
|
|
aclAccessResult uint
|
|
|
|
|
|
|
|
aclRuleSet struct {
|
|
|
|
Rules []aclRule `yaml:"rules"`
|
|
|
|
|
|
|
|
Allow []string `yaml:"allow"`
|
|
|
|
Deny []string `yaml:"deny"`
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
accessDunno aclAccessResult = iota
|
|
|
|
accessAllow
|
|
|
|
accessDeny
|
|
|
|
)
|
|
|
|
|
|
|
|
// --- ACL
|
|
|
|
|
|
|
|
func (a acl) HasAccess(user string, groups []string, r *http.Request) bool {
|
|
|
|
var (
|
|
|
|
collectionAllow = map[string]bool{}
|
|
|
|
collectionDeny = map[string]bool{}
|
|
|
|
)
|
|
|
|
|
|
|
|
for _, rs := range a.RuleSets {
|
|
|
|
if !rs.AppliesToRequest(r) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Collect the allows from all matching rulesets
|
|
|
|
for _, a := range rs.Allow {
|
|
|
|
collectionAllow[a] = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Collect the denies from all matching rulesets
|
|
|
|
for _, d := range rs.Deny {
|
|
|
|
collectionDeny[d] = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Form lists from the collections
|
|
|
|
var allowed, denied []string
|
|
|
|
|
|
|
|
for k := range collectionAllow {
|
|
|
|
allowed = append(allowed, k)
|
|
|
|
}
|
|
|
|
for k := range collectionDeny {
|
|
|
|
denied = append(denied, k)
|
|
|
|
}
|
|
|
|
|
|
|
|
return a.checkAccess(user, groups, allowed, denied)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a acl) Validate() error {
|
|
|
|
for i, r := range a.RuleSets {
|
|
|
|
if err := r.Validate(); err != nil {
|
|
|
|
return fmt.Errorf("RuleSet on position %d is invalid: %s", i+1, err)
|
2018-01-28 14:16:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-07-28 14:57:34 +00:00
|
|
|
func (a acl) checkAccess(user string, groups, allowed, denied []string) bool {
|
|
|
|
if !str.StringInSlice(user, []string{"", "\x00"}) {
|
|
|
|
// The user is set to a non-anon user, we add the pseudo-group
|
|
|
|
// authenticated to the groups list the user has
|
|
|
|
groups = append(groups, groupAuthenticated)
|
|
|
|
} else {
|
|
|
|
// The user did match anon, therefore we set the pseudo-group
|
|
|
|
// for anonymous to be used in group matching
|
|
|
|
groups = []string{groupAnonymous}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Quoting the documentation here:
|
|
|
|
// "There is a simple logic: Users before groups, denies before allows."
|
|
|
|
|
|
|
|
// Lets check the user
|
|
|
|
if str.StringInSlice(user, denied) {
|
|
|
|
// Explicit deny on the user, they're out!
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if str.StringInSlice(user, allowed) {
|
|
|
|
// Explicit allow on the user, they're in!
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// The user yielded no result, lets check the groups
|
|
|
|
for _, group := range groups {
|
|
|
|
if str.StringInSlice(a.fixGroupName(group), denied) {
|
|
|
|
// The group is denied access
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if str.StringInSlice(a.fixGroupName(group), allowed) {
|
|
|
|
// The group is allowed access
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// We found no match for the user and/or group. Last chance is
|
|
|
|
// no ruleset denied anonymous access and at least one ruleset
|
|
|
|
// enabled anonymous access
|
|
|
|
if !str.StringInSlice(groupAnonymous, denied) && str.StringInSlice(groupAnonymous, allowed) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// We found neither a user nor a group with access or deny config
|
|
|
|
// so we fall back to the default: No access.
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (acl) fixGroupName(group string) string {
|
|
|
|
return "@" + strings.TrimLeft(group, "@")
|
|
|
|
}
|
|
|
|
|
|
|
|
// --- ACL Rule
|
|
|
|
|
|
|
|
// AppliesToFields checks whether the given rule conditions matches
|
|
|
|
// the given fields
|
2018-01-28 14:16:52 +00:00
|
|
|
func (a aclRule) AppliesToFields(fields map[string]string) bool {
|
|
|
|
var field, value string
|
|
|
|
|
|
|
|
for f, v := range fields {
|
|
|
|
if strings.ToLower(a.Field) == f {
|
|
|
|
field = f
|
|
|
|
value = v
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if a.IsPresent != nil {
|
|
|
|
if !a.Invert && *a.IsPresent && field == "" {
|
|
|
|
// Field is expected to be present but isn't, rule does not apply
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if !a.Invert && !*a.IsPresent && field != "" {
|
|
|
|
// Field is expected not to be present but is, rule does not apply
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if a.Invert && *a.IsPresent && field != "" {
|
|
|
|
// Field is expected not to be present but is, rule does not apply
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if a.Invert && !*a.IsPresent && field == "" {
|
|
|
|
// Field is expected to be present but isn't, rule does not apply
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
if field == "" {
|
|
|
|
// We found a rule which has no matching field, rule does not apply
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if a.MatchString != nil {
|
|
|
|
if (*a.MatchString != value) == !a.Invert {
|
|
|
|
// Value does not match expected string, rule does not apply
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if a.MatchRegex != nil {
|
|
|
|
if regexp.MustCompile(*a.MatchRegex).MatchString(value) == a.Invert {
|
|
|
|
// Value does not match expected regexp, rule does not apply
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2023-07-28 14:57:34 +00:00
|
|
|
func (a aclRule) Validate() error {
|
|
|
|
if a.Field == "" {
|
|
|
|
return fmt.Errorf("field is not set")
|
|
|
|
}
|
2018-01-28 14:16:52 +00:00
|
|
|
|
2023-07-28 14:57:34 +00:00
|
|
|
if a.IsPresent == nil && a.MatchRegex == nil && a.MatchString == nil {
|
|
|
|
return fmt.Errorf("no matcher (present, regexp, equals) is set")
|
|
|
|
}
|
2018-01-28 14:16:52 +00:00
|
|
|
|
2023-07-28 14:57:34 +00:00
|
|
|
if a.MatchRegex != nil {
|
|
|
|
if _, err := regexp.Compile(*a.MatchRegex); err != nil {
|
|
|
|
return fmt.Errorf("regexp is invalid: %s", err)
|
|
|
|
}
|
2018-01-28 14:16:52 +00:00
|
|
|
}
|
|
|
|
|
2023-07-28 14:57:34 +00:00
|
|
|
return nil
|
2018-01-28 14:16:52 +00:00
|
|
|
}
|
|
|
|
|
2023-07-28 14:57:34 +00:00
|
|
|
// --- ACL Rule Set
|
|
|
|
|
|
|
|
// AppliesToRequest checks whether every rule in the aclRuleSet
|
|
|
|
// matches the http.Request. If not this rule-set must not be applied
|
|
|
|
// to the given request
|
|
|
|
func (a aclRuleSet) AppliesToRequest(r *http.Request) bool {
|
2018-01-28 14:16:52 +00:00
|
|
|
fields := a.buildFieldSet(r)
|
|
|
|
|
|
|
|
for _, rule := range a.Rules {
|
|
|
|
if !rule.AppliesToFields(fields) {
|
|
|
|
// At least one rule does not match the request
|
2023-07-28 14:57:34 +00:00
|
|
|
return false
|
2018-01-28 14:16:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-28 14:57:34 +00:00
|
|
|
return true
|
2018-01-28 14:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (a aclRuleSet) Validate() error {
|
|
|
|
for i, r := range a.Rules {
|
|
|
|
if err := r.Validate(); err != nil {
|
2023-07-28 14:57:34 +00:00
|
|
|
return fmt.Errorf("rule on position %d is invalid: %s", i+1, err)
|
2018-01-28 14:16:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-07-28 14:57:34 +00:00
|
|
|
func (a aclRuleSet) buildFieldSet(r *http.Request) map[string]string {
|
|
|
|
result := map[string]string{}
|
2018-01-28 14:16:52 +00:00
|
|
|
|
2023-07-28 14:57:34 +00:00
|
|
|
for k := range r.Header {
|
|
|
|
result[strings.ToLower(k)] = r.Header.Get(k)
|
2018-01-28 14:16:52 +00:00
|
|
|
}
|
|
|
|
|
2023-07-28 14:57:34 +00:00
|
|
|
return result
|
2018-01-28 14:16:52 +00:00
|
|
|
}
|