/***********************************************************************
 * 
 *  Copyright (C) 2006 Novell, Inc. All Rights Reserved.
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; version 2.1
 *  of the License.
 *
 *  This library 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
 *  Library Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, Novell, Inc.
 * 
 *  To contact Novell about this file by physical or electronic mail, 
 *  you may find current contact information at www.novell.com.
 * 
 *  Author: Juan Carlos Luciani <jluciani@novell.com>
 *
 ***********************************************************************/

#define _GNU_SOURCE

#include <stdarg.h>
#include <syslog.h>
#include <stdbool.h>

#ifndef LINUX 
#include <security/pam_appl.h>
#endif

#define PAM_SM_AUTH
#define PAM_SM_ACCOUNT
#define PAM_SM_PASSWORD
#define PAM_SM_SESSION

#include <security/pam_modules.h>
#include <security/_pam_macros.h>

#include <casa_s_authtoken.h>

//
// Environment variables set by module
// 
static char CasaIdentityIdEnvVar[] = "IdentityId= ";
static char CasaIdentitySourceNameEnvVar[] = "IdentityDataSourceName= ";
static char CasaIdentitySourceUrlEnvVar[] = "IdentityDataSourceUrl= ";


/* ************************************************************************
 * LogError()
 *
 * Logs error to syslog.
 *
 * L2
 * ************************************************************************/
static void
LogError(char *pFormatStr, ... )
{
   va_list  args;

   openlog("pam_casaauthtok", LOG_CONS | LOG_NOWAIT | LOG_ODELAY, LOG_USER);
   va_start(args, pFormatStr);
   vsyslog(LOG_USER | LOG_INFO, pFormatStr, args);
   va_end(args);
   closelog();
}


/* ************************************************************************
 * pam_sm_authenticate()
 *
 * Service provider implementation for pam_authenticate().
 *
 * This is a PAM authentication management function.
 *
 * We are going to validate the credentials using the CASA Authentication
 * Token Credential APIs.
 *
 * L2
 * ************************************************************************/
PAM_EXTERN int
pam_sm_authenticate(pam_handle_t *pamh,
                    int flags,
                    int argc,
                    const char **argv)
{
   int         retStatus = PAM_SUCCESS;
   bool        performUsernameCheck = false;
   int         i;
   char        *pServicename = NULL;
   char        *pAuthToken = NULL;

   // Verify input parameters
   if (pamh == NULL
       || (argc > 0 && argv == NULL))
   {
      LogError("Invalid parameter detected");
      return PAM_SYSTEM_ERR;
   }

   // Determine if we are supposed to perform the username check
   // based on the arguments specified.
   for (i = 0; i < argc; i++)
   {
      // Do safety check
      if (argv[i] == NULL)
      {
         LogError("Invalid parameter detected");
         return PAM_SYSTEM_ERR;
      }

      if (*(argv[i]) == 'U')
      {
         // The arguments indicate that we should check the username
         performUsernameCheck = true;

         // No need to keep going through the arguments
         break;
      }
   }

   // Get the servicename.
   if (pam_get_item(pamh, PAM_SERVICE, (void*) &pServicename) == PAM_SUCCESS
       && pServicename != NULL)
   {
      // We got the service name, now check if it is necessary to perform
      // the username check.
      if (performUsernameCheck)
      {
         char                 *pUsername = NULL;
         struct pam_response  *responses = NULL;

         // Obtain the username so that it can be checked.
         // .
         // Note that we are not calling pam_get_user() because we
         // assume that the service has set it before calling PAM_Authenticate.
         if (pam_get_item(pamh, PAM_USER, (void*) &pUsername) == PAM_SUCCESS
             && pUsername != NULL)
         {
            // Check if the username matches the name that we are expecting
            if (strcmp(pUsername, "CasaPrincipal") != 0)
            {
               LogError("Un-expected username, %s", pUsername);
               retStatus = PAM_USER_UNKNOWN;
            }
         }
         else
         {
            struct pam_conv *pConv = NULL;

            // The username has not been set, try to obtain it from the
            // application through the use of the conversation function.
            if (pam_get_item(pamh, PAM_CONV, (void*) &pConv) == PAM_SUCCESS
                && pConv != NULL)
            {
               struct pam_message   msg;
               struct pam_message   *messages = &msg;

               // Obtained the conversation structure, now query the conversation
               // function for the username.
               msg.msg_style = PAM_PROMPT_ECHO_ON;
               if (pConv->conv(1,
                               (const struct pam_message **) &messages,
                               &responses,
                               pConv->appdata_ptr) == PAM_SUCCESS
                   && responses != NULL)
               {
                  // Check if we have a successful response
                  if (responses[0].resp_retcode == PAM_SUCCESS
                      && responses[0].resp)
                  {
                     // Check if the username matches the name that we are expecting
                     if (strcmp(responses[0].resp, "CasaPrincipal") != 0)
                     {
                        LogError("Un-expected username, %s", responses[0].resp);
                        retStatus = PAM_USER_UNKNOWN;
                     }
                  }
                  else
                  {
                     LogError("Username not returned");
                     retStatus = PAM_CRED_INSUFFICIENT;
                  }
               }
               else
               {
                  LogError("Conversation function error");
                  retStatus = PAM_AUTH_ERR;
               }
            }
            else
            {
               LogError("Unable to obtain conversation structure");
               retStatus = PAM_AUTH_ERR;
            }
         }

         // Free conversation function response buffers if necessary
         if (responses)
         {
            if (responses[0].resp)
               free(responses[0].resp);
            free(responses);
         }
      }

      // Proceed with the authentication token check if we have not encountered any
      // problems.
      if (retStatus == PAM_SUCCESS)
      {
         struct pam_response  *responses = NULL;

         // Now obtain the authentication token.
         if (pam_get_item(pamh, PAM_AUTHTOK, (void*) &pAuthToken) != PAM_SUCCESS
             || pAuthToken == NULL)
         {
            struct pam_conv *pConv;

            // The authentication token has not been set, try to obtain it from the
            // application through the use of the conversation function.
            if (pam_get_item(pamh, PAM_CONV, (void*) &pConv) == PAM_SUCCESS)
            {
               struct pam_message   msg;
               struct pam_message   *messages = &msg;

               // Obtained the conversation structure, now query the conversation
               // function for the authentication token.
               msg.msg_style = PAM_PROMPT_ECHO_OFF;
               if (pConv->conv(1,
                               (const struct pam_message **) &messages,
                               &responses,
                               pConv->appdata_ptr) == PAM_SUCCESS
                   && responses != NULL)
               {
                  // Check if we have a successful response
                  if (responses[0].resp_retcode == PAM_SUCCESS
                      && responses[0].resp)
                  {
                     // Set the authentication token with PAM
                     if (pam_set_item(pamh, PAM_AUTHTOK, responses[0].resp) == PAM_SUCCESS)
                     {
                        // Use the buffer returned by the caller as the authentication token
                        pAuthToken = responses[0].resp;
                     }
                     else
                     {
                        LogError("Unable to set the authentication token");
                     }
                  }
                  else
                  {
                     LogError("Token not returned");
                  }
               }
               else
               {
                  LogError("Conversation function error");
               }
            }
            else
            {
               LogError("Unable to obtain conversation structure");
            }
         }

         // Check if we succeeded at obtaining the authentication token
         if (pAuthToken)
         {
            CasaStatus  casaStatus;
            PrincipalIf *pPrincipalIf;

            // Validate the token
            casaStatus = ValidateAuthToken(pServicename,
                                           pAuthToken,
                                           strlen(pAuthToken),
                                           &pPrincipalIf);
            if (CASA_SUCCESS(casaStatus))
            {
               size_t   buffLen;

               // Assume success
               retStatus = PAM_SUCCESS;

               // Associate necessary environment variables with the PAM Handle
               buffLen = 0;
               casaStatus = pPrincipalIf->getIdentityId(pPrincipalIf,
                                                        NULL,
                                                        &buffLen);
               if (CasaStatusCode(casaStatus) == CASA_STATUS_BUFFER_OVERFLOW)
               {
                  char  *pBuff;

                  // Allocate buffer to contain the Identity Id Environment Variable
                  pBuff = malloc(sizeof(CasaIdentityIdEnvVar) + buffLen);
                  if (pBuff)
                  {
                     // Start constructing the environment variable
                     memcpy(pBuff, CasaIdentityIdEnvVar, sizeof(CasaIdentityIdEnvVar) - 1);

                     // Read the value into our buffer
                     if (CASA_SUCCESS(pPrincipalIf->getIdentityId(pPrincipalIf,
                                                                  pBuff + sizeof(CasaIdentityIdEnvVar) - 1,
                                                                  &buffLen)))
                     {
                        // Now set the environment variable
                        if (pam_putenv(pamh, pBuff) != PAM_SUCCESS)
                        {
                           LogError("Unable to set identity id environment variable");
                           retStatus = PAM_SYSTEM_ERR;
                        }

                        // Also set the identity id as the username
                        if (pam_set_item(pamh, PAM_USER, pBuff + sizeof(CasaIdentityIdEnvVar) - 1) != PAM_SUCCESS)
                        {
                           LogError("Error setting the username");
                        }
                     }
                     else
                     {
                        LogError("Unable to obtain identity id");
                        retStatus = PAM_SYSTEM_ERR;
                     }

                     // Free allocated buffer
                     free(pBuff);
                  }
                  else
                  {
                     LogError("Buffer allocation failure");
                     retStatus = PAM_BUF_ERR;
                  }
               }
               else
               {
                  LogError("Un-expected error obtaining identity id, %08X", casaStatus);
                  retStatus = PAM_SYSTEM_ERR;
               }

               if (retStatus == PAM_SUCCESS)
               {
                  buffLen = 0;
                  casaStatus = pPrincipalIf->getSourceName(pPrincipalIf,
                                                           NULL,
                                                           &buffLen);
                  if (CasaStatusCode(casaStatus) == CASA_STATUS_BUFFER_OVERFLOW)
                  {
                     char  *pBuff;

                     // Allocate buffer to contain the Identity Source Name Environment Variable
                     pBuff = malloc(sizeof(CasaIdentitySourceNameEnvVar) + buffLen);
                     if (pBuff)
                     {
                        // Start constructing the environment variable
                        memcpy(pBuff, CasaIdentitySourceNameEnvVar, sizeof(CasaIdentitySourceNameEnvVar) - 1);

                        // Read the value into our buffer
                        if (CASA_SUCCESS(pPrincipalIf->getSourceName(pPrincipalIf,
                                                                     pBuff + sizeof(CasaIdentitySourceNameEnvVar) - 1,
                                                                     &buffLen)))
                        {
                           // Now set the environment variable
                           if (pam_putenv(pamh, pBuff) != PAM_SUCCESS)
                           {
                              LogError("Unable to set identity source name environment variable");
                              retStatus = PAM_SYSTEM_ERR;
                           }
                        }
                        else
                        {
                           LogError("Unable to obtain identity source name");
                           retStatus = PAM_SYSTEM_ERR;
                        }

                        // Free allocated buffer
                        free(pBuff);
                     }
                     else
                     {
                        LogError("Buffer allocation failure");
                        retStatus = PAM_BUF_ERR;
                     }
                  }
                  else
                  {
                     LogError("Un-expected error obtaining identity source name, %08X", casaStatus);
                     retStatus = PAM_SYSTEM_ERR;
                  }
               }

               if (retStatus == PAM_SUCCESS)
               {
                  buffLen = 0;
                  casaStatus = pPrincipalIf->getSourceUrl(pPrincipalIf,
                                                          NULL,
                                                          &buffLen);
                  if (CasaStatusCode(casaStatus) == CASA_STATUS_BUFFER_OVERFLOW)
                  {
                     char  *pBuff;

                     // Allocate buffer to contain the Identity Source Url Environment Variable
                     pBuff = malloc(sizeof(CasaIdentitySourceUrlEnvVar) + buffLen);
                     if (pBuff)
                     {
                        // Start constructing the environment variable
                        memcpy(pBuff, CasaIdentitySourceUrlEnvVar, sizeof(CasaIdentitySourceUrlEnvVar) - 1);

                        // Read the value into our buffer
                        if (CASA_SUCCESS(pPrincipalIf->getSourceUrl(pPrincipalIf,
                                                                    pBuff + sizeof(CasaIdentitySourceUrlEnvVar) - 1,
                                                                    &buffLen)))
                        {
                           // Now set the environment variable
                           if (pam_putenv(pamh, pBuff) != PAM_SUCCESS)
                           {
                              LogError("Unable to set identity source url environment variable");
                              retStatus = PAM_SYSTEM_ERR;
                           }
                        }
                        else
                        {
                           LogError("Unable to obtain identity source url");
                           retStatus = PAM_SYSTEM_ERR;
                        }

                        // Free allocated buffer
                        free(pBuff);
                     }
                     else
                     {
                        LogError("Buffer allocation failure");
                        retStatus = PAM_BUF_ERR;
                     }
                  }
                  else
                  {
                     LogError("Un-expected error obtaining identity source url, %08X", casaStatus);
                     retStatus = PAM_SYSTEM_ERR;
                  }
               }

               if (retStatus == PAM_SUCCESS)
               {
                  char           *pBuff;
                  unsigned int   enumHandle = 0;
                  size_t         buff2Len;

                  while (retStatus == PAM_SUCCESS)
                  {
                     // Get attribute lengths
                     buffLen = buff2Len = 0;
                     casaStatus = pPrincipalIf->attributeEnumerate(pPrincipalIf,
                                                                   &enumHandle,
                                                                   NULL,
                                                                   &buffLen,
                                                                   NULL,
                                                                   &buff2Len);
                     if (CasaStatusCode(casaStatus) == CASA_STATUS_BUFFER_OVERFLOW)
                     {
                        // Allocate buffer to contain the Identity attribute Environment Variable
                        pBuff = malloc(buffLen + 2 + buff2Len);
                        if (pBuff)
                        {
                           // Read the attribute into our buffer
                           if (CASA_SUCCESS(pPrincipalIf->attributeEnumerate(pPrincipalIf,
                                                                             &enumHandle,
                                                                             pBuff,
                                                                             &buffLen,
                                                                             pBuff + buffLen + 1, // This includes the NULL terminator
                                                                             &buff2Len)))
                           {
                              // Finish constructing the environment variable string
                              *(pBuff + buffLen - 1) = '=';
                              *(pBuff + buffLen) = ' ';

                              // Now set the environment variable
                              if (pam_putenv(pamh, pBuff) != PAM_SUCCESS)
                              {
                                 LogError("Unable to set identity attribute environment variable");
                                 retStatus = PAM_SYSTEM_ERR;
                              }
                           }
                           else
                           {
                              LogError("Unable to obtain identity attribute");
                              retStatus = PAM_SYSTEM_ERR;
                           }

                           // Free allocated buffer
                           free(pBuff);
                        }
                        else
                        {
                           LogError("Buffer allocation failure");
                           retStatus = PAM_BUF_ERR;
                        }
                     }
                     else
                     {
                        // Check if we are done going through the attributes
                        if (CasaStatusCode(casaStatus) == CASA_STATUS_NO_MORE_ENTRIES)
                        {
                           // Done
                           break;
                        }
                        else
                        {
                           LogError("Un-expected error during attribute enumeration, %08X", casaStatus);
                           retStatus = PAM_SYSTEM_ERR;
                        }
                     }
                  }
               }

               // Release the principal interface instance
               pPrincipalIf->releaseReference(pPrincipalIf);
            }
            else
            {
               LogError("Service %s failed to authenticate with status = %08X", pServicename, casaStatus);
               retStatus = PAM_AUTH_ERR;
            }
         }
         else
         {
            LogError("Unable to obtain authentication token");
            retStatus = PAM_CRED_INSUFFICIENT;
         }

         // Free conversation function response buffers if necessary
         if (responses)
         {
            if (responses[0].resp)
               free(responses[0].resp);
            free(responses);
         }
      }
   }
   else
   {
      LogError("Unable to obtain servicename");
      retStatus = PAM_SYSTEM_ERR;
   }

   return retStatus;
}


/* ************************************************************************
 * pam_sm_setcred()
 *
 * Service provider implementation for pam_setcred().
 *
 * This is a PAM authentication management function.
 *
 * This function is here just for completedness and to protect against
 * PAM misconfiguration.
 *
 * ************************************************************************/
PAM_EXTERN int
pam_sm_setcred(pam_handle_t *pamh,
               int flags,
               int argc,
               const char **argv)
{
   return PAM_SUCCESS;
}


/* ************************************************************************
 * pam_sm_acct_mgmt()
 *
 * Service provider implementation for pam_acct_mgmt().
 *
 * This is a PAM account management function.
 *
 * This function is here just for completedness and to protect against
 * PAM misconfiguration.
 *
 * ************************************************************************/
PAM_EXTERN int
pam_sm_acct_mgmt(pam_handle_t *pamh,
                 int flags,
                 int argc,
                 const char **argv)
{
   return PAM_SUCCESS;
}


/* ************************************************************************
 * pam_sm_chauthtok()
 *
 * Service provider implementation for pam_chauthtok().
 *
 * This is a PAM password management function.
 *
 * This function is here just for completedness and to protect against
 * PAM misconfiguration.
 *
 * ************************************************************************/
PAM_EXTERN int
pam_sm_chauthtok(pam_handle_t *pamh,
                 int flags,
                 int argc,
                 const char **argv)
{
   return PAM_SUCCESS;
}


/* ************************************************************************
 * pam_sm_open_session()
 *
 * Service provider implementation for pam_open_session().
 *
 * This is a PAM session management function.
 *
 * This function is here just for completedness and to protect against
 * PAM misconfiguration.
 *
 * ************************************************************************/
PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh,
                                   int flags,
                                   int argc,
                                   const char **argv)
{
   return PAM_SUCCESS;
}


/* ************************************************************************
 * pam_sm_close_session()
 *
 * Service provider implementation for pam_close_session().
 *
 * This is a PAM session management function.
 *
 * This function is here just for completedness and to protect against
 * PAM misconfiguration.
 *
 * ************************************************************************/
PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh,
                                    int flags,
                                    int argc,
                                    const char **argv)
{
   return PAM_SUCCESS;
}


/* static module data */
#ifdef PAM_STATIC
struct pam_module _pam_casa_authtoken_modstruct = {
   "pam_casa_authtoken",
   pam_sm_authenticate,
   pam_sm_setcred,
   pam_sm_acct_mgmt,
   pam_sm_chauthtok,
   pam_sm_open_session,
   pam_sm_close_session
};
#endif