#
#   Copyright © 2021,2022 IsardVDI S.L.
#
#   This file is part of DD
#
#   DD is free software: you can redistribute it and/or modify
#   it under the terms of the GNU Affero General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or (at your
#   option) any later version.
#
#   DD is distributed in the hope that it will be useful, but WITHOUT ANY
#   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
#   FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
#   details.
#
#   You should have received a copy of the GNU Affero General Public License
#   along with DD. If not, see <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: AGPL-3.0-or-later

import json
import logging as log
import os
import pprint
import random
import string
import time
import traceback
from datetime import datetime, timedelta

import psycopg2
import yaml
from admin.lib.keycloak_client import KeycloakClient
from admin.lib.postgres import Postgres

app = {}
app["config"] = {}


class MoodleSaml:
    def __init__(self):
        ready = False
        while not ready:
            try:
                self.pg = Postgres(
                    "dd-apps-postgresql",
                    "moodle",
                    os.environ["MOODLE_POSTGRES_USER"],
                    os.environ["MOODLE_POSTGRES_PASSWORD"],
                )
                ready = True
            except:
                log.warning("Could not connect to moodle database. Retrying...")
                time.sleep(2)
        log.info("Connected to moodle database.")

        ready = False
        while not ready:
            try:
                privatekey_pass = self.get_privatekey_pass()
                log.info("Read the private key")
                if privatekey_pass.endswith(os.environ["DOMAIN"]):
                    app["config"]["MOODLE_SAML_PRIVATEKEYPASS"] = privatekey_pass
                    ready = True
                else:
                    log.err(f"Private key does not end with: {os.environ['DOMAIN']}")
            except:
                # print(traceback.format_exc())
                log.warning("Could not get moodle site identifier. Retrying...")
                time.sleep(2)
        log.info("Got moodle site identifier.")

        basepath = os.path.dirname(__file__)

        ready = False
        while not ready:
            try:
                with open(
                    os.path.abspath(
                        os.path.join(
                            basepath,
                            "../moodledata/saml2/moodle."
                            + os.environ["DOMAIN"]
                            + ".crt",
                        )
                    ),
                    "r",
                ) as crt:
                    app["config"]["SP_CRT"] = crt.read()
                    ready = True
            except IOError:
                log.warning("Could not get moodle SAML2 crt certificate. Retrying...")
                time.sleep(2)
            except:
                log.error(traceback.format_exc())
        log.info("Got moodle crt certificate.")

        ready = False
        while not ready:
            try:
                with open(
                    os.path.abspath(
                        os.path.join(
                            basepath,
                            "../moodledata/saml2/moodle."
                            + os.environ["DOMAIN"]
                            + ".pem",
                        )
                    ),
                    "r",
                ) as pem:
                    app["config"]["SP_PEM"] = pem.read()
                    ready = True
            except IOError:
                log.warning("Could not get moodle SAML2 pem certificate. Retrying...")
                time.sleep(2)
        log.info("Got moodle pem certificate.")

        idp_name="0f635d0e0f3874fff8b581c132e6c7a7"
        ## Upstream bug:
        ## https://github.com/catalyst/moodle-auth_saml2/issues/652
        ##
        ## This seems related to the fact that the certificate generated the first time does'nt work.
        ## And when regenerating the certificate the privatekeypass seems not to be used and instead it
        ## will use always this code as filename: 0f635d0e0f3874fff8b581c132e6c7a7
        ## As this bug I'm not able to solve, the process is:
        ## 1.- Bring up moodle and regenerate certificates on saml2 plugin in plugins-authentication
        ## 2.- Execute this script
        ## 3.- Cleanup all caches in moodle (Development tab)
        with open(
            os.path.abspath(
                os.path.join(
                    basepath,
                    f"../moodledata/saml2/{idp_name}.idp.xml",
                )
            ),
            "w",
        ) as xml:
            xml.write(self.parse_idp_metadata())

        log.info("Written SP file on moodledata.")

        try:
            self.activate_saml_plugin()
        except:
            print("Error activating saml on moodle")

        try:
            self.set_moodle_saml_plugin()
        except:
            print("Error setting saml on moodle")

        try:
            self.delete_keycloak_moodle_saml_plugin()
        except:
            print("Error deleting saml on keycloak")

        try:
            self.add_keycloak_moodle_saml()
        except:
            print("Error adding saml on keycloak")

        # SAML clients don't work well with composite roles so disabling and adding on realm
        # self.add_client_roles()

    def activate_saml_plugin(self):
        ## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php
        return self.pg.update(
            """UPDATE "mdl_config" SET value = 'email,saml2' WHERE "name" = 'auth'"""
        )

    def get_privatekey_pass(self):
        return self.pg.select(
            """SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'"""
        )[0][2]

    def parse_idp_metadata(self):
        keycloak = KeycloakClient()
        rsa = keycloak.get_server_rsa_key()
        keycloak = None
        return (
            '<md:EntitiesDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Name="urn:keycloak"><md:EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://sso.'
            + os.environ["DOMAIN"]
            + '/auth/realms/master"><md:IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><md:KeyDescriptor use="signing"><ds:KeyInfo><ds:KeyName>'
            + rsa["name"]
            + "</ds:KeyName><ds:X509Data><ds:X509Certificate>"
            + rsa["certificate"]
            + '</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.'
            + os.environ["DOMAIN"]
            + '/auth/realms/master/protocol/saml/resolve" index="0"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.'
            + os.environ["DOMAIN"]
            + '/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.'
            + os.environ["DOMAIN"]
            + '/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.'
            + os.environ["DOMAIN"]
            + '/auth/realms/master/protocol/saml"/><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.'
            + os.environ["DOMAIN"]
            + '/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.'
            + os.environ["DOMAIN"]
            + '/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.'
            + os.environ["DOMAIN"]
            + '/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.'
            + os.environ["DOMAIN"]
            + '/auth/realms/master/protocol/saml"/></md:IDPSSODescriptor></md:EntityDescriptor></md:EntitiesDescriptor>'
        )

    def set_keycloak_moodle_saml_plugin(self):
        keycloak = KeycloakClient()
        keycloak.add_moodle_client()
        keycloak = None

    def delete_keycloak_moodle_saml_plugin(self):
        keycloak = KeycloakClient()
        keycloak.delete_client("a92d5417-92b6-4678-9cb9-51bc0edcee8c")
        keycloak = None

    def set_moodle_saml_plugin(self):
        config = {
            "idpmetadata": self.parse_idp_metadata(),
            "certs_locked": "1",
            "duallogin": "0",
            "idpattr": "username",
            "autocreate": "1",
            "anyauth": "1",
            "saml_role_siteadmin_map": "admin",
            "saml_role_coursecreator_map": "teacher",
            "saml_role_manager_map": "manager",
            "field_map_email": "email",
            "field_map_firstname": "givenName",
            "field_map_lastname": "sn",
        }
        for name in config.keys():
            self.pg.update(
                """UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'auth_saml2' AND "name" = '%s'"""
                % (config[name], name)
            )
            self.pg.update(
                """INSERT INTO "mdl_auth_saml2_idps" ("metadataurl", "entityid", "activeidp", "defaultidp", "adminidp", "defaultname", "displayname", "logo", "alias", "whitelist") VALUES
                            ('xml',	'https://sso.%s/auth/realms/master',	1,	0,	0,	'Login via SAML2',	'',	NULL,	NULL,	NULL);"""
                % (os.environ["DOMAIN"])
            )

    def add_keycloak_moodle_saml(self):
        client = {
            "id": "a92d5417-92b6-4678-9cb9-51bc0edcee8c",
            "name": "moodle",
            "description": "moodle",
            "clientId": "https://moodle."
            + os.environ["DOMAIN"]
            + "/auth/saml2/sp/metadata.php",
            "surrogateAuthRequired": False,
            "enabled": True,
            "alwaysDisplayInConsole": False,
            "clientAuthenticatorType": "client-secret",
            "redirectUris": [
                "https://moodle."
                + os.environ["DOMAIN"]
                + "/auth/saml2/sp/saml2-acs.php/moodle."
                + os.environ["DOMAIN"]
                + ""
            ],
            "webOrigins": ["https://moodle." + os.environ["DOMAIN"] + ""],
            "notBefore": 0,
            "bearerOnly": False,
            "consentRequired": False,
            "standardFlowEnabled": True,
            "implicitFlowEnabled": False,
            "directAccessGrantsEnabled": False,
            "serviceAccountsEnabled": False,
            "publicClient": False,
            "frontchannelLogout": True,
            "protocol": "saml",
            "attributes": {
                "saml.force.post.binding": True,
                "saml.encrypt": False,
                "saml_assertion_consumer_url_post": "https://moodle."
                + os.environ["DOMAIN"]
                + "/auth/saml2/sp/saml2-acs.php/moodle."
                + os.environ["DOMAIN"]
                + "",
                "saml.server.signature": True,
                "saml.server.signature.keyinfo.ext": False,
                "saml.signing.certificate": app["config"]["SP_CRT"],
                "saml_single_logout_service_url_redirect": "https://moodle."
                + os.environ["DOMAIN"]
                + "/auth/saml2/sp/saml2-logout.php/moodle."
                + os.environ["DOMAIN"]
                + "",
                "saml.signature.algorithm": "RSA_SHA256",
                "saml_force_name_id_format": False,
                "saml.client.signature": True,
                "saml.encryption.certificate": app["config"]["SP_PEM"],
                "saml.authnstatement": True,
                "saml_name_id_format": "username",
                "saml_signature_canonicalization_method": "http://www.w3.org/2001/10/xml-exc-c14n#",
            },
            "authenticationFlowBindingOverrides": {},
            "fullScopeAllowed": True,
            "nodeReRegistrationTimeout": -1,
            "protocolMappers": [
                {
                    "id": "9296daa3-4fc4-4b80-b007-5070f546ae13",
                    "name": "X500 sn",
                    "protocol": "saml",
                    "protocolMapper": "saml-user-property-mapper",
                    "consentRequired": False,
                    "config": {
                        "attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
                        "user.attribute": "lastName",
                        "friendly.name": "sn",
                        "attribute.name": "urn:oid:2.5.4.4",
                    },
                },
                {
                    "id": "ccecf6e4-d20a-4211-b67c-40200a6b2c5d",
                    "name": "username",
                    "protocol": "saml",
                    "protocolMapper": "saml-user-property-mapper",
                    "consentRequired": False,
                    "config": {
                        "attribute.nameformat": "Basic",
                        "user.attribute": "username",
                        "friendly.name": "username",
                        "attribute.name": "username",
                    },
                },
                {
                    "id": "53858403-eba2-4f6d-81d0-cced700b5719",
                    "name": "X500 givenName",
                    "protocol": "saml",
                    "protocolMapper": "saml-user-property-mapper",
                    "consentRequired": False,
                    "config": {
                        "attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
                        "user.attribute": "firstName",
                        "friendly.name": "givenName",
                        "attribute.name": "urn:oid:2.5.4.42",
                    },
                },
                {
                    "id": "20034db5-1d0e-4e66-b815-fb0440c6d1e2",
                    "name": "X500 email",
                    "protocol": "saml",
                    "protocolMapper": "saml-user-property-mapper",
                    "consentRequired": False,
                    "config": {
                        "attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
                        "user.attribute": "email",
                        "friendly.name": "email",
                        "attribute.name": "urn:oid:1.2.840.113549.1.9.1",
                    },
                },
            ],
            "defaultClientScopes": [
                "web-origins",
                "role_list",
                "roles",
                "profile",
                "email",
            ],
            "optionalClientScopes": [
                "address",
                "phone",
                "offline_access",
                "microprofile-jwt",
            ],
            "access": {"view": True, "configure": True, "manage": True},
        }
        keycloak = KeycloakClient()
        keycloak.add_client(client)
        keycloak = None

    def add_client_roles(self):
        keycloak = KeycloakClient()
        keycloak.add_client_role(
            "a92d5417-92b6-4678-9cb9-51bc0edcee8c", "admin", "Moodle admins"
        )
        keycloak.add_client_role(
            "a92d5417-92b6-4678-9cb9-51bc0edcee8c", "manager", "Moodle managers"
        )
        keycloak.add_client_role(
            "a92d5417-92b6-4678-9cb9-51bc0edcee8c", "teacher", "Moodle teachers"
        )
        keycloak.add_client_role(
            "a92d5417-92b6-4678-9cb9-51bc0edcee8c", "student", "Moodle students"
        )
        keycloak = None


m = MoodleSaml()
