The setting
I want to include role-based access control in an application I am currently working on.
In the domain a user may have any subset of a fixed set of roles, which are represented by an enum, similar to the following:
from enum import Enum
class Role(Enum):
ADMIN = "admin"
AUTHOR = "author"
EDITOR = "editor"
(That is, a user may have none of this roles, some, or all of them.)
We are using "classical" mapping, due to architectural choices regarding clean architecture (I ommitted some irrelevant fields):
user_table = db.Table(
"users",
metadata,
Column("id", db.Integer(), primary_key=True, autoincrement=True),
Column("name", db.String(255), nullable=False),
)
mapper(domain.User, user_table)
roles_table = db.Table(
"role_associations",
metadata,
Column("user_id",
db.ForeignKey("users.id", ondelete="CASCADE", onupdate="CASCADE"),
primary_key=True
),
Column("role", db.Enum(Role), primary_key=True),
)
This schema – using a composite primary key for the association table – was chosen over
- having a table for roles, since the roles are static
- boolean attributes for every role, since there are many roles and users, but users usually only have a handful of roles.
The problem
I want to achieve an association between a User class and a set of Role enumeration members within the framework described above.
What I tried
In the SQLAlchemy documentation I found association proxies, which seemed to be applicable in my situation.
I defined a mapping for the association table (role_assiciations) and installed the association proxy on the User class:
User.roles = association_proxy("_role_associations", "role")
This helps insofar that user.roles does provide me with a list of roles associated to that user.
However, adding a role to a user is not possible. I added a creator:
User.roles = association_proxy("_role_associations", "role", creator=lambda role: role)
I thought that this would be sufficient for adding a role using a command such as:
user.roles.append(Role.ADMIN)
However, this produces the following traceback:
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/project/pip_packages/lib/python3.8/site-packages/sqlalchemy/ext/associationproxy.py", line 1080, in append
col.append(item)
File "/project/pip_packages/lib/python3.8/site-packages/sqlalchemy/orm/collections.py", line 1116, in append
item = __set(self, item, _sa_initiator)
File "/project/pip_packages/lib/python3.8/site-packages/sqlalchemy/orm/collections.py", line 1081, in __set
item = executor.fire_append_event(item, _sa_initiator)
File "/project/pip_packages/lib/python3.8/site-packages/sqlalchemy/orm/collections.py", line 717, in fire_append_event
return self.attr.fire_append_event(
File "/project/pip_packages/lib/python3.8/site-packages/sqlalchemy/orm/attributes.py", line 1183, in fire_append_event
value = fn(state, value, initiator or self._append_token)
File "/project/pip_packages/lib/python3.8/site-packages/sqlalchemy/orm/attributes.py", line 1492, in emit_backref_from_collection_append_event
child_state, child_dict = instance_state(child), instance_dict(child)
AttributeError: 'Role' object has no attribute '_sa_instance_state'
So I guess, this would only work if Role was a SQLAlchemy mapped class.
Is this the correct approach?
If so, how can I get it to work and can the proxy be modified to be a set?
Is there a better way to achieve the association between users and sets of enum values described above?