From da82edb8d4a4775e4b5bcfa413e1377328e5d4c8 Mon Sep 17 00:00:00 2001 From: Christopher Sams Date: Tue, 8 Nov 2022 14:23:49 -0600 Subject: [PATCH] Add rudimentary user gating The user gate is a rudimentary authorization filter that controls which authenticated users are authorized to make requests against CPS. It is dynamically configured by a secret containing rules in the kcp-system namespace of the root workspace. This filter is meant to be used along with another system that dynamically updates the secret during a user sign-up workflow ahead of general availability. It should not be used after a more general mechanism provided by CIAM is in place. --- cmd/cps-front-proxy/main.go | 17 +- cmd/cps-front-proxy/options/options.go | 69 +++++++ go.mod | 25 +-- go.sum | 32 +-- pkg/proxy/config.go | 61 ++++++ pkg/proxy/options/options.go | 53 +++++ pkg/proxy/options/usergate.go | 42 ++++ pkg/proxy/server.go | 114 +++++++++++ pkg/proxy/usergating/controller.go | 200 +++++++++++++++++++ pkg/proxy/usergating/handler.go | 69 +++++++ pkg/proxy/usergating/ruleset/ruleset.go | 106 ++++++++++ pkg/proxy/usergating/ruleset/ruleset_test.go | 56 ++++++ 12 files changed, 813 insertions(+), 31 deletions(-) create mode 100644 cmd/cps-front-proxy/options/options.go create mode 100644 pkg/proxy/config.go create mode 100644 pkg/proxy/options/options.go create mode 100644 pkg/proxy/options/usergate.go create mode 100644 pkg/proxy/server.go create mode 100644 pkg/proxy/usergating/controller.go create mode 100644 pkg/proxy/usergating/handler.go create mode 100644 pkg/proxy/usergating/ruleset/ruleset.go create mode 100644 pkg/proxy/usergating/ruleset/ruleset_test.go diff --git a/cmd/cps-front-proxy/main.go b/cmd/cps-front-proxy/main.go index 3753c35..031fc0c 100644 --- a/cmd/cps-front-proxy/main.go +++ b/cmd/cps-front-proxy/main.go @@ -24,9 +24,7 @@ import ( "os" "time" - frontproxyoptions "github.com/kcp-dev/kcp/cmd/kcp-front-proxy/options" kcpfeatures "github.com/kcp-dev/kcp/pkg/features" - "github.com/kcp-dev/kcp/pkg/proxy" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -36,6 +34,9 @@ import ( utilflag "k8s.io/component-base/cli/flag" _ "k8s.io/component-base/logs/json/register" "k8s.io/component-base/version" + + frontproxyoptions "github.com/redhat-cps/front-proxy/cmd/cps-front-proxy/options" + "github.com/redhat-cps/front-proxy/pkg/proxy" ) func main() { @@ -71,12 +72,12 @@ routed based on paths.`, return errors.NewAggregate(errs) } - if options.Proxy.ProfilerAddress != "" { + if options.CPSProxy.KCPProxyOptions.ProfilerAddress != "" { //nolint:errcheck - go http.ListenAndServe(options.Proxy.ProfilerAddress, nil) + go http.ListenAndServe(options.CPSProxy.KCPProxyOptions.ProfilerAddress, nil) } - config, err := proxy.NewConfig(options.Proxy) + config, err := proxy.NewConfig(options.CPSProxy) if err != nil { return err } @@ -89,11 +90,13 @@ routed based on paths.`, if err != nil { return err } - prepared, err := server.PrepareRun(ctx) + + preparedServer, err := server.PrepareRun(ctx) if err != nil { return err } - return prepared.Run(ctx) + + return preparedServer.Run(ctx) }, } diff --git a/cmd/cps-front-proxy/options/options.go b/cmd/cps-front-proxy/options/options.go new file mode 100644 index 0000000..1e4e95e --- /dev/null +++ b/cmd/cps-front-proxy/options/options.go @@ -0,0 +1,69 @@ +/* +Copyright 2022 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package options + +import ( + "github.com/spf13/pflag" + + "k8s.io/component-base/config" + "k8s.io/component-base/logs" + + cpsproxyoptions "github.com/redhat-cps/front-proxy/pkg/proxy/options" +) + +/* +Options are organized in a tree with these at the root since they're for the +command that is used to start the proxy process. The CPS proxy options are +specific to CPS, but they also reference the options provided by the proxy +implemention in kcp. +*/ +type Options struct { + CPSProxy *cpsproxyoptions.Options + Logs *logs.Options +} + +func NewOptions() *Options { + o := &Options{ + CPSProxy: cpsproxyoptions.NewOptions(), + Logs: logs.NewOptions(), + } + + // Default to -v=2 + o.Logs.Config.Verbosity = config.VerbosityLevel(2) + return o +} + +func (o *Options) AddFlags(fs *pflag.FlagSet) { + o.CPSProxy.AddFlags(fs) + o.Logs.AddFlags(fs) +} + +func (o *Options) Complete() error { + if err := o.CPSProxy.Complete(); err != nil { + return err + } + + return nil +} + +func (o *Options) Validate() []error { + var errs []error + + errs = append(errs, o.CPSProxy.Validate()...) + + return errs +} diff --git a/go.mod b/go.mod index 7513949..1a4300d 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,19 @@ module github.com/redhat-cps/front-proxy go 1.18 require ( + github.com/kcp-dev/apimachinery v0.0.0-20221102195355-d65878bc16be + github.com/kcp-dev/client-go v0.0.0-20221103171446-a51d1144350f github.com/kcp-dev/kcp v0.8.1-0.20221005104043-b96cf4e08e12 + github.com/kcp-dev/kcp/pkg/apis v0.8.1-0.20221005104043-b96cf4e08e12 github.com/spf13/cobra v1.4.0 github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace + k8s.io/api v0.24.3 k8s.io/apimachinery v0.24.3 k8s.io/apiserver v0.24.3 + k8s.io/client-go v0.24.3 k8s.io/component-base v0.24.3 + k8s.io/klog/v2 v2.70.1 + sigs.k8s.io/yaml v1.3.0 ) require ( @@ -52,7 +59,7 @@ require ( github.com/google/btree v1.0.1 // indirect github.com/google/cel-go v0.10.1 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect - github.com/google/go-cmp v0.5.6 // indirect + github.com/google/go-cmp v0.5.8 // indirect github.com/google/gofuzz v1.1.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.1.2 // indirect @@ -66,8 +73,6 @@ require ( github.com/jonboulle/clockwork v0.2.2 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kcp-dev/apimachinery v0.0.0-20220912132244-efe716c18e43 // indirect - github.com/kcp-dev/kcp/pkg/apis v0.8.1-0.20221005104043-b96cf4e08e12 // indirect github.com/kcp-dev/logicalcluster/v2 v2.0.0-alpha.3 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.6 // indirect @@ -121,10 +126,10 @@ require ( go.uber.org/multierr v1.7.0 // indirect go.uber.org/zap v1.19.0 // indirect golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect - golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect + golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect @@ -134,21 +139,18 @@ require ( google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368 // indirect google.golang.org/grpc v1.40.0 // indirect - google.golang.org/protobuf v1.27.1 // indirect + google.golang.org/protobuf v1.28.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/square/go-jose.v2 v2.2.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect - k8s.io/api v0.24.3 // indirect k8s.io/apiextensions-apiserver v0.24.3 // indirect k8s.io/cli-runtime v0.24.3 // indirect - k8s.io/client-go v0.24.3 // indirect k8s.io/cloud-provider v0.0.0 // indirect k8s.io/cluster-bootstrap v0.0.0 // indirect k8s.io/component-helpers v0.0.0 // indirect k8s.io/controller-manager v0.0.0 // indirect - k8s.io/klog/v2 v2.60.1 // indirect k8s.io/kube-aggregator v0.0.0 // indirect k8s.io/kube-controller-manager v0.0.0 // indirect k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect @@ -156,13 +158,12 @@ require ( k8s.io/kubernetes v1.24.3 // indirect k8s.io/mount-utils v0.0.0 // indirect k8s.io/pod-security-admission v0.0.0 // indirect - k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect + k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30 // indirect - sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect + sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect sigs.k8s.io/kustomize/api v0.11.4 // indirect sigs.k8s.io/kustomize/kyaml v0.13.6 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect - sigs.k8s.io/yaml v1.2.0 // indirect ) replace ( diff --git a/go.sum b/go.sum index 4f45ae5..b4c38d3 100644 --- a/go.sum +++ b/go.sum @@ -320,8 +320,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -416,8 +416,10 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= -github.com/kcp-dev/apimachinery v0.0.0-20220912132244-efe716c18e43 h1:vPv81j3mT5VYQ6YnCXrnKJQPeRNHwPcGJNsQNQfIG9Q= -github.com/kcp-dev/apimachinery v0.0.0-20220912132244-efe716c18e43/go.mod h1:qnvUHkdxOrNzX17yX+z8r81CZEBuFdveNzWqFlwZ55w= +github.com/kcp-dev/apimachinery v0.0.0-20221102195355-d65878bc16be h1:2uDzJ896+ojtzgr9HJL8+tZEoqhq8blwymGinWFrQ6E= +github.com/kcp-dev/apimachinery v0.0.0-20221102195355-d65878bc16be/go.mod h1:qnvUHkdxOrNzX17yX+z8r81CZEBuFdveNzWqFlwZ55w= +github.com/kcp-dev/client-go v0.0.0-20221103171446-a51d1144350f h1:4lxaO6TZ4SFPJSkv/t0dW5NQpVW7b6b3tBwpbVzEGx8= +github.com/kcp-dev/client-go v0.0.0-20221103171446-a51d1144350f/go.mod h1:M1KBevWifitzA15pZmJ9wslAtpljNfHzZZLLVYS6XHA= github.com/kcp-dev/kcp v0.8.1-0.20221005104043-b96cf4e08e12 h1:HRUGY0xZX4P3LZ6JLTWhDN3kP8VrWq57F9TOVf9tjJI= github.com/kcp-dev/kcp v0.8.1-0.20221005104043-b96cf4e08e12/go.mod h1:AJOr9fzaWBjlrB13e0ibSHEg8rwmtxzgy5hmjicbSYQ= github.com/kcp-dev/kcp/pkg/apis v0.8.1-0.20221005104043-b96cf4e08e12 h1:eIRDulnWivT1NUcPeckdHtNU/wwpZboIRDqlrxrmSt4= @@ -880,8 +882,9 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -988,8 +991,8 @@ golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1201,8 +1204,9 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1253,16 +1257,18 @@ k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAE k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.60.1 h1:VW25q3bZx9uE3vvdL6M8ezOX79vA2Aq1nEWLqNQclHc= k8s.io/klog/v2 v2.60.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.70.1 h1:7aaoSdahviPmR+XkS7FyxlkkXs6tHISSG03RxleQAVQ= +k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 h1:Gii5eqf+GmIEwGNKQYQClCayuJCe2/4fZUvF7VG99sU= k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42/go.mod h1:Z/45zLw8lUo4wdiUkI+v/ImEGAvu3WatcZl3lPMR4Rk= k8s.io/system-validators v1.7.0/go.mod h1:gP1Ky+R9wtrSiFbrpEPwWMeYz9yqyy1S/KOh0Vci7WI= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 h1:HNSDgDCrr/6Ly3WEGKZftiE7IY19Vz2GdbOCyI4qqhc= k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4= +k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= @@ -1274,8 +1280,9 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30 h1:dUk62HQ3ZFhD48Qr8MIXCiKA8wInBQCtuE4QGfFW7yA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30/go.mod h1:fEO7lRTdivWO2qYVCVG7dEADOMo/MLDCVr8So2g88Uw= -sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 h1:kDi4JBNAsJWfz1aEXhO8Jg87JJaPNLh5tIzYHgStQ9Y= sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/kustomize/api v0.11.4 h1:/0Mr3kfBBNcNPOW5Qwk/3eb8zkswCwnqQxxKtmrTkRo= sigs.k8s.io/kustomize/api v0.11.4/go.mod h1:k+8RsqYbgpkIrJ4p9jcdPqe8DprLxFUUO0yNOq8C+xI= sigs.k8s.io/kustomize/cmd/config v0.10.6/go.mod h1:/S4A4nUANUa4bZJ/Edt7ZQTyKOY9WCER0uBS1SW2Rco= @@ -1286,5 +1293,6 @@ sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= -sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/pkg/proxy/config.go b/pkg/proxy/config.go new file mode 100644 index 0000000..27d5f33 --- /dev/null +++ b/pkg/proxy/config.go @@ -0,0 +1,61 @@ +/* +Copyright 2022 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package proxy + +import ( + kcpproxy "github.com/kcp-dev/kcp/pkg/proxy" + + "github.com/redhat-cps/front-proxy/pkg/proxy/options" +) + +type Config struct { + Options *options.Options + KCPConfig *kcpproxy.Config +} + +type completedConfig struct { + Options *options.Options + KCPConfig *kcpproxy.CompletedConfig +} + +type CompletedConfig struct { + *completedConfig +} + +func NewConfig(o *options.Options) (*Config, error) { + kcpconfig, err := kcpproxy.NewConfig(o.KCPProxyOptions) + if err != nil { + return nil, err + } + return &Config{ + Options: o, + KCPConfig: kcpconfig, + }, nil +} + +func (c *Config) Complete() (CompletedConfig, error) { + completedKcpConfig, err := c.KCPConfig.Complete() + if err != nil { + return CompletedConfig{}, nil + } + return CompletedConfig{ + &completedConfig{ + Options: c.Options, + KCPConfig: &completedKcpConfig, + }, + }, nil +} diff --git a/pkg/proxy/options/options.go b/pkg/proxy/options/options.go new file mode 100644 index 0000000..aa8f4ee --- /dev/null +++ b/pkg/proxy/options/options.go @@ -0,0 +1,53 @@ +/* +Copyright 2022 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package options + +import ( + kcpproxyoptions "github.com/kcp-dev/kcp/pkg/proxy/options" + "github.com/spf13/pflag" +) + +type Options struct { + KCPProxyOptions *kcpproxyoptions.Options + UserGate *UserGate +} + +func NewOptions() *Options { + o := &Options{ + KCPProxyOptions: kcpproxyoptions.NewOptions(), + UserGate: NewUserGate(), + } + return o +} + +func (o *Options) AddFlags(fs *pflag.FlagSet) { + o.KCPProxyOptions.AddFlags(fs) + o.UserGate.AddFlags(fs) +} + +func (o *Options) Complete() error { + return o.KCPProxyOptions.Complete() +} + +func (o *Options) Validate() []error { + var errs []error + + errs = append(errs, o.KCPProxyOptions.Validate()...) + errs = append(errs, o.UserGate.Validate()...) + + return errs +} diff --git a/pkg/proxy/options/usergate.go b/pkg/proxy/options/usergate.go new file mode 100644 index 0000000..d0cefbb --- /dev/null +++ b/pkg/proxy/options/usergate.go @@ -0,0 +1,42 @@ +/* +Copyright 2022 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package options + +import ( + "github.com/spf13/pflag" +) + +type UserGate struct { + Enabled bool + DefaultRulesFile string +} + +func NewUserGate() *UserGate { + return &UserGate{ + Enabled: false, + DefaultRulesFile: "", + } +} + +func (o *UserGate) AddFlags(fs *pflag.FlagSet) { + fs.BoolVar(&o.Enabled, "enable-user-gating", false, "Enabled user gating.") + fs.StringVar(&o.DefaultRulesFile, "user-gating-rules", "", "Provide a YAML file containing user gating rules.") +} + +func (c *UserGate) Validate() []error { + return nil +} diff --git a/pkg/proxy/server.go b/pkg/proxy/server.go new file mode 100644 index 0000000..7068bff --- /dev/null +++ b/pkg/proxy/server.go @@ -0,0 +1,114 @@ +/* +Copyright 2022 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package proxy + +import ( + "context" + "fmt" + "os" + "time" + + coreinformers "github.com/kcp-dev/client-go/informers" + coreclient "github.com/kcp-dev/client-go/kubernetes" + kcpproxy "github.com/kcp-dev/kcp/pkg/proxy" + kcpproxyfilters "github.com/kcp-dev/kcp/pkg/proxy/filters" + + "github.com/redhat-cps/front-proxy/pkg/proxy/usergating" + "github.com/redhat-cps/front-proxy/pkg/proxy/usergating/ruleset" +) + +type Server struct { + KCPProxyServer *kcpproxy.Server + CoreSharedInformerFactory coreinformers.SharedInformerFactory + CompletedConfig CompletedConfig + UserGateController *usergating.Controller +} + +func NewServer(ctx context.Context, c CompletedConfig) (*Server, error) { + kcpServer, err := kcpproxy.NewServer(ctx, *c.KCPConfig) + if err != nil { + return nil, err + } + + s := &Server{ + KCPProxyServer: kcpServer, + CompletedConfig: c, + } + + if c.Options.UserGate.Enabled { + // setup the shared informer factory + + // TODO(csams): we could (should?) use some workspace other than root + rootShardCoreInformerClient, err := coreclient.NewForConfig(s.CompletedConfig.KCPConfig.RootShardConfig) + if err != nil { + return s, fmt.Errorf("failed to create client for informers: %w", err) + } + s.CoreSharedInformerFactory = coreinformers.NewSharedInformerFactoryWithOptions(rootShardCoreInformerClient, 30*time.Minute) + + var rules *ruleset.RuleSet + if c.Options.UserGate.DefaultRulesFile != "" { + // load the default rule set from a file passed in options + data, err := os.ReadFile(c.Options.UserGate.DefaultRulesFile) + if err != nil { + return nil, err + } + rules, err = ruleset.FromYAML(data) + if err != nil { + return nil, err + } + } + // create the controller to watch for updates to the usergating secret in root:kcp-system + s.UserGateController = usergating.NewController(ctx, s.CoreSharedInformerFactory, rules) + } + return s, nil +} + +// preparedServer is a private wrapper that enforces a call of PrepareRun() before Run can be invoked. +type preparedServer struct { + *Server + RunKCPProxyServer func(context.Context) error +} + +// PrepareRun is basically a no-op, but we contort a little to get the Run func +// out of the KCP proxy delegate +func (s *Server) PrepareRun(ctx context.Context) (preparedServer, error) { + p, err := s.KCPProxyServer.PrepareRun(ctx) + if err != nil { + return preparedServer{}, err + } + return preparedServer{ + Server: s, + RunKCPProxyServer: p.Run, + }, nil +} + +func (s preparedServer) Run(ctx context.Context) error { + // optionally start the user gate controller and wrap preparedServer.Handler with the + // gate filter + if s.CompletedConfig.Options.UserGate.Enabled { + go s.UserGateController.Start(ctx, 2) + s.CoreSharedInformerFactory.Start(ctx.Done()) + s.CoreSharedInformerFactory.WaitForCacheSync(ctx.Done()) + + unauthorizedHandler := kcpproxyfilters.NewUnauthorizedHandler() + s.KCPProxyServer.Handler = usergating.WithUserGating(s.KCPProxyServer.Handler, unauthorizedHandler, s.UserGateController.GetRuleSet) + } + + // installs the default handler chain around preparedServer.Handler and + // actually starts the server + return s.RunKCPProxyServer(ctx) +} diff --git a/pkg/proxy/usergating/controller.go b/pkg/proxy/usergating/controller.go new file mode 100644 index 0000000..bf8323a --- /dev/null +++ b/pkg/proxy/usergating/controller.go @@ -0,0 +1,200 @@ +/* +Copyright 2022 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package usergating + +import ( + "context" + "fmt" + "sync" + "time" + + kcpcache "github.com/kcp-dev/apimachinery/pkg/cache" + "github.com/kcp-dev/client-go/informers" + tenancyv1alpha1 "github.com/kcp-dev/kcp/pkg/apis/tenancy/v1alpha1" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" + + "github.com/redhat-cps/front-proxy/pkg/proxy/usergating/ruleset" +) + +const ( + controllerName = "kcp-user-gating" + resyncPeriod = 10 * time.Minute + + UserGateNamespace = "kcp-system" + UserGateResource = "usergate.kcp.dev" + RuleKey = "ruleset.yaml" +) + +type SecretGetter func(string) (*v1.Secret, error) + +// Controller maintains a set of rules configured from a secret in +// root:kcp-system that determine which users can use the service. +type Controller struct { + queue workqueue.RateLimitingInterface + + GetSecret SecretGetter + + lock sync.RWMutex + Rules *ruleset.RuleSet + + DefaultRules *ruleset.RuleSet +} + +// NewController creates a controller that watches a secret named +// usergate.kcp.dev in root:kcp-system. The secret contains yaml under the key +// "usergate.yaml" that describes which users are allowed or denied access to +// the system. +func NewController(ctx context.Context, versionedInformers informers.SharedInformerFactory, defaultRules *ruleset.RuleSet) *Controller { + queue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), controllerName) + secretInformer := versionedInformers.Core().V1().Secrets().Cluster(tenancyv1alpha1.RootCluster) + + c := &Controller{ + queue: queue, + GetSecret: secretInformer.Lister().Secrets(UserGateNamespace).Get, + DefaultRules: defaultRules, + } + + secretInformer.Informer().AddEventHandlerWithResyncPeriod( + cache.FilteringResourceEventHandler{ + FilterFunc: func(obj interface{}) bool { + if secret, ok := obj.(v1.Secret); ok { + return secret.Name == UserGateResource + } + return false + }, + Handler: cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + c.enqueueSecret(ctx, obj) + }, + UpdateFunc: func(old, obj interface{}) { + c.enqueueSecret(ctx, obj) + }, + DeleteFunc: func(obj interface{}) { + c.lock.Lock() + defer c.lock.Unlock() + c.Rules = nil + }, + }, + }, + resyncPeriod) + return c +} + +// GetRuleSet returns the parsed rules that determine whether a user +// is allowed through the gate. +func (c *Controller) GetRuleSet() (*ruleset.RuleSet, bool) { + c.lock.RLock() + defer c.lock.RUnlock() + if c.Rules != nil { + + // TODO(csams): This is unnecessarily inefficient since the rule set should change + // infrequently compared to the number of requests. Consider refactoring + // to a callback pattern that doesn't require the copy and still hides + // the lock handling. + clone := c.Rules.Clone() + return &clone, true + } + if c.DefaultRules != nil { + return c.DefaultRules, true + } + return &ruleset.RuleSet{}, false +} + +func (c *Controller) enqueueSecret(ctx context.Context, obj interface{}) { + key, err := kcpcache.DeletionHandlingMetaClusterNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + return + } + + c.queue.Add(key) +} + +// Start the controller. +func (c *Controller) Start(ctx context.Context, numThreads int) { + defer runtime.HandleCrash() + + logger := klog.FromContext(ctx).WithValues("controller", controllerName) + logger.Info("Starting controller") + defer logger.Info("Shutting down controller") + + for i := 0; i < numThreads; i++ { + go wait.UntilWithContext(ctx, c.startWorker, time.Second) + } + + <-ctx.Done() +} +func (c *Controller) startWorker(ctx context.Context) { + for c.processNextWorkItem(ctx) { + } +} + +func (c *Controller) processNextWorkItem(ctx context.Context) bool { + // Wait until there is a new item in the working queue + k, quit := c.queue.Get() + if quit { + return false + } + key := k.(string) + + // No matter what, tell the queue we're done with this key, to unblock + // other workers. + defer c.queue.Done(key) + + if err := c.process(ctx, key); err != nil { + runtime.HandleError(fmt.Errorf("%q controller failed to sync %q, err: %w", controllerName, key, err)) + c.queue.AddRateLimited(key) + return true + } + c.queue.Forget(key) + return true +} + +func (c *Controller) process(ctx context.Context, key string) error { + logger := klog.FromContext(ctx) + logger.WithValues("key", key).Info("updating usergate secret") + + secret, err := c.GetSecret(key) + if err != nil { + if errors.IsNotFound(err) { + c.lock.Lock() + defer c.lock.Unlock() + c.Rules = nil + + return nil + } + return err + } + + rules, err := ruleset.FromYAML(secret.Data[RuleKey]) + if err != nil { + return err + } + + c.lock.Lock() + defer c.lock.Unlock() + c.Rules = rules + + return nil +} diff --git a/pkg/proxy/usergating/handler.go b/pkg/proxy/usergating/handler.go new file mode 100644 index 0000000..12c74f4 --- /dev/null +++ b/pkg/proxy/usergating/handler.go @@ -0,0 +1,69 @@ +/* +Copyright 2022 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package usergating + +import ( + "context" + "net/http" + + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/klog/v2" + + "github.com/redhat-cps/front-proxy/pkg/proxy/usergating/ruleset" +) + +type UserGate struct { + GetRuleSet ruleset.RuleSetGetter +} + +func WithUserGating(delegate http.Handler, failed http.Handler, getRuleSet ruleset.RuleSetGetter) http.Handler { + gate := &UserGate{ + GetRuleSet: getRuleSet, + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + u, ok := request.UserFrom(ctx) + if !ok { + delegate.ServeHTTP(w, r) + return + } + + decision := gate.Authorize(ctx, u) + if decision != authorizer.DecisionDeny { + delegate.ServeHTTP(w, r) + return + } + + failed.ServeHTTP(w, r) + }) +} + +// Authorize whether the user is allowed to make the request +func (a *UserGate) Authorize(ctx context.Context, info user.Info) authorizer.Decision { + logger := klog.FromContext(ctx).WithValues("component", "user-gating") + logger.V(1).Info("checking auth", "user", info.GetName()) + + if rules, found := a.GetRuleSet(); found { + if rules.UserNameIsAllowed(info.GetName()) { + return authorizer.DecisionAllow + } + return authorizer.DecisionDeny + } + return authorizer.DecisionNoOpinion +} diff --git a/pkg/proxy/usergating/ruleset/ruleset.go b/pkg/proxy/usergating/ruleset/ruleset.go new file mode 100644 index 0000000..fc52547 --- /dev/null +++ b/pkg/proxy/usergating/ruleset/ruleset.go @@ -0,0 +1,106 @@ +/* +Copyright 2022 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package ruleset + +import ( + "fmt" + "regexp" + + "sigs.k8s.io/yaml" +) + +type RuleSetGetter func() (*RuleSet, bool) + +type matchFunc func(string) bool + +type rawRuleSet struct { + DeniedUserNames []string `json:"denied_user_names,omitempty"` + AllowedUserNames []string `json:"allowed_user_names,omitempty"` +} + +// RuleSet holds rules for determining whether a user is authorized to perform +// some action. Rules come in two types: deny and allow. Each rule's +// raw form is a regular expression string. A RuleSet holds the corresponding +// rexexp string matching functions. +type RuleSet struct { + denyMatchers []matchFunc + allowMatchers []matchFunc +} + +// UserNameIsAllowed applies a RuleSet to the given name to determine whether +// the user is authorized perform some action. Deny rules are applied first, and +// a matching user name is immediately denied. Allow rules are applied next, and +// only a matching user name is allowed. +func (rs *RuleSet) UserNameIsAllowed(name string) bool { + // deny rules get priority + for _, denied := range rs.denyMatchers { + if denied(name) { + return false + } + } + + // only allow requests with user names that match to continue + for _, allowed := range rs.allowMatchers { + if allowed(name) { + return true + } + } + return false +} + +// FromYAML populates a RuleSet from yaml data +func FromYAML(data []byte) (*RuleSet, error) { + var rawRules rawRuleSet + if err := yaml.Unmarshal(data, &rawRules); err != nil { + return nil, fmt.Errorf("failed to unmarshal gating rules: %w", err) + } + + rules := RuleSet{} + + // populate disallow patterns + for _, s := range rawRules.DeniedUserNames { + r, err := regexp.Compile(s) + if err != nil { + return nil, err + } + rules.denyMatchers = append(rules.denyMatchers, r.MatchString) + } + + // populate allow patterns + for _, s := range rawRules.AllowedUserNames { + r, err := regexp.Compile(s) + if err != nil { + return nil, err + } + rules.allowMatchers = append(rules.allowMatchers, r.MatchString) + } + + return &rules, nil +} + +func (rs *RuleSet) Clone() RuleSet { + d := make([]matchFunc, len(rs.denyMatchers)) + copy(d, rs.denyMatchers) + + a := make([]matchFunc, len(rs.allowMatchers)) + copy(a, rs.allowMatchers) + + return RuleSet{ + allowMatchers: a, + denyMatchers: d, + } +} diff --git a/pkg/proxy/usergating/ruleset/ruleset_test.go b/pkg/proxy/usergating/ruleset/ruleset_test.go new file mode 100644 index 0000000..4f6950e --- /dev/null +++ b/pkg/proxy/usergating/ruleset/ruleset_test.go @@ -0,0 +1,56 @@ +package ruleset + +import ( + "testing" +) + +const ( + doc = ` +denied_user_names: + - "@hack.er$" + - "hacker@doesnoevil.com" + +allowed_user_names: + - "@doesnoevil.com$" + - "^user@example.com$" + - "^user@hack.er" +` +) + +func TestRuleSet(t *testing.T) { + rules, err := FromYAML([]byte(doc)) + + if err != nil { + t.Error(err) + } + + // should allow: not denied and username is exact match + if !rules.UserNameIsAllowed("user@example.com") { + t.Fail() + } + + // should deny: user name is not explicitly allowed + if rules.UserNameIsAllowed("user@example.com.foo") { + t.Fail() + } + + // should deny: user name is not explicitly allowed + if rules.UserNameIsAllowed("some+user@example.com") { + t.Fail() + } + + // should deny: explicit denials take precedence even if user is explicitly allowed + if rules.UserNameIsAllowed("user@hack.er") { + t.Fail() + } + + // should allow: user matches allow rule and does not match any deny rule + if !rules.UserNameIsAllowed("you@doesnoevil.com") { + t.Fail() + } + + // should deny: user matches a deny rule + if rules.UserNameIsAllowed("hacker@doesnoevil.com") { + t.Fail() + } +}