def _set_calibration_for_data_product(self, dp_obj, dev_cfg): from ion.util.direct_coverage_utils import DirectCoverageAccess from coverage_model import SparseConstantType log.debug("Setting calibration for data product '%s'", dp_obj.name) dataset_ids, _ = self.rr.find_objects(dp_obj, PRED.hasDataset, id_only=True) publisher = EventPublisher(OT.InformationContentModifiedEvent) if not dataset_ids: data_product_management = DataProductManagementServiceProcessClient(process=self) log.debug(" Creating dataset for data product %s", dp_obj.name) data_product_management.create_dataset_for_data_product(dp_obj._id) dataset_ids, _ = self.rr.find_objects(dp_obj, PRED.hasDataset, id_only=True) if not dataset_ids: raise NotFound('No datasets were found for this data product, ensure that it was created') for dataset_id in dataset_ids: # Synchronize with ingestion with DirectCoverageAccess() as dca: cov = dca.get_editable_coverage(dataset_id) # Iterate over the calibrations for cal_name, contents in dev_cfg.iteritems(): if cal_name in cov.list_parameters() and isinstance(cov.get_parameter_context(cal_name).param_type, SparseConstantType): value = float(contents['value']) log.info(' Updating Calibrations for %s in %s', cal_name, dataset_id) cov.set_parameter_values(cal_name, value) else: log.warn(" Calibration %s not found in dataset", cal_name) publisher.publish_event(origin=dataset_id, description="Calibrations Updated") publisher.close() log.info("Calibration set for data product '%s' in %s coverages", dp_obj.name, len(dataset_ids))
def perform_action(self, predicate=None, action=None): userid = None # get from context event_pub = EventPublisher(process=self) event_pub.publish_event(event_type=OT.ContainerManagementRequest, origin=userid, predicate=predicate, action=action)
def _publish_access_event(self, access_type, data_product_id=None, access_params=None): try: pub = EventPublisher(OT.InformationContentAccessedEvent, process=self) event_data = dict(origin_type=RT.DataProduct, origin=data_product_id or "", sub_type=access_type, access_params=access_params or {}) pub.publish_event(**event_data) except Exception as ex: log.exception("Error publishing InformationContentAccessedEvent for data product: %s", data_product_id)
def _publish_access_event(self, access_type, data_product_id=None, access_params=None): try: pub = EventPublisher(OT.InformationContentAccessedEvent, process=self) event_data = dict(origin_type=RT.DataProduct, origin=data_product_id or "", sub_type=access_type, access_params=access_params or {}) pub.publish_event(**event_data) except Exception as ex: log.exception( "Error publishing InformationContentAccessedEvent for data product: %s", data_product_id)
def test_procs(self): # Start procs config = {} pid1 = self.container.spawn_process("hello1", __name__, "CoordinatedProcess", config=config) pid2 = self.container.spawn_process("hello2", __name__, "CoordinatedProcess", config=config) pid3 = self.container.spawn_process("hello3", __name__, "CoordinatedProcess", config=config) # Wait for them to be ready gevent.sleep(0.3) # Call service evt_pub = EventPublisher(event_type=OT.ResourceCommandEvent) hc = HelloServiceClient() end_time = get_ion_ts_millis() + 4000 counter = 0 while get_ion_ts_millis() < end_time: counter += 1 res = hc.hello("foo") evt_pub.publish_event(origin=str(counter)) gevent.sleep(random.random() * 0.1) # Wait for them to be finish gevent.sleep(0.3) # Wrap up self.container.terminate_process(pid1) self.container.terminate_process(pid2) self.container.terminate_process(pid3) evt_count = CoordinatedProcess.evt_count log.info("Counters: %s", evt_count) self.assertEqual(evt_count["total"], 3 * counter) master_evts = evt_count[pid1] + evt_count[pid2] + evt_count[pid3] self.assertLessEqual(master_evts, counter) if master_evts < counter: log.info("Lost %s events - no functioning master", counter - master_evts)
def process_oms_event(): if not request.data: log.warning('process_oms_event: invalid OMS event payload: %r', request.data) return gateway_json_response(OMS_BAD_REQUEST_RESPONSE) payload = json_loads(str(request.data)) if not isinstance(payload, list): log.warning('process_oms_event: invalid OMS event payload: ' 'expecting array but got: %r', payload) return gateway_json_response(OMS_BAD_REQUEST_RESPONSE) log.debug('process_oms_event: payload=%s', payload) event_publisher = EventPublisher() for obj in payload: for k in ['event_id', 'platform_id', 'message']: if k not in obj: log.warning('process_oms_event: invalid OMS event: %r missing. ' 'Received object: %s', k, obj) #return gateway_json_response(OMS_BAD_REQUEST_RESPONSE) # note the the external event_id is captured in the sub_type field: evt = dict( event_type = 'OMSDeviceStatusEvent', origin_type = 'OMS Platform', origin = obj.get('platform_id', 'platform_id NOT PROVIDED'), sub_type = obj.get('event_id', 'event_id NOT PROVIDED'), description = obj.get('message', ''), status_details = obj) try: event_publisher.publish_event(**evt) log.debug('process_oms_event: published: %s', evt) except Exception as e: log.exception('process_oms_event: could not publish OMS event: %s', evt) return gateway_json_response(OMS_ACCEPTED_RESPONSE)
def test_procs(self): # Start procs config = {} pid1 = self.container.spawn_process("hello1", __name__, "CoordinatedProcess", config=config) pid2 = self.container.spawn_process("hello2", __name__, "CoordinatedProcess", config=config) pid3 = self.container.spawn_process("hello3", __name__, "CoordinatedProcess", config=config) # Wait for them to be ready gevent.sleep(0.3) # Call service evt_pub = EventPublisher(event_type=OT.ResourceCommandEvent) hc = HelloServiceClient() end_time = get_ion_ts_millis() + 4000 counter = 0 while get_ion_ts_millis() < end_time: counter += 1 res = hc.hello("foo") evt_pub.publish_event(origin=str(counter)) gevent.sleep(random.random()*0.1) # Wait for them to be finish gevent.sleep(0.3) # Wrap up self.container.terminate_process(pid1) self.container.terminate_process(pid2) self.container.terminate_process(pid3) evt_count = CoordinatedProcess.evt_count log.info("Counters: %s", evt_count) self.assertEqual(evt_count["total"], 3*counter) master_evts = evt_count[pid1] + evt_count[pid2] + evt_count[pid3] self.assertLessEqual(master_evts, counter) if master_evts < counter: log.info("Lost %s events - no functioning master", counter - master_evts)
class PolicyManagementService(BasePolicyManagementService): event_pub = None def on_start(self): self.event_pub = EventPublisher(process=self) # ------------------------------------------------------------------------- # Policy management def create_resource_access_policy(self, resource_id='', policy_name='', description='', policy_rule='', ordinal=0): """Boilerplate operation for creating an access policy for a specific resource. """ if not resource_id: raise BadRequest("The resource_id argument is missing") if not policy_name: raise BadRequest("The policy_name argument is missing") if not description: raise BadRequest("The description argument is missing") if not policy_rule: raise BadRequest("The policy_rule argument is missing") policy_obj = IonObject(RT.Policy, name=policy_name, description=description, policy_type=PolicyTypeEnum.RESOURCE_ACCESS, definition=policy_rule, ordinal=ordinal, details=IonObject(OT.ResourceAccessPolicyDetails, resource_id=resource_id)) policy_id = self.create_policy(policy_obj) self._add_resource_policy(resource_id, policy_id, publish_event=False) return policy_id def create_service_access_policy(self, service_name='', policy_name='', description='', policy_rule='', ordinal=0): """Boilerplate operation for creating an access policy for a specific service. """ if not service_name: raise BadRequest("The service_name argument is missing") if not policy_name: raise BadRequest("The policy_name argument is missing") if not description: raise BadRequest("The description argument is missing") if not policy_rule: raise BadRequest("The policy_rule argument is missing") policy_obj = IonObject(RT.Policy, name=policy_name, description=description, policy_type=PolicyTypeEnum.SERVICE_ACCESS, definition=policy_rule, ordinal=ordinal, details=IonObject(OT.ServiceAccessPolicyDetails, service_name=service_name)) return self.create_policy(policy_obj) def create_common_service_access_policy(self, policy_name='', description='', policy_rule='', ordinal=0): """Boilerplate operation for creating a service access policy common to all services. """ if not policy_name: raise BadRequest("The policy_name argument is missing") if not description: raise BadRequest("The description argument is missing") if not policy_rule: raise BadRequest("The policy_rule argument is missing") policy_obj = IonObject(RT.Policy, name=policy_name, description=description, policy_type=PolicyTypeEnum.COMMON_SERVICE_ACCESS, definition=policy_rule, ordinal=ordinal,) return self.create_policy(policy_obj) def add_process_operation_precondition_policy(self, process_id='', op='', policy_content=''): """Boilerplate operation for adding a precondition policy for a specific process operation. The precondition method must return a tuple (boolean, string). """ if not process_id: raise BadRequest("The process_id argument is missing") if not op: raise BadRequest("The op argument is missing") if not policy_content: raise BadRequest("The policy_content argument is missing") policy_name = "Process_" + process_id + "_" + op + "_Precondition_Policies" policies, _ = self.clients.resource_registry.find_resources(restype=RT.Policy, name=policy_name) if policies: # Update existing policy by adding to list if len(policies) > 1: raise Inconsistent('There should only be one Policy object per process and operation') policy_obj = policies[0] if policy_obj.details.type_ != OT.ProcessOperationPreconditionPolicyDetails or policy_obj.details.op != op: raise Inconsistent('The Policy %s does not match the requested process operation %s: %s' % ( policy_obj.name, process_id, op)) policy_obj.details.preconditions.append(policy_content) self.update_policy(policy_obj) return policy_obj._id else: policy_obj = IonObject(RT.Policy, name=policy_name, description='List of operation precondition policies for process ' + process_id, policy_type=PolicyTypeEnum.PROC_OP_PRECOND, details=IonObject(OT.ProcessOperationPreconditionPolicyDetails, process_id=process_id, op=op, preconditions=[policy_content])) return self.create_policy(policy_obj) def add_service_operation_precondition_policy(self, service_name='', op='', policy_content=''): """Boilerplate operation for adding a precondition policy for a specific service operation. The precondition method must return a tuple (boolean, string). """ if not service_name: raise BadRequest("The service_name argument is missing") if not op: raise BadRequest("The op argument is missing") if not policy_content: raise BadRequest("The policy_content argument is missing") policy_name = "Service_" + service_name + "_" + op + "_Precondition_Policies" policies, _ = self.clients.resource_registry.find_resources(restype=RT.Policy, name=policy_name) if policies: # Update existing policy by adding to list if len(policies) > 1: raise Inconsistent('There should only be one Policy object per service and operation') policy_obj = policies[0] if policy_obj.details.type_ != OT.ServiceOperationPreconditionPolicyDetails or policy_obj.details.op != op: raise Inconsistent('The Policy %s does not match the requested service operation %s: %s' % ( policy_obj.name, service_name, op)) policy_obj.details.preconditions.append(policy_content) self.update_policy(policy_obj) return policy_obj._id else: policy_obj = IonObject(RT.Policy, name=policy_name, description='List of operation precondition policies for service ' + service_name, policy_type=PolicyTypeEnum.SERVICE_OP_PRECOND, details=IonObject(OT.ProcessOperationPreconditionPolicyDetails, service_name=service_name, op=op, preconditions=[policy_content])) return self.create_policy(policy_obj) def create_policy(self, policy=None): """Persists the provided Policy object. Returns the policy id. """ self._validate_resource_obj("policy", policy, RT.Policy, checks="noid,name") if not is_basic_identifier(policy.name): raise BadRequest("The policy name '%s' can only contain alphanumeric and underscore characters" % policy.name) try: # If there is a policy_rule field then try to add the policy name and description to the rule text if policy.definition: rule_tokens = dict(rule_id=policy.name, description=policy.description) policy.definition = policy.definition.format(**rule_tokens) except Exception as e: raise Inconsistent("Missing the elements in the policy rule to set the description: " + e.message) policy_id, _ = self.clients.resource_registry.create(policy) policy._id = policy_id log.debug('Policy created: ' + policy.name) self._publish_policy_event(policy) return policy_id def update_policy(self, policy=None): """Updates the provided Policy object. """ self._validate_resource_obj("policy", policy, RT.Policy, checks="id,name") if not is_basic_identifier(policy.name): raise BadRequest("The policy name '%s' can only contain alphanumeric and underscore characters" % policy.name) self.clients.resource_registry.update(policy) self._publish_policy_event(policy) def read_policy(self, policy_id=''): """Returns the Policy object for the specified policy id. Throws exception if id does not match any persisted Policy objects. """ policy_obj = self._validate_resource_id("policy_id", policy_id, RT.Policy) return policy_obj def delete_policy(self, policy_id=''): """For now, permanently deletes Policy object with the specified id. Throws exception if id does not match any persisted Policy. """ policy_obj = self._validate_resource_id("policy_id", policy_id, RT.Policy) res_list = self._find_resources_for_policy(policy_id) for res in res_list: self._remove_resource_policy(res, policy_obj) self.clients.resource_registry.delete(policy_id) self._publish_policy_event(policy_obj, delete_policy=True) def enable_policy(self, policy_id=''): """Sets a flag to enable the use of the policy """ policy_obj = self._validate_resource_id("policy_id", policy_id, RT.Policy) policy_obj.enabled = True self.update_policy(policy_obj) def disable_policy(self, policy_id=''): """Resets a flag to disable the use of the policy """ policy_obj = self._validate_resource_id("policy_id", policy_id, RT.Policy) policy_obj.enabled = False self.update_policy(policy_obj) def add_resource_policy(self, resource_id='', policy_id=''): """Associates a policy to a specific resource """ resource_obj, policy_obj = self._add_resource_policy(resource_id, policy_id) return True def _add_resource_policy(self, resource_id, policy_id, publish_event=True): """Removing a policy resource association and publish event for containers to update """ resource_obj = self._validate_resource_id("resource_id", resource_id) policy_obj = self._validate_resource_id("policy_id", policy_id, RT.Policy) self.clients.resource_registry.create_association(resource_obj, PRED.hasPolicy, policy_obj) # Publish an event that the resource policy has changed if publish_event: self._publish_resource_policy_event(policy_obj, resource_obj) return resource_obj, policy_obj def remove_resource_policy(self, resource_id='', policy_id=''): """Removes an association for a policy to a specific resource """ resource_obj = self._validate_resource_id("resource_id", resource_id) policy_obj = self._validate_resource_id("policy_id", policy_id, RT.Policy) self._remove_resource_policy(resource_obj, policy_obj) return True def _remove_resource_policy(self, resource, policy): aid = self.clients.resource_registry.get_association(resource, PRED.hasPolicy, policy) self.clients.resource_registry.delete_association(aid) # Publish an event that the resource policy has changed self._publish_resource_policy_event(policy, resource) def _publish_policy_event(self, policy, delete_policy=False): if policy.policy_type == PolicyTypeEnum.COMMON_SERVICE_ACCESS: self._publish_service_policy_event(policy, delete_policy) elif policy.policy_type == PolicyTypeEnum.SERVICE_ACCESS or policy.policy_type == PolicyTypeEnum.SERVICE_OP_PRECOND: self._publish_service_policy_event(policy, delete_policy) else: # Need to publish an event that a policy has changed for any associated resource res_list = self._find_resources_for_policy(policy._id) for res in res_list: self._publish_resource_policy_event(policy, res, delete_policy) def _publish_resource_policy_event(self, policy, resource, delete_policy=False): if self.event_pub: event_data = dict() event_data['origin_type'] = 'Resource_Policy' event_data['description'] = 'Updated Resource Policy' event_data['resource_id'] = resource._id event_data['resource_type'] = resource.type_ event_data['resource_name'] = resource.name event_data['sub_type'] = 'DeletePolicy' if delete_policy else '' self.event_pub.publish_event(event_type='ResourcePolicyEvent', origin=policy._id, **event_data) def _publish_related_resource_policy_event(self, policy, resource_id, delete_policy=False): if self.event_pub: event_data = dict() event_data['origin_type'] = 'Resource_Policy' event_data['description'] = 'Updated Related Resource Policy' event_data['resource_id'] = resource_id event_data['sub_type'] = 'DeletePolicy' if delete_policy else '' self.event_pub.publish_event(event_type='RelatedResourcePolicyEvent', origin=policy._id, **event_data) def _publish_service_policy_event(self, policy, delete_policy=False): if self.event_pub: event_data = dict() event_data['origin_type'] = 'Service_Policy' event_data['description'] = 'Updated Service Policy' event_data['sub_type'] = 'DeletePolicy' if delete_policy else '' event_data['service_name'] = getattr(policy.details, 'service_name', "") if policy.policy_type == PolicyTypeEnum.SERVICE_OP_PRECOND: event_data['op'] = policy.details.op self.event_pub.publish_event(event_type='ServicePolicyEvent', origin=policy._id, **event_data) def find_resource_policies(self, resource_id=''): """Finds all policies associated with a specific resource """ resource_obj = self._validate_resource_id("resource_id", resource_id) return self._find_resource_policies(resource_obj) def _find_resource_policies(self, resource, policy=None): policy_list, _ = self.clients.resource_registry.find_objects(resource, PRED.hasPolicy, policy) return policy_list def _find_resources_for_policy(self, policy_id=''): """Finds all resources associated with a specific policy """ resource_list, _ = self.clients.resource_registry.find_subjects(None, PRED.hasPolicy, policy_id) return resource_list def get_active_resource_access_policy_rules(self, resource_id='', org_name=''): """Generates the set of all enabled access policies for the specified resource within the specified Org. If the org_name is not provided, then the root ION Org will be assumed. """ # TODO - extend to handle Org specific service policies at some point. policy_list = [] resource_obj = self._validate_resource_id("resource_id", resource_id) resource_id_list = self._get_related_resource_ids(resource_obj) if not resource_id_list: resource_id_list.append(resource_id) log.debug("Retrieving policies for resources: %s", resource_id_list) for res_id in resource_id_list: policy_set = self._find_resource_policies(res_id) for p in policy_set: if p.enabled and p.policy_type == PolicyTypeEnum.RESOURCE_ACCESS: log.debug("Including policy: %s", p.name) policy_list.append(p) policy_list.sort(key=lambda o: (o.ordinal, o.ts_created)) return policy_list def _get_related_resource_ids(self, resource): """For given resource object, find related resources based on type""" resource_id_list = [] # TODO - This could be following associations (parents, children, etc) return resource_id_list def get_active_service_access_policy_rules(self, service_name='', org_name=''): """Generates the set of all enabled access policies for the specified service within the specified Org. If the org_name is not provided, then the root ION Org will be assumed. """ # TODO - extend to handle Org specific service policies at some point. rq = ResourceQuery() rq.set_filter(rq.filter_type(RT.Policy), rq.filter_attribute("enabled", True)) if service_name: rq.add_filter(rq.or_(rq.filter_attribute("policy_type", PolicyTypeEnum.COMMON_SERVICE_ACCESS), rq.and_(rq.filter_attribute("policy_type", [PolicyTypeEnum.SERVICE_ACCESS, PolicyTypeEnum.SERVICE_OP_PRECOND]), rq.filter_attribute("details.service_name", service_name)))) else: rq.add_filter(rq.filter_attribute("policy_type", PolicyTypeEnum.COMMON_SERVICE_ACCESS)) policy_list = self.clients.resource_registry.find_resources_ext(query=rq.get_query(), id_only=False) policy_list.sort(key=lambda o: (o.ordinal, o.ts_created)) return policy_list def get_active_service_operation_preconditions(self, service_name='', op='', org_name=''): """Generates the set of all enabled precondition policies for the specified service operation within the specified Org. If the org_name is not provided, then the root ION Org will be assumed. """ # TODO - extend to handle Org specific service policies at some point. if not service_name: raise BadRequest("The service_name argument is missing") rq = ResourceQuery() rq.set_filter(rq.filter_type(RT.Policy), rq.filter_attribute("enabled", True), rq.filter_attribute("policy_type", PolicyTypeEnum.SERVICE_OP_PRECOND), rq.filter_attribute("details.service_name", service_name)) if op: rq.add_filter(rq.filter_attribute("details.op", op)) policy_list = self.clients.resource_registry.find_resources_ext(query=rq.get_query(), id_only=False) policy_list.sort(key=lambda o: (o.ordinal, o.ts_created)) return policy_list def get_active_process_operation_preconditions(self, process_key='', op='', org_name=''): """Generates the set of all enabled precondition policies for the specified process operation within the specified Org. If the org_name is not provided, then the root ION Org will be assumed. """ # TODO - extend to handle Org specific service policies at some point. if not process_key: raise BadRequest("The process_key argument is missing") rq = ResourceQuery() rq.set_filter(rq.filter_type(RT.Policy), rq.filter_attribute("enabled", True), rq.filter_attribute("policy_type", PolicyTypeEnum.PROC_OP_PRECOND), rq.filter_attribute("details.process_key", process_key)) if op: rq.add_filter(rq.filter_attribute("details.op", op)) policy_list = self.clients.resource_registry.find_resources_ext(query=rq.get_query(), id_only=False) policy_list.sort(key=lambda o: (o.ordinal, o.ts_created)) return policy_list # Local helper functions for testing policies - do not remove def func1_pass(self, msg, header): return True, '' def func2_deny(self, msg, header): return False, 'Denied for no reason'
class UserNotificationService(BaseUserNotificationService): """ A service that provides users with an API for CRUD methods for notifications. """ def __init__(self, *args, **kwargs): self._schedule_ids = [] BaseUserNotificationService.__init__(self, *args, **kwargs) def on_start(self): self.ION_NOTIFICATION_EMAIL_ADDRESS = CFG.get_safe('server.smtp.sender') # Create an event processor self.event_processor = EmailEventProcessor() # Dictionaries that maintain information asetting_up_smtp_clientbout users and their subscribed notifications self.user_info = {} # The reverse_user_info is calculated from the user_info dictionary self.reverse_user_info = {} self.event_publisher = EventPublisher(process=self) self.start_time = get_ion_ts() #------------------------------------------------------------------------------------ # Create an event subscriber for Reload User Info events #------------------------------------------------------------------------------------ def reload_user_info(event_msg, headers): """ Callback method for the subscriber to ReloadUserInfoEvent """ notification_id = event_msg.notification_id log.debug("(UNS instance) received a ReloadNotificationEvent. The relevant notification_id is %s" % notification_id) try: self.user_info = self.load_user_info() except NotFound: log.warning("ElasticSearch has not yet loaded the user_index.") self.reverse_user_info = calculate_reverse_user_info(self.user_info) log.debug("(UNS instance) After a reload, the user_info: %s" % self.user_info) log.debug("(UNS instance) The recalculated reverse_user_info: %s" % self.reverse_user_info) # the subscriber for the ReloadUSerInfoEvent self.reload_user_info_subscriber = EventSubscriber( event_type=OT.ReloadUserInfoEvent, origin='UserNotificationService', callback=reload_user_info ) self.add_endpoint(self.reload_user_info_subscriber) def on_quit(self): """ Handles stop/terminate. Cleans up subscribers spawned here, terminates any scheduled tasks to the scheduler. """ for sid in self._schedule_ids: try: self.clients.scheduler.cancel_timer(sid) except IonException as ex: log.info("Ignoring exception while cancelling schedule id (%s): %s: %s", sid, ex.__class__.__name__, ex) super(UserNotificationService, self).on_quit() def set_process_batch_key(self, process_batch_key = ''): """ This method allows an operator to set the process_batch_key, a string. Once this method is used by the operator, the UNS will start listening for timer events published by the scheduler with origin = process_batch_key. @param process_batch_key str """ def process(event_msg, headers): self.end_time = get_ion_ts() # run the process_batch() method self.process_batch(start_time=self.start_time, end_time=self.end_time) self.start_time = self.end_time # the subscriber for the batch processing # To trigger the batch notification, have the scheduler create a timer with event_origin = process_batch_key self.batch_processing_subscriber = EventSubscriber( event_type=OT.TimerEvent, origin=process_batch_key, callback=process ) self.add_endpoint(self.batch_processing_subscriber) def create_notification(self, notification=None, user_id=''): """ Persists the provided NotificationRequest object for the specified Origin id. Associate the Notification resource with the user_id string. returned id is the internal id by which NotificationRequest will be identified in the data store. @param notification NotificationRequest @param user_id str @retval notification_id str @throws BadRequest if object passed has _id or _rev attribute """ if not user_id: raise BadRequest("User id not provided.") log.debug("Create notification called for user_id: %s, and notification: %s", user_id, notification) #--------------------------------------------------------------------------------------------------- # Persist Notification object as a resource if it has already not been persisted #--------------------------------------------------------------------------------------------------- notification_id = None # if the notification has already been registered, simply use the old id existing_user_notifications = self.get_user_notifications(user_info_id=user_id) if existing_user_notifications: notification_id = self._notification_in_notifications(notification, existing_user_notifications) # since the notification has not been registered yet, register it and get the id temporal_bounds = TemporalBounds() temporal_bounds.start_datetime = get_ion_ts() temporal_bounds.end_datetime = '' if not notification_id: notification.temporal_bounds = temporal_bounds notification_id, rev = self.clients.resource_registry.create(notification) else: log.debug("Notification object has already been created in resource registry before. No new id to be generated. notification_id: %s", notification_id) # Read the old notification already in the resource registry notification = self.clients.resource_registry.read(notification_id) # Update the temporal bounds of the old notification resource notification.temporal_bounds = temporal_bounds # Update the notification in the resource registry self.clients.resource_registry.update(notification) log.debug("The temporal bounds for this resubscribed notification object with id: %s, is: %s", notification._id,notification.temporal_bounds) # Link the user and the notification with a hasNotification association assocs= self.clients.resource_registry.find_associations(subject=user_id, predicate=PRED.hasNotification, object=notification_id, id_only=True) if assocs: log.debug("Got an already existing association: %s, between user_id: %s, and notification_id: %s", assocs,user_id,notification_id) return notification_id else: log.debug("Creating association between user_id: %s, and notification_id: %s", user_id, notification_id ) self.clients.resource_registry.create_association(user_id, PRED.hasNotification, notification_id) # read the registered notification request object because this has an _id and is more useful notification = self.clients.resource_registry.read(notification_id) #------------------------------------------------------------------------------------------------------------------- # Generate an event that can be picked by a notification worker so that it can update its user_info dictionary #------------------------------------------------------------------------------------------------------------------- #log.debug("(create notification) Publishing ReloadUserInfoEvent for notification_id: %s", notification_id) self.event_publisher.publish_event( event_type= OT.ReloadUserInfoEvent, origin="UserNotificationService", description= "A notification has been created.", notification_id = notification_id) return notification_id def update_notification(self, notification=None, user_id = ''): """Updates the provided NotificationRequest object. Throws NotFound exception if an existing version of NotificationRequest is not found. Throws Conflict if the provided NotificationRequest object is not based on the latest persisted version of the object. @param notification NotificationRequest @throws BadRequest if object does not have _id or _rev attribute @throws NotFound object with specified id does not exist @throws Conflict object not based on latest persisted object version """ raise NotImplementedError("This method needs to be worked out in terms of implementation") # #------------------------------------------------------------------------------------------------------------------- # # Get the old notification # #------------------------------------------------------------------------------------------------------------------- # # old_notification = self.clients.resource_registry.read(notification._id) # # #------------------------------------------------------------------------------------------------------------------- # # Update the notification in the notifications dict # #------------------------------------------------------------------------------------------------------------------- # # # self._update_notification_in_notifications_dict(new_notification=notification, # notifications=self.notifications) # #------------------------------------------------------------------------------------------------------------------- # # Update the notification in the registry # #------------------------------------------------------------------------------------------------------------------- # ''' # Since one user should not be able to update the notification request resource without the knowledge of other users # who have subscribed to the same notification request, we do not update the resource in the resource registry # ''' # ## self.clients.resource_registry.update(notification) # # #------------------------------------------------------------------------------------------------------------------- # # reading up the notification object to make sure we have the newly registered notification request object # #------------------------------------------------------------------------------------------------------------------- # # notification_id = notification._id # notification = self.clients.resource_registry.read(notification_id) # # #------------------------------------------------------------------------------------ # # Update the UserInfo object # #------------------------------------------------------------------------------------ # # user = self.update_user_info_object(user_id, notification) # # #------------------------------------------------------------------------------------------------------------------- # # Generate an event that can be picked by notification workers so that they can update their user_info dictionary # #------------------------------------------------------------------------------------------------------------------- # log.info("(update notification) Publishing ReloadUserInfoEvent for updated notification") # # self.event_publisher.publish_event( event_type= "ReloadUserInfoEvent", # origin="UserNotificationService", # description= "A notification has been updated.", # notification_id = notification_id # ) def read_notification(self, notification_id=''): """Returns the NotificationRequest object for the specified notification id. Throws exception if id does not match any persisted NotificationRequest objects. @param notification_id str @retval notification NotificationRequest @throws NotFound object with specified id does not exist """ notification = self.clients.resource_registry.read(notification_id) return notification def delete_notification(self, notification_id=''): """For now, permanently deletes NotificationRequest object with the specified id. Throws exception if id does not match any persisted NotificationRequest. @param notification_id str @throws NotFound object with specified id does not exist """ #------------------------------------------------------------------------------------------------------------------- # Stop the event subscriber for the notification #------------------------------------------------------------------------------------------------------------------- notification_request = self.clients.resource_registry.read(notification_id) #------------------------------------------------------------------------------------------------------------------- # Update the resource registry #------------------------------------------------------------------------------------------------------------------- notification_request.temporal_bounds.end_datetime = get_ion_ts() self.clients.resource_registry.update(notification_request) #------------------------------------------------------------------------------------------------------------------- # Find users who are interested in the notification and update the notification in the list maintained by the UserInfo object #------------------------------------------------------------------------------------------------------------------- # user_ids, _ = self.clients.resource_registry.find_subjects(RT.UserInfo, PRED.hasNotification, notification_id, True) # # for user_id in user_ids: # self.update_user_info_object(user_id, notification_request) #------------------------------------------------------------------------------------------------------------------- # Generate an event that can be picked by a notification worker so that it can update its user_info dictionary #------------------------------------------------------------------------------------------------------------------- log.info("(delete notification) Publishing ReloadUserInfoEvent for notification_id: %s", notification_id) self.event_publisher.publish_event( event_type= OT.ReloadUserInfoEvent, origin="UserNotificationService", description= "A notification has been deleted.", notification_id = notification_id) # def delete_notification_from_user_info(self, notification_id): # """ # Helper method to delete the notification from the user_info dictionary # # @param notification_id str # """ # # user_ids, assocs = self.clients.resource_registry.find_subjects(object=notification_id, predicate=PRED.hasNotification, id_only=True) # # for assoc in assocs: # self.clients.resource_registry.delete_association(assoc) # # for user_id in user_ids: # # value = self.user_info[user_id] # # for notif in value['notifications']: # if notification_id == notif._id: # # remove the notification # value['notifications'].remove(notif) # # self.reverse_user_info = calculate_reverse_user_info(self.user_info) def get_user_notifications(self, user_info_id=''): """ Get the notification request objects that are subscribed to by the user @param user_info_id str @retval notifications list of NotificationRequest objects """ notifications = [] user_notif_req_objs, _ = self.clients.resource_registry.find_objects( subject=user_info_id, predicate=PRED.hasNotification, object_type=RT.NotificationRequest, id_only=False) log.debug("Got %s notifications, for the user: %s", len(user_notif_req_objs), user_info_id) for notif in user_notif_req_objs: # do not include notifications that have expired if notif.temporal_bounds.end_datetime == '': notifications.append(notif) return notifications def create_worker(self, number_of_workers=1): """ Creates notification workers @param number_of_workers int @retval pids list """ pids = [] for n in xrange(number_of_workers): process_definition = ProcessDefinition( name='notification_worker_%s' % n) process_definition.executable = { 'module': 'ion.processes.data.transforms.notification_worker', 'class':'NotificationWorker' } process_definition_id = self.clients.process_dispatcher.create_process_definition(process_definition=process_definition) # ------------------------------------------------------------------------------------ # Process Spawning # ------------------------------------------------------------------------------------ pid2 = self.clients.process_dispatcher.create_process(process_definition_id) #@todo put in a configuration configuration = {} configuration['process'] = dict({ 'name': 'notification_worker_%s' % n, 'type':'simple', 'queue_name': 'notification_worker_queue' }) pid = self.clients.process_dispatcher.schedule_process( process_definition_id, configuration = configuration, process_id=pid2 ) pids.append(pid) return pids def process_batch(self, start_time = '', end_time = ''): """ This method is launched when an process_batch event is received. The user info dictionary maintained by the User Notification Service is used to query the event repository for all events for a particular user that have occurred in a provided time interval, and then an email is sent to the user containing the digest of all the events. @param start_time int milliseconds @param end_time int milliseconds """ self.smtp_client = setting_up_smtp_client() if end_time <= start_time: return for user_id, value in self.user_info.iteritems(): notifications = self.get_user_notifications(user_info_id=user_id) notifications_disabled = value['notifications_disabled'] notifications_daily_digest = value['notifications_daily_digest'] # Ignore users who do NOT want batch notifications or who have disabled the delivery switch # However, if notification preferences have not been set for the user, use the default mechanism and do not bother if notifications_disabled or not notifications_daily_digest: continue events_for_message = [] search_time = "SEARCH 'ts_created' VALUES FROM %s TO %s FROM 'events_index'" % (start_time, end_time) for notification in notifications: # If the notification request has expired, then do not use it in the search if notification.temporal_bounds.end_datetime: continue event_tuples = self.container.event_repository.find_events( origin=notification.origin, event_type=notification.event_type, start_ts=start_time, end_ts=end_time) events = [item[2] for item in event_tuples] events_for_message.extend(events) log.debug("Found following events of interest to user, %s: %s", user_id, events_for_message) # send a notification email to each user using a _send_email() method if events_for_message: self.format_and_send_email(events_for_message = events_for_message, user_id = user_id, smtp_client=self.smtp_client) self.smtp_client.quit() def format_and_send_email(self, events_for_message=None, user_id=None, smtp_client=None): """ Format the message for a particular user containing information about the events he is to be notified about @param events_for_message list @param user_id str """ message = str(events_for_message) log.debug("The user, %s, will get the following events in his batch notification email: %s", user_id, message) msg = convert_events_to_email_message(events_for_message, self.clients.resource_registry) msg["Subject"] = "(SysName: " + get_sys_name() + ") ION event " msg["To"] = self.user_info[user_id]['user_contact'].email self.send_batch_email(msg, smtp_client) def send_batch_email(self, msg=None, smtp_client=None): """ Send the email @param msg MIMEText object of email message @param smtp_client object """ if msg is None: msg = {} for f in ["Subject", "To"]: if not f in msg: raise BadRequest("'%s' not in msg %s" % (f, msg)) msg_subject = msg["Subject"] msg_recipient = msg["To"] msg['From'] = self.ION_NOTIFICATION_EMAIL_ADDRESS log.debug("UNS sending batch (digest) email from %s to %s", self.ION_NOTIFICATION_EMAIL_ADDRESS, msg_recipient) smtp_sender = CFG.get_safe('server.smtp.sender') smtp_client.sendmail(smtp_sender, [msg_recipient], msg.as_string()) def update_user_info_object(self, user_id, new_notification): """ Update the UserInfo object. If the passed in parameter, od_notification, is None, it does not need to remove the old notification @param user_id str @param new_notification NotificationRequest """ #this is not necessary if notifiactions are not stored in the userinfo object raise NotImplementedError("This method is not necessary because Notifications are not stored in the userinfo object") #------------------------------------------------------------------------------------ # read the user #------------------------------------------------------------------------------------ # user = self.clients.resource_registry.read(user_id) # # if not user: # raise BadRequest("No user with the provided user_id: %s" % user_id) # # for item in user.variables: # if type(item) is dict and item.has_key('name') and item['name'] == 'notifications': # for notif in item['value']: # if notif._id == new_notification._id: # log.debug("came here for updating notification") # notifications = item['value'] # notifications.remove(notif) # notifications.append(new_notification) # break # else: # log.warning('Invalid variables attribute on UserInfo instance. UserInfo: %s', user) # # # #------------------------------------------------------------------------------------ # # update the resource registry # #------------------------------------------------------------------------------------ # # log.debug("user.variables::: %s", user.variables) # # self.clients.resource_registry.update(user) # # return user def get_subscriptions(self, resource_id='', user_id = '', include_nonactive=False): """ @param resource_id a resource id (or other origin) that is the origin of events for notifications @param user_id a UserInfo ID that owns the NotificationRequest @param include_nonactive if False, filter to active NotificationRequest only Return all NotificationRequest resources where origin is given resource_id. """ notif_reqs, _ = self.clients.resource_registry.find_resources_ext( restype=RT.NotificationRequest, attr_name="origin", attr_value=resource_id, id_only=False) log.debug("Got %s active and past NotificationRequests for resource/origin %s", len(notif_reqs), resource_id) if not include_nonactive: notif_reqs = [nr for nr in notif_reqs if nr.temporal_bounds.end_datetime == ''] log.debug("Filtered to %s active NotificationRequests", len(notif_reqs)) if user_id: # Get all NotificationRequests (ID) that are associated to given UserInfo_id user_notif_req_ids, _ = self.clients.resource_registry.find_objects( subject=user_id, predicate=PRED.hasNotification, object_type=RT.NotificationRequest, id_only=True) notif_reqs = [nr for nr in notif_reqs if nr._id in user_notif_req_ids] log.debug("Filtered to %s NotificationRequests associated to user %s", len(notif_reqs), user_id) return notif_reqs def get_subscriptions_attribute(self, resource_id='', user_id = '', include_nonactive=False): retval = self.get_subscriptions(resource_id=resource_id, user_id=user_id, include_nonactive=include_nonactive) container = ComputedListValue(value=retval) return container def _notification_in_notifications(self, notification = None, notifications = None): for notif in notifications: if notif.name == notification.name and \ notif.origin == notification.origin and \ notif.origin_type == notification.origin_type and \ notif.event_type == notification.event_type: return notif._id return None def load_user_info(self): """ Method to load the user info dictionary used by the notification workers and the UNS @retval user_info dict """ users, _ = self.clients.resource_registry.find_resources(restype=RT.UserInfo) user_info = {} if not users: return {} for user in users: notifications = [] notifications_disabled = False notifications_daily_digest = False #retrieve all the active notifications assoc to this user notifications = self.get_user_notifications(user_info_id=user) log.debug('load_user_info notifications: %s', notifications) for variable in user.variables: if type(variable) is dict and variable.has_key('name'): if variable['name'] == 'notifications_daily_digest': notifications_daily_digest = variable['value'] if variable['name'] == 'notifications_disabled': notifications_disabled = variable['value'] else: log.warning('Invalid variables attribute on UserInfo instance. UserInfo: %s', user) user_info[user._id] = { 'user_contact' : user.contact, 'notifications' : notifications, 'notifications_daily_digest' : notifications_daily_digest, 'notifications_disabled' : notifications_disabled} return user_info # ------------------------------------------------------------------------- # Governance operations def check_subscription_policy(self, process, message, headers): try: gov_values = GovernanceHeaderValues(headers=headers, process=process, resource_id_required=False) except Inconsistent, ex: return False, ex.message if gov_values.op == 'delete_notification': return True, '' notification = message['notification'] resource_id = notification.origin if notification.origin_type == RT.Org: org = self.clients.resource_registry.read(resource_id) if (has_org_role(gov_values.actor_roles, org.org_governance_name, [ORG_MEMBER_ROLE])): return True, '' else: orgs,_ = self.clients.resource_registry.find_subjects(subject_type=RT.Org, predicate=PRED.hasResource, object=resource_id, id_only=False) for org in orgs: if (has_org_role(gov_values.actor_roles, org.org_governance_name, [ORG_MEMBER_ROLE])): return True, '' return False, '%s(%s) has been denied since the user is not a member in any org to which the resource id %s belongs ' % (process.name, gov_values.op, resource_id)
def test_user_role_cache(self): # Create a user id_client = IdentityManagementServiceClient() actor_id, valid_until, registered = id_client.signon(USER1_CERTIFICATE, True) # Make a request with this new user to get it into the cache response = self.test_app.get( "/ion-service/resource_registry/find_resources?name=TestDataProduct&id_only=True&requester=" + actor_id ) self.check_response_headers(response) self.assertIn(GATEWAY_RESPONSE, response.json["data"]) # Check the contents of the user role cache for this user service_gateway_user_role_cache = self.container.proc_manager.procs_by_name["service_gateway"].user_role_cache self.assertEqual(service_gateway_user_role_cache.has_key(actor_id), True) role_header = service_gateway_user_role_cache.get(actor_id) self.assertIn("ION", role_header) self.assertEqual(len(role_header["ION"]), 1) self.assertIn("ORG_MEMBER", role_header["ION"]) org_client = OrgManagementServiceClient() ion_org = org_client.find_org() manager_role = org_client.find_org_role_by_name(org_id=ion_org._id, role_name="ORG_MANAGER") org_client.grant_role(org_id=ion_org._id, actor_id=actor_id, role_name="ORG_MANAGER") # Just allow some time for event processing on slower platforms gevent.sleep(2) # The user should be evicted from the cache due to a change in roles self.assertEqual(service_gateway_user_role_cache.has_key(actor_id), False) # Do it again to check for new roles response = self.test_app.get( "/ion-service/resource_registry/find_resources?name=TestDataProduct&id_only=True&requester=" + actor_id ) self.check_response_headers(response) self.assertIn(GATEWAY_RESPONSE, response.json["data"]) # Check the contents of the user role cache for this user self.assertEqual(service_gateway_user_role_cache.has_key(actor_id), True) role_header = service_gateway_user_role_cache.get(actor_id) self.assertIn("ION", role_header) self.assertEqual(len(role_header["ION"]), 2) self.assertIn("ORG_MEMBER", role_header["ION"]) self.assertIn("ORG_MANAGER", role_header["ION"]) # Now flush the user_role_cache and make sure it was flushed event_publisher = EventPublisher() event_publisher.publish_event(event_type=OT.UserRoleCacheResetEvent) # Just allow some time for event processing on slower platforms gevent.sleep(2) self.assertEqual(service_gateway_user_role_cache.has_key(actor_id), False) self.assertEqual(service_gateway_user_role_cache.size(), 0) # Change the role once again and see if it is there again org_client.revoke_role(org_id=ion_org._id, actor_id=actor_id, role_name="ORG_MANAGER") # Just allow some time for event processing on slower platforms gevent.sleep(2) # The user should still not be there self.assertEqual(service_gateway_user_role_cache.has_key(actor_id), False) # Do it again to check for new roles response = self.test_app.get( "/ion-service/resource_registry/find_resources?name=TestDataProduct&id_only=True&requester=" + actor_id ) self.check_response_headers(response) self.assertIn(GATEWAY_RESPONSE, response.json["data"]) # Check the contents of the user role cache for this user self.assertEqual(service_gateway_user_role_cache.has_key(actor_id), True) role_header = service_gateway_user_role_cache.get(actor_id) self.assertIn("ION", role_header) self.assertEqual(len(role_header["ION"]), 1) self.assertIn("ORG_MEMBER", role_header["ION"]) id_client.delete_actor_identity(actor_id)
class PolicyManagementService(BasePolicyManagementService): event_pub = None def on_start(self): self.event_pub = EventPublisher(process=self) # ------------------------------------------------------------------------- # Policy management def create_resource_access_policy(self, resource_id='', policy_name='', description='', policy_rule='', ordinal=0): """Boilerplate operation for creating an access policy for a specific resource. """ if not resource_id: raise BadRequest("The resource_id argument is missing") if not policy_name: raise BadRequest("The policy_name argument is missing") if not description: raise BadRequest("The description argument is missing") if not policy_rule: raise BadRequest("The policy_rule argument is missing") policy_obj = IonObject(RT.Policy, name=policy_name, description=description, policy_type=PolicyTypeEnum.RESOURCE_ACCESS, definition=policy_rule, ordinal=ordinal, details=IonObject( OT.ResourceAccessPolicyDetails, resource_id=resource_id)) policy_id = self.create_policy(policy_obj) self._add_resource_policy(resource_id, policy_id, publish_event=False) return policy_id def create_service_access_policy(self, service_name='', policy_name='', description='', policy_rule='', ordinal=0): """Boilerplate operation for creating an access policy for a specific service. """ if not service_name: raise BadRequest("The service_name argument is missing") if not policy_name: raise BadRequest("The policy_name argument is missing") if not description: raise BadRequest("The description argument is missing") if not policy_rule: raise BadRequest("The policy_rule argument is missing") policy_obj = IonObject(RT.Policy, name=policy_name, description=description, policy_type=PolicyTypeEnum.SERVICE_ACCESS, definition=policy_rule, ordinal=ordinal, details=IonObject(OT.ServiceAccessPolicyDetails, service_name=service_name)) return self.create_policy(policy_obj) def create_common_service_access_policy(self, policy_name='', description='', policy_rule='', ordinal=0): """Boilerplate operation for creating a service access policy common to all services. """ if not policy_name: raise BadRequest("The policy_name argument is missing") if not description: raise BadRequest("The description argument is missing") if not policy_rule: raise BadRequest("The policy_rule argument is missing") policy_obj = IonObject( RT.Policy, name=policy_name, description=description, policy_type=PolicyTypeEnum.COMMON_SERVICE_ACCESS, definition=policy_rule, ordinal=ordinal, ) return self.create_policy(policy_obj) def add_process_operation_precondition_policy(self, process_id='', op='', policy_content=''): """Boilerplate operation for adding a precondition policy for a specific process operation. The precondition method must return a tuple (boolean, string). """ if not process_id: raise BadRequest("The process_id argument is missing") if not op: raise BadRequest("The op argument is missing") if not policy_content: raise BadRequest("The policy_content argument is missing") policy_name = "Process_" + process_id + "_" + op + "_Precondition_Policies" policies, _ = self.clients.resource_registry.find_resources( restype=RT.Policy, name=policy_name) if policies: # Update existing policy by adding to list if len(policies) > 1: raise Inconsistent( 'There should only be one Policy object per process and operation' ) policy_obj = policies[0] if policy_obj.details.type_ != OT.ProcessOperationPreconditionPolicyDetails or policy_obj.details.op != op: raise Inconsistent( 'The Policy %s does not match the requested process operation %s: %s' % (policy_obj.name, process_id, op)) policy_obj.details.preconditions.append(policy_content) self.update_policy(policy_obj) return policy_obj._id else: policy_obj = IonObject( RT.Policy, name=policy_name, description= 'List of operation precondition policies for process ' + process_id, policy_type=PolicyTypeEnum.PROC_OP_PRECOND, details=IonObject(OT.ProcessOperationPreconditionPolicyDetails, process_id=process_id, op=op, preconditions=[policy_content])) return self.create_policy(policy_obj) def add_service_operation_precondition_policy(self, service_name='', op='', policy_content=''): """Boilerplate operation for adding a precondition policy for a specific service operation. The precondition method must return a tuple (boolean, string). """ if not service_name: raise BadRequest("The service_name argument is missing") if not op: raise BadRequest("The op argument is missing") if not policy_content: raise BadRequest("The policy_content argument is missing") policy_name = "Service_" + service_name + "_" + op + "_Precondition_Policies" policies, _ = self.clients.resource_registry.find_resources( restype=RT.Policy, name=policy_name) if policies: # Update existing policy by adding to list if len(policies) > 1: raise Inconsistent( 'There should only be one Policy object per service and operation' ) policy_obj = policies[0] if policy_obj.details.type_ != OT.ServiceOperationPreconditionPolicyDetails or policy_obj.details.op != op: raise Inconsistent( 'The Policy %s does not match the requested service operation %s: %s' % (policy_obj.name, service_name, op)) policy_obj.details.preconditions.append(policy_content) self.update_policy(policy_obj) return policy_obj._id else: policy_obj = IonObject( RT.Policy, name=policy_name, description= 'List of operation precondition policies for service ' + service_name, policy_type=PolicyTypeEnum.SERVICE_OP_PRECOND, details=IonObject(OT.ProcessOperationPreconditionPolicyDetails, service_name=service_name, op=op, preconditions=[policy_content])) return self.create_policy(policy_obj) def create_policy(self, policy=None): """Persists the provided Policy object. Returns the policy id. """ self._validate_resource_obj("policy", policy, RT.Policy, checks="noid,name") if not is_basic_identifier(policy.name): raise BadRequest( "The policy name '%s' can only contain alphanumeric and underscore characters" % policy.name) try: # If there is a policy_rule field then try to add the policy name and description to the rule text if policy.definition: rule_tokens = dict(rule_id=policy.name, description=policy.description) policy.definition = policy.definition.format(**rule_tokens) except Exception as e: raise Inconsistent( "Missing the elements in the policy rule to set the description: " + e.message) policy_id, _ = self.clients.resource_registry.create(policy) policy._id = policy_id log.debug('Policy created: ' + policy.name) self._publish_policy_event(policy) return policy_id def update_policy(self, policy=None): """Updates the provided Policy object. """ self._validate_resource_obj("policy", policy, RT.Policy, checks="id,name") if not is_basic_identifier(policy.name): raise BadRequest( "The policy name '%s' can only contain alphanumeric and underscore characters" % policy.name) self.clients.resource_registry.update(policy) self._publish_policy_event(policy) def read_policy(self, policy_id=''): """Returns the Policy object for the specified policy id. Throws exception if id does not match any persisted Policy objects. """ policy_obj = self._validate_resource_id("policy_id", policy_id, RT.Policy) return policy_obj def delete_policy(self, policy_id=''): """For now, permanently deletes Policy object with the specified id. Throws exception if id does not match any persisted Policy. """ policy_obj = self._validate_resource_id("policy_id", policy_id, RT.Policy) res_list = self._find_resources_for_policy(policy_id) for res in res_list: self._remove_resource_policy(res, policy_obj) self.clients.resource_registry.delete(policy_id) self._publish_policy_event(policy_obj, delete_policy=True) def enable_policy(self, policy_id=''): """Sets a flag to enable the use of the policy """ policy_obj = self._validate_resource_id("policy_id", policy_id, RT.Policy) policy_obj.enabled = True self.update_policy(policy_obj) def disable_policy(self, policy_id=''): """Resets a flag to disable the use of the policy """ policy_obj = self._validate_resource_id("policy_id", policy_id, RT.Policy) policy_obj.enabled = False self.update_policy(policy_obj) def add_resource_policy(self, resource_id='', policy_id=''): """Associates a policy to a specific resource """ resource_obj, policy_obj = self._add_resource_policy( resource_id, policy_id) return True def _add_resource_policy(self, resource_id, policy_id, publish_event=True): """Removing a policy resource association and publish event for containers to update """ resource_obj = self._validate_resource_id("resource_id", resource_id) policy_obj = self._validate_resource_id("policy_id", policy_id, RT.Policy) self.clients.resource_registry.create_association( resource_obj, PRED.hasPolicy, policy_obj) # Publish an event that the resource policy has changed if publish_event: self._publish_resource_policy_event(policy_obj, resource_obj) return resource_obj, policy_obj def remove_resource_policy(self, resource_id='', policy_id=''): """Removes an association for a policy to a specific resource """ resource_obj = self._validate_resource_id("resource_id", resource_id) policy_obj = self._validate_resource_id("policy_id", policy_id, RT.Policy) self._remove_resource_policy(resource_obj, policy_obj) return True def _remove_resource_policy(self, resource, policy): aid = self.clients.resource_registry.get_association( resource, PRED.hasPolicy, policy) self.clients.resource_registry.delete_association(aid) # Publish an event that the resource policy has changed self._publish_resource_policy_event(policy, resource) def _publish_policy_event(self, policy, delete_policy=False): if policy.policy_type == PolicyTypeEnum.COMMON_SERVICE_ACCESS: self._publish_service_policy_event(policy, delete_policy) elif policy.policy_type == PolicyTypeEnum.SERVICE_ACCESS or policy.policy_type == PolicyTypeEnum.SERVICE_OP_PRECOND: self._publish_service_policy_event(policy, delete_policy) else: # Need to publish an event that a policy has changed for any associated resource res_list = self._find_resources_for_policy(policy._id) for res in res_list: self._publish_resource_policy_event(policy, res, delete_policy) def _publish_resource_policy_event(self, policy, resource, delete_policy=False): if self.event_pub: event_data = dict() event_data['origin_type'] = 'Resource_Policy' event_data['description'] = 'Updated Resource Policy' event_data['resource_id'] = resource._id event_data['resource_type'] = resource.type_ event_data['resource_name'] = resource.name event_data['sub_type'] = 'DeletePolicy' if delete_policy else '' self.event_pub.publish_event(event_type='ResourcePolicyEvent', origin=policy._id, **event_data) def _publish_related_resource_policy_event(self, policy, resource_id, delete_policy=False): if self.event_pub: event_data = dict() event_data['origin_type'] = 'Resource_Policy' event_data['description'] = 'Updated Related Resource Policy' event_data['resource_id'] = resource_id event_data['sub_type'] = 'DeletePolicy' if delete_policy else '' self.event_pub.publish_event( event_type='RelatedResourcePolicyEvent', origin=policy._id, **event_data) def _publish_service_policy_event(self, policy, delete_policy=False): if self.event_pub: event_data = dict() event_data['origin_type'] = 'Service_Policy' event_data['description'] = 'Updated Service Policy' event_data['sub_type'] = 'DeletePolicy' if delete_policy else '' event_data['service_name'] = getattr(policy.details, 'service_name', "") if policy.policy_type == PolicyTypeEnum.SERVICE_OP_PRECOND: event_data['op'] = policy.details.op self.event_pub.publish_event(event_type='ServicePolicyEvent', origin=policy._id, **event_data) def find_resource_policies(self, resource_id=''): """Finds all policies associated with a specific resource """ resource_obj = self._validate_resource_id("resource_id", resource_id) return self._find_resource_policies(resource_obj) def _find_resource_policies(self, resource, policy=None): policy_list, _ = self.clients.resource_registry.find_objects( resource, PRED.hasPolicy, policy) return policy_list def _find_resources_for_policy(self, policy_id=''): """Finds all resources associated with a specific policy """ resource_list, _ = self.clients.resource_registry.find_subjects( None, PRED.hasPolicy, policy_id) return resource_list def get_active_resource_access_policy_rules(self, resource_id='', org_name=''): """Generates the set of all enabled access policies for the specified resource within the specified Org. If the org_name is not provided, then the root ION Org will be assumed. """ # TODO - extend to handle Org specific service policies at some point. policy_list = [] resource_obj = self._validate_resource_id("resource_id", resource_id) resource_id_list = self._get_related_resource_ids(resource_obj) if not resource_id_list: resource_id_list.append(resource_id) log.debug("Retrieving policies for resources: %s", resource_id_list) for res_id in resource_id_list: policy_set = self._find_resource_policies(res_id) for p in policy_set: if p.enabled and p.policy_type == PolicyTypeEnum.RESOURCE_ACCESS: log.debug("Including policy: %s", p.name) policy_list.append(p) policy_list.sort(key=lambda o: (o.ordinal, o.ts_created)) return policy_list def _get_related_resource_ids(self, resource): """For given resource object, find related resources based on type""" resource_id_list = [] # TODO - This could be following associations (parents, children, etc) return resource_id_list def get_active_service_access_policy_rules(self, service_name='', org_name=''): """Generates the set of all enabled access policies for the specified service within the specified Org. If the org_name is not provided, then the root ION Org will be assumed. """ # TODO - extend to handle Org specific service policies at some point. rq = ResourceQuery() rq.set_filter(rq.filter_type(RT.Policy), rq.filter_attribute("enabled", True)) if service_name: rq.add_filter( rq.or_( rq.filter_attribute("policy_type", PolicyTypeEnum.COMMON_SERVICE_ACCESS), rq.and_( rq.filter_attribute("policy_type", [ PolicyTypeEnum.SERVICE_ACCESS, PolicyTypeEnum.SERVICE_OP_PRECOND ]), rq.filter_attribute("details.service_name", service_name)))) else: rq.add_filter( rq.filter_attribute("policy_type", PolicyTypeEnum.COMMON_SERVICE_ACCESS)) policy_list = self.clients.resource_registry.find_resources_ext( query=rq.get_query(), id_only=False) policy_list.sort(key=lambda o: (o.ordinal, o.ts_created)) return policy_list def get_active_service_operation_preconditions(self, service_name='', op='', org_name=''): """Generates the set of all enabled precondition policies for the specified service operation within the specified Org. If the org_name is not provided, then the root ION Org will be assumed. """ # TODO - extend to handle Org specific service policies at some point. if not service_name: raise BadRequest("The service_name argument is missing") rq = ResourceQuery() rq.set_filter( rq.filter_type(RT.Policy), rq.filter_attribute("enabled", True), rq.filter_attribute("policy_type", PolicyTypeEnum.SERVICE_OP_PRECOND), rq.filter_attribute("details.service_name", service_name)) if op: rq.add_filter(rq.filter_attribute("details.op", op)) policy_list = self.clients.resource_registry.find_resources_ext( query=rq.get_query(), id_only=False) policy_list.sort(key=lambda o: (o.ordinal, o.ts_created)) return policy_list def get_active_process_operation_preconditions(self, process_key='', op='', org_name=''): """Generates the set of all enabled precondition policies for the specified process operation within the specified Org. If the org_name is not provided, then the root ION Org will be assumed. """ # TODO - extend to handle Org specific service policies at some point. if not process_key: raise BadRequest("The process_key argument is missing") rq = ResourceQuery() rq.set_filter( rq.filter_type(RT.Policy), rq.filter_attribute("enabled", True), rq.filter_attribute("policy_type", PolicyTypeEnum.PROC_OP_PRECOND), rq.filter_attribute("details.process_key", process_key)) if op: rq.add_filter(rq.filter_attribute("details.op", op)) policy_list = self.clients.resource_registry.find_resources_ext( query=rq.get_query(), id_only=False) policy_list.sort(key=lambda o: (o.ordinal, o.ts_created)) return policy_list # Local helper functions for testing policies - do not remove def func1_pass(self, msg, header): return True, '' def func2_deny(self, msg, header): return False, 'Denied for no reason'
def perform_action(self, predicate=None, action=None): userid = None # get from context event_pub = EventPublisher(process=self) event_pub.publish_event(event_type=OT.ContainerManagementRequest, origin=userid, predicate=predicate, action=action)
class OrgManagementService(BaseOrgManagementService): """ Services to define and administer an Org (organization, facility), to enroll/remove members and to provide access to the resources of an Org to enrolled or affiliated entities (identities). Contains contract and commitment repository """ def on_init(self): self.rr = self.clients.resource_registry self.event_pub = EventPublisher(process=self) self.negotiation_handler = Negotiation(self, negotiation_rules, self.event_pub) self.root_org_id = None def _get_root_org_name(self): if self.container is None or self.container.governance_controller is None: return CFG.get_safe("system.root_org", "ION") return self.container.governance_controller.system_root_org_name def _get_root_org_id(self): if not self.root_org_id: root_org = self.find_org() self.root_org_id = root_org._id return self.root_org_id def _validate_user_role(self, arg_name, role_name, org_id): """ Check that the given argument is a resource id, by retrieving the resource from the resource registry. Additionally checks type and returns the result object """ if not role_name: raise BadRequest("The argument '%s' is missing" % arg_name) if not org_id: raise BadRequest("The argument 'org_id' is missing") user_role = self._find_org_role(org_id, role_name) return user_role # ------------------------------------------------------------------------- # Org management (CRUD) def create_org(self, org=None): """Creates an Org based on the provided object. The id string returned is the internal id by which Org will be identified in the data store. """ # Only allow one root ION Org in the system self._validate_resource_obj("org", org, RT.Org, checks="noid,name,unique") # If this governance identifier is not set, then set to a safe version of the org name. if not org.org_governance_name: org.org_governance_name = create_basic_identifier(org.name) if not is_basic_identifier(org.org_governance_name): raise BadRequest( "The Org org_governance_name '%s' contains invalid characters" % org.org_governance_name) org_id, _ = self.rr.create(org) # Instantiate a Directory for this Org directory = Directory(orgname=org.name) # Instantiate initial set of User Roles for this Org self._create_org_roles(org_id) return org_id def _create_org_roles(self, org_id): # Instantiate initial set of User Roles for this Org manager_role = IonObject( RT.UserRole, name="MODERATOR", governance_name=MODERATOR_ROLE, description="Manage organization members, resources and roles") self.add_org_role(org_id, manager_role) operator_role = IonObject( RT.UserRole, name="OPERATOR", governance_name=OPERATOR_ROLE, description="Modify and control organization resources") self.add_org_role(org_id, operator_role) member_role = IonObject(RT.UserRole, name="MEMBER", governance_name=MEMBER_ROLE, description="Access organization resources") self.add_org_role(org_id, member_role) def update_org(self, org=None): """Updates the Org based on provided object. """ old_org = self._validate_resource_obj("org", org, RT.Org, checks="id") if org.org_governance_name != old_org.org_governance_name: raise BadRequest("Cannot update Org org_governance_name") self.rr.update(org) def read_org(self, org_id=""): """Returns the Org object for the specified id. Throws exception if id does not match any persisted Org objects. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) return org_obj def delete_org(self, org_id=""): """Permanently deletes Org object with the specified id. Throws exception if id does not match any persisted Org object. """ self._validate_resource_id("org_id", org_id, RT.Org) self.rr.delete(org_id) def find_org(self, name=""): """Finds an Org object with the specified name. Defaults to the root ION object. Throws a NotFound exception if the object does not exist. """ # Default to the root ION Org if not specified if not name: name = self._get_root_org_name() res_list, _ = self.rr.find_resources(restype=RT.Org, name=name, id_only=False) if not res_list: raise NotFound("The Org with name %s does not exist" % name) return res_list[0] # ------------------------------------------------------------------------- # Org roles def add_org_role(self, org_id="", user_role=None): """Adds a UserRole to an Org, if the role by the specified name does not exist. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) self._validate_resource_obj("user_role", user_role, RT.UserRole, checks="noid,name") if not is_basic_identifier(user_role.governance_name): raise BadRequest("Invalid role governance_name") user_role.org_governance_name = org_obj.org_governance_name try: self._find_org_role(org_id, user_role.governance_name) raise BadRequest("Role '%s' is already associated with this Org" % user_role.governance_name) except NotFound: pass user_role_id, _ = self.rr.create(user_role) self.rr.create_association(org_obj, PRED.hasRole, user_role_id) return user_role_id def remove_org_role(self, org_id="", role_name="", force_removal=False): """Removes a UserRole from an Org. The UserRole will not be removed if there are users associated with the UserRole unless the force_removal parameter is set to True """ self._validate_resource_id("org_id", org_id, RT.Org) user_role = self._validate_user_role("role_name", role_name, org_id) if not force_removal: alist, _ = self.rr.find_subjects(RT.ActorIdentity, PRED.hasRole, user_role) if alist: raise BadRequest( "UserRole %s still in use and cannot be removed" % user_role.name) self.rr.delete(user_role._id) def find_org_role_by_name(self, org_id="", role_name=""): """Returns the UserRole object for the specified name in the Org. """ self._validate_resource_id("org_id", org_id, RT.Org) user_role = self._find_org_role(org_id, role_name) return user_role def _find_org_role(self, org_id="", role_name=""): if not org_id: raise BadRequest("The org_id argument is missing") if not role_name: raise BadRequest("The role_name argument is missing") # Iterating (vs. query) is just fine, because the number of org roles is sufficiently small org_roles = self._list_org_roles(org_id) for role in org_roles: if role.governance_name == role_name: return role raise NotFound("Role %s not found in Org id=%s" % (role_name, org_id)) def list_org_roles(self, org_id=""): """Returns a list of roles available in an Org. Will throw a not NotFound exception if none of the specified ids do not exist. """ self._validate_resource_id("org_id", org_id, RT.Org) return self._list_org_roles(org_id) def _list_org_roles(self, org_id=""): if not org_id: raise BadRequest("Illegal org_id") role_list, _ = self.rr.find_objects(org_id, PRED.hasRole, RT.UserRole, id_only=False) return role_list # ------------------------------------------------------------------------- # Negotiations def negotiate(self, sap=None): """A generic operation for negotiating actions with an Org, such as for enrollment, role request or to acquire a resource managed by the Org. The ServiceAgreementProposal object is used to specify conditions of the proposal as well as counter proposals and the Org will create a Negotiation resource to track the history and status of the negotiation. """ if sap is None or ( sap.type_ != OT.ServiceAgreementProposal and not issubtype(sap.type_, OT.ServiceAgreementProposal)): raise BadRequest( "The sap argument must be a valid ServiceAgreementProposal object" ) if sap.proposal_status == ProposalStatusEnum.INITIAL: neg_id = self.negotiation_handler.create_negotiation(sap) org = self.read_org(org_id=sap.provider) # Publish an event indicating an Negotiation has been initiated self.event_pub.publish_event( event_type=OT.OrgNegotiationInitiatedEvent, origin=org._id, origin_type="Org", description=sap.description, org_name=org.name, negotiation_id=neg_id, sub_type=sap.type_) # Synchronize the internal reference for later use sap.negotiation_id = neg_id # Get the most recent version of the Negotiation resource negotiation = self.negotiation_handler.read_negotiation(sap) # Update the Negotiation object with the latest SAP neg_id = self.negotiation_handler.update_negotiation(sap) # Get the most recent version of the Negotiation resource negotiation = self.rr.read(neg_id) # hardcoding some rules at the moment - could be replaced by a Rules Engine if sap.type_ == OT.AcquireResourceExclusiveProposal: if self.is_resource_acquired_exclusively(None, sap.resource_id): # Automatically accept the proposal for exclusive access if it is not already acquired exclusively provider_accept_sap = Negotiation.create_counter_proposal( negotiation, ProposalStatusEnum.REJECTED, ProposalOriginatorEnum.PROVIDER) rejection_reason = "The resource has already been acquired exclusively" # Update the Negotiation object with the latest SAP neg_id = self.negotiation_handler.update_negotiation( provider_accept_sap, rejection_reason) # Get the most recent version of the Negotiation resource negotiation = self.rr.read(neg_id) else: # Automatically reject the proposal if the expiration request is greater than 12 hours from now or 0 cur_time = int(get_ion_ts()) expiration = int(cur_time + (12 * 60 * 60 * 1000)) # 12 hours from now if int(sap.expiration) == 0 or int( sap.expiration) > expiration: # Automatically accept the proposal for exclusive access if it is not already acquired exclusively provider_accept_sap = Negotiation.create_counter_proposal( negotiation, ProposalStatusEnum.REJECTED, ProposalOriginatorEnum.PROVIDER) rejection_reason = "A proposal to acquire a resource exclusively must be more than 0 and be less than 12 hours." # Update the Negotiation object with the latest SAP neg_id = self.negotiation_handler.update_negotiation( provider_accept_sap, rejection_reason) # Get the most recent version of the Negotiation resource negotiation = self.rr.read(neg_id) else: # Automatically accept the proposal for exclusive access if it is not already acquired exclusively provider_accept_sap = Negotiation.create_counter_proposal( negotiation, ProposalStatusEnum.ACCEPTED, ProposalOriginatorEnum.PROVIDER) # Update the Negotiation object with the latest SAP neg_id = self.negotiation_handler.update_negotiation( provider_accept_sap) # Get the most recent version of the Negotiation resource negotiation = self.rr.read(neg_id) # Check to see if the rules allow for auto acceptance of the negotiations - # where the second party is assumed to accept if the # first party accepts. if negotiation_rules[sap.type_]["auto_accept"]: # Automatically accept for the consumer if the Org Manager as provider accepts the proposal latest_sap = negotiation.proposals[-1] if latest_sap.proposal_status == ProposalStatusEnum.ACCEPTED and latest_sap.originator == ProposalOriginatorEnum.PROVIDER: consumer_accept_sap = Negotiation.create_counter_proposal( negotiation, ProposalStatusEnum.ACCEPTED) # Update the Negotiation object with the latest SAP neg_id = self.negotiation_handler.update_negotiation( consumer_accept_sap) # Get the most recent version of the Negotiation resource negotiation = self.rr.read(neg_id) elif latest_sap.proposal_status == ProposalStatusEnum.ACCEPTED and latest_sap.originator == ProposalOriginatorEnum.CONSUMER: provider_accept_sap = Negotiation.create_counter_proposal( negotiation, ProposalStatusEnum.ACCEPTED, ProposalOriginatorEnum.PROVIDER) # Update the Negotiation object with the latest SAP neg_id = self.negotiation_handler.update_negotiation( provider_accept_sap) # Get the most recent version of the Negotiation resource negotiation = self.rr.read(neg_id) # Return the latest proposal return negotiation.proposals[-1] def find_org_negotiations(self, org_id="", proposal_type="", negotiation_status=-1): """Returns a list of negotiations for an Org. An optional proposal_type can be supplied or else all proposals will be returned. An optional negotiation_status can be supplied or else all proposals will be returned. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) neg_list, _ = self.rr.find_objects(org_id, PRED.hasNegotiation) if proposal_type: neg_list = [ neg for neg in neg_list if neg.proposals[0].type_ == proposal_type ] if negotiation_status > -1: neg_list = [ neg for neg in neg_list if neg.negotiation_status == negotiation_status ] return neg_list def find_org_closed_negotiations(self, org_id="", proposal_type=""): """Returns a list of closed negotiations for an Org - those which are Accepted or Rejected. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) neg_list, _ = self.rr.find_objects(org_id, PRED.hasNegotiation) if proposal_type: neg_list = [ neg for neg in neg_list if neg.proposals[0].type_ == proposal_type ] neg_list = [ neg for neg in neg_list if neg.negotiation_status != NegotiationStatusEnum.OPEN ] return neg_list def find_user_negotiations(self, actor_id="", org_id="", proposal_type="", negotiation_status=-1): """Returns a list of negotiations for a specified Actor. All negotiations for all Orgs will be returned unless an org_id is specified. An optional proposal_type can be supplied or else all proposals will be returned. An optional negotiation_status can be provided or else all proposals will be returned. """ actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) org_obj = self._validate_resource_id("org_id", org_id, RT.Org, optional=True) neg_list, _ = self.rr.find_objects(actor_obj, PRED.hasNegotiation) if org_id: neg_list = [ neg for neg in neg_list if neg.proposals[0].provider == org_id ] if proposal_type: neg_list = [ neg for neg in neg_list if neg.proposals[0].type_ == proposal_type ] if negotiation_status > -1: neg_list = [ neg for neg in neg_list if neg.negotiation_status == negotiation_status ] return neg_list # ------------------------------------------------------------------------- # Member management def enroll_member(self, org_id="", actor_id=""): """Enrolls an actor into an Org so that they may find and negotiate to use resources of the Org. Membership in the ION Org is implied by registration with the system, so a membership association to the ION Org is not maintained. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) if org_obj.name == self._get_root_org_name(): raise BadRequest( "A request to enroll in the root ION Org is not allowed") self.rr.create_association(org_obj, PRED.hasMember, actor_obj) member_role = self.find_org_role_by_name(org_id, MEMBER_ROLE) self._add_role_association(org_obj, actor_obj, member_role) self.event_pub.publish_event( event_type=OT.OrgMembershipGrantedEvent, origin=org_id, origin_type="Org", description="The member has enrolled in the Org", actor_id=actor_id, org_name=org_obj.name) def cancel_member_enrollment(self, org_id="", actor_id=""): """Cancels the membership of a specific actor actor within the specified Org. Once canceled, the actor will no longer have access to the resource of that Org. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) if org_obj.name == self._get_root_org_name(): raise BadRequest( "A request to cancel enrollment in the root ION Org is not allowed" ) # First remove all associations to any roles role_list = self.list_actor_roles(actor_id, org_id) for user_role in role_list: self._delete_role_association(org_obj, actor_obj, user_role) # Finally remove the association to the Org aid = self.rr.get_association(org_obj, PRED.hasMember, actor_obj) if not aid: raise NotFound( "The membership association between the specified actor and Org is not found" ) self.rr.delete_association(aid) self.event_pub.publish_event( event_type=OT.OrgMembershipCancelledEvent, origin=org_id, origin_type="Org", description="The member has cancelled enrollment in the Org", actor_id=actor_id, org_name=org_obj.name) def is_registered(self, actor_id=""): """Returns True if the specified actor_id is registered with the ION system; otherwise False. """ if not actor_id: raise BadRequest("The actor_id argument is missing") try: # If this ID happens to be another resource, just return false user = self.rr.read(actor_id) if user.type_ == RT.ActorIdentity and user.lcstate != LCS.DELETED: return True except NotFound: pass return False def is_enrolled(self, org_id="", actor_id=""): """Returns True if the specified actor_id is enrolled in the Org and False if not. Throws a NotFound exception if neither id is found. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) return self._is_enrolled(org_id, actor_id) def _is_enrolled(self, org_id="", actor_id=""): # Membership into the Root ION Org is implied as part of registration if org_id == self._get_root_org_id(): return True try: self.rr.get_association(org_id, PRED.hasMember, actor_id) except NotFound: return False return True def list_enrolled_actors(self, org_id=""): """Returns a list of users enrolled in an Org. Will throw a not NotFound exception if none of the specified ids do not exist. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) # Membership into the Root ION Org is implied as part of registration if org_obj.name == self._get_root_org_name(): user_list, _ = self.rr.find_resources(RT.ActorIdentity) else: user_list, _ = self.rr.find_objects(org_obj, PRED.hasMember, RT.ActorIdentity) return user_list def list_orgs_for_actor(self, actor_id=""): """Returns a list of Orgs that the actor is enrolled in. Will throw a not NotFound exception if none of the specified ids do not exist. """ actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) org_list, _ = self.rr.find_subjects(RT.Org, PRED.hasMember, actor_obj) # Membership into the Root ION Org is implied as part of registration ion_org = self.find_org() org_list.append(ion_org) return org_list # ------------------------------------------------------------------------- # Org role management def grant_role(self, org_id="", actor_id="", role_name="", scope=None): """Grants a defined role within an organization to a specific actor. A role of Member is automatically implied with successful enrollment. Will throw a not NotFound exception if none of the specified ids or role_name does not exist. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) user_role = self._validate_user_role("role_name", role_name, org_id) if not self._is_enrolled(org_id, actor_id): raise BadRequest( "The actor is not a member of the specified Org (%s)" % org_obj.name) self._add_role_association(org_obj, actor_obj, user_role) def _add_role_association(self, org, actor, user_role): self.rr.create_association(actor, PRED.hasRole, user_role) self.event_pub.publish_event(event_type=OT.UserRoleGrantedEvent, origin=org._id, origin_type="Org", sub_type=user_role.governance_name, description="Granted the %s role" % user_role.name, actor_id=actor._id, role_name=user_role.governance_name, org_name=org.name) def revoke_role(self, org_id="", actor_id="", role_name=""): """Revokes a defined Role within an organization to a specific actor. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) user_role = self._validate_user_role("role_name", role_name, org_id) self._delete_role_association(org_obj, actor_obj, user_role) def _delete_role_association(self, org, actor, user_role): aid = self.rr.get_association(actor, PRED.hasRole, user_role) if not aid: raise NotFound( "ActorIdentity %s to UserRole %s association not found" % (actor._id, user_role._id)) self.rr.delete_association(aid) self.event_pub.publish_event(event_type=OT.UserRoleRevokedEvent, origin=org._id, origin_type="Org", sub_type=user_role.governance_name, description="Revoked the %s role" % user_role.name, actor_id=actor._id, role_name=user_role.governance_name, org_name=org.name) def has_role(self, org_id="", actor_id="", role_name=""): """Returns True if the specified actor_id has the specified role_name in the Org and False if not. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) if not role_name: raise BadRequest("Invalid argument role_name") if org_id == self._get_root_org_id() and role_name == MEMBER_ROLE: return True role_list, _ = self.rr.find_objects(actor_id, PRED.hasRole, RT.UserRole, id_only=False) for role in role_list: if role.governance_name == role_name and role.org_governance_name == org_obj.org_governance_name: return True return False def list_actor_roles(self, actor_id="", org_id=""): """Returns a list of User Roles for a specific actor in an Org. """ actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) org_obj = self._validate_resource_id("org_id", org_id, RT.Org, optional=True) role_list, _ = self.rr.find_objects(actor_id, PRED.hasRole, RT.UserRole, id_only=False) if org_id: role_list = [ r for r in role_list if r.org_governance_name == org_obj.org_governance_name ] if not org_id or org_id == self._get_root_org_id(): # Because a user is automatically enrolled with the ION Org then the membership role # is implied - so add it to the list member_role = self._find_org_role(self._get_root_org_id(), MEMBER_ROLE) role_list.append(member_role) return role_list # ------------------------------------------------------------------------- # Resource sharing in Org def share_resource(self, org_id="", resource_id=""): """Share a resource with the specified Org. Once shared, the resource will be added to a directory of available resources within the Org. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) resource_obj = self._validate_resource_id("resource_id", resource_id) self.rr.create_association(org_obj, PRED.hasResource, resource_obj) self.event_pub.publish_event( event_type=OT.ResourceSharedEvent, origin=org_obj._id, origin_type="Org", sub_type=resource_obj.type_, description="The resource has been shared in the Org", resource_id=resource_id, org_name=org_obj.name) def unshare_resource(self, org_id="", resource_id=""): """Unshare a resource with the specified Org. Once unshared, the resource will be removed from the directory of available resources within the Org. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) resource_obj = self._validate_resource_id("resource_id", resource_id) aid = self.rr.get_association(org_obj, PRED.hasResource, resource_obj) if not aid: raise NotFound("Association between Resource and Org not found") self.rr.delete_association(aid) self.event_pub.publish_event( event_type=OT.ResourceUnsharedEvent, origin=org_obj._id, origin_type="Org", sub_type=resource_obj.type_, description="The resource has been unshared in the Org", resource_id=resource_id, org_name=org_obj.name) def is_resource_shared(self, org_id="", resource_id=""): """Returns True if the resource has been shared in the specified org_id; otherwise False is returned. """ self._validate_resource_id("org_id", org_id, RT.Org) self._validate_resource_id("resource_id", resource_id) org_ids, _ = self.rr.find_subjects(RT.Org, PRED.hasResource, resource_id, id_only=True) return org_id in org_ids def list_shared_resources(self, org_id=''): self._validate_resource_id("org_id", org_id, RT.Org) if org_id == self._get_root_org_id(): # All resources - reject for security reasons raise BadRequest("Cannot enumerate resources for root Org") res_objs, _ = self.rr.find_objects(org_id, PRED.hasResource, id_only=False) return res_objs def list_orgs_for_resource(self, resource_id=''): self._validate_resource_id("resource_id", resource_id) org_objs, _ = self.rr.find_subjects(RT.Org, PRED.hasResource, resource_id, id_only=False) root_org = self.find_org() org_objs.append(root_org) return org_objs # ------------------------------------------------------------------------- # Resource commitments def acquire_resource(self, sap=None): """Creates a Commitment for the specified resource for a specified user within the specified Org as defined in the proposal. Once shared, the resource is committed to the user. """ if not sap: raise BadRequest("The sap argument is missing") if sap.type_ == OT.AcquireResourceExclusiveProposal: exclusive = True else: exclusive = False commitment_id = self.create_resource_commitment( sap.provider, sap.consumer, sap.resource_id, exclusive, int(sap.expiration)) # Create association between the Commitment and the Negotiation objects self.rr.create_association(sap.negotiation_id, PRED.hasContract, commitment_id) return commitment_id def create_resource_commitment(self, org_id="", actor_id="", resource_id="", exclusive=False, expiration=0): """Creates a Commitment for the specified resource for a specified actor within the specified Org. Once shared, the resource is committed to the actor. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org, optional=True) actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) resource_obj = self._validate_resource_id("resource_id", resource_id) if org_id: # Check that resource is shared in Org? pass res_commitment = IonObject(OT.ResourceCommitment, resource_id=resource_id, exclusive=exclusive) commitment = IonObject(RT.Commitment, name="", provider=org_id, consumer=actor_id, commitment=res_commitment, description="Resource Commitment", expiration=str(expiration)) commitment._id, commitment._rev = self.rr.create(commitment) # Creating associations to all related objects self.rr.create_association(actor_id, PRED.hasCommitment, commitment._id) self.rr.create_association(commitment._id, PRED.hasTarget, resource_id) if org_id: self.rr.create_association(org_id, PRED.hasCommitment, commitment._id) self.event_pub.publish_event( event_type=OT.ResourceCommitmentCreatedEvent, origin=org_id, origin_type="Org", sub_type=resource_obj.type_, description="The resource has been committed by the Org", resource_id=resource_id, org_name=org_obj.name, commitment_id=commitment._id, commitment_type=commitment.commitment.type_) return commitment._id def release_commitment(self, commitment_id=""): """Release the commitment that was created for resources. The commitment is retained in DELETED state for historic records. """ commitment_obj = self._validate_resource_id("commitment_id", commitment_id, RT.Commitment) self.rr.lcs_delete(commitment_id) self.event_pub.publish_event( event_type=OT.ResourceCommitmentReleasedEvent, origin=commitment_obj.provider, origin_type="Org", sub_type="", description="The resource has been uncommitted by the Org", resource_id=commitment_obj.commitment.resource_id, commitment_id=commitment_id, commitment_type=commitment_obj.commitment.type_) def find_commitments(self, org_id='', resource_id='', actor_id='', exclusive=False, include_expired=False): """Returns all commitments in specified org and optionally a given actor and/or optionally a given resource. If exclusive == True, only return exclusive commitments. """ self._validate_resource_id("org_id", org_id, RT.Org, optional=True) self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity, optional=True) if not org_id and not resource_id and not actor_id: raise BadRequest("Must restrict search for commitments") if resource_id: com_objs, _ = self.rr.find_subjects(RT.Commitment, PRED.hasTarget, resource_id, id_only=False) if actor_id: com_objs = [c for c in com_objs if c.consumer == actor_id] if org_id: com_objs = [c for c in com_objs if c.provider == org_id] elif actor_id: com_objs, _ = self.rr.find_objects(actor_id, PRED.hasCommitment, RT.Commitment, id_only=False) if org_id: com_objs = [c for c in com_objs if c.provider == org_id] else: com_objs, _ = self.rr.find_objects(org_id, PRED.hasCommitment, RT.Commitment, id_only=False) if exclusive: com_objs = [ c for c in com_objs if c.commitment.type_ == OT.ResourceCommitment and c.commitment.exclusive ] else: com_objs = [ c for c in com_objs if c.commitment.type_ != OT.ResourceCommitment or ( c.commitment.type_ == OT.ResourceCommitment and not c.commitment.exclusive) ] if not include_expired: cur_time = get_ion_ts_millis() com_objs = [ c for c in com_objs if int(c.expiration) == 0 or cur_time < int(c.expiration) ] return com_objs def is_resource_acquired(self, actor_id="", resource_id=""): """Returns True if the specified resource_id has been acquired. The actor_id is optional, as the operation can return True if the resource is acquired by any actor or specifically by the specified actor_id, otherwise False is returned. """ return self._is_resource_acquired(actor_id, resource_id, exclusive=False) def is_resource_acquired_exclusively(self, actor_id="", resource_id=""): """Returns True if the specified resource_id has been acquired exclusively. The actor_id is optional, as the operation can return True if the resource is acquired exclusively by any actor or specifically by the specified actor_id, otherwise False is returned. """ return self._is_resource_acquired(actor_id, resource_id, exclusive=True) def _is_resource_acquired(self, actor_id="", resource_id="", exclusive=False): if not resource_id: raise BadRequest("The resource_id argument is missing") try: com_objs = self.find_commitments(resource_id=resource_id, actor_id=actor_id, exclusive=exclusive) return bool(com_objs) except Exception as ex: log.exception( "Error checking acquired status, actor_id=%s, resource_id=%s" % (actor_id, resource_id)) return False def find_acquired_resources(self, org_id='', actor_id='', exclusive=False, include_expired=False): if not org_id and not actor_id: raise BadRequest("Must provide org_id or actor_id") com_objs = self.find_commitments(org_id=org_id, actor_id=actor_id, exclusive=exclusive, include_expired=include_expired) res_ids = { c.commitment.resource_id for c in com_objs if c.commitment.type_ == OT.ResourceCommitment } res_objs = self.rr.read_mult(list(res_ids)) return res_objs # ------------------------------------------------------------------------- # Org containers def is_in_org(self, container): container_list, _ = self.rr.find_subjects(RT.Org, PRED.hasResource, container) return bool(container_list) def find_org_containers(self, org_id=""): """Returns a list of containers associated with an Org. Will throw a not NotFound exception if the specified id does not exist. TODO: Fix inefficient implementation with index """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) # Containers in the Root ION Org are implied if org_obj.org_governance_name == self._get_root_org_name(): container_list, _ = self.rr.find_resources(RT.CapabilityContainer) container_list[:] = [ container for container in container_list if not self.is_in_org(container) ] else: container_list, _ = self.rr.find_objects(org_obj, PRED.hasResource, RT.CapabilityContainer) return container_list def affiliate_org(self, org_id="", affiliate_org_id=""): """Creates an association between multiple Orgs as an affiliation so that they may coordinate activities between them. Throws a NotFound exception if neither id is found. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) affiliate_org_obj = self._validate_resource_id("affiliate_org_id", affiliate_org_id, RT.Org) aid = self.rr.create_association(org_obj, PRED.hasAffiliation, affiliate_org_obj) if not aid: return False def unaffiliate_org(self, org_id="", affiliate_org_id=""): """Removes an association between multiple Orgs as an affiliation. Throws a NotFound exception if neither id is found. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) affiliate_org_obj = self._validate_resource_id("affiliate_org_id", affiliate_org_id, RT.Org) aid = self.rr.get_association(org_obj, PRED.hasAffiliation, affiliate_org_obj) if not aid: raise NotFound( "The affiliation association between the specified Orgs is not found" ) self.rr.delete_association(aid) # Local helper functions are below - do not remove them def is_enroll_negotiation_open(self, org_id, actor_id): try: neg_list = self.find_user_negotiations( actor_id, org_id, proposal_type=OT.EnrollmentProposal, negotiation_status=NegotiationStatusEnum.OPEN) if neg_list: return True except Exception as ex: log.exception("org_id:%s and actor_id:%s" % (org_id, actor_id)) return False
class OrgManagementService(BaseOrgManagementService): """ Services to define and administer an Org (organization, facility), to enroll/remove members and to provide access to the resources of an Org to enrolled or affiliated entities (identities). Contains contract and commitment repository """ def on_init(self): self.rr = self.clients.resource_registry self.event_pub = EventPublisher(process=self) self.negotiation_handler = Negotiation(self, negotiation_rules, self.event_pub) self.root_org_id = None def _get_root_org_name(self): if self.container is None or self.container.governance_controller is None: return CFG.get_safe("system.root_org", "ION") return self.container.governance_controller.system_root_org_name def _get_root_org_id(self): if not self.root_org_id: root_org = self.find_org() self.root_org_id = root_org._id return self.root_org_id def _validate_user_role(self, arg_name, role_name, org_id): """ Check that the given argument is a resource id, by retrieving the resource from the resource registry. Additionally checks type and returns the result object """ if not role_name: raise BadRequest("The argument '%s' is missing" % arg_name) if not org_id: raise BadRequest("The argument 'org_id' is missing") user_role = self._find_org_role(org_id, role_name) return user_role # ------------------------------------------------------------------------- # Org management (CRUD) def create_org(self, org=None): """Creates an Org based on the provided object. The id string returned is the internal id by which Org will be identified in the data store. """ # Only allow one root ION Org in the system self._validate_resource_obj("org", org, RT.Org, checks="noid,name,unique") # If this governance identifier is not set, then set to a safe version of the org name. if not org.org_governance_name: org.org_governance_name = create_basic_identifier(org.name) if not is_basic_identifier(org.org_governance_name): raise BadRequest("The Org org_governance_name '%s' contains invalid characters" % org.org_governance_name) org_id, _ = self.rr.create(org) # Instantiate a Directory for this Org directory = Directory(orgname=org.name) # Instantiate initial set of User Roles for this Org self._create_org_roles(org_id) return org_id def _create_org_roles(self, org_id): # Instantiate initial set of User Roles for this Org manager_role = IonObject(RT.UserRole, name="MODERATOR", governance_name=MODERATOR_ROLE, description="Manage organization members, resources and roles") self.add_org_role(org_id, manager_role) operator_role = IonObject(RT.UserRole, name="OPERATOR", governance_name=OPERATOR_ROLE, description="Modify and control organization resources") self.add_org_role(org_id, operator_role) member_role = IonObject(RT.UserRole, name="MEMBER", governance_name=MEMBER_ROLE, description="Access organization resources") self.add_org_role(org_id, member_role) def update_org(self, org=None): """Updates the Org based on provided object. """ old_org = self._validate_resource_obj("org", org, RT.Org, checks="id") if org.org_governance_name != old_org.org_governance_name: raise BadRequest("Cannot update Org org_governance_name") self.rr.update(org) def read_org(self, org_id=""): """Returns the Org object for the specified id. Throws exception if id does not match any persisted Org objects. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) return org_obj def delete_org(self, org_id=""): """Permanently deletes Org object with the specified id. Throws exception if id does not match any persisted Org object. """ self._validate_resource_id("org_id", org_id, RT.Org) self.rr.delete(org_id) def find_org(self, name=""): """Finds an Org object with the specified name. Defaults to the root ION object. Throws a NotFound exception if the object does not exist. """ # Default to the root ION Org if not specified if not name: name = self._get_root_org_name() res_list, _ = self.rr.find_resources(restype=RT.Org, name=name, id_only=False) if not res_list: raise NotFound("The Org with name %s does not exist" % name) return res_list[0] # ------------------------------------------------------------------------- # Org roles def add_org_role(self, org_id="", user_role=None): """Adds a UserRole to an Org, if the role by the specified name does not exist. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) self._validate_resource_obj("user_role", user_role, RT.UserRole, checks="noid,name") if not is_basic_identifier(user_role.governance_name): raise BadRequest("Invalid role governance_name") user_role.org_governance_name = org_obj.org_governance_name try: self._find_org_role(org_id, user_role.governance_name) raise BadRequest("Role '%s' is already associated with this Org" % user_role.governance_name) except NotFound: pass user_role_id, _ = self.rr.create(user_role) self.rr.create_association(org_obj, PRED.hasRole, user_role_id) return user_role_id def remove_org_role(self, org_id="", role_name="", force_removal=False): """Removes a UserRole from an Org. The UserRole will not be removed if there are users associated with the UserRole unless the force_removal parameter is set to True """ self._validate_resource_id("org_id", org_id, RT.Org) user_role = self._validate_user_role("role_name", role_name, org_id) if not force_removal: alist, _ = self.rr.find_subjects(RT.ActorIdentity, PRED.hasRole, user_role) if alist: raise BadRequest("UserRole %s still in use and cannot be removed" % user_role.name) self.rr.delete(user_role._id) def find_org_role_by_name(self, org_id="", role_name=""): """Returns the UserRole object for the specified name in the Org. """ self._validate_resource_id("org_id", org_id, RT.Org) user_role = self._find_org_role(org_id, role_name) return user_role def _find_org_role(self, org_id="", role_name=""): if not org_id: raise BadRequest("The org_id argument is missing") if not role_name: raise BadRequest("The role_name argument is missing") # Iterating (vs. query) is just fine, because the number of org roles is sufficiently small org_roles = self._list_org_roles(org_id) for role in org_roles: if role.governance_name == role_name: return role raise NotFound("Role %s not found in Org id=%s" % (role_name, org_id)) def list_org_roles(self, org_id=""): """Returns a list of roles available in an Org. Will throw a not NotFound exception if none of the specified ids do not exist. """ self._validate_resource_id("org_id", org_id, RT.Org) return self._list_org_roles(org_id) def _list_org_roles(self, org_id=""): if not org_id: raise BadRequest("Illegal org_id") role_list, _ = self.rr.find_objects(org_id, PRED.hasRole, RT.UserRole, id_only=False) return role_list # ------------------------------------------------------------------------- # Negotiations def negotiate(self, sap=None): """A generic operation for negotiating actions with an Org, such as for enrollment, role request or to acquire a resource managed by the Org. The ServiceAgreementProposal object is used to specify conditions of the proposal as well as counter proposals and the Org will create a Negotiation resource to track the history and status of the negotiation. """ if sap is None or (sap.type_ != OT.ServiceAgreementProposal and not issubtype(sap.type_, OT.ServiceAgreementProposal)): raise BadRequest("The sap argument must be a valid ServiceAgreementProposal object") if sap.proposal_status == ProposalStatusEnum.INITIAL: neg_id = self.negotiation_handler.create_negotiation(sap) org = self.read_org(org_id=sap.provider) # Publish an event indicating an Negotiation has been initiated self.event_pub.publish_event(event_type=OT.OrgNegotiationInitiatedEvent, origin=org._id, origin_type="Org", description=sap.description, org_name=org.name, negotiation_id=neg_id, sub_type=sap.type_) # Synchronize the internal reference for later use sap.negotiation_id = neg_id # Get the most recent version of the Negotiation resource negotiation = self.negotiation_handler.read_negotiation(sap) # Update the Negotiation object with the latest SAP neg_id = self.negotiation_handler.update_negotiation(sap) # Get the most recent version of the Negotiation resource negotiation = self.rr.read(neg_id) # hardcoding some rules at the moment - could be replaced by a Rules Engine if sap.type_ == OT.AcquireResourceExclusiveProposal: if self.is_resource_acquired_exclusively(None, sap.resource_id): # Automatically accept the proposal for exclusive access if it is not already acquired exclusively provider_accept_sap = Negotiation.create_counter_proposal(negotiation, ProposalStatusEnum.REJECTED, ProposalOriginatorEnum.PROVIDER) rejection_reason = "The resource has already been acquired exclusively" # Update the Negotiation object with the latest SAP neg_id = self.negotiation_handler.update_negotiation(provider_accept_sap, rejection_reason) # Get the most recent version of the Negotiation resource negotiation = self.rr.read(neg_id) else: # Automatically reject the proposal if the expiration request is greater than 12 hours from now or 0 cur_time = int(get_ion_ts()) expiration = int(cur_time + ( 12 * 60 * 60 * 1000 )) # 12 hours from now if int(sap.expiration) == 0 or int(sap.expiration) > expiration: # Automatically accept the proposal for exclusive access if it is not already acquired exclusively provider_accept_sap = Negotiation.create_counter_proposal(negotiation, ProposalStatusEnum.REJECTED, ProposalOriginatorEnum.PROVIDER) rejection_reason = "A proposal to acquire a resource exclusively must be more than 0 and be less than 12 hours." # Update the Negotiation object with the latest SAP neg_id = self.negotiation_handler.update_negotiation(provider_accept_sap, rejection_reason) # Get the most recent version of the Negotiation resource negotiation = self.rr.read(neg_id) else: # Automatically accept the proposal for exclusive access if it is not already acquired exclusively provider_accept_sap = Negotiation.create_counter_proposal(negotiation, ProposalStatusEnum.ACCEPTED, ProposalOriginatorEnum.PROVIDER) # Update the Negotiation object with the latest SAP neg_id = self.negotiation_handler.update_negotiation(provider_accept_sap) # Get the most recent version of the Negotiation resource negotiation = self.rr.read(neg_id) # Check to see if the rules allow for auto acceptance of the negotiations - # where the second party is assumed to accept if the # first party accepts. if negotiation_rules[sap.type_]["auto_accept"]: # Automatically accept for the consumer if the Org Manager as provider accepts the proposal latest_sap = negotiation.proposals[-1] if latest_sap.proposal_status == ProposalStatusEnum.ACCEPTED and latest_sap.originator == ProposalOriginatorEnum.PROVIDER: consumer_accept_sap = Negotiation.create_counter_proposal(negotiation, ProposalStatusEnum.ACCEPTED) # Update the Negotiation object with the latest SAP neg_id = self.negotiation_handler.update_negotiation(consumer_accept_sap) # Get the most recent version of the Negotiation resource negotiation = self.rr.read(neg_id) elif latest_sap.proposal_status == ProposalStatusEnum.ACCEPTED and latest_sap.originator == ProposalOriginatorEnum.CONSUMER: provider_accept_sap = Negotiation.create_counter_proposal(negotiation, ProposalStatusEnum.ACCEPTED, ProposalOriginatorEnum.PROVIDER) # Update the Negotiation object with the latest SAP neg_id = self.negotiation_handler.update_negotiation(provider_accept_sap) # Get the most recent version of the Negotiation resource negotiation = self.rr.read(neg_id) # Return the latest proposal return negotiation.proposals[-1] def find_org_negotiations(self, org_id="", proposal_type="", negotiation_status=-1): """Returns a list of negotiations for an Org. An optional proposal_type can be supplied or else all proposals will be returned. An optional negotiation_status can be supplied or else all proposals will be returned. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) neg_list, _ = self.rr.find_objects(org_id, PRED.hasNegotiation) if proposal_type: neg_list = [neg for neg in neg_list if neg.proposals[0].type_ == proposal_type] if negotiation_status > -1: neg_list = [neg for neg in neg_list if neg.negotiation_status == negotiation_status] return neg_list def find_org_closed_negotiations(self, org_id="", proposal_type=""): """Returns a list of closed negotiations for an Org - those which are Accepted or Rejected. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) neg_list, _ = self.rr.find_objects(org_id, PRED.hasNegotiation) if proposal_type: neg_list = [neg for neg in neg_list if neg.proposals[0].type_ == proposal_type] neg_list = [neg for neg in neg_list if neg.negotiation_status != NegotiationStatusEnum.OPEN] return neg_list def find_user_negotiations(self, actor_id="", org_id="", proposal_type="", negotiation_status=-1): """Returns a list of negotiations for a specified Actor. All negotiations for all Orgs will be returned unless an org_id is specified. An optional proposal_type can be supplied or else all proposals will be returned. An optional negotiation_status can be provided or else all proposals will be returned. """ actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) org_obj = self._validate_resource_id("org_id", org_id, RT.Org, optional=True) neg_list, _ = self.rr.find_objects(actor_obj, PRED.hasNegotiation) if org_id: neg_list = [neg for neg in neg_list if neg.proposals[0].provider == org_id] if proposal_type: neg_list = [neg for neg in neg_list if neg.proposals[0].type_ == proposal_type] if negotiation_status > -1: neg_list = [neg for neg in neg_list if neg.negotiation_status == negotiation_status] return neg_list # ------------------------------------------------------------------------- # Member management def enroll_member(self, org_id="", actor_id=""): """Enrolls an actor into an Org so that they may find and negotiate to use resources of the Org. Membership in the ION Org is implied by registration with the system, so a membership association to the ION Org is not maintained. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) if org_obj.name == self._get_root_org_name(): raise BadRequest("A request to enroll in the root ION Org is not allowed") self.rr.create_association(org_obj, PRED.hasMember, actor_obj) member_role = self.find_org_role_by_name(org_id, MEMBER_ROLE) self._add_role_association(org_obj, actor_obj, member_role) self.event_pub.publish_event(event_type=OT.OrgMembershipGrantedEvent, origin=org_id, origin_type="Org", description="The member has enrolled in the Org", actor_id=actor_id, org_name=org_obj.name) def cancel_member_enrollment(self, org_id="", actor_id=""): """Cancels the membership of a specific actor actor within the specified Org. Once canceled, the actor will no longer have access to the resource of that Org. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) if org_obj.name == self._get_root_org_name(): raise BadRequest("A request to cancel enrollment in the root ION Org is not allowed") # First remove all associations to any roles role_list = self.list_actor_roles(actor_id, org_id) for user_role in role_list: self._delete_role_association(org_obj, actor_obj, user_role) # Finally remove the association to the Org aid = self.rr.get_association(org_obj, PRED.hasMember, actor_obj) if not aid: raise NotFound("The membership association between the specified actor and Org is not found") self.rr.delete_association(aid) self.event_pub.publish_event(event_type=OT.OrgMembershipCancelledEvent, origin=org_id, origin_type="Org", description="The member has cancelled enrollment in the Org", actor_id=actor_id, org_name=org_obj.name) def is_registered(self, actor_id=""): """Returns True if the specified actor_id is registered with the ION system; otherwise False. """ if not actor_id: raise BadRequest("The actor_id argument is missing") try: # If this ID happens to be another resource, just return false user = self.rr.read(actor_id) if user.type_ == RT.ActorIdentity and user.lcstate != LCS.DELETED: return True except NotFound: pass return False def is_enrolled(self, org_id="", actor_id=""): """Returns True if the specified actor_id is enrolled in the Org and False if not. Throws a NotFound exception if neither id is found. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) return self._is_enrolled(org_id, actor_id) def _is_enrolled(self, org_id="", actor_id=""): # Membership into the Root ION Org is implied as part of registration if org_id == self._get_root_org_id(): return True try: self.rr.get_association(org_id, PRED.hasMember, actor_id) except NotFound: return False return True def list_enrolled_actors(self, org_id=""): """Returns a list of users enrolled in an Org. Will throw a not NotFound exception if none of the specified ids do not exist. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) # Membership into the Root ION Org is implied as part of registration if org_obj.name == self._get_root_org_name(): user_list, _ = self.rr.find_resources(RT.ActorIdentity) else: user_list, _ = self.rr.find_objects(org_obj, PRED.hasMember, RT.ActorIdentity) return user_list def list_orgs_for_actor(self, actor_id=""): """Returns a list of Orgs that the actor is enrolled in. Will throw a not NotFound exception if none of the specified ids do not exist. """ actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) org_list, _ = self.rr.find_subjects(RT.Org, PRED.hasMember, actor_obj) # Membership into the Root ION Org is implied as part of registration ion_org = self.find_org() org_list.append(ion_org) return org_list # ------------------------------------------------------------------------- # Org role management def grant_role(self, org_id="", actor_id="", role_name="", scope=None): """Grants a defined role within an organization to a specific actor. A role of Member is automatically implied with successful enrollment. Will throw a not NotFound exception if none of the specified ids or role_name does not exist. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) user_role = self._validate_user_role("role_name", role_name, org_id) if not self._is_enrolled(org_id, actor_id): raise BadRequest("The actor is not a member of the specified Org (%s)" % org_obj.name) self._add_role_association(org_obj, actor_obj, user_role) def _add_role_association(self, org, actor, user_role): self.rr.create_association(actor, PRED.hasRole, user_role) self.event_pub.publish_event(event_type=OT.UserRoleGrantedEvent, origin=org._id, origin_type="Org", sub_type=user_role.governance_name, description="Granted the %s role" % user_role.name, actor_id=actor._id, role_name=user_role.governance_name, org_name=org.name) def revoke_role(self, org_id="", actor_id="", role_name=""): """Revokes a defined Role within an organization to a specific actor. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) user_role = self._validate_user_role("role_name", role_name, org_id) self._delete_role_association(org_obj, actor_obj, user_role) def _delete_role_association(self, org, actor, user_role): aid = self.rr.get_association(actor, PRED.hasRole, user_role) if not aid: raise NotFound("ActorIdentity %s to UserRole %s association not found" % (actor._id, user_role._id)) self.rr.delete_association(aid) self.event_pub.publish_event(event_type=OT.UserRoleRevokedEvent, origin=org._id, origin_type="Org", sub_type=user_role.governance_name, description="Revoked the %s role" % user_role.name, actor_id=actor._id, role_name=user_role.governance_name, org_name=org.name) def has_role(self, org_id="", actor_id="", role_name=""): """Returns True if the specified actor_id has the specified role_name in the Org and False if not. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) if not role_name: raise BadRequest("Invalid argument role_name") if org_id == self._get_root_org_id() and role_name == MEMBER_ROLE: return True role_list, _ = self.rr.find_objects(actor_id, PRED.hasRole, RT.UserRole, id_only=False) for role in role_list: if role.governance_name == role_name and role.org_governance_name == org_obj.org_governance_name: return True return False def list_actor_roles(self, actor_id="", org_id=""): """Returns a list of User Roles for a specific actor in an Org. """ actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) org_obj = self._validate_resource_id("org_id", org_id, RT.Org, optional=True) role_list, _ = self.rr.find_objects(actor_id, PRED.hasRole, RT.UserRole, id_only=False) if org_id: role_list = [r for r in role_list if r.org_governance_name == org_obj.org_governance_name] if not org_id or org_id == self._get_root_org_id(): # Because a user is automatically enrolled with the ION Org then the membership role # is implied - so add it to the list member_role = self._find_org_role(self._get_root_org_id(), MEMBER_ROLE) role_list.append(member_role) return role_list # ------------------------------------------------------------------------- # Resource sharing in Org def share_resource(self, org_id="", resource_id=""): """Share a resource with the specified Org. Once shared, the resource will be added to a directory of available resources within the Org. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) resource_obj = self._validate_resource_id("resource_id", resource_id) self.rr.create_association(org_obj, PRED.hasResource, resource_obj) self.event_pub.publish_event(event_type=OT.ResourceSharedEvent, origin=org_obj._id, origin_type="Org", sub_type=resource_obj.type_, description="The resource has been shared in the Org", resource_id=resource_id, org_name=org_obj.name ) def unshare_resource(self, org_id="", resource_id=""): """Unshare a resource with the specified Org. Once unshared, the resource will be removed from the directory of available resources within the Org. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) resource_obj = self._validate_resource_id("resource_id", resource_id) aid = self.rr.get_association(org_obj, PRED.hasResource, resource_obj) if not aid: raise NotFound("Association between Resource and Org not found") self.rr.delete_association(aid) self.event_pub.publish_event(event_type=OT.ResourceUnsharedEvent, origin=org_obj._id, origin_type="Org", sub_type=resource_obj.type_, description="The resource has been unshared in the Org", resource_id=resource_id, org_name=org_obj.name ) def is_resource_shared(self, org_id="", resource_id=""): """Returns True if the resource has been shared in the specified org_id; otherwise False is returned. """ self._validate_resource_id("org_id", org_id, RT.Org) self._validate_resource_id("resource_id", resource_id) org_ids, _ = self.rr.find_subjects(RT.Org, PRED.hasResource, resource_id, id_only=True) return org_id in org_ids def list_shared_resources(self, org_id=''): self._validate_resource_id("org_id", org_id, RT.Org) if org_id == self._get_root_org_id(): # All resources - reject for security reasons raise BadRequest("Cannot enumerate resources for root Org") res_objs, _ = self.rr.find_objects(org_id, PRED.hasResource, id_only=False) return res_objs def list_orgs_for_resource(self, resource_id=''): self._validate_resource_id("resource_id", resource_id) org_objs, _ = self.rr.find_subjects(RT.Org, PRED.hasResource, resource_id, id_only=False) root_org = self.find_org() org_objs.append(root_org) return org_objs # ------------------------------------------------------------------------- # Resource commitments def acquire_resource(self, sap=None): """Creates a Commitment for the specified resource for a specified user within the specified Org as defined in the proposal. Once shared, the resource is committed to the user. """ if not sap: raise BadRequest("The sap argument is missing") if sap.type_ == OT.AcquireResourceExclusiveProposal: exclusive = True else: exclusive = False commitment_id = self.create_resource_commitment(sap.provider, sap.consumer, sap.resource_id, exclusive, int(sap.expiration)) # Create association between the Commitment and the Negotiation objects self.rr.create_association(sap.negotiation_id, PRED.hasContract, commitment_id) return commitment_id def create_resource_commitment(self, org_id="", actor_id="", resource_id="", exclusive=False, expiration=0): """Creates a Commitment for the specified resource for a specified actor within the specified Org. Once shared, the resource is committed to the actor. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org, optional=True) actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) resource_obj = self._validate_resource_id("resource_id", resource_id) if org_id: # Check that resource is shared in Org? pass res_commitment = IonObject(OT.ResourceCommitment, resource_id=resource_id, exclusive=exclusive) commitment = IonObject(RT.Commitment, name="", provider=org_id, consumer=actor_id, commitment=res_commitment, description="Resource Commitment", expiration=str(expiration)) commitment._id, commitment._rev = self.rr.create(commitment) # Creating associations to all related objects self.rr.create_association(actor_id, PRED.hasCommitment, commitment._id) self.rr.create_association(commitment._id, PRED.hasTarget, resource_id) if org_id: self.rr.create_association(org_id, PRED.hasCommitment, commitment._id) self.event_pub.publish_event(event_type=OT.ResourceCommitmentCreatedEvent, origin=org_id, origin_type="Org", sub_type=resource_obj.type_, description="The resource has been committed by the Org", resource_id=resource_id, org_name=org_obj.name, commitment_id=commitment._id, commitment_type=commitment.commitment.type_) return commitment._id def release_commitment(self, commitment_id=""): """Release the commitment that was created for resources. The commitment is retained in DELETED state for historic records. """ commitment_obj = self._validate_resource_id("commitment_id", commitment_id, RT.Commitment) self.rr.lcs_delete(commitment_id) self.event_pub.publish_event(event_type=OT.ResourceCommitmentReleasedEvent, origin=commitment_obj.provider, origin_type="Org", sub_type="", description="The resource has been uncommitted by the Org", resource_id=commitment_obj.commitment.resource_id, commitment_id=commitment_id, commitment_type=commitment_obj.commitment.type_) def find_commitments(self, org_id='', resource_id='', actor_id='', exclusive=False, include_expired=False): """Returns all commitments in specified org and optionally a given actor and/or optionally a given resource. If exclusive == True, only return exclusive commitments. """ self._validate_resource_id("org_id", org_id, RT.Org, optional=True) self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity, optional=True) if not org_id and not resource_id and not actor_id: raise BadRequest("Must restrict search for commitments") if resource_id: com_objs, _ = self.rr.find_subjects(RT.Commitment, PRED.hasTarget, resource_id, id_only=False) if actor_id: com_objs = [c for c in com_objs if c.consumer == actor_id] if org_id: com_objs = [c for c in com_objs if c.provider == org_id] elif actor_id: com_objs, _ = self.rr.find_objects(actor_id, PRED.hasCommitment, RT.Commitment, id_only=False) if org_id: com_objs = [c for c in com_objs if c.provider == org_id] else: com_objs, _ = self.rr.find_objects(org_id, PRED.hasCommitment, RT.Commitment, id_only=False) if exclusive: com_objs = [c for c in com_objs if c.commitment.type_ == OT.ResourceCommitment and c.commitment.exclusive] else: com_objs = [c for c in com_objs if c.commitment.type_ != OT.ResourceCommitment or ( c.commitment.type_ == OT.ResourceCommitment and not c.commitment.exclusive)] if not include_expired: cur_time = get_ion_ts_millis() com_objs = [c for c in com_objs if int(c.expiration) == 0 or cur_time < int(c.expiration)] return com_objs def is_resource_acquired(self, actor_id="", resource_id=""): """Returns True if the specified resource_id has been acquired. The actor_id is optional, as the operation can return True if the resource is acquired by any actor or specifically by the specified actor_id, otherwise False is returned. """ return self._is_resource_acquired(actor_id, resource_id, exclusive=False) def is_resource_acquired_exclusively(self, actor_id="", resource_id=""): """Returns True if the specified resource_id has been acquired exclusively. The actor_id is optional, as the operation can return True if the resource is acquired exclusively by any actor or specifically by the specified actor_id, otherwise False is returned. """ return self._is_resource_acquired(actor_id, resource_id, exclusive=True) def _is_resource_acquired(self, actor_id="", resource_id="", exclusive=False): if not resource_id: raise BadRequest("The resource_id argument is missing") try: com_objs = self.find_commitments(resource_id=resource_id, actor_id=actor_id, exclusive=exclusive) return bool(com_objs) except Exception as ex: log.exception("Error checking acquired status, actor_id=%s, resource_id=%s" % (actor_id, resource_id)) return False def find_acquired_resources(self, org_id='', actor_id='', exclusive=False, include_expired=False): if not org_id and not actor_id: raise BadRequest("Must provide org_id or actor_id") com_objs = self.find_commitments(org_id=org_id, actor_id=actor_id, exclusive=exclusive, include_expired=include_expired) res_ids = {c.commitment.resource_id for c in com_objs if c.commitment.type_ == OT.ResourceCommitment} res_objs = self.rr.read_mult(list(res_ids)) return res_objs # ------------------------------------------------------------------------- # Org containers def is_in_org(self, container): container_list, _ = self.rr.find_subjects(RT.Org, PRED.hasResource, container) return bool(container_list) def find_org_containers(self, org_id=""): """Returns a list of containers associated with an Org. Will throw a not NotFound exception if the specified id does not exist. TODO: Fix inefficient implementation with index """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) # Containers in the Root ION Org are implied if org_obj.org_governance_name == self._get_root_org_name(): container_list, _ = self.rr.find_resources(RT.CapabilityContainer) container_list[:] = [container for container in container_list if not self.is_in_org(container)] else: container_list, _ = self.rr.find_objects(org_obj, PRED.hasResource, RT.CapabilityContainer) return container_list def affiliate_org(self, org_id="", affiliate_org_id=""): """Creates an association between multiple Orgs as an affiliation so that they may coordinate activities between them. Throws a NotFound exception if neither id is found. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) affiliate_org_obj = self._validate_resource_id("affiliate_org_id", affiliate_org_id, RT.Org) aid = self.rr.create_association(org_obj, PRED.hasAffiliation, affiliate_org_obj) if not aid: return False def unaffiliate_org(self, org_id="", affiliate_org_id=""): """Removes an association between multiple Orgs as an affiliation. Throws a NotFound exception if neither id is found. """ org_obj = self._validate_resource_id("org_id", org_id, RT.Org) affiliate_org_obj = self._validate_resource_id("affiliate_org_id", affiliate_org_id, RT.Org) aid = self.rr.get_association(org_obj, PRED.hasAffiliation, affiliate_org_obj) if not aid: raise NotFound("The affiliation association between the specified Orgs is not found") self.rr.delete_association(aid) # Local helper functions are below - do not remove them def is_enroll_negotiation_open(self, org_id, actor_id): try: neg_list = self.find_user_negotiations(actor_id, org_id, proposal_type=OT.EnrollmentProposal, negotiation_status=NegotiationStatusEnum.OPEN ) if neg_list: return True except Exception as ex: log.exception("org_id:%s and actor_id:%s" % (org_id, actor_id)) return False
def test_user_role_cache(self): #Create a user id_client = IdentityManagementServiceClient() actor_id, valid_until, registered = id_client.signon( USER1_CERTIFICATE, True) #Make a request with this new user to get it into the cache response = self.test_app.get( '/ion-service/resource_registry/find_resources?name=TestDataProduct&id_only=True&requester=' + actor_id) self.check_response_headers(response) self.assertIn(GATEWAY_RESPONSE, response.json['data']) #Check the contents of the user role cache for this user service_gateway_user_role_cache = self.container.proc_manager.procs_by_name[ 'service_gateway'].user_role_cache self.assertEqual(service_gateway_user_role_cache.has_key(actor_id), True) role_header = service_gateway_user_role_cache.get(actor_id) self.assertIn('ION', role_header) self.assertEqual(len(role_header['ION']), 1) self.assertIn('ORG_MEMBER', role_header['ION']) org_client = OrgManagementServiceClient() ion_org = org_client.find_org() manager_role = org_client.find_org_role_by_name( org_id=ion_org._id, role_name='ORG_MANAGER') org_client.grant_role(org_id=ion_org._id, actor_id=actor_id, role_name='ORG_MANAGER') #Just allow some time for event processing on slower platforms gevent.sleep(2) #The user should be evicted from the cache due to a change in roles self.assertEqual(service_gateway_user_role_cache.has_key(actor_id), False) #Do it again to check for new roles response = self.test_app.get( '/ion-service/resource_registry/find_resources?name=TestDataProduct&id_only=True&requester=' + actor_id) self.check_response_headers(response) self.assertIn(GATEWAY_RESPONSE, response.json['data']) #Check the contents of the user role cache for this user self.assertEqual(service_gateway_user_role_cache.has_key(actor_id), True) role_header = service_gateway_user_role_cache.get(actor_id) self.assertIn('ION', role_header) self.assertEqual(len(role_header['ION']), 2) self.assertIn('ORG_MEMBER', role_header['ION']) self.assertIn('ORG_MANAGER', role_header['ION']) #Now flush the user_role_cache and make sure it was flushed event_publisher = EventPublisher() event_publisher.publish_event(event_type=OT.UserRoleCacheResetEvent) #Just allow some time for event processing on slower platforms gevent.sleep(2) self.assertEqual(service_gateway_user_role_cache.has_key(actor_id), False) self.assertEqual(service_gateway_user_role_cache.size(), 0) #Change the role once again and see if it is there again org_client.revoke_role(org_id=ion_org._id, actor_id=actor_id, role_name='ORG_MANAGER') #Just allow some time for event processing on slower platforms gevent.sleep(2) #The user should still not be there self.assertEqual(service_gateway_user_role_cache.has_key(actor_id), False) #Do it again to check for new roles response = self.test_app.get( '/ion-service/resource_registry/find_resources?name=TestDataProduct&id_only=True&requester=' + actor_id) self.check_response_headers(response) self.assertIn(GATEWAY_RESPONSE, response.json['data']) #Check the contents of the user role cache for this user self.assertEqual(service_gateway_user_role_cache.has_key(actor_id), True) role_header = service_gateway_user_role_cache.get(actor_id) self.assertIn('ION', role_header) self.assertEqual(len(role_header['ION']), 1) self.assertIn('ORG_MEMBER', role_header['ION']) id_client.delete_actor_identity(actor_id)
class IdentityManagementService(BaseIdentityManagementService): """ Stores identities of users and resources, including bindings of internal identities to external identities. Also stores metadata such as a user profile. """ event_pub = None def on_init(self): self.rr = self.clients.resource_registry #self.authentication = Authentication() self.event_pub = EventPublisher(process=self) def create_actor_identity(self, actor_identity=None): self._validate_resource_obj("actor_identity", actor_identity, RT.ActorIdentity, checks="noid,name") if actor_identity.credentials: raise BadRequest("Cannot create actor with credentials") if actor_identity.details and actor_identity.details.type_ == OT.IdentityDetails: actor_identity.details = None actor_identity.passwd_reset_token = None actor_id, _ = self.rr.create(actor_identity) return actor_id def update_actor_identity(self, actor_identity=None): old_actor = self._validate_resource_obj("actor_identity", actor_identity, RT.ActorIdentity, checks="id,name") # Prevent security risk because contained credentials may be manipulated actor_identity.credentials = old_actor.credentials self.rr.update(actor_identity) def read_actor_identity(self, actor_id=''): actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) return actor_obj def delete_actor_identity(self, actor_id=''): self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) self.rr.delete(actor_id) def find_actor_identity_by_name(self, name=''): """Return the ActorIdentity object whose name attribute matches the passed value. """ objects, _ = self.rr.find_resources(RT.ActorIdentity, None, name, id_only=False) if not objects: raise NotFound("ActorIdentity with name %s does not exist" % name) if len(objects) > 1: raise Inconsistent("Multiple ActorIdentity objects with name %s exist" % name) return objects[0] # ------------------------------------------------------------------------- # Credentials handling def register_credentials(self, actor_id='', credentials=None): actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) self._validate_arg_obj("credentials", credentials, OT.Credentials) actor_obj.credentials.append(credentials) if credentials.username: actor_obj.alt_ids.append("UNAME:" + credentials.username) # Lower level RR call to avoid credentials clearing self.rr.update(actor_obj) def unregister_credentials(self, actor_id='', credentials_name=''): actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) if not credentials_name: raise BadRequest("Invalid credentials_name") found_cred = -1 for i, cred in enumerate(actor_obj.credentials): if cred.username == credentials_name: found_cred = i break if found_cred != -1: del actor_obj.credentials[found_cred] else: raise NotFound("Credentials not found") actor_obj.alt_ids.remove("UNAME:" + credentials_name) # Lower level RR call to avoid credentials clearing self.rr.update(actor_obj) def find_actor_identity_by_username(self, username=''): if not username: raise BadRequest("Invalid username") res_ids, _ = self.rr.find_resources_ext(alt_id_ns="UNAME", alt_id=username, id_only=True) if not res_ids: raise NotFound("No actor found with username") return res_ids[0] def is_user_existing(self, username=''): if not username: raise BadRequest("Invalid username") res_ids, _ = self.rr.find_resources_ext(alt_id_ns="UNAME", alt_id=username, id_only=True) return bool(res_ids) def set_actor_credentials(self, actor_id='', username='', password=''): if not username: raise BadRequest("Invalid username") IdentityUtils.check_password_policy(password) actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) cred_obj = None for cred in actor_obj.credentials: if cred.username == username: cred_obj = cred break if not cred_obj: cred_obj = Credentials() cred_obj.username = username actor_obj.credentials.append(cred_obj) actor_obj.alt_ids.append("UNAME:" + username) self._generate_password_hash(cred_obj, password) # Lower level RR call to avoid credentials clearing self.rr.update(actor_obj) def set_user_password(self, username='', password=''): if not username: raise BadRequest("Invalid username") IdentityUtils.check_password_policy(password) actor_id = self.find_actor_identity_by_username(username) actor_obj = self.read_actor_identity(actor_id) cred_obj = None for cred in actor_obj.credentials: if cred.username == username: cred_obj = cred break self._generate_password_hash(cred_obj, password) # Lower level RR call to avoid credentials clearing self.rr.update(actor_obj) def _generate_password_hash(self, cred_obj, password): if not cred_obj or cred_obj.type_ != OT.Credentials: raise BadRequest("Invalid cred_obj") cred_obj.identity_provider = "SciON" cred_obj.authentication_service = "SciON IdM" cred_obj.password_salt = bcrypt.gensalt() cred_obj.password_hash = bcrypt.hashpw(password, cred_obj.password_salt) def check_actor_credentials(self, username='', password=''): if not username: raise BadRequest("Invalid argument username") if not password: raise BadRequest("Invalid argument password") actor_id = self.find_actor_identity_by_username(username) actor_obj = self.read_actor_identity(actor_id) try: if actor_obj.auth_status != AuthStatusEnum.ENABLED: raise NotFound("Actor not enabled") cred_obj = None for cred in actor_obj.credentials: if cred.username == username: cred_obj = cred break if bcrypt.hashpw(password, cred_obj.password_salt) != cred_obj.password_hash: # Failed login if password: # Only record fail if password is non-empty and wrong actor_obj.auth_fail_count += 1 actor_obj.auth_ts_last_fail = get_ion_ts() max_fail_cnt = IdentityUtils.get_auth_fail_lock_count() if actor_obj.auth_fail_count > max_fail_cnt: actor_obj.auth_status = AuthStatusEnum.LOCKED raise NotFound("Invalid password") # Success actor_obj.auth_count += 1 actor_obj.auth_fail_count = 0 actor_obj.auth_ts_last = get_ion_ts() return actor_obj._id finally: # Lower level RR call to avoid credentials clearing self.rr.update(actor_obj) self._publish_auth_event(actor_obj, username) def _publish_auth_event(self, actor_obj, username): if not self.event_pub: return event_data = dict() event_data['origin'] = actor_obj._id event_data['origin_type'] = RT.ActorIdentity event_data['sub_type'] = "SUCCESS" if actor_obj.auth_fail_count == 0 else "FAIL" event_data['username'] = username event_data['auth_count'] = actor_obj.auth_count event_data['auth_status'] = actor_obj.auth_status self.event_pub.publish_event(event_type='AuthenticationEvent', **event_data) def set_actor_auth_status(self, actor_id='', status=None): actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) if not status: raise BadRequest("Invalid argument status") prev_status = actor_obj.auth_status actor_obj.auth_status = status if status == AuthStatusEnum.ENABLED: actor_obj.auth_fail_count = 0 # Lower level RR call to avoid credentials clearing self.rr.update(actor_obj) return prev_status def request_password_reset(self, username=''): actor_id = self.find_actor_identity_by_username(username) actor = self.rr.read(actor_id) validity = CFG.get_safe(CFG_PREFIX + '.password_token_validity', 60*60) actor.passwd_reset_token = self._create_token(actor_id=actor_id, validity=validity, token_type=TokenTypeEnum.ACTOR_RESET_PASSWD) self.rr.update(actor) return actor.passwd_reset_token.token_string def reset_password(self, username='', token_string='', new_password=''): actor_id = self.find_actor_identity_by_username(username) actor = self.rr.read(actor_id) if not actor.passwd_reset_token or actor.passwd_reset_token.status != 'OPEN': raise Unauthorized("Token status invalid") cur_time = get_ion_ts_millis() if cur_time >= int(actor.passwd_reset_token.expires): raise Unauthorized("Token expired") if actor.passwd_reset_token.token_string != token_string: raise Unauthorized("Password reset token_string does not match") # Update password self.set_user_password(username, new_password) # Invalidate token after success actor = self.rr.read(actor_id) # Read again, resource was updated in between actor.passwd_reset_token = None self.rr.update(actor) # ------------------------------------------------------------------------- # Identity details (user profile) handling def define_identity_details(self, actor_id='', identity_details=None): actor_obj = self._validate_resource_id("actor_id", actor_id, RT.ActorIdentity) if not identity_details: raise BadRequest("Invalid argument identity_details") if actor_obj.details: if actor_obj.details.type_ != identity_details.type_: raise BadRequest("Type for identity_details does not match") actor_obj.details = identity_details self.update_actor_identity(actor_obj) def read_identity_details(self, actor_id=''): actor_obj = self.read_actor_identity(actor_id) return actor_obj.details # ------------------------------------------------------------------------- # Manage tokens - authentication and others # TODO: Make more compliant with OAuth2, use HMAC, JWT etc def _create_token(self, actor_id='', start_time='', validity=0, token_type=TokenTypeEnum.ACTOR_AUTH): if not actor_id: raise BadRequest("Must provide argument: actor_id") actor_obj = self.rr.read(actor_id) if actor_obj.type_ != RT.ActorIdentity: raise BadRequest("Illegal type for argument actor_id") if type(validity) not in (int, long): raise BadRequest("Illegal type for argument validity") if validity <= 0 or validity > MAX_TOKEN_VALIDITY: raise BadRequest("Illegal value for argument validity") cur_time = get_ion_ts_millis() if not start_time: start_time = cur_time start_time = int(start_time) if start_time > cur_time: raise BadRequest("Illegal value for start_time: Future values not allowed") if (start_time + 1000*validity) < cur_time: raise BadRequest("Illegal value for start_time: Already expired") expires = str(start_time + 1000*validity) token = self._generate_auth_token(actor_id, expires=expires, token_type=token_type) token_id = "token_%s" % token.token_string self.container.object_store.create(token, token_id) return token def _generate_auth_token(self, actor_id=None, expires="", token_type=TokenTypeEnum.ACTOR_AUTH): token_string = uuid4().hex token = SecurityToken(token_type=token_type, token_string=token_string, actor_id=actor_id, expires=expires, status="OPEN") return token def create_authentication_token(self, actor_id='', start_time='', validity=0): """Create an authentication token for provided actor id with a given start time and validity. start_time defaults to current time if empty and uses a system timestamp. validity is in seconds and must be set. """ return self._create_token(actor_id, start_time, validity).token_string def read_authentication_token(self, token_string=''): """Returns the token object for given actor authentication token string. """ token_id = "token_%s" % token_string token = self.container.object_store.read(token_id) if not isinstance(token, SecurityToken): raise Inconsistent("Token illegal type") return token def update_authentication_token(self, token=None): """Updates the given token. """ if not isinstance(token, SecurityToken): raise BadRequest("Illegal argument type: token") if token.token_type != TokenTypeEnum.ACTOR_AUTH: raise BadRequest("Argument token: Illegal type") cur_time = get_ion_ts_millis() token_exp = int(token.expires) if token_exp > cur_time + 1000*MAX_TOKEN_VALIDITY: raise BadRequest("Argument token: Maximum expiry extended") self.container.object_store.update(token) def invalidate_authentication_token(self, token_string=''): """Invalidates an authentication token, but leaves it in place for auditing purposes. """ token_id = "token_%s" % token_string token = self.container.object_store.read(token_id) if not isinstance(token, SecurityToken): raise Inconsistent("Token illegal type") if token.token_type != TokenTypeEnum.ACTOR_AUTH: raise BadRequest("Illegal token type") token.status = "INVALID" self.container.object_store.update(token) log.info("Invalidated security auth token: %s", token.token_string) def check_authentication_token(self, token_string=''): """Checks given token and returns a dict with actor id if valid. """ token_id = "token_%s" % token_string token = self.container.object_store.read(token_id) if not isinstance(token, SecurityToken): raise Inconsistent("Token illegal type") if token.token_type != TokenTypeEnum.ACTOR_AUTH: raise BadRequest("Illegal token type") if token.token_string != token_string: raise Inconsistent("Found token's token_string does not match") cur_time = get_ion_ts_millis() if token.status != "OPEN": raise Unauthorized("Token status invalid") if cur_time >= int(token.expires): raise Unauthorized("Token expired") token_info = dict(actor_id=token.actor_id, expiry=token.expires, token=token, token_id=token_id) log.info("Authentication token %s resolved to actor %s, expiry %s", token_string, token.actor_id, token.expires) return token_info def _get_actor_authentication_tokens(self, actor_id): actor_tokens = [] raise NotImplementedError("TODO")