# Copyright (c) 2013-2014 Rackspace, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Defines database models for Barbican
"""
import hashlib

from oslo_serialization import jsonutils as json
from oslo_utils import timeutils
import six
import sqlalchemy as sa
from sqlalchemy.ext import compiler
from sqlalchemy.ext import declarative
from sqlalchemy import orm
from sqlalchemy.orm import collections as col
from sqlalchemy import types as sql_types

from barbican.common import exception
from barbican.common import utils
from barbican import i18n as u

LOG = utils.getLogger(__name__)
BASE = declarative.declarative_base()
ERROR_REASON_LENGTH = 255
SUB_STATUS_LENGTH = 36
SUB_STATUS_MESSAGE_LENGTH = 255


# Allowed entity states
class States(object):
    PENDING = 'PENDING'
    ACTIVE = 'ACTIVE'
    ERROR = 'ERROR'

    @classmethod
    def is_valid(cls, state_to_test):
        """Tests if a state is a valid one."""
        return state_to_test in cls.__dict__


class OrderType(object):
    KEY = 'key'
    ASYMMETRIC = 'asymmetric'
    CERTIFICATE = 'certificate'

    @classmethod
    def is_valid(cls, order_type):
        """Tests if a order type is a valid one."""
        return order_type in cls.__dict__


class OrderStatus(object):
    def __init__(self, id, message):
        self.id = id
        self.message = message


@compiler.compiles(sa.BigInteger, 'sqlite')
def compile_big_int_sqlite(type_, compiler, **kw):
    return 'INTEGER'


class JsonBlob(sql_types.TypeDecorator):
    """JsonBlob is custom type for fields which need to store JSON text."""
    impl = sa.Text

    def process_bind_param(self, value, dialect):
        if value is not None:
            return json.dumps(value)
        return value

    def process_result_value(self, value, dialect):
        if value is not None:
            return json.loads(value)
        return value


class ModelBase(object):
    """Base class for Nova and Barbican Models."""
    __table_args__ = {'mysql_engine': 'InnoDB'}
    __table_initialized__ = False
    __protected_attributes__ = {
        "created_at", "updated_at", "deleted_at", "deleted"}

    id = sa.Column(sa.String(36), primary_key=True,
                   default=utils.generate_uuid)

    created_at = sa.Column(sa.DateTime, default=timeutils.utcnow,
                           nullable=False)
    updated_at = sa.Column(sa.DateTime, default=timeutils.utcnow,
                           nullable=False, onupdate=timeutils.utcnow)
    deleted_at = sa.Column(sa.DateTime)
    deleted = sa.Column(sa.Boolean, nullable=False, default=False)

    status = sa.Column(sa.String(20), nullable=False, default=States.PENDING)

    def save(self, session=None):
        """Save this object."""
        # import api here to prevent circular dependency problem
        import barbican.model.repositories
        session = session or barbican.model.repositories.get_session()
        # if model is being created ensure that created/updated are the same
        if self.id is None:
            self.created_at = timeutils.utcnow()
            self.updated_at = self.created_at
        session.add(self)
        session.flush()

    def delete(self, session=None):
        """Delete this object."""
        import barbican.model.repositories
        session = session or barbican.model.repositories.get_session()
        self._do_delete_children(session)
        session.delete(self)

    def _do_delete_children(self, session):
        """Sub-class hook: delete children relationships."""
        pass

    def update(self, values):
        """dict.update() behaviour."""
        for k, v in values.items():
            self[k] = v

    def __setitem__(self, key, value):
        setattr(self, key, value)

    def __getitem__(self, key):
        return getattr(self, key)

    def __iter__(self):
        self._i = iter(orm.object_mapper(self).sa.Columns)
        return self

    def next(self):
        n = next(self._i).name
        return n, getattr(self, n)

    def keys(self):
        return self.__dict__.keys()

    def values(self):
        return self.__dict__.values()

    def items(self):
        return self.__dict__.items()

    def to_dict(self):
        return self.__dict__.copy()

    def to_dict_fields(self):
        """Returns a dictionary of just the db fields of this entity."""

        if self.created_at:
            created_at = self.created_at.isoformat()
        else:
            created_at = self.created_at

        if self.updated_at:
            updated_at = self.updated_at.isoformat()
        else:
            updated_at = self.updated_at

        dict_fields = {
            'created': created_at,
            'updated': updated_at,
            'status': self.status
        }

        if self.deleted_at:
            dict_fields['deleted_at'] = self.deleted_at.isoformat()
        if self.deleted:
            dict_fields['deleted'] = True
        dict_fields.update(self._do_extra_dict_fields())
        return dict_fields

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        return {}

    def _iso_to_datetime(self, expiration):
        """Convert ISO formatted string to datetime."""
        if isinstance(expiration, six.string_types):
            expiration_iso = timeutils.parse_isotime(expiration.strip())
            expiration = timeutils.normalize_time(expiration_iso)

        return expiration


class SoftDeleteMixIn(object):
    """Mix-in class that adds soft delete functionality."""

    def delete(self, session=None):
        """Delete this object."""
        import barbican.model.repositories
        session = session or barbican.model.repositories.get_session()
        self.deleted = True
        self.deleted_at = timeutils.utcnow()
        self.save(session=session)

        self._do_delete_children(session)


class ContainerSecret(BASE, SoftDeleteMixIn, ModelBase):
    """Represents an association between a Container and a Secret."""

    __tablename__ = 'container_secret'

    name = sa.Column(sa.String(255), nullable=True)
    container_id = sa.Column(
        sa.String(36), sa.ForeignKey('containers.id'), index=True,
        nullable=False)
    secret_id = sa.Column(
        sa.String(36), sa.ForeignKey('secrets.id'), index=True, nullable=False)

    # Eager load this relationship via 'lazy=False'.
    container = orm.relationship(
        'Container',
        backref=orm.backref('container_secrets', lazy=False,
                            primaryjoin="and_(ContainerSecret.container_id == "
                            "Container.id, ContainerSecret.deleted!=1)"))
    secrets = orm.relationship(
        'Secret',
        backref=orm.backref('container_secrets',
                            primaryjoin="and_(ContainerSecret.secret_id == "
                            "Secret.id, ContainerSecret.deleted!=1)"))

    __table_args__ = (sa.UniqueConstraint('container_id', 'secret_id', 'name',
                                          name='_container_secret_name_uc'),)


class Project(BASE, SoftDeleteMixIn, ModelBase):
    """Represents a Project in the datastore.

    Projects are users that wish to store secret information within Barbican.
    """

    __tablename__ = 'projects'

    external_id = sa.Column(sa.String(255), unique=True)

    orders = orm.relationship("Order", backref="project")
    secrets = orm.relationship("Secret", backref="project")
    keks = orm.relationship("KEKDatum", backref="project")
    containers = orm.relationship("Container", backref="project")
    cas = orm.relationship("ProjectCertificateAuthority", backref="project")
    project_quotas = orm.relationship("ProjectQuotas", backref="project")

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        return {'external_id': self.external_id}


class Secret(BASE, SoftDeleteMixIn, ModelBase):
    """Represents a Secret in the datastore.

    Secrets are any information Projects wish to store within
    Barbican, though the actual encrypted data is stored in one
    or more EncryptedData entities on behalf of a Secret.
    """

    __tablename__ = 'secrets'

    name = sa.Column(sa.String(255))
    secret_type = sa.Column(sa.String(255),
                            server_default=utils.SECRET_TYPE_OPAQUE)
    expiration = sa.Column(sa.DateTime, default=None)
    algorithm = sa.Column(sa.String(255))
    bit_length = sa.Column(sa.Integer)
    mode = sa.Column(sa.String(255))
    creator_id = sa.Column(sa.String(255))
    project_id = sa.Column(
        sa.String(36),
        sa.ForeignKey('projects.id', name='secrets_project_fk'),
        index=True,
        nullable=False)

    # TODO(jwood): Performance - Consider avoiding full load of all
    #   datum attributes here. This is only being done to support the
    #   building of the list of supported content types when secret
    #   metadata is retrieved.
    #   See barbican.api.resources.py::SecretsResource.on_get()
    # Eager load this relationship via 'lazy=False'.
    encrypted_data = orm.relationship("EncryptedDatum", lazy=False)

    secret_store_metadata = orm.relationship(
        "SecretStoreMetadatum",
        collection_class=col.attribute_mapped_collection('key'),
        backref="secret",
        cascade="all, delete-orphan")

    secret_user_metadata = orm.relationship(
        "SecretUserMetadatum",
        collection_class=col.attribute_mapped_collection('key'),
        backref="secret",
        cascade="all, delete-orphan")

    def __init__(self, parsed_request=None):
        """Creates secret from a dict."""
        super(Secret, self).__init__()

        if parsed_request:
            self.name = parsed_request.get('name')
            self.secret_type = parsed_request.get(
                'secret_type',
                utils.SECRET_TYPE_OPAQUE)
            expiration = self._iso_to_datetime(parsed_request.get
                                               ('expiration'))
            self.expiration = expiration
            self.algorithm = parsed_request.get('algorithm')
            self.bit_length = parsed_request.get('bit_length')
            self.mode = parsed_request.get('mode')
            self.creator_id = parsed_request.get('creator_id')
            self.project_id = parsed_request.get('project_id')

        self.status = States.ACTIVE

    def _do_delete_children(self, session):
        """Sub-class hook: delete children relationships."""
        for k, v in self.secret_store_metadata.items():
            v.delete(session)

        for k, v in self.secret_user_metadata.items():
            v.delete(session)

        for datum in self.encrypted_data:
            datum.delete(session)

        for secret_ref in self.container_secrets:
            session.delete(secret_ref)

        for secret_acl in self.secret_acls:
            session.delete(secret_acl)

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        if self.expiration:
            expiration = self.expiration.isoformat()
        else:
            expiration = self.expiration

        return {
            'secret_id': self.id,
            'name': self.name,
            'secret_type': self.secret_type,
            'expiration': expiration,
            'algorithm': self.algorithm,
            'bit_length': self.bit_length,
            'mode': self.mode,
            'creator_id': self.creator_id,
        }


class SecretStoreMetadatum(BASE, SoftDeleteMixIn, ModelBase):
    """Represents Secret Store metadatum for a single key-value pair."""

    __tablename__ = "secret_store_metadata"

    key = sa.Column(sa.String(255), nullable=False)
    value = sa.Column(sa.String(255), nullable=False)
    secret_id = sa.Column(
        sa.String(36), sa.ForeignKey('secrets.id'), index=True, nullable=False)

    def __init__(self, key, value):
        super(SecretStoreMetadatum, self).__init__()

        msg = u._("Must supply non-None {0} argument "
                  "for SecretStoreMetadatum entry.")

        if key is None:
            raise exception.MissingArgumentError(msg.format("key"))
        self.key = key

        if value is None:
            raise exception.MissingArgumentError(msg.format("value"))
        self.value = value

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        return {
            'key': self.key,
            'value': self.value
        }


class SecretUserMetadatum(BASE, SoftDeleteMixIn, ModelBase):
    """Represents Secret user metadatum for a single key-value pair."""

    __tablename__ = "secret_user_metadata"

    key = sa.Column(sa.String(255), nullable=False)
    value = sa.Column(sa.String(255), nullable=False)
    secret_id = sa.Column(
        sa.String(36), sa.ForeignKey('secrets.id'), index=True, nullable=False)

    __table_args__ = (sa.UniqueConstraint('secret_id', 'key',
                                          name='_secret_key_uc'),)

    def __init__(self, key, value):
        super(SecretUserMetadatum, self).__init__()

        msg = u._("Must supply non-None {0} argument "
                  "for SecretUserMetadatum entry.")

        if key is None:
            raise exception.MissingArgumentError(msg.format("key"))
        self.key = key

        if value is None:
            raise exception.MissingArgumentError(msg.format("value"))
        self.value = value

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        return {
            'key': self.key,
            'value': self.value
        }


class EncryptedDatum(BASE, SoftDeleteMixIn, ModelBase):
    """Represents the encrypted data for a Secret."""

    __tablename__ = 'encrypted_data'

    content_type = sa.Column(sa.String(255))
    secret_id = sa.Column(
        sa.String(36), sa.ForeignKey('secrets.id'), index=True, nullable=False)
    kek_id = sa.Column(
        sa.String(36), sa.ForeignKey('kek_data.id'), index=True,
        nullable=False)

    # TODO(jwood) Why LargeBinary on Postgres (BYTEA) not work correctly?
    cypher_text = sa.Column(sa.Text)
    kek_meta_extended = sa.Column(sa.Text)

    # Eager load this relationship via 'lazy=False'.
    kek_meta_project = orm.relationship("KEKDatum", lazy=False)

    def __init__(self, secret=None, kek_datum=None):
        """Creates encrypted datum from a secret and KEK metadata."""
        super(EncryptedDatum, self).__init__()

        if secret:
            self.secret_id = secret.id

        if kek_datum:
            self.kek_id = kek_datum.id
            self.kek_meta_project = kek_datum

        self.status = States.ACTIVE

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        return {'content_type': self.content_type}


class KEKDatum(BASE, SoftDeleteMixIn, ModelBase):
    """Key encryption key (KEK) metadata model.

    Represents the key encryption key (KEK) metadata associated with a process
    used to encrypt/decrypt secret information.

    When a secret is encrypted, in addition to the cypher text, the Barbican
    encryption process produces a KEK metadata object. The cypher text is
    stored via the EncryptedDatum model above, whereas the metadata is stored
    within this model. Decryption processes utilize this KEK metadata
    to decrypt the associated cypher text.

    Note that this model is intended to be agnostic to the specific means used
    to encrypt/decrypt the secret information, so please do not place vendor-
    specific attributes here.

    Note as well that each Project will have at most one 'active=True' KEKDatum
    instance at a time, representing the most recent KEK metadata instance
    to use for encryption processes performed on behalf of the Project.
    KEKDatum instances that are 'active=False' are associated to previously
    used encryption processes for the Project, that eventually should be
    rotated and deleted with the Project's active KEKDatum.
    """

    __tablename__ = 'kek_data'

    plugin_name = sa.Column(sa.String(255), nullable=False)
    kek_label = sa.Column(sa.String(255))

    project_id = sa.Column(
        sa.String(36),
        sa.ForeignKey('projects.id', name='kek_data_project_fk'),
        index=True,
        nullable=False)

    active = sa.Column(sa.Boolean, nullable=False, default=True)
    bind_completed = sa.Column(sa.Boolean, nullable=False, default=False)
    algorithm = sa.Column(sa.String(255))
    bit_length = sa.Column(sa.Integer)
    mode = sa.Column(sa.String(255))
    plugin_meta = sa.Column(sa.Text)

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        return {'algorithm': self.algorithm}


class Order(BASE, SoftDeleteMixIn, ModelBase):
    """Represents an Order in the datastore.

    Orders are requests for Barbican to generate secrets,
    ranging from symmetric, asymmetric keys to automated
    requests to Certificate Authorities to generate SSL
    certificates.
    """

    __tablename__ = 'orders'

    type = sa.Column(sa.String(255), nullable=False, default='key')
    project_id = sa.Column(
        sa.String(36),
        sa.ForeignKey('projects.id', name='orders_project_fk'),
        index=True,
        nullable=False)

    error_status_code = sa.Column(sa.String(16))
    error_reason = sa.Column(sa.String(ERROR_REASON_LENGTH))

    meta = sa.Column(JsonBlob(), nullable=True)

    secret_id = sa.Column(sa.String(36), sa.ForeignKey('secrets.id'),
                          index=True, nullable=True)
    container_id = sa.Column(sa.String(36), sa.ForeignKey('containers.id'),
                             index=True, nullable=True)
    sub_status = sa.Column(sa.String(SUB_STATUS_LENGTH), nullable=True)
    sub_status_message = sa.Column(sa.String(SUB_STATUS_MESSAGE_LENGTH),
                                   nullable=True)
    creator_id = sa.Column(sa.String(255))

    order_plugin_metadata = orm.relationship(
        "OrderPluginMetadatum",
        collection_class=col.attribute_mapped_collection('key'),
        backref="order",
        cascade="all, delete-orphan")

    order_barbican_metadata = orm.relationship(
        "OrderBarbicanMetadatum",
        collection_class=col.attribute_mapped_collection('key'),
        backref="order",
        cascade="all, delete-orphan")

    def __init__(self, parsed_request=None):
            """Creates a Order entity from a dict."""
            super(Order, self).__init__()
            if parsed_request:
                self.type = parsed_request.get('type')
                self.meta = parsed_request.get('meta')
                self.status = States.ACTIVE
                self.sub_status = parsed_request.get('sub_status')
                self.sub_status_message = parsed_request.get(
                    'sub_status_message')
                self.creator_id = parsed_request.get('creator_id')

    def set_error_reason_safely(self, error_reason_raw):
        """Ensure error reason does not raise database attribute exceptions."""
        self.error_reason = error_reason_raw[:ERROR_REASON_LENGTH]

    def set_sub_status_safely(self, sub_status_raw):
        """Ensure sub-status does not raise database attribute exceptions."""
        self.sub_status = sub_status_raw[:SUB_STATUS_LENGTH]

    def set_sub_status_message_safely(self, sub_status_message_raw):
        """Ensure status message doesn't raise database attrib. exceptions."""
        self.sub_status_message = sub_status_message_raw[
            :SUB_STATUS_MESSAGE_LENGTH
        ]

    def _do_delete_children(self, session):
        """Sub-class hook: delete children relationships."""
        for k, v in self.order_plugin_metadata.items():
            v.delete(session)
        for k, v in self.order_barbican_metadata.items():
            v.delete(session)

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        ret = {
            'type': self.type,
            'meta': self.meta,
            'order_id': self.id
        }
        if self.secret_id:
            ret['secret_id'] = self.secret_id
        if self.container_id:
            ret['container_id'] = self.container_id
        if self.error_status_code:
            ret['error_status_code'] = self.error_status_code
        if self.error_reason:
            ret['error_reason'] = self.error_reason
        if self.sub_status:
            ret['sub_status'] = self.sub_status
        if self.sub_status_message:
            ret['sub_status_message'] = self.sub_status_message
        if self.creator_id:
            ret['creator_id'] = self.creator_id
        return ret


class OrderPluginMetadatum(BASE, SoftDeleteMixIn, ModelBase):
    """Represents Order plugin metadatum for a single key-value pair.

    This entity is used to store plugin-specific metadata on behalf of an
    Order instance.
    """

    __tablename__ = "order_plugin_metadata"

    order_id = sa.Column(sa.String(36), sa.ForeignKey('orders.id'),
                         index=True, nullable=False)
    key = sa.Column(sa.String(255), nullable=False)
    value = sa.Column(sa.String(255), nullable=False)

    def __init__(self, key, value):
        super(OrderPluginMetadatum, self).__init__()

        msg = u._("Must supply non-None {0} argument "
                  "for OrderPluginMetadatum entry.")

        if key is None:
            raise exception.MissingArgumentError(msg.format("key"))
        self.key = key

        if value is None:
            raise exception.MissingArgumentError(msg.format("value"))
        self.value = value

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        return {'key': self.key,
                'value': self.value}


class OrderBarbicanMetadatum(BASE, SoftDeleteMixIn, ModelBase):
    """Represents Order barbican metadatum for a single key-value pair.

    This entity is used to store barbican-specific metadata on behalf of an
    Order instance.  This is data that is stored by the server to help
    process the order through its life cycle, but which is not in the original
    request.
    """

    __tablename__ = "order_barbican_metadata"

    order_id = sa.Column(sa.String(36), sa.ForeignKey('orders.id'),
                         index=True, nullable=False)
    key = sa.Column(sa.String(255), nullable=False)
    value = sa.Column(sa.Text, nullable=False)

    def __init__(self, key, value):
        super(OrderBarbicanMetadatum, self).__init__()

        msg = u._("Must supply non-None {0} argument "
                  "for OrderBarbicanMetadatum entry.")

        if key is None:
            raise exception.MissingArgumentError(msg.format("key"))
        self.key = key

        if value is None:
            raise exception.MissingArgumentError(msg.format("value"))
        self.value = value

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        return {'key': self.key,
                'value': self.value}


class OrderRetryTask(BASE, SoftDeleteMixIn, ModelBase):

    __tablename__ = "order_retry_tasks"
    __table_args__ = {"mysql_engine": "InnoDB"}
    __table_initialized__ = False

    id = sa.Column(
        sa.String(36), primary_key=True, default=utils.generate_uuid,
    )
    order_id = sa.Column(
        sa.String(36), sa.ForeignKey("orders.id"), index=True, nullable=False,
    )
    retry_task = sa.Column(sa.Text, nullable=False)
    retry_at = sa.Column(sa.DateTime, default=None, nullable=False)
    retry_args = sa.Column(JsonBlob(), nullable=False)
    retry_kwargs = sa.Column(JsonBlob(), nullable=False)
    retry_count = sa.Column(sa.Integer, nullable=False, default=0)


class Container(BASE, SoftDeleteMixIn, ModelBase):
    """Represents a Container for Secrets in the datastore.

    Containers store secret references. Containers are owned by Projects.
    Containers can be generic or have a predefined type. Predefined typed
    containers allow users to store structured key relationship
    inside Barbican.
    """

    __tablename__ = 'containers'

    name = sa.Column(sa.String(255))
    type = sa.Column(sa.Enum('generic', 'rsa', 'dsa', 'certificate',
                             name='container_types'))
    project_id = sa.Column(
        sa.String(36),
        sa.ForeignKey('projects.id', name='containers_project_fk'),
        index=True,
        nullable=False)
    consumers = sa.orm.relationship("ContainerConsumerMetadatum")
    creator_id = sa.Column(sa.String(255))

    def __init__(self, parsed_request=None):
        """Creates a Container entity from a dict."""
        super(Container, self).__init__()

        if parsed_request:
            self.name = parsed_request.get('name')
            self.type = parsed_request.get('type')
            self.status = States.ACTIVE
            self.creator_id = parsed_request.get('creator_id')

            secret_refs = parsed_request.get('secret_refs')
            if secret_refs:
                for secret_ref in parsed_request.get('secret_refs'):
                    container_secret = ContainerSecret()
                    container_secret.name = secret_ref.get('name')
                    # TODO(hgedikli) move this into a common location
                    # TODO(hgedikli) validate provided url
                    # TODO(hgedikli) parse out secret_id with regex
                    secret_id = secret_ref.get('secret_ref')
                    if secret_id.endswith('/'):
                        secret_id = secret_id.rsplit('/', 2)[1]
                    elif '/' in secret_id:
                        secret_id = secret_id.rsplit('/', 1)[1]
                    else:
                        secret_id = secret_id
                    container_secret.secret_id = secret_id
                    self.container_secrets.append(container_secret)

    def _do_delete_children(self, session):
        """Sub-class hook: delete children relationships."""
        for container_secret in self.container_secrets:
            session.delete(container_secret)

        for container_acl in self.container_acls:
            session.delete(container_acl)

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        return {'container_id': self.id,
                'name': self.name,
                'type': self.type,
                'creator_id': self.creator_id,
                'secret_refs': [
                    {
                        'secret_id': container_secret.secret_id,
                        'name': container_secret.name
                        if hasattr(container_secret, 'name') else None
                    } for container_secret in self.container_secrets],
                'consumers': [
                    {
                        'name': consumer.name,
                        'URL': consumer.URL
                    } for consumer in self.consumers if not consumer.deleted
                ]}


class ContainerConsumerMetadatum(BASE, SoftDeleteMixIn, ModelBase):
    """Stores Consumer Registrations for Containers in the datastore.

    Services can register interest in Containers. Services will provide a type
    and a URL for the object that is using the Container.
    """

    __tablename__ = 'container_consumer_metadata'

    container_id = sa.Column(sa.String(36), sa.ForeignKey('containers.id'),
                             index=True, nullable=False)
    project_id = sa.Column(sa.String(36), sa.ForeignKey('projects.id'),
                           index=True, nullable=True)
    name = sa.Column(sa.String(36))
    URL = sa.Column(sa.String(255))
    data_hash = sa.Column(sa.CHAR(64))

    __table_args__ = (
        sa.UniqueConstraint('data_hash',
                            name='_consumer_hashed_container_name_url_uc'),
        sa.Index('values_index', 'container_id', 'name', 'URL')
    )

    def __init__(self, container_id, project_id, parsed_request):
        """Registers a Consumer to a Container."""
        super(ContainerConsumerMetadatum, self).__init__()

        # TODO(john-wood-w) This class should really be immutable due to the
        # data_hash attribute.
        if container_id and parsed_request:
            self.container_id = container_id
            self.project_id = project_id
            self.name = parsed_request.get('name')
            self.URL = parsed_request.get('URL')
            hash_text = ''.join((self.container_id, self.name, self.URL))
            self.data_hash = hashlib.sha256(hash_text.
                                            encode('utf-8')).hexdigest()
            self.status = States.ACTIVE

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        return {'name': self.name,
                'URL': self.URL}


class TransportKey(BASE, SoftDeleteMixIn, ModelBase):
    """Transport Key model for wrapping secrets in transit

    Represents the transport key used for wrapping secrets in transit
    to/from clients when storing/retrieving secrets.
    """

    __tablename__ = 'transport_keys'

    plugin_name = sa.Column(sa.String(255), nullable=False)
    transport_key = sa.Column(sa.Text, nullable=False)

    def __init__(self, plugin_name, transport_key):
        """Creates transport key entity."""
        super(TransportKey, self).__init__()

        msg = u._("Must supply non-None {0} argument for TransportKey entry.")

        if plugin_name is None:
            raise exception.MissingArgumentError(msg.format("plugin_name"))

        self.plugin_name = plugin_name

        if transport_key is None:
            raise exception.MissingArgumentError(msg.format("transport_key"))

        self.transport_key = transport_key
        self.status = States.ACTIVE

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        return {'transport_key_id': self.id,
                'plugin_name': self.plugin_name}


class CertificateAuthority(BASE, ModelBase):
    """CertificateAuthority model to specify the CAs available to Barbican

    Represents the CAs available for certificate issuance to Barbican.
    """

    __tablename__ = 'certificate_authorities'

    plugin_name = sa.Column(sa.String(255), nullable=False)
    plugin_ca_id = sa.Column(sa.Text, nullable=False)
    expiration = sa.Column(sa.DateTime, default=None)
    creator_id = sa.Column(sa.String(255), nullable=True)
    project_id = sa.Column(
        sa.String(36),
        sa.ForeignKey('projects.id', name='cas_project_fk'),
        nullable=True)

    ca_meta = orm.relationship(
        'CertificateAuthorityMetadatum',
        collection_class=col.attribute_mapped_collection('key'),
        backref="ca",
        cascade="all, delete-orphan"
    )

    def __init__(self, parsed_ca_in):
        """Creates certificate authority entity."""
        super(CertificateAuthority, self).__init__()

        msg = u._("Must supply Non-None {0} argument "
                  "for CertificateAuthority entry.")

        parsed_ca = dict(parsed_ca_in)

        plugin_name = parsed_ca.pop('plugin_name', None)
        if plugin_name is None:
            raise exception.MissingArgumentError(msg.format("plugin_name"))
        self.plugin_name = plugin_name

        plugin_ca_id = parsed_ca.pop('plugin_ca_id', None)
        if plugin_ca_id is None:
            raise exception.MissingArgumentError(msg.format("plugin_ca_id"))
        self.plugin_ca_id = plugin_ca_id

        expiration = parsed_ca.pop('expiration', None)
        self.expiration = self._iso_to_datetime(expiration)

        creator_id = parsed_ca.pop('creator_id', None)
        if creator_id is not None:
            self.creator_id = creator_id

        project_id = parsed_ca.pop('project_id', None)
        if project_id is not None:
            self.project_id = project_id

        for key in parsed_ca:
            meta = CertificateAuthorityMetadatum(key, parsed_ca[key])
            self.ca_meta[key] = meta

        self.status = States.ACTIVE

    def _do_delete_children(self, session):
        """Sub-class hook: delete children relationships."""
        for k, v in self.ca_meta.items():
            v.delete(session)

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        if self.expiration:
            expiration = self.expiration.isoformat()
        else:
            expiration = None

        return {
            'ca_id': self.id,
            'plugin_name': self.plugin_name,
            'plugin_ca_id': self.plugin_ca_id,
            'expiration': expiration,
            'meta': [
                {
                    meta['key']: meta['value']
                } for key, meta in self.ca_meta.items()
            ]
        }


class CertificateAuthorityMetadatum(BASE, ModelBase):
    """Represents CA metadatum for a single key-value pair."""

    __tablename__ = "certificate_authority_metadata"

    key = sa.Column(sa.String(255), index=True, nullable=False)
    value = sa.Column(sa.Text, nullable=False)
    ca_id = sa.Column(
        sa.String(36), sa.ForeignKey('certificate_authorities.id'),
        index=True, nullable=False)

    __table_args__ = (sa.UniqueConstraint(
        'ca_id', 'key', name='_certificate_authority_metadatum_uc'),)

    def __init__(self, key, value):
        super(CertificateAuthorityMetadatum, self).__init__()

        msg = u._("Must supply non-None {0} argument "
                  "for CertificateAuthorityMetadatum entry.")

        if key is None:
            raise exception.MissingArgumentError(msg.format("key"))
        self.key = key

        if value is None:
            raise exception.MissingArgumentError(msg.format("value"))
        self.value = value

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        return {
            'key': self.key,
            'value': self.value
        }


class ProjectCertificateAuthority(BASE, ModelBase):
    """Stores CAs available for a project.

    Admins can define a set of CAs that are available for use in a particular
    project.  There can be multiple entries for any given project.
    """

    __tablename__ = 'project_certificate_authorities'

    project_id = sa.Column(sa.String(36),
                           sa.ForeignKey('projects.id'),
                           index=True,
                           nullable=False)

    ca_id = sa.Column(sa.String(36),
                      sa.ForeignKey('certificate_authorities.id'),
                      index=True,
                      nullable=False)

    ca = orm.relationship("CertificateAuthority", backref="project_cas")

    __table_args__ = (sa.UniqueConstraint(
        'project_id', 'ca_id', name='_project_certificate_authority_uc'),)

    def __init__(self, project_id, ca_id):
        """Registers a Consumer to a Container."""
        super(ProjectCertificateAuthority, self).__init__()

        msg = u._("Must supply non-None {0} argument "
                  "for ProjectCertificateAuthority entry.")

        if project_id is None:
            raise exception.MissingArgumentError(msg.format("project_id"))
        self.project_id = project_id

        if ca_id is None:
            raise exception.MissingArgumentError(msg.format("ca_id"))
        self.ca_id = ca_id

        self.status = States.ACTIVE

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        return {'project_id': self.project_id,
                'ca_id': self.ca_id}


class PreferredCertificateAuthority(BASE, ModelBase):
    """Stores preferred CAs for any project.

    Admins can define a set of CAs available for issuance requests for
    any project in the ProjectCertificateAuthority table..
    """

    __tablename__ = 'preferred_certificate_authorities'

    project_id = sa.Column(sa.String(36),
                           sa.ForeignKey('projects.id'),
                           index=True,
                           unique=True,
                           nullable=False)

    ca_id = sa.Column(sa.String(36),
                      sa.ForeignKey(
                          'certificate_authorities.id',
                          name='preferred_certificate_authorities_fk'),
                      index=True,
                      nullable=False)

    project = orm.relationship('Project',
                               backref=orm.backref('preferred_ca'),
                               uselist=False)

    ca = orm.relationship('CertificateAuthority',
                          backref=orm.backref('preferred_ca'))

    def __init__(self, project_id, ca_id):
        """Registers a Consumer to a Container."""
        super(PreferredCertificateAuthority, self).__init__()

        msg = u._("Must supply non-None {0} argument "
                  "for PreferredCertificateAuthority entry.")

        if project_id is None:
            raise exception.MissingArgumentError(msg.format("project_id"))
        self.project_id = project_id

        if ca_id is None:
            raise exception.MissingArgumentError(msg.format("ca_id"))
        self.ca_id = ca_id

        self.status = States.ACTIVE

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        return {'project_id': self.project_id,
                'ca_id': self.ca_id}


class SecretACL(BASE, ModelBase):
    """Stores Access Control List (ACL) for a secret.

    Class to define whitelist of user ids who are allowed specific operation
    on a secret. List of user ids is defined via SecretACLUser via
    acl_users association.
    Creator_only flag helps in making a secret private for
    non-admin project users who may have access otherwise.

    SecretACL deletes are not soft-deletes.
    """

    __tablename__ = 'secret_acls'

    secret_id = sa.Column(sa.String(36), sa.ForeignKey('secrets.id'),
                          index=True, nullable=False)

    operation = sa.Column(sa.String(255), nullable=False)

    project_access = sa.Column(sa.Boolean, nullable=False, default=True)

    secret = orm.relationship(
        'Secret', backref=orm.backref('secret_acls', lazy=False))

    acl_users = orm.relationship(
        'SecretACLUser', backref=orm.backref('secret_acl', lazy=False),
        cascade="all, delete-orphan")

    __table_args__ = (sa.UniqueConstraint(
        'secret_id', 'operation', name='_secret_acl_operation_uc'),)

    def __init__(self, secret_id, operation, project_access=None,
                 user_ids=None):
        """Creates secret ACL entity."""
        super(SecretACL, self).__init__()

        msg = u._("Must supply non-None {0} argument for SecretACL entry.")

        if secret_id is None:
            raise exception.MissingArgumentError(msg.format("secret_id"))
        self.secret_id = secret_id

        if operation is None:
            raise exception.MissingArgumentError(msg.format("operation"))
        self.operation = operation

        if project_access is not None:
            self.project_access = project_access
        self.status = States.ACTIVE
        if user_ids is not None and isinstance(user_ids, list):
            userids = set(user_ids)  # remove duplicate if any
            for user_id in userids:
                acl_user = SecretACLUser(self.id, user_id)
                self.acl_users.append(acl_user)

    def _do_delete_children(self, session):
        """Sub-class hook: delete children relationships."""
        for acl_user in self.acl_users:
            acl_user.delete(session)

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields.

        Adds non-deleted acl related users from relationship if there.
        """
        users = [acl_user.user_id for acl_user in self.acl_users
                 if not acl_user.deleted]
        fields = {'acl_id': self.id,
                  'secret_id': self.secret_id,
                  'operation': self.operation,
                  'project_access': self.project_access}
        if users:
            fields['users'] = users
        return fields


class ContainerACL(BASE, ModelBase):
    """Stores Access Control List (ACL) for a container.

    Class to define whitelist of user ids who are allowed specific operation
    on a container. List of user ids is defined in ContainerACLUser via
    acl_users association.
    Creator_only flag helps in making a container private for
    non-admin project users who may have access otherwise.

    ContainerACL deletes are not soft-deletes.
    """

    __tablename__ = 'container_acls'

    container_id = sa.Column(sa.String(36), sa.ForeignKey('containers.id'),
                             index=True, nullable=False)

    operation = sa.Column(sa.String(255), nullable=False)

    project_access = sa.Column(sa.Boolean, nullable=False, default=True)

    container = orm.relationship(
        'Container', backref=orm.backref('container_acls', lazy=False))

    acl_users = orm.relationship(
        'ContainerACLUser', backref=orm.backref('container_acl', lazy=False),
        cascade="all, delete-orphan")

    __table_args__ = (sa.UniqueConstraint(
        'container_id', 'operation', name='_container_acl_operation_uc'),)

    def __init__(self, container_id, operation, project_access=None,
                 user_ids=None):
        """Creates container ACL entity."""
        super(ContainerACL, self).__init__()

        msg = u._("Must supply non-None {0} argument for ContainerACL entry.")

        if container_id is None:
            raise exception.MissingArgumentError(msg.format("container_id"))
        self.container_id = container_id

        if operation is None:
            raise exception.MissingArgumentError(msg.format("operation"))
        self.operation = operation

        if project_access is not None:
            self.project_access = project_access
        self.status = States.ACTIVE

        if user_ids is not None and isinstance(user_ids, list):
            userids = set(user_ids)  # remove duplicate if any
            for user_id in userids:
                acl_user = ContainerACLUser(self.id, user_id)
                self.acl_users.append(acl_user)

    def _do_delete_children(self, session):
        """Sub-class hook: delete children relationships."""
        for acl_user in self.acl_users:
            acl_user.delete(session)

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields.

        Adds non-deleted acl related users from relationship if there.
        """
        users = [acl_user.user_id for acl_user in self.acl_users
                 if not acl_user.deleted]
        fields = {'acl_id': self.id,
                  'container_id': self.container_id,
                  'operation': self.operation,
                  'project_access': self.project_access}
        if users:
            fields['users'] = users
        return fields


class SecretACLUser(BASE, ModelBase):
    """Stores user id for a secret ACL.

    This class provides way to store list of users associated with a
    specific ACL operation.

    SecretACLUser deletes are not soft-deletes.
    """

    __tablename__ = 'secret_acl_users'

    acl_id = sa.Column(sa.String(36), sa.ForeignKey('secret_acls.id'),
                       index=True, nullable=False)

    user_id = sa.Column(sa.String(255), nullable=False)

    __table_args__ = (sa.UniqueConstraint(
        'acl_id', 'user_id', name='_secret_acl_user_uc'),)

    def __init__(self, acl_id, user_id):
        """Creates secret ACL user entity."""
        super(SecretACLUser, self).__init__()

        msg = u._("Must supply non-None {0} argument for SecretACLUser entry.")

        self.acl_id = acl_id
        if user_id is None:
            raise exception.MissingArgumentError(msg.format("user_id"))
        self.user_id = user_id
        self.status = States.ACTIVE

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        return {'acl_id': self.acl_id,
                'user_id': self.user_id}


class ContainerACLUser(BASE, ModelBase):
    """Stores user id for a container ACL.

    This class provides way to store list of users associated with a
    specific ACL operation.

    ContainerACLUser deletes are not soft-deletes.
    """

    __tablename__ = 'container_acl_users'

    acl_id = sa.Column(sa.String(36), sa.ForeignKey('container_acls.id'),
                       index=True, nullable=False)

    user_id = sa.Column(sa.String(255), nullable=False)

    __table_args__ = (sa.UniqueConstraint(
        'acl_id', 'user_id', name='_container_acl_user_uc'),)

    def __init__(self, acl_id, user_id):
        """Creates container ACL user entity."""
        super(ContainerACLUser, self).__init__()

        msg = u._("Must supply non-None {0} argument for ContainerACLUser "
                  "entry.")

        self.acl_id = acl_id
        if user_id is None:
            raise exception.MissingArgumentError(msg.format("user_id"))
        self.user_id = user_id
        self.status = States.ACTIVE

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        return {'acl_id': self.acl_id,
                'user_id': self.user_id}


class ProjectQuotas(BASE, ModelBase):
    """Stores Project Quotas.

    Class to define project specific resource quotas.

    Project quota deletes are not soft-deletes.
    """

    __tablename__ = 'project_quotas'

    project_id = sa.Column(
        sa.String(36),
        sa.ForeignKey('projects.id', name='project_quotas_fk'),
        index=True,
        nullable=False)
    secrets = sa.Column(sa.Integer, nullable=True)
    orders = sa.Column(sa.Integer, nullable=True)
    containers = sa.Column(sa.Integer, nullable=True)
    consumers = sa.Column(sa.Integer, nullable=True)
    cas = sa.Column(sa.Integer, nullable=True)

    def __init__(self, project_id=None, parsed_project_quotas=None):
        """Creates Project Quotas entity from a project and a dict.

        :param project_id: the internal id of the project with quotas
        :param parsed_project_quotas: a dict with the keys matching the
        resources for which quotas are to be set, and the values containing
        the quota value to be set for this project and that resource.
        :return: None
        """
        super(ProjectQuotas, self).__init__()

        msg = u._("Must supply non-None {0} argument for ProjectQuotas entry.")

        if project_id is None:
            raise exception.MissingArgumentError(msg.format("project_id"))
        self.project_id = project_id

        if parsed_project_quotas is None:
            self.secrets = None
            self.orders = None
            self.containers = None
            self.consumers = None
            self.cas = None
        else:
            self.secrets = parsed_project_quotas.get('secrets')
            self.orders = parsed_project_quotas.get('orders')
            self.containers = parsed_project_quotas.get('containers')
            self.consumers = parsed_project_quotas.get('consumers')
            self.cas = parsed_project_quotas.get('cas')

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        ret = {
            'project_id': self.project_id,
        }
        if self.secrets:
            ret['secrets'] = self.secrets
        if self.orders:
            ret['orders'] = self.orders
        if self.containers:
            ret['containers'] = self.containers
        if self.consumers:
            ret['consumers'] = self.consumers
        if self.cas:
            ret['cas'] = self.cas
        return ret


class SecretStores(BASE, ModelBase):
    """List of secret stores defined via service configuration.

    This class provides a list of secret stores entities with their respective
    secret store plugin and crypto plugin names.

    SecretStores deletes are NOT soft-deletes.
    """

    __tablename__ = 'secret_stores'

    store_plugin = sa.Column(sa.String(255), nullable=False)
    crypto_plugin = sa.Column(sa.String(255), nullable=True)
    global_default = sa.Column(sa.Boolean, nullable=False, default=False)
    name = sa.Column(sa.String(255), nullable=False)

    __table_args__ = (sa.UniqueConstraint(
        'store_plugin', 'crypto_plugin',
        name='_secret_stores_plugin_names_uc'),
        sa.UniqueConstraint('name', name='_secret_stores_name_uc'),)

    def __init__(self, name, store_plugin, crypto_plugin=None,
                 global_default=None):
        """Creates secret store entity."""
        super(SecretStores, self).__init__()

        msg = u._("Must supply non-Blank {0} argument for SecretStores entry.")

        if not name:
            raise exception.MissingArgumentError(msg.format("name"))
        if not store_plugin:
            raise exception.MissingArgumentError(msg.format("store_plugin"))

        self.store_plugin = store_plugin
        self.name = name
        self.crypto_plugin = crypto_plugin
        if global_default is not None:
            self.global_default = global_default

        self.status = States.ACTIVE

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        return {'secret_store_id': self.id,
                'store_plugin': self.store_plugin,
                'crypto_plugin': self.crypto_plugin,
                'global_default': self.global_default,
                'name': self.name}


class ProjectSecretStore(BASE, ModelBase):
    """Stores secret store to be used for new project secrets.

    This class maintains secret store and project mapping so that new project
    secret entries uses it as plugin backend.

    ProjectSecretStores deletes are NOT soft-deletes.
    """

    __tablename__ = 'project_secret_store'

    secret_store_id = sa.Column(sa.String(36),
                                sa.ForeignKey('secret_stores.id'),
                                index=True,
                                nullable=False)
    project_id = sa.Column(sa.String(36),
                           sa.ForeignKey('projects.id'),
                           index=True,
                           nullable=False)

    secret_store = orm.relationship("SecretStores", backref="project_store")
    project = orm.relationship('Project',
                               backref=orm.backref('preferred_secret_store'))

    __table_args__ = (sa.UniqueConstraint(
        'project_id', name='_project_secret_store_project_uc'),)

    def __init__(self, project_id, secret_store_id):
        """Creates project secret store mapping entity."""
        super(ProjectSecretStore, self).__init__()

        msg = u._("Must supply non-None {0} argument for ProjectSecretStore "
                  " entry.")

        if not project_id:
            raise exception.MissingArgumentError(msg.format("project_id"))
        self.project_id = project_id
        if not secret_store_id:
            raise exception.MissingArgumentError(msg.format("secret_store_id"))
        self.secret_store_id = secret_store_id

        self.status = States.ACTIVE

    def _do_extra_dict_fields(self):
        """Sub-class hook method: return dict of fields."""
        return {'secret_store_id': self.secret_store_id,
                'project_id': self.project_id}
