"go.fuhry.dev/runtime/mtls"
"go.fuhry.dev/runtime/utils/log"
+ "go.fuhry.dev/runtime/utils/stringmatch"
"gopkg.in/yaml.v3"
)
type AclRule struct {
RemoteId mtls.RemoteIdentity
Allow string
+ Deny string
}
type TemplateRule struct {
var _ yaml.Unmarshaler = &AclRule{}
+type decision uint
+
+const (
+ noDecision decision = iota
+ decisionAllow
+ decisionDeny
+)
+
func (r *AclRule) UnmarshalYAML(node *yaml.Node) error {
type unpacked struct {
User string `yaml:"user"`
Service string `yaml:"service"`
Domain string `yaml:"domain"`
Allow string `yaml:"allow"`
+ Deny string `yaml:"deny"`
}
u := new(unpacked)
return errors.New("user and service are mutually exclusive")
}
- if u.Allow == "" {
- return errors.New("property \"allow\" is required")
+ if u.Allow == "" && u.Deny == "" {
+ return errors.New("either \"allow\" or \"deny\" is required")
+ }
+
+ if u.Allow != "" && u.Deny != "" {
+ return errors.New("\"allow\" and \"deny\" are mutually exclusive")
}
r.RemoteId = mtls.RemoteIdentity{
}
r.Allow = u.Allow
+ r.Deny = u.Deny
return nil
}
-func (r *AclRule) Check(id *mtls.RemoteIdentity, requestedPrincipal string) bool {
+func (r *AclRule) Check(id *mtls.RemoteIdentity, requestedPrincipal string) decision {
logger := log.Default().WithPrefix(fmt.Sprintf("mint.servicer.Acl[%s, %s]", r.RemoteId.ToSpiffe().String(), r.Allow))
- if r.Allow != "*" && r.Allow != requestedPrincipal {
+ onMatch := noDecision
+ if r.Allow == "*" || r.Allow == requestedPrincipal {
+ onMatch = decisionAllow
+ } else if r.Deny == "*" || r.Deny == requestedPrincipal {
+ onMatch = decisionDeny
+ } else {
logger.Infof("Check(%q, %q) -> false (princ mismatch)", id.ToSpiffe().String(), requestedPrincipal)
- return false
+ return noDecision
}
switch r.RemoteId.Class {
if r.RemoteId.Domain != "" {
if id.Domain != r.RemoteId.Domain && !strings.HasSuffix(id.Domain, "."+r.RemoteId.Domain) {
logger.Infof("Check(%q, %q) -> false (domain mismatch)", id.ToSpiffe().String(), requestedPrincipal)
- return false
+ return noDecision
}
}
if id.Class != r.RemoteId.Class {
logger.Infof("Check(%q, %q) -> false (class mismatch)", id.ToSpiffe().String(), requestedPrincipal)
- return false
+ return noDecision
}
if r.RemoteId.Principal != "*" && id.Principal != r.RemoteId.Principal {
logger.Infof("Check(%q, %q) -> false (requester princ mismatch)", id.ToSpiffe().String(), requestedPrincipal)
- return false
+ return noDecision
}
logger.Infof("Check(%q, %q) -> true", id.ToSpiffe().String(), requestedPrincipal)
- return true
+ return onMatch
case mtls.Domain:
if r.RemoteId.Domain == "" {
logger.Infof("Check(%q, %q) -> false (domain mismatch)", id.ToSpiffe().String(), requestedPrincipal)
- return false
+ return noDecision
}
match := id.Domain == r.RemoteId.Domain || strings.HasSuffix(id.Domain, "."+r.RemoteId.Domain)
logger.Infof("Check(%q, %q) -> %t (domain check)", id.ToSpiffe().String(), requestedPrincipal, match)
- return match
+ if match {
+ return onMatch
+ }
+ return noDecision
}
logger.Infof("Check(%q, %q) -> false (fallback case)", id.ToSpiffe().String(), requestedPrincipal)
- return false
+ return noDecision
}
func (f *AclRulesFile) Check(id *mtls.RemoteIdentity, requestedPrincipal string) bool {
for _, r := range f.Rules {
- if r.Check(id, requestedPrincipal) {
+ d := r.Check(id, requestedPrincipal)
+ if d == decisionAllow {
return true
+ } else if d == decisionDeny {
+ return false
}
+ // else, continue processing
}
return false