-
-
Save jimbaker/6a4fd7e07a16a318a45d7d1d96819040 to your computer and use it in GitHub Desktop.
Role assignment additions to Craton modeling
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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