def fully_unlock(self): """This fully unlocks the mutex, removing all levels of recursion Returns: None """ if self._is_locked == 0: return from Acquire.ObjectStore import ObjectStore as _ObjectStore from Acquire.ObjectStore import get_datetime_now as _get_datetime_now try: holder = _ObjectStore.get_string_object(self._bucket, self._key) except: holder = None if holder == self._lockstring: # we hold the mutex - delete the key _ObjectStore.delete_object(self._bucket, self._key) self._lockstring = None self._is_locked = 0 if self._end_lease < _get_datetime_now(): self._end_lease = None from Acquire.ObjectStore import MutexTimeoutError raise MutexTimeoutError("The lease on this mutex expired before " "this mutex was unlocked!") else: self._end_lease = None
def untrust_service(service): """Stop trusting the passed service. This will remove the service as being trusted. You must pass in a valid admin_user authorisation for this service """ from Acquire.Service import is_running_service as _is_running_service if _is_running_service(): from Acquire.Service import get_service_account_bucket as \ _get_service_account_bucket from Acquire.ObjectStore import url_to_encoded as \ _url_to_encoded bucket = _get_service_account_bucket() urlkey = "_trusted/url/%s" % _url_to_encoded(service.canonical_url()) uidkey = "_trusted/uid/%s" % service.uid() # delete the trusted service by both canonical_url and uid try: _ObjectStore.delete_object(bucket, uidkey) except: pass try: _ObjectStore.delete_object(bucket, urlkey) except: pass from Acquire.Service import clear_services_cache \ as _clear_services_cache _clear_services_cache() else: from Acquire.Client import Wallet as _Wallet wallet = _Wallet() wallet.remove_service(service)
def _delete_note(self, note, bucket=None): """Internal function called to delete the passed note from the record. This is unsafe and should only be called from DebitNote.return_value or CreditNote.return_value (which themselves are only called from Ledger) """ if note is None: return if isinstance(note, _DebitNote) or isinstance(note, _CreditNote): item_key = "%s/%s" % (self._key(), note.uid()) if bucket is None: bucket = _login_to_service_account() # remove the note try: _ObjectStore.delete_object(bucket, item_key) except: pass # now remove all day-balances from the day before this note # to today. Hopefully this will prevent any ledger errors... day0 = _datetime.datetime.fromtimestamp( note.timestamp()).toordinal() - 1 day1 = _datetime.datetime.now().toordinal() for day in range(day0, day1 + 1): balance_key = self._get_balance_key( _datetime.datetime.fromordinal(day)) try: _ObjectStore.delete_object(bucket, balance_key) except: pass
def remove_trusted_service_info(service_url): """Remove the passed 'service_url' from the list of trusted services""" bucket = _login_to_service_account() try: _ObjectStore.delete_object(bucket, "services/%s" % url_to_encoded(service_url)) except: pass
def close(par): """Close the passed PAR. This will remove the registration for the PAR and will also call the associated cleanup_function (if any) """ from Acquire.Service import is_running_service as _is_running_service if not _is_running_service(): return from Acquire.Service import get_service_account_bucket \ as _get_service_account_bucket from Acquire.ObjectStore import OSPar as _OSPar from Acquire.ObjectStore import ObjectStore as _ObjectStore from Acquire.ObjectStore import datetime_to_string \ as _datetime_to_string from Acquire.ObjectStore import Function as _Function if par is None: return if not isinstance(par, _OSPar): raise TypeError("You can only close OSPar objects!") if par.is_null(): return expire_string = _datetime_to_string(par.expires_when()) bucket = _get_service_account_bucket() key = "%s/expire/%s/%s" % (_registry_key, expire_string, par.uid()) try: _ObjectStore.delete_object(bucket=bucket, key=key) except: pass key = "%s/uid/%s/%s" % (_registry_key, par.uid(), expire_string) try: data = _ObjectStore.take_object_from_json(bucket=bucket, key=key) except: data = None if data is None: # this PAR has already been closed return if "cleanup_function" in data: cleanup_function = _Function.from_data(data["cleanup_function"]) cleanup_function(par=par)
def delete_object(bucket, key): """ Delete the object at key in bucket Args: bucket (str): Bucket containing data key (str): Key for data in bucket Returns: None """ return ObjectStore.delete_object(bucket=bucket, key=key)
def _set_status(self, status): """Internal function to set the status of the session. This ensures that the data for the session is saved into the correct part of the object store """ if self.is_null(): raise PermissionError( "Cannot set the status of a null LoginSession") if status not in [ "approved", "pending", "denied", "suspicious", "logged_out" ]: raise ValueError("Cannot set an invalid status '%s'" % status) if status == self._status: return from Acquire.ObjectStore import ObjectStore as _ObjectStore from Acquire.Service import get_service_account_bucket \ as _get_service_account_bucket bucket = _get_service_account_bucket() key = self._get_key() try: _ObjectStore.delete_object(bucket=bucket, key=key) except: pass self._status = status key = self._get_key() _ObjectStore.set_object_from_json(bucket=bucket, key=key, data=self.to_data()) key = "%s/status/%s" % (_sessions_key, self._uid) _ObjectStore.set_string_object(bucket=bucket, key=key, string_data=status)
def create_account(self, name, description=None, overdraft_limit=None, bucket=None): """Create a new account called 'name' in this group. This will return the existing account if it already exists """ if name is None: raise ValueError("You must pass a name of the new account") account_key = self._account_key(name) if bucket is None: bucket = _login_to_service_account() try: account_uid = _ObjectStore.get_string_object(bucket, account_key) except: account_uid = None if account_uid is not None: # this account already exists - just return it account = _Account(uid=account_uid, bucket=bucket) if overdraft_limit is not None: account.set_overdraft_limit(overdraft_limit, bucket=bucket) return account # make sure that no-one has created this account before m = _Mutex(account_key, timeout=600, lease_time=600, bucket=bucket) try: account_uid = _ObjectStore.get_string_object(bucket, account_key) except: account_uid = None if account_uid is not None: m.unlock() # this account already exists - just return it account = _Account(uid=account_uid, bucket=bucket) if overdraft_limit is not None: account.set_overdraft_limit(overdraft_limit, bucket=bucket) return account # write a temporary UID to the object store so that we # can ensure we are the only function to create it try: _ObjectStore.set_string_object(bucket, account_key, "under_construction") except: m.unlock() raise m.unlock() # ok - we are the only function creating this account. Let's try # to create it properly try: account = _Account(name=name, description=description, bucket=bucket) except: try: _ObjectStore.delete_object(bucket, account_key) except: pass raise if overdraft_limit is not None: account.set_overdraft_limit(overdraft_limit, bucket=bucket) _ObjectStore.set_string_object(bucket, account_key, account.uid()) return account
def run(args): """This function will allow the current user to authorise a logout from the current session - this will be authorised by signing the request to logout""" status = 0 message = None session_uid = args["session_uid"] username = args["username"] permission = args["permission"] signature = string_to_bytes(args["signature"]) # generate a sanitised version of the username user_account = UserAccount(username) # now log into the central identity account to query # the current status of this login session bucket = login_to_service_account() user_session_key = "sessions/%s/%s" % \ (user_account.sanitised_name(), session_uid) request_session_key = "requests/%s/%s" % (session_uid[:8], session_uid) login_session = LoginSession.from_data( ObjectStore.get_object_from_json(bucket, user_session_key)) if login_session: # get the signing certificate from the login session and # validate that the permission object has been signed by # the user requesting the logout cert = login_session.public_certificate() cert.verify(signature, permission) # the signature was correct, so log the user out. For record # keeping purposes we change the loginsession to a logout state # and move it to another part of the object store if login_session.is_approved(): login_session.logout() # only save sessions that were successfully approved if login_session: if login_session.is_logged_out(): expired_session_key = "expired_sessions/%s/%s" % \ (user_account.sanitised_name(), session_uid) ObjectStore.set_object_from_json(bucket, expired_session_key, login_session.to_data()) try: ObjectStore.delete_object(bucket, user_session_key) except: pass try: ObjectStore.delete_object(bucket, request_session_key) except: pass status = 0 message = "Successfully logged out" return_value = create_return_value(status, message) return return_value
def _debit(self, transaction, authorisation, is_provisional, bucket=None): """Debit the value of the passed transaction from this account based on the authorisation contained in 'authorisation'. This will create a unique ID (UID) for this debit and will return this together with the timestamp of the debit. If this transaction 'is_provisional' then it will be recorded as a liability. The UID will encode both the date of the debit and provide a random ID that together can be used to identify the transaction associated with this debit in the future. This will raise an exception if the debit cannot be completed, e.g. if the authorisation is invalid, if the debit exceeds a limit or there are insufficient funds in the account Note that this function is private as it should only be called by the DebitNote class """ if self.is_null() or transaction.value() <= 0: return None if not isinstance(transaction, _Transaction): raise TypeError("The passed transaction must be a Transaction!") self.assert_valid_authorisation(authorisation) if bucket is None: bucket = _login_to_service_account() if self.available_balance(bucket) < transaction.value(): raise InsufficientFundsError( "You cannot debit '%s' from account %s as there " "are insufficient funds in this account." % (transaction, str(self))) # create a UID and timestamp for this debit and record # it in the account now = self._get_safe_now() # we need to record the exact timestamp of this debit... timestamp = now.timestamp() # and to create a key to find this debit later. The key is made # up from the date and timestamp of the debit and a random string day_key = "%4d-%02d-%02d/%s" % (now.year, now.month, now.day, timestamp) uid = "%s/%s" % (day_key, str(_uuid.uuid4())[0:8]) # the key in the object store is a combination of the key for this # account plus the uid for the debit plus the actual debit value. # We record the debit value in the key so that we can accumulate # the balance from just the key names if is_provisional: encoded_value = _TransactionInfo.encode( _TransactionCode.CURRENT_LIABILITY, transaction.value()) else: encoded_value = _TransactionInfo.encode(_TransactionCode.DEBIT, transaction.value()) item_key = "%s/%s/%s" % (self._key(), uid, encoded_value) # create a line_item for this debit and save it to the object store line_item = _LineItem(uid, authorisation) _ObjectStore.set_object_from_json(bucket, item_key, line_item.to_data()) if self.is_beyond_overdraft_limit(bucket): # This transaction has helped push the account beyond the # overdraft limit. Delete the transaction and raise # an InsufficientFundsError _ObjectStore.delete_object(bucket, item_key) raise InsufficientFundsError( "You cannot debit '%s' from account %s as there " "are insufficient funds in this account." % (transaction, str(self))) return (uid, timestamp)
def get_service(self, service_uid=None, service_url=None): """Load and return the service with specified url or uid from the registry. This will consult with other registry services to find the matching service """ from Acquire.ObjectStore import ObjectStore as _ObjectStore from Acquire.Service import Service as _Service from Acquire.ObjectStore import string_to_encoded \ as _string_to_encoded from Acquire.Service import get_this_service as _get_this_service this_service = _get_this_service(need_private_access=False) if service_url is not None: from Acquire.Service import Service as _Service service_url = _Service.get_canonical_url(service_url) if this_service.uid() == service_uid: return this_service elif this_service.canonical_url() == service_url: return this_service bucket = self.get_bucket() service_key = self.get_service_key(service_uid=service_uid, service_url=service_url) service = None if service_key is not None: try: data = _ObjectStore.get_object_from_json(bucket=bucket, key=service_key) service = _Service.from_data(data) except: pass if service is not None: must_write = False if service.uid() == "STAGE1": # we need to directly ask the service for its info service = self.challenge_service(service) if service.uid() == "STAGE1": from Acquire.Service import MissingServiceError raise MissingServiceError( "Service %s|%s not available as it is still under " "construction!" % (service_uid, service)) # we can now move this service from pending to active uidkey = self._get_key_for_uid(service.uid()) domain = self._get_domain(service.service_url()) domainroot = self._get_root_key_for_domain(domain=domain) pending_key = "%s/pending/%s" % (domainroot, service.uid()) active_key = "%s/active/%s" % (domainroot, service.uid()) try: _ObjectStore.delete_object(bucket=bucket, key=pending_key) except: pass try: _ObjectStore.set_string_object(bucket=bucket, key=active_key, string_data=uidkey) except: pass must_write = True elif service.should_refresh_keys(): service.refresh_keys() must_write = True if must_write: data = service.to_data() _ObjectStore.set_object_from_json(bucket=bucket, key=service_key, data=data) return service # we only get here if we can't find the service on this registry. # In the future, we will use the initial part of the UID of # the service to ask its registering registry for its data. # For now, we just raise an error from Acquire.Service import MissingServiceError raise MissingServiceError( "No service available: service_url=%s service_uid=%s" % (service_url, service_uid))
def run(args): """This function is called by the user to log in and validate that a session is authorised to connect""" status = 0 message = None provisioning_uri = None assigned_device_uid = None short_uid = args["short_uid"] username = args["username"] password = args["password"] otpcode = args["otpcode"] try: remember_device = args["remember_device"] except: remember_device = False try: device_uid = args["device_uid"] except: device_uid = None # create the user account for the user user_account = UserAccount(username) # log into the central identity account to query # the current status of this login session bucket = login_to_service_account() # locate the session referred to by this uid base_key = "requests/%s" % short_uid session_keys = ObjectStore.get_all_object_names(bucket, base_key) # try all of the sessions to find the one that the user # may be referring to... login_session_key = None request_session_key = None for session_key in session_keys: request_session_key = "%s/%s" % (base_key, session_key) session_user = ObjectStore.get_string_object( bucket, request_session_key) # did the right user request this session? if user_account.name() == session_user: if login_session_key: # this is an extremely unlikely edge case, whereby # two login requests within a 30 minute interval for the # same user result in the same short UID. This should be # signified as an error and the user asked to create a # new request raise LoginError( "You have found an extremely rare edge-case " "whereby two different login requests have randomly " "obtained the same short UID. As we can't work out " "which request is valid, the login is denied. Please " "create a new login request, which will then have a " "new login request UID") else: login_session_key = session_key if not login_session_key: raise LoginError( "There is no active login request with the " "short UID '%s' for user '%s'" % (short_uid, username)) login_session_key = "sessions/%s/%s" % (user_account.sanitised_name(), login_session_key) # fully load the user account from the object store so that we # can validate the username and password try: account_key = "accounts/%s" % user_account.sanitised_name() user_account = UserAccount.from_data( ObjectStore.get_object_from_json(bucket, account_key)) except: raise LoginError("No account available with username '%s'" % username) if (not remember_device) and device_uid: # see if this device has been seen before device_key = "devices/%s/%s" % (user_account.sanitised_name(), device_uid) try: device_secret = ObjectStore.get_string_object(bucket, device_key) except: device_secret = None if device_secret is None: raise LoginError( "The login device is not recognised. Please try to " "log in again using your master one-time-password.") else: device_secret = None # now try to log into this account using the supplied # password and one-time-code try: if device_secret: user_account.validate_password(password, otpcode, device_secret=device_secret) elif remember_device: (device_secret, provisioning_uri) = \ user_account.validate_password( password, otpcode, remember_device=True) device_uid = str(uuid.uuid4()) device_key = "devices/%s/%s" % (user_account.sanitised_name(), device_uid) assigned_device_uid = device_uid else: user_account.validate_password(password, otpcode) except: # don't leak info about why validation failed raise LoginError("The password or OTP code is incorrect") # the user is valid - load up the actual login session login_session = LoginSession.from_data( ObjectStore.get_object_from_json(bucket, login_session_key)) # we must record the session against which this otpcode has # been validated. This is to stop us validating an otpcode more than # once (e.g. if the password and code have been intercepted). # Any sessions validated using the same code should be treated # as immediately suspcious otproot = "otps/%s" % user_account.sanitised_name() sessions = ObjectStore.get_all_strings(bucket, otproot) utcnow = datetime.datetime.utcnow() for session in sessions: otpkey = "%s/%s" % (otproot, session) otpstring = ObjectStore.get_string_object(bucket, otpkey) (timestamp, code) = otpstring.split("|||") # remove all codes that are more than 10 minutes old. The # otp codes are only valid for 3 minutes, so no need to record # codes that have been used that are older than that... timedelta = utcnow - datetime.datetime.fromtimestamp( float(timestamp)) if timedelta.seconds > 600: try: ObjectStore.delete_object(bucket, otpkey) except: pass elif code == str(otpcode): # Low probability there is some recycling, # but very suspicious if the code was validated within the last # 10 minutes... (as 3 minute timeout of a code) suspect_key = "sessions/%s/%s" % ( user_account.sanitised_name(), session) suspect_session = None try: suspect_session = LoginSession.from_data( ObjectStore.get_object_from_json(bucket, suspect_key)) except: pass if suspect_session: suspect_session.set_suspicious() ObjectStore.set_object_from_json(bucket, suspect_key, suspect_session.to_data()) raise LoginError( "Cannot authorise the login as the one-time-code " "you supplied has already been used within the last 10 " "minutes. The chance of this happening is really low, so " "we are treating this as a suspicious event. You need to " "try another code. Meanwhile, the other login that used " "this code has been put into a 'suspicious' state.") # record the value and timestamp of when this otpcode was used otpkey = "%s/%s" % (otproot, login_session.uuid()) otpstring = "%s|||%s" % (datetime.datetime.utcnow().timestamp(), otpcode) ObjectStore.set_string_object(bucket, otpkey, otpstring) login_session.set_approved() # write this session back to the object store ObjectStore.set_object_from_json(bucket, login_session_key, login_session.to_data()) # save the device secret as everything has now worked if assigned_device_uid: ObjectStore.set_string_object(bucket, device_key, device_secret) # finally, remove this from the list of requested logins try: ObjectStore.delete_object(bucket, request_session_key) except: pass status = 0 message = "Success: Status = %s" % login_session.status() return_value = create_return_value(status, message) if provisioning_uri: return_value["provisioning_uri"] = provisioning_uri return_value["device_uid"] = assigned_device_uid return return_value
def create_account(self, name, description=None, overdraft_limit=None, bucket=None, authorisation=None): """Create a new account called 'name' in this group. This will return the existing account if it already exists Args: name (str): Name of account to create description (default=None): Description of account overdraft_limit (int, default=None): Limit of overdraft bucket (dict, default=None): Bucket to load data from Returns: Account: New Account object """ if name is None: raise ValueError("You must pass a name of the new account") from Acquire.Identity import Authorisation as _Authorisation if authorisation is not None: if not isinstance(authorisation, _Authorisation): raise TypeError("The authorisation must be type Authorisation") if self._user_guid is not None: if self._user_guid != authorisation.user_guid(): raise PermissionError( "The same user who opened this accounts group (%s) " "must create accounts in this group (%s)" % (self._user_guid, authorisation.user_guid())) authorisation.verify("create_account %s" % name) self._assert_is_writeable() account_key = self._account_key(name) if bucket is None: from Acquire.Service import get_service_account_bucket \ as _get_service_account_bucket bucket = _get_service_account_bucket() from Acquire.ObjectStore import ObjectStore as _ObjectStore from Acquire.Accounting import Account as _Account try: account_uid = _ObjectStore.get_string_object(bucket, account_key) except: account_uid = None if account_uid is not None: # this account already exists - just return it account = _Account(uid=account_uid, bucket=bucket) if account.group_name() != self.name(): account.set_group(self) if overdraft_limit is not None: account.set_overdraft_limit(overdraft_limit, bucket=bucket) return account # make sure that no-one has created this account before from Acquire.ObjectStore import Mutex as _Mutex m = _Mutex(account_key, timeout=600, lease_time=600, bucket=bucket) try: account_uid = _ObjectStore.get_string_object(bucket, account_key) except: account_uid = None if account_uid is not None: m.unlock() # this account already exists - just return it account = _Account(uid=account_uid, bucket=bucket) if account.group_name() != self.name(): account.set_group(self) if overdraft_limit is not None: account.set_overdraft_limit(overdraft_limit, bucket=bucket) return account # write a temporary UID to the object store so that we # can ensure we are the only function to create it try: _ObjectStore.set_string_object(bucket, account_key, "under_construction") except: m.unlock() raise m.unlock() # ok - we are the only function creating this account. Let's try # to create it properly. The account is created with the same # ACLRules of the group. try: from Acquire.Identity import ACLRules as _ACLRules account = _Account(name=name, description=description, group_name=self.name(), aclrules=_ACLRules.inherit(), bucket=bucket) except: try: _ObjectStore.delete_object(bucket, account_key) except: pass raise if overdraft_limit is not None: account.set_overdraft_limit(overdraft_limit, bucket=bucket) _ObjectStore.set_string_object(bucket, account_key, account.uid()) return account
def prune_expired_sessions(bucket, user_account, root, sessions, log=[]): """This function will scan through all open requests and login sessions and will prune away old, expired or otherwise weird sessions. It will also use the ipaddress of the source to rate limit or blacklist sources""" for name in sessions: key = "%s/%s" % (root, name) request_key = "requests/%s/%s" % (name[:8], name) try: session = ObjectStore.get_object_from_json(bucket, key) except: log.append("Session %s does not exist!" % name) session = None if session: should_delete = False should_logout = False try: session = LoginSession.from_data(session) if session.is_approved() or session.is_suspicious(): if session.hours_since_creation() > user_account \ .login_timeout(): should_logout = True should_delete = True else: if session.hours_since_creation() > user_account \ .login_request_timeout(): log.append("Expired login request: %s > %s" % (session.hours_since_creation(), user_account.login_request_timeout())) should_delete = True except Exception as e: # this is corrupt - delete it log.append("Deleting session as corrupt? %s" % str(e)) should_delete = True if should_logout: # auto-logout expired sessions log.append("Auto-logging out expired session '%s'" % key) session.logout() expire_session_key = "expired_sessions/%s/%s" % \ (user_account.sanitised_name(), session.uuid()) ObjectStore.set_object_from_json(bucket, expire_session_key, session.to_data()) # now delete any expired sessions if should_delete: log.append("Deleting expired session '%s'" % key) try: ObjectStore.delete_object(bucket, key) except: pass try: ObjectStore.delete_object(bucket, request_key) except: pass