A cloud-native LDAP server backed by a Consul datastore
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

141 lines
3.0 KiB

package main
import (
"fmt"
"path"
"strings"
)
type Login struct {
user string
groups []string
}
type ACL []ACLEntry
type ACLEntry struct {
// The authenticated user (or ANONYMOUS if not authenticated) must match this string
user string
// For each of this groups, the authenticated user must belong to one group that matches
reqGroups []string
// The action requested must match one of these strings
actions []string
// The requested target must match this string. The special word SELF is replaced in the pattern by the user's dn before matching
target string
// All attributes requested must match one of these patterns
attributes []string
// All attributes requested must not match any of these patterns
exclAttributes []string
}
func splitNoEmpty(s string) []string {
tmp := strings.Split(s, " ")
ret := []string{}
for _, s := range tmp {
if len(s) > 0 {
ret = append(ret, s)
}
}
return ret
}
func ParseACL(def []string) (ACL, error) {
acl := []ACLEntry{}
for _, item := range def {
parts := strings.Split(item, ":")
if len(parts) != 5 {
return nil, fmt.Errorf("Invalid ACL entry: %s", item)
}
attr, exclAttr := []string{}, []string{}
for _, s := range splitNoEmpty(parts[4]) {
if s[0] == '!' {
exclAttr = append(exclAttr, s[1:])
} else {
attr = append(attr, s)
}
}
item_def := ACLEntry{
user: parts[0],
reqGroups: splitNoEmpty(parts[1]),
actions: splitNoEmpty(parts[2]),
target: parts[3],
attributes: attr,
exclAttributes: exclAttr,
}
acl = append(acl, item_def)
}
return acl, nil
}
func (acl ACL) Check(login *Login, action string, target string, attributes []string) bool {
for _, item := range acl {
if item.Check(login, action, target, attributes) {
return true
}
}
return false
}
func (entry *ACLEntry) Check(login *Login, action string, target string, attributes []string) bool {
if !match(entry.user, login.user) {
return false
}
for _, grp := range entry.reqGroups {
if !matchAny(grp, login.groups) {
return false
}
}
matchTarget := match(entry.target, target)
if !matchTarget && len(target) >= len(login.user) {
start := len(target) - len(login.user)
if target[start:] == login.user {
matchTarget = match(entry.target, target[:start]+"SELF")
}
}
if !matchTarget {
return false
}
if !anyMatch(entry.actions, action) {
return false
}
for _, attrib := range attributes {
if !anyMatch(entry.attributes, attrib) {
return false
}
}
for _, exclAttr := range entry.exclAttributes {
if matchAny(exclAttr, attributes) {
return false
}
}
return true
}
func match(pattern string, val string) bool {
rv, err := path.Match(strings.ToLower(pattern), strings.ToLower(val))
return err == nil && rv
}
func matchAny(pattern string, vals []string) bool {
for _, val := range vals {
if match(pattern, val) {
return true
}
}
return false
}
func anyMatch(patterns []string, val string) bool {
for _, pattern := range patterns {
if match(pattern, val) {
return true
}
}
return false
}