"""
.. module: lemur.schemas
    :platform: unix
    :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
    :license: Apache, see LICENSE for more details.

.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>

"""
from sqlalchemy.orm.exc import NoResultFound

from marshmallow import fields, post_load, pre_load, post_dump
from marshmallow.exceptions import ValidationError

from lemur.common import validators
from lemur.common.schema import LemurSchema, LemurInputSchema, LemurOutputSchema
from lemur.common.fields import KeyUsageExtension, ExtendedKeyUsageExtension, BasicConstraintsExtension, SubjectAlternativeNameExtension

from lemur.plugins import plugins
from lemur.plugins.utils import get_plugin_option
from lemur.roles.models import Role
from lemur.users.models import User
from lemur.authorities.models import Authority
from lemur.certificates.models import Certificate
from lemur.destinations.models import Destination
from lemur.notifications.models import Notification


def validate_options(options):
    """
    Ensures that the plugin options are valid.
    :param options:
    :return:
    """
    interval = get_plugin_option('interval', options)
    unit = get_plugin_option('unit', options)

    if interval == 'month':
        unit *= 30

    elif interval == 'week':
        unit *= 7

    if unit > 90:
        raise ValidationError('Notification cannot be more than 90 days into the future.')


def get_object_attribute(data, many=False):
    if many:
        ids = [d.get('id') for d in data]
        names = [d.get('name') for d in data]

        if None in ids:
            if None in names:
                raise ValidationError('Associated object require a name or id.')
            else:
                return 'name'
        return 'id'
    else:
        if data.get('id'):
            return 'id'
        elif data.get('name'):
            return 'name'
        else:
            raise ValidationError('Associated object require a name or id.')


def fetch_objects(model, data, many=False):
    attr = get_object_attribute(data, many=many)

    if many:
        values = [v[attr] for v in data]
        items = model.query.filter(getattr(model, attr).in_(values)).all()
        found = [getattr(i, attr) for i in items]
        diff = set(values).symmetric_difference(set(found))

        if diff:
            raise ValidationError('Unable to locate {model} with {attr} {diff}'.format(
                model=model,
                attr=attr,
                diff=",".join(list(diff))))

        return items

    else:
        try:
            return model.query.filter(getattr(model, attr) == data[attr]).one()
        except NoResultFound:
            raise ValidationError('Unable to find {model} with {attr}: {data}'.format(
                model=model,
                attr=attr,
                data=data[attr]))


class AssociatedAuthoritySchema(LemurInputSchema):
    id = fields.Int()
    name = fields.String()

    @post_load
    def get_object(self, data, many=False):
        return fetch_objects(Authority, data, many=many)


class AssociatedRoleSchema(LemurInputSchema):
    id = fields.Int()
    name = fields.String()

    @post_load
    def get_object(self, data, many=False):
        return fetch_objects(Role, data, many=many)


class AssociatedDestinationSchema(LemurInputSchema):
    id = fields.Int()
    name = fields.String()

    @post_load
    def get_object(self, data, many=False):
        return fetch_objects(Destination, data, many=many)


class AssociatedNotificationSchema(LemurInputSchema):
    id = fields.Int()
    name = fields.String()

    @post_load
    def get_object(self, data, many=False):
        return fetch_objects(Notification, data, many=many)


class AssociatedCertificateSchema(LemurInputSchema):
    id = fields.Int()
    name = fields.String()

    @post_load
    def get_object(self, data, many=False):
        return fetch_objects(Certificate, data, many=many)


class AssociatedUserSchema(LemurInputSchema):
    id = fields.Int()
    name = fields.String()

    @post_load
    def get_object(self, data, many=False):
        return fetch_objects(User, data, many=many)


class PluginInputSchema(LemurInputSchema):
    plugin_options = fields.List(fields.Dict(), validate=validate_options)
    slug = fields.String(required=True)
    title = fields.String()
    description = fields.String()

    @post_load
    def get_object(self, data, many=False):
        try:
            data['plugin_object'] = plugins.get(data['slug'])
            return data
        except Exception:
            raise ValidationError('Unable to find plugin: {0}'.format(data['slug']))


class PluginOutputSchema(LemurOutputSchema):
    id = fields.Integer()
    label = fields.String()
    description = fields.String()
    active = fields.Boolean()
    options = fields.List(fields.Dict(), dump_to='pluginOptions')
    slug = fields.String()
    title = fields.String()


plugins_output_schema = PluginOutputSchema(many=True)
plugin_output_schema = PluginOutputSchema


class BaseExtensionSchema(LemurSchema):
    @pre_load(pass_many=True)
    def preprocess(self, data, many):
        return self.under(data, many=many)

    @post_dump(pass_many=True)
    def post_process(self, data, many):
        if data:
            data = self.camel(data, many=many)
        return data


class AuthorityKeyIdentifierSchema(BaseExtensionSchema):
    use_key_identifier = fields.Boolean()
    use_authority_cert = fields.Boolean()


class CertificateInfoAccessSchema(BaseExtensionSchema):
    include_aia = fields.Boolean()

    @post_dump
    def handle_keys(self, data):
        return {'includeAIA': data['include_aia']}


class SubjectKeyIdentifierSchema(BaseExtensionSchema):
    include_ski = fields.Boolean()

    @post_dump
    def handle_keys(self, data):
        return {'includeSKI': data['include_ski']}


class CustomOIDSchema(BaseExtensionSchema):
    oid = fields.String()
    encoding = fields.String(validate=validators.encoding)
    value = fields.String()
    is_critical = fields.Boolean()


class NamesSchema(BaseExtensionSchema):
    names = SubjectAlternativeNameExtension()


class ExtensionSchema(BaseExtensionSchema):
    basic_constraints = BasicConstraintsExtension(missing={'ca': False})
    key_usage = KeyUsageExtension()
    extended_key_usage = ExtendedKeyUsageExtension()
    subject_key_identifier = fields.Nested(SubjectKeyIdentifierSchema)
    sub_alt_names = fields.Nested(NamesSchema)
    authority_key_identifier = fields.Nested(AuthorityKeyIdentifierSchema)
    certificate_info_access = fields.Nested(CertificateInfoAccessSchema)
    # FIXME: Convert custom OIDs to a custom field in fields.py like other Extensions
    # FIXME: Remove support in UI for Critical custom extensions https://github.com/Netflix/lemur/issues/665
    custom = fields.List(fields.Nested(CustomOIDSchema))


class EndpointNestedOutputSchema(LemurOutputSchema):
    __envelope__ = False
    id = fields.Integer()
    description = fields.String()
    name = fields.String()
    dnsname = fields.String()
    owner = fields.Email()
    type = fields.String()
    active = fields.Boolean()

lemur/schemas.py
"""
.. module: lemur.schemas
    :platform: unix
    :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
    :license: Apache, see LICENSE for more details.

.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>

"""
from sqlalchemy.orm.exc import NoResultFound

from marshmallow import fields, post_load, pre_load, post_dump
from marshmallow.exceptions import ValidationError

from lemur.common import validators
from lemur.common.schema import LemurSchema, LemurInputSchema, LemurOutputSchema
from lemur.common.fields import KeyUsageExtension, ExtendedKeyUsageExtension, BasicConstraintsExtension, SubjectAlternativeNameExtension

from lemur.plugins import plugins
from lemur.plugins.utils import get_plugin_option
from lemur.roles.models import Role
from lemur.users.models import User
from lemur.authorities.models import Authority
from lemur.certificates.models import Certificate
from lemur.destinations.models import Destination
from lemur.notifications.models import Notification


def validate_options(options):
    """
    Ensures that the plugin options are valid.
    :param options:
    :return:
    """
    interval = get_plugin_option('interval', options)
    unit = get_plugin_option('unit', options)

    if not interval and not unit:
        return

    if interval == 'month':
        unit *= 30

    elif interval == 'week':
        unit *= 7

    if unit > 90:
        raise ValidationError('Notification cannot be more than 90 days into the future.')


def get_object_attribute(data, many=False):
    if many:
        ids = [d.get('id') for d in data]
        names = [d.get('name') for d in data]

        if None in ids:
            if None in names:
                raise ValidationError('Associated object require a name or id.')
            else:
                return 'name'
        return 'id'
    else:
        if data.get('id'):
            return 'id'
        elif data.get('name'):
            return 'name'
        else:
            raise ValidationError('Associated object require a name or id.')


def fetch_objects(model, data, many=False):
    attr = get_object_attribute(data, many=many)

    if many:
        values = [v[attr] for v in data]
        items = model.query.filter(getattr(model, attr).in_(values)).all()
        found = [getattr(i, attr) for i in items]
        diff = set(values).symmetric_difference(set(found))

        if diff:
            raise ValidationError('Unable to locate {model} with {attr} {diff}'.format(
                model=model,
                attr=attr,
                diff=",".join(list(diff))))

        return items

    else:
        try:
            return model.query.filter(getattr(model, attr) == data[attr]).one()
        except NoResultFound:
            raise ValidationError('Unable to find {model} with {attr}: {data}'.format(
                model=model,
                attr=attr,
                data=data[attr]))


class AssociatedAuthoritySchema(LemurInputSchema):
    id = fields.Int()
    name = fields.String()

    @post_load
    def get_object(self, data, many=False):
        return fetch_objects(Authority, data, many=many)


class AssociatedRoleSchema(LemurInputSchema):
    id = fields.Int()
    name = fields.String()

    @post_load
    def get_object(self, data, many=False):
        return fetch_objects(Role, data, many=many)


class AssociatedDestinationSchema(LemurInputSchema):
    id = fields.Int()
    name = fields.String()

    @post_load
    def get_object(self, data, many=False):
        return fetch_objects(Destination, data, many=many)


class AssociatedNotificationSchema(LemurInputSchema):
    id = fields.Int()
    name = fields.String()

    @post_load
    def get_object(self, data, many=False):
        return fetch_objects(Notification, data, many=many)


class AssociatedCertificateSchema(LemurInputSchema):
    id = fields.Int()
    name = fields.String()

    @post_load
    def get_object(self, data, many=False):
        return fetch_objects(Certificate, data, many=many)


class AssociatedUserSchema(LemurInputSchema):
    id = fields.Int()
    name = fields.String()

    @post_load
    def get_object(self, data, many=False):
        return fetch_objects(User, data, many=many)


class PluginInputSchema(LemurInputSchema):
    plugin_options = fields.List(fields.Dict(), validate=validate_options)
    slug = fields.String(required=True)
    title = fields.String()
    description = fields.String()

    @post_load
    def get_object(self, data, many=False):
        try:
            data['plugin_object'] = plugins.get(data['slug'])
            return data
        except Exception:
            raise ValidationError('Unable to find plugin: {0}'.format(data['slug']))


class PluginOutputSchema(LemurOutputSchema):
    id = fields.Integer()
    label = fields.String()
    description = fields.String()
    active = fields.Boolean()
    options = fields.List(fields.Dict(), dump_to='pluginOptions')
    slug = fields.String()
    title = fields.String()


plugins_output_schema = PluginOutputSchema(many=True)
plugin_output_schema = PluginOutputSchema


class BaseExtensionSchema(LemurSchema):
    @pre_load(pass_many=True)
    def preprocess(self, data, many):
        return self.under(data, many=many)

    @post_dump(pass_many=True)
    def post_process(self, data, many):
        if data:
            data = self.camel(data, many=many)
        return data


class AuthorityKeyIdentifierSchema(BaseExtensionSchema):
    use_key_identifier = fields.Boolean()
    use_authority_cert = fields.Boolean()


class CertificateInfoAccessSchema(BaseExtensionSchema):
    include_aia = fields.Boolean()

    @post_dump
    def handle_keys(self, data):
        return {'includeAIA': data['include_aia']}


class SubjectKeyIdentifierSchema(BaseExtensionSchema):
    include_ski = fields.Boolean()

    @post_dump
    def handle_keys(self, data):
        return {'includeSKI': data['include_ski']}


class CustomOIDSchema(BaseExtensionSchema):
    oid = fields.String()
    encoding = fields.String(validate=validators.encoding)
    value = fields.String()
    is_critical = fields.Boolean()


class NamesSchema(BaseExtensionSchema):
    names = SubjectAlternativeNameExtension()


class ExtensionSchema(BaseExtensionSchema):
    basic_constraints = BasicConstraintsExtension(missing={'ca': False})
    key_usage = KeyUsageExtension()
    extended_key_usage = ExtendedKeyUsageExtension()
    subject_key_identifier = fields.Nested(SubjectKeyIdentifierSchema)
    sub_alt_names = fields.Nested(NamesSchema)
    authority_key_identifier = fields.Nested(AuthorityKeyIdentifierSchema)
    certificate_info_access = fields.Nested(CertificateInfoAccessSchema)
    # FIXME: Convert custom OIDs to a custom field in fields.py like other Extensions
    # FIXME: Remove support in UI for Critical custom extensions https://github.com/Netflix/lemur/issues/665
    custom = fields.List(fields.Nested(CustomOIDSchema))


class EndpointNestedOutputSchema(LemurOutputSchema):
    __envelope__ = False
    id = fields.Integer()
    description = fields.String()
    name = fields.String()
    dnsname = fields.String()
    owner = fields.Email()
    type = fields.String()
    active = fields.Boolean()

lemur/schemas.py
diff --git a/lemur/schemas.py b/lemur/schemas.py
index 7a5e62cccb..bbc5fd0acb 100644
--- a/lemur/schemas.py
+++ b/lemur/schemas.py
@@ -35,6 +35,9 @@ def validate_options(options):
     interval = get_plugin_option('interval', options)
     unit = get_plugin_option('unit', options)
 
+    if not interval and not unit:
+        return
+
     if interval == 'month':
         unit *= 30
 
diff --git a/lemur/tests/test_authorities.py b/lemur/tests/test_authorities.py
index 164f033b76..6b2f86b2db 100644
--- a/lemur/tests/test_authorities.py
+++ b/lemur/tests/test_authorities.py
@@ -13,7 +13,7 @@ def test_authority_input_schema(client, role):
         'owner': 'jim@example.com',
         'description': 'An example authority.',
         'commonName': 'AnExampleAuthority',
-        'pluginName': {'slug': 'verisign-issuer'},
+        'plugin': {'slug': 'verisign-issuer', 'plugin_options': [{'name': 'test', 'value': 'blah'}]},
         'type': 'root',
         'signingAlgorithm': 'sha256WithRSA',
         'keyType': 'RSA2048',

Getting error while creating authority with lemur_cryptography plugin
I added a comment to the offending commit code here: https://github.com/Netflix/lemur/commit/d53f64890cb656765bc1c18f4b8442ee3a592f47

Upon creating an authority certificate with the lemur_cryptography plugin here, I get an error because unit is None and cannot be compared to 90. Is it reasonable to make an validation assumption that unit should be 0 if undefined?

I haven't quite been able to trace how this validation function is called, so I'm not entirely clear on what it is doing or why it's involved in the creation of an authority certificate, but not in the creation of a certificate signed by an authority.


Here's the error I get upon submitting an authority for creation.
```
2017-03-07 01:44:41,352 ERROR: Exception on /api/1/authorities [POST] [in /home/lemur/venv/lib/python3.4/site-packages/flask/app.py:1560]
Traceback (most recent call last):
  File "/home/lemur/venv/lib/python3.4/site-packages/flask/app.py", line 1612, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/lemur/venv/lib/python3.4/site-packages/flask/app.py", line 1598, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/home/lemur/venv/lib/python3.4/site-packages/flask_restful/__init__.py", line 477, in wrapper
    resp = resource(*args, **kwargs)
  File "/home/lemur/venv/lib/python3.4/site-packages/flask/views.py", line 84, in view
    return self.dispatch_request(*args, **kwargs)
  File "/home/lemur/venv/lib/python3.4/site-packages/flask_restful/__init__.py", line 587, in dispatch_request
    resp = meth(*args, **kwargs)
  File "/home/lemur/app/lemur/auth/service.py", line 110, in decorated_function
    return f(*args, **kwargs)
  File "/home/lemur/app/lemur/common/schema.py", line 150, in decorated_function
    data, errors = input_schema.load(request_data)
  File "/home/lemur/venv/lib/python3.4/site-packages/marshmallow/schema.py", line 578, in load
    result, errors = self._do_load(data, many, partial=partial, postprocess=True)
  File "/home/lemur/venv/lib/python3.4/site-packages/marshmallow/schema.py", line 658, in _do_load
    index_errors=self.opts.index_errors,
  File "/home/lemur/venv/lib/python3.4/site-packages/marshmallow/marshalling.py", line 295, in deserialize
    index=(index if index_errors else None)
  File "/home/lemur/venv/lib/python3.4/site-packages/marshmallow/marshalling.py", line 68, in call_and_store
    value = getter_func(data)
  File "/home/lemur/venv/lib/python3.4/site-packages/marshmallow/marshalling.py", line 288, in <lambda>
    data
  File "/home/lemur/venv/lib/python3.4/site-packages/marshmallow/fields.py", line 265, in deserialize
    output = self._deserialize(value, attr, data)
  File "/home/lemur/venv/lib/python3.4/site-packages/marshmallow/fields.py", line 465, in _deserialize
    data, errors = self.schema.load(value)
  File "/home/lemur/venv/lib/python3.4/site-packages/marshmallow/schema.py", line 578, in load
    result, errors = self._do_load(data, many, partial=partial, postprocess=True)
  File "/home/lemur/venv/lib/python3.4/site-packages/marshmallow/schema.py", line 658, in _do_load
    index_errors=self.opts.index_errors,
  File "/home/lemur/venv/lib/python3.4/site-packages/marshmallow/marshalling.py", line 295, in deserialize
    index=(index if index_errors else None)
  File "/home/lemur/venv/lib/python3.4/site-packages/marshmallow/marshalling.py", line 68, in call_and_store
    value = getter_func(data)
  File "/home/lemur/venv/lib/python3.4/site-packages/marshmallow/marshalling.py", line 288, in <lambda>
    data
  File "/home/lemur/venv/lib/python3.4/site-packages/marshmallow/fields.py", line 266, in deserialize
    self._validate(output)
  File "/home/lemur/venv/lib/python3.4/site-packages/marshmallow/fields.py", line 196, in _validate
    r = validator(value)
  File "/home/lemur/app/lemur/schemas.py", line 44, in validate_options
    if unit > 90:
TypeError: unorderable types: NoneType() > int()
```
