Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

💩 🔧 Enable suspending data collection for certain subgroups #1016

Merged
merged 2 commits into from
Feb 3, 2025
Merged
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
37 changes: 24 additions & 13 deletions emission/net/api/cfc_webapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
auth_method = config.get("WEBSERVER_AUTH", "skip")
aggregate_call_auth = config.get("WEBSERVER_AGGREGATE_CALL_AUTH", "no_auth")
not_found_redirect = config.get("WEBSERVER_NOT_FOUND_REDIRECT", "https://nrel.gov/openpath")
dynamic_config = None

BaseRequest.MEMFILE_MAX = 1024 * 1024 * 1024 # Allow the request size to be 1G
# to accomodate large section sizes
Expand Down Expand Up @@ -483,6 +484,25 @@ def after_request():
stats.store_server_api_time(request.params.user_uuid, "%s_%s_cputime" % (request.method, request.path),
msTimeNow, new_duration)

# Dynamic config BEGIN

def get_dynamic_config():
logging.debug(f"STUDY_CONFIG is {STUDY_CONFIG}")
download_url = "https://raw.githubusercontent.com/e-mission/nrel-openpath-deploy-configs/main/configs/" + STUDY_CONFIG + ".nrel-op.json"
logging.debug("About to download config from %s" % download_url)
r = requests.get(download_url)
if r.status_code != 200:
logging.debug(f"Unable to download study config for {STUDY_CONFIG=}, status code: {r.status_code}")
return None
else:
dynamic_config = json.loads(r.text)
logging.debug(f"Successfully downloaded config with version {dynamic_config['version']} "\
f"for {dynamic_config['intro']['translated_text']['en']['deployment_name']} "\
f"and data collection URL {dynamic_config.get('server', {}).get('connectUrl', 'OS DEFAULT')}")
return dynamic_config

# Dynamic config END

# Auth helpers BEGIN
# This should only be used by createUserProfile since we may not have a UUID
# yet. All others should use the UUID.
Expand Down Expand Up @@ -513,7 +533,7 @@ def get_user_or_aggregate_auth(request):

def getUUID(request, inHeader=False):
try:
retUUID = enaa.getUUID(request, auth_method, inHeader)
retUUID = enaa.getUUID(dynamic_config, request, auth_method, inHeader)
logging.debug("retUUID = %s" % retUUID)
if retUUID is None:
raise HTTPError(403, "token is valid, but no account found for user")
Expand All @@ -524,20 +544,10 @@ def getUUID(request, inHeader=False):

def resolve_auth(auth_method):
if auth_method == "dynamic":
logging.debug("auth_method is dynamic")
logging.debug(f"STUDY_CONFIG is {STUDY_CONFIG}")
download_url = "https://raw.githubusercontent.com/e-mission/nrel-openpath-deploy-configs/main/configs/" + STUDY_CONFIG + ".nrel-op.json"
logging.debug("About to download config from %s" % download_url)
r = requests.get(download_url)
if r.status_code != 200:
logging.debug(f"Unable to download study config, status code: {r.status_code}")
logging.debug("auth_method is dynamic, using dynamic config to find the actual auth method")
if dynamic_config is None:
sys.exit(1)
else:
dynamic_config = json.loads(r.text)
logging.debug(f"Successfully downloaded config with version {dynamic_config['version']} "\
f"for {dynamic_config['intro']['translated_text']['en']['deployment_name']} "\
f"and data collection URL {dynamic_config['server']['connectUrl']}")

if "opcode" in dynamic_config:
# New style config
if dynamic_config["opcode"]["autogen"] == True:
Expand Down Expand Up @@ -567,6 +577,7 @@ def resolve_auth(auth_method):

logging.config.dictConfig(webserver_log_config)
logging.debug("attempting to resolve auth_method")
dynamic_config = get_dynamic_config()
auth_method = resolve_auth(auth_method)

logging.debug(f"Using auth method {auth_method}")
Expand Down
38 changes: 37 additions & 1 deletion emission/net/auth/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,45 @@ def __getToken__(request, inHeader):

return userToken

def getUUID(request, authMethod, inHeader=False):
def getSubgroupFromToken(token, config):
if "opcode" in config:
# new style study, expects token with sub-group
tokenParts = token.split('_');
if len(tokenParts) <= 3:
# no subpart defined
raise ValueError(f"Not enough parts in {token=}, expected 3, found {tokenParts.length=}")
if "subgroups" in config.get("opcode", {}):
if tokenParts[2] not in config['opcode']['subgroups']:
# subpart not in config list
raise ValueError(f"Invalid subgroup {tokenParts[2]} not in {config.opcode.subgroups}")
else:
logging.debug('subgroup ' + tokenParts[2] + ' found in list ' + str(config['opcode']['subgroups']))
return tokenParts[2];
else:
if tokenParts[2] != 'default':
# subpart not in config list
raise ValueError(f"No subgroups found in config, but subgroup {tokenParts[2]} is not 'default'")
else:
logging.debug("no subgroups in config, 'default' subgroup found in token ");
return tokenParts[2];
else:
# old style study, expect token without subgroup
# nothing further to validate at this point
# only validation required is `nrelop_` and valid study name
# first is already handled in getStudyNameFromToken, second is handled
# by default since download will fail if it is invalid
logging.debug('Old-style study, expecting token without a subgroup...');
return None;

def getUUID(dynamicConfig, request, authMethod, inHeader=False):
retUUID = None
userToken = __getToken__(request, inHeader)
curr_subgroup = getSubgroupFromToken(userToken, dynamicConfig)
suspended_subgroups = dynamicConfig.get("opcode", {}).get("suspended_subgroups", [])
if request.path == "/usercache/put":
if curr_subgroup in suspended_subgroups:
logging.info(f"Received put message for subgroup {curr_subgroup} in {suspended_subgroups=}, returning uuid = None")
return None
retUUID = getUUIDFromToken(authMethod, userToken)
request.params.user_uuid = retUUID
return retUUID
Expand Down
6 changes: 3 additions & 3 deletions emission/tests/netTests/TestAuthSelection.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def testGetUUIDSkipAuth(self):
logging.debug("Found request body = %s" % request.body.getvalue())
logging.debug("Found request headers = %s" % list(request.headers.keys()))
user = ecwu.User.register(self.test_email)
self.assertEqual(enaa.getUUID(request, "skip", inHeader=False), user.uuid)
self.assertEqual(enaa.getUUID({}, request, "skip", inHeader=False), user.uuid)
ecwu.User.unregister(self.test_email)

def testGetUUIDTokenAuthSuccess(self):
Expand All @@ -161,7 +161,7 @@ def testGetUUIDTokenAuthSuccess(self):
logging.debug("Found request body = %s" % request.body.getvalue())
logging.debug("Found request headers = %s" % list(request.headers.keys()))
user = ecwu.User.register(self.test_email)
self.assertEqual(enaa.getUUID(request, "token_list", inHeader=False), user.uuid)
self.assertEqual(enaa.getUUID({}, request, "token_list", inHeader=False), user.uuid)
ecwu.User.unregister(self.test_email)

def testGetUUIDTokenAuthFailure(self):
Expand All @@ -180,7 +180,7 @@ def testGetUUIDTokenAuthFailure(self):
user = ecwu.User.register(self.test_email)
ecwu.User.unregister(self.test_email)
with self.assertRaises(ValueError):
self.assertEqual(enaa.getUUID(request, "token_list", inHeader=False), user.uuid)
self.assertEqual(enaa.getUUID({}, request, "token_list", inHeader=False), user.uuid)

if __name__ == '__main__':
import emission.tests.common as etc
Expand Down
4 changes: 4 additions & 0 deletions emission/tests/netTests/TestWebserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,24 @@ def test404Redirect(self):
@mock.patch.dict(os.environ, {"STUDY_CONFIG":"nrel-commute"}, clear=True)
def test_ResolveAuthWithEnvVar(self):
importlib.reload(enacw)
enacw.dynamic_config = enacw.get_dynamic_config()
self.assertEqual(enacw.resolve_auth("dynamic"),"skip")

@mock.patch.dict(os.environ, {"STUDY_CONFIG":"denver-casr"}, clear=True)
def test_ResolveAuthWithEnvVar(self):
importlib.reload(enacw)
enacw.dynamic_config = enacw.get_dynamic_config()
self.assertEqual(enacw.resolve_auth("dynamic"),"skip")

@mock.patch.dict(os.environ, {"STUDY_CONFIG":"stage-program"}, clear=True)
def test_ResolveAuthWithEnvVar(self):
importlib.reload(enacw)
enacw.dynamic_config = enacw.get_dynamic_config()
self.assertEqual(enacw.resolve_auth("dynamic"),"token_list")

def testResolveAuthNoEnvVar(self):
importlib.reload(enacw)
enacw.dynamic_config = enacw.get_dynamic_config()
self.assertEqual(enacw.resolve_auth("skip"),"skip")
self.assertEqual(enacw.resolve_auth("token_list"),"token_list")
self.assertEqual(enacw.resolve_auth("dynamic"),"token_list")
Expand Down