/**
 * Copyright 2016 Yahoo Inc.
 *
 * 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 com.yahoo.athenz.auth.impl;

import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.yahoo.athenz.auth.Authority;
import com.yahoo.athenz.auth.Principal;
import com.yahoo.athenz.auth.util.Crypto;

public class CertificateAuthority implements Authority {

    private static final Logger LOG = LoggerFactory.getLogger(CertificateAuthority.class);

    @Override
    public void initialize() {
    }

    @Override
    public String getDomain() {
        return null;
    }

    @Override
    public String getHeader() {
        return null;
    }

    @Override
    public Principal authenticate(String creds, String remoteAddr, String httpMethod, StringBuilder errMsg) {
        return null;
    }

    @Override
    public CredSource getCredSource() {
        return CredSource.CERTIFICATE;
    }
    
    @Override
    public Principal authenticate(X509Certificate[] certs, StringBuilder errMsg) {

        if (LOG.isDebugEnabled()) {
            LOG.debug("CertificateAuthority:authenticate: TLS Certificates: " + certs);
            if (certs != null) {
                for (X509Certificate cert : certs) {
                    LOG.debug("CertificateAuthority:authenticate: TLS Certificate: " + cert);
                }
            }
        }

        errMsg = errMsg == null ? new StringBuilder(512) : errMsg;

        // make sure we have at least one valid certificate in our list
        
        if (certs == null || certs[0] == null) {
            errMsg.append("CertificateAuthority:authenticate: No certificate available in request");
            return null;
        }
        
        X509Certificate x509Cert = certs[0];
        String principalName = Crypto.extractX509CertCommonName(x509Cert);
        if (principalName == null || principalName.isEmpty()) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("CertificateAuthority:authenticate: Certificate principal is empty");
            }
            errMsg.append("CertificateAuthority:authenticate: Certificate principal is empty");
            return null;
        }

        // For role cert, the principal information is in the SAN email
        
        List<String> roles = null;
        int idx = principalName.indexOf(":role.");
        if (idx != -1) {
            
            // fist we need to keep the role name in our object
            
            roles = new ArrayList<>();
            roles.add(principalName);
            
            // now extract the email field
            
            List<String> emails = Crypto.extractX509CertEmails(x509Cert);
            if (emails.isEmpty()) {
                errMsg.append("CertificateAuthority:authenticate: Invalid role cert, no email SAN entry"
                    + principalName);
                return null;
            }
            String email = emails.get(0);
            idx = email.indexOf('@');
            if (idx == -1) {
                errMsg.append("CertificateAuthority:authenticate: Invalid role cert, invalid email SAN entry"
                    + principalName);
                return null;
            }
            principalName = email.substring(0, idx);
        }

        // extract domain and service names from the name. We must have
        // a valid service identity in the form domain.service

        idx = principalName.lastIndexOf('.');
        if (idx == -1) {
            errMsg.append("CertificateAuthority:authenticate: Principal is not a valid service identity: "
                + principalName);
            return null;
        }

        String domain = principalName.substring(0, idx);
        String name = principalName.substring(idx + 1);
        
        // all the role members in Athenz are normalized to lower case so we need to make
        // sure our principal's name and domain are created with lower case as well
        
        SimplePrincipal principal = (SimplePrincipal) SimplePrincipal.create(domain.toLowerCase(),
                name.toLowerCase(), x509Cert.toString(), this);
        principal.setUnsignedCreds(x509Cert.getSubjectX500Principal().toString());
        principal.setX509Certificate(x509Cert);
        principal.setRoles(roles);
        return principal;
    }
}

@@ -713,19 +713,92 @@
 va.util.
-Lis
+Arrays;%0Aimport java.util.HashSet;%0Aimport java.util.List;%0Aimport java.util.Se
 t;%0A%0Aimpo
@@ -1127,53 +1127,469 @@
 s);%0A
-%0A    @Override%0A    public void initialize() %7B
+    static final String ATHENZ_PROP_EXCLUDED_PRINCIPALS = %22athenz.auth.certificate.excluded_principals%22;%0A%0A    Set%3CString%3E excludedPrincipalSet = null;%0A    %0A    @Override%0A    public void initialize() %7B%0A        %0A        final String exPrincipals = System.getProperty(ATHENZ_PROP_EXCLUDED_PRINCIPALS);%0A        if (exPrincipals != null && !exPrincipals.isEmpty()) %7B%0A            excludedPrincipalSet = new HashSet%3C%3E(Arrays.asList(exPrincipals.split(%22,%22)));%0A        %7D
 %0A   
@@ -2984,62 +2984,31 @@
     
-if (LOG.isDebugEnabled()) %7B%0A                LOG.debug(
+final String message = 
 %22Cer
@@ -3074,118 +3074,554 @@
 pty%22
-);%0A            %7D%0A            errMsg.append(%22CertificateAuthority:authenticate: Certificate principal is empty%22
+;%0A            if (LOG.isDebugEnabled()) %7B%0A                LOG.debug(message);%0A            %7D%0A            errMsg.append(message);%0A            return null;%0A        %7D%0A%0A        // make sure the principal is not on our excluded list%0A        %0A        if (excludedPrincipalSet != null && excludedPrincipalSet.contains(principalName)) %7B%0A            final String message = %22CertificateAuthority:authenticate: Principal is excluded%22;%0A            if (LOG.isDebugEnabled()) %7B%0A                LOG.debug(message);%0A            %7D%0A            errMsg.append(message
 );%0A 
@@ -3646,32 +3646,40 @@
 null;%0A        %7D%0A
+        
 %0A        // For 

configure option to exclude principals from cert authority (#267)