From f824d2ea0ff67cfc24ed1930e662bc5322b089a9 Mon Sep 17 00:00:00 2001 From: JohnDamilola Date: Wed, 20 Sep 2023 15:08:22 -0400 Subject: [PATCH 01/12] Implement authentication and blacklisting rules --- extension/writing-process/src/background.js | 12 ++- .../learning_observer/auth/events.py | 79 ++++++++++++++++++- .../incoming_student_event.py | 21 +++-- 3 files changed, 101 insertions(+), 11 deletions(-) diff --git a/extension/writing-process/src/background.js b/extension/writing-process/src/background.js index 75bc9de72..a537c5394 100644 --- a/extension/writing-process/src/background.js +++ b/extension/writing-process/src/background.js @@ -9,7 +9,7 @@ var RAW_DEBUG = false; /* This variable must be manually updated to specify the server that * the data will be sent to. */ -var WEBSOCKET_SERVER_URL = "wss://learning-observer.org/wsapi/in/" +var WEBSOCKET_SERVER_URL = "wss://learning-observer.org/wsapi/in/" import { googledocs_id_from_url } from './writing_common'; /* @@ -85,6 +85,7 @@ function websocket_logger(server) { var socket; var state = new Set() var queue = []; + var authStatus; function new_websocket() { socket = new WebSocket(server); @@ -189,8 +190,13 @@ function websocket_logger(server) { } return function(data) { - queue.push(data); - dequeue(); + switch (authStatus) { + case "deny": // don't update/empty the queue if authStatus is "deny" + break; + default: + queue.push(data); + dequeue(); + } } } diff --git a/learning_observer/learning_observer/auth/events.py b/learning_observer/learning_observer/auth/events.py index df7be787b..a4dd7761b 100644 --- a/learning_observer/learning_observer/auth/events.py +++ b/learning_observer/learning_observer/auth/events.py @@ -24,6 +24,8 @@ import urllib.parse import secrets import sys +import re +import json import aiohttp_session import aiohttp.web @@ -37,6 +39,9 @@ from learning_observer.log_event import debug_log AUTH_METHODS = {} +ALLOW = "allow" +DENY = "deny" +DENY_FOR_TWO_DAYS = "deny_for_two_days" def register_event_auth(name): @@ -201,7 +206,6 @@ async def local_storage_auth(request, headers, first_event, source): 'providence': 'ls' # local storage } - @register_event_auth("chromebook") async def chromebook_auth(request, headers, first_event, source): ''' @@ -230,8 +234,12 @@ async def chromebook_auth(request, headers, first_event, source): if untrusted_google_id is None: return False + payload_for_validation = authdata.get('chrome_identity', {}) + + auth_response = authenticate_payload(payload_for_validation) gc_uid = learning_observer.auth.utils.google_id_to_user_id(untrusted_google_id) return { + 'auth_response': auth_response, 'sec': auth, 'user_id': gc_uid, 'safe_user_id': gc_uid, @@ -321,6 +329,11 @@ async def authenticate(request, headers, first_event, source): for auth_method in learning_observer.settings.settings['event_auth']: auth_metadata = await AUTH_METHODS[auth_method](request, headers, first_event, source) if auth_metadata: + auth_response = auth_metadata.get('auth_response') + if auth_response and "status_code" in auth_response and auth_response.get("status_code") == 403: + print("Forbidden.") + raise aiohttp.web.HTTPForbidden(reason=json.dumps(auth_response)) + if "safe_user_id" not in auth_metadata: auth_metadata['safe_user_id'] = encode_id( source=auth_metadata["providence"], @@ -331,7 +344,6 @@ async def authenticate(request, headers, first_event, source): print("All authentication methods failed. Unauthorized.") raise aiohttp.web.HTTPUnauthorized() - @learning_observer.prestartup.register_startup_check def check_event_auth_config(): ''' @@ -348,6 +360,69 @@ def check_event_auth_config(): list(AUTH_METHODS.keys()) )) +# Responses for different rule types +RULES_RESPONSES = { + ALLOW: { + "type": ALLOW, + "msg": "Allow events to be sent", + "status_code": 200 + }, + DENY: { + "type": DENY, + "msg": "Deny events from being sent", + "status_code": 403 + }, + DENY_FOR_TWO_DAYS: { + "type": DENY_FOR_TWO_DAYS, + "msg": "Deny events from being sent for two days", + "status_code": 403 + } +} + +# Patterns to match against for different rule types +RULES_PATTERNS = { + DENY: [ + { + "field": "email", + "patterns": ["^.*@ncsu.edu"] + }, + { + "field": "google_id", + "patterns": ["1234"] + } + ], + DENY_FOR_TWO_DAYS: [ + { + "field": "email", + "patterns": ["^.*@ncsu.edu"] + } + ] +} + +# Priority order of rule types for sorting +RULE_TYPES_BY_PRIORITIES = [DENY, DENY_FOR_TWO_DAYS] +def authenticate_payload(payload): + failed_rule_types = [] # A list to store rule types that the payload fails to comply with + for rule_type, rules in RULES_PATTERNS.items(): + for rule in rules: + field = rule["field"] # Get the field to be looked up in the payload + patterns = rule["patterns"] # Get the patterns to match against for the payload value of the field + value = payload.get(field) # Get the value of the field from the payload + + if value: + for pattern in patterns: + # If there is a pattern match, add the rule type to the failed list + if re.match(pattern, value): + failed_rule_types.append(rule_type) + + # Sort the failed rule types based on their priority order + sorted_failed_rule_types = sorted(failed_rule_types, key=RULE_TYPES_BY_PRIORITIES.index) + + # Determine the response key based on the highest priority failed rule, or 'allow' if no rule failed + response_key = sorted_failed_rule_types[0] if sorted_failed_rule_types else ALLOW + + # Return the appropriate response based on the response key + return RULES_RESPONSES[response_key] if __name__ == "__main__": import doctest diff --git a/learning_observer/learning_observer/incoming_student_event.py b/learning_observer/learning_observer/incoming_student_event.py index efcd66a4a..c85f2f51e 100644 --- a/learning_observer/learning_observer/incoming_student_event.py +++ b/learning_observer/learning_observer/incoming_student_event.py @@ -319,6 +319,7 @@ async def incoming_websocket_handler(request): INIT_PIPELINE = settings.settings.get("init_pipeline", True) json_msg = None + ws.send_str('Acknowledgment received') if INIT_PIPELINE: async for msg in ws: debug_log("Auth", msg.data) @@ -328,6 +329,7 @@ async def incoming_websocket_handler(request): print("Bad message:", msg) raise header_events.append(json_msg) + if json_msg["event"] == "metadata_finished": break else: @@ -345,12 +347,19 @@ async def incoming_websocket_handler(request): event_metadata['source'] = first_event['source'] # We authenticate the student - event_metadata['auth'] = await learning_observer.auth.events.authenticate( - request=request, - headers=header_events, - first_event=first_event, # This is obsolete - source=json_msg['source'] - ) + try: + event_metadata['auth'] = await learning_observer.auth.events.authenticate( + request=request, + headers=header_events, + first_event=first_event, # This is obsolete + source=json_msg['source'] + ) + except aiohttp.web.HTTPForbidden as e: + auth_response = json.loads(e.reason) + # Send the status message to the client (chrome extension) + await ws.send_json({"status": auth_response.get('type')}) + debug_log(auth_response.get('msg')) + return ws print(event_metadata['auth']) From fbf260a5f6c842202d69f68ebfc51292c0233169 Mon Sep 17 00:00:00 2001 From: JohnDamilola Date: Wed, 27 Sep 2023 00:30:23 -0400 Subject: [PATCH 02/12] Refactor blacklisting settings to a separate file --- .../auth/blacklisting_settings.py | 46 +++++++++++ .../learning_observer/auth/events.py | 77 +++++++------------ 2 files changed, 75 insertions(+), 48 deletions(-) create mode 100644 learning_observer/learning_observer/auth/blacklisting_settings.py diff --git a/learning_observer/learning_observer/auth/blacklisting_settings.py b/learning_observer/learning_observer/auth/blacklisting_settings.py new file mode 100644 index 000000000..a8a05edaa --- /dev/null +++ b/learning_observer/learning_observer/auth/blacklisting_settings.py @@ -0,0 +1,46 @@ +AUTH_METHODS = {} +ALLOW = "allow" +DENY = "deny" +DENY_FOR_TWO_DAYS = "deny_for_two_days" + +# Responses for different rule types +RULES_RESPONSES = { + ALLOW: { + "type": ALLOW, + "msg": "Allow events to be sent", + "status_code": 200 + }, + DENY: { + "type": DENY, + "msg": "Deny events from being sent", + "status_code": 403 + }, + DENY_FOR_TWO_DAYS: { + "type": DENY_FOR_TWO_DAYS, + "msg": "Deny events from being sent temporarily for two days", + "status_code": 403 + } +} + +# Patterns to match against for different rule types +RULES_PATTERNS = { + DENY: [ + { + "field": "email", + "patterns": ["^.*@ncsu.edu"] + }, + { + "field": "google_id", + "patterns": ["1234"] + } + ], + DENY_FOR_TWO_DAYS: [ + { + "field": "email", + "patterns": ["^.*@ncsu.edu"] + } + ] +} + +# Priority order of rule types for sorting +RULE_TYPES_BY_PRIORITIES = [DENY, DENY_FOR_TWO_DAYS] \ No newline at end of file diff --git a/learning_observer/learning_observer/auth/events.py b/learning_observer/learning_observer/auth/events.py index a4dd7761b..7a4207268 100644 --- a/learning_observer/learning_observer/auth/events.py +++ b/learning_observer/learning_observer/auth/events.py @@ -38,6 +38,8 @@ from learning_observer.log_event import debug_log +from learning_observer.auth.blacklisting_settings import RULES_PATTERNS, RULE_TYPES_BY_PRIORITIES, RULES_RESPONSES + AUTH_METHODS = {} ALLOW = "allow" DENY = "deny" @@ -206,6 +208,7 @@ async def local_storage_auth(request, headers, first_event, source): 'providence': 'ls' # local storage } + @register_event_auth("chromebook") async def chromebook_auth(request, headers, first_event, source): ''' @@ -235,7 +238,6 @@ async def chromebook_auth(request, headers, first_event, source): return False payload_for_validation = authdata.get('chrome_identity', {}) - auth_response = authenticate_payload(payload_for_validation) gc_uid = learning_observer.auth.utils.google_id_to_user_id(untrusted_google_id) return { @@ -333,7 +335,7 @@ async def authenticate(request, headers, first_event, source): if auth_response and "status_code" in auth_response and auth_response.get("status_code") == 403: print("Forbidden.") raise aiohttp.web.HTTPForbidden(reason=json.dumps(auth_response)) - + if "safe_user_id" not in auth_metadata: auth_metadata['safe_user_id'] = encode_id( source=auth_metadata["providence"], @@ -344,6 +346,7 @@ async def authenticate(request, headers, first_event, source): print("All authentication methods failed. Unauthorized.") raise aiohttp.web.HTTPUnauthorized() + @learning_observer.prestartup.register_startup_check def check_event_auth_config(): ''' @@ -360,61 +363,38 @@ def check_event_auth_config(): list(AUTH_METHODS.keys()) )) -# Responses for different rule types -RULES_RESPONSES = { - ALLOW: { - "type": ALLOW, - "msg": "Allow events to be sent", - "status_code": 200 - }, - DENY: { - "type": DENY, - "msg": "Deny events from being sent", - "status_code": 403 - }, - DENY_FOR_TWO_DAYS: { - "type": DENY_FOR_TWO_DAYS, - "msg": "Deny events from being sent for two days", - "status_code": 403 - } -} - -# Patterns to match against for different rule types -RULES_PATTERNS = { - DENY: [ - { - "field": "email", - "patterns": ["^.*@ncsu.edu"] - }, - { - "field": "google_id", - "patterns": ["1234"] - } - ], - DENY_FOR_TWO_DAYS: [ - { - "field": "email", - "patterns": ["^.*@ncsu.edu"] - } - ] -} -# Priority order of rule types for sorting -RULE_TYPES_BY_PRIORITIES = [DENY, DENY_FOR_TWO_DAYS] def authenticate_payload(payload): - failed_rule_types = [] # A list to store rule types that the payload fails to comply with + ''' + Evaluate a payload against a set of rules and determine the authentication response. + + This function iterates through a list of rules for various fields in the payload. + For each field, it checks if the value matches any of the specified patterns. + If a match is found, the associated rule type is added to a list of failed rule types. + The failed rule types are then sorted by priority, and the highest priority failed rule + determines the authentication response. + + If no rules fail, the function returns the 'allow' response. + + Args: + payload (dict): The payload containing data to be authenticated. + + Returns: + str: The authentication response based on the payload and rule evaluation. + ''' + failed_rule_types = [] # A list to store rule types that the payload fails to comply with for rule_type, rules in RULES_PATTERNS.items(): for rule in rules: - field = rule["field"] # Get the field to be looked up in the payload - patterns = rule["patterns"] # Get the patterns to match against for the payload value of the field - value = payload.get(field) # Get the value of the field from the payload - + field = rule["field"] # Get the field to be looked up in the payload + patterns = rule["patterns"] # Get the patterns to match against for the payload value of the field + value = payload.get(field) # Get the value of the field from the payload + if value: for pattern in patterns: # If there is a pattern match, add the rule type to the failed list if re.match(pattern, value): failed_rule_types.append(rule_type) - + # Sort the failed rule types based on their priority order sorted_failed_rule_types = sorted(failed_rule_types, key=RULE_TYPES_BY_PRIORITIES.index) @@ -424,6 +404,7 @@ def authenticate_payload(payload): # Return the appropriate response based on the response key return RULES_RESPONSES[response_key] + if __name__ == "__main__": import doctest print("Running tests") From 14a5210e99eedd9c1f0750451a5c1cfe4a11025d Mon Sep 17 00:00:00 2001 From: JohnDamilola Date: Wed, 27 Sep 2023 01:35:23 -0400 Subject: [PATCH 03/12] Improve logging and close ws on forbidden error --- learning_observer/learning_observer/auth/events.py | 5 +---- .../learning_observer/incoming_student_event.py | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/learning_observer/learning_observer/auth/events.py b/learning_observer/learning_observer/auth/events.py index 7a4207268..3443196d2 100644 --- a/learning_observer/learning_observer/auth/events.py +++ b/learning_observer/learning_observer/auth/events.py @@ -41,9 +41,6 @@ from learning_observer.auth.blacklisting_settings import RULES_PATTERNS, RULE_TYPES_BY_PRIORITIES, RULES_RESPONSES AUTH_METHODS = {} -ALLOW = "allow" -DENY = "deny" -DENY_FOR_TWO_DAYS = "deny_for_two_days" def register_event_auth(name): @@ -333,7 +330,7 @@ async def authenticate(request, headers, first_event, source): if auth_metadata: auth_response = auth_metadata.get('auth_response') if auth_response and "status_code" in auth_response and auth_response.get("status_code") == 403: - print("Forbidden.") + debug_log("Auth Forbidden: the returned response code given the rules is 403") raise aiohttp.web.HTTPForbidden(reason=json.dumps(auth_response)) if "safe_user_id" not in auth_metadata: diff --git a/learning_observer/learning_observer/incoming_student_event.py b/learning_observer/learning_observer/incoming_student_event.py index c85f2f51e..85f927272 100644 --- a/learning_observer/learning_observer/incoming_student_event.py +++ b/learning_observer/learning_observer/incoming_student_event.py @@ -319,7 +319,6 @@ async def incoming_websocket_handler(request): INIT_PIPELINE = settings.settings.get("init_pipeline", True) json_msg = None - ws.send_str('Acknowledgment received') if INIT_PIPELINE: async for msg in ws: debug_log("Auth", msg.data) @@ -359,7 +358,8 @@ async def incoming_websocket_handler(request): # Send the status message to the client (chrome extension) await ws.send_json({"status": auth_response.get('type')}) debug_log(auth_response.get('msg')) - return ws + # Close the web socket connection after an authentication failure + return print(event_metadata['auth']) From 22917313ddd3947fb2ee2ddcb2c6a91f2f0aa0a2 Mon Sep 17 00:00:00 2001 From: JohnDamilola Date: Thu, 28 Sep 2023 00:42:28 -0400 Subject: [PATCH 04/12] Handle blacklisting errors using localstorage --- extension/writing-process/src/background.js | 56 ++++++++++++++++--- .../auth/blacklisting_patterns.yaml | 18 ++++++ 2 files changed, 66 insertions(+), 8 deletions(-) create mode 100644 learning_observer/learning_observer/auth/blacklisting_patterns.yaml diff --git a/extension/writing-process/src/background.js b/extension/writing-process/src/background.js index a537c5394..971a82978 100644 --- a/extension/writing-process/src/background.js +++ b/extension/writing-process/src/background.js @@ -12,6 +12,11 @@ var RAW_DEBUG = false; var WEBSOCKET_SERVER_URL = "wss://learning-observer.org/wsapi/in/" import { googledocs_id_from_url } from './writing_common'; +var ALLOW = "allow" +var DENY = "deny" +var DENY_FOR_TWO_DAYS = "deny_for_two_days" + + /* TODO: FSM @@ -85,7 +90,6 @@ function websocket_logger(server) { var socket; var state = new Set() var queue = []; - var authStatus; function new_websocket() { socket = new WebSocket(server); @@ -97,6 +101,23 @@ function websocket_logger(server) { event = JSON.stringify(event); queue.push(event); }; + socket.onmessage = function(event) { + const jsonData = JSON.parse(event.data); + + if (jsonData && jsonData.status) { + // Store the 'status' value in the local storage. + chrome.storage.local.set({ authStatus: jsonData.status }).then(() => { + console.log("Set the authStatus to localstorage"); + }); + + if (jsonData.status === DENY_FOR_TWO_DAYS) { + // Store the 'timestamp' value in the local storage. + chrome.storage.local.set({ authResponseTimeStamp: jsonData.timestamp }).then(() => { + console.log("Set the authResponseTimeStamp to localstorage"); + }); + } + } + } socket.onclose = function(event) { console.log("Lost connection"); var event = { "issue": "Lost connection", "code": event.code }; @@ -190,13 +211,32 @@ function websocket_logger(server) { } return function(data) { - switch (authStatus) { - case "deny": // don't update/empty the queue if authStatus is "deny" - break; - default: - queue.push(data); - dequeue(); - } + chrome.storage.local.get(["authStatus", "authResponseTimeStamp"]).then((result) => { + const authStatus = result.authStatus + switch (authStatus) { + case DENY: // don't update/empty the queue + break; + case DENY_FOR_TWO_DAYS: // check back after two days to continue updating/emptying the queue + const currentDate = new Date(); + const authResponseDate = new Date(result.authResponseTimeStamp); + + // Calculate the date 2 days after the authResponseDate + const twoDaysAfterAuthResponseDate = new Date(authResponseDate) + .setDate(authResponseDate.getDate() + 2); + + // Compare the date 2 days after the authResponseDate with the current date + if (currentDate >= twoDaysAfterAuthResponseDate) { + queue.push(data); + dequeue(); + } else { + console.log("I am being denied for 2 days") + break; + } + default: + queue.push(data); + dequeue(); + } + }); } } diff --git a/learning_observer/learning_observer/auth/blacklisting_patterns.yaml b/learning_observer/learning_observer/auth/blacklisting_patterns.yaml new file mode 100644 index 000000000..45325f9f9 --- /dev/null +++ b/learning_observer/learning_observer/auth/blacklisting_patterns.yaml @@ -0,0 +1,18 @@ +# This file serves as a configuration file for blacklisting/authentication rules and their associated patterns. +# Each rule type (e.g. DENY, etc.) contains a list of rules defined as follows: +# - field (str): The field to match against in incoming payloads, e.g., 'email', 'google_id', etc. +# - patterns (list of str): An array of regular expression patterns used for matching against the field value. +# +# Curly-braces are used for variables which ought to be filled + +DENY: + - field: email + patterns: + - '^.*@ncsu.edu' + - field: google_id + patterns: + - '1234' +DENY_FOR_TWO_DAYS: + - field: email + patterns: + - '^.*@ncsu.edu' \ No newline at end of file From 1058849ee49e3cec6c38df9e3e9960cb0302f523 Mon Sep 17 00:00:00 2001 From: JohnDamilola Date: Thu, 28 Sep 2023 00:45:10 -0400 Subject: [PATCH 05/12] Add current timestamp to json payload sent to client --- .../learning_observer/incoming_student_event.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/learning_observer/learning_observer/incoming_student_event.py b/learning_observer/learning_observer/incoming_student_event.py index 85f927272..867c117ed 100644 --- a/learning_observer/learning_observer/incoming_student_event.py +++ b/learning_observer/learning_observer/incoming_student_event.py @@ -355,11 +355,13 @@ async def incoming_websocket_handler(request): ) except aiohttp.web.HTTPForbidden as e: auth_response = json.loads(e.reason) - # Send the status message to the client (chrome extension) - await ws.send_json({"status": auth_response.get('type')}) + # Send the status error type and timestamp to the client (chrome extension) + await ws.send_json({ + "status": auth_response.get('type'), + "timestamp": datetime.datetime.utcnow().isoformat() + }) debug_log(auth_response.get('msg')) - # Close the web socket connection after an authentication failure - return + return ws print(event_metadata['auth']) From 820788eedb74cee3c79eca4788f81386c61f9131 Mon Sep 17 00:00:00 2001 From: JohnDamilola Date: Thu, 28 Sep 2023 00:46:03 -0400 Subject: [PATCH 06/12] Add current timestamp to json payload sent to client --- learning_observer/learning_observer/auth/events.py | 1 + learning_observer/learning_observer/incoming_student_event.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/learning_observer/learning_observer/auth/events.py b/learning_observer/learning_observer/auth/events.py index 3443196d2..4f77f74d3 100644 --- a/learning_observer/learning_observer/auth/events.py +++ b/learning_observer/learning_observer/auth/events.py @@ -41,6 +41,7 @@ from learning_observer.auth.blacklisting_settings import RULES_PATTERNS, RULE_TYPES_BY_PRIORITIES, RULES_RESPONSES AUTH_METHODS = {} +ALLOW = "allow" def register_event_auth(name): diff --git a/learning_observer/learning_observer/incoming_student_event.py b/learning_observer/learning_observer/incoming_student_event.py index 867c117ed..9d3103200 100644 --- a/learning_observer/learning_observer/incoming_student_event.py +++ b/learning_observer/learning_observer/incoming_student_event.py @@ -355,7 +355,7 @@ async def incoming_websocket_handler(request): ) except aiohttp.web.HTTPForbidden as e: auth_response = json.loads(e.reason) - # Send the status error type and timestamp to the client (chrome extension) + # Send the status error type and current timestamp to the client (chrome extension) await ws.send_json({ "status": auth_response.get('type'), "timestamp": datetime.datetime.utcnow().isoformat() From 624a04ef21866d9bd081badefe8e7df7fee39f27 Mon Sep 17 00:00:00 2001 From: JohnDamilola Date: Thu, 28 Sep 2023 10:24:51 -0400 Subject: [PATCH 07/12] Add docstring to blacklisting config/settings --- .../auth/blacklisting_settings.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/learning_observer/learning_observer/auth/blacklisting_settings.py b/learning_observer/learning_observer/auth/blacklisting_settings.py index a8a05edaa..4d8132696 100644 --- a/learning_observer/learning_observer/auth/blacklisting_settings.py +++ b/learning_observer/learning_observer/auth/blacklisting_settings.py @@ -1,4 +1,22 @@ -AUTH_METHODS = {} +""" +This file contains constants for blacklisting/authentication rule types, their responses, priorities and patterns. + +RULES_RESPONSES +A dictionary containing responses for different rule types, including: +- type (str) +- message (str) +- status_code (int) + +RULES_PATTERNS +A dictionary containing patterns to match against for different rule types, specifying: +- field (str) +- patterns (arrays of regex patterns) +to apply for rule evaluation. + +RULE_TYPES_BY_PRIORITIES +A list defining the priority order of rule types for sorting. +""" + ALLOW = "allow" DENY = "deny" DENY_FOR_TWO_DAYS = "deny_for_two_days" @@ -43,4 +61,4 @@ } # Priority order of rule types for sorting -RULE_TYPES_BY_PRIORITIES = [DENY, DENY_FOR_TWO_DAYS] \ No newline at end of file +RULE_TYPES_BY_PRIORITIES = [DENY, DENY_FOR_TWO_DAYS] From 674f20a2241ad78ec2c0720a81f6060509d92bf7 Mon Sep 17 00:00:00 2001 From: JohnDamilola Date: Thu, 28 Sep 2023 10:26:00 -0400 Subject: [PATCH 08/12] Add comments and fix linting issues --- learning_observer/learning_observer/auth/events.py | 6 +----- .../learning_observer/incoming_student_event.py | 3 ++- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/learning_observer/learning_observer/auth/events.py b/learning_observer/learning_observer/auth/events.py index 4f77f74d3..f5bf52f5d 100644 --- a/learning_observer/learning_observer/auth/events.py +++ b/learning_observer/learning_observer/auth/events.py @@ -333,7 +333,6 @@ async def authenticate(request, headers, first_event, source): if auth_response and "status_code" in auth_response and auth_response.get("status_code") == 403: debug_log("Auth Forbidden: the returned response code given the rules is 403") raise aiohttp.web.HTTPForbidden(reason=json.dumps(auth_response)) - if "safe_user_id" not in auth_metadata: auth_metadata['safe_user_id'] = encode_id( source=auth_metadata["providence"], @@ -386,19 +385,16 @@ def authenticate_payload(payload): field = rule["field"] # Get the field to be looked up in the payload patterns = rule["patterns"] # Get the patterns to match against for the payload value of the field value = payload.get(field) # Get the value of the field from the payload - if value: for pattern in patterns: # If there is a pattern match, add the rule type to the failed list if re.match(pattern, value): failed_rule_types.append(rule_type) - + # Sort the failed rule types based on their priority order sorted_failed_rule_types = sorted(failed_rule_types, key=RULE_TYPES_BY_PRIORITIES.index) - # Determine the response key based on the highest priority failed rule, or 'allow' if no rule failed response_key = sorted_failed_rule_types[0] if sorted_failed_rule_types else ALLOW - # Return the appropriate response based on the response key return RULES_RESPONSES[response_key] diff --git a/learning_observer/learning_observer/incoming_student_event.py b/learning_observer/learning_observer/incoming_student_event.py index 9d3103200..9f384f782 100644 --- a/learning_observer/learning_observer/incoming_student_event.py +++ b/learning_observer/learning_observer/incoming_student_event.py @@ -328,7 +328,6 @@ async def incoming_websocket_handler(request): print("Bad message:", msg) raise header_events.append(json_msg) - if json_msg["event"] == "metadata_finished": break else: @@ -361,6 +360,8 @@ async def incoming_websocket_handler(request): "timestamp": datetime.datetime.utcnow().isoformat() }) debug_log(auth_response.get('msg')) + # We don't close the websocket connection here because the client needs to be able + # to reauthenticate or handle other temporary permission denied auth status return ws print(event_metadata['auth']) From 76b6bf28b321ba4a9137ebbb3f9f74842317d665 Mon Sep 17 00:00:00 2001 From: JohnDamilola Date: Thu, 28 Sep 2023 15:51:22 -0400 Subject: [PATCH 09/12] refactor blacklisting to use a yaml config file --- .gitignore | 1 + .../auth/blacklisting_patterns.yaml.template | 19 ++++ .../auth/blacklisting_settings.py | 92 +++++++++++++------ .../learning_observer/auth/events.py | 42 +-------- 4 files changed, 84 insertions(+), 70 deletions(-) create mode 100644 learning_observer/learning_observer/auth/blacklisting_patterns.yaml.template diff --git a/.gitignore b/.gitignore index f09233f1d..b6a83014d 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ learning_observer/learning_observer/static_data/dash_assets/ learning_observer/learning_observer/static_data/courses.json learning_observer/learning_observer/static_data/students.json learning_observer/passwd.lo +learning_observer/learning_observer/auth/blacklisting_patterns.yaml --* .venv/ .vscode/ diff --git a/learning_observer/learning_observer/auth/blacklisting_patterns.yaml.template b/learning_observer/learning_observer/auth/blacklisting_patterns.yaml.template new file mode 100644 index 000000000..9c03e1862 --- /dev/null +++ b/learning_observer/learning_observer/auth/blacklisting_patterns.yaml.template @@ -0,0 +1,19 @@ +# This file serves as a configuration file for blacklisting/authentication rules and their associated patterns. +# Each rule type (e.g. DENY, etc.) contains a list of rules defined as follows: +# - field (str): The field to match against in incoming payloads, e.g., 'email', 'google_id', etc. +# - patterns (list of str): An array of regular expression patterns used for matching against the field value. +# +# Curly-braces are used for variables which ought to be filled + +DENY: + - field: {field} + patterns: + - {pattern} + - {pattern} + - field: {field} + patterns: + - {pattern} +DENY_FOR_TWO_DAYS: + - field: {field} + patterns: + - {pattern1} diff --git a/learning_observer/learning_observer/auth/blacklisting_settings.py b/learning_observer/learning_observer/auth/blacklisting_settings.py index 4d8132696..cb7f76326 100644 --- a/learning_observer/learning_observer/auth/blacklisting_settings.py +++ b/learning_observer/learning_observer/auth/blacklisting_settings.py @@ -1,26 +1,34 @@ """ -This file contains constants for blacklisting/authentication rule types, their responses, priorities and patterns. +This file contains constants, patterns, and functions related to blacklisting/authentication rules and responses. RULES_RESPONSES A dictionary containing responses for different rule types, including: - type (str) -- message (str) +- msg (str) - status_code (int) -RULES_PATTERNS -A dictionary containing patterns to match against for different rule types, specifying: -- field (str) -- patterns (arrays of regex patterns) -to apply for rule evaluation. - RULE_TYPES_BY_PRIORITIES A list defining the priority order of rule types for sorting. + +load_patterns(file_path='blacklisting_patterns.yaml') +A function that loads blacklisting patterns from a YAML file and returns them as a dictionary. + +authenticate_payload(payload) +A function that evaluates a payload against a set of rules and determines the authentication response. """ + +import re +import yaml +from learning_observer.log_event import debug_log + ALLOW = "allow" DENY = "deny" DENY_FOR_TWO_DAYS = "deny_for_two_days" +# Priority order of rule types for sorting +RULE_TYPES_BY_PRIORITIES = [DENY, DENY_FOR_TWO_DAYS] + # Responses for different rule types RULES_RESPONSES = { ALLOW: { @@ -40,25 +48,51 @@ } } -# Patterns to match against for different rule types -RULES_PATTERNS = { - DENY: [ - { - "field": "email", - "patterns": ["^.*@ncsu.edu"] - }, - { - "field": "google_id", - "patterns": ["1234"] - } - ], - DENY_FOR_TWO_DAYS: [ - { - "field": "email", - "patterns": ["^.*@ncsu.edu"] - } - ] -} -# Priority order of rule types for sorting -RULE_TYPES_BY_PRIORITIES = [DENY, DENY_FOR_TWO_DAYS] +def load_patterns(file_path='blacklisting_patterns.yaml'): + try: + with open(file_path, 'r') as file: + rules_patterns = yaml.safe_load(file) + return rules_patterns + except FileNotFoundError: + debug_log(f"No blacklisting patterns file added: '{file_path}' not found.") + return {} + + +def authenticate_payload(payload): + ''' + Evaluate a payload against a set of rules and determine the authentication response. + + This function iterates through a list of rules for various fields in the payload. + For each field, it checks if the value matches any of the specified patterns. + If a match is found, the associated rule type is added to a list of failed rule types. + The failed rule types are then sorted by priority, and the highest priority failed rule + determines the authentication response. + + If no rules fail, the function returns the 'allow' response. + + Args: + payload (dict): The payload containing data to be authenticated. + + Returns: + str: The authentication response based on the payload and rule evaluation. + ''' + RULES_PATTERNS = load_patterns() + failed_rule_types = [] # A list to store rule types that the payload fails to comply with + for rule_type, rules in RULES_PATTERNS.items(): + for rule in rules: + field = rule["field"] # Get the field to be looked up in the payload + patterns = rule["patterns"] # Get the patterns to match against for the payload value of the field + value = payload.get(field) # Get the value of the field from the payload + if value: + for pattern in patterns: + # If there is a pattern match, add the rule type to the failed list + if re.match(pattern, value): + failed_rule_types.append(rule_type) + + # Sort the failed rule types based on their priority order + sorted_failed_rule_types = sorted(failed_rule_types, key=RULE_TYPES_BY_PRIORITIES.index) + # Determine the response key based on the highest priority failed rule, or 'allow' if no rule failed + response_key = sorted_failed_rule_types[0] if sorted_failed_rule_types else ALLOW + # Return the appropriate response based on the response key + return RULES_RESPONSES[response_key] diff --git a/learning_observer/learning_observer/auth/events.py b/learning_observer/learning_observer/auth/events.py index f5bf52f5d..e6e55f1c0 100644 --- a/learning_observer/learning_observer/auth/events.py +++ b/learning_observer/learning_observer/auth/events.py @@ -24,7 +24,6 @@ import urllib.parse import secrets import sys -import re import json import aiohttp_session @@ -38,10 +37,9 @@ from learning_observer.log_event import debug_log -from learning_observer.auth.blacklisting_settings import RULES_PATTERNS, RULE_TYPES_BY_PRIORITIES, RULES_RESPONSES +from learning_observer.auth.blacklisting_settings import authenticate_payload AUTH_METHODS = {} -ALLOW = "allow" def register_event_auth(name): @@ -361,44 +359,6 @@ def check_event_auth_config(): )) -def authenticate_payload(payload): - ''' - Evaluate a payload against a set of rules and determine the authentication response. - - This function iterates through a list of rules for various fields in the payload. - For each field, it checks if the value matches any of the specified patterns. - If a match is found, the associated rule type is added to a list of failed rule types. - The failed rule types are then sorted by priority, and the highest priority failed rule - determines the authentication response. - - If no rules fail, the function returns the 'allow' response. - - Args: - payload (dict): The payload containing data to be authenticated. - - Returns: - str: The authentication response based on the payload and rule evaluation. - ''' - failed_rule_types = [] # A list to store rule types that the payload fails to comply with - for rule_type, rules in RULES_PATTERNS.items(): - for rule in rules: - field = rule["field"] # Get the field to be looked up in the payload - patterns = rule["patterns"] # Get the patterns to match against for the payload value of the field - value = payload.get(field) # Get the value of the field from the payload - if value: - for pattern in patterns: - # If there is a pattern match, add the rule type to the failed list - if re.match(pattern, value): - failed_rule_types.append(rule_type) - - # Sort the failed rule types based on their priority order - sorted_failed_rule_types = sorted(failed_rule_types, key=RULE_TYPES_BY_PRIORITIES.index) - # Determine the response key based on the highest priority failed rule, or 'allow' if no rule failed - response_key = sorted_failed_rule_types[0] if sorted_failed_rule_types else ALLOW - # Return the appropriate response based on the response key - return RULES_RESPONSES[response_key] - - if __name__ == "__main__": import doctest print("Running tests") From a3009f4f9a062050279edd34f4cc0429b11c8477 Mon Sep 17 00:00:00 2001 From: JohnDamilola Date: Tue, 24 Oct 2023 19:48:01 -0400 Subject: [PATCH 10/12] Add the case for ALLOW --- extension/writing-process/src/background.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extension/writing-process/src/background.js b/extension/writing-process/src/background.js index 971a82978..696dad98c 100644 --- a/extension/writing-process/src/background.js +++ b/extension/writing-process/src/background.js @@ -232,7 +232,11 @@ function websocket_logger(server) { console.log("I am being denied for 2 days") break; } + case ALLOW: + queue.push(data); + dequeue(); default: + // if authStatus does not exist, still push the events to the queue queue.push(data); dequeue(); } From 9776a2141e72712f98e1d2b915d79e6910a20ef8 Mon Sep 17 00:00:00 2001 From: JohnDamilola Date: Fri, 27 Oct 2023 01:36:17 -0400 Subject: [PATCH 11/12] Fix blacklisting pattern file loading --- .../auth/blacklisting_patterns.yaml | 18 ---------------- .../auth/blacklisting_patterns.yaml.template | 4 ++-- .../auth/blacklisting_settings.py | 21 ++++++++++++++++--- 3 files changed, 20 insertions(+), 23 deletions(-) delete mode 100644 learning_observer/learning_observer/auth/blacklisting_patterns.yaml diff --git a/learning_observer/learning_observer/auth/blacklisting_patterns.yaml b/learning_observer/learning_observer/auth/blacklisting_patterns.yaml deleted file mode 100644 index 45325f9f9..000000000 --- a/learning_observer/learning_observer/auth/blacklisting_patterns.yaml +++ /dev/null @@ -1,18 +0,0 @@ -# This file serves as a configuration file for blacklisting/authentication rules and their associated patterns. -# Each rule type (e.g. DENY, etc.) contains a list of rules defined as follows: -# - field (str): The field to match against in incoming payloads, e.g., 'email', 'google_id', etc. -# - patterns (list of str): An array of regular expression patterns used for matching against the field value. -# -# Curly-braces are used for variables which ought to be filled - -DENY: - - field: email - patterns: - - '^.*@ncsu.edu' - - field: google_id - patterns: - - '1234' -DENY_FOR_TWO_DAYS: - - field: email - patterns: - - '^.*@ncsu.edu' \ No newline at end of file diff --git a/learning_observer/learning_observer/auth/blacklisting_patterns.yaml.template b/learning_observer/learning_observer/auth/blacklisting_patterns.yaml.template index 9c03e1862..c5c4eb8ed 100644 --- a/learning_observer/learning_observer/auth/blacklisting_patterns.yaml.template +++ b/learning_observer/learning_observer/auth/blacklisting_patterns.yaml.template @@ -5,7 +5,7 @@ # # Curly-braces are used for variables which ought to be filled -DENY: +deny: - field: {field} patterns: - {pattern} @@ -13,7 +13,7 @@ DENY: - field: {field} patterns: - {pattern} -DENY_FOR_TWO_DAYS: +deny_for_two_days: - field: {field} patterns: - {pattern1} diff --git a/learning_observer/learning_observer/auth/blacklisting_settings.py b/learning_observer/learning_observer/auth/blacklisting_settings.py index cb7f76326..3d4653a00 100644 --- a/learning_observer/learning_observer/auth/blacklisting_settings.py +++ b/learning_observer/learning_observer/auth/blacklisting_settings.py @@ -18,9 +18,11 @@ """ +import os import re import yaml from learning_observer.log_event import debug_log +import learning_observer.paths as paths ALLOW = "allow" DENY = "deny" @@ -49,13 +51,26 @@ } -def load_patterns(file_path='blacklisting_patterns.yaml'): +def load_patterns(file_name='blacklisting_patterns.yaml'): + """ + Load blacklisting patterns from a YAML config file. + + Args: + file_name (str, optional): The name of the YAML file containing the blacklisting patterns. + Defaults to 'blacklisting_patterns.yaml'. + + Returns: + dict: A dictionary containing the loaded blacklisting patterns, or an empty dictionary + if the file is not found or there is an error loading the patterns. + """ + pathname = os.path.join(os.path.dirname(paths.base_path()), 'learning_observer/auth', file_name) try: - with open(file_path, 'r') as file: + with open(pathname, 'r') as file: rules_patterns = yaml.safe_load(file) + debug_log("Blacklisting patterns loaded") return rules_patterns except FileNotFoundError: - debug_log(f"No blacklisting patterns file added: '{file_path}' not found.") + debug_log(f"No blacklisting patterns file added: '{pathname}' not found.") return {} From 8e4c68c75117934518818f24f3f07eb12ed6a8bc Mon Sep 17 00:00:00 2001 From: JohnDamilola Date: Fri, 27 Oct 2023 02:06:48 -0400 Subject: [PATCH 12/12] Implement local date to UTC conversion --- extension/writing-process/src/background.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/extension/writing-process/src/background.js b/extension/writing-process/src/background.js index 696dad98c..b03f6d578 100644 --- a/extension/writing-process/src/background.js +++ b/extension/writing-process/src/background.js @@ -218,7 +218,7 @@ function websocket_logger(server) { break; case DENY_FOR_TWO_DAYS: // check back after two days to continue updating/emptying the queue const currentDate = new Date(); - const authResponseDate = new Date(result.authResponseTimeStamp); + const authResponseDate = convertLocalDateToUTC(new Date(result.authResponseTimeStamp)); // Calculate the date 2 days after the authResponseDate const twoDaysAfterAuthResponseDate = new Date(authResponseDate) @@ -244,6 +244,23 @@ function websocket_logger(server) { } } +function convertLocalDateToUTC(inputDateStr) { + /* + The returned server timestamp is in UTC and local time often time is not + This function converts the local time to UTC format to ensure accurate + time difference + */ + const date = new Date(inputDateStr); + + // Extract the time zone offset from the input date string + const timeZoneOffset = date.getTimezoneOffset(); + + // Calculate the UTC date by sub the time zone offset + const utcDate = new Date(date.getTime() - timeZoneOffset * 60000); // Convert minutes to milliseconds + + return utcDate; +} + function ajax_logger(ajax_server) { /* HTTP event per request.