-
Notifications
You must be signed in to change notification settings - Fork 81
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
[Gh 998] Maintenance window #1236
Changes from 36 commits
a176949
dc1eb1a
7febedf
c2eedda
482db02
485129e
89e9944
2c400b7
3f5ace1
9dd0890
24a2493
be31c18
174ce68
1fe784b
627d31c
95c63f5
093fab2
fed1609
11deb7b
f996471
16a7dc1
672ee82
cdf7eee
5b83e74
2ae74ce
3cd7d15
f3eab6f
02abe68
70dbc24
ea896f8
f7fb577
e7eba0c
85803fb
713605f
6b383ce
8180382
c027fa1
c5d6be9
e5728a5
9088ea5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
TejasRGitHub marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import logging | ||
|
||
import boto3 | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class EventBridge: | ||
def __init__(self, region=None): | ||
self.client = boto3.client('events', region_name=region) | ||
|
||
def enable_scheduled_ecs_tasks(self, list_of_tasks): | ||
logger.info('Enabling ecs tasks') | ||
try: | ||
for ecs_task in list_of_tasks: | ||
self.client.enable_rule(Name=ecs_task) | ||
except Exception as e: | ||
logger.error(f'Error while re-enabling scheduled ecs tasks due to {e}') | ||
raise e | ||
|
||
def disable_scheduled_ecs_tasks(self, list_of_tasks): | ||
logger.info('Disabling ecs tasks') | ||
try: | ||
for ecs_task in list_of_tasks: | ||
self.client.disable_rule(Name=ecs_task) | ||
except Exception as e: | ||
logger.error(f'Error while disabling scheduled ecs tasks due to {e}') | ||
raise e |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
import datetime | ||
import json | ||
import os | ||
import logging | ||
|
||
from graphql import parse, utilities, OperationType, GraphQLSyntaxError | ||
from dataall.base.aws.parameter_store import ParameterStoreManager | ||
from dataall.base.db import get_engine | ||
from dataall.base.services.service_provider_factory import ServiceProviderFactory | ||
from dataall.core.permissions.services.tenant_permissions import TENANT_ALL | ||
from dataall.core.permissions.services.tenant_policy_service import TenantPolicyService | ||
from dataall.modules.maintenance.api.enums import MaintenanceModes, MaintenanceStatus | ||
from dataall.modules.maintenance.services.maintenance_service import MaintenanceService | ||
from dataall.base.config import config | ||
from dataall.core.permissions.services.tenant_policy_service import TenantPolicyValidationService | ||
|
||
logger = logging.getLogger() | ||
logger.setLevel(os.environ.get('LOG_LEVEL', 'INFO')) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is this used for? We are using "log" everywhere else. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean why are we using the setLevel command ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the setLevel method is used mostly at the top of logger initialization. I am adding it here to set the log level to whatever is set for each lambda in their environment variable - which is available via the |
||
log = logging.getLogger(__name__) | ||
|
||
ENVNAME = os.getenv('envname', 'local') | ||
REAUTH_TTL = int(os.environ.get('REAUTH_TTL', '5')) | ||
# ALLOWED OPERATIONS WHEN A USER IS NOT DATAALL ADMIN AND NO-ACCESS MODE IS SELECTED | ||
MAINTENANCE_ALLOWED_OPERATIONS_WHEN_NO_ACCESS = [ | ||
item.casefold() for item in ['getGroupsForUser', 'getMaintenanceWindowStatus'] | ||
] | ||
ENGINE = get_engine(envname=ENVNAME) | ||
|
||
|
||
def get_cognito_groups(claims): | ||
if not claims: | ||
raise ValueError( | ||
'Received empty claims. ' 'Please verify authorizer configuration', | ||
claims, | ||
) | ||
groups = list() | ||
saml_groups = claims.get('custom:saml.groups', '') | ||
translation_table = str.maketrans({'[': None, ']': None, ', ': ','}) | ||
if len(saml_groups): | ||
groups = saml_groups.translate(translation_table).split(',') | ||
cognito_groups = claims.get('cognito:groups', '') | ||
if len(cognito_groups): | ||
groups.extend(cognito_groups.split(',')) | ||
return groups | ||
|
||
|
||
def get_custom_groups(user_id): | ||
service_provider = ServiceProviderFactory.get_service_provider_instance() | ||
return service_provider.get_groups_for_user(user_id) | ||
|
||
|
||
def send_unauthorized_response(operation='', message='', extension=None): | ||
response = { | ||
'data': {operation: None}, | ||
'errors': [ | ||
{ | ||
'message': message, | ||
'locations': None, | ||
'path': [operation], | ||
} | ||
], | ||
} | ||
if extension is not None: | ||
response['errors'][0]['extensions'] = extension | ||
return { | ||
'statusCode': 401, | ||
'headers': { | ||
'content-type': 'application/json', | ||
'Access-Control-Allow-Origin': '*', | ||
'Access-Control-Allow-Headers': '*', | ||
'Access-Control-Allow-Methods': '*', | ||
}, | ||
'body': json.dumps(response), | ||
} | ||
|
||
|
||
def extract_groups(user_id, claims): | ||
groups = [] | ||
try: | ||
if os.environ.get('custom_auth', None): | ||
groups.extend(get_custom_groups(user_id)) | ||
else: | ||
groups.extend(get_cognito_groups(claims)) | ||
log.debug('groups are %s', ','.join(groups)) | ||
return groups | ||
except Exception as e: | ||
log.exception(f'Error managing groups due to: {e}') | ||
return groups | ||
|
||
|
||
def attach_tenant_policy_for_groups(groups=None): | ||
if groups is None: | ||
groups = [] | ||
with ENGINE.scoped_session() as session: | ||
for group in groups: | ||
policy = TenantPolicyService.find_tenant_policy(session, group, TenantPolicyService.TENANT_NAME) | ||
if not policy: | ||
log.info(f'No policy found for Team {group}. Attaching TENANT_ALL permissions') | ||
TenantPolicyService.attach_group_tenant_policy( | ||
session=session, | ||
group=group, | ||
permissions=TENANT_ALL, | ||
tenant_name=TenantPolicyService.TENANT_NAME, | ||
) | ||
|
||
|
||
def check_reauth(query, auth_time, username): | ||
# Determine if there are any Operations that Require ReAuth From SSM Parameter | ||
try: | ||
reauth_apis = ParameterStoreManager.get_parameter_value( | ||
region=os.getenv('AWS_REGION', 'eu-west-1'), parameter_path=f'/dataall/{ENVNAME}/reauth/apis' | ||
).split(',') | ||
except Exception: | ||
log.info('No ReAuth APIs Found in SSM') | ||
reauth_apis = None | ||
|
||
# If The Operation is a ReAuth Operation - Ensure A Non-Expired Session or Return Error | ||
if reauth_apis and query.get('operationName', None) in reauth_apis: | ||
now = datetime.datetime.now(datetime.timezone.utc) | ||
try: | ||
auth_time_datetime = datetime.datetime.fromtimestamp(int(auth_time), tz=datetime.timezone.utc) | ||
if auth_time_datetime + datetime.timedelta(minutes=REAUTH_TTL) < now: | ||
raise Exception('ReAuth') | ||
except Exception as e: | ||
log.info(f'ReAuth Required for User {username} on Operation {query.get("operationName", "")}, Error: {e}') | ||
return send_unauthorized_response( | ||
operation=query.get('operationName', 'operation'), | ||
message=f"ReAuth Required To Perform This Action {query.get('operationName', '')}", | ||
extension={'code': 'REAUTH'}, | ||
) | ||
|
||
|
||
def validate_and_block_if_maintenance_window(query, groups, blocked_for_mode_enum=None): | ||
""" | ||
When the maintenance module is set to active, checks | ||
- If the maintenance mode is enabled | ||
- Based on the maintenance mode, actions which can be taken by user can be modified | ||
- READ-ONLY -> Block All Mutation calls and allow query graphql calls | ||
- NO-ACCESS -> Block All graphql query call irrespective of type | ||
- Check if the user belongs to the DAAdministrators group | ||
@param query: graphql query dict containing operation, query, variables | ||
@param groups: user groups | ||
@param blocked_for_mode_enum: sets the mode for blocking only specific modes. When set to None, both graphql types ( Query and Mutation ) will be blocked. When a specific mode is set, blocking will only occure for that mode | ||
@return: error response if maintenance window is blocking gql calls else None | ||
""" | ||
if config.get_property('modules.maintenance.active'): | ||
maintenance_mode = MaintenanceService._get_maintenance_window_mode(engine=ENGINE) | ||
maintenance_status = MaintenanceService.get_maintenance_window_status().status | ||
isAdmin = TenantPolicyValidationService.is_tenant_admin(groups) | ||
|
||
if ( | ||
(maintenance_mode == MaintenanceModes.NOACCESS.value) | ||
and (maintenance_status is not MaintenanceStatus.INACTIVE.value) | ||
and not isAdmin | ||
and (blocked_for_mode_enum is None or blocked_for_mode_enum == MaintenanceModes.NOACCESS) | ||
): | ||
if query.get('operationName', '').casefold() not in MAINTENANCE_ALLOWED_OPERATIONS_WHEN_NO_ACCESS: | ||
return send_unauthorized_response( | ||
operation=query.get('operationName', 'operation'), | ||
message='Access Restricted: data.all is currently undergoing maintenance, and your actions are temporarily blocked.', | ||
) | ||
elif ( | ||
(maintenance_mode == MaintenanceModes.READONLY.value) | ||
and (maintenance_status is not MaintenanceStatus.INACTIVE.value) | ||
and not isAdmin | ||
and (blocked_for_mode_enum is None or blocked_for_mode_enum == MaintenanceModes.READONLY) | ||
): | ||
# If its mutation then block and return | ||
try: | ||
parsed_query_document = parse(query.get('query', '')) | ||
graphQL_operation_type = utilities.get_operation_ast(parsed_query_document) | ||
TejasRGitHub marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if graphQL_operation_type.operation == OperationType.MUTATION: | ||
return send_unauthorized_response( | ||
operation=query.get('operationName', 'operation'), | ||
message='Access Restricted: data.all is currently undergoing maintenance, and your actions are temporarily blocked.', | ||
) | ||
except GraphQLSyntaxError as e: | ||
log.error( | ||
f'Error occured while parsing query when validating for {maintenance_mode} maintenance mode due to - {e}' | ||
) | ||
raise e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the handler function looks very messy, it is trying to do many things:
Can we divide it into smaller functions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Updated in the next commit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd move those methods in separate files and reuse them across different handlers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. I can do that . I see the value of reusing them. Thanks @petrkalos!