# Copyright 2021 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

import contextlib
import time
from unittest.mock import Mock

import jwt

import odoo.http
from odoo.exceptions import ValidationError
from odoo.tests.common import TransactionCase
from odoo.tools import mute_logger
from odoo.tools.misc import DotDict

from ..exceptions import (
    AmbiguousJwtValidator,
    JwtValidatorNotFound,
    UnauthorizedCompositeJwtError,
    UnauthorizedInvalidToken,
    UnauthorizedMalformedAuthorizationHeader,
    UnauthorizedMissingAuthorizationHeader,
    UnauthorizedPartnerNotFound,
)


class TestAuthMethod(TransactionCase):
    @contextlib.contextmanager
    def _mock_request(self, authorization):
        environ = {}
        if authorization:
            environ["HTTP_AUTHORIZATION"] = authorization
        request = Mock(
            context={},
            db=self.env.cr.dbname,
            uid=None,
            httprequest=Mock(environ=environ),
            session=DotDict(),
            env=self.env,
            cr=self.env.cr,
        )
        # These attributes are added upon successful auth, so make sure
        # calling hasattr on the mock when they are not yet set returns False.
        del request.jwt_payload
        del request.jwt_partner_id

        with contextlib.ExitStack() as s:
            odoo.http._request_stack.push(request)
            s.callback(odoo.http._request_stack.pop)
            yield request

    def _create_token(
        self,
        key="thesecret",
        audience="me",
        issuer="http://the.issuer",
        exp_delta=100,
        nbf=None,
        email=None,
    ):
        payload = dict(aud=audience, iss=issuer, exp=time.time() + exp_delta)
        if email:
            payload["email"] = email
        if nbf:
            payload["nbf"] = nbf
        return jwt.encode(payload, key=key, algorithm="HS256")

    def _create_validator(
        self,
        name,
        audience="me",
        issuer="http://the.issuer",
        secret_key="thesecret",
        partner_id_required=False,
        static_user_id=1,
    ):
        return self.env["auth.jwt.validator"].create(
            dict(
                name=name,
                signature_type="secret",
                secret_algorithm="HS256",
                secret_key=secret_key,
                audience=audience,
                issuer=issuer,
                user_id_strategy="static",
                static_user_id=static_user_id,
                partner_id_strategy="email",
                partner_id_required=partner_id_required,
            )
        )

    def test_missing_authorization_header(self):
        self._create_validator("validator")
        with self._mock_request(authorization=None):
            with self.assertRaises(UnauthorizedMissingAuthorizationHeader):
                self.env["ir.http"]._auth_method_jwt(validator_name="validator")

    def test_malformed_authorization_header(self):
        self._create_validator("validator")
        for authorization in (
            "a",
            "Bearer",
            "Bearer ",
            "Bearer x y",
            "Bearer token ",
            "bearer token",
        ):
            with self._mock_request(authorization=authorization):
                with self.assertRaises(UnauthorizedMalformedAuthorizationHeader):
                    self.env["ir.http"]._auth_method_jwt(validator_name="validator")

    def test_auth_method_valid_token(self):
        self._create_validator("validator")
        authorization = "Bearer " + self._create_token()
        with self._mock_request(authorization=authorization):
            self.env["ir.http"]._auth_method_jwt_validator()

    def test_auth_method_valid_token_two_validators_one_bad_issuer(self):
        self._create_validator("validator2", issuer="http://other.issuer")
        self._create_validator("validator3")

        authorization = "Bearer " + self._create_token()
        with self._mock_request(authorization=authorization):
            # first validator rejects the token because of invalid audience
            with self.assertRaises(UnauthorizedInvalidToken):
                self.env["ir.http"]._auth_method_jwt_validator2()
            # second validator accepts the token
            self.env["ir.http"]._auth_method_jwt_validator3()

    def test_auth_method_valid_token_two_validators_one_bad_issuer_chained(self):
        validator2 = self._create_validator("validator2", issuer="http://other.issuer")
        validator3 = self._create_validator("validator3")
        validator2.next_validator_id = validator3

        authorization = "Bearer " + self._create_token()
        with self._mock_request(authorization=authorization):
            # Validator2 rejects the token because of invalid issuer but chain
            # on validator3 which accepts it
            self.env["ir.http"]._auth_method_jwt_validator2()

    def test_auth_method_valid_token_two_validators_one_bad_audience(self):
        self._create_validator("validator2", audience="bad")
        self._create_validator("validator3")

        authorization = "Bearer " + self._create_token()
        with self._mock_request(authorization=authorization):
            # first validator rejects the token because of invalid audience
            with self.assertRaises(UnauthorizedInvalidToken):
                self.env["ir.http"]._auth_method_jwt_validator2()
            # second validator accepts the token
            self.env["ir.http"]._auth_method_jwt_validator3()

    def test_auth_method_valid_token_two_validators_one_bad_audience_chained(self):
        validator2 = self._create_validator("validator2", audience="bad")
        validator3 = self._create_validator("validator3")

        validator2.next_validator_id = validator3
        authorization = "Bearer " + self._create_token()
        with self._mock_request(authorization=authorization):
            self.env["ir.http"]._auth_method_jwt_validator2()

    def test_auth_method_invalid_token(self):
        # Test invalid token via _auth_method_jwt
        # Other types of invalid tokens are unit tested elswhere.
        self._create_validator("validator4")
        authorization = "Bearer " + self._create_token(audience="bad")
        with self._mock_request(authorization=authorization):
            with self.assertRaises(UnauthorizedInvalidToken):
                self.env["ir.http"]._auth_method_jwt_validator4()

    def test_auth_method_invalid_token_on_chain(self):
        validator1 = self._create_validator("validator", issuer="http://other.issuer")
        validator2 = self._create_validator("validator2", audience="bad audience")
        validator3 = self._create_validator("validator3", secret_key="bad key")
        validator4 = self._create_validator(
            "validator4", issuer="http://other.issuer", audience="bad audience"
        )
        validator5 = self._create_validator(
            "validator5", issuer="http://other.issuer", secret_key="bad key"
        )
        validator6 = self._create_validator(
            "validator6", audience="bad audience", secret_key="bad key"
        )
        validator7 = self._create_validator(
            "validator7",
            issuer="http://other.issuer",
            audience="bad audience",
            secret_key="bad key",
        )
        validator1.next_validator_id = validator2
        validator2.next_validator_id = validator3
        validator3.next_validator_id = validator4
        validator4.next_validator_id = validator5
        validator5.next_validator_id = validator6
        validator6.next_validator_id = validator7

        authorization = "Bearer " + self._create_token()
        with self._mock_request(authorization=authorization):
            with self.assertRaises(UnauthorizedCompositeJwtError) as composite_error:
                self.env["ir.http"]._auth_method_jwt_validator()
            self.assertEqual(
                str(composite_error.exception),
                "401 Unauthorized: Multiple errors occurred during JWT chain validation:\n"
                "validator: 401 Unauthorized: "
                "The server could not verify that you are authorized to "
                "access the URL requested. You either supplied the wrong "
                "credentials (e.g. a bad password), or your browser doesn't "
                "understand how to supply the credentials required.\n"
                "validator2: 401 Unauthorized: "
                "The server could not verify that you are authorized to "
                "access the URL requested. You either supplied the wrong "
                "credentials (e.g. a bad password), or your browser doesn't "
                "understand how to supply the credentials required.\n"
                "validator3: 401 Unauthorized: "
                "The server could not verify that you are authorized to "
                "access the URL requested. You either supplied the wrong "
                "credentials (e.g. a bad password), or your browser doesn't "
                "understand how to supply the credentials required.\n"
                "validator4: 401 Unauthorized: "
                "The server could not verify that you are authorized to "
                "access the URL requested. You either supplied the wrong "
                "credentials (e.g. a bad password), or your browser doesn't "
                "understand how to supply the credentials required.\n"
                "validator5: 401 Unauthorized: "
                "The server could not verify that you are authorized to "
                "access the URL requested. You either supplied the wrong "
                "credentials (e.g. a bad password), or your browser doesn't "
                "understand how to supply the credentials required.\n"
                "validator6: 401 Unauthorized: "
                "The server could not verify that you are authorized to "
                "access the URL requested. You either supplied the wrong "
                "credentials (e.g. a bad password), or your browser doesn't "
                "understand how to supply the credentials required.\n"
                "validator7: 401 Unauthorized: "
                "The server could not verify that you are authorized to "
                "access the URL requested. You either supplied the wrong "
                "credentials (e.g. a bad password), or your browser doesn't "
                "understand how to supply the credentials required.",
            )

    def test_invalid_validation_chain(self):
        validator1 = self._create_validator("validator")
        validator2 = self._create_validator("validator2")
        validator3 = self._create_validator("validator3")

        validator1.next_validator_id = validator2
        validator2.next_validator_id = validator3
        with self.assertRaises(ValidationError) as error:
            validator3.next_validator_id = validator1
        self.assertEqual(
            str(error.exception),
            "Validators mustn't make a closed chain: "
            "validator3 -> validator -> validator2 -> validator3.",
        )

    def test_invalid_validation_auto_chain(self):
        validator = self._create_validator("validator")
        with self.assertRaises(ValidationError) as error:
            validator.next_validator_id = validator
        self.assertEqual(
            str(error.exception),
            "Validators mustn't make a closed chain: " "validator -> validator.",
        )

    def test_partner_id_strategy_email_found(self):
        partner = self.env["res.partner"].search([("email", "!=", False)])[0]
        self._create_validator("validator6")
        authorization = "Bearer " + self._create_token(email=partner.email)
        with self._mock_request(authorization=authorization) as request:
            self.env["ir.http"]._auth_method_jwt_validator6()
            self.assertEqual(request.jwt_partner_id, partner.id)

    def test_partner_id_strategy_email_not_found(self):
        self._create_validator("validator6")
        authorization = "Bearer " + self._create_token(email="notanemail@example.com")
        with self._mock_request(authorization=authorization) as request:
            self.env["ir.http"]._auth_method_jwt_validator6()
            self.assertFalse(request.jwt_partner_id)

    def test_partner_id_strategy_email_not_found_partner_required(self):
        self._create_validator("validator6", partner_id_required=True)
        authorization = "Bearer " + self._create_token(email="notanemail@example.com")
        with self._mock_request(authorization=authorization):
            with self.assertRaises(UnauthorizedPartnerNotFound):
                self.env["ir.http"]._auth_method_jwt_validator6()

    def test_get_validator(self):
        AuthJwtValidator = self.env["auth.jwt.validator"]
        AuthJwtValidator.search([]).unlink()
        with self.assertRaises(JwtValidatorNotFound), mute_logger(
            "odoo.addons.auth_jwt.models.auth_jwt_validator"
        ):
            AuthJwtValidator._get_validator_by_name(None)
        with self.assertRaises(JwtValidatorNotFound), mute_logger(
            "odoo.addons.auth_jwt.models.auth_jwt_validator"
        ):
            AuthJwtValidator._get_validator_by_name("notavalidator")
        validator1 = self._create_validator(name="validator1")
        with self.assertRaises(JwtValidatorNotFound), mute_logger(
            "odoo.addons.auth_jwt.models.auth_jwt_validator"
        ):
            AuthJwtValidator._get_validator_by_name("notavalidator")
        self.assertEqual(AuthJwtValidator._get_validator_by_name(None), validator1)
        self.assertEqual(
            AuthJwtValidator._get_validator_by_name("validator1"), validator1
        )
        # create a second validator
        validator2 = self._create_validator(name="validator2")
        with self.assertRaises(AmbiguousJwtValidator), mute_logger(
            "odoo.addons.auth_jwt.models.auth_jwt_validator"
        ):
            AuthJwtValidator._get_validator_by_name(None)
        self.assertEqual(
            AuthJwtValidator._get_validator_by_name("validator2"), validator2
        )

    def test_bad_tokens(self):
        validator = self._create_validator("validator")
        token = self._create_token(key="badsecret")
        with self.assertRaises(UnauthorizedInvalidToken):
            validator._decode(token)
        token = self._create_token(audience="badaudience")
        with self.assertRaises(UnauthorizedInvalidToken):
            validator._decode(token)
        token = self._create_token(issuer="badissuer")
        with self.assertRaises(UnauthorizedInvalidToken):
            validator._decode(token)
        token = self._create_token(exp_delta=-100)
        with self.assertRaises(UnauthorizedInvalidToken):
            validator._decode(token)

    def test_multiple_aud(self):
        validator = self._create_validator("validator", audience="a1,a2")
        token = self._create_token(audience="a1")
        validator._decode(token)
        token = self._create_token(audience="a2")
        validator._decode(token)
        token = self._create_token(audience="a3")
        with self.assertRaises(UnauthorizedInvalidToken):
            validator._decode(token)

    def test_nbf(self):
        validator = self._create_validator("validator")
        token = self._create_token(nbf=time.time() - 60)
        validator._decode(token)
        token = self._create_token(nbf=time.time() + 60)
        with self.assertRaises(UnauthorizedInvalidToken):
            validator._decode(token)

    def test_auth_method_registration_on_create(self):
        IrHttp = self.env["ir.http"]
        self.assertFalse(hasattr(IrHttp.__class__, "_auth_method_jwt_validator1"))
        self.assertFalse(
            hasattr(IrHttp.__class__, "_auth_method_public_or_jwt_validator1")
        )
        self._create_validator("validator1")
        self.assertTrue(hasattr(IrHttp.__class__, "_auth_method_jwt_validator1"))
        self.assertTrue(
            hasattr(IrHttp.__class__, "_auth_method_public_or_jwt_validator1")
        )

    def test_auth_method_unregistration_on_unlink(self):
        IrHttp = self.env["ir.http"]
        validator = self._create_validator("validator1")
        self.assertTrue(hasattr(IrHttp.__class__, "_auth_method_jwt_validator1"))
        self.assertTrue(
            hasattr(IrHttp.__class__, "_auth_method_public_or_jwt_validator1")
        )
        validator.unlink()
        self.assertFalse(hasattr(IrHttp.__class__, "_auth_method_jwt_validator1"))
        self.assertFalse(
            hasattr(IrHttp.__class__, "_auth_method_public_or_jwt_validator1")
        )

    def test_auth_method_registration_on_rename(self):
        IrHttp = self.env["ir.http"]
        validator = self._create_validator("validator1")
        self.assertTrue(hasattr(IrHttp.__class__, "_auth_method_jwt_validator1"))
        self.assertTrue(
            hasattr(IrHttp.__class__, "_auth_method_public_or_jwt_validator1")
        )
        validator.name = "validator2"
        self.assertFalse(hasattr(IrHttp.__class__, "_auth_method_jwt_validator1"))
        self.assertFalse(
            hasattr(IrHttp.__class__, "_auth_method_public_or_jwt_validator1")
        )
        self.assertTrue(hasattr(IrHttp.__class__, "_auth_method_jwt_validator2"))
        self.assertTrue(
            hasattr(IrHttp.__class__, "_auth_method_public_or_jwt_validator2")
        )

    def test_name_check(self):
        with self.assertRaises(ValidationError):
            self._create_validator(name="not an identifier")

    def test_public_or_jwt_valid_token(self):
        self._create_validator("validator")
        authorization = "Bearer " + self._create_token()
        with self._mock_request(authorization=authorization) as request:
            self.env["ir.http"]._auth_method_public_or_jwt_validator()
            assert request.jwt_payload["aud"] == "me"
