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 save_service_keys_to_objstore(include_old_keys=False): """Call this function to ensure that the current set of keys used for this service are saved to object store """ service = get_this_service(need_private_access=True) oldkeys = service.dump_keys(include_old_keys=include_old_keys) # now write the old keys to storage 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 = "%s/oldkeys/%s" % (_service_key, oldkeys["datetime"]) _ObjectStore.set_object_from_json(bucket, key, oldkeys) # now write the pointers from fingerprint to file... for fingerprint in oldkeys.keys(): if fingerprint not in ["datetime", "encrypted_passphrase"]: _ObjectStore.set_string_object( bucket, "%s/oldkeys/fingerprints/%s" % (_service_key, fingerprint), key)
def trust_service(service): """Trust the passed service. This will record this service as trusted, e.g. saving the keys and certificates for this service and allowing it to be used for the specified type. """ 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() service_data = service.to_data() # store the trusted service by both canonical_url and uid from Acquire.ObjectStore import ObjectStore as _ObjectStore _ObjectStore.set_object_from_json(bucket, uidkey, service_data) _ObjectStore.set_string_object(bucket, urlkey, uidkey) 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.add_service(service)
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 _refresh_this_service_keys_and_certs(service_info, service_password): from Acquire.Service import Service as _Service service = _Service.from_data(service_info, service_password) if service._uid == "STAGE1": return service_info if not service.should_refresh_keys(): return service_info oldkeys = service.dump_keys(include_old_keys=False) # now write the old keys to storage from Acquire.ObjectStore import ObjectStore as _ObjectStore from Acquire.ObjectStore import Mutex as _Mutex from Acquire.Service import get_service_account_bucket as \ _get_service_account_bucket bucket = _get_service_account_bucket() key = "%s/oldkeys/%s" % (_service_key, oldkeys["datetime"]) _ObjectStore.set_object_from_json(bucket, key, oldkeys) # now write the pointers from fingerprint to file... for fingerprint in oldkeys.keys(): if fingerprint not in ["datetime", "encrypted_passphrase"]: _ObjectStore.set_string_object( bucket, "%s/oldkeys/fingerprints/%s" % (_service_key, fingerprint), key) # generate new keys last_update = service.last_key_update() service.refresh_keys() # now lock the object store so that we are the only function # that can write the new keys to global state m = _Mutex(key=service.uid(), bucket=bucket) service_data = _ObjectStore.get_object_from_json(bucket, _service_key) service_info = _Service.from_data(service_data) if service_info.last_key_update() == last_update: # no-one else has beaten us - write the updated keys to global state _ObjectStore.set_object_from_json(bucket, _service_key, service.to_data(service_password)) m.unlock() return service_data
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 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 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 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 create(username, password, _service_uid=None, _service_public_key=None): """Create a new account with username 'username', which will be secured using the passed password. Note that this will create an account with a specified user UID, meaning that different users can have the same username. We identify the right user via the combination of username, password and OTP code. Normally the UID of the service, and the skeleton key used to encrypt the backup password are obtained directly from the service. However, when initialising a new service we must pass these directly. In those cases, pass the object using _service_uid and _service_public_key This returns a tuple of the user_uid and OTP for the newly-created account """ from Acquire.ObjectStore import create_uuid as _create_uuid from Acquire.Crypto import PrivateKey as _PrivateKey from Acquire.Crypto import PublicKey as _PublicKey from Acquire.ObjectStore import ObjectStore as _ObjectStore from Acquire.Service import get_service_account_bucket \ as _get_service_account_bucket from Acquire.ObjectStore import bytes_to_string as _bytes_to_string from Acquire.Identity import UserCredentials as _UserCredentials from Acquire.ObjectStore import get_datetime_now_to_string \ as _get_datetime_now_to_string if _service_public_key is None: from Acquire.Service import get_this_service as _get_this_service service_pubkey = _get_this_service().public_skeleton_key() assert (service_pubkey is not None) else: service_pubkey = _service_public_key if not isinstance(service_pubkey, _PublicKey): raise TypeError("The service public key must be type PublicKey") if _service_uid is None: from Acquire.Service import get_this_service \ as _get_this_service service_uid = _get_this_service(need_private_access=False).uid() else: service_uid = _service_uid # create a UID for this new user user_uid = _create_uuid() # now create the primary password for this user and use # this to encrypt the special keys for this user privkey = _PrivateKey(name="user_secret_key %s %s" % (username, user_uid)) primary_password = _PrivateKey.random_passphrase() bucket = _get_service_account_bucket() # now create the credentials used to validate a login otp = _UserCredentials.create(user_uid=user_uid, password=password, primary_password=primary_password) # create the user account user = UserAccount(username=username, user_uid=user_uid, private_key=privkey, status="active") # now save a lookup from the username to this user_uid # (many users can have the same username). Use this lookup # to hold a recovery password for this account recovery_password = _bytes_to_string( service_pubkey.encrypt(primary_password)) key = "%s/names/%s/%s" % (_user_root, user.encoded_name(), user_uid) _ObjectStore.set_string_object(bucket=bucket, key=key, string_data=recovery_password) # now save a lookup from the hashed username+password # to the user_uid, so that we can # quickly find matching user_uids (expect few people will have # exactly the same username and password). This will # save the exact time this username-password combination # was set encoded_password = _UserCredentials.hash(username=username, password=password, service_uid=service_uid) key = "%s/passwords/%s/%s" % (_user_root, encoded_password, user_uid) _ObjectStore.set_string_object( bucket=bucket, key=key, string_data=_get_datetime_now_to_string()) # finally(!) save the account itself to the object store key = "%s/uids/%s" % (_user_root, user_uid) data = user.to_data(passphrase=primary_password) _ObjectStore.set_object_from_json(bucket=bucket, key=key, data=data) # return the OTP and user_uid return (user_uid, otp)
def run(args): """This function will allow a user to register an account with a username and password""" status = 0 message = None provisioning_uri = None username = args["username"] password = args["password"] # generate a sanitised version of the username user_account = UserAccount(username) # generate the encryption keys and otp secret privkey = PrivateKey() pubkey = privkey.public_key() otp = OTP() provisioning_uri = otp.provisioning_uri(username) # save the encrypted private key (encrypted using the user's password) # and encrypted OTP secret (encrypted using the public key) user_account.set_keys(privkey.bytes(password), pubkey.bytes(), otp.encrypt(pubkey)) # remove the key and password from memory privkey = None password = None # now log into the central identity account to either register # the user, or to update to a new password bucket = login_to_service_account() account_key = "accounts/%s" % user_account.sanitised_name() try: existing_data = ObjectStore.get_object_from_json(bucket, account_key) except: existing_data = None message = "Created a new account for '%s'" % username status = 0 if existing_data is None: # save the new account details ObjectStore.set_object_from_json(bucket, account_key, user_account.to_data()) # need to update the "whois" database with the uuid of this user ObjectStore.set_string_object(bucket, "whois/%s" % user_account.uuid(), user_account.username()) else: # The account already exists. See if this is a password change # request old_password = None try: old_password = args["old_password"] except: raise ExistingAccountError( "An account by this name already exists!") if old_password != password: # this is a change of password request - validate that # the existing password unlocks the existing key user_account = UserAccount.from_data(existing_data) testkey = PrivateKey.read_bytes(user_account.private_key(), old_password) # decrypt the old secret old_secret = testkey.decrypt(user_account.otp_secret()) # now encrypt the secret with the new key new_key = PublicKey.read_bytes(pubkey) new_secret = new_key.encrypt(old_secret) user_account.set_keys(privkey, pubkey, new_secret) # save the new account details ObjectStore.set_object_from_json(bucket, account_key, user_account.to_data()) message = "Updated the password for '%s'" % username else: message = "No need to change account '%s'" % username return_value = create_return_value(status, message) if provisioning_uri: return_value["provisioning_uri"] = provisioning_uri return return_value
def validate_password(user_uid, username, device_uid, secrets, password, otpcode, remember_device): """Validate that the passed password and one-time-code are valid. If they are, then return a tuple of the UserAccount of the unlocked user, the OTP that is used to generate secrets, and the device_uid of the login device If 'remember_device' is True and 'device_uid' is None, then this creates a new OTP for the login device, which is returned, and a new device_uid for that device. The password needed to match this device is a MD5 of the normal user password. """ from Acquire.Crypto import PrivateKey as _PrivateKey from Acquire.Crypto import OTP as _OTP from Acquire.ObjectStore import string_to_bytes as _string_to_bytes privkey = _PrivateKey.from_data(data=secrets["private_key"], passphrase=password) # decrypt and validate the OTP code data = _string_to_bytes(secrets["otpsecret"]) otpsecret = privkey.decrypt(data) otp = _OTP(secret=otpsecret) otp.verify(code=otpcode, once_only=True) # everything is ok - we can load the user account via the # decrypted primary password primary_password = _string_to_bytes(secrets["primary_password"]) primary_password = privkey.decrypt(primary_password) from Acquire.ObjectStore import ObjectStore as _ObjectStore from Acquire.Service import get_service_account_bucket \ as _get_service_account_bucket data = None secrets = None key = "%s/uids/%s" % (_user_root, user_uid) bucket = _get_service_account_bucket() try: data = _ObjectStore.get_object_from_json(bucket=bucket, key=key) except: pass if data is None: from Acquire.Identity import UserValidationError raise UserValidationError( "Unable to validate user as no account data is present!") from Acquire.Identity import UserAccount as _UserAccount user = _UserAccount.from_data(data=data, passphrase=primary_password) if user.uid() != user_uid: from Acquire.Identity import UserValidationError raise UserValidationError( "Unable to validate user as mismatch in user_uids!") if device_uid is None and remember_device: # create a new OTP that is unique for this device from Acquire.ObjectStore import create_uuid as _create_uuid from Acquire.Client import Credentials as _Credentials device_uid = _create_uuid() device_password = _Credentials.encode_device_uid( encoded_password=password, device_uid=device_uid) otp = UserCredentials.create(user_uid=user_uid, password=device_password, primary_password=primary_password, device_uid=device_uid) # now save a lookup so that we can find the user_uid from # the username and device-specific password encoded_password = UserCredentials.hash( username=username, password=device_password) key = "%s/passwords/%s/%s" % (_user_root, encoded_password, user_uid) from Acquire.ObjectStore import get_datetime_now_to_string \ as _get_datetime_now_to_string _ObjectStore.set_string_object( bucket=bucket, key=key, string_data=_get_datetime_now_to_string()) return {"user": user, "otp": otp, "device_uid": device_uid}
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 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 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 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 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 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 a user to request a new session that will be validated by the passed public key and public signing certificate. This will return a URL that the user must connect to to then log in and validate that request. """ status = 0 message = None login_url = None login_uid = None user_uid = None username = args["username"] public_key = PublicKey.from_data(args["public_key"]) public_cert = PublicKey.from_data(args["public_certificate"]) ip_addr = None hostname = None login_message = None try: ip_addr = args["ipaddr"] except: pass try: hostname = args["hostname"] except: pass try: login_message = args["message"] except: pass # generate a sanitised version of the username user_account = UserAccount(username) # Now generate a login session for this request login_session = LoginSession(public_key, public_cert, ip_addr, hostname, login_message) # now log into the central identity account to record # that a request to open a login session has been opened bucket = login_to_service_account() # first, make sure that the user exists... account_key = "accounts/%s" % user_account.sanitised_name() try: existing_data = ObjectStore.get_object_from_json(bucket, account_key) except: existing_data = None if existing_data is None: raise InvalidLoginError("There is no user with name '%s'" % username) user_account = UserAccount.from_data(existing_data) user_uid = user_account.uid() # first, make sure that the user doens't have too many open # login sessions at once - this prevents denial of service user_session_root = "sessions/%s" % user_account.sanitised_name() open_sessions = ObjectStore.get_all_object_names(bucket, user_session_root) # take the opportunity to prune old user login sessions prune_expired_sessions(bucket, user_account, user_session_root, open_sessions) # this is the key for the session in the object store user_session_key = "%s/%s" % (user_session_root, login_session.uuid()) ObjectStore.set_object_from_json(bucket, user_session_key, login_session.to_data()) # we will record a pointer to the request using the short # UUID. This way we can give a simple URL. If there is a clash, # then we will use the username provided at login to find the # correct request from a much smaller pool (likely < 3) request_key = "requests/%s/%s" % (login_session.short_uuid(), login_session.uuid()) ObjectStore.set_string_object(bucket, request_key, user_account.name()) status = 0 # the login URL is the URL of this identity service plus the # short UID of the session login_url = "%s/s?id=%s" % (get_service_info().service_url(), login_session.short_uuid()) login_uid = login_session.uuid() message = "Success: Login via %s" % login_url return_value = create_return_value(status, message) if login_uid: return_value["session_uid"] = login_uid if login_url: return_value["login_url"] = login_url else: return_value["login_url"] = None if user_uid: return_value["user_uid"] = user_uid return return_value