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 +}