From 0ce9ae756ee3750c74ea951b072766d681951628 Mon Sep 17 00:00:00 2001
From: Joel Speed <joel.speed@hotmail.co.uk>
Date: Sun, 19 Jul 2020 09:37:06 +0100
Subject: [PATCH] Add request authorization ruleset

---
 pkg/apis/options/authorization.go |  17 +++++
 pkg/authorization/conditions.go   |  86 ++++++++++++++++++++++++
 pkg/authorization/rules.go        | 106 ++++++++++++++++++++++++++++++
 3 files changed, 209 insertions(+)
 create mode 100644 pkg/apis/options/authorization.go
 create mode 100644 pkg/authorization/conditions.go
 create mode 100644 pkg/authorization/rules.go

diff --git a/pkg/apis/options/authorization.go b/pkg/apis/options/authorization.go
new file mode 100644
index 00000000..2c5f9176
--- /dev/null
+++ b/pkg/apis/options/authorization.go
@@ -0,0 +1,17 @@
+package options
+
+type AuthorizationPolicy string
+
+const (
+	AllowPolicy AuthorizationPolicy = "Allow"
+	DenyPolicy  AuthorizationPolicy = "Deny"
+)
+
+type AuthorizationRule struct {
+	Policy  AuthorizationPolicy
+	Path    string
+	Methods []string
+	IPs     []string
+}
+
+type RequestRules []AuthorizationRule
diff --git a/pkg/authorization/conditions.go b/pkg/authorization/conditions.go
new file mode 100644
index 00000000..d5b63d16
--- /dev/null
+++ b/pkg/authorization/conditions.go
@@ -0,0 +1,86 @@
+package authorization
+
+import (
+	"errors"
+	"fmt"
+	"net"
+	"net/http"
+	"regexp"
+	"strings"
+
+	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
+)
+
+type condition interface {
+	matches(*http.Request) bool
+}
+
+type methodCondition struct {
+	methods map[string]struct{}
+}
+
+func (m methodCondition) matches(req *http.Request) bool {
+	_, ok := m.methods[strings.ToUpper(req.Method)]
+	return ok
+}
+
+func newMethodCondition(methods []string) condition {
+	methodMap := make(map[string]struct{})
+	for _, method := range methods {
+		methodMap[strings.ToUpper(method)] = struct{}{}
+	}
+	return methodCondition{
+		methods: methodMap,
+	}
+}
+
+type pathCondition struct {
+	pathRegexp *regexp.Regexp
+}
+
+func (p pathCondition) matches(req *http.Request) bool {
+	return p.pathRegexp.MatchString(req.URL.Path)
+}
+
+func newPathCondition(path string) (condition, error) {
+	exp, err := regexp.Compile(path)
+	if err != nil {
+		return nil, err
+	}
+	return pathCondition{
+		pathRegexp: exp,
+	}, nil
+}
+
+type ipCondition struct {
+	netSet      *ip.NetSet
+	getClientIP func(req *http.Request) net.IP
+}
+
+func (i ipCondition) matches(req *http.Request) bool {
+	ip := i.getClientIP(req)
+	if ip == nil {
+		return false
+	}
+	return i.netSet.Has(ip)
+}
+
+func newIPCondition(rawIPs []string, getClientIPFunc func(req *http.Request) net.IP) (condition, error) {
+	if getClientIPFunc == nil {
+		return nil, errors.New("client IP function required for IP condition")
+	}
+
+	netSet := ip.NewNetSet()
+	for _, rawIP := range rawIPs {
+		ipNet := ip.ParseIPNet(rawIP)
+		if ipNet == nil {
+			return nil, fmt.Errorf("could not parse IP network: %s", rawIP)
+		}
+		netSet.AddIPNet(*ipNet)
+	}
+
+	return ipCondition{
+		netSet:      netSet,
+		getClientIP: getClientIPFunc,
+	}, nil
+}
diff --git a/pkg/authorization/rules.go b/pkg/authorization/rules.go
new file mode 100644
index 00000000..41c36b31
--- /dev/null
+++ b/pkg/authorization/rules.go
@@ -0,0 +1,106 @@
+package authorization
+
+import (
+	"net"
+	"net/http"
+
+	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
+)
+
+type AuthorizationPolicy int
+
+const (
+	NonePolicy AuthorizationPolicy = iota
+	AllowPolicy
+	DenyPolicy
+)
+
+type RuleSet interface {
+	Matches(req *http.Request) AuthorizationPolicy
+}
+
+type rule struct {
+	conditions []condition
+	policy     AuthorizationPolicy
+}
+
+func (r rule) matches(req *http.Request) AuthorizationPolicy {
+	for _, condition := range r.conditions {
+		if !condition.matches(req) {
+			// One of the conditions didn't match so this rule does not apply
+			return NonePolicy
+		}
+	}
+	// If all conditions match, return the configured rule policy
+	return r.policy
+}
+
+func newRule(authRule options.AuthorizationRule, getClientIPFunc func(*http.Request) net.IP) (rule, error) {
+	// This function should add the conditions in order of complexity, least complex first
+	conditions := []condition{}
+
+	if len(authRule.Methods) > 0 {
+		conditions = append(conditions, newMethodCondition(authRule.Methods))
+	}
+
+	if len(authRule.Path) > 0 {
+		condition, err := newPathCondition(authRule.Path)
+		if err != nil {
+			return rule{}, err
+		}
+		conditions = append(conditions, condition)
+	}
+
+	if len(authRule.IPs) > 0 {
+		condition, err := newIPCondition(authRule.IPs, getClientIPFunc)
+		if err != nil {
+			return rule{}, err
+		}
+		conditions = append(conditions, condition)
+	}
+
+	var policy AuthorizationPolicy
+	switch authRule.Policy {
+	case options.AllowPolicy:
+		policy = AllowPolicy
+	case options.DenyPolicy:
+		policy = DenyPolicy
+	default:
+		// This shouldn't be the case and should be prevented by validation
+		policy = NonePolicy
+	}
+
+	return rule{
+		conditions: conditions,
+		policy:     policy,
+	}, nil
+}
+
+type ruleSet struct {
+	rules []rule
+}
+
+func (r ruleSet) Matches(req *http.Request) AuthorizationPolicy {
+	for _, rule := range r.rules {
+		if policy := rule.matches(req); policy != NonePolicy {
+			// The rule applies to this request, return its policy
+			return policy
+		}
+	}
+	// No rules matched
+	return NonePolicy
+}
+
+func NewRuleSet(requestRules options.RequestRules, getClientIPFunc func(*http.Request) net.IP) (RuleSet, error) {
+	rules := []rule{}
+	for _, requestRule := range requestRules {
+		r, err := newRule(requestRule, getClientIPFunc)
+		if err != nil {
+			return nil, err
+		}
+		rules = append(rules, r)
+	}
+	return ruleSet{
+		rules: rules,
+	}, nil
+}