0

I have to manage permissions and I have different user types, as for example a couple here.

def not_allowed(*args, **kwargs): return False

class User(object):
    def __init__(self, userid):
        self.userid = userid

    def __getattr__(self, attr):
        return not_allowed

    def view_profile(self)
        return True


class Admin(User):
    def edit_comment(self):
        return True

class Expert(User):
    def delete_post(self):
        return True


user = {'userid': 'user', 'roles': ['admin', 'expert']}

Now I want to be able to have MultiRole type, which in theory should simply be able to do everything that its roles are able to do.

I tried with something like this:

class MultiRoleUser(User):
    """A multirole user has all the power of all the roles together
    """
    def __init__(self, userid, roles):
        super(MultiRoleUser, self).__init__(userid)
        self.roles = roles

    def __getattr__(self, attr):
        all_funcs = [getattr(x, attr) for x in self.roles]
        return any(x() for x in all_funcs)

Which should be used as

u = MultiRoleUser('userid', [Admin, Expert])

But it doesn't work, ideally I would like to call the methods for all the classes passed in and do a or (with any).

The problem is that to call the methods I need to have an object of that type..

In theory I might even just use a dictionary for each role instead, but I liked the default to false trick which makes, and sometimes I also need a function to compute the permission.

Any suggestions?

andrea_crotti
  • 3,004
  • 2
  • 28
  • 33
  • For starters, why are you rolling your own authorization framework? This is a solved problem, and both uniform enough and hard enough to avoid reinventing the wheel. –  Feb 09 '13 at 11:11
  • Because at the moment I have no choice, but I'm open to suggestions for further integrations (the API is running on bottle). Anyway it's really simple when I get this last thing it's done I will be good with 60 lines of code. – andrea_crotti Feb 09 '13 at 11:14

2 Answers2

1

Although I tend to agree that you probably don't want to reinvent the wheel on this, below is version of your attempt that appears to work AFAIK.

Basically, I first I had switch to explicit new-style classes by making them derived from object so that the super() call would work, then second, I changed the self.roles = roles initialization in the MultiRoleUser class to create the instances needed. Lastly I changed how the MultiRoleUser.__getattr__()used them to handle role classes that didn't have the sought permission attribute.

def not_allowed(*args, **kwargs): return False

class User(object):
    def __init__(self, userid):
        self.userid = userid

    def __getattr__(self, attr):
        return not_allowed

    def view_profile(self):
        return True

class Admin(User):
    def edit_comment(self):
        return True

class Expert(User):
    def delete_post(self):
        return True

class MultiRoleUser(User):
    """A multirole user has all the power of all the roles together"""
    def __init__(self, userid, roles):
        super(MultiRoleUser, self).__init__(userid)
        self.roles = [role(userid) for role in roles] # create instances needed

    def __getattr__(self, attr):
        all_funcs = [getattr(x, attr, None) for x in self.roles]
        return any(x() for x in all_funcs if x) # check permission if there was one

u = MultiRoleUser('userid', [Admin, Expert])

print 'u.edit_comment:', u.edit_comment
print 'u.delete_post:', u.delete_post

BTW, I think a better Python implementation would use sets and operations with them to accomplish what you're trying to do.

martineau
  • 119,623
  • 25
  • 170
  • 301
  • Ok there were a couple of mistakes in the code: 1. in the real code I was using new style classes already. 2. I need the inheritance in Admin an Expert which implies that 3. role() would not work because I need to pass the "userid". Thanks! – andrea_crotti Feb 10 '13 at 11:39
  • Fine, I've added those things from the revised code in your question to my answer -- which doesn't change the fact my other suggestions for changes make it work. – martineau Feb 10 '13 at 17:46
0

Here's an alternative answer which uses multiple inheritance and properties to greatly simplify the implementation (because, once again, you were essentially reinventing wheels). It completely eliminates the need to have a roles attribute in the MultiRoleUser class, as well as the specialized getattr() method required to make use it.

The idea for using multiple inheritance occurred to me because it seemed odd to define class MultiRoleUser as having multiple instances of User sublclasses, rather than just being one itself. The idea of using properties came as a result of thinking about the fact that the gettattr() the class required by the class was calling the function attributes found to get their values, which is part of what properties are all about.

class User(object):
    def __init__(self, userid):
        self.userid = userid

    def __getattr__(self, attr):  # unknown/undefined permission
        return False

    @property
    def view_profile(self):
        return True

class Admin(User):
    @property
    def edit_comment(self):
        return True

class Expert(User):
    @property
    def delete_post(self):
        return True

def multi_role_user(cls_name, *roles):
    """Factory function to create a multirole user class which has the combined
       power of all the User subclasses given"""
    if not roles:
        raise TypeError('at least one subclass of class User must be specified')
    if not all(issubclass(role, User) for role in roles):
        raise TypeError('all roles must be subclasses of class User')
    return type(cls_name, roles, {})

MultiRoleUser = multi_role_user('MultiRoleUser', Admin, Expert)
u = MultiRoleUser('my_userid')

print 'u.userid:', u.userid
print 'u.view_profile:', u.view_profile
print 'u.edit_comment:', u.edit_comment
print 'u.delete_post:', u.delete_post
print 'u.can_spam:', u.can_spam
martineau
  • 119,623
  • 25
  • 170
  • 301
  • That's nice, but I don't want to create a class for each possible combination of different roles, so I should generate the classes dynamically, which I'm not sure is much better.. – andrea_crotti Feb 11 '13 at 09:54
  • Oh, it's still possible to dynamically generate different subclasses if desired, which I've updated the answer to illustrate how to do. Most of the added factory function is error checking. – martineau Feb 11 '13 at 17:03