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.
If we log in as Alice, we can view the history of the Checking account.
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.
Dave has no accounts yet. However, Dave can paste in the URL and easily view Alice's account history.
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.
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
. 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
.def get_username(self, id): return self.dt.query_for_object(""" SELECT owner FROM account WHERE id = ? AND status = 'OPEN'""", (id,), str)
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.
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.
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> """
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
.
One of the simplest business rules is to not allow people to draw more from an account than exists.
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)
BankException
class.class BankException(Exception): pass
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)
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)
Withdrawals and transfers are now protected from overdrafts.
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.
@transactional
decorator.@transactional def transfer(self, amount, source, target): self.withdraw(source, amount) self.deposit(target, amount)
@transactional
decorator available, the following import statement is required.from springpython.database.transaction import *
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())
3.14.245.221