# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright 2012 OpenStack Foundation
#
# 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.
#

import os

from keystone.common import environment
from keystone.common import utils
from keystone import config
from keystone.openstack.common import log as logging

LOG = logging.getLogger(__name__)
CONF = config.CONF

PUBLIC_DIR_PERMS = 0o755        # -rwxr-xr-x
PRIVATE_DIR_PERMS = 0o750       # -rwxr-x---
PUBLIC_FILE_PERMS = 0o644       # -rw-r--r--
PRIVATE_FILE_PERMS = 0o640      # -rw-r-----


def file_exists(file_path):
    return os.path.exists(file_path)


class BaseCertificateConfigure(object):
    """Create a certificate signing environment.

    This is based on a config section and reasonable OpenSSL defaults.

    """

    def __init__(self, conf_obj, keystone_user, keystone_group, **kwargs):
        self.conf_dir = os.path.dirname(conf_obj.ca_certs)
        self.use_keystone_user = keystone_user
        self.use_keystone_group = keystone_group
        self.ssl_config_file_name = os.path.join(self.conf_dir, "openssl.conf")
        self.request_file_name = os.path.join(self.conf_dir, "req.pem")
        self.ssl_dictionary = {'conf_dir': self.conf_dir,
                               'ca_cert': conf_obj.ca_certs,
                               'default_md': 'default',
                               'ssl_config': self.ssl_config_file_name,
                               'ca_private_key': conf_obj.ca_key,
                               'request_file': self.request_file_name,
                               'signing_key': conf_obj.keyfile,
                               'signing_cert': conf_obj.certfile,
                               'key_size': int(conf_obj.key_size),
                               'valid_days': int(conf_obj.valid_days),
                               'cert_subject': conf_obj.cert_subject}

        try:
            # OpenSSL 1.0 and newer support default_md = default, olders do not
            openssl_ver = environment.subprocess.Popen(
                ['openssl', 'version'],
                stdout=environment.subprocess.PIPE).stdout.read()
            if "OpenSSL 0." in openssl_ver:
                self.ssl_dictionary['default_md'] = 'sha1'
        except OSError:
            LOG.warn('Failed to invoke ``openssl version``, '
                     'assuming is v1.0 or newer')
        self.ssl_dictionary.update(kwargs)

    def exec_command(self, command):
        to_exec = command % self.ssl_dictionary
        LOG.info(to_exec)
        environment.subprocess.check_call(to_exec.rsplit(' '))

    def build_ssl_config_file(self):
        utils.make_dirs(os.path.dirname(self.ssl_config_file_name),
                        mode=PUBLIC_DIR_PERMS,
                        user=self.use_keystone_user,
                        group=self.use_keystone_group, log=LOG)
        if not file_exists(self.ssl_config_file_name):
            ssl_config_file = open(self.ssl_config_file_name, 'w')
            ssl_config_file.write(self.sslconfig % self.ssl_dictionary)
            ssl_config_file.close()
        utils.set_permissions(self.ssl_config_file_name,
                              mode=PRIVATE_FILE_PERMS,
                              user=self.use_keystone_user,
                              group=self.use_keystone_group, log=LOG)

        index_file_name = os.path.join(self.conf_dir, 'index.txt')
        if not file_exists(index_file_name):
            index_file = open(index_file_name, 'w')
            index_file.write('')
            index_file.close()
        utils.set_permissions(index_file_name,
                              mode=PRIVATE_FILE_PERMS,
                              user=self.use_keystone_user,
                              group=self.use_keystone_group, log=LOG)

        serial_file_name = os.path.join(self.conf_dir, 'serial')
        if not file_exists(serial_file_name):
            index_file = open(serial_file_name, 'w')
            index_file.write('01')
            index_file.close()
        utils.set_permissions(serial_file_name,
                              mode=PRIVATE_FILE_PERMS,
                              user=self.use_keystone_user,
                              group=self.use_keystone_group, log=LOG)

    def build_ca_cert(self):
        ca_key_file = self.ssl_dictionary['ca_private_key']
        utils.make_dirs(os.path.dirname(ca_key_file),
                        mode=PRIVATE_DIR_PERMS,
                        user=self.use_keystone_user,
                        group=self.use_keystone_group, log=LOG)
        if not file_exists(ca_key_file):
            self.exec_command('openssl genrsa -out %(ca_private_key)s '
                              '%(key_size)d')
        utils.set_permissions(ca_key_file,
                              mode=PRIVATE_FILE_PERMS,
                              user=self.use_keystone_user,
                              group=self.use_keystone_group, log=LOG)

        ca_cert = self.ssl_dictionary['ca_cert']
        utils.make_dirs(os.path.dirname(ca_cert),
                        mode=PUBLIC_DIR_PERMS,
                        user=self.use_keystone_user,
                        group=self.use_keystone_group, log=LOG)
        if not file_exists(ca_cert):
            self.exec_command('openssl req -new -x509 -extensions v3_ca '
                              '-key %(ca_private_key)s -out %(ca_cert)s '
                              '-days %(valid_days)d '
                              '-config %(ssl_config)s '
                              '-subj %(cert_subject)s')
        utils.set_permissions(ca_cert,
                              mode=PUBLIC_FILE_PERMS,
                              user=self.use_keystone_user,
                              group=self.use_keystone_group, log=LOG)

    def build_private_key(self):
        signing_keyfile = self.ssl_dictionary['signing_key']
        utils.make_dirs(os.path.dirname(signing_keyfile),
                        mode=PRIVATE_DIR_PERMS,
                        user=self.use_keystone_user,
                        group=self.use_keystone_group, log=LOG)
        if not file_exists(signing_keyfile):
            self.exec_command('openssl genrsa -out %(signing_key)s '
                              '%(key_size)d ')
        utils.set_permissions(signing_keyfile,
                              mode=PRIVATE_FILE_PERMS,
                              user=self.use_keystone_user,
                              group=self.use_keystone_group, log=LOG)

    def build_signing_cert(self):
        signing_cert = self.ssl_dictionary['signing_cert']

        utils.make_dirs(os.path.dirname(signing_cert),
                        mode=PUBLIC_DIR_PERMS,
                        user=self.use_keystone_user,
                        group=self.use_keystone_group, log=LOG)
        if not file_exists(signing_cert):
            self.exec_command('openssl req -key %(signing_key)s -new '
                              '-out %(request_file)s -config %(ssl_config)s '
                              '-subj %(cert_subject)s')

            self.exec_command('openssl ca -batch -out %(signing_cert)s '
                              '-config %(ssl_config)s -days %(valid_days)dd '
                              '-cert %(ca_cert)s -keyfile %(ca_private_key)s '
                              '-infiles %(request_file)s')

    def run(self):
        self.build_ssl_config_file()
        self.build_ca_cert()
        self.build_private_key()
        self.build_signing_cert()


class ConfigurePKI(BaseCertificateConfigure):
    """Generate files for PKI signing using OpenSSL.

    Signed tokens require a private key and signing certificate which itself
    must be signed by a CA.  This class generates them with workable defaults
    if each of the files are not present

    """

    def __init__(self, keystone_user, keystone_group):
        super(ConfigurePKI, self).__init__(CONF.signing,
                                           keystone_user, keystone_group)


class ConfigureSSL(BaseCertificateConfigure):
    """Generate files for HTTPS using OpenSSL.

    Creates a public/private key and certificates. If a CA is not given
    one will be generated using provided arguments.
    """

    def __init__(self, keystone_user, keystone_group):
        super(ConfigureSSL, self).__init__(CONF.ssl,
                                           keystone_user, keystone_group)


BaseCertificateConfigure.sslconfig = """
# OpenSSL configuration file.
#

# Establish working directory.

dir            = %(conf_dir)s

[ ca ]
default_ca        = CA_default

[ CA_default ]
new_certs_dir     = $dir
serial            = $dir/serial
database          = $dir/index.txt
default_days      = 365
default_md        = %(default_md)s
preserve          = no
email_in_dn       = no
nameopt           = default_ca
certopt           = default_ca
policy            = policy_anything
x509_extensions   = usr_cert
unique_subject    = no

[ policy_anything ]
countryName             = optional
stateOrProvinceName     = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ req ]
default_bits       = 2048 # Size of keys
default_keyfile    = key.pem # name of generated keys
string_mask        = utf8only # permitted characters
distinguished_name = req_distinguished_name
req_extensions     = v3_req
x509_extensions = v3_ca

[ req_distinguished_name ]
countryName                 = Country Name (2 letter code)
countryName_min             = 2
countryName_max             = 2
stateOrProvinceName         = State or Province Name (full name)
localityName                = Locality Name (city, district)
0.organizationName          = Organization Name (company)
organizationalUnitName      = Organizational Unit Name (department, division)
commonName                  = Common Name (hostname, IP, or your name)
commonName_max              = 64
emailAddress                = Email Address
emailAddress_max            = 64

[ v3_ca ]
basicConstraints       = CA:TRUE
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always,issuer

[ v3_req ]
basicConstraints     = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment

[ usr_cert ]
basicConstraints       = CA:FALSE
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always
"""
