/* * * Copyright (c) 1999-2005 Apple Computer, Inc. All Rights Reserved. * * @APPLE_LICENSE_HEADER_START@ * * Copyright (c) 1999-2008 Apple Inc. All Rights Reserved. * * This file contains Original Code and/or Modifications of Original Code * as defined in and that are subject to the Apple Public Source License * Version 2.0 (the 'License'). You may not use this file except in * compliance with the License. Please obtain a copy of the License at * http://www.opensource.apple.com/apsl/ and read it before using this * file. * * The Original Code and all software distributed under the License are * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. * Please see the License for the specific language governing rights and * limitations under the License. * * @APPLE_LICENSE_HEADER_END@ * */ /* File: DSAccessChecker.cpp Contains: Class definition for access checking via Open Directory Created By: Dan Sinema Created: Jan 14, 2005 */ /* * Directory Service code added by Dan Sinema * * Jan 14, 2005 - Cleaned up code and added more comments. * Nov 8, 2004 - Finsihed final code. Added group support. * */ // ANSI / POSIX Headers #include #include #include #include #include // STL Headers #include #include #include // Project Headers #include "SafeStdLib.h" #include "StrPtrLen.h" #include "StringParser.h" #include "ResizeableStringFormatter.h" #include "DSAccessChecker.h" #include "DSDataList.h" #include "QTAccessFile.h" #define DEBUG_DSACCESS 0 #define debug_printf if (DEBUG_DSACCESS) ::qtss_printf #include #ifdef AVAILABLE_MAC_OS_X_VERSION_10_5_AND_LATER #if OSX_OD_API #define OD_API 1 #define DS_API 0 #else #define OD_API 0 #define DS_API 1 #endif #else #define OD_API 0 #define DS_API 1 #endif // Framework Headers #if DS_API #include using namespace DirectoryServices; #endif #if OD_API #include #endif #if __LP64__ #define ds_API_PTR UInt32* #else #define ds_API_PTR long unsigned int* #endif #pragma mark DSAccessChecker class globals const char* DSAccessChecker::kDefaultAccessFileName = "qtaccess"; #pragma mark DSAccessChecker class implementation #if DS_API // Find a list of records that match the given criteria. static SInt32 _GetRecordList( tDirReference inDSRef, const char *inDomain, const char *inRecName, const char *inRecType, tDataList *inAttrType, tDirNodeReference *outNodeRef, tDataBuffer **outDataBuff, UInt32 *outRecCount ) { SInt32 status = eDSNoErr; tDataBuffer *pDataBuff = NULL; tDirNodeReference nodeRef = 0; tContextData context = NULL; UInt32 nodeCount = 0; tDataList *nodeName = NULL; UInt32 recCount = 0; *outNodeRef = 0; *outDataBuff = NULL; *outRecCount = 0; pDataBuff = ::dsDataBufferAllocate( inDSRef, 4096 ); if (pDataBuff == NULL) { // We need the buffer for locating the node for which the user object resides debug_printf("QTSSODAuthModule: Unable to allocate buffer.\n"); return eDSAllocationFailed; } // This is the default action with no domain: use the Search node. status = ::dsFindDirNodes( inDSRef, pDataBuff, NULL, eDSSearchNodeName, (ds_API_PTR) &nodeCount, &context ); if ( context != NULL ) { ::dsReleaseContinueData( inDSRef, context ); context = NULL; } // Check for failure of the dsFindDirNodes // Node count less than 1 means no node found...doh! if ( nodeCount < 1 ) { status = eDSNodeNotFound; } if ( status != eDSNoErr ) { goto cleanupBadGetRecordList; } // Extract the name of the found node. status = ::dsGetDirNodeName( inDSRef, pDataBuff, 1, &nodeName ); if (status == eDSNoErr) { // Open the node so we can do the DS magic status = ::dsOpenDirNode( inDSRef, nodeName, &nodeRef ); ::dsDataListDeallocate( inDSRef, nodeName ); std::free( nodeName ); nodeName = NULL; } if (status != eDSNoErr) { // Bail if we cannot open the node. debug_printf("QTSSODAuthModule: Could not open node - error: %"_S32BITARG_"\n", status); } else { // Specify what we are looking for... // pRecName: the passed name of the record // pRecType: the passed name of the record type // pAttrType: attributes to return to the caller DSDataList recName( inDSRef, inRecName ); DSDataList recType( inDSRef, inRecType ); recCount = 1; // Find the record that matchs the above criteria status = ::dsGetRecordList( nodeRef, pDataBuff, recName, eDSExact, recType, inAttrType, 0, (ds_API_PTR)&recCount, &context ); if ( context != NULL ) { ::dsReleaseContinueData( inDSRef, context ); context = NULL; } if ( recCount == 0 ) { status = eDSRecordNotFound; debug_printf("QTSSODAuthModule: No records found.\n"); } else if ( status != eDSNoErr ) { debug_printf("QTSSODAuthModule: No records found - error: %"_S32BITARG_"\n", status); } } if ( status == eDSNoErr ) { *outNodeRef = nodeRef; *outDataBuff = pDataBuff; *outRecCount = recCount; return eDSNoErr; } cleanupBadGetRecordList: if ( nodeRef != 0 ) { ::dsCloseDirNode( nodeRef ); } // This variable is guaranteed to be valid because the function would // have returned if it was bad. ::dsDataBufferDeAllocate( inDSRef, pDataBuff ); return status; } static SInt32 _FindRecordNode( tDirReference inDSRef, const char *inDomain, const char *inRecName, const char *inRecType, tDataList *outHomeNodeName ) { tDataBuffer *pRecBuff = NULL; tDirNodeReference nodeRef = 0; SInt32 status = eDSNoErr; UInt32 attrIndex = 0; UInt32 recCount = 0; tRecordEntry *pRecEntry = NULL; tAttributeListRef attrListRef = 0; if ( outHomeNodeName == NULL ) { return eDSNullDataList; } std::memset( outHomeNodeName, 0, sizeof( *outHomeNodeName) ); // A Username and Password is needed, if either one is not present then bail! if ( inRecName == NULL ) { return eDSInvalidRecordName; } if ( inRecType == NULL ) { return eDSInvalidRecordType; } status = ::_GetRecordList( inDSRef, inDomain, inRecName, inRecType, DSDataList (inDSRef, kDSNAttrMetaNodeLocation), &nodeRef, &pRecBuff, &recCount ); if ( status != eDSNoErr ) { return status; } // Get the record entry out of the list, there should only be one record! status = ::dsGetRecordEntry( nodeRef, pRecBuff, 1, &attrListRef, &pRecEntry ); if ( status != eDSNoErr ) { // These variables are guaranteed to be valid because the function would // have returned if they were bad. ::dsCloseDirNode( nodeRef ); ::dsDataBufferDeAllocate( inDSRef, pRecBuff ); return status; } // Now loop through attributes of the entry...looking for kDSNAttrMetaNodeLocation and kDSNAttrRecordName for ( attrIndex = 1; (attrIndex <= pRecEntry->fRecordAttributeCount) && (status == eDSNoErr); attrIndex++ ) { tAttributeEntryPtr pAttrEntry = NULL; tAttributeValueListRef valueRef = 0; status = ::dsGetAttributeEntry( nodeRef, pRecBuff, attrListRef, attrIndex, &valueRef, &pAttrEntry ); if ( ( status != eDSNoErr ) || ( pAttrEntry == NULL ) ) continue; // Test for kDSNAttrMetaNodeLocation if ( std::strcmp( pAttrEntry->fAttributeSignature.fBufferData, kDSNAttrMetaNodeLocation ) == 0 ) { tAttributeValueEntry *pValueEntry = NULL; // If it matches then get the value of the attribute status = ::dsGetAttributeValue( nodeRef, pRecBuff, 1, valueRef, &pValueEntry ); if ( ( status == eDSNoErr ) && ( pValueEntry != NULL ) ) { // Store the node location in outHomeNodeName if ( outHomeNodeName->fDataNodeCount != 0 ) { debug_printf("QTSSODAuthModule: Multiple user locations found!?\n"); } else { status = ::dsBuildListFromPathAlloc( inDSRef, outHomeNodeName, pValueEntry->fAttributeValueData.fBufferData, "/" ); ::dsDeallocAttributeValueEntry( inDSRef, pValueEntry ); } } } ::dsDeallocAttributeEntry( inDSRef, pAttrEntry ); ::dsCloseAttributeValueList( valueRef ); } // Cleanup dsGetRecordEntry() return values. ::dsCloseAttributeList( attrListRef ); ::dsDeallocRecordEntry( inDSRef, pRecEntry ); ::dsCloseDirNode( nodeRef ); ::dsDataBufferDeAllocate( inDSRef, pRecBuff ); return status; } #endif #pragma mark - #pragma mark "Public Methods" // Now the class proper. DSAccessChecker::DSAccessChecker() { } DSAccessChecker::~DSAccessChecker() { #if DEBUG debug_printf("QTSSODAuthModule: Access checker object destroyed.\n"); #endif } #if 0 //OD_API notes /* This is Leopard or later code so some check before using OD based code is needed. Implement this api for digest auth. */ #include /System/Library/PrivateFrameworks/OpenDirectory.framework/Frameworks/CFOpenDirectory.framework/CFOpenDirectory CFErrorRef outError = NULL; ODNodeRef cfNode = ODNodeCreateWithNodeType( kCFAllocatorDefault, kODSessionDefault, kODTypeAuthenticationSearchNode, NULL ); if (cfNode) { ODRecordRef cfUserRecord = ODNodeCopyRecord( kCFAllocatorDefault, cfNode, CFSTR("username"), NULL ); if (cfUserRecord != NULL) { CFArrayRef authItems = CFArrayCreate.... ( username, server challenge, client response, http method); // for DIGEST_MD5 if (ODRecordVerifyPasswordExtended( cfUserRecord, CFSTR(kDSStdAuthDIGEST_MD5), authItems, NULL, NULL, &outError )) { } // this for password if (ODRecordVerifyPassword( cfUserRecord, CFSTR("password") ) ) { } CFRelease( cfUserRecord ); CFRelease( autItems ); } CFRelease( cfNode ); } // kDSStdAuthDIGEST_MD5 * user name in UTF8 encoding, * server challenge in UTF8 encoding, * client response data, * HTTP method in UTF8 encoding } } #endif #if OD_API Bool16 DSAccessChecker::CheckPassword(const char* inUsername, const char* inPassword) { Bool16 checkedResult = false; CFErrorRef outError = NULL; debug_printf("DSAccessChecker::CheckPassword userName=%s password=%s\n", inUsername,inPassword); ODNodeRef cfNodeRef= ODNodeCreateWithNodeType( kCFAllocatorDefault, kODSessionDefault, kODTypeAuthenticationSearchNode, NULL ); //static ODRecordRef _ODNodeCopyRecord( ODNodeRef inNodeRef, CFStringRef inRecordType, CFStringRef inRecordName, CFArrayRef inAttributes, CFErrorRef *outError ); CFStringRef cfPassword = CFStringCreateWithCString(kCFAllocatorDefault, inPassword, kCFStringEncodingUTF8); CFStringRef cfUsername = CFStringCreateWithCString(kCFAllocatorDefault, inUsername, kCFStringEncodingUTF8); CFTypeRef vals[] = { CFSTR(kDSAttributesStandardAll) }; CFArrayRef reqAttrs = CFArrayCreate(NULL, vals,1, &kCFTypeArrayCallBacks); ODRecordRef cfUserRecord = ODNodeCopyRecord(cfNodeRef, CFSTR(kDSStdRecordTypeUsers), cfUsername, reqAttrs, &outError ); if (cfNodeRef && cfUserRecord && cfPassword && cfUsername) { // this for password if ( ODRecordVerifyPassword( cfUserRecord, cfPassword , NULL ) ) { checkedResult = true; debug_printf("DSAccessChecker::CheckPassword ODRecordVerifyPassword user is authenticated\n"); } else { debug_printf("DSAccessChecker::CheckPassword ODRecordVerifyPassword user failed to authenticate\n"); } } if (reqAttrs) CFRelease( reqAttrs ); if (cfUserRecord) CFRelease( cfUserRecord ); if (cfPassword) CFRelease( cfPassword ); if (cfUsername) CFRelease( cfUsername ); if (cfNodeRef) CFRelease( cfNodeRef ); return checkedResult; } Bool16 DSAccessChecker::CheckDigest(const char* inUsername, const char* inServerChallenge, const char* inClientResponse, const char* inMethod) { Bool16 checkedResult = false; CFErrorRef outError = NULL; CFArrayRef outItems = NULL; if (NULL == inUsername || NULL == inServerChallenge || NULL == inClientResponse ) return false; ResizeableStringFormatter challengeString; challengeString.Put((char*) inServerChallenge); challengeString.PutTerminator(); char* challengeCString= challengeString.GetBufPtr(); ResizeableStringFormatter responseString; responseString.Put( (char*)inClientResponse); responseString.PutTerminator(); char* responseCString= responseString.GetBufPtr(); ODNodeRef cfNode = ODNodeCreateWithNodeType( kCFAllocatorDefault, kODSessionDefault, kODTypeAuthenticationSearchNode, NULL ); debug_printf("DSAccessChecker::CheckDigest \nuserName=[%s] \nchallenge=[%s] \nresponse=[%s] \nmethod=[%s]\n", inUsername,challengeCString, responseCString,inMethod); CFTypeRef vals[] = { CFSTR(kDSAttributesStandardAll) }; CFArrayRef reqAttrs = CFArrayCreate(NULL, vals,1, &kCFTypeArrayCallBacks); CFStringRef cfUsername = CFStringCreateWithCString(kCFAllocatorDefault, inUsername, kCFStringEncodingUTF8); ODRecordRef cfUserRecord = ODNodeCopyRecord(cfNode, CFSTR(kDSStdRecordTypeUsers), cfUsername, reqAttrs, &outError ); CFRelease( cfUsername ); cfUsername = NULL; enum { kNumAuthValues=4 }; CFStringRef cfStringArray[kNumAuthValues]; cfStringArray[0] = CFStringCreateWithCString(kCFAllocatorDefault, inUsername, kCFStringEncodingUTF8); cfStringArray[1] = CFStringCreateWithCString(kCFAllocatorDefault, challengeCString, kCFStringEncodingUTF8); cfStringArray[2] = CFStringCreateWithCString(kCFAllocatorDefault, responseCString, kCFStringEncodingUTF8); cfStringArray[3] = CFStringCreateWithCString(kCFAllocatorDefault, inMethod, kCFStringEncodingUTF8); CFArrayRef cfAuthItems = CFArrayCreate(kCFAllocatorDefault, (const void **) &cfStringArray,kNumAuthValues, &kCFTypeArrayCallBacks); if (cfNode && cfUserRecord && cfAuthItems) { debug_printf("DSAccessChecker::CheckDigest call ODRecordVerifyPasswordExtended\n"); // for DIGEST_MD5 if (ODRecordVerifyPasswordExtended( cfUserRecord, CFSTR(kDSStdAuthDIGEST_MD5), cfAuthItems, &outItems, NULL, &outError )) { checkedResult = true; debug_printf("DSAccessChecker::CheckDigest SUCCESS ODRecordVerifyPasswordExtended response=%d\n",outError); } else { debug_printf("DSAccessChecker::CheckDigest ODRecordVerifyPasswordExtended response=%d\n", outError ? CFErrorGetCode(outError) : -1); } } for (int i = 0; i < kNumAuthValues; i++) CFRelease(cfStringArray[i]); if (reqAttrs) CFRelease( reqAttrs ); if (cfAuthItems) CFRelease( cfAuthItems ); if (cfUserRecord) CFRelease( cfUserRecord ); if (cfNode) CFRelease( cfNode ); if (outItems) CFRelease( outItems ); if (outError) CFRelease( outError ); return checkedResult; } #endif #if DS_API Bool16 DSAccessChecker::CheckPassword(const char* inUsername, const char* inPassword) { tDirReference dsRef = 0; tDataList userNode = { 0, NULL }; tDirNodeReference nodeRef = 0; SInt32 status = eDSNoErr; // A Username and Password is needed, if either one is not present then bail! if ( inUsername == NULL ) { debug_printf("QTSSODAuthModule: Username required.\n"); return false; } if ( inPassword == NULL ) { debug_printf("QTSSODAuthModule: Password required.\n"); return false; } status = ::dsOpenDirService( &dsRef ); if ( status != eDSNoErr ) { // Some DS error, tell the admin what the error is and bail. // Error can be found in DirectoryService man page. debug_printf("QTSSODAuthModule: Could not open Directory Services - error: %"_S32BITARG_"", status); return false; } status = _FindRecordNode( dsRef, NULL, inUsername, kDSStdRecordTypeUsers, &userNode ); if ( status != eDSNoErr ) { debug_printf("QTSSODAuthModule: Could not find user node.\n"); return false; } // Now that we know the node location of the user object, lets open that node. status = ::dsOpenDirNode( dsRef, &userNode, &nodeRef ); ::dsDataListDeallocate( dsRef, &userNode ); if ( status == eDSNoErr ) { UInt32 uiLen = std::strlen( inUsername ); tDataNode *pAuthType = ::dsDataNodeAllocateString( dsRef, kDSStdAuthNodeNativeClearTextOK ); tDataBuffer *pStepBuff = ::dsDataBufferAllocate( dsRef, 128 ); tDataBuffer *pAuthBuff = ::dsDataBufferAllocate( dsRef, ( sizeof( UInt32 ) + sizeof( UInt32 ) + uiLen + std::strlen( inPassword ) ) ); if ( ( pStepBuff != NULL ) && ( pAuthType != NULL ) && ( pAuthBuff != NULL ) ) { UInt32 uiCurr = 0; // Copy username (that is passed into this function) into buffer for dsDoDirNodeAuth() std::memcpy( &(pAuthBuff->fBufferData[ uiCurr ]), &uiLen, sizeof( UInt32 ) ); uiCurr += sizeof( UInt32 ); std::memcpy( &(pAuthBuff->fBufferData[ uiCurr ]), inUsername, uiLen ); uiCurr += uiLen; // Copy password into a buffer for dsDoDirNodeAuth() uiLen = std::strlen( inPassword ); std::memcpy( &(pAuthBuff->fBufferData[ uiCurr ]), &uiLen, sizeof( UInt32 ) ); uiCurr += sizeof( UInt32 ); std::memcpy( &(pAuthBuff->fBufferData[ uiCurr ]), inPassword, uiLen ); uiCurr += uiLen; pAuthBuff->fBufferLength = uiCurr; // Perform the authentication status = ::dsDoDirNodeAuth( nodeRef, pAuthType, 1, pAuthBuff, pStepBuff, NULL ); // Since the buffer held a name & password, clear it immediately. std::memset( pAuthBuff, 0, pAuthBuff->fBufferSize ); } // Free the auth buffer. if ( pAuthBuff != NULL ) { ::dsDataBufferDeAllocate( dsRef, pAuthBuff ); } // Free the ignored step buffer. if ( pStepBuff != NULL ) { ::dsDataBufferDeAllocate( dsRef, pStepBuff ); } // Free the auth string. if ( pAuthType != NULL ) { ::dsDataNodeDeAllocate( dsRef, pAuthType ); } ::dsCloseDirNode( nodeRef ); } ::dsCloseDirService( dsRef ); if( status == eDSNoErr ) { debug_printf("QTSSODAuthModule: Authentication is good.\n"); return true; } // For admins running QTSS in debug debug_printf("QTSSODAuthModule: OD returned %"_S32BITARG_" status.\n", status); debug_printf("QTSSODAuthModule: Authentication failed.\n"); // If the Authentication failed then return false, which boots the user... return false; } Bool16 DSAccessChecker::CheckDigest(const char* inUsername, const char* inServerChallenge, const char* inClientResponse, const char* inMethod) { return false; } #endif #pragma mark - #pragma mark "Protected Methods" Bool16 DSAccessChecker::CheckGroupMembership(const char* inUsername, const char* inGroupName) { // In Tiger, group membership is painfully simple: we ask memberd for it! struct passwd *user = NULL; struct group *group = NULL; uuid_t userID; uuid_t groupID; int isMember = 0; // Look up the user using the POSIX APIs: only care about the UID. user = getpwnam(inUsername); endpwent(); if ( user == NULL ) return false; uuid_clear(userID); if ( mbr_uid_to_uuid(user->pw_uid, userID) ) return false; // Look up the group using the POSIX APIs: only care about the GID. group = getgrnam(inGroupName); endgrent(); if ( group == NULL ) return false; uuid_clear(groupID); if ( mbr_gid_to_uuid(group->gr_gid, groupID) ) return false; // mbr_check_membership() returns 0 on success and error code on failure. if ( mbr_check_membership(userID, groupID, &isMember) ) return false; return (bool)isMember; }