/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2022 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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.
 */

package org.jboss.installer.postinstall.task.secdom;

import org.jboss.as.controller.PathAddress;
import org.jboss.dmr.ModelNode;
import org.jboss.installer.postinstall.server.EmbeddedServer;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static org.jboss.as.cli.Util.DEFAULT_PERMISSION_MAPPER;
import static org.jboss.as.cli.Util.ELYTRON;
import static org.jboss.as.cli.Util.FILESYSTEM_REALM;
import static org.jboss.as.cli.Util.NAME;
import static org.jboss.as.cli.Util.PERMISSION_MAPPER;
import static org.jboss.as.cli.Util.REALMS;
import static org.jboss.as.cli.Util.ROLE_DECODER;
import static org.jboss.as.cli.Util.SECURITY_DOMAIN;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ATTRIBUTE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.HOST;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.IDENTITY;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.PRIMARY;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.REALM;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUBSYSTEM;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SYSTEM_PROPERTY;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.VALUE;
import static org.jboss.as.controller.operations.common.Util.createEmptyOperation;

public class KerberosSecurity {

    public List<ModelNode> toOperations(EmbeddedServer server, String domainName, KerberosConfig config) {

        final String kerberosSecurityFactoryName = config.getKerberosSecurityFactoryName();
        final String principal = config.getPrincipal();
        final String[] mechanismNames = config.getMechanismNames();
        final String[] mechanismOids = config.getMechanismOids();
        final String fileSystemRealmName = config.getFileSystemRealmName();
        final String identityName = config.getIdentityName();
        final String[] identityAttributeValue = config.getIdentityAttributeValue();
        final String simpleRoleDecoderName = config.getSimpleRoleDecoderName();

        final List<ModelNode> ops = new ArrayList<>();

        // 1. Configure a kerberos-security-factory.
        // standalone: /subsystem=elytron/kerberos-security-factory=krbSF:add(principal="HTTP/host@REALM",path="/path/to/http.keytab",mechanism-oids=[1.2.840.113554.1.2.2,1.3.6.1.5.5.2])
        // domain: /profile=*/subsystem=elytron/kerberos-security-factory=krbSF:add(principal="HTTP/host@REALM",path="/path/to/http.keytab",mechanism-oids=[1.2.840.113554.1.2.2,1.3.6.1.5.5.2])
        final ModelNode addKSFOp = createEmptyOperation(ADD, PathAddress.pathAddress(SUBSYSTEM, ELYTRON)
                .append("kerberos-security-factory", kerberosSecurityFactoryName));
        addKSFOp.get("principal").set(principal);
        server.relativise(config.getKeyTabPath(), addKSFOp);
        if (mechanismNames.length > 0) {
            ModelNode list = new ModelNode();
            for (String mechanismName : mechanismNames) {
                list.add(mechanismName);
            }
            addKSFOp.get("mechanism-names").set(list);
        }
        if (mechanismOids.length > 0) {
            ModelNode list = new ModelNode();
            for (String mechanismOid : mechanismOids) {
                list.add(mechanismOid);
            }
            addKSFOp.get("mechanism-oids").set(list);
        }
        ops.add(addKSFOp);

        // 2. Configure the system properties for Kerberos.
        // standalone: /system-property=java.security.krb5.conf:add(value="/path/to/krb5.conf")
        // domain: /host=primary/server-config=*/system-property=java.security.krb5.conf:add(value="/path/to/krb5.conf")
        final ModelNode addKrb5ConfOp = server.isDomain() ? createEmptyOperation(ADD, PathAddress.pathAddress(HOST, PRIMARY).append(SYSTEM_PROPERTY, "java.security.krb5.conf")) : createEmptyOperation(ADD, PathAddress.pathAddress(SYSTEM_PROPERTY, "java.security.krb5.conf"));
        addKrb5ConfOp.get(VALUE).set(config.getKrb5Conf().toString());
        ops.add(addKrb5ConfOp);

        // Configure an Elytron security realm for assigning roles.
        // standalone:
        // /subsystem=elytron/filesystem-realm=exampleFsRealm:add(path=fs-realm-users,relative-to=jboss.server.config.dir)
        // /subsystem=elytron/filesystem-realm=exampleFsRealm:add-identity(identity=user1@REALM)
        // /subsystem=elytron/filesystem-realm=exampleFsRealm:add-identity-attribute(identity=user1@REALM,name=Roles,value=["Admin","Guest"])
        // domain mode:
        // /profile=*/subsystem=elytron/filesystem-realm=exampleFsRealm:add(path=fs-realm-users,relative-to=jboss.domain.config.dir)
        // /host=primary/subsystem=elytron/filesystem-realm=exampleFsRealm:add(path=fs-realm-users,relative-to=jboss.domain.config.dir)
        // /host=primary/subsystem=elytron/filesystem-realm=exampleFsRealm:add-identity(identity=user1@REALM)
        // /host=primary/subsystem=elytron/filesystem-realm=exampleFsRealm:add-identity-attribute(identity=user1@REALM,name=Roles,value=["Admin","Guest"])

        final ModelNode addFilesystemRealmOp = createEmptyOperation(ADD, PathAddress.pathAddress(SUBSYSTEM, ELYTRON)
                .append(FILESYSTEM_REALM, fileSystemRealmName));
        if (server.isDomain()) {
            server.relativise(config.getFileSystemRealmDomainPath(), addFilesystemRealmOp);
        } else {
            server.relativise(config.getFileSystemRealmStandalonePath(), addFilesystemRealmOp);
        }
        ops.add(addFilesystemRealmOp);
        if (server.isDomain()) {
            // /host=primary/subsystem=elytron/filesystem-realm=exampleFsRealm:add(path=fs-realm-users,relative-to=jboss.domain.config.dir)
            // define the realm on the HC with same realm file location, because we need to call runtime operations add-identity and add-identity-attribute from embed-host-controller.
            final ModelNode addFilesystemRealmOnHCOp = createEmptyOperation(ADD, PathAddress.pathAddress(HOST, PRIMARY).append(SUBSYSTEM, ELYTRON)
                    .append(FILESYSTEM_REALM, fileSystemRealmName));
            server.relativise(config.getFileSystemRealmDomainPath(), addFilesystemRealmOnHCOp);
            ops.add(addFilesystemRealmOnHCOp);
        }
        final ModelNode addIdentityOp = server.isDomain() ? createEmptyOperation("add-identity", PathAddress.pathAddress(HOST, PRIMARY).append(SUBSYSTEM, ELYTRON)
                .append(FILESYSTEM_REALM, fileSystemRealmName)) : createEmptyOperation("add-identity", PathAddress.pathAddress(SUBSYSTEM, ELYTRON)
                .append(FILESYSTEM_REALM, fileSystemRealmName));
        addIdentityOp.get(IDENTITY).set(identityName);
        ops.add(addIdentityOp);
        final ModelNode addIdentityAttributeOp = server.isDomain() ? createEmptyOperation("add-identity-attribute", PathAddress.pathAddress(HOST, PRIMARY).append(SUBSYSTEM, ELYTRON)
                .append(FILESYSTEM_REALM, fileSystemRealmName)) : createEmptyOperation("add-identity-attribute", PathAddress.pathAddress(SUBSYSTEM, ELYTRON)
                .append(FILESYSTEM_REALM, fileSystemRealmName));
        addIdentityAttributeOp.get(IDENTITY).set(identityName);
        addIdentityAttributeOp.get(NAME).set(config.getIdentityAttributeName());
        ModelNode list = new ModelNode();
        for (String s : identityAttributeValue) {
            list.add(s);
        }
        addIdentityAttributeOp.get(VALUE).set(list);
        ops.add(addIdentityAttributeOp);

        // 3. Add a simple-role-decoder.
        // standalone: /subsystem=elytron/simple-role-decoder=from-roles-attribute:add(attribute=Roles)
        // domain: /profile=*/subsystem=elytron/simple-role-decoder=from-roles-attribute:add(attribute=Roles)
        final ModelNode addSimpleRoleDecoderOp = createEmptyOperation(ADD, PathAddress.pathAddress(SUBSYSTEM, ELYTRON)
                .append("simple-role-decoder", simpleRoleDecoderName));
        addSimpleRoleDecoderOp.get(ATTRIBUTE).set(config.getSimpleRoleDecoderAttribute());
        ops.add(addSimpleRoleDecoderOp);

        // 4. Configure a security-domain.
        // standalone: /subsystem=elytron/security-domain=exampleFsSD:add(realms=[{realm=exampleFsRealm,role-decoder=from-roles-attribute}],default-realm=exampleFsRealm,permission-mapper=default-permission-mapper)
        // domain: /profile=*/subsystem=elytron/security-domain=exampleFsSD:add(realms=[{realm=exampleFsRealm,role-decoder=from-roles-attribute}],default-realm=exampleFsRealm,permission-mapper=default-permission-mapper)

        final ModelNode addSecurityDomainOp = createEmptyOperation(ADD, PathAddress.pathAddress(SUBSYSTEM, ELYTRON)
                .append(SECURITY_DOMAIN, domainName));
        final ModelNode realm = new ModelNode();
        realm.get(REALM).set(fileSystemRealmName);
        realm.get(ROLE_DECODER).set(simpleRoleDecoderName);
        final ModelNode realms = new ModelNode();
        realms.add(realm);
        addSecurityDomainOp.get(REALMS).set(realms);
        addSecurityDomainOp.get("default-realm").set(fileSystemRealmName);
        addSecurityDomainOp.get(PERMISSION_MAPPER).set(DEFAULT_PERMISSION_MAPPER);
        ops.add(addSecurityDomainOp);

        return ops;
    }

    public static class KerberosConfig {
        // serialization keys
        private static final String KERBEROS_SECURITY_FACTORY_NAME = "kerberos-security-factory";
        private static final String PRINCIPAL = "principal";
        private static final String KEYTAB_PATH = "keytab";
        private static final String MECHANISM_NAME = "mechanism-name-";
        private static final String MECHANISM_NAMES_COUNT = "mechanism-names-count";
        private static final String MECHANISM_OID = "mechanism-oid-";
        private static final String MECHANISM_OIDS_COUNT = "mechanism-oids-count";
        private static final String KRB5_CONF = "krb5.conf";
        private static final String FILESYSTEM_REALM_NAME = "filesystemRealmName";
        private static final String FILESYSTEM_REALM_PATH = "filesystemRealmPath";
        private static final String FILESYSTEM_REALM_STANDALONE_PATH = "filesystemRealmStandalonePath";
        private static final String FILESYSTEM_REALM_DOMAIN_PATH = "filesystemRealmDomainPath";
        private static final String IDENTITY_NAME = "identityName";
        private static final String IDENTITY_ATTRIBUTE_NAME = "identityAttribute";
        private static final String IDENTITY_ATTRIBUTE_VALUE = "identity-attribute-value-";
        private static final String IDENTITY_ATTRIBUTE_VALUE_COUNT = "identity-attribute-value-count";
        private static final String SIMPLE_ROLE_DECODER_NAME = "simpleRoleDecoderName";
        private static final String SIMPLE_ROLE_DECODER_VALUE = "simpleRoleDecoderAttribute";
        // end of serialization keys
        private String kerberosSecurityFactoryName;
        private String principal;
        private Path keyTabPath;
        private String[] mechanismNames;
        private String[] mechanismOids;
        private Path krb5Conf;
        private String fileSystemRealmName;
        private String fileSystemRealmPath;
        private Path fileSystemRealmStandalonePath;
        private Path fileSystemRealmDomainPath;
        private String identityName;
        private String identityAttributeName;
        private String[] identityAttributeValue;
        private String simpleRoleDecoderName;
        private String simpleRoleDecoderAttribute;

        public KerberosConfig() {
        }

        public KerberosConfig(Map<String, String> attributes) {
            this.kerberosSecurityFactoryName = attributes.get(KERBEROS_SECURITY_FACTORY_NAME);
            this.principal = attributes.get(PRINCIPAL);
            this.keyTabPath = Paths.get(attributes.get(KEYTAB_PATH));
            final int mechanismNamesCount = Integer.parseInt(attributes.get(MECHANISM_NAMES_COUNT));
            if (mechanismNamesCount > 0) {
                final List<String> mechanismNames = new ArrayList<>(mechanismNamesCount);
                for (int i = 0; i < mechanismNamesCount; i++) {
                    mechanismNames.add(attributes.get(MECHANISM_NAME + i));
                }
                this.mechanismNames = mechanismNames.toArray(String[]::new);
            } else {
                this.mechanismNames = new String[]{};
            }
            final int mechanismOidsCount = Integer.parseInt(attributes.get(MECHANISM_OIDS_COUNT));
            if (mechanismOidsCount > 0) {
                final List<String> mechanismOids = new ArrayList<>(mechanismOidsCount);
                for (int i = 0; i < mechanismOidsCount; i++) {
                    mechanismOids.add(attributes.get(MECHANISM_OID + i));
                }
                this.mechanismOids = mechanismOids.toArray(String[]::new);
            } else {
                this.mechanismOids = new String[]{};
            }
            this.krb5Conf = Paths.get(attributes.get(KRB5_CONF));
            this.fileSystemRealmName = attributes.get(FILESYSTEM_REALM_NAME);
            this.fileSystemRealmPath = attributes.get(FILESYSTEM_REALM_PATH);
            this.fileSystemRealmStandalonePath = Paths.get(attributes.get(FILESYSTEM_REALM_STANDALONE_PATH));
            this.fileSystemRealmDomainPath = Paths.get(attributes.get(FILESYSTEM_REALM_DOMAIN_PATH));
            this.identityName = attributes.get(IDENTITY_NAME);
            this.identityAttributeName = attributes.get(IDENTITY_ATTRIBUTE_NAME);
            final int identityAttributeValueCount = Integer.parseInt(attributes.get(IDENTITY_ATTRIBUTE_VALUE_COUNT));
            if (identityAttributeValueCount > 0) {
                final List<String> identityAttributeValue = new ArrayList<>(identityAttributeValueCount);
                for (int i = 0; i < identityAttributeValueCount; i++) {
                    identityAttributeValue.add(attributes.get(IDENTITY_ATTRIBUTE_VALUE + i));
                }
                this.identityAttributeValue = identityAttributeValue.toArray(String[]::new);
            } else {
                this.identityAttributeValue = new String[]{};
            }
            this.simpleRoleDecoderName = attributes.get(SIMPLE_ROLE_DECODER_NAME);
            this.simpleRoleDecoderAttribute = attributes.get(SIMPLE_ROLE_DECODER_VALUE);
        }

        public String getKerberosSecurityFactoryName() {
            return kerberosSecurityFactoryName;
        }

        public void setKerberosSecurityFactoryName(String kerberosSecurityFactoryName) {
            this.kerberosSecurityFactoryName = kerberosSecurityFactoryName;
        }

        public String getPrincipal() {
            return principal;
        }

        public void setPrincipal(String principal) {
            this.principal = principal;
        }

        public Path getKeyTabPath() {
            return keyTabPath;
        }

        public void setKeyTabPath(Path keyTabPath) {
            this.keyTabPath = keyTabPath;
        }

        public String[] getMechanismNames() {
            return mechanismNames;
        }

        public void setMechanismNames(String[] mechanismNames) {
            this.mechanismNames = mechanismNames;
        }

        public String[] getMechanismOids() {
            return mechanismOids;
        }

        public void setMechanismOids(String[] mechanismOids) {
            this.mechanismOids = mechanismOids;
        }

        public Path getKrb5Conf() {
            return krb5Conf;
        }

        public void setKrb5Conf(Path krb5Conf) {
            this.krb5Conf = krb5Conf;
        }

        public String getFileSystemRealmName() {
            return fileSystemRealmName;
        }

        public void setFileSystemRealmName(String fileSystemRealmName) {
            this.fileSystemRealmName = fileSystemRealmName;
        }

        public String getFileSystemRealmPath() {
            return fileSystemRealmPath;
        }

        public void setFileSystemRealmPath(String fileSystemRealmPath) {
            this.fileSystemRealmPath = fileSystemRealmPath;
        }

        public Path getFileSystemRealmStandalonePath() {
            return fileSystemRealmStandalonePath;
        }

        public void setFileSystemRealmStandalonePath(Path fileSystemRealmStandalonePath) {
            this.fileSystemRealmStandalonePath = fileSystemRealmStandalonePath;
        }

        public Path getFileSystemRealmDomainPath() {
            return fileSystemRealmDomainPath;
        }

        public void setFileSystemRealmDomainPath(Path fileSystemRealmDomainPath) {
            this.fileSystemRealmDomainPath = fileSystemRealmDomainPath;
        }

        public String getIdentityName() {
            return identityName;
        }

        public void setIdentityName(String identityName) {
            this.identityName = identityName;
        }

        public String getIdentityAttributeName() {
            return identityAttributeName;
        }

        public void setIdentityAttributeName(String identityAttributeName) {
            this.identityAttributeName = identityAttributeName;
        }

        public String[] getIdentityAttributeValue() {
            return identityAttributeValue;
        }

        public void setIdentityAttributeValue(String[] identityAttributeValue) {
            this.identityAttributeValue = identityAttributeValue;
        }

        public String getSimpleRoleDecoderName() {
            return simpleRoleDecoderName;
        }

        public void setSimpleRoleDecoderName(String simpleRoleDecoderName) {
            this.simpleRoleDecoderName = simpleRoleDecoderName;
        }

        public String getSimpleRoleDecoderAttribute() {
            return simpleRoleDecoderAttribute;
        }

        public void setSimpleRoleDecoderAttribute(String simpleRoleDecoderAttribute) {
            this.simpleRoleDecoderAttribute = simpleRoleDecoderAttribute;
        }

        public Map<String, String> toAttributes() {
            final Map<String, String> attrs = new HashMap<>();
            attrs.put(KERBEROS_SECURITY_FACTORY_NAME, kerberosSecurityFactoryName);
            attrs.put(PRINCIPAL, principal);
            attrs.put(KEYTAB_PATH, keyTabPath.toString());
            if (mechanismNames.length > 0) {
                for (int i = 0; i < mechanismNames.length; i++) {
                    attrs.put(MECHANISM_NAME + i, mechanismNames[i]);
                }
                attrs.put(MECHANISM_NAMES_COUNT, String.valueOf(mechanismNames.length));
            }
            if (mechanismOids.length > 0) {
                for (int i = 0; i < mechanismOids.length; i++) {
                    attrs.put(MECHANISM_OID + i, mechanismOids[i]);
                }
                attrs.put(MECHANISM_OIDS_COUNT, String.valueOf(mechanismOids.length));
            }
            attrs.put(KRB5_CONF, krb5Conf.toString());
            attrs.put(FILESYSTEM_REALM_NAME, fileSystemRealmName);
            attrs.put(FILESYSTEM_REALM_PATH, fileSystemRealmPath);
            attrs.put(FILESYSTEM_REALM_STANDALONE_PATH, fileSystemRealmStandalonePath.toString());
            attrs.put(FILESYSTEM_REALM_DOMAIN_PATH, fileSystemRealmDomainPath.toString());
            attrs.put(IDENTITY_NAME, identityName);
            attrs.put(IDENTITY_ATTRIBUTE_NAME, identityAttributeName);
            if (identityAttributeValue.length > 0) {
                for (int i = 0; i < identityAttributeValue.length; i++) {
                    attrs.put(IDENTITY_ATTRIBUTE_VALUE + i, identityAttributeValue[i]);
                }
                attrs.put(IDENTITY_ATTRIBUTE_VALUE_COUNT, String.valueOf(identityAttributeValue.length));
            }
            attrs.put(SIMPLE_ROLE_DECODER_NAME, simpleRoleDecoderName);
            attrs.put(SIMPLE_ROLE_DECODER_VALUE, simpleRoleDecoderAttribute);
            return attrs;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            KerberosConfig that = (KerberosConfig) o;
            return Objects.equals(kerberosSecurityFactoryName, that.kerberosSecurityFactoryName) && Objects.equals(principal, that.principal) && Objects.equals(keyTabPath, that.keyTabPath) && Arrays.equals(mechanismNames, that.mechanismNames) && Arrays.equals(mechanismOids, that.mechanismOids) && Objects.equals(krb5Conf, that.krb5Conf) && Objects.equals(fileSystemRealmName, that.fileSystemRealmName) && Objects.equals(fileSystemRealmPath, that.fileSystemRealmPath) && Objects.equals(fileSystemRealmStandalonePath, that.fileSystemRealmStandalonePath) && Objects.equals(fileSystemRealmDomainPath, that.fileSystemRealmDomainPath) && Objects.equals(identityName, that.identityName) && Objects.equals(identityAttributeName, that.identityAttributeName) && Arrays.equals(identityAttributeValue, that.identityAttributeValue) && Objects.equals(simpleRoleDecoderName, that.simpleRoleDecoderName) && Objects.equals(simpleRoleDecoderAttribute, that.simpleRoleDecoderAttribute);
        }

        @Override
        public int hashCode() {
            int result = Objects.hash(kerberosSecurityFactoryName, principal, keyTabPath, krb5Conf, fileSystemRealmName, fileSystemRealmPath, fileSystemRealmStandalonePath, fileSystemRealmDomainPath, identityName, identityAttributeName, simpleRoleDecoderName, simpleRoleDecoderAttribute);
            result = 31 * result + Arrays.hashCode(mechanismNames);
            result = 31 * result + Arrays.hashCode(mechanismOids);
            result = 31 * result + Arrays.hashCode(identityAttributeValue);
            return result;
        }
    }
}
