class Group(sql.ModelBase, sql.ModelDictMixinWithExtras): __tablename__ = 'group' attributes = ['id', 'name', 'domain_id', 'description'] id = sql.Column(sql.String(64), primary_key=True) name = sql.Column(sql.String(64), nullable=False) domain_id = sql.Column(sql.String(64), nullable=False) description = sql.Column(sql.Text()) extra = sql.Column(sql.JsonBlob()) # Unique constraint across two columns to create the separation # rather than just only 'name' being unique __table_args__ = (sql.UniqueConstraint('domain_id', 'name'), )
class LocalUser(sql.ModelBase, sql.DictBase): __tablename__ = 'local_user' attributes = ['id', 'user_id', 'domain_id', 'name'] id = sql.Column(sql.Integer, primary_key=True) user_id = sql.Column(sql.String(64), sql.ForeignKey('user.id', ondelete='CASCADE'), unique=True) domain_id = sql.Column(sql.String(64), nullable=False) name = sql.Column(sql.String(255), nullable=False) passwords = orm.relationship('Password', single_parent=True, cascade='all,delete-orphan', backref='local_user') __table_args__ = (sql.UniqueConstraint('domain_id', 'name'), {})
class LocalUser(sql.ModelBase, sql.ModelDictMixin): __tablename__ = 'local_user' attributes = ['id', 'user_id', 'domain_id', 'name'] id = sql.Column(sql.Integer, primary_key=True) user_id = sql.Column(sql.String(64)) domain_id = sql.Column(sql.String(64), nullable=False) name = sql.Column(sql.String(255), nullable=False) passwords = orm.relationship('Password', single_parent=True, cascade='all,delete-orphan', lazy='joined', backref='local_user', order_by='Password.created_at_int') failed_auth_count = sql.Column(sql.Integer, nullable=True) failed_auth_at = sql.Column(sql.DateTime, nullable=True) __table_args__ = ( sql.UniqueConstraint('user_id'), sql.UniqueConstraint('domain_id', 'name'), sqlalchemy.ForeignKeyConstraint(['user_id', 'domain_id'], ['user.id', 'user.domain_id'], onupdate='CASCADE', ondelete='CASCADE') )
class Project(sql.ModelBase, sql.DictBase): __tablename__ = 'project' attributes = ['id', 'name', 'domain_id', 'description', 'enabled'] id = sql.Column(sql.String(64), primary_key=True) name = sql.Column(sql.String(64), nullable=False) domain_id = sql.Column(sql.String(64), sql.ForeignKey('domain.id'), nullable=False) description = sql.Column(sql.Text()) enabled = sql.Column(sql.Boolean) extra = sql.Column(sql.JsonBlob()) # Unique constraint across two columns to create the separation # rather than just only 'name' being unique __table_args__ = (sql.UniqueConstraint('domain_id', 'name'), {})
class NonLocalUser(sql.ModelBase, sql.ModelDictMixin): """SQL data model for nonlocal users (LDAP and custom).""" __tablename__ = 'nonlocal_user' attributes = ['domain_id', 'name', 'user_id'] domain_id = sql.Column(sql.String(64), primary_key=True) name = sql.Column(sql.String(255), primary_key=True) user_id = sql.Column(sql.String(64)) __table_args__ = ( sql.UniqueConstraint('user_id'), sqlalchemy.ForeignKeyConstraint( ['user_id', 'domain_id'], ['user.id', 'user.domain_id'], onupdate='CASCADE', ondelete='CASCADE'),)
class ProjectTag(sql.ModelBase, sql.ModelDictMixin): def to_dict(self): d = super(ProjectTag, self).to_dict() return d __tablename__ = 'project_tag' attributes = ['project_id', 'name'] project_id = sql.Column(sql.String(64), sql.ForeignKey('project.id', ondelete='CASCADE'), nullable=False, primary_key=True) name = sql.Column(sql.Unicode(255), nullable=False, primary_key=True) __table_args__ = (sql.UniqueConstraint('project_id', 'name'), )
class RoleTable(sql.ModelBase, sql.ModelDictMixinWithExtras): def to_dict(self, include_extra_dict=False): d = super(RoleTable, self).to_dict( include_extra_dict=include_extra_dict) if d['domain_id'] == base.NULL_DOMAIN_ID: d['domain_id'] = None # NOTE(notmorgan): Eventually it may make sense to drop the empty # option dict creation to the superclass (if enough models use it) d['options'] = resource_options.ref_mapper_to_dict_options(self) return d @classmethod def from_dict(cls, role_dict): if 'domain_id' in role_dict and role_dict['domain_id'] is None: new_dict = role_dict.copy() new_dict['domain_id'] = base.NULL_DOMAIN_ID else: new_dict = role_dict # TODO(morgan): move this functionality to a common location resource_options = {} options = new_dict.pop('options', {}) for opt in cls.resource_options_registry.options: if opt.option_name in options: opt_value = options[opt.option_name] # NOTE(notmorgan): None is always a valid type if opt_value is not None: opt.validator(opt_value) resource_options[opt.option_id] = opt_value role_obj = super(RoleTable, cls).from_dict(new_dict) setattr(role_obj, '_resource_options', resource_options) return role_obj __tablename__ = 'role' attributes = ['id', 'name', 'domain_id', 'description'] resource_options_registry = ro.ROLE_OPTIONS_REGISTRY id = sql.Column(sql.String(64), primary_key=True) name = sql.Column(sql.String(255), nullable=False) domain_id = sql.Column(sql.String(64), nullable=False, server_default=base.NULL_DOMAIN_ID) description = sql.Column(sql.String(255), nullable=True) extra = sql.Column(sql.JsonBlob()) _resource_option_mapper = orm.relationship( 'RoleOption', single_parent=True, cascade='all,delete,delete-orphan', lazy='subquery', backref='role', collection_class=collections.attribute_mapped_collection('option_id') ) __table_args__ = (sql.UniqueConstraint('name', 'domain_id'),)
class IDMapping(sql.ModelBase, sql.ModelDictMixin): __tablename__ = 'id_mapping' public_id = sql.Column(sql.String(64), primary_key=True) domain_id = sql.Column(sql.String(64), nullable=False) local_id = sql.Column(sql.String(64), nullable=False) # NOTE(henry-nash): Postgres requires a name to be defined for an Enum entity_type = sql.Column(sql.Enum(identity_mapping.EntityType.USER, identity_mapping.EntityType.GROUP, name='entity_type'), nullable=False) # Unique constraint to ensure you can't store more than one mapping to the # same underlying values __table_args__ = (sql.UniqueConstraint('domain_id', 'local_id', 'entity_type'), )
class Group(sql.ModelBase, sql.ModelDictMixinWithExtras): __tablename__ = 'group' attributes = ['id', 'name', 'domain_id', 'description'] id = sql.Column(sql.String(64), primary_key=True) name = sql.Column(sql.String(64), nullable=False) domain_id = sql.Column(sql.String(64), nullable=False) description = sql.Column(sql.Text()) extra = sql.Column(sql.JsonBlob()) expiring_user_group_memberships = orm.relationship( 'ExpiringUserGroupMembership', cascade='all, delete-orphan', backref="group") # Unique constraint across two columns to create the separation # rather than just only 'name' being unique __table_args__ = (sql.UniqueConstraint('domain_id', 'name'), )
class Permission(sql.ModelBase, sql.ModelDictMixin): __tablename__ = 'permission_fiware' __table_args__ = (sql.UniqueConstraint('name', 'application_id'), {'extend_existing': True}) attributes = ['id', 'name', 'is_internal', 'application_id', 'action' 'resource', 'xml'] id = sql.Column(sql.String(64), primary_key=True, nullable=False) name = sql.Column(sql.String(64), nullable=False) is_internal = sql.Column(sql.Boolean(), default=False, nullable=False) application_id = sql.Column( sql.String(64), sql.ForeignKey('consumer_oauth2.id'), nullable=False, index=True) action = sql.Column(sql.String(10), nullable=True) resource = sql.Column(sql.String(256), nullable=True) xml = sql.Column(sql.Text(), nullable=True)
class AccessRuleModel(sql.ModelBase, sql.ModelDictMixin): __tablename__ = 'access_rule' attributes = ['external_id', 'user_id', 'service', 'path', 'method'] id = sql.Column(sql.Integer, primary_key=True, nullable=False) external_id = sql.Column(sql.String(64), index=True, unique=True) user_id = sql.Column(sql.String(64), index=True) service = sql.Column(sql.String(64)) path = sql.Column(sql.String(128)) method = sql.Column(sql.String(16)) __table_args__ = (sql.UniqueConstraint( 'user_id', 'service', 'path', 'method', name='duplicate_access_rule_for_user_constraint'), ) application_credential = sqlalchemy.orm.relationship( 'ApplicationCredentialAccessRuleModel', backref=sqlalchemy.orm.backref('access_rule'))
class FederatedUser(sql.ModelBase, sql.ModelDictMixin): __tablename__ = 'federated_user' attributes = ['id', 'user_id', 'idp_id', 'protocol_id', 'unique_id', 'display_name'] id = sql.Column(sql.Integer, primary_key=True) user_id = sql.Column(sql.String(64), sql.ForeignKey('user.id', ondelete='CASCADE')) idp_id = sql.Column(sql.String(64), sql.ForeignKey('identity_provider.id', ondelete='CASCADE')) protocol_id = sql.Column(sql.String(64), nullable=False) unique_id = sql.Column(sql.String(255), nullable=False) display_name = sql.Column(sql.String(255), nullable=True) __table_args__ = ( sql.UniqueConstraint('idp_id', 'protocol_id', 'unique_id'), sqlalchemy.ForeignKeyConstraint(['protocol_id', 'idp_id'], ['federation_protocol.id', 'federation_protocol.idp_id']) )
class TrustModel(sql.ModelBase, sql.DictBase): __tablename__ = 'trust' attributes = ['id', 'trustor_user_id', 'trustee_user_id', 'project_id', 'impersonation', 'expires_at', 'remaining_uses', 'deleted_at'] id = sql.Column(sql.String(64), primary_key=True) # user id of owner trustor_user_id = sql.Column(sql.String(64), nullable=False,) # user_id of user allowed to consume this preauth trustee_user_id = sql.Column(sql.String(64), nullable=False) project_id = sql.Column(sql.String(64)) impersonation = sql.Column(sql.Boolean, nullable=False) deleted_at = sql.Column(sql.DateTime) expires_at = sql.Column(sql.DateTime) remaining_uses = sql.Column(sql.Integer, nullable=True) extra = sql.Column(sql.JsonBlob()) __table_args__ = (sql.UniqueConstraint( 'trustor_user_id', 'trustee_user_id', 'project_id', 'impersonation', 'expires_at', name='duplicate_trust_constraint'),)
class User(sql.ModelBase, sql.DictBase): __tablename__ = 'user' attributes = ['id', 'name', 'domain_id', 'password', 'enabled', 'default_project_id'] id = sql.Column(sql.String(64), primary_key=True) name = sql.Column(sql.String(255), nullable=False) domain_id = sql.Column(sql.String(64), nullable=False) password = sql.Column(sql.String(128)) enabled = sql.Column(sql.Boolean) extra = sql.Column(sql.JsonBlob()) default_project_id = sql.Column(sql.String(64)) # Unique constraint across two columns to create the separation # rather than just only 'name' being unique __table_args__ = (sql.UniqueConstraint('domain_id', 'name'), {}) def to_dict(self, include_extra_dict=False): d = super(User, self).to_dict(include_extra_dict=include_extra_dict) if 'default_project_id' in d and d['default_project_id'] is None: del d['default_project_id'] return d
class TrustModel(sql.ModelBase, sql.ModelDictMixinWithExtras): __tablename__ = 'trust' attributes = [ 'id', 'trustor_user_id', 'trustee_user_id', 'project_id', 'impersonation', 'expires_at', 'remaining_uses', 'deleted_at', 'redelegated_trust_id', 'redelegation_count' ] id = sql.Column(sql.String(64), primary_key=True) # user id of owner trustor_user_id = sql.Column( sql.String(64), nullable=False, ) # user_id of user allowed to consume this preauth trustee_user_id = sql.Column(sql.String(64), nullable=False) project_id = sql.Column(sql.String(64)) impersonation = sql.Column(sql.Boolean, nullable=False) deleted_at = sql.Column(sql.DateTime) _expires_at = sql.Column('expires_at', sql.DateTime) expires_at_int = sql.Column(sql.DateTimeInt(), nullable=True) remaining_uses = sql.Column(sql.Integer, nullable=True) redelegated_trust_id = sql.Column(sql.String(64), nullable=True) redelegation_count = sql.Column(sql.Integer, nullable=True) extra = sql.Column(sql.JsonBlob()) __table_args__ = (sql.UniqueConstraint( 'trustor_user_id', 'trustee_user_id', 'project_id', 'impersonation', 'expires_at', name='duplicate_trust_constraint'), ) @hybrid_property def expires_at(self): return self.expires_at_int or self._expires_at @expires_at.setter def expires_at(self, value): self._expires_at = value self.expires_at_int = value
class FederationProtocolModel(sql.ModelBase, sql.DictBase): __tablename__ = 'federation_protocol' attributes = ['id', 'idp_id', 'mapping_id'] mutable_attributes = frozenset(['mapping_id']) id = sql.Column(sql.String(64), primary_key=True) idp_id = sql.Column(sql.String(64), sql.ForeignKey('identity_provider.id', ondelete='CASCADE'), primary_key=True) mapping_id = sql.Column(sql.String(64), nullable=False) __table_args__ = (sql.UniqueConstraint('id', 'idp_id'), dict()) @classmethod def from_dict(cls, dictionary): new_dictionary = dictionary.copy() return cls(**new_dictionary) def to_dict(self): """Return a dictionary with model's attributes.""" d = dict() for attr in self.__class__.attributes: d[attr] = getattr(self, attr) return d
class ApplicationCredentialModel(sql.ModelBase, sql.ModelDictMixin): __tablename__ = 'application_credential' attributes = [ 'internal_id', 'id', 'name', 'secret_hash', 'description', 'user_id', 'project_id', 'system', 'expires_at', 'unrestricted' ] internal_id = sql.Column(sql.Integer, primary_key=True, nullable=False) id = sql.Column(sql.String(64), nullable=False) name = sql.Column(sql.String(255), nullable=False) secret_hash = sql.Column(sql.String(255), nullable=False) description = sql.Column(sql.Text()) user_id = sql.Column(sql.String(64), nullable=False) project_id = sql.Column(sql.String(64), nullable=True) system = sql.Column(sql.String(64), nullable=True) expires_at = sql.Column(sql.DateTimeInt()) unrestricted = sql.Column(sql.Boolean) __table_args__ = (sql.UniqueConstraint( 'name', 'user_id', name='duplicate_app_cred_constraint'), ) roles = sqlalchemy.orm.relationship( 'ApplicationCredentialRoleModel', backref=sqlalchemy.orm.backref('application_credential'), cascade='all, delete-orphan')
class Project(sql.ModelBase, sql.ModelDictMixinWithExtras): # NOTE(henry-nash): From the manager and above perspective, the domain_id # is nullable. However, to ensure uniqueness in multi-process # configurations, it is better to still use the sql uniqueness constraint. # Since the support for a nullable component of a uniqueness constraint # across different sql databases is mixed, we instead store a special value # to represent null, as defined in NULL_DOMAIN_ID above. def to_dict(self, include_extra_dict=False): d = super(Project, self).to_dict( include_extra_dict=include_extra_dict) if d['domain_id'] == base.NULL_DOMAIN_ID: d['domain_id'] = None # NOTE(notmorgan): Eventually it may make sense to drop the empty # option dict creation to the superclass (if enough models use it) d['options'] = resource_options.ref_mapper_to_dict_options(self) return d @classmethod def from_dict(cls, project_dict): new_dict = project_dict.copy() # TODO(morgan): move this functionality to a common location resource_options = {} options = new_dict.pop('options', {}) for opt in cls.resource_options_registry.options: if opt.option_name in options: opt_value = options[opt.option_name] # NOTE(notmorgan): None is always a valid type if opt_value is not None: opt.validator(opt_value) resource_options[opt.option_id] = opt_value project_obj = super(Project, cls).from_dict(new_dict) setattr(project_obj, '_resource_options', resource_options) return project_obj __tablename__ = 'project' attributes = ['id', 'name', 'domain_id', 'description', 'enabled', 'parent_id', 'is_domain', 'tags'] resource_options_registry = ro.PROJECT_OPTIONS_REGISTRY id = sql.Column(sql.String(64), primary_key=True) name = sql.Column(sql.String(64), nullable=False) domain_id = sql.Column(sql.String(64), sql.ForeignKey('project.id'), nullable=False) description = sql.Column(sql.Text()) enabled = sql.Column(sql.Boolean) extra = sql.Column(sql.JsonBlob()) parent_id = sql.Column(sql.String(64), sql.ForeignKey('project.id')) is_domain = sql.Column(sql.Boolean, default=False, nullable=False, server_default='0') _tags = orm.relationship( 'ProjectTag', single_parent=True, lazy='subquery', cascade='all,delete-orphan', backref='project', primaryjoin='and_(ProjectTag.project_id==Project.id)' ) _resource_option_mapper = orm.relationship( 'ProjectOption', single_parent=True, cascade='all,delete,delete-orphan', lazy='subquery', backref='project', collection_class=collections.attribute_mapped_collection('option_id') ) # Unique constraint across two columns to create the separation # rather than just only 'name' being unique __table_args__ = (sql.UniqueConstraint('domain_id', 'name'),) @property def tags(self): if self._tags: return [tag.name for tag in self._tags] return [] @tags.setter def tags(self, values): new_tags = [] for tag in values: tag_ref = ProjectTag() tag_ref.project_id = self.id tag_ref.name = text_type(tag) new_tags.append(tag_ref) self._tags = new_tags
class User(sql.ModelBase, sql.ModelDictMixinWithExtras): __tablename__ = 'user' attributes = ['id', 'name', 'domain_id', 'password', 'enabled', 'default_project_id', 'password_expires_at'] readonly_attributes = ['id', 'password_expires_at', 'password'] resource_options_registry = iro.USER_OPTIONS_REGISTRY id = sql.Column(sql.String(64), primary_key=True) domain_id = sql.Column(sql.String(64), nullable=False) _enabled = sql.Column('enabled', sql.Boolean) extra = sql.Column(sql.JsonBlob()) default_project_id = sql.Column(sql.String(64), index=True) _resource_option_mapper = orm.relationship( 'UserOption', single_parent=True, cascade='all,delete,delete-orphan', lazy='subquery', backref='user', collection_class=collections.attribute_mapped_collection('option_id')) local_user = orm.relationship('LocalUser', uselist=False, single_parent=True, lazy='joined', cascade='all,delete-orphan', backref='user') federated_users = orm.relationship('FederatedUser', single_parent=True, lazy='joined', cascade='all,delete-orphan', backref='user') nonlocal_user = orm.relationship('NonLocalUser', uselist=False, single_parent=True, lazy='joined', cascade='all,delete-orphan', backref='user') created_at = sql.Column(sql.DateTime, nullable=True) last_active_at = sql.Column(sql.Date, nullable=True) # unique constraint needed here to support composite fk constraints __table_args__ = (sql.UniqueConstraint('id', 'domain_id'), {}) # NOTE(stevemar): we use a hybrid property here because we leverage the # expression method, see `@name.expression` and `LocalUser.name` below. @hybrid_property def name(self): """Return the current user name.""" if self.local_user: return self.local_user.name elif self.nonlocal_user: return self.nonlocal_user.name elif self.federated_users: return self.federated_users[0].display_name else: return None @name.setter def name(self, value): if not self.local_user: self.local_user = LocalUser() self.local_user.name = value @name.expression def name(cls): return LocalUser.name # password properties @property def password_ref(self): """Return the current password ref.""" if self.local_user and self.local_user.passwords: return self.local_user.passwords[-1] return None # NOTE(stevemar): we use a hybrid property here because we leverage the # expression method, see `@password.expression` and `Password.password` # below. @hybrid_property def password(self): """Return the current password.""" if self.password_ref: return self.password_ref.password_hash return None @property def password_created_at(self): """Return when password was created at.""" if self.password_ref: return self.password_ref.created_at return None @property def password_expires_at(self): """Return when password expires at.""" if self.password_ref: return self.password_ref.expires_at return None @property def password_is_expired(self): """Return whether password is expired or not.""" if self.password_expires_at and not self._password_expiry_exempt(): return datetime.datetime.utcnow() >= self.password_expires_at return False @password.setter def password(self, value): now = datetime.datetime.utcnow() if not self.local_user: self.local_user = LocalUser() # truncate extra passwords if self.local_user.passwords: unique_cnt = CONF.security_compliance.unique_last_password_count unique_cnt = unique_cnt + 1 if unique_cnt == 0 else unique_cnt self.local_user.passwords = self.local_user.passwords[-unique_cnt:] # set all previous passwords to be expired for ref in self.local_user.passwords: if not ref.expires_at or ref.expires_at > now: ref.expires_at = now new_password_ref = Password() hashed_passwd = None if value is not None: # NOTE(notmorgan): hash the passwords, never directly bind the # "value" in the unhashed form to hashed_passwd to ensure the # unhashed password cannot end up in the db. If an unhashed # password ends up in the DB, it cannot be used for auth, it is # however incorrect and could leak user credentials (due to users # doing insecure things such as sharing passwords across # different systems) to unauthorized parties. hashed_passwd = password_hashing.hash_password(value) new_password_ref.password_hash = hashed_passwd new_password_ref.created_at = now new_password_ref.expires_at = self._get_password_expires_at(now) self.local_user.passwords.append(new_password_ref) def _password_expiry_exempt(self): # Get the IGNORE_PASSWORD_EXPIRY_OPT value from the user's # option_mapper. return getattr( self.get_resource_option(iro.IGNORE_PASSWORD_EXPIRY_OPT.option_id), 'option_value', False) def _get_password_expires_at(self, created_at): expires_days = CONF.security_compliance.password_expires_days if not self._password_expiry_exempt(): if expires_days: expired_date = (created_at + datetime.timedelta(days=expires_days)) return expired_date.replace(microsecond=0) return None @password.expression def password(cls): return Password.password_hash # NOTE(stevemar): we use a hybrid property here because we leverage the # expression method, see `@enabled.expression` and `User._enabled` below. @hybrid_property def enabled(self): """Return whether user is enabled or not.""" if self._enabled: max_days = ( CONF.security_compliance.disable_user_account_days_inactive) last_active = self.last_active_at if not last_active and self.created_at: last_active = self.created_at.date() if max_days and last_active: now = datetime.datetime.utcnow().date() days_inactive = (now - last_active).days if days_inactive >= max_days: self._enabled = False return self._enabled @enabled.setter def enabled(self, value): if (value and CONF.security_compliance.disable_user_account_days_inactive): self.last_active_at = datetime.datetime.utcnow().date() if value and self.local_user: self.local_user.failed_auth_count = 0 self.local_user.failed_auth_at = None self._enabled = value @enabled.expression def enabled(cls): return User._enabled def get_resource_option(self, option_id): if option_id in self._resource_option_mapper.keys(): return self._resource_option_mapper[option_id] return None def to_dict(self, include_extra_dict=False): d = super(User, self).to_dict(include_extra_dict=include_extra_dict) if 'default_project_id' in d and d['default_project_id'] is None: del d['default_project_id'] # NOTE(notmorgan): Eventually it may make sense to drop the empty # option dict creation to the superclass (if enough models use it) d['options'] = resource_options.ref_mapper_to_dict_options(self) return d @classmethod def from_dict(cls, user_dict): """Override from_dict to remove password_expires_at attribute. Overriding this method to remove password_expires_at attribute to support update_user and unit tests where password_expires_at inadvertently gets added by calling to_dict followed by from_dict. :param user_dict: User entity dictionary :returns User: User object """ new_dict = user_dict.copy() resource_options = {} options = new_dict.pop('options', {}) password_expires_at_key = 'password_expires_at' if password_expires_at_key in user_dict: del new_dict[password_expires_at_key] for opt in cls.resource_options_registry.options: if opt.option_name in options: opt_value = options[opt.option_name] # NOTE(notmorgan): None is always a valid type if opt_value is not None: opt.validator(opt_value) resource_options[opt.option_id] = opt_value user_obj = super(User, cls).from_dict(new_dict) setattr(user_obj, '_resource_options', resource_options) return user_obj
class Project(sql.ModelBase, sql.ModelDictMixinWithExtras): # NOTE(henry-nash): From the manager and above perspective, the domain_id # is nullable. However, to ensure uniqueness in multi-process # configurations, it is better to still use the sql uniqueness constraint. # Since the support for a nullable component of a uniqueness constraint # across different sql databases is mixed, we instead store a special value # to represent null, as defined in NULL_DOMAIN_ID above. def to_dict(self, include_extra_dict=False): d = super(Project, self).to_dict(include_extra_dict=include_extra_dict) if d['domain_id'] == base.NULL_DOMAIN_ID: d['domain_id'] = None return d __tablename__ = 'project' attributes = [ 'id', 'name', 'domain_id', 'description', 'enabled', 'parent_id', 'is_domain', 'tags' ] id = sql.Column(sql.String(64), primary_key=True) name = sql.Column(sql.String(64), nullable=False) domain_id = sql.Column(sql.String(64), sql.ForeignKey('project.id'), nullable=False) description = sql.Column(sql.Text()) enabled = sql.Column(sql.Boolean) extra = sql.Column(sql.JsonBlob()) parent_id = sql.Column(sql.String(64), sql.ForeignKey('project.id')) is_domain = sql.Column(sql.Boolean, default=False, nullable=False, server_default='0') _tags = orm.relationship( 'ProjectTag', single_parent=True, lazy='subquery', cascade='all,delete-orphan', backref='project', primaryjoin='and_(ProjectTag.project_id==Project.id)') # Unique constraint across two columns to create the separation # rather than just only 'name' being unique __table_args__ = (sql.UniqueConstraint('domain_id', 'name'), ) @hybrid_property def tags(self): if self._tags: return [tag.name for tag in self._tags] return [] @tags.setter def tags(self, values): new_tags = [] for tag in values: tag_ref = ProjectTag() tag_ref.project_id = self.id tag_ref.name = tag new_tags.append(tag_ref) self._tags = new_tags @tags.expression def tags(cls): return ProjectTag.name