Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
73 changes: 70 additions & 3 deletions extension/writing-process/src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ 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';
var ALLOW = "allow"
var DENY = "deny"
var DENY_FOR_TWO_DAYS = "deny_for_two_days"


/*
TODO: FSM

Expand Down Expand Up @@ -96,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 };
Expand Down Expand Up @@ -189,11 +211,56 @@ function websocket_logger(server) {
}

return function(data) {
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 = convertLocalDateToUTC(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;
}
case ALLOW:
queue.push(data);
dequeue();
default:
// if authStatus does not exist, still push the events to the queue
queue.push(data);
dequeue();
}
});
}
}

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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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}
113 changes: 113 additions & 0 deletions learning_observer/learning_observer/auth/blacklisting_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""
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)
- msg (str)
- status_code (int)

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 os
import re
import yaml
from learning_observer.log_event import debug_log
import learning_observer.paths as paths

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: {
"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
}
}


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(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: '{pathname}' 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]
10 changes: 10 additions & 0 deletions learning_observer/learning_observer/auth/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import urllib.parse
import secrets
import sys
import json

import aiohttp_session
import aiohttp.web
Expand All @@ -36,6 +37,8 @@

from learning_observer.log_event import debug_log

from learning_observer.auth.blacklisting_settings import authenticate_payload

AUTH_METHODS = {}


Expand Down Expand Up @@ -230,8 +233,11 @@ 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,
Expand Down Expand Up @@ -321,6 +327,10 @@ 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:
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"],
Expand Down
24 changes: 18 additions & 6 deletions learning_observer/learning_observer/incoming_student_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,12 +345,24 @@ 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 error type and current 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'))
# 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'])

Expand Down