#!/usr/bin/env python3

# 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.

from collections.abc import MutableMapping
from typing import Any

from oslo_serialization import jsonutils

from oslo_config import cfg
from oslo_policy import _checks
from oslo_policy import opts
from oslo_policy import policy


class FakeEnforcer:
    def __init__(self, rules: policy.Rules, config: str | None) -> None:
        self.rules = rules
        self.conf = None

        if config:
            self.conf = cfg.ConfigOpts()

            for group, options in opts.list_opts():
                self.conf.register_opts(options, group)

            self.conf([f'--config-file={config}'])


def _try_rule(
    key: str,
    rule: _checks.BaseCheck,
    target: _checks.TargetT,
    access_data: MutableMapping[str, Any],
    o: FakeEnforcer,
) -> None:
    try:
        result = rule(
            target,
            access_data,
            o,  # type: ignore
            current_rule=key,
        )
        if result:
            print(f'passed: {key}')
        else:
            print(f'failed: {key}')
    except Exception as e:
        print(e)
        print(f'exception: {rule}')


def flatten(
    d: MutableMapping[str, Any], parent_key: str = ''
) -> dict[str, Any]:
    """Flatten a nested dictionary

    Converts a dictionary with nested values to a single level flat
    dictionary, with dotted notation for each key.

    """
    items: list[tuple[str, Any]] = []
    for k, v in d.items():
        new_key = parent_key + '.' + k if parent_key else k
        if isinstance(v, MutableMapping):
            items.extend(flatten(v, new_key).items())
        else:
            items.append((new_key, v))
    return dict(items)


def tool(
    policy_file: str,
    access_file: str,
    apply_rule: str | None,
    is_admin: bool = False,
    target_file: str | None = None,
    enforcer_config: str | None = None,
) -> None:
    with open(access_file, 'rb', 0) as a:
        access = a.read()

    access_data = jsonutils.loads(access)['token']
    access_data['roles'] = [role['name'] for role in access_data['roles']]
    access_data['user_id'] = access_data['user']['id']
    if access_data.get('project'):
        access_data['project_id'] = access_data['project']['id']
    if access_data.get('system'):
        access_data['system_scope'] = 'all'
    access_data['is_admin'] = is_admin

    with open(policy_file, 'rb', 0) as p:
        policy_data = p.read()

    rules = policy.Rules.load(policy_data, 'default')

    enforcer = FakeEnforcer(rules, enforcer_config)

    if target_file:
        with open(target_file, 'rb', 0) as t:
            target = t.read()

        target_data = flatten(jsonutils.loads(target))
    else:
        target_data = {'user_id': access_data['user']['id']}
        if access_data.get('project_id'):
            target_data['project_id'] = access_data['project_id']

    if apply_rule:
        key = apply_rule
        rule = rules[apply_rule]
        _try_rule(key, rule, target_data, access_data, enforcer)
        return

    for key, rule in sorted(rules.items()):
        if ':' in key:
            _try_rule(key, rule, target_data, access_data, enforcer)


def main() -> None:
    conf = cfg.ConfigOpts()

    conf.register_cli_opt(
        cfg.StrOpt('policy', required=True, help='path to a policy file.')
    )

    conf.register_cli_opt(
        cfg.StrOpt(
            'access',
            required=True,
            help=(
                'path to a file containing OpenStack Identity API '
                'access info in JSON format.'
            ),
        )
    )

    conf.register_cli_opt(
        cfg.StrOpt(
            'target',
            help=(
                'path to a file containing custom target info in '
                'JSON format. This will be used to evaluate the policy with.'
            ),
        )
    )

    conf.register_cli_opt(cfg.StrOpt('rule', help='rule to test.'))

    conf.register_cli_opt(
        cfg.BoolOpt(
            'is_admin',
            help=(
                'set is_admin=True on the credentials used for the evaluation.'
            ),
            default=False,
        )
    )

    conf.register_cli_opt(
        cfg.StrOpt(
            'enforcer_config',
            help='configuration file for the oslopolicy-checker enforcer',
        )
    )

    conf()

    tool(
        conf.policy,
        conf.access,
        conf.rule,
        conf.is_admin,
        conf.target,
        conf.enforcer_config,
    )


if __name__ == '__main__':
    main()
