diff --git a/casbin/model.conf b/casbin/model.conf new file mode 100644 index 000000000000..dd9aba9385ce --- /dev/null +++ b/casbin/model.conf @@ -0,0 +1,15 @@ +# Request definition - this is set by Argo and cannot be changed +[request_definition] +r = sub, obj, act + +# Policy definition - this set by the operator and can be anything you want +[policy_definition] +p = sub, resource, namespace, act + +# Policy effect +[policy_effect] +e = some(where (p.eft == allow)) + +# Matchers +[matchers] +m = r.sub.Sub == p.sub && r.obj.Resource == p.resource && r.obj.Namespace == p.namespace && r.act == p.act \ No newline at end of file diff --git a/casbin/policy.csv b/casbin/policy.csv new file mode 100644 index 000000000000..3f1ff486c0e5 --- /dev/null +++ b/casbin/policy.csv @@ -0,0 +1 @@ +p, anonymous, workflows, argo, list diff --git a/go.mod b/go.mod index 51205995444a..4e0b8d65e09a 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/argoproj/pkg v0.11.0 github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect github.com/blushft/go-diagrams v0.0.0-20201006005127-c78c821223d9 + github.com/casbin/casbin/v2 v2.36.1 github.com/colinmarc/hdfs v1.1.4-0.20180805212432-9746310a4d31 github.com/coreos/go-oidc v2.2.1+incompatible github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect diff --git a/go.sum b/go.sum index 42f1503de96d..c3dc51fcce90 100644 --- a/go.sum +++ b/go.sum @@ -209,6 +209,8 @@ github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx2 github.com/bombsimon/logrusr v1.1.0/go.mod h1:Jq0nHtvxabKE5EMwAAdgTaz7dfWE8C4i11NOltxGQpc= github.com/boynton/repl v0.0.0-20170116235056-348863958e3e/go.mod h1:Crc/GCZ3NXDVCio7Yr0o+SSrytpcFhLmVCIzi0s49t4= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/casbin/casbin/v2 v2.36.1 h1:6b7PQuOEcNR4ZGvQcN82+E1o/n2KMNSUk+np9iryU8A= +github.com/casbin/casbin/v2 v2.36.1/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= @@ -502,6 +504,7 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= diff --git a/server/auth/casbin/foo.go b/server/auth/casbin/foo.go new file mode 100644 index 000000000000..cd83a1e21cec --- /dev/null +++ b/server/auth/casbin/foo.go @@ -0,0 +1,165 @@ +package casbin + +import ( + "context" + "fmt" + "os" + + "github.com/casbin/casbin/v2" + log "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/discovery" + "k8s.io/client-go/rest" + + wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" + workflow "github.com/argoproj/argo-workflows/v3/pkg/client/clientset/versioned" + "github.com/argoproj/argo-workflows/v3/pkg/client/clientset/versioned/typed/workflow/v1alpha1" + authclaims "github.com/argoproj/argo-workflows/v3/server/auth/types" +) + +type Sub struct { // TODO - own file + Sub string +} + +func (s Sub) String() string { // TODO - tests + return s.Sub +} + +type Obj struct { // TODO - own file + Resource string + Namespace string + Name string +} + +func (o Obj) String() string { // TODO - tests + return fmt.Sprintf("%s/%s/%s", o.Resource, o.Namespace, o.Name) +} + +var enforce = func(ctx context.Context, resource, namespace, name, verb string) error { return nil } +var GetClaims func(ctx context.Context) *authclaims.Claims + +func init() { + println("ALEX", "init") // TODO - replace with debug logging + // these files must be mounted at /casbin using configmap volume mount + // TODO if these files are not found then log they are not found, + // and then use a "allow everything" enforcer + e, err := casbin.NewEnforcer("casbin/model.conf", "casbin/policy.csv") + if os.IsNotExist(err) { + log.WithError(err).Info("Casbin RBAC disabled") + return + } + if err != nil { + log.Fatal(err) + } + e.EnableLog(true) // TODO - do we want config for this? e.g. ARGO_CASBIN_ENABLE_LOG=true + enforce = func(ctx context.Context, resource, namespace, name, verb string) error { + claims := GetClaims(ctx) + sub := Sub{Sub: "anonymous"} // TODO -is this the best name for an "anonymous" users? could it conflict with a real user with name "anonymous" + if claims != nil { + sub.Sub = claims.Subject // TODO - needs testing + } + obj := Obj{Resource: resource, Namespace: namespace, Name: name} + act := verb + // TODO - claims exposes + // - email - because many "subjects" are opaque strings - do we want to use that somehow? + // - groups - can we map these to roles somehow? + println("ALEX", "enforce", sub.String(), obj.String(), act) // TODO - replace with debug logging + + if ok, err := e.Enforce(sub, obj, act); err != nil { + return err + } else if !ok { + return status.Error(codes.Unauthenticated, "not allowed") + } + return nil + } +} + +type foo struct { // TODO - new name and own file + x workflow.Interface // TODO - rename to "delegate" +} + +func (c foo) RESTClient() rest.Interface { + panic("not supported") // not all of these need to be implemented, this one is unused for example +} + +func (c foo) ClusterWorkflowTemplates() v1alpha1.ClusterWorkflowTemplateInterface { + panic("implement me") +} + +func (c foo) CronWorkflows(namespace string) v1alpha1.CronWorkflowInterface { + panic("implement me") +} + +type bar struct { // TODO - new name and own file + x v1alpha1.WorkflowInterface // TODO - rename to "delegate" + namespace string +} + +func (c bar) Create(ctx context.Context, workflow *wfv1.Workflow, opts metav1.CreateOptions) (*wfv1.Workflow, error) { + panic("implement me") +} + +func (c bar) Update(ctx context.Context, workflow *wfv1.Workflow, opts metav1.UpdateOptions) (*wfv1.Workflow, error) { + panic("implement me") +} + +func (c bar) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + panic("implement me") +} + +func (c bar) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + panic("implement me") +} + +func (c bar) Get(ctx context.Context, name string, opts metav1.GetOptions) (*wfv1.Workflow, error) { + panic("implement me") +} + +func (c bar) List(ctx context.Context, opts metav1.ListOptions) (*wfv1.WorkflowList, error) { + if err := enforce(ctx, "workflows", c.namespace, "", "list"); err != nil { + return nil, err + } + return c.x.List(ctx, opts) +} + +func (c bar) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + panic("implement me") +} + +func (c bar) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *wfv1.Workflow, err error) { + panic("implement me") +} + +func (c foo) Workflows(namespace string) v1alpha1.WorkflowInterface { + println("ALEX", "Workflows", namespace) + return &bar{c.x.ArgoprojV1alpha1().Workflows(namespace), namespace} +} + +func (c foo) WorkflowEventBindings(namespace string) v1alpha1.WorkflowEventBindingInterface { + panic("implement me") +} + +func (c foo) WorkflowTaskSets(namespace string) v1alpha1.WorkflowTaskSetInterface { + panic("implement me") +} + +func (c foo) WorkflowTemplates(namespace string) v1alpha1.WorkflowTemplateInterface { + panic("implement me") +} + +func (c foo) Discovery() discovery.DiscoveryInterface { + panic("not supported") +} + +func (c foo) ArgoprojV1alpha1() v1alpha1.ArgoprojV1alpha1Interface { + return c +} + +func WrapWorkflowInterface(x workflow.Interface) workflow.Interface { + println("ALEX", "workflowInterface") + return &foo{x} +} diff --git a/server/auth/gatekeeper.go b/server/auth/gatekeeper.go index 827584cd51c1..42dd6f675efd 100644 --- a/server/auth/gatekeeper.go +++ b/server/auth/gatekeeper.go @@ -7,6 +7,8 @@ import ( "sort" "strconv" + "github.com/argoproj/argo-workflows/v3/server/auth/casbin" + "github.com/antonmedv/expr" eventsource "github.com/argoproj/argo-events/pkg/client/eventsource/clientset/versioned" sensor "github.com/argoproj/argo-events/pkg/client/sensor/clientset/versioned" @@ -43,6 +45,10 @@ const ( ClaimsKey ContextKey = "types.Claims" ) +func init() { + casbin.GetClaims = GetClaims // prevent circular dependency +} + //go:generate mockery -name Gatekeeper type Gatekeeper interface { @@ -98,11 +104,11 @@ func (s *gatekeeper) Context(ctx context.Context) (context.Context, error) { if err != nil { return nil, err } - ctx = context.WithValue(ctx, DynamicKey, clients.Dynamic) - ctx = context.WithValue(ctx, WfKey, clients.Workflow) - ctx = context.WithValue(ctx, EventSourceKey, clients.EventSource) - ctx = context.WithValue(ctx, SensorKey, clients.Sensor) - ctx = context.WithValue(ctx, KubeKey, clients.Kubernetes) + ctx = context.WithValue(ctx, DynamicKey, clients.Dynamic) // TODO - wrap + ctx = context.WithValue(ctx, WfKey, casbin.WrapWorkflowInterface(clients.Workflow)) + ctx = context.WithValue(ctx, EventSourceKey, clients.EventSource) // TODO - wrap + ctx = context.WithValue(ctx, SensorKey, clients.Sensor) // TODO - wrap + ctx = context.WithValue(ctx, KubeKey, clients.Kubernetes) // TODO - wrap ctx = context.WithValue(ctx, ClaimsKey, claims) return ctx, nil }