"""Tests related to the authorization of api calls.

NOTE: while all of these are conceptually authorization related, some illegal
operations will raise exceptions other than AuthorizationError. This usually
happens when the operation is illegal *in principle*, and would not be fixed by
authenticating as someone else. We were already raising exceptions in
these cases before actually adding authentication and authorization to
the mix. They are still tested here, since they are important for security.
"""

import pytest
import unittest
import json
from hil import api, config, model, deferred
from hil.auth import get_auth_backend
from hil.errors import AuthorizationError, BadArgumentError, \
    ProjectMismatchError, BlockedError
from hil.test_common import config_testsuite, config_merge, fresh_database, \
    with_request_context, additional_db, fail_on_log_warnings, server_init

MOCK_OBM_API_NAME = 'http://schema.massopencloud.org/haas/v0/obm/mock'
MOCK_SWITCH_API_NAME = 'http://schema.massopencloud.org/haas/v0/switches/mock'


def auth_call_test(fn, error, admin, project, args, kwargs={}):
    """Test the authorization properties of an api call.

    Parmeters:

        * `fn` - the api function to call
        * `error` - The error that should be raised. None if no error should
                    be raised.
        * `admin` - Whether the request should have admin access.
        * `project` - The name of the project the request should be
                      authenticated as. Can be None if `admin` is True.
        * `args` - the arguments (as a list) to `fn`.
    """
    auth_backend = get_auth_backend()
    auth_backend.set_admin(admin)
    if not admin:
        project = model.Project.query \
            .filter_by(label=project).one()
        auth_backend.set_project(project)

    if error is None:
        fn(*args, **kwargs)
    else:
        with pytest.raises(error):
            fn(*args, **kwargs)


additional_db = pytest.fixture(additional_db)
fail_on_log_warnings = pytest.fixture(autouse=True)(fail_on_log_warnings)


@pytest.fixture
def configure():
    "Configure HIL"
    config_testsuite()
    config_merge({
        'extensions': {
            'hil.ext.auth.mock': '',

            # This extension is enabled by default in the tests, so we need to
            # disable it explicitly:
            'hil.ext.auth.null': None,
            'hil.ext.switches.mock': '',
            'hil.ext.obm.mock': ''
        },
    })
    config.load_extensions()


fresh_database = pytest.fixture(fresh_database)
with_request_context = pytest.yield_fixture(with_request_context)
server_init = pytest.fixture(server_init)


pytestmark = pytest.mark.usefixtures('configure',
                                     'fresh_database',
                                     'additional_db',
                                     'server_init',
                                     'with_request_context')


# We have a *lot* of different parameters with which we're going to invoke
# `test_auth_call`, below. Rather than passing one giant list to the decorator
# in-line, we construct it in stages here:


auth_call_params = [
    #
    # network_create
    #

    # Legal Cases:
    # Admin creates a public network internal to HIL.
    dict(fn=api.network_create,
         error=None,
         admin=True,
         project=None,
         args=['pub', 'admin', '', '']),

    # Admin creates a public network with an existing net_id.
    dict(fn=api.network_create,
         error=None,
         admin=True,
         project=None,
         args=['pub', 'admin', '', 'some-id']),

    # Admin creates a provider network for some project.
    dict(fn=api.network_create,
         error=None,
         admin=True,
         project=None,
         args=['pxe', 'admin', 'runway', 'some-id']),

    # Admin creates an allocated network on behalf of a project.
    dict(fn=api.network_create,
         error=None,
         admin=True,
         project=None,
         args=['pxe', 'admin', 'runway', '']),

    # Project creates a private network for themselves.
    dict(fn=api.network_create,
         error=None,
         admin=False,
         project='runway',
         args=['pxe', 'runway', 'runway', '']),

    # Illegal Cases:
    # Project tries to create a private network for another project.
    dict(fn=api.network_create,
         error=AuthorizationError,
         admin=False,
         project='runway',
         args=['pxe', 'manhattan', 'manhattan', '']),

    # Project tries to specify a net_id.
    dict(fn=api.network_create,
         error=BadArgumentError,
         admin=False,
         project='runway',
         args=['pxe', 'runway', 'runway', 'some-id']),

    # Project tries to create a public network.
    dict(fn=api.network_create,
         error=AuthorizationError,
         admin=False,
         project='runway',
         args=['pub', 'admin', '', '']),

    # Project tries to set owner to 'admin' on its own network:
    dict(fn=api.network_create,
         error=AuthorizationError,
         admin=False,
         project='runway',
         args=['pxe', 'admin', 'runway', '']),
]

#
# network_delete
#

# Legal Cases
# Admin should be able to delete any network.
for net in [
    'stock_int_pub',
    'stock_ext_pub',
    'runway_pxe',
    'runway_provider',
    'manhattan_pxe',
    'manhattan_provider',
]:
    auth_call_params.append(dict(
        fn=api.network_delete,
        error=None,
        admin=True,
        project=None,
        args=[net]
    ))

# Project should be able to delete it's own (created) network.
auth_call_params.append(dict(
    fn=api.network_delete,
    error=None,
    admin=False,
    project='runway',
    args=['runway_pxe']
))

# Illegal Cases:
# Project should not be able to delete admin-created networks.
for net in [
    'stock_int_pub',
    'stock_ext_pub',
    'runway_provider',  # ... including networks created for said project.
    'manhattan_runway_provider',
]:
    auth_call_params.append(dict(
        fn=api.network_delete,
        error=AuthorizationError,
        admin=False,
        project='runway',
        args=[net]
    ))

# Project should not be able to delete networks created by other projects.
for net in [
    'manhattan_pxe',
    'manhattan_provider',
    'manhattan_runway_pxe',
]:
    auth_call_params.append(dict(
        fn=api.network_delete,
        error=AuthorizationError,
        admin=False,
        project='runway',
        args=[net]))

#
# network_grant_project_access
#

# Legal cases
# admin should be able to add  access to a network
# for any project (that does not already have access)
for (project, net) in [
    ('manhattan', 'runway_provider'),
    ('runway', 'manhattan_provider'),
    ('runway', 'manhattan_pxe'),
    ('manhattan', 'runway_pxe'),
]:
    auth_call_params.append(dict(
        fn=api.network_grant_project_access,
        error=None,
        admin=True,
        project=None,
        args=[project, net]
    ))

# project that is the owner of the network should
# be able to add access for other projects
for (project, project_access, net) in [
    ('manhattan', 'runway', 'manhattan_pxe'),
    ('runway', 'manhattan', 'runway_pxe'),
]:
    auth_call_params.append(dict(
        fn=api.network_grant_project_access,
        error=None,
        admin=False,
        project=project,
        args=[project_access, net]
    ))


# Illegal cases:
# Projects other than the network owner should not be ble to grant access
for (project, project_access, net) in [
    ('manhattan', 'manhattan', 'runway_pxe'),
    ('runway', 'runway', 'manhattan_pxe'),
]:
    auth_call_params.append(dict(
        fn=api.network_grant_project_access,
        error=AuthorizationError,
        admin=False,
        project=project,
        args=[project_access, net]
    ))

#
# network_revoke_project_access
#

# Legal cases
# admin should be able to remove access to a network
# for any project (that was not the owner of the network)
# admin created networks with all the access removed will become
# public networks
for (project, net) in [
    ('runway', 'runway_provider'),
    ('runway', 'manhattan_runway_pxe'),
    ('manhattan', 'manhattan_provider'),
    ('runway', 'manhattan_runway_provider'),
    ('manhattan', 'manhattan_runway_provider'),
]:
    auth_call_params.append(dict(
        fn=api.network_revoke_project_access,
        error=None,
        admin=True,
        project=None,
        args=[project, net]
    ))

# project that is the owner of the network should
# be able to remove the access of other projects
# projects should be able to remove their own access
for (project, project_access, net) in [
    ('manhattan', 'runway', 'manhattan_runway_pxe'),
    ('runway', 'runway', 'manhattan_runway_pxe'),
    ('manhattan', 'manhattan', 'manhattan_runway_provider'),
    ('runway', 'runway', 'manhattan_runway_provider'),
]:
    auth_call_params.append(dict(
        fn=api.network_revoke_project_access,
        error=None,
        admin=False,
        project=project,
        args=[project_access, net]
    ))


# Illegal cases:
# Projects other than the network owner or the project
# itself should  not be able to remove access of other projects
for (project, project_access, net) in [
    ('manhattan', 'runway', 'manhattan_runway_provider'),
]:
    auth_call_params.append(dict(
        fn=api.network_revoke_project_access,
        error=AuthorizationError,
        admin=False,
        project=project,
        args=[project_access, net]
    ))

#
# list_network_attachments
#

# Legal cases
# Admin should be able to list attachments for public network:
for net in ('stock_int_pub', 'stock_ext_pub'):
    for project in ('runway', 'manhattan'):
            auth_call_params.append(dict(
                fn=api.list_network_attachments,
                error=None,
                admin=True,
                project=project,
                args=[net]
            ))

# Projects should be able to view their own nodes in a network:
for (project, net) in [
    ('runway', 'runway_pxe'),
    ('runway', 'runway_provider'),
    ('manhattan', 'manhattan_pxe'),
    ('manhattan', 'manhattan_provider'),
    ('manhattan', 'manhattan_runway_pxe'),
    ('manhattan', 'manhattan_runway_provider'),
    ('runway', 'manhattan_runway_pxe'),
    ('runway', 'manhattan_runway_provider'),
]:
    auth_call_params.append(dict(
        fn=api.list_network_attachments,
        error=None,
        admin=False,
        project=project,
        args=[net, project]
    ))

# owner of a network should be able to view all nodes in the network:
for (project, net) in [
    ('runway', 'runway_pxe'),
    ('manhattan', 'manhattan_pxe'),
    ('manhattan', 'manhattan_runway_pxe'),
]:
    auth_call_params.append(dict(
        fn=api.list_network_attachments,
        error=None,
        admin=False,
        project=project,
        args=[net]
    ))


# Illegal cases
# Projects should not be able to list nodes that do not belong to them
# (on network they do not own)
for (project, access_project, net) in [
    ('runway', 'manhattan', 'manhattan_runway_pxe'),
    ('runway', 'manhattan', 'manhattan_runway_provider'),
    ('runway', None, 'manhattan_runway_pxe'),
    ('runway', None, 'manhattan_runway_provider'),
]:
    auth_call_params.append(dict(
        fn=api.list_network_attachments,
        error=AuthorizationError,
        admin=False,
        project=project,
        args=[net, access_project]
    ))

# or on networks they do not have access to
for (project, net) in [
    ('runway', 'manhattan_pxe'),
    ('runway', 'manhattan_provider'),
    ('manhattan', 'runway_pxe'),
    ('manhattan', 'runway_provider'),
]:
    auth_call_params.append(dict(
        fn=api.list_network_attachments,
        error=AuthorizationError,
        admin=False,
        project=project,
        args=[net, project]
    ))

#
# show_network
#

# Legal Cases
# Public networks should be accessible by anyone.
for net in ('stock_int_pub', 'stock_ext_pub'):
    for project in ('runway', 'manhattan'):
        for admin in (True, False):
            auth_call_params.append(dict(