diff --git a/auth/casbin/middleware.go b/auth/casbin/middleware.go new file mode 100644 index 000000000..fdbf6dc31 --- /dev/null +++ b/auth/casbin/middleware.go @@ -0,0 +1,65 @@ +package casbin + +import ( + "context" + "errors" + + stdcasbin "github.com/casbin/casbin" + "github.com/go-kit/kit/endpoint" +) + +type contextKey string + +const ( + // CasbinModelContextKey holds the key to store the access control model + // in context, it can be a path to configuration file or a casbin/model + // Model. + CasbinModelContextKey contextKey = "CasbinModel" + + // CasbinPolicyContextKey holds the key to store the access control policy + // in context, it can be a path to policy file or an implementation of + // casbin/persist Adapter interface. + CasbinPolicyContextKey contextKey = "CasbinPolicy" + + // CasbinEnforcerContextKey holds the key to retrieve the active casbin + // Enforcer. + CasbinEnforcerContextKey contextKey = "CasbinEnforcer" +) + +var ( + // ErrModelContextMissing denotes a casbin model was not passed into + // the parsing of middleware's context. + ErrModelContextMissing = errors.New("CasbinModel is required in context") + + // ErrPolicyContextMissing denotes a casbin policy was not passed into + // the parsing of middleware's context. + ErrPolicyContextMissing = errors.New("CasbinPolicy is required in context") + + // ErrUnauthorized denotes the subject is not authorized to do the action + // intended on the given object, based on the context model and policy. + ErrUnauthorized = errors.New("Unauthorized Access") +) + +// NewEnforcer checks whether the subject is authorized to do the specified +// action on the given object. If a valid access control model and policy +// is given, then the generated casbin Enforcer is stored in the context +// with CasbinEnforcer as the key. +func NewEnforcer( + subject string, object interface{}, action string, +) endpoint.Middleware { + return func(next endpoint.Endpoint) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) ( + response interface{}, err error, + ) { + casbinModel := ctx.Value(CasbinModelContextKey) + casbinPolicy := ctx.Value(CasbinPolicyContextKey) + + enforcer := stdcasbin.NewEnforcer(casbinModel, casbinPolicy) + ctx = context.WithValue(ctx, CasbinEnforcerContextKey, enforcer) + if !enforcer.Enforce(subject, object, action) { + return nil, ErrUnauthorized + } + return next(ctx, request) + } + } +} diff --git a/auth/casbin/middleware_test.go b/auth/casbin/middleware_test.go new file mode 100644 index 000000000..5dbb003df --- /dev/null +++ b/auth/casbin/middleware_test.go @@ -0,0 +1,55 @@ +package casbin + +import ( + "context" + "testing" + + stdcasbin "github.com/casbin/casbin" + fileadapter "github.com/casbin/casbin/persist/file-adapter" +) + +func TestStructBaseContext(t *testing.T) { + e := func(ctx context.Context, i interface{}) (interface{}, error) { return ctx, nil } + + m := stdcasbin.NewModel() + m.AddDef("r", "r", "sub, obj, act") + m.AddDef("p", "p", "sub, obj, act") + m.AddDef("e", "e", "some(where (p.eft == allow))") + m.AddDef("m", "m", "r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)") + + a := fileadapter.NewAdapter("testdata/keymatch_policy.csv") + + ctx := context.WithValue(context.Background(), CasbinModelContextKey, m) + ctx = context.WithValue(ctx, CasbinPolicyContextKey, a) + + // positive case + middleware := NewEnforcer("alice", "/alice_data/resource1", "GET")(e) + ctx1, err := middleware(ctx, struct{}{}) + if err != nil { + t.Fatalf("Enforcer returned error: %s", err) + } + _, ok := ctx1.(context.Context).Value(CasbinEnforcerContextKey).(*stdcasbin.Enforcer) + if !ok { + t.Fatalf("context should contains the active enforcer") + } + + // negative case + middleware = NewEnforcer("alice", "/alice_data/resource2", "POST")(e) + _, err = middleware(ctx, struct{}{}) + if err == nil { + t.Fatalf("Enforcer should return error") + } +} + +func TestFileBaseContext(t *testing.T) { + e := func(ctx context.Context, i interface{}) (interface{}, error) { return ctx, nil } + ctx := context.WithValue(context.Background(), CasbinModelContextKey, "testdata/basic_model.conf") + ctx = context.WithValue(ctx, CasbinPolicyContextKey, "testdata/basic_policy.csv") + + // positive case + middleware := NewEnforcer("alice", "data1", "read")(e) + _, err := middleware(ctx, struct{}{}) + if err != nil { + t.Fatalf("Enforcer returned error: %s", err) + } +} diff --git a/auth/casbin/testdata/basic_model.conf b/auth/casbin/testdata/basic_model.conf new file mode 100644 index 000000000..dc6da8136 --- /dev/null +++ b/auth/casbin/testdata/basic_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/auth/casbin/testdata/basic_policy.csv b/auth/casbin/testdata/basic_policy.csv new file mode 100644 index 000000000..57aaa9760 --- /dev/null +++ b/auth/casbin/testdata/basic_policy.csv @@ -0,0 +1,2 @@ +p, alice, data1, read +p, bob, data2, write \ No newline at end of file diff --git a/auth/casbin/testdata/keymatch_policy.csv b/auth/casbin/testdata/keymatch_policy.csv new file mode 100644 index 000000000..d6e9b7d40 --- /dev/null +++ b/auth/casbin/testdata/keymatch_policy.csv @@ -0,0 +1,7 @@ +p, alice, /alice_data/*, GET +p, alice, /alice_data/resource1, POST + +p, bob, /alice_data/resource2, GET +p, bob, /bob_data/*, POST + +p, cathy, /cathy_data, (GET)|(POST) \ No newline at end of file