Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added authz-casbin plugin and doc and tests for it #4710

Merged
merged 16 commits into from
Aug 6, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ jobs:
tar zxvf ${{ steps.branch_env.outputs.fullname }}

- name: Linux Get dependencies
run: sudo apt install -y cpanminus build-essential libncurses5-dev libreadline-dev libssl-dev perl
run: sudo apt install -y cpanminus build-essential libncurses5-dev libreadline-dev libssl-dev perl libpcre3 libpcre3-dev
Yiyiyimu marked this conversation as resolved.
Show resolved Hide resolved

- name: Linux Before install
run: sudo ./ci/${{ matrix.os_name }}_runner.sh before_install
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/chaos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
bash ./t/chaos/utils/setup_chaos_utils.sh start_minikube
wget https://raw.githubusercontent.com/apache/apisix-docker/master/alpine-local/Dockerfile
mkdir logs
sudo apt install -y libpcre3 libpcre3-dev
docker build -t apache/apisix:alpine-local --build-arg APISIX_PATH=. -f Dockerfile .
minikube cache add apache/apisix:alpine-local -v 7 --alsologtostderr

Expand Down
119 changes: 119 additions & 0 deletions apisix/plugins/authz-casbin.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You 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.
--

local casbin = require("casbin")
local core = require("apisix.core")
local plugin = require("apisix.plugin")
local ngx = ngx
local get_headers = ngx.req.get_headers
local lrucache = core.lrucache.new({
ttl = 300, count = 32
})

local plugin_name = "authz-casbin"

local schema = {
type = "object",
properties = {
model_path = { type = "string" },
policy_path = { type = "string" },
username = { type = "string"}
},
required = {"model_path", "policy_path", "username"},
additionalProperties = false
}

local metadata_schema = {
type = "object",
properties = {
model = {type = "string"},
policy = {type = "string"},
},
required = {"model", "policy"},
additionalProperties = false
}

local _M = {
version = 0.1,
priority = 2560,
name = plugin_name,
schema = schema,
metadata_schema = metadata_schema
}

function _M.check_schema(conf, schema_type)
if schema_type == core.schema.TYPE_METADATA then
return core.schema.check(metadata_schema, conf)
end
local ok, err = core.schema.check(schema, conf)
if ok then
return true
else
local metadata = plugin.plugin_metadata(plugin_name)
if metadata and metadata.value.model and metadata.value.policy and conf.username then
rushitote marked this conversation as resolved.
Show resolved Hide resolved
return true
end
end
return false, err
end


local function new_enforcer(conf)
local model_path = conf.model_path
local policy_path = conf.policy_path

local e

if model_path and policy_path then
e = casbin:new(model_path, policy_path)
e.type = "file"
end

local metadata = plugin.plugin_metadata(plugin_name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO, it surprises me that the enforcer from metadata can override the one from the route. Normally, the route level configuration can override the global one.

if metadata and metadata.value.model and metadata.value.policy and not e then
local model = metadata.value.model
local policy = metadata.value.policy
e = casbin:newEnforcerFromText(model, policy)
e.type = "metadata"
end

return e
end


function _M.rewrite(conf)
rushitote marked this conversation as resolved.
Show resolved Hide resolved
-- creates an enforcer when request sent for the first time

local metadata = plugin.plugin_metadata(plugin_name)
local casbin_enforcer = lrucache(plugin_name, metadata.modifiedIndex, new_enforcer, conf)

local path = ngx.var.request_uri
rushitote marked this conversation as resolved.
Show resolved Hide resolved
local method = ngx.var.request_method
rushitote marked this conversation as resolved.
Show resolved Hide resolved
local username = get_headers()[conf.username]
rushitote marked this conversation as resolved.
Show resolved Hide resolved
if not username then username = "anonymous" end

if path and method and username then
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to check the existence for these three variables if we use them in the HTTP sub-system.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, right - this is redundant here.

if not casbin_enforcer:enforce(username, path, method) then
return 403, {message = "Access Denied"}
end
else
return 403, {message = "Access Denied"}
end
end


return _M
2 changes: 1 addition & 1 deletion ci/centos7-ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ install_dependencies() {

# install openresty to make apisix's rpm test work
yum install -y yum-utils && yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
yum install -y openresty openresty-debug openresty-openssl111-debug-devel
yum install -y openresty openresty-debug openresty-openssl111-debug-devel pcre pcre-devel

# install luarocks
./utils/linux-install-luarocks.sh
Expand Down
1 change: 1 addition & 0 deletions conf/config-default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ plugins: # plugin list (sorted by priority)
- uri-blocker # priority: 2900
- request-validation # priority: 2800
- openid-connect # priority: 2599
- authz-casbin # priority: 2560
- wolf-rbac # priority: 2555
- hmac-auth # priority: 2530
- basic-auth # priority: 2520
Expand Down
205 changes: 205 additions & 0 deletions docs/en/latest/plugins/authz-casbin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
---
title: authz-casbin
---

<!--
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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.
#
-->

## Summary

- [**Name**](#name)
- [**Attributes**](#attributes)
- [**Metadata**](#metadata)
- [**How To Enable**](#how-to-enable)
- [**Test Plugin**](#test-plugin)
- [**Disable Plugin**](#disable-plugin)
- [**Examples**](#examples)

## Name

`authz-casbin` is an authorization plugin based on [Lua Casbin](https://github.com/casbin/lua-casbin/). This plugin supports powerful authorization scenarios based on various access control models.

For detailed documentation on how to create model and policy, refer [Casbin](https://casbin.org/docs/en/supported-models).

## Attributes

| Name | Type | Requirement | Default | Valid | Description |
| ----------- | ------ | ----------- | ------- | ----- | ------------------------------------------------------------ |
| model_path | string | required | | | The path of the Casbin model configuration file. |
| policy_path | string | required | | | The path of the Casbin policy file. |
| username | string | required | | | The header you will be using in request to pass the username (subject). |

**NOTE**: You must either specify both the `model_path` and `policy_path` in plugin config or specify both the `model` and `policy` in the plugin metadata for the schema to be valid.

## Metadata
tokers marked this conversation as resolved.
Show resolved Hide resolved

| Name | Type | Requirement | Default | Valid | Description |
| ----------- | ------ | ----------- | ------- | ----- | ---------------------------------------------------------------------- |
| model | string | required | | | The Casbin model configuration in text format. |
| policy | string | required | | | The Casbin policy in text format. |

## How To Enable

You can enable the plugin on any route either by using the model/policy file paths or directly using the model/policy text.

### By using file paths

```shell
curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"plugins": {
"authz-casbin": {
"model_path": "/path/to/model.conf",
"policy_path": "/path/to/policy.csv",
"username": "user"
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/*"
}'
```

This will create a Casbin enforcer from the model and policy files at your first request.

### By using model/policy text
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please specify the precedence between this one and the path way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I will make it more clear there.


First, send a `PUT` request to add the model and policy text to the plugin's metadata using the Admin API. You can also update the model/policy this way, the plugin will automatically update with this configuration.

```shell
curl http://127.0.0.1:9080/apisix/admin/plugin_metadata/authz-casbin -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -X PUT -d '
{
"model": "[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = (g(r.sub, p.sub) || keyMatch(r.sub, p.sub)) && keyMatch(r.obj, p.obj) && keyMatch(r.act, p.act)",

"policy": "p, *, /, GET
p, admin, *, *
g, alice, admin"
}'
```

Then add this plugin on a route by sending the following request. Note, there is no requirement for model/policy file paths now.

```shell
curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"plugins": {
"authz-casbin": {
"username": "user"
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/*"
}'
```

## Test Plugin

We defined the example model as:

```conf
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = (g(r.sub, p.sub) || keyMatch(r.sub, p.sub)) && keyMatch(r.obj, p.obj) && keyMatch(r.act, p.act)
```

And the example policy as:

```conf
p, *, /, GET
p, admin, *, *
g, alice, admin
```

This means that anyone can access the homepage (`/`) using `GET` request method while only users with admin permissions can access other pages and use other request methods.

For example, here anyone can access the homepage with the GET request method and the request proceeds normally:

```shell
curl -i http://127.0.0.1:9080/ -X GET
```

If some unauthorized user `bob` tries to access any other page, they will get a 403 error:

```shell
curl -i http://127.0.0.1:9080/res -H 'user: bob' -X GET
HTTP/1.1 403 Forbidden
```

But someone with admin permissions like `alice`can access it:

```shell
curl -i http://127.0.0.1:9080/res -H 'user: alice' -X GET
```

## Disable Plugin

Remove the corresponding json configuration in the plugin configuration to disable the `authz-casbin` plugin.
APISIX plugins are hot-reloaded, therefore no need to restart APISIX.

```shell
$ curl http://127.0.0.1:2379/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d value='
{
"methods": ["GET"],
"uri": "/*",
"plugins": {},
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:1980": 1
}
}
}'
```

## Examples

Checkout examples for model and policy conguration [here](https://github.com/casbin/lua-casbin/tree/master/examples).
1 change: 1 addition & 0 deletions rockspec/apisix-master-0.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ dependencies = {
"lua-resty-consul = 0.3-2",
"penlight = 1.9.2-1",
"ext-plugin-proto = 0.1.1",
"casbin = 1.26.0",
}

build = {
Expand Down
2 changes: 1 addition & 1 deletion t/admin/plugins.t
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ __DATA__
--- request
GET /apisix/admin/plugins/list
--- response_body_like eval
qr/\["client-control","ext-plugin-pre-req","zipkin","request-id","fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","ua-restriction","referer-restriction","uri-blocker","request-validation","openid-connect","wolf-rbac","hmac-auth","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","api-breaker","limit-conn","limit-count","limit-req","gzip","server-info","traffic-split","redirect","response-rewrite","grpc-transcode","prometheus","echo","http-logger","sls-logger","tcp-logger","kafka-logger","syslog","udp-logger","example-plugin","serverless-post-function","ext-plugin-post-req"\]/
qr/\["client-control","ext-plugin-pre-req","zipkin","request-id","fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","ua-restriction","referer-restriction","uri-blocker","request-validation","openid-connect","authz-casbin","wolf-rbac","hmac-auth","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","api-breaker","limit-conn","limit-count","limit-req","gzip","server-info","traffic-split","redirect","response-rewrite","grpc-transcode","prometheus","echo","http-logger","sls-logger","tcp-logger","kafka-logger","syslog","udp-logger","example-plugin","serverless-post-function","ext-plugin-post-req"\]/
--- no_error_log
[error]

Expand Down
Loading