Skip to content

Commit 145f1bf

Browse files
authored
Merge pull request #16 from pycasbin/audit-logging
CASBIN_USER_NAME_HEADERS configuration for audit logging with user name
2 parents 440488e + e25143e commit 145f1bf

3 files changed

Lines changed: 44 additions & 17 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ app = Flask(__name__)
3232
app.config['CASBIN_MODEL'] = 'casbinmodel.conf'
3333
# Set headers where owner for enforcement policy should be located
3434
app.config['CASBIN_OWNER_HEADERS'] = {'X-User', 'X-Group'}
35+
# Add User Audit Logging with user name associated to log
36+
# i.e. `[2020-11-10 12:55:06,060] ERROR in casbin_enforcer: Unauthorized attempt: method: GET resource: /api/v1/item by user: janedoe@example.com`
37+
app.config['CASBIN_USER_NAME_HEADERS'] = {'X-User'}
3538
# Set up Casbin Adapter
3639
adapter = FileAdapter('rbac_policy.csv')
3740
casbin_enforcer = CasbinEnforcer(app, adapter)

flask_authz/casbin_enforcer.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def __init__(self, app, adapter, watcher=None):
2828
if watcher:
2929
self.e.set_watcher(watcher)
3030
self._owner_loader = None
31+
self.user_name_headers = app.config.get("CASBIN_USER_NAME_HEADERS", None)
3132

3233
def set_watcher(self, watcher):
3334
"""
@@ -55,6 +56,9 @@ def enforcer(self, func):
5556
def wrapper(*args, **kwargs):
5657
if self.e.watcher and self.e.watcher.should_reload():
5758
self.e.watcher.update_callback()
59+
# String used to hold the owners user name for audit logging
60+
owner_audit = ""
61+
5862
# Check sub, obj act against Casbin polices
5963
self.app.logger.debug(
6064
"Enforce Headers Config: %s\nRequest Headers: %s"
@@ -70,10 +74,10 @@ def wrapper(*args, **kwargs):
7074
owner.strip('"'), uri, request.method
7175
):
7276
return func(*args, **kwargs)
73-
for header in self.app.config.get("CASBIN_OWNER_HEADERS"):
77+
for header in map(str.lower, self.app.config.get("CASBIN_OWNER_HEADERS")):
7478
if header in request.headers:
7579
# Make Authorization Header Parser standard
76-
if header == "Authorization":
80+
if header == "authorization":
7781
# Get Auth Value then decode and parse for owner
7882
try:
7983
owner = authorization_decoder(request.headers.get(header))
@@ -85,6 +89,9 @@ def wrapper(*args, **kwargs):
8589
"decoding is unsupported by flask-casbin at this time"
8690
)
8791
continue
92+
93+
if self.user_name_headers and header in self.user_name_headers:
94+
owner_audit = owner
8895
if self.e.enforce(owner, uri, request.method):
8996
return func(*args, **kwargs)
9097
else:
@@ -97,11 +104,20 @@ def wrapper(*args, **kwargs):
97104
"Enforce against owner: %s header: %s"
98105
% (owner.strip('"'), header)
99106
)
107+
if self.user_name_headers and header in map(str.lower, self.user_name_headers):
108+
owner_audit = owner
100109
if self.e.enforce(
101110
owner.strip('"'), uri, request.method
102111
):
103112
return func(*args, **kwargs)
104113
else:
114+
self.app.logger.error(
115+
"Unauthorized attempt: method: %s resource: %s%s" % (
116+
request.method,
117+
uri,
118+
"" if not self.user_name_headers and owner_audit != "" else " by user: %s" % owner_audit
119+
)
120+
)
105121
return (jsonify({"message": "Unauthorized"}), 401)
106122

107123
return wrapper

tests/test_casbin_enforcer.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,24 +44,32 @@ def update_callback(self):
4444

4545

4646
@pytest.mark.parametrize(
47-
"header, user, method, status",
47+
"header, user, method, status, user_name",
4848
[
49-
("X-User", "alice", "GET", 200),
50-
("X-User", "alice", "POST", 201),
51-
("X-User", "alice", "DELETE", 202),
52-
("X-User", "bob", "GET", 200),
53-
("X-User", "bob", "POST", 401),
54-
("X-User", "bob", "DELETE", 401),
55-
("X-Idp-Groups", "admin", "GET", 401),
56-
("X-Idp-Groups", "users", "GET", 200),
57-
("X-Idp-Groups", "noexist,testnoexist,users", "GET", 200),
58-
("X-Idp-Groups", "noexist testnoexist users", "GET", 200),
59-
("X-Idp-Groups", "noexist, testnoexist, users", "GET", 200),
60-
("Authorization", "Basic Ym9iOnBhc3N3b3Jk", "GET", 200),
61-
("Authorization", "Unsupported Ym9iOnBhc3N3b3Jk", "GET", 401),
49+
("X-User", "alice", "GET", 200, "X-User"),
50+
("X-USER", "alice", "GET", 200, "x-user"),
51+
("x-user", "alice", "GET", 200, "X-USER"),
52+
("X-User", "alice", "GET", 200, "X-USER"),
53+
("X-User", "alice", "GET", 200, "X-Not-A-Header"),
54+
("X-User", "alice", "POST", 201, None),
55+
("X-User", "alice", "DELETE", 202, None),
56+
("X-User", "bob", "GET", 200, None),
57+
("X-User", "bob", "POST", 401, None),
58+
("X-User", "bob", "DELETE", 401, None),
59+
("X-Idp-Groups", "admin", "GET", 401, "X-User"),
60+
("X-Idp-Groups", "users", "GET", 200, None),
61+
("X-Idp-Groups", "noexist,testnoexist,users", "GET", 200, None),
62+
("X-Idp-Groups", "noexist testnoexist users", "GET", 200, None),
63+
("X-Idp-Groups", "noexist, testnoexist, users", "GET", 200, None),
64+
("Authorization", "Basic Ym9iOnBhc3N3b3Jk", "GET", 200, "Authorization"),
65+
("Authorization", "Unsupported Ym9iOnBhc3N3b3Jk", "GET", 401, None),
6266
],
6367
)
64-
def test_enforcer(app_fixture, enforcer, header, user, method, status):
68+
def test_enforcer(app_fixture, enforcer, header, user, method, status, user_name):
69+
# enable auditing with user name
70+
if user_name:
71+
enforcer.user_name_headers = {user_name}
72+
6573
@app_fixture.route("/")
6674
@enforcer.enforcer
6775
def index():

0 commit comments

Comments
 (0)