Skip to content

Instantly share code, notes, and snippets.

@jimbaker
Last active November 28, 2016 16:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jimbaker/6a4fd7e07a16a318a45d7d1d96819040 to your computer and use it in GitHub Desktop.
Save jimbaker/6a4fd7e07a16a318a45d7d1d96819040 to your computer and use it in GitHub Desktop.
Role assignment additions to Craton modeling
diff --git a/craton/db/sqlalchemy/models.py b/craton/db/sqlalchemy/models.py
index cb1cee7..7ea5040 100644
--- a/craton/db/sqlalchemy/models.py
+++ b/craton/db/sqlalchemy/models.py
@@ -5,7 +5,7 @@ Craton uses the following related aspects of inventory:
* Device inventory, with devices are further organized by region,
cell, and labels. Variables are associated with all of these
entities, with the ability to override via resolution and to track
- with blaming. This in terms forms the foundation of an *inventory
+ with blaming. This in turn forms the foundation of an *inventory
fabric*, which is implemented above this level.
* Workflows are run against this inventory, taking in account the
@@ -46,18 +46,21 @@ class CratonBase(models.ModelBase, models.TimestampMixin):
', '.join(['{0}={1!r}'.format(*item) for item in items]))
-def _variable_mixin_aware_constructor(self, **kwargs):
+def _polymorphic_association_mixin_aware_constructor(self, **kwargs):
# The standard default for the underlying relationship for
- # variables sets it to None, which means it cannot directly be
- # used as a mappable collection. Cure the problem accordingly with
- # a different default.
+ # variables and role_assignments sets each to None, which means it
+ # cannot directly be used as a mappable collection or to a
+ # set. Cure the problem accordingly with a different default.
if isinstance(self, VariableMixin):
kwargs.setdefault('variables', {})
+ if isinstance(self, RoleAssignmentMixin):
+ kwargs.setdefault('role_assignment', set())
return _declarative_constructor(self, **kwargs)
Base = declarative_base(
- cls=CratonBase, constructor=_variable_mixin_aware_constructor)
+ cls=CratonBase,
+ constructor=_polymorphic_association_mixin_aware_constructor)
class VariableAssociation(Base):
@@ -174,6 +177,7 @@ class VariableMixin(object):
rel = relationship(
assoc_cls,
+ foreign_keys=[cls.variable_association_id],
collection_class=attribute_mapped_collection('key'),
cascade='all, delete-orphan', lazy='joined',
single_parent=True,
@@ -182,6 +186,123 @@ class VariableMixin(object):
return rel
+class Principal(Base):
+ """Base class for all principals, including users and workflows"""
+
+ __tablename__ = 'principals'
+ id = Column(Integer, primary_key=True)
+ type = Column(String(50), nullable=False)
+ project_id = Column(
+ UUIDType(binary=False), ForeignKey('projects.id'), index=True,
+ nullable=False)
+
+ role_assignment = relationship(
+ 'RoleAssignment', back_populates='principal', collection_class=set,
+ cascade='all, delete-orphan', lazy='joined')
+
+ __mapper_args__ = {
+ 'polymorphic_on': type,
+ 'polymorphic_identity': 'principals',
+ 'with_polymorphic': '*'
+ }
+
+
+class RoleAssignmentAssociation(Base):
+ """Associates a set of RoleAssignment objects with a resource.
+
+ Used to associate any principal with a corresponding resource
+ (that needs suitable role-based access control). Such resources
+ need to mix in the RoleAssignmentMixin.
+ """
+
+ __tablename__ = "role_assignment_association"
+
+ id = Column(Integer, primary_key=True)
+ discriminator = Column(String(50), nullable=False)
+ """Refers to the type of resource, such as 'region' or 'workflow'"""
+
+ role_assignment = relationship(
+ 'RoleAssignment',
+ collection_class=set,
+ back_populates='association',
+ cascade='all, delete-orphan', lazy='joined',
+ )
+
+ __mapper_args__ = {
+ 'polymorphic_on': discriminator,
+ }
+
+
+class RoleAssignment(Base):
+ """Uses a triple to assigns principals to resources with roles.
+
+ All assignments are stored in this one table, with polymorphic
+ associations used to map to resources that mixin this support.
+ """
+ __tablename__ = 'role_assignments'
+ association_id = Column(
+ Integer,
+ ForeignKey(RoleAssignmentAssociation.id,
+ name='fk_role_assignment_association'),
+ primary_key=True)
+ principal_id = Column(
+ Integer, ForeignKey(Principal.id), primary_key=True)
+ role = Column(String(255), primary_key=True)
+
+ association = relationship(
+ RoleAssignmentAssociation, back_populates='role_assignment')
+ principal = relationship(
+ Principal, back_populates='role_assignment')
+ resource = association_proxy('association', 'resource')
+
+ def __repr__(self):
+ return '%s(principal=%r, resource=%r, role=%r)' % \
+ (self.__class__.__name__,
+ self.principal, self.resource, self.role)
+
+
+class RoleAssignmentMixin(object):
+ """Mixins role assignments for a given class of resource."""
+
+ @declared_attr
+ def role_assignment_association_id(cls):
+ return Column(
+ Integer,
+ ForeignKey(RoleAssignmentAssociation.id,
+ name='fk_%ss_role_assignment_association' %
+ cls.__name__.lower()))
+
+ @declared_attr
+ def role_assignment_association(cls):
+ name = cls.__name__
+ discriminator = name.lower()
+
+ # Defines a polymorphic class to distinguish variables stored
+ # for regions, cells, etc.
+ cls.role_assignment_assoc_cls = assoc_cls = type(
+ "%sRoleAssignmentAssociation" % name,
+ (RoleAssignmentAssociation,),
+ {
+ '__tablename__': None, # because mapping into a shared table
+ '__mapper_args__': {
+ 'polymorphic_identity': discriminator
+ }
+ })
+
+ cls.role_assignment = association_proxy(
+ 'role_assignment_association', 'role_assignment',
+ creator=lambda role_assignment: assoc_cls(role_assignment=role_assignment))
+
+ rel = relationship(
+ assoc_cls,
+ foreign_keys=[cls.role_assignment_association_id],
+ collection_class=set,
+ lazy='joined',
+ backref=backref('resource', uselist=False))
+
+ return rel
+
+
class Project(Base):
"""Supports multitenancy for all other schema elements."""
__tablename__ = 'projects'
@@ -200,28 +321,38 @@ class Project(Base):
networks = relationship('Network', back_populates='project')
-class User(Base, VariableMixin):
+class User(Principal, VariableMixin):
__tablename__ = 'users'
- __table_args__ = (
- UniqueConstraint("username", "project_id",
- name="uq_user0username0project"),
- )
- id = Column(Integer, primary_key=True)
- project_id = Column(
- UUIDType(binary=False), ForeignKey('projects.id'), index=True,
- nullable=False)
+ # __table_args__ = (
+ # UniqueConstraint("username", "project_id",
+ # name="uq_user0username0project"),
+ # )
+ id = Column(Integer, ForeignKey('principals.id'), primary_key=True)
+ # project_id = Column(
+ # UUIDType(binary=False), ForeignKey('projects.id'), index=True,
+ # nullable=False)
username = Column(String(255))
api_key = Column(String(36))
- # root = craton admin that can create other pojects/usrs
+ _repr_columns = [id, username]
+
+ # TODO(jimbaker) remove these booleans in favor of the use of role
+ # assignments
+
+ # root = craton admin that can create other projects/users
is_root = Column(Boolean, default=False)
# admin = project context admin
is_admin = Column(Boolean, default=False)
- roles = Column(JSONType)
project = relationship('Project', back_populates='users')
+ __mapper_args__ = {
+ 'polymorphic_identity': 'users',
+ 'inherit_condition': (id == Principal.id)
+ }
+
+
-class Region(Base, VariableMixin):
+class Region(Base, VariableMixin, RoleAssignmentMixin):
__tablename__ = 'regions'
__table_args__ = (
UniqueConstraint("project_id", "name",
from ipaddress import IPv4Address
import sys
from sqlalchemy import create_engine
from sqlalchemy.orm import lazyload, sessionmaker
from craton.db.sqlalchemy import models
engine = create_engine(sys.argv[1] if len(sys.argv) > 1 else 'mysql+pymysql://craton:craton@localhost/craton', echo=True)
models.Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
project = models.Project(id='b9f10eca66ac4c279c139d01e65f96b4', name='project-1')
user = models.User(username='demo', api_key='demo', project=project)
user2 = models.User(username='demo2', api_key='demo', project=project)
region = models.Region(project=project, name="region-1", variables={'region-level': {'region-variable': 42}})
region2 = models.Region(project=project, name="region-2", variables={'region-level': {'region-variable': 42}})
cell = models.Cell(project=project, region=region, name="cell-1")
host = models.Host(
region=region, cell=cell, project=project, device_type='server',
name='www1.example.com',
ip_address=IPv4Address(u'10.1.2.101'), active=True)
#assignment = models.RoleAssignment(principal=user, resource=region, role='fleet:can-do-anything')
assignment = models.RoleAssignment(role='fleet:can-do-anything')
user.role_assignment.add(assignment)
region.role_assignment.add(assignment)
session.add(project)
session.add(user)
session.add(region)
session.add(assignment)
assignment = models.RoleAssignment(role='fleet:can-do-anything-2')
user.role_assignment.add(assignment)
region.role_assignment.add(assignment)
session.add(assignment)
assignment = models.RoleAssignment(role='fleet:can-do-some-things')
user2.role_assignment.add(assignment)
region.role_assignment.add(assignment)
session.add(assignment)
assignment = models.RoleAssignment(role='fleet:can-do-other-things')
user.role_assignment.add(assignment)
region2.role_assignment.add(assignment)
session.add(assignment)
assignment = models.RoleAssignment(role='fleet:can-do-some-things')
user.role_assignment.add(assignment)
cell.role_assignment.add(assignment)
session.add(assignment)
assignment = models.RoleAssignment(role='fleet:can-do-some-things')
user2.role_assignment.add(assignment)
cell.role_assignment.add(assignment)
session.add(assignment)
session.add(host)
session.commit()
def get_roles(resource, principal):
# implicitly assumes that every obj in resolution_order path has
# RoleAssignment mixed in...
for obj in resource.resolution_order:
q = session.query(models.RoleAssignment).\
join(models.RoleAssignmentAssociation).\
join(obj.__class__).\
options(lazyload('*')).\
filter(models.RoleAssignment.principal == principal).\
filter(obj.__class__.id == obj.id).\
with_entities(models.RoleAssignment.role)
yield q
print(set(role[0] for q in get_roles(host, user) for role in q))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment