feat: Make a Casbin authorization middleware for Tornado (#2)

diff --git a/.gitignore b/.gitignore
index 68bc17f..2dc53ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -157,4 +157,4 @@
 #  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
 #  and can be added to the global gitignore or merged into this file.  For a more nuclear
 #  option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
+.idea/
diff --git a/README.md b/README.md
index 1c2b3a7..a9b207b 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,98 @@
-# tornado-authz
\ No newline at end of file
+# tornado-authz
+
+[![Discord](https://img.shields.io/discord/1022748306096537660?logo=discord&label=discord&color=5865F2)](https://discord.gg/S5UjpzGZjN)
+
+## Installation
+
+Clone this repo
+
+```bash
+git clone https://github.com/pycasbin/tornado-authz
+```
+
+## Simple Example
+
+```python
+import asyncio
+import tornado
+from casbin import Enforcer
+
+from tornado_authz import CasbinMiddleware
+
+
+# Create a CasbinMiddleware instance with the enforcer
+enforcer = Enforcer("../examples/authz_model.conf", "../examples/authz_policy.csv")
+middleware = CasbinMiddleware(enforcer)
+
+
+class BaseHandler(tornado.web.RequestHandler):
+    def get_current_user(self):
+        user = None
+        if self.get_secure_cookie("user"):
+            user = self.get_secure_cookie("user").decode('utf-8')
+        return user
+
+    def prepare(self):
+        # Check the permission for the current request
+        middleware(self)
+
+
+class MainHandler(BaseHandler):
+    def get(self):
+        self.write("Main Page")
+
+
+class LoginHandler(BaseHandler):
+    def get(self):
+        self.write('<html><body><form action="/login" method="post">'
+                   'Name: <input type="text" name="name">'
+                   '<input type="submit" value="Sign in">'
+                   '</form></body></html>')
+
+    def post(self):
+        self.set_secure_cookie("user", self.get_argument("name"))
+        self.redirect("/dataset1/")
+
+
+class DatasetHandler(BaseHandler):
+    def get(self):
+        self.write("You must be alice to see this.")
+
+
+def make_app():
+    return tornado.web.Application([
+        (r"/", MainHandler),
+        (r"/login", LoginHandler),
+        (r"/dataset1/.*", DatasetHandler),
+    ], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__")
+
+
+async def main():
+    app = make_app()
+    app.listen(8888)
+    await asyncio.Event().wait()
+
+
+if __name__ == "__main__":
+    asyncio.run(main())
+
+```
+
+## Documentation
+
+The authorization determines a request based on ``{subject, object, action}``, which means what ``subject`` can perform
+what ``action`` on what ``object``. In this plugin, the meanings are:
+
+1. ``subject``: the logged-in username
+2. ``object``: the URL path for the web resource like `dataset1/item1`
+3. ``action``: HTTP method like GET, POST, PUT, DELETE, or the high-level actions you defined like "read-file", "write-blog"
+
+For how to write authorization policy and other details, please refer to [the Casbin's documentation](https://casbin.org).
+
+## Getting Help
+
+- [Casbin](https://casbin.org)
+
+## License
+
+This project is under Apache 2.0 License. See the [LICENSE](LICENSE) file for the full license text.
\ No newline at end of file
diff --git a/demo/demo.py b/demo/demo.py
new file mode 100644
index 0000000..1750a8a
--- /dev/null
+++ b/demo/demo.py
@@ -0,0 +1,76 @@
+# Copyright 2024 The Casbin Authors. All Rights Reserved.
+#
+# 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.
+
+import asyncio
+import tornado
+from casbin import Enforcer
+
+from tornado_authz import CasbinMiddleware
+
+
+# Create a CasbinMiddleware instance with the enforcer
+enforcer = Enforcer("../examples/authz_model.conf", "../examples/authz_policy.csv")
+middleware = CasbinMiddleware(enforcer)
+
+
+class BaseHandler(tornado.web.RequestHandler):
+    def get_current_user(self):
+        user = None
+        if self.get_secure_cookie("user"):
+            user = self.get_secure_cookie("user").decode('utf-8')
+        return user
+
+    def prepare(self):
+        # Check the permission for the current request
+        middleware(self)
+
+
+class MainHandler(BaseHandler):
+    def get(self):
+        self.write("Main Page")
+
+
+class LoginHandler(BaseHandler):
+    def get(self):
+        self.write('<html><body><form action="/login" method="post">'
+                   'Name: <input type="text" name="name">'
+                   '<input type="submit" value="Sign in">'
+                   '</form></body></html>')
+
+    def post(self):
+        self.set_secure_cookie("user", self.get_argument("name"))
+        self.redirect("/dataset1/")
+
+
+class DatasetHandler(BaseHandler):
+    def get(self):
+        self.write("You must be alice to see this.")
+
+
+def make_app():
+    return tornado.web.Application([
+        (r"/", MainHandler),
+        (r"/login", LoginHandler),
+        (r"/dataset1/.*", DatasetHandler),
+    ], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__")
+
+
+async def main():
+    app = make_app()
+    app.listen(8888)
+    await asyncio.Event().wait()
+
+
+if __name__ == "__main__":
+    asyncio.run(main())
diff --git a/examples/authz_model.conf b/examples/authz_model.conf
new file mode 100644
index 0000000..eb7589b
--- /dev/null
+++ b/examples/authz_model.conf
@@ -0,0 +1,14 @@
+[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) || p.sub == "*") && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")
\ No newline at end of file
diff --git a/examples/authz_policy.csv b/examples/authz_policy.csv
new file mode 100644
index 0000000..ce17ef3
--- /dev/null
+++ b/examples/authz_policy.csv
@@ -0,0 +1,11 @@
+p, alice, /dataset1/*, GET
+p, alice, /dataset1/resource1, POST
+p, bob, /dataset2/resource1, *
+p, bob, /dataset2/resource2, GET
+p, bob, /dataset2/folder1/*, POST
+p, dataset1_admin, /dataset1/*, *
+p, *, /login, *
+
+p, anonymous, /, GET
+
+g, cathy, dataset1_admin
\ No newline at end of file
diff --git a/tornado_authz/__init__.py b/tornado_authz/__init__.py
new file mode 100644
index 0000000..fd002c4
--- /dev/null
+++ b/tornado_authz/__init__.py
@@ -0,0 +1 @@
+from .middleware import CasbinMiddleware
diff --git a/tornado_authz/middleware.py b/tornado_authz/middleware.py
new file mode 100644
index 0000000..3db576e
--- /dev/null
+++ b/tornado_authz/middleware.py
@@ -0,0 +1,39 @@
+# Copyright 2024 The Casbin Authors. All Rights Reserved.
+#
+# 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.
+
+from tornado.web import RequestHandler
+from casbin import Enforcer
+
+
+class CasbinMiddleware:
+    def __init__(self, enforcer: Enforcer):
+        self.enforcer = enforcer
+
+    def __call__(self, handler: RequestHandler):
+        # Check the permission for each request.
+        if not self.check_permission(handler):
+            # Not authorized, return HTTP 403 error.
+            self.require_permission(handler)
+
+    def check_permission(self, handler: RequestHandler):
+        user = handler.current_user
+        if not user:
+            user = 'anonymous'
+        path = handler.request.uri
+        method = handler.request.method
+        return self.enforcer.enforce(user, path, method)
+
+    @staticmethod
+    def require_permission(handler: RequestHandler):
+        handler.send_error(403)