def run(args): """This function is used to request access to a bucket for data in the object store. The user can request read-only or read-write access. Access is granted based on a permission list """ status = 0 message = None access_token = None user_uuid = args["user_uuid"] identity_service_url = args["identity_service"] # log into the central access account bucket = login_to_service_account() # is the identity service supplied by the user one that we trust? identity_service = Service.from_data( ObjectStore.get_object_from_json(bucket, "services/%s" % identity_service_url)) if not identity_service: raise RequestBucketError( "You cannot request a bucket because " "this access service does not know or trust your supplied " "identity service (%s)" % identity_service_url) if not identity_service.is_identity_service(): raise RequestBucketError( "You cannot request a bucket because " "the passed service (%s) is not an identity service. It is " "a %s" % (identity_service_url, identity_service.service_type())) # Since we trust this identity service, we can ask it to give us the # public certificate and signing certificate for this user. key = PrivateKey() response = call_function(identity_service_url, "get_user_keys", args_key=identity_service.public_key(), response_key=key, user_uuid=user_uuid) status = 0 message = "Success: Status = %s" % str(response) return_value = create_return_value(status, message) if access_token: return_value["access_token"] = access_token return return_value
def run(args): """This function is called to handle requests for information about particular accounts """ status = 0 message = None account = None balance_status = None try: account_name = str(args["account_name"]) except: account_name = None try: authorisation = Authorisation.from_data(args["authorisation"]) except: authorisation = None if account_name is None: raise AccountError("You must supply the account_name") if authorisation is None: raise AccountError("You must supply a valid authorisation") # load the account bucket = login_to_service_account() account = Accounts(authorisation.user_uid()).get_account(account_name, bucket=bucket) # validate the authorisation for this account authorisation.verify(resource=account.uid()) balance_status = account.balance_status(bucket=bucket) status = 0 message = "Success" return_value = create_return_value(status, message) if account: return_value["description"] = account.description() return_value["overdraft_limit"] = str(account.get_overdraft_limit()) if balance_status: for key in balance_status.keys(): return_value[key] = str(balance_status[key]) return return_value
def run(args): """This function will allow anyone to query the current login status of the session with passed UID""" status = 0 message = None session_status = None session_uid = args["session_uid"] username = args["username"] # 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) 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) status = 0 message = "Success: Status = %s" % login_session.status() session_status = login_session.status() return_value = create_return_value(status, message) if session_status: return_value["session_status"] = session_status return return_value
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 run(args): """This function is called to handle admin setup of the service, e.g. setting admin passwords, introducing trusted services etc. """ status = 0 message = None provisioning_uri = None try: password = args["password"] except: password = None try: otpcode = args["otpcode"] except: otpcode = None try: new_service = args["new_service"] except: new_service = None try: remove_service = args["remove_service"] except: remove_service = None try: new_password = args["new_password"] except: new_password = None try: remember_device = args["remember_device"] except: remember_device = False # first, do we have an existing Service object? If not, # we grant access to the first user! bucket = login_to_service_account() # The data is stored in the object store at the key _service_info # and is encrypted using the value of $SERVICE_PASSWORD try: service = get_service_info(True) except MissingServiceAccountError: service = None if service: if not service.is_accounting_service(): raise ServiceSetupError( "Why is the accounting service info " "for a service of type %s" % service.service_type()) provisioning_uri = service.verify_admin_user(password, otpcode, remember_device) else: # we need to create the service service_url = args["service_url"] service_type = "accounting" service = Service(service_type, service_url) provisioning_uri = service.set_admin_password(password) # write the service data, encrypted using the service password service_password = os.getenv("SERVICE_PASSWORD") if service_password is None: raise ServiceSetupError( "You must supply $SERVICE_PASSWORD " "to setup a new service!") service_data = service.to_data(service_password) ObjectStore.set_object_from_json(bucket, "_service_info", service_data) # we are definitely the admin user, so let's add or remove remote services if remove_service: remove_trusted_service_info(remove_service) if new_service: service = get_remote_service_info(new_service) if new_service: set_trusted_service_info(new_service, service) status = 0 message = "Success" return_value = create_return_value(status, message) if provisioning_uri: return_value["provisioning_uri"] = provisioning_uri return return_value
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 bucket(tmpdir_factory): try: return login_to_service_account() except: d = tmpdir_factory.mktemp("objstore") return login_to_service_account(str(d))
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 bucket(tmpdir_factory): d = tmpdir_factory.mktemp("simple_objstore") return login_to_service_account(str(d))
def run(args): """This function will allow anyone to obtain the public keys for the passed login session of a user with a specified login UID""" public_key = None public_cert = None login_status = None logout_timestamp = None session_uid = args["session_uid"] username = args["username"] # 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) 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) # only send valid keys if the user had logged in! 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: Status = %s" % login_session.status() return_value = create_return_value(status, message) if public_key: return_value["public_key"] = public_key.to_data() if public_cert: return_value["public_cert"] = public_cert.to_data() if login_status: return_value["login_status"] = str(login_status) if logout_timestamp: return_value["logout_timestamp"] = logout_timestamp return return_value
def run(args): """This function is called to handle requests from a user to deposit more funds into their account. This will add this deposit as a debt for the user. Once the debt exceeds a certain value, then the backend-payment system will charge the user's real account to recover the funds """ status = 0 message = None transaction_records = None invoice_value = None invoice_user = None try: authorisation = Authorisation.from_data(args["authorisation"]) except: authorisation = None try: transaction = Transaction.from_data(args["transaction"]) except: transaction = Transaction(args["value"], "Deposit on %s" % datetime.datetime.now()) if authorisation is None: raise PermissionError("You must supply a valid authorisation " "to deposit funds into your account") if transaction is None or transaction.is_null(): raise ValueError("You must supply a valid transaction that " "represents the deposit") if transaction.value() > 0: authorisation.verify() user_uid = authorisation.user_uid() # load the account from which the transaction will be performed bucket = login_to_service_account() accounts = Accounts(user_uid) # deposits are made by transferring funds from the user's # 'billing' account to their 'deposits' account. deposit_account = accounts.create_account("deposits", "Deposit account", bucket=bucket) billing_account = accounts.create_account("billing", "Billing account", overdraft_limit=150, bucket=bucket) billing_balance = billing_account.balance() - transaction.value() if billing_balance < -50.0: # there are sufficient funds that need to be transferred that # it is worth really charging the user invoice_user = user_uid invoice_value = billing_balance # we have enough information to perform the transaction transaction_records = Ledger.perform(transactions=transaction, debit_account=billing_account, credit_account=deposit_account, authorisation=authorisation, is_provisional=False, bucket=bucket) status = 0 message = "Success" return_value = create_return_value(status, message) if transaction_records: try: transaction_records[0] except: transaction_records = [transaction_records] for i in range(0, len(transaction_records)): transaction_records[i] = transaction_records[i].to_data() return_value["transaction_records"] = transaction_records if invoice_user: return_value["invoice_user"] = invoice_user return_value["invoice_value"] = str(invoice_value) return return_value
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 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