Issues with customer features

With this version of the features, we have roughed in some useful banking features. However, there are some things that still need to be handled.

  • There is not enough security preventing another customer from accessing Alice's accounts and transferring money away.
  • Withdrawing money from an account has no overdraft protection. This also impacts transfers, because they reuse the withdraw function. A simple solution would be to fail if the requested amount exceeds the balance.
  • Transferring money should be transactional to avoid leaking money.

Securing Alice's accounts

If we log in as Alice, we can view the history of the Checking account.

Securing Alice's accounts

That is alright. However, if there is another customer, then things are not as secured as you might think. Let's adjust the context so that another customer, "Dave" exists.

@Object
def user_details_service(self):
user_details_service = InMemoryUserDetailsService()
user_details_service.user_dict = {
"alice": ("alicespassword",["ROLE_CUSTOMER"], True),
"bob": ("bobspassword", ["ROLE_MGR"], True),
"carol": ("carolspassword", ["ROLE_SUPERVISOR"], True),
"dave": ("davespassword",["ROLE_CUSTOMER"], True)
}
return user_details_service

After looking at Alice's account history, we can easily copy the URL from the browser to clipboard, logout, and then log back in as Dave.

Securing Alice's accounts

Dave has no accounts yet. However, Dave can paste in the URL and easily view Alice's account history.

Securing Alice's accounts

This is because both of these users have ROLE_CUSTOMER. This means we need to write a customized AccessDecisionVoter that will decide whether or not a certain record can be viewed by the current user.

The security chapter showed us how to code a custom authenticator. In this situation, the users are already authenticated. What we need is proper handling of authorization. Our problem requires confirming that the currently logged in user has permission to look at the current account.

  1. Let's code our own AccessDecisionVoter.
    class OwnerVoter(AccessDecisionVoter):
    def __init__(self, controller=None):
    self.controller = controller
    def supports(self, attr):
    """This voter will support a list."""
    if isinstance(attr, list) or 
    (attr is not None and attr == "OWNER"):
    return True
    else:
    return False
    def vote(self, authentication, invocation, config):
    """Grant access if any of the granted
    authorities matches any of the required roles.
    """
    results = self.ACCESS_ABSTAIN
    for attribute in config:
    if self.supports(attribute):
    results = self.ACCESS_DENIED
    id = cgi.parse_qs(
    invocation.environ["QUERY_STRING"])["id"][0]
    if self.controller.get_username(id) == 
    authentication.username:
    return self.ACCESS_GRANTED
    return results
    
    • OwnerVoter needs a supports method to determine if it is going to vote. In this case, it will if given security configuration in the form of a list, and also if one of the list values is OWNER.
    • In order to vote, the voter is provided with a copy of the user's authentication credentials, and a handle on the invocation object which includes all the web request parameters, and what the security role configuration is.
    • The voter initializes its results to ACCESS_ABSTAIN. It iterates over the list of roles, and checks if any of them match OWNER. If so, it then changes the results to ACCESS_DENIED. Then, it checks if the there are any id parameters, and if so, retrieves the value. It requests that the controller lookup the user associated with the account id and if it matches the current user's username, it updates the results to ACCESS_GRANTED.
  2. Add a method to the controller to look up the username of an account.
    def get_username(self, id):
    return self.dt.query_for_object("""
    SELECT owner
    FROM account
    WHERE id = ?
    AND status = 'OPEN'""", (id,), str)
    
  3. Now we need to plug the OwnerVoter into the context's AccessDecisionManager configuration.
@Object
def access_decision_mgr(self):
access_decision_mgr = AffirmativeBased()
access_decision_mgr.allow_if_all_abstain = False
access_decision_mgr.access_decision_voters = [RoleVoter(),
OwnerVoter(self.controller())]
return access_decision_mgr

As you can see, we just added an instance of OwnerVoter to the access_decision_voters list. OwnerVoter needs a handle on the controller so it can query for the username linked to the account.

  1. Update the filter_security_interceptor so that history requests are routed through the OwnerVoter.
@Object
def filter_security_interceptor(self):
filter = FilterSecurityInterceptor()
filter.auth_manager = self.auth_manager()
filter.access_decision_mgr = self.access_decision_mgr()
filter.sessionStrategy = self.session_strategy()
filter.obj_def_source = [
("/history.*", ["OWNER"]),
("/.*", ["ROLE_CUSTOMER", "ROLE_MGR", "ROLE_SUPERVISOR"])
]
return filter

Notice how we added a rule for /history.*. Because OWNER doesn't start with ROLE_, the RoleVoter will not vote on it. Only the OwnerVoter will examine this rule to determine if the current user is authorized to access.

Because the interceptor starts with the fi rst rule and iterates until it finds a match, it is important to put specialized rules towards the top, with more general ones at the bottom.

  1. Add an accessDenied page at the view level, so that when Dave tries to access the history of Alice's account, he is redirected due to lack of ownership.
@cherrypy.expose
def accessDenied(self):
return """
<h2>You are trying to access
an un-authorized page.</h2>
"""
Securing Alice's accounts

We have now secured pages based on ownership. This way, only the account holder can view his or her own history. It is left as an exercise to secure other pages that are id-based using the same OwnerVoter.

Adding overdraft protection to withdrawals

One of the simplest business rules is to not allow people to draw more from an account than exists.

  1. Modify the withdraw operation to first retrieve the account's balance and check it against the amount, throwing an exception if there are insufficient funds.
def withdraw(self, id, amount):
balance = self.dt.query("""
SELECT balance
FROM account
WHERE id = ?""", (id,),
DictionaryRowMapper())[0]["balance"]
if float(balance) < amount:
raise BankException("Insufficient balance in acct %s" % id)
self.dt.execute("""
UPDATE account
SET balance = balance - ?
WHERE id = ?""", (amount, id))
self.log("TX", id, "Withdrew %s" % amount)
  1. Define a simple BankException class.
class BankException(Exception):
pass
  1. Modify the beginning of the withdraw web method by adding a try-except block that either redirects with a success message or an error message.
@cherrypy.expose
def withdraw(self, id="", amount=""):
if id != "" and amount != "":
try:
self.controller.withdraw(id, float(amount))
raise cherrypy.HTTPRedirect(
"/?message=Successfully withdrew %s" % amount)
except BankException, e:
raise cherrypy.HTTPRedirect("/?message=%s" % e)
  1. Modify the beginning of the transfer web method by adding a try-except block that either redirects with a success message or an error message.
@cherrypy.expose
def transfer(self, amount="", source="", target=""):
if amount != "" and source != "" and target != "":
try:
self.controller.transfer(amount, source, target)
raise cherrypy.HTTPRedirect(
"/?message=Successful transfer")
except BankException, e:
raise cherrypy.HTTPRedirect("/?message=%s" % e)
Adding overdraft protection to withdrawals

Withdrawals and transfers are now protected from overdrafts.

Making transfers transactional

The final key thing that needs to be implemented is increasing the integrity of transfers. It is vital for any banking operation that they be performed with atomic consistency. Otherwise, the bank will leak money.

  1. Markup the controller's transfer operation using the @transactional decorator.
@transactional
def transfer(self, amount, source, target):
self.withdraw(source, amount)
self.deposit(target, amount)
  1. In order to have the @transactional decorator available, the following import statement is required.
from springpython.database.transaction import *
  1. In the application context, define a TransactionManager and an AutoTransactionalObject, in order to activate the @transactional decorator.
@Object
def tx_mgr(self):
return ConnectionFactoryTransactionManager(self.factory())
@Object
def transactionalObject(self):
return AutoTransactionalObject(self.tx_mgr())
  1. This change to the application context also requires the same import statement shown above.
  2. This is all that is needed. The transfer method now utilizes transactions to avoid leaking money.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.223.190