/***************************************************************
 *
 * Copyright (C) 1990-2007, Condor Team, Computer Sciences Department,
 * University of Wisconsin-Madison, WI.
 * 
 * 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.
 *
 ***************************************************************/


#if !defined(WIN32)
#include "condor_auth_fs.h"
#include "condor_config.h"
#include "condor_environ.h"
#include "CondorError.h"
#include "condor_mkstemp.h"
#include "ipv6_hostname.h"
#include "basename.h"
#include "condor_sinful.h"
#include "condor_daemon_core.h"

Condor_Auth_FS :: Condor_Auth_FS(ReliSock * sock, int remote)
    : Condor_Auth_Base    ( sock, CAUTH_FILESYSTEM ),
      remote_             ( remote )
{
}

Condor_Auth_FS :: ~Condor_Auth_FS()
{
}

int Condor_Auth_FS::authenticate(const char * /* remoteHost */, CondorError* errstack, bool non_blocking)
{
	int client_result = -1;
	int server_result = -1;
	int fail = -1 == 0;

	std::string parent_dir;
	if (remote_) {
		param(parent_dir, "FS_REMOTE_DIR");
		if (parent_dir.empty()) {
			// misconfiguration.  complain, and then use /tmp
			dprintf (D_ERROR, "AUTHENTICATE_FS: FS_REMOTE was used but no FS_REMOTE_DIR defined!\n");
		}
	} else {
		param(parent_dir, "FS_LOCAL_DIR");
	}
	if (parent_dir.empty()) {
		parent_dir = "/tmp";
	}
	while (parent_dir.back() == '/') {
		parent_dir.pop_back();
	}

	if ( mySock_->isClient() ) {
		std::string new_dir;
		std::string ext;

		// receive the directory name to create
		mySock_->decode();
		if (!mySock_->code( new_dir )) { 
			dprintf(D_SECURITY, "Protocol failure at %s, %d!\n",
				__FUNCTION__, __LINE__);
			return fail; 
		}

		if (!mySock_->end_of_message()) { 
			dprintf(D_SECURITY, "Protocol failure at %s, %d!\n",
				__FUNCTION__, __LINE__);
			return fail; 
		}

		// Until session caching supports clients that present
		// different identities to the same service at different
		// times, we want condor daemons to always authenticate
		// as condor priv, rather than as the current euid.
		// For tools and daemons not started as root, this
		// is a no-op.
		TemporaryPrivSentry sentry(PRIV_CONDOR);

		if (new_dir.empty()) {
			// server sends empty string if its mktemp failed
			client_result = -1;  // redundant, but safety first!
			errstack->pushf((remote_?"FS_REMOTE":"FS"), 1001,
			                "Server Error, check server log.%s",
			                (remote_?" FS_REMOTE_DIR is likely misconfigured.":""));
			goto client_reply;
		}

		// Perform some sanity checks on the directory the server sent...
		if (param_boolean("SEC_FS_ENFORCE_CHANNEL_BINDING", true)) {
			// If the server's filename ends in '_', then it wants us to
			// add our view of its ip:port to the end of the filename
			// to perform channel binding (and tell it via CEDAR what we
			// added), instead of validating the ip:port it included in
			// the filename.
			// In some networking environments, our view of the server's
			// ip address may not match its view for this connection,
			// even if we're on the same machine. Particularly, when it's
			// using NAT and TCP_FORWARDING_HOST.
			if (new_dir.back() == '_') {
				ext = mySock_->peer_addr().to_ip_and_port_string();
				new_dir += ext;
				dprintf(D_SECURITY|D_VERBOSE, "AUTHENTICATE_FS%s: Adding server ip:port to directory name: %s\n", (remote_?"_REMOTE":""), ext.c_str());
			} else {
				std::string new_dir_prefix;
				formatstr(new_dir_prefix, "%s_%s_%d_", (remote_?"FS_REMOTE":"FS"), mySock_->peer_ip_str(), mySock_->peer_port());
				const char* new_dir_basename = condor_basename(new_dir.c_str());
				if (strncmp(new_dir_basename, new_dir_prefix.c_str(), new_dir_prefix.size()) != 0) {
					dprintf(D_SECURITY, "AUTHENTICATE_FS%s: Server-requested directory '%s' doesn't match expected prefix '%s'\n", (remote_?"_REMOTE":""), new_dir_basename, new_dir_prefix.c_str());
					errstack->push((remote_?"FS_REMOTE":"FS"), 1004,
						"Directory name doesn't match server's network address.");
					goto client_reply;
				}
			}
		}

		// try to create the directory the server sent.
		// mkdir has just the properties we need here.
		// it will fail if it already exists (like O_EXCL) and
		// can be created with initial proper permissions.
		client_result = mkdir(new_dir.c_str(), 0700);
		if (client_result == -1) {
			errstack->pushf((remote_?"FS_REMOTE":"FS"), 1000,
					"mkdir(%s, 0700): %s (%i)",
					new_dir.c_str(), strerror(errno), errno);
		}

	client_reply:
		// send over result as a success/failure indicator (-1 == failure)
		// If we added characters to the filename, send that as well.
		mySock_->encode();
		if (!mySock_->code( client_result ) || !(ext.empty() || mySock_->code(ext)) || !mySock_->end_of_message()) {
			dprintf(D_SECURITY, "Protocol failure at %s, %d!\n",
				__FUNCTION__, __LINE__);
			if (!new_dir.empty()) {
				rmdir(new_dir.c_str());
			}
			return fail; 
		}

		// now let the server verify that we created the dir correctly
		mySock_->decode();
		if (!mySock_->code( server_result ) || !mySock_->end_of_message()) { 
			dprintf(D_SECURITY, "Protocol failure at %s, %d!\n",
				__FUNCTION__, __LINE__);
			if (!new_dir.empty()) {
				rmdir(new_dir.c_str());
			}
			return fail; 
		}

		if (client_result != -1) {
			rmdir(new_dir.c_str());
		}

		dprintf( D_SECURITY, "AUTHENTICATE_FS%s: used dir %s, status: %d\n",
				(remote_?"_REMOTE":""),
				new_dir.c_str(), (server_result == 0) );

		// this function returns TRUE on success, FALSE on failure,
		// which is just the opposite of server_result.
		return( server_result == 0 );

	} else {

		// server code
		setRemoteUser( NULL );

		std::string filename;
		formatstr(filename, "%s/%s_%s_%d_XXXXXXXXX",
		          parent_dir.c_str(), (remote_?"FS_REMOTE":"FS"), mySock_->my_ip_str(), mySock_->get_port());
		dprintf(D_SECURITY, "%s: client template is %s\n", (remote_?"FS_REMOTE":"FS"), filename.c_str());

		int sync_fd;
		char * new_dir = strdup(filename.c_str());
		sync_fd = condor_mkstemp(new_dir);
		m_new_dir = new_dir;
		free(new_dir);
		if( sync_fd < 0 ) {
			// path must be invalid?
			//
			// this means that mktemp set new_dir to the empty string.  we
			// code this across.  old clients will try it and fail, and new
			// clients will know that the server had trouble.  for the
			// error message we print the filename, not new_dir which is
			// empty.
			int en = errno;  // strerror could replace errno!
			errstack->pushf((remote_?"FS_REMOTE":"FS"), 1002,
					"condor_mkstemp(%s) failed: %s (%i)",
					filename.c_str(), strerror(en), en);
			m_new_dir = "";			//the other process will expect an
									//empty string on failure
		} else {
			::close( sync_fd );
			unlink( m_new_dir.c_str() );
			// Ending the name in '_' is an invitation for the client to
			// append their view of our ip:port for this connection to the
			// end of the filename and tell us via CEDAR what they added.
			m_new_dir += '_';
			dprintf( D_SECURITY, "%s: client filename is %s\n", (remote_?"FS_REMOTE":"FS"), m_new_dir.c_str() );
		}

		// now, send over directory name for client to create
		// man page says string returned by tempnam has been malloc()'d
		// except in the case of error, where we malloc an empty string.
		mySock_->encode();
		if (!mySock_->code( m_new_dir ) || !mySock_->end_of_message()) {
			dprintf(D_SECURITY, "Protocol failure at %s, %d!\n",
				__FUNCTION__, __LINE__);
			return fail;
		}
		return authenticate_continue(errstack, non_blocking);
	}
}

int Condor_Auth_FS::authenticate_continue(CondorError* errstack, bool non_blocking)
{
	bool used_file = false;
	int client_result = -1;
	int server_result = -1;
	int fail = -1 == 0;
	std::string client_ext;

	if( non_blocking && !mySock_->readReady() ) {
		return 2;
	}

	// see if the client claims success (it could be lying of course!)
	// The client can optionally tell us that they added some characters
	// to the end of the filename.
	mySock_->decode();
	if (!mySock_->code( client_result ) || !(mySock_->peek_end_of_message() || mySock_->code(client_ext)) || !mySock_->end_of_message()) {
		dprintf(D_SECURITY, "Protocol failure at %s, %d!\n",
			__FUNCTION__, __LINE__);
		return fail;
	}

	// we're going to respond to the client with success or failure
	mySock_->encode();

	// assume failure.  i am glass-half-empty man.
	server_result = -1;

	// but first, we need to verify that everything is correct
	if (!client_ext.empty()) {
		// If the client added characters to the end of the filename, check
		// that they represent a valid ip:port for contacting us.
		bool client_ext_matches = false;
		dprintf(D_SECURITY|D_VERBOSE, "AUTHENTICATE_FS%s: Client sent extension '%s'\n", (remote_?"_REMOTE":""), client_ext.c_str());
		// First, prevent directory escape via '/'.
		if (condor_basename(client_ext.c_str()) != client_ext.c_str()) {
			dprintf(D_SECURITY, "AUTHENTICATE_FS%s: Client extension contains directory separator (%s)\n", (remote_?"_REMOTE":""), client_ext.c_str());
			errstack->pushf((remote_?"FS_REMOTE":"FS"), 1004,
		                "Client extension contains directory separator (%s)", client_ext.c_str());
			server_result = -1;
			goto send_reply;
		}
		condor_sockaddr client_peer;
		if (!client_peer.from_ip_and_port_string(client_ext.c_str())) {
			dprintf(D_SECURITY, "AUTHENTICATE_FS%s: Client extension not valid ip:port (%s)\n", (remote_?"_REMOTE":""), client_ext.c_str());
			errstack->pushf((remote_?"FS_REMOTE":"FS"), 1004,
		                "Client extension not valid ip:port (%s)", client_ext.c_str());
			server_result = -1;
			goto send_reply;
		}
		// Check if the client's view of our endpoint of the TCP connection
		// matches our own.
		if (client_peer == mySock_->my_addr()) {
			client_ext_matches = true;
		}
		// If we're a daemon, check if the client's view of our endpoint
		// of the TCP connection appears in our command port sinful string.
		// This check handles situations where we're using NAT and
		// TCP_FORWARDING_HOST.
		if (!client_ext_matches && daemonCore && daemonCore->InfoCommandSinfulStringMyself(false)) {
			Sinful myself(daemonCore->InfoCommandSinfulStringMyself(false));
			myself.setSharedPortID(nullptr);
			if (myself.addressPointsToMe(Sinful(client_peer.to_ip_and_port_string().c_str()))) {
				client_ext_matches = true;
			}
		}
		if (!client_ext_matches) {
			dprintf(D_SECURITY, "AUTHENTICATE_FS%s: Client extension (%s) doesn't match our endpoint\n", (remote_?"_REMOTE":""), client_ext.c_str());
			errstack->pushf((remote_?"FS_REMOTE":"FS"), 1004,
		                "Client extension (%s) doesn't match our endpoint", client_ext.c_str());
			server_result = -1;
			goto send_reply;
		}
	}
	if ( client_result != -1  && m_new_dir.size() && m_new_dir[0]) {

		if (remote_) {
			// now, if this is a remote filesystem, it is possibly NFS.
			// before we stat the file, we need to do something that causes the
			// NFS client to sync to the NFS server.  fsync() does not do this.
			// in practice, creating a file or directory should force the NFS
			// client to sync in order to avoid race conditions.
			std::string filename_template = "/tmp";

			char * rendezvous_dir = param("FS_REMOTE_DIR");
			if (rendezvous_dir) {
				filename_template = rendezvous_dir;
				free(rendezvous_dir);
			}

			pid_t    mypid = 0;
#ifdef WIN32
			mypid = ::GetCurrentProcessId();
#else
			mypid = ::getpid();
#endif

			// construct the template. mkstemp modifies the string you pass
			// in so we create a dup of it just in case MyString does
			// anything funny or uses string spaces, etc.
			formatstr_cat( filename_template, "/FS_REMOTE_%s_%d_XXXXXX",
			               get_local_hostname().c_str(), mypid );
			char* filename_inout = strdup(filename_template.c_str());

			dprintf( D_SECURITY, "FS_REMOTE: sync filename is %s\n", filename_inout );

			int sync_fd = condor_mkstemp(filename_inout);
			if (sync_fd >= 0) {
				::close(sync_fd);
				unlink( filename_inout );
			} else {
				// we could have an else that fails here, but we may as well still
				// check for the file -- this was just an attempt to make NFS sync.
				// if the file still isn't there, we'll fail anyways.
				dprintf( D_ALWAYS, "FS_REMOTE: warning, failed to make temp file %s\n", filename_inout );
			}

			free (filename_inout);

			// at this point we should have created and deleted a unique file
			// from the server side, forcing the nfs client to flush everything
			// and sync with the server.  now, we can finally stat the file.
		}

		m_new_dir += client_ext;

		//check dir to ensure that claimant is owner
		struct stat stat_buf;

		// verify the dir's attributes by stat()ing the dir
		if ( lstat( m_new_dir.c_str(), &stat_buf ) < 0 ) {
			server_result = -1;  
			errstack->pushf((remote_?"FS_REMOTE":"FS"), 1004,
					"Unable to lstat(%s)", m_new_dir.c_str());
		} else {
			// Authentication should fail if a) owner match fails, or b) the
			// directory is either a hard or soft link (no links allowed
			// because they could spoof the owner match).  -Todd 3/98
			//
			// also, it must now be a directory instead of a file, due to
			// hardlink trickery where you could in fact create a hard link
			// (with count 1!) as another user with the supplied filename.
			// however, normal users cannot create hardlinks to
			// directories, only root.  -Zach 8/06

			// The test of link count is historical.  In btrfs,
			// directories have link count 1, unlike other
			// filesystems in which the link count is 2.  We do
			// not believe there is any reason to care about the
			// link count, but since it was already required to be
			// 2, and we want to make it work on btrfs, we are
			// making a conservative change in 7.6.5 that allows
			// the link count to be 1 or 2.  -Dan 11/11

			// The CREATE_LOCKS_ON_LOCAL_DISK code relies on
			// creating directories with mode=0777 that may be
			// owned by any user who happens to need a lock. This
			// is especially true for users with running
			// jobs. Because local lock code uses a tree of these
			// directories, it is possibly for a malicious user to
			// satify FS authentication requirements by renamed a
			// target user's lock directory into /tmp. To avoid
			// this attack, the server side must also fail
			// authentication for any directories whose mode is
			// not 0700. -matt July 2012

			// assume it is wrong until we see otherwise
			bool stat_is_okay = false;

			if ((stat_buf.st_nlink == 1 || stat_buf.st_nlink == 2) &&
				(!S_ISLNK(stat_buf.st_mode)) && // check for soft link
				(S_ISDIR(stat_buf.st_mode)) &&      // check for directory
				((stat_buf.st_mode & 07777) == 0700)) // check for mode=0700
			{
					// client created a directory as instructed
				stat_is_okay = true;
			} else {
				// it wasn't proper.  however, there is a config knob for
				// backwards compatibility that allows either a file or a
				// directory.  THIS IS NOT SECURE.  it is off by default.
				if (param_boolean("FS_ALLOW_UNSAFE", false)) {
					if ((stat_buf.st_nlink == 1) &&	    // check hard link count
						(!S_ISLNK(stat_buf.st_mode)) && // check for soft link
						(S_ISREG(stat_buf.st_mode)) )   // check for regular file
					{
						stat_is_okay = true;
						used_file = true;
					}
				}
			}

			if (!stat_is_okay) {
				// something was wrong with the stat info
				server_result = -1;
				errstack->pushf((remote_?"FS_REMOTE":"FS"), 1005,
						"Bad attributes on (%s)", m_new_dir.c_str());
			} else {
				// need to lookup username from dir and do setOwner()
				char *tmpOwner = nullptr;
				// failure is detected via the tmpOwner null check below
				std::ignore = pcache()->get_user_name(stat_buf.st_uid, tmpOwner);
				if (!tmpOwner) {
					// this could happen if, for instance,
					// getpwuid() failed.
					server_result = -1;
					errstack->pushf((remote_?"FS_REMOTE":"FS"), 1006,
							"Unable to lookup uid %i", stat_buf.st_uid);
				} else {
					server_result = 0;	// 0 means success here. sigh.
					// Map user 'root' to the condor user. The client attempts
					// to switch euid to the condor user for FS authentication,
					// but some code has all priv-switching disabled.
					if (param_boolean("FS_ROOT_TO_CONDOR", true) && strcmp(tmpOwner, "root") == 0 && get_condor_username()) {
						dprintf(D_SECURITY, "FS Mapping 'root' to '%s'\n", get_condor_username());
						free(tmpOwner);
						tmpOwner = strdup(get_condor_username());
					}
					setRemoteUser( tmpOwner );
					setAuthenticatedName( tmpOwner );
					free( tmpOwner );
					setRemoteDomain( getLocalDomain() );
				}
			}
		}
	} else {
		server_result = -1;
		if (m_new_dir.size() && m_new_dir[0]) {
			errstack->pushf((remote_?"FS_REMOTE":"FS"), 1007,
					"Client unable to create dir (%s)", m_new_dir.c_str());
		}
		// else we gave the client a bogus name to begin with!
	}

 send_reply:
	if (!mySock_->code( server_result ) || !mySock_->end_of_message())
	{
		dprintf(D_SECURITY, "Protocol failure at %s, %d!\n",
			__FUNCTION__, __LINE__);
		return fail;
	}
	// leave new_dir allocated until after dprintf below

	dprintf( D_SECURITY, "AUTHENTICATE_FS%s: used %s %s, status: %d\n", 
			(remote_?"_REMOTE":""), (used_file?"file":"dir"),
			(m_new_dir.size() ? m_new_dir.c_str() : "(null)"), (server_result == 0) );

	// this function returns TRUE on success, FALSE on failure,
	// which is just the opposite of server_result.
	return( server_result == 0 );
}

int Condor_Auth_FS :: isValid() const
{
    return TRUE;
}

#endif
