Skip to content

Commit aaaafb6

Browse files
committed
initial commit
1 parent 16f7791 commit aaaafb6

File tree

8 files changed

+207
-1
lines changed

8 files changed

+207
-1
lines changed

README.md

+22-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,22 @@
1-
# postgresql-watcher
1+
# postgresql-watcher
2+
3+
4+
Casbin role watcher to be used for monitoring updates to casbin policies
5+
## Installation
6+
```bash
7+
pip install postgresql-watcher
8+
```
9+
10+
## Basic Usage Example
11+
### With Flask-authz
12+
```python
13+
from flask_authz import CasbinEnforcer
14+
from postgresql_watcher import PostgresqlWatcher
15+
from flask import Flask
16+
from casbin.persist.adapters import FileAdapter
17+
18+
casbin_enforcer = CasbinEnforcer(app, adapter)
19+
watcher = PostgresqlWatcher(host=HOST, port=PORT, user=USER, password=PASSWORD)
20+
watcher.set_update_callback(casbin_enforcer.e.load_policy())
21+
casbin_enforcer.set_watcher(watcher)
22+
```

dev_requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
black==20.8b1

examples/rbac_model.conf

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[request_definition]
2+
r = sub, obj, act
3+
4+
[policy_definition]
5+
p = sub, obj, act
6+
7+
[role_definition]
8+
g = _, _
9+
10+
[policy_effect]
11+
e = some(where (p.eft == allow))
12+
13+
[matchers]
14+
m = (p.sub == "*" || g(r.sub, p.sub)) && r.obj == p.obj && (p.act == "*" || r.act == p.act)

examples/rbac_policy.csv

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
p,alice,data1,read
2+
p,bob,data2,write
3+
p,data2_admin,data2,read
4+
p,data2_admin,data2,write
5+
g,alice,data2_admin

postgresql_watcher/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .watcher import PostgresqlWatcher

postgresql_watcher/watcher.py

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
from typing import Optional, Callable, Any
2+
from psycopg2 import connect, extensions
3+
from multiprocessing import Process, Pipe, connection
4+
import time
5+
from select import select
6+
7+
POSTGRESQL_CHANNEL_NAME = "casbin_role_watcher"
8+
9+
10+
def casbin_subscription(
11+
process_conn: connection.PipeConnection,
12+
host: str,
13+
user: str,
14+
password: str,
15+
port: Optional[int] = 5432,
16+
delay: Optional[int] = 2,
17+
channel_name: Optional[str] = POSTGRESQL_CHANNEL_NAME,
18+
):
19+
# delay connecting to postgresql (postgresql connection failure)
20+
time.sleep(delay)
21+
conn = connect(host=host, port=port, user=user, password=password)
22+
# Can only receive notifications when not in transaction, set this for easier usage
23+
conn.set_isolation_level(extensions.ISOLATION_LEVEL_AUTOCOMMIT)
24+
curs = conn.cursor()
25+
curs.execute(f"LISTEN {channel_name};")
26+
print("Waiting for casbin policy update")
27+
while True and not curs.closed:
28+
if not select([conn], [], [], 5) == ([], [], []):
29+
print("Casbin policy update identified..")
30+
conn.poll()
31+
while conn.notifies:
32+
notify = conn.notifies.pop(0)
33+
print(f"Notify: {notify.payload}")
34+
process_conn.put(notify.payload)
35+
36+
37+
38+
class PostgresqlWatcher(object):
39+
def __init__(
40+
self,
41+
host: str,
42+
user: str,
43+
password: str,
44+
port: Optional[int] = 5432,
45+
channel_name: Optional[str] = POSTGRESQL_CHANNEL_NAME,
46+
start_process: Optional[bool] = True,
47+
):
48+
self.host = host
49+
self.port = port
50+
self.user = user
51+
self.password = password
52+
self.channel_name = channel_name
53+
self.subscribed_process, self.parent_conn = self.create_subscriber_process(
54+
start_process
55+
)
56+
57+
def create_subscriber_process(
58+
self,
59+
start_process: Optional[bool] = True,
60+
delay: Optional[int] = 2,
61+
):
62+
parent_conn, child_conn = Pipe()
63+
p = Process(
64+
target=casbin_subscription,
65+
args=(
66+
child_conn,
67+
self.host,
68+
self.user,
69+
self.password,
70+
self.port,
71+
delay,
72+
self.channel_name,
73+
),
74+
daemon=True,
75+
)
76+
if start_process:
77+
p.start()
78+
self.should_reload()
79+
return p, parent_conn
80+
81+
def update_callback(self):
82+
print("callback called because casbin role updated")
83+
84+
def set_update_callback(self, fn_name: Any):
85+
print("runtime is set update callback",fn_name)
86+
self.update_callback = fn_name
87+
88+
89+
def update(self):
90+
conn = connect(
91+
host=self.host,
92+
port=self.port,
93+
user=self.user,
94+
password=self.password,
95+
)
96+
# Can only receive notifications when not in transaction, set this for easier usage
97+
conn.set_isolation_level(extensions.ISOLATION_LEVEL_AUTOCOMMIT)
98+
curs = conn.cursor()
99+
curs.execute(
100+
f"NOTIFY {self.channel_name},'casbin policy update at {time.time()}'"
101+
)
102+
conn.close()
103+
return True
104+
105+
def should_reload(self):
106+
try:
107+
if self.parent_conn.poll():
108+
message = self.parent_conn.recv()
109+
print(f"message:{message}")
110+
return True
111+
except EOFError:
112+
print(
113+
"Child casbin-watcher subscribe process has stopped, "
114+
"attempting to recreate the process in 10 seconds..."
115+
)
116+
self.subscribed_process, self.parent_conn = self.create_subscriber_process(
117+
delay=10
118+
)
119+
return False

requirements.txt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
casbin==0.8.4
2+
psycopg2-binary==2.8.6

tests/test_postgresql_watcher.py

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import unittest
2+
from postgresql_watcher import PostgresqlWatcher
3+
from multiprocessing import connection, context
4+
5+
# Warning!!! , Please setup yourself config
6+
HOST = "127.0.0.1"
7+
PORT = 5432
8+
USER = "postgres"
9+
PASSWORD = "123456"
10+
11+
12+
def get_watcher():
13+
pg_watcher = PostgresqlWatcher(host=HOST, port=PORT, user=USER, password=PASSWORD)
14+
return pg_watcher
15+
16+
17+
pg_watcher = get_watcher()
18+
19+
20+
class TestConfig(unittest.TestCase):
21+
def test_pg_watcher_init(self):
22+
assert isinstance(pg_watcher.parent_conn, connection.PipeConnection)
23+
assert isinstance(pg_watcher.subscribed_process, context.Process)
24+
25+
def test_update_pg_watcher(self):
26+
assert pg_watcher.update() is True
27+
28+
def test_not_reload(self):
29+
assert not pg_watcher.should_reload()
30+
31+
def test_default_update_callback(self):
32+
assert pg_watcher.update_callback() is None
33+
34+
def test_add_update_callback(self):
35+
def _test_callback():
36+
pass
37+
38+
pg_watcher.set_update_callback(_test_callback)
39+
assert pg_watcher.update_callback == _test_callback
40+
41+
42+
if __name__ == "__main__":
43+
unittest.main()

0 commit comments

Comments
 (0)