def test_objstore(bucket): keys = [] message = "ƒƒƒ Hello World ∂∂∂" ObjectStore.set_string_object(bucket, "test", message) keys.append("test") assert (message == ObjectStore.get_string_object(bucket, "test")) message = "€€#¢∞ Hello ˚ƒ´πµçµΩ" ObjectStore.set_string_object(bucket, "test/something", message) keys.append("test/something") assert (message == ObjectStore.get_string_object(bucket, "test/something")) data = { "cat": "mieow", "dog": "woof", "sounds": [1, 2, 3, 4, 5], "flag": True } ObjectStore.set_object_from_json(bucket, "test/object", data) keys.append("test/object") assert (data == ObjectStore.get_object_from_json(bucket, "test/object")) names = ObjectStore.get_all_object_names(bucket) assert (len(names) == len(keys)) for name in names: assert (name in keys)
def get_service_user_account_uid(accounting_service_uid): """Return the UID of the financial Acquire.Accounting.Account that is held on the accounting service with UID 'accounting_service_uid' for the service user on this service. This is the account to which payment for this service should be sent """ assert_running_service() from Acquire.Service import get_service_account_bucket as \ _get_service_account_bucket from Acquire.ObjectStore import ObjectStore as _ObjectStore bucket = _get_service_account_bucket() key = "%s/account/%s" % (_service_key, accounting_service_uid) try: account_uid = _ObjectStore.get_string_object(bucket, key) except: account_uid = None if account_uid is None: from Acquire.Service import ServiceAccountError raise ServiceAccountError( "This service does not have a valid financial account on " "the accounting service at '%s'" % accounting_service_uid) return account_uid
def contains(self, account, bucket=None): """Return whether or not this group contains the passed account Args: account (:obj:`Account`): Account to check against group bucket (dict, default=None): Bucket to load data from Returns: bool : True if account in group, else False """ self._assert_is_readable() from Acquire.Accounting import Account as _Account if not isinstance(account, _Account): raise TypeError("The passed account must be of type Account") if bucket is None: from Acquire.Service import get_service_account_bucket \ as _get_service_account_bucket bucket = _get_service_account_bucket() # read the UID of the account in this group that matches the # passed account's name try: from Acquire.ObjectStore import ObjectStore as _ObjectStore account_uid = _ObjectStore.get_string_object( bucket, self._account_key(account.name())) except: account_uid = None return account.uid() == account_uid
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 create_service_user_account(service, accounting_service_url): """Call this function to create the financial service account for this service on the accounting service at 'accounting_service_url' This does nothing if the account already exists """ assert_running_service() accounting_service = service.get_trusted_service( service_url=accounting_service_url) accounting_service_uid = accounting_service.uid() key = "%s/account/%s" % (_service_key, accounting_service_uid) bucket = service.bucket() from Acquire.ObjectStore import ObjectStore as _ObjectStore try: account_uid = _ObjectStore.get_string_object(bucket, key) except: account_uid = None if account_uid: # we already have an account... return service_user = service.login_service_user() try: from Acquire.Client import create_account as _create_account from Acquire.Client import deposit as _deposit account = _create_account( service_user, "main", "Main account to receive payment for all use on service " "%s (%s)" % (service.canonical_url(), service.uid()), accounting_service=accounting_service) _deposit(user=service_user, value=100.0, account_name="main", accounting_service=accounting_service) account_uid = account.uid() _ObjectStore.set_string_object(bucket, key, account_uid) except Exception as e: from Acquire.Service import exception_to_string from Acquire.Service import ServiceAccountError raise ServiceAccountError( "Unable to create a financial account for the service " "principal for '%s' on accounting service '%s'\n\nERROR\n%s" % (str(service), str(accounting_service), exception_to_string(e)))
def get_service_key(self, service_uid=None, service_url=None): """Return the key for the passed service in the object store""" if service_uid is not None: return self._get_key_for_uid(service_uid) else: bucket = self.get_bucket() key = self._get_key_for_url(service_url) try: from Acquire.ObjectStore import ObjectStore as _ObjectStore service_key = _ObjectStore.get_string_object(bucket=bucket, key=key) except: service_key = None return service_key
def contains(self, account, bucket=None): """Return whether or not this group contains the passed account""" if not isinstance(account, _Account): raise TypeError("The passed account must be of type Account") if bucket is None: bucket = _login_to_service_account() # read the UID of the account in this group that matches the # passed account's name try: account_uid = _ObjectStore.get_string_object( bucket, self._account_key(account.name())) except: account_uid = None return account.uid() == account_uid
def get_status(uid): """Return the status of the LoginSession with specified UID""" 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() try: key = "%s/status/%s" % (_sessions_key, uid) status = _ObjectStore.get_string_object(bucket=bucket, key=key) except: status = None if status is None: from Acquire.Identity import LoginSessionError raise LoginSessionError("Cannot find a session with UID '%s'" % uid) return status
def get_account(self, name, bucket=None): """Return the account called 'name' from this group Args: name (:obj:`str`): Name of account to retrieve bucket (:obj:`dict`): Bucket to load data from Returns: :obj:`Account`: Account object """ self._assert_is_readable() if bucket is None: from Acquire.Service import get_service_account_bucket \ as _get_service_account_bucket bucket = _get_service_account_bucket() try: from Acquire.ObjectStore import ObjectStore as _ObjectStore account_uid = _ObjectStore.get_string_object( bucket, self._account_key(name)) except: account_uid = None if account_uid is None: # ensure that the user always has a "main" account if name == "main": return self.create_account("main", "primary user account", overdraft_limit=0, bucket=bucket) from Acquire.Accounting import AccountError raise AccountError("There is no account called '%s' in the " "group '%s'" % (name, self.group())) from Acquire.Accounting import Account as _Account return _Account(uid=account_uid, bucket=bucket)
def get_account(self, name, bucket=None): """Return the account called 'name' from this group""" if bucket is None: bucket = _login_to_service_account() try: account_uid = _ObjectStore.get_string_object( bucket, self._account_key(name)) except: account_uid = None if account_uid is None: # ensure that the user always has a "main" account if name == "main": return self.create_account("main", "primary user account", overdraft_limit=0, bucket=bucket) raise AccountError("There is no account called '%s' in the " "group '%s'" % (name, self.group())) return _Account(uid=account_uid, bucket=bucket)
def load_service_key_from_objstore(fingerprint): """This function will see if we have an old key with the requested fingerprint, and if so, we will try to load and return that key from the object store """ from Acquire.ObjectStore import ObjectStore as _ObjectStore from Acquire.Service import get_service_account_bucket \ as _get_service_account_bucket from Acquire.Crypto import KeyManipulationError bucket = _get_service_account_bucket() try: key = "%s/oldkeys/fingerprints/%s" % (_service_key, fingerprint) keyfile = _ObjectStore.get_string_object(bucket, key) except: keyfile = None if keyfile is None: raise KeyManipulationError( "Cannot find a key or certificate with fingerprint '%s' : %s" % (fingerprint, key)) try: keydata = _ObjectStore.get_object_from_json(bucket, keyfile) except Exception as e: keydata = None error = str(e) if keydata is None: raise KeyManipulationError( "Unable to load the key or certificate with fingerprint '%s': %s" % (fingerprint, error)) service = get_this_service(need_private_access=True) return service.load_keys(keydata)[fingerprint]
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): """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 get_trusted_service(service_url=None, service_uid=None, service_type=None, autofetch=True): """Return the trusted service info for the service with specified service_url or service_uid""" if service_url is not None: from Acquire.Service import Service as _Service service_url = _Service.get_canonical_url(service_url, service_type=service_type) from Acquire.Service import is_running_service as _is_running_service if _is_running_service(): from Acquire.Service import get_this_service as _get_this_service from Acquire.Service import Service as _Service from Acquire.Service import get_service_account_bucket as \ _get_service_account_bucket from Acquire.ObjectStore import ObjectStore as _ObjectStore from Acquire.ObjectStore import url_to_encoded as \ _url_to_encoded service = _get_this_service() if service_url is not None and service.canonical_url() == service_url: # we trust ourselves :-) return service if service_uid is not None and service.uid() == service_uid: # we trust ourselves :-) return service bucket = _get_service_account_bucket() uidkey = None data = None if service_uid is not None: uidkey = "_trusted/uid/%s" % service_uid try: data = _ObjectStore.get_object_from_json(bucket, uidkey) except: pass elif service_url is not None: urlkey = "_trusted/url/%s" % _url_to_encoded(service_url) try: uidkey = _ObjectStore.get_string_object(bucket, urlkey) if uidkey is not None: data = _ObjectStore.get_object_from_json(bucket, uidkey) except: pass if data is not None: remote_service = _Service.from_data(data) if remote_service.should_refresh_keys(): # need to update the keys in our copy of the service remote_service.refresh_keys() if uidkey is not None: _ObjectStore.set_object_from_json(bucket, uidkey, remote_service.to_data()) return remote_service if not autofetch: from Acquire.Service import ServiceAccountError if service_uid is not None: raise ServiceAccountError( "We do not trust the service with UID '%s'" % service_uid) else: raise ServiceAccountError( "We do not trust the service at URL '%s'" % service_url) # we can try to fetch this data - we will ask our own # registry from Acquire.Registry import get_trusted_registry_service \ as _get_trusted_registry_service registry = _get_trusted_registry_service(service_uid=service.uid()) service = registry.get_service(service_uid=service_uid, service_url=service_url) from Acquire.Service import trust_service as _trust_service _trust_service(service) return service else: # this is running on the client from Acquire.Client import Wallet as _Wallet wallet = _Wallet() service = wallet.get_service(service_uid=service_uid, service_url=service_url, service_type=service_type, autofetch=autofetch) return service
def test_objstore(bucket): keys = [] message = "ƒƒƒ Hello World ∂∂∂" ObjectStore.set_string_object(bucket, "test", message) keys.append("test") assert(message == ObjectStore.get_string_object(bucket, "test")) message = "€€#¢∞ Hello ˚ƒ´πµçµΩ" ObjectStore.set_string_object(bucket, "test/something", message) keys.append("test/something") assert(message == ObjectStore.get_string_object(bucket, "test/something")) data = {"cat": "mieow", "dog": "woof", "sounds": [1, 2, 3, 4, 5], "flag": True} ObjectStore.set_object_from_json(bucket, "test/object", data) keys.append("test/object") assert(data == ObjectStore.get_object_from_json(bucket, "test/object")) names = ObjectStore.get_all_object_names(bucket) assert(len(names) == len(keys)) names = ObjectStore.get_all_object_names(bucket, "test") assert(len(names) == 3) names = ObjectStore.get_all_object_names(bucket, "test/") assert(len(names) == 2) names = ObjectStore.get_all_object_names(bucket, "test/some") assert(len(names) == 1) for name in names: assert(name in keys) new_bucket = ObjectStore.create_bucket(bucket, "new_bucket") ObjectStore.set_object_from_json(new_bucket, "test/object2", data) assert(data == ObjectStore.get_object_from_json(new_bucket, "test/object2")) with pytest.raises(ObjectStoreError): new_bucket = ObjectStore.create_bucket(bucket, "testing_objstore") with pytest.raises(ObjectStoreError): new_bucket = ObjectStore.create_bucket(bucket, "new_bucket") with pytest.raises(ObjectStoreError): new_bucket = ObjectStore.get_bucket(bucket, "get_bucket", create_if_needed=False) new_bucket = ObjectStore.get_bucket(bucket, "get_bucket", create_if_needed=True) test_key = "test_string" test_value = "test_string_value" ObjectStore.set_string_object(new_bucket, test_key, test_value) new_bucket2 = ObjectStore.get_bucket(bucket, "get_bucket", create_if_needed=False) test_value2 = ObjectStore.get_string_object(new_bucket2, test_key) assert(test_value == test_value2)
def register_service(self, service, force_new_uid=False): """Register the passed service""" from Acquire.Service import Service as _Service from Acquire.ObjectStore import ObjectStore as _ObjectStore if not isinstance(service, _Service): raise TypeError("You can only register Service objects") if service.uid() != "STAGE1": raise PermissionError("You cannot register a service twice!") # first, stop a single domain monopolising resources... bucket = self.get_bucket() domain = self._get_domain(service.service_url()) domainroot = self._get_root_key_for_domain(domain=domain) try: pending_keys = _ObjectStore.get_all_object_names( bucket=bucket, prefix="%s/pending/" % domainroot) num_pending = len(pending_keys) except: num_pending = 0 if num_pending >= 4: raise PermissionError( "You cannot register a new service as you have reached " "the quota (4) for the number of pending services registered " "against the domain '%s'. Please get some of these services " "so that you can make them active." % domain) try: active_keys = _ObjectStore.get_all_object_names( bucket=bucket, prefix="%s/active/" % domainroot) num_active = len(active_keys) except: num_active = 0 if num_active + num_pending >= 16: raise PermissionError( "You cannot register a new service as you have reached " "the quota (16) for the number registered against the " "domain '%s'" % domain) # first, challenge the service to ensure that it exists # and our keys are correct service = self.challenge_service(service) if service.uid() != "STAGE1": raise PermissionError("You cannot register a service twice!") bucket = self.get_bucket() urlkey = self._get_key_for_url(service.canonical_url()) try: uidkey = _ObjectStore.get_string_object(bucket=bucket, key=urlkey) except: uidkey = None service_uid = None if uidkey is not None: # there is already a service registered at this domain. Since # we have successfully challenged the service, this must be # someone re-bootstrapping a service. It is safe to give them # back their UID if requested if not force_new_uid: service_uid = self._get_uid_from_key(uidkey) if service_uid is None: # how many services from this domain are still pending? service_uid = _generate_service_uid( bucket=self.get_bucket(), registry_uid=self.registry_uid()) # save this service to the object store uidkey = self._get_key_for_uid(service_uid) _ObjectStore.set_object_from_json(bucket=bucket, key=uidkey, data=service.to_data()) _ObjectStore.set_string_object(bucket=bucket, key=urlkey, string_data=uidkey) domainkey = self._get_root_key_for_domain(domain=domain) _ObjectStore.set_string_object( bucket=bucket, key="%s/pending/%s" % (domainkey, service_uid), string_data=uidkey) return service_uid
def run(args): """This function will allow anyone to query who matches the passed UID or username (map from one to the other)""" status = 0 message = None user_uid = None username = None public_key = None public_cert = None logout_timestamp = None login_status = None try: user_uid = args["user_uid"] except: pass try: username = args["username"] except: pass try: session_uid = args["session_uid"] except: session_uid = None bucket = None user_account = None if user_uid is None and username is None: raise WhoisLookupError( "You must supply either a username or user_uid to look up...") elif user_uid is None: # look up the user_uid from the username user_account = UserAccount(username) bucket = login_to_service_account() user_key = "accounts/%s" % user_account.sanitised_name() try: user_account = UserAccount.from_data( ObjectStore.get_object_from_json(bucket, user_key)) except: raise WhoisLookupError("Cannot find an account for name '%s'" % username) user_uid = user_account.uid() elif username is None: # look up the username from the uuid bucket = login_to_service_account() uid_key = "whois/%s" % user_uid try: username = ObjectStore.get_string_object(bucket, uid_key) except: raise WhoisLookupError("Cannot find an account for user_uid '%s'" % user_uid) else: raise WhoisLookupError("You must only supply one of the username " "or user_uid to look up - not both!") if session_uid: # now look up the public signing key for this session, if it is # a valid login session if user_account is None: user_account = UserAccount(username) user_session_key = "sessions/%s/%s" % \ (user_account.sanitised_name(), session_uid) try: login_session = LoginSession.from_data( ObjectStore.get_object_from_json(bucket, user_session_key)) except: login_session = None if login_session is None: user_session_key = "expired_sessions/%s/%s" % \ (user_account.sanitised_name(), session_uid) login_session = LoginSession.from_data( ObjectStore.get_object_from_json(bucket, user_session_key)) if login_session is None: raise InvalidSessionError("Cannot find the session '%s'" % session_uid) if login_session.is_approved(): public_key = login_session.public_key() public_cert = login_session.public_certificate() elif login_session.is_logged_out(): public_cert = login_session.public_certificate() logout_timestamp = login_session.logout_time().timestamp() else: raise InvalidSessionError("You cannot get the keys for a session " "for which the user has not logged in!") login_status = login_session.status() status = 0 message = "Success" return_value = create_return_value(status, message) if user_uid: return_value["user_uid"] = str(user_uid) if username: return_value["username"] = str(username) if public_key: return_value["public_key"] = public_key.to_data() if public_cert: return_value["public_cert"] = public_cert.to_data() if logout_timestamp: return_value["logout_timestamp"] = logout_timestamp if login_status: return_value["login_status"] = str(login_status) 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 assert_once(self, stale_time=7200, scope=None, permissions=None): """Assert that this is in the one and only time that this service has seen this authorisation. This records the UID of the authorisation to the object store and then verifies that the signature of the UID is correct. There is a small race condition if the service asserts the authorisation at the exact same time, but this is a highly unlikely occurance. The aim is to prevent replay attacks. """ if self.is_null(): raise PermissionError("Cannot assert_once a null Authorisation") if self.is_stale(stale_time): if now < self._auth_datetime: raise PermissionError( "Cannot assert_once an Authorisation signed " "in the future - please check your clock") else: raise PermissionError( "Cannot assert_once a stale Authorisation") from Acquire.ObjectStore import ObjectStore as _ObjectStore from Acquire.Service import get_service_account_bucket \ as _get_service_account_bucket from Acquire.ObjectStore import get_datetime_now_to_string \ as _get_datetime_now_to_string bucket = _get_service_account_bucket() authkey = "auth_once/%s" % self._uid now = _get_datetime_now_to_string() try: data = _ObjectStore.get_string_object(bucket=bucket, key=authkey) except: data = None if data is not None: raise PermissionError( "Cannot auth_once the authorisation as it has been used " "before on this service!") # This is the first time this authorisation has been seen. # Record this to the object store to prevent anyone else # from using this authorisation on this service. There is a # small race condition here, but this would be extremely # challenging to exploit, and mitigating it would be a # significant performance problem. Ideally the object store # would have a "test_and_set" to enable us to set only if # the previous value is None _ObjectStore.set_string_object(bucket=bucket, key=authkey, string_data=now) # Now validate that the signature of the UID is correct public_cert = self._get_user_public_cert(scope=scope, permissions=permissions) if public_cert is None: raise PermissionError( "There is no public certificate for this user in " "scope '%s' with permissions '%s'" % (scope, permissions)) try: public_cert.verify(self._siguid, self._uid) except Exception as e: raise PermissionError( "Cannot auth_once the authorisation as the signature " "is invalid! % s" % str(e))
def lock(self, timeout=None, lease_time=None): """Lock the mutex, blocking until the mutex is held, or until 'timeout' seconds have passed. If we time out, then an exception is raised. The lock is held for a maximum of 'lease_time' seconds. Args: timeout (int): Number of seconds to block lease_time (int): Number of seconds to hold the lock Returns: None """ # if the user does not provide a timeout, then we will set a timeout # to 10 seconds if timeout is None: timeout = 10.0 else: timeout = float(timeout) # if the user does not provide a lease_time, then we will set a # default of only 10 seconds if lease_time is None: lease_time = 10.0 else: lease_time = float(lease_time) from Acquire.ObjectStore import get_datetime_now as _get_datetime_now from Acquire.ObjectStore import datetime_to_string \ as _datetime_to_string from Acquire.ObjectStore import string_to_datetime \ as _string_to_datetime from Acquire.ObjectStore import ObjectStore as _ObjectStore if self.is_locked(): # renew the lease - if there is less than a second remaining # on the lease then unlock and then lock again from scratch now = _get_datetime_now() if (now > self._end_lease) or (now - self._end_lease).seconds < 1: self.fully_unlock() self.lock(timeout, lease_time) else: self._end_lease = now + _datetime.timedelta(seconds=lease_time) self._lockstring = "%s{}%s" % ( self._secret, _datetime_to_string(self._end_lease)) _ObjectStore.set_string_object(self._bucket, self._key, self._lockstring) self._is_locked += 1 return now = _get_datetime_now() endtime = now + _datetime.timedelta(seconds=timeout) # This is the first time we are trying to get a lock while now < endtime: # does anyone else hold the lock? try: holder = _ObjectStore.get_string_object( self._bucket, self._key) except: holder = None is_held = True if holder is None: is_held = False else: end_lease = _string_to_datetime(holder.split("{}")[-1]) if now > end_lease: # the lease from the other holder has expired :-) is_held = False if not is_held: # no-one holds this mutex - try to hold it now self._end_lease = now + _datetime.timedelta(seconds=lease_time) self._lockstring = "%s{}%s" % ( self._secret, _datetime_to_string(self._end_lease)) _ObjectStore.set_string_object(self._bucket, self._key, self._lockstring) holder = _ObjectStore.get_string_object( self._bucket, self._key) else: self._lockstring = None if holder == self._lockstring: # it looks like we are the holder - read and write again # just to make sure holder = _ObjectStore.get_string_object( self._bucket, self._key) if holder == self._lockstring: # write again just to make sure _ObjectStore.set_string_object(self._bucket, self._key, self._lockstring) holder = _ObjectStore.get_string_object( self._bucket, self._key) if holder == self._lockstring: # we have read and written our secret to the object store # three times. While a race condition is still possible, # I'd hope it is now highly unlikely - we now hold the mutex self._is_locked = 1 return # only try the lock 4 times a second _time.sleep(0.25) now = _get_datetime_now() from Acquire.ObjectStore import MutexTimeoutError raise MutexTimeoutError("Cannot acquire a mutex lock on the " "key '%s'" % self._key)
def get_drive(self, name, aclrules=None, autocreate=True): """Return the DriveMeta for the Drive that the user has called 'name'. If 'autocreate' is True then this drive is automatically created if it does not exist. Note that the '/' in the name will be interpreted as drive separators. """ if self.is_null(): raise PermissionError( "You cannot get a DriveInfo from a null UserDrives") # break the name into directory parts from Acquire.ObjectStore import string_to_filepath_parts \ as _string_to_filepath_parts parts = _string_to_filepath_parts(name) # first get the root drive... root_name = parts[0] from Acquire.Service import get_service_account_bucket \ as _get_service_account_bucket from Acquire.ObjectStore import ObjectStore as _ObjectStore from Acquire.ObjectStore import string_to_encoded as _string_to_encoded encoded_name = _string_to_encoded(root_name) drive_name = root_name bucket = _get_service_account_bucket() drive_key = "%s/%s/%s" % (_drives_root, self._user_guid, encoded_name) try: drive_uid = _ObjectStore.get_string_object(bucket, drive_key) except: drive_uid = None if drive_uid is not None: from Acquire.Storage import DriveInfo as _DriveInfo drive = _DriveInfo(drive_uid=drive_uid, is_authorised=self._is_authorised, identifiers=self._identifiers) else: drive = None if drive is None: if self._is_authorised and autocreate: # create a new UID for the drive and write this to the # object store from Acquire.ObjectStore import create_uid as _create_uid drive_uid = _create_uid() drive_uid = _ObjectStore.set_ins_string_object( bucket, drive_key, drive_uid) from Acquire.Storage import DriveInfo as _DriveInfo drive = _DriveInfo(drive_uid=drive_uid, identifiers=self._identifiers, is_authorised=self._is_authorised, aclrules=aclrules, autocreate=True) if drive is None: from Acquire.Storage import MissingDriveError raise MissingDriveError("There is no Drive called '%s' available" % name) container = [] # now we have the drive, get the sub-drive in this drive... if len(parts) > 1: for subdrive in parts[1:]: container.append(drive.uid()) drive_name = subdrive drive = self._get_subdrive(drive_uid=drive.uid(), name=drive_name, autocreate=autocreate) if drive is None: from Acquire.Storage import MissingDriveError raise MissingDriveError( "There is no Drive called '%s' available" % name) from Acquire.Storage import DriveMeta as _DriveMeta drivemeta = _DriveMeta(name=drive_name, uid=drive.uid(), container=container, aclrules=drive.aclrules()) drivemeta.resolve_acl(identifiers=self._identifiers) return drivemeta
def test_par(bucket): privkey = get_private_key() pubkey = privkey.public_key() # first try to create a PAR for the whole bucket par = ObjectStore.create_par(bucket, readable=False, writeable=True, duration=100, encrypt_key=pubkey) # should not take 10 seconds to create and return the PAR... assert(par.seconds_remaining(buffer=0) > 90) assert(par.seconds_remaining(buffer=0) < 101) # trying to create a par for a non-existant object should fail key = "something" value = "∆ƒ^ø ®∆ ®∆ #®∆… ®#€ €" with pytest.raises(PARError): par = ObjectStore.create_par(bucket, key=key, encrypt_key=pubkey) ObjectStore.set_string_object(bucket, key, value) par = ObjectStore.create_par(bucket, key=key, duration=60, encrypt_key=pubkey) assert(par.seconds_remaining(buffer=0) > 55) assert(par.seconds_remaining(buffer=0) < 61) assert(not par.is_writeable()) assert(par.key() == key) val = par.read(privkey).get_string_object() assert(val == value) value = "∆˚¬# #ª ƒ∆ ¬¬¬˚¬∂ß ˚¬ ¬¬¬ßßß" with pytest.raises(PARPermissionsError): par.write(privkey).set_string_object(value) # close the PAR and then assert a closed PAR is null par.close() assert(par.is_null()) par = ObjectStore.create_par(bucket, key=key, readable=True, writeable=True, encrypt_key=pubkey) data = par.to_data() par2 = OSPar.from_data(data) value = "something " + str(uuid.uuid4()) par2.write(privkey).set_string_object(value) val = par.read(privkey).get_string_object() assert(val == value) par = ObjectStore.create_par(bucket, encrypt_key=pubkey, key=key, writeable=True, duration=60) par.write(privkey).set_string_object(value) assert(par.read(privkey).get_string_object() == value) assert(ObjectStore.get_string_object(bucket, key) == value) par = ObjectStore.create_par(bucket, readable=False, writeable=True, duration=120, encrypt_key=pubkey) assert(not par.is_readable()) assert(par.is_writeable()) assert(par.is_bucket()) d = "testing" keyvals = {"one": "^¬#∆˚¬€", "two": "∆¡πª¨ƒ∆", "three": "€√≠ç~ç~€", "four": "hello world!", "subdir/five": "#º©√∆˚∆˚¬€ €˚∆ƒ¬"} for (key, value) in keyvals.items(): par.write(privkey).set_string_object("%s/%s" % (d, key), value) for key in keyvals.keys(): par = ObjectStore.create_par(bucket, key="%s/%s" % (d, key), duration=60, encrypt_key=pubkey) value = par.read(privkey).get_string_object() assert(keyvals[key] == value)
def _get_subdrive(self, drive_uid, name, autocreate=True): """Return the DriveInfo for the Drive that the user has called 'name' in the drive with UID 'drive_uid'. If 'autocreate' is True then this drive is automatically created if it does not exist. """ if self.is_null(): raise PermissionError( "You cannot get a DriveInfo from a null UserDrives") from Acquire.ObjectStore import string_to_filepath_parts \ as _string_to_filepath_parts parts = _string_to_filepath_parts(name) if len(parts) != 1: raise ValueError("The passed drive name '%s' is not valid!" % name) from Acquire.Service import get_service_account_bucket \ as _get_service_account_bucket from Acquire.ObjectStore import ObjectStore as _ObjectStore from Acquire.ObjectStore import string_to_encoded as _string_to_encoded encoded_name = _string_to_encoded(name) bucket = _get_service_account_bucket() drive_key = "%s/%s/%s/%s" % (_subdrives_root, self._user_guid, drive_uid, encoded_name) try: drive_uid = _ObjectStore.get_string_object(bucket, drive_key) except: drive_uid = None if drive_uid is not None: from Acquire.Storage import DriveInfo as _DriveInfo drive = _DriveInfo(drive_uid=drive_uid, identifiers=self._identifiers, is_authorised=self._is_authorised) else: drive = None if drive is None: if self._is_authorised and autocreate: # create a new UID for the drive and write this to the # object store from Acquire.ObjectStore import create_uuid as _create_uuid drive_uid = _create_uuid() drive_uid = _ObjectStore.set_ins_string_object( bucket, drive_key, drive_uid) from Acquire.Storage import DriveInfo as _DriveInfo drive = _DriveInfo(drive_uid=drive_uid, identifiers=self._identifiers, is_authorised=self._is_authorised, autocreate=True) return drive