/*++ Copyright (c) 1991 Microsoft Corporation Module Name: dbmisc.c Abstract: Local Security Authority - Miscellaneous API This file contains worker routines for miscellaneous API that are not specific to objects of a given type. Author: Scott Birrell (ScottBi) January 15, 1992 Environment: Revision History: --*/ #include #include "dbp.h" #include "lsawmi.h" #include #include #include #include "adtp.h" NTSTATUS LsarClose( IN OUT LSAPR_HANDLE *ObjectHandle ) /*++ Routine Description: This function is the LSA server RPC worker routine for the LsaClose API. Arguments: ObjectHandle - Handle returned from an LsaOpen or LsaCreate call. Return Value: NTSTATUS - Standard Nt Result Code --*/ { return LsapCloseHandle( ObjectHandle, STATUS_SUCCESS ); } NTSTATUS LsapCloseHandle( IN OUT LSAPR_HANDLE *ObjectHandle, IN NTSTATUS PreliminaryStatus ) /*++ Routine Description: This function is the LSA server RPC worker routine for the LsaClose API. The LsaClose API closes a handle to an open object within the database. If closing a handle to the Policy object and there are no objects still open within the current connection to the LSA, the connection is closed. If a handle to an object within the database is closed and the object is marked for DELETE access, the object will be deleted when the last handle to that object is closed. Arguments: ObjectHandle - Handle returned from an LsaOpen or LsaCreate call. PreliminaryStatus - Status of the operation. Used to decide whether to abort or commit a transaction. Return Value: NTSTATUS - Standard Nt Result Code --*/ { NTSTATUS Status; LSAP_DB_HANDLE InternalHandle = *ObjectHandle; LSAP_DB_OBJECT_TYPE_ID ObjectTypeId; LsapDsDebugOut(( DEB_FTRACE, "LsapCloseHandle\n" )); LsapTraceEvent(EVENT_TRACE_TYPE_START, LsaTraceEvent_Close); if ( *ObjectHandle == NULL ) { Status = STATUS_INVALID_HANDLE; goto Cleanup; } ObjectTypeId = InternalHandle->ObjectTypeId; if ( *ObjectHandle == LsapPolicyHandle ) { #if DBG DbgPrint("Closing global policy handle!!!\n"); #endif DbgBreakPoint(); Status = STATUS_INVALID_HANDLE; goto Cleanup; } // // Acquire the Lsa Database Lock // LsapDbAcquireLockEx( ObjectTypeId, LSAP_DB_READ_ONLY_TRANSACTION ); // // Validate and close the object handle, dereference its container (if any). // Status = LsapDbCloseObject( ObjectHandle, LSAP_DB_VALIDATE_HANDLE | LSAP_DB_DEREFERENCE_CONTR | LSAP_DB_ADMIT_DELETED_OBJECT_HANDLES | LSAP_DB_READ_ONLY_TRANSACTION | LSAP_DB_DS_OP_TRANSACTION, PreliminaryStatus ); LsapDbReleaseLockEx( ObjectTypeId, LSAP_DB_READ_ONLY_TRANSACTION ); Cleanup: *ObjectHandle = NULL; LsapDsDebugOut(( DEB_FTRACE, "LsapCloseHandle: 0x%lx\n", Status )); LsapTraceEvent(EVENT_TRACE_TYPE_END, LsaTraceEvent_Close); return Status; } NTSTATUS LsarDeleteObject( IN OUT LSAPR_HANDLE *ObjectHandle ) /*++ Routine Description: This function is the LSA server RPC worker routine for the LsaDelete API. The LsaDelete API deletes an object from the LSA Database. The object must be open for DELETE access. Arguments: ObjectHandle - Pointer to Handle from an LsaOpen or LsaCreate call. On return, this location will contain NULL if the call is successful. Return Value: NTSTATUS - Standard Nt Result Code STATUS_ACCESS_DENIED - Caller does not have the appropriate access to complete the operation. STATUS_OBJECT_NAME_NOT_FOUND - There is no object in the target system's LSA Database having the name and type specified by the handle. --*/ { return LsapDeleteObject( ObjectHandle , TRUE ); } NTSTATUS LsapDeleteObject( IN OUT LSAPR_HANDLE *ObjectHandle, IN BOOL LockSce ) /*++ Routine Description: This is the worker routine for LsarDeleteObject, with an added semantics of not locking the SCE policy. --*/ { NTSTATUS Status = STATUS_SUCCESS; NTSTATUS IgnoreStatus; LSAP_DB_HANDLE InternalHandle = *ObjectHandle; BOOLEAN ObjectReferenced = FALSE; ULONG ReferenceOptions = LSAP_DB_START_TRANSACTION | LSAP_DB_LOCK ; ULONG DereferenceOptions = LSAP_DB_FINISH_TRANSACTION | LSAP_DB_LOCK | LSAP_DB_DEREFERENCE_CONTR; PLSAPR_TRUST_INFORMATION TrustInformation = NULL; LSAPR_TRUST_INFORMATION OutputTrustInformation; BOOLEAN TrustInformationPresent = FALSE; LSAP_DB_OBJECT_TYPE_ID ObjectTypeId; PTRUSTED_DOMAIN_INFORMATION_EX CurrentTrustedDomainInfoEx = NULL; BOOLEAN ScePolicyLocked = FALSE; BOOLEAN NotifySce = FALSE; SECURITY_DB_OBJECT_TYPE ObjectType = SecurityDbObjectLsaPolicy; PSID ObjectSid = NULL; BOOL RevertResult = FALSE; BOOL Impersonating = FALSE; LsarpReturnCheckSetup(); LsapDsDebugOut(( DEB_FTRACE, "LsarDeleteObject\n" )); ObjectTypeId = InternalHandle->ObjectTypeId; // // The LSA will only call SceNotify if the policy change was made via a handle // not marked 'SCE handle'. This prevents SCE from being notified of its own // changes. This is required to ensure that policy read from the LSA matches // that written by a non-SCE application. // if ( !InternalHandle->SceHandle && !InternalHandle->SceHandleChild ) { switch ( ObjectTypeId ) { case AccountObject: { ULONG SidLength; ASSERT( InternalHandle->Sid ); SidLength = RtlLengthSid( InternalHandle->Sid ); ObjectSid = ( PSID )LsapAllocateLsaHeap( SidLength ); if ( ObjectSid ) { RtlCopyMemory( ObjectSid, InternalHandle->Sid, SidLength ); } else { Status = STATUS_INSUFFICIENT_RESOURCES; goto DeleteObjectError; } ObjectType = SecurityDbObjectLsaAccount; // FALLTHRU } case PolicyObject: if ( LockSce ) { RtlEnterCriticalSection( &LsapDbState.ScePolicyLock.CriticalSection ); if ( LsapDbState.ScePolicyLock.NumberOfWaitingShared > MAX_SCE_WAITING_SHARED ) { Status = STATUS_TOO_MANY_THREADS; } RtlLeaveCriticalSection( &LsapDbState.ScePolicyLock.CriticalSection ); if ( !NT_SUCCESS( Status )) { goto DeleteObjectError; } WaitForSingleObject( LsapDbState.SceSyncEvent, INFINITE ); RtlAcquireResourceShared( &LsapDbState.ScePolicyLock, TRUE ); ASSERT( !g_ScePolicyLocked ); ScePolicyLocked = TRUE; } NotifySce = TRUE; break; default: break; } } // // Verify that the Object handle is valid, is of the expected type and // has all of the desired accesses granted. Reference the handle and // open a database transaction. // Status = LsapDbReferenceObject( *ObjectHandle, DELETE, ObjectTypeId, ObjectTypeId, ReferenceOptions ); if (!NT_SUCCESS(Status)) { goto DeleteObjectError; } ObjectReferenced = TRUE; // // Perform object type specific pre-processing. Note that some // pre-processing is also done within LsapDbReferenceObject(), for // example, for local secrets. // switch (ObjectTypeId) { case PolicyObject: Status = STATUS_INVALID_PARAMETER; break; case TrustedDomainObject: if ( LsapDsWriteDs && InternalHandle->Sid == NULL ) { BOOLEAN TrustedStatus = InternalHandle->Trusted; // // Toggle trusted bit so that access cks do not prevent following // query from succeeding // InternalHandle->Trusted = TRUE; Status = LsarQueryInfoTrustedDomain( *ObjectHandle, TrustedDomainInformationEx, (PLSAPR_TRUSTED_DOMAIN_INFO *) &CurrentTrustedDomainInfoEx ); InternalHandle->Trusted = TrustedStatus; if ( NT_SUCCESS( Status ) ) { InternalHandle->Sid = CurrentTrustedDomainInfoEx->Sid; CurrentTrustedDomainInfoEx->Sid = NULL; } else { LsapDsDebugOut(( DEB_ERROR, "Query for TD Sid failed with 0x%lx\n", Status )); } } if (LsapAdtAuditingEnabledHint(AuditCategoryPolicyChange, EVENTLOG_AUDIT_SUCCESS)) { // // If we're auditing deletions of TrustedDomain objects, we need // to retrieve the TrustedDomain name and keep it for later when // we generate the audit. // Status = LsapDbAcquireReadLockTrustedDomainList(); if (NT_SUCCESS(Status)) { Status = LsapDbLookupSidTrustedDomainList( InternalHandle->Sid, &TrustInformation ); if (STATUS_NO_SUCH_DOMAIN==Status) { // // If we could not find by the SID, then lookup by the logical name // field. // Status = LsapDbLookupNameTrustedDomainList( (PLSAPR_UNICODE_STRING) &InternalHandle->LogicalNameU, &TrustInformation ); } if ( NT_SUCCESS( Status )) { Status = LsapRpcCopyTrustInformation( NULL, &OutputTrustInformation, TrustInformation ); TrustInformationPresent = NT_SUCCESS( Status ); } LsapDbReleaseLockTrustedDomainList(); } // // Reset the status to SUCCESS. Failure to get the information for // auditing should not be a failure to delete // Status = STATUS_SUCCESS; } if ( NT_SUCCESS( Status )) { // // Notify netlogon. Potentially ignore any failures // Status = LsapNotifyNetlogonOfTrustChange( InternalHandle->Sid, SecurityDbDelete ); #if DBG if ( !NT_SUCCESS( Status ) ) { LsapDsDebugOut(( DEB_ERROR, "LsapNotifyNetlogonOfTrustChange failed with an unexpected 0x%lx\n", Status )); ASSERT( NT_SUCCESS(Status) ); } #endif Status = STATUS_SUCCESS; } break; case AccountObject: { PLSAPR_PRIVILEGE_SET Privileges; LSAPR_HANDLE AccountHandle; PLSAPR_SID AccountSid = NULL; ULONG AuditEventId; AccountHandle = *ObjectHandle; AccountSid = LsapDbSidFromHandle( AccountHandle ); if (LsapAdtAuditingEnabledHint(AuditCategoryPolicyChange, EVENTLOG_AUDIT_SUCCESS)) { Status = LsarEnumeratePrivilegesAccount( AccountHandle, &Privileges ); if (!NT_SUCCESS( Status )) { LsapDsDebugOut(( DEB_ERROR, "LsarEnumeratePrivilegesAccount ret'd %x\n", Status )); break; } AuditEventId = SE_AUDITID_USER_RIGHT_REMOVED; // // Audit the privilege set change. Ignore failures from Auditing. // IgnoreStatus = LsapAdtGenerateLsaAuditEvent( AccountHandle, SE_CATEGID_POLICY_CHANGE, AuditEventId, (PPRIVILEGE_SET)Privileges, 1, (PSID *) &AccountSid, 0, NULL, NULL ); MIDL_user_free( Privileges ); } } break; case SecretObject: break; default: Status = STATUS_INVALID_PARAMETER; break; } if (!NT_SUCCESS(Status)) { goto DeleteObjectError; } Status = LsapDbDeleteObject( *ObjectHandle ); if (!NT_SUCCESS(Status)) { goto DeleteObjectError; } // // Decrement the Reference Count so that the object's handle will be // freed upon dereference. // LsapDbDereferenceHandle( *ObjectHandle, TRUE ); // // Perform object post-processing. The only post-processing is // the auditing of TrustedDomain object deletion. // if (TrustInformationPresent && LsapAdtAuditingEnabledHint( AuditCategoryPolicyChange, EVENTLOG_AUDIT_SUCCESS)) { if (ObjectTypeId == TrustedDomainObject) { (void) LsapAdtTrustedDomainRem( EVENTLOG_AUDIT_SUCCESS, (PUNICODE_STRING) &OutputTrustInformation.Name, InternalHandle->Sid, NULL, // UserSid NULL // UserAuthenticationId ); // // Call fgs routine because we want to free the graph of the // structure, but not the top level of the structure. // _fgs__LSAPR_TRUST_INFORMATION ( &OutputTrustInformation ); TrustInformation = NULL; } } // // Delete new object from the in-memory cache (if any) // if ( ObjectTypeId == AccountObject && LsapDbIsCacheSupported( AccountObject ) && LsapDbIsCacheValid( AccountObject )) { IgnoreStatus = LsapDbDeleteAccount( InternalHandle->Sid ); } // // impersonate the client so that audit event shows correct user // Do this only for untrusted clients // if ( !InternalHandle->Trusted ) { if ( InternalHandle->Options & LSAP_DB_USE_LPC_IMPERSONATE ) { IgnoreStatus = LsapImpersonateClient( ); } else { IgnoreStatus = I_RpcMapWin32Status(RpcImpersonateClient(0)); } if ( NT_SUCCESS(IgnoreStatus) ) { Impersonating = TRUE; } else if ( ( IgnoreStatus == RPC_NT_NO_CALL_ACTIVE ) || ( IgnoreStatus == RPC_NT_NO_CONTEXT_AVAILABLE ) ) { // // we dont want to fail the audit if // -- the call is not over RPC (RPC_NT_NO_CALL_ACTIVE) // -- the client died prematurely (RPC_NT_NO_CONTEXT_AVAILABLE) // IgnoreStatus = STATUS_SUCCESS; } DsysAssertMsg( NT_SUCCESS(IgnoreStatus), "LsapDeleteObject: failed to impersonate" ); if (!NT_SUCCESS( IgnoreStatus )) { LsapAuditFailed( IgnoreStatus ); } } // // Audit the deletion // IgnoreStatus = NtDeleteObjectAuditAlarm( &LsapState.SubsystemName, *ObjectHandle, InternalHandle->GenerateOnClose); // // unimpersonate // if ( !InternalHandle->Trusted ) { if ( Impersonating ) { if ( InternalHandle->Options & LSAP_DB_USE_LPC_IMPERSONATE ) { RevertResult = RevertToSelf(); DsysAssertMsg( RevertResult, "LsapDeleteObject: RevertToSelf() failed" ); } else { IgnoreStatus = I_RpcMapWin32Status(RpcRevertToSelf()); DsysAssertMsg( NT_SUCCESS(IgnoreStatus), "LsapDeleteObject: RpcRevertToSelf() failed" ); } } } DeleteObjectFinish: // // If we referenced the object, dereference it, close the database // transaction, notify the replicator of the delete, release the LSA // Database lock and return. // if (ObjectReferenced) { Status = LsapDbDereferenceObject( ObjectHandle, ObjectTypeId, ObjectTypeId, DereferenceOptions, SecurityDbDelete, Status ); ObjectReferenced = FALSE; if (!NT_SUCCESS(Status)) { goto DeleteObjectError; } } if ( CurrentTrustedDomainInfoEx ) { LsaIFree_LSAPR_TRUSTED_DOMAIN_INFO( TrustedDomainInformationEx, (PLSAPR_TRUSTED_DOMAIN_INFO) CurrentTrustedDomainInfoEx ); } // // Notify SCE of the change. Only notify for callers // that did not open their policy handles with LsaOpenPolicySce. // if ( NotifySce && NT_SUCCESS( Status )) { LsapSceNotify( SecurityDbDelete, ObjectType, ObjectSid ); } if ( ScePolicyLocked ) { RtlReleaseResource( &LsapDbState.ScePolicyLock ); } if ( ObjectSid ) { LsapFreeLsaHeap( ObjectSid ); } LsarpReturnPrologue(); LsapDsDebugOut(( DEB_FTRACE, "LsarDeleteObject: 0x%lx\n", Status )); // // Under all circumstances tell RPC we're done with this handle // *ObjectHandle = NULL; return(Status); DeleteObjectError: goto DeleteObjectFinish; } NTSTATUS LsarDelete( IN LSAPR_HANDLE ObjectHandle ) /*++ Routine Description: This function is the former LSA server RPC worker routine for the LsaDelete API. It has been termorarily retained for compatibility with pre Beta 2 versions 1.369 and earlier of the system. It has been necessary to replace this routine with a new one, LsarDeleteObject(), on the RPC interface. This is because, like LsarClose(), a pointer to a handle is required rather than a handle so that LsarDeleteObject() can inform the RPC server calling stub that the handle has been deleted by returning NULL. The client wrapper for LsaDelete() will try to call LsarDeleteObject(). If the server code does not contain this interface, the client will call LsarDelete(). In this event, the server's LSAPR_HANDLE_rundown() routine may attempt to rundown the handle after it has been deleted (versions 1.363 - 369 only). The LsaDelete API deletes an object from the LSA Database. The object must be open for DELETE access. Arguments: ObjectHandle - Handle from an LsaOpen or LsaCreate call. None. Return Value: NTSTATUS - Standard Nt Result Code STATUS_ACCESS_DENIED - Caller does not have the appropriate access to complete the operation. STATUS_OBJECT_NAME_NOT_FOUND - There is no object in the target system's LSA Database having the name and type specified by the handle. --*/ { // // Call the replacement routine LsarDeleteObject() // return( LsarDeleteObject((LSAPR_HANDLE *) &ObjectHandle)); } NTSTATUS LsarChangePassword( IN PLSAPR_UNICODE_STRING ServerName, IN PLSAPR_UNICODE_STRING DomainName, IN PLSAPR_UNICODE_STRING AccountName, IN PLSAPR_UNICODE_STRING OldPassword, IN PLSAPR_UNICODE_STRING NewPassword ) /*++ Routine Description: The LsaChangePassword API is used to change a user account's password. The user must have appropriate access to the user account and must know the current password value. Arguments: ServerName - The name of the Domain Controller at which the password can be changed. DomainName - The name of the domain in which the account exists. AccountName - The name of the account whose password is to be changed. NewPassword - The new password value. OldPassword - The old (current) password value. Return Values: NTSTATUS - Standard Nt Result Code STATUS_ACCESS_DENIED - Caller does not have the appropriate access to complete the operation. STATUS_ILL_FORMED_PASSWORD - The new password is poorly formed, e.g. contains characters that can't be entered from the keyboard. STATUS_PASSWORD_RESTRICTION - A restriction prevents the password from being changed. This may be for an number of reasons, including time restrictions on how often a password may be changed or length restrictions on the provided (new) password. This error might also be returned if the new password matched a password in the recent history log for the account. Security administrators indicate how many of the most recently used passwords may not be re-used. STATUS_WRONG_PASSWORD - OldPassword does not contain the user's current password. STATUS_NO_SUCH_USER - The SID provided does not lead to a user account. STATUS_CANT_UPDATE_MASTER - An attempt to update the master copy of the password was unsuccessful. Please try again later. --*/ { NTSTATUS Status; LsarpReturnCheckSetup(); LsapDsDebugOut(( DEB_FTRACE, "LsarChangePassword\n" )); DBG_UNREFERENCED_PARAMETER( ServerName ); DBG_UNREFERENCED_PARAMETER( DomainName ); DBG_UNREFERENCED_PARAMETER( AccountName ); DBG_UNREFERENCED_PARAMETER( OldPassword ); DBG_UNREFERENCED_PARAMETER( NewPassword ); Status = STATUS_NOT_IMPLEMENTED; LsarpReturnPrologue(); LsapDsDebugOut(( DEB_FTRACE, "LsarChangePassword: 0x%lx\n", Status )); return(Status); } NTSTATUS LsapDbIsRpcClientNetworkClient( OUT PBOOLEAN IsNetworkClient ) /*++ Routine Description: This call is used to determine if the current RPC call is from a network client (came in via the network as opposed to locally) or not. Arguments: IsNetworkClient - Pointer to a BOOLEAN that gets set to the results of whether this is a network client or not Return Values: STATUS_SUCCESS - Success --*/ { RPC_STATUS RpcStatus; unsigned int ClientLocalFlag; RpcStatus = I_RpcBindingIsClientLocal ( NULL, &ClientLocalFlag ); if ( RpcStatus != RPC_S_OK ) { return I_RpcMapWin32Status( RpcStatus ); } *IsNetworkClient = (ClientLocalFlag == 0); return STATUS_SUCCESS; } NTSTATUS LsapValidateNetbiosName( IN const UNICODE_STRING * Name, OUT BOOLEAN * Valid ) /*++ Routine Description: Validates that a NetBIOS name conforms to certain minimum standards. For more details, see the description of NetpIsDomainNameValid. Arguments: Name name to validate Valid will be set to TRUE if validation checks out, FALSE otherwise Returns: STATUS_SUCCESS STATUS_INSUFFICIENT_RESOURCES --*/ { WCHAR * Buffer; BOOLEAN BufferAllocated = FALSE; ASSERT( Name ); ASSERT( Valid ); ASSERT( LsapValidateLsaUnicodeString( Name )); // // Empty names and names that are too long are not allowed // if ( Name->Length == 0 || Name->Length > DNLEN * sizeof( WCHAR )) { *Valid = FALSE; return STATUS_SUCCESS; } if ( Name->MaximumLength > Name->Length ) { Buffer = Name->Buffer; } else { SafeAllocaAllocate( Buffer, Name->Length + sizeof( WCHAR )); if ( Buffer == NULL ) { *Valid = FALSE; return STATUS_INSUFFICIENT_RESOURCES; } BufferAllocated = TRUE; RtlCopyMemory( Buffer, Name->Buffer, Name->Length ); } Buffer[Name->Length / sizeof( WCHAR )] = L'\0'; *Valid = ( TRUE == NetpIsDomainNameValid( Buffer )); if ( BufferAllocated ) { SafeAllocaFree( Buffer ); } return STATUS_SUCCESS; } NTSTATUS LsapValidateDnsName( IN const UNICODE_STRING * Name, OUT BOOLEAN * Valid ) /*++ Routine Description: Validates that a DNS name conforms to certain minimum standards. Arguments: Name name to validate Valid will be set to TRUE if validation checks out, FALSE otherwise Returns: STATUS_SUCCESS STATUS_INSUFFICIENT_RESOURCES --*/ { DNS_STATUS DnsStatus; WCHAR * Buffer; BOOLEAN BufferAllocated = FALSE; ASSERT( Name ); ASSERT( Valid ); ASSERT( LsapValidateLsaUnicodeString( Name )); // // Empty names and names that are too long are not allowed // if ( Name->Length == 0 || Name->Length > DNS_MAX_NAME_LENGTH * sizeof( WCHAR )) { *Valid = FALSE; return STATUS_SUCCESS; } if ( Name->MaximumLength > Name->Length ) { Buffer = Name->Buffer; } else { SafeAllocaAllocate( Buffer, Name->Length + sizeof( WCHAR )); if ( Buffer == NULL ) { *Valid = FALSE; return STATUS_INSUFFICIENT_RESOURCES; } BufferAllocated = TRUE; RtlCopyMemory( Buffer, Name->Buffer, Name->Length ); } Buffer[Name->Length / sizeof( WCHAR )] = L'\0'; DnsStatus = DnsValidateName_W( Buffer, DnsNameDomain ); // // Bug 350434: Must allow non-standard characters in DNS names // (which cause DNS_ERROR_NON_RFC_NAME) // *Valid = ( DnsStatus == ERROR_SUCCESS || DnsStatus == DNS_ERROR_NON_RFC_NAME ); if ( BufferAllocated ) { SafeAllocaFree( Buffer ); } return STATUS_SUCCESS; } BOOLEAN LsapIsRunningOnPersonal( VOID ) /*++ Routine Description: This function checks the system to see if we are running on the personal version of the operating system. The personal version is denoted by the product id equal to WINNT, which is really workstation, and the product suite containing the personal suite string. Arguments: None. Return Value: TRUE if we are running on personal, FALSE otherwise. --*/ { OSVERSIONINFOEXW OsVer = {0}; ULONGLONG ConditionMask = 0; OsVer.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX); OsVer.wSuiteMask = VER_SUITE_PERSONAL; OsVer.wProductType = VER_NT_WORKSTATION; VER_SET_CONDITION( ConditionMask, VER_PRODUCT_TYPE, VER_EQUAL ); VER_SET_CONDITION( ConditionMask, VER_SUITENAME, VER_AND ); return RtlVerifyVersionInfo( &OsVer, VER_PRODUCT_TYPE | VER_SUITENAME, ConditionMask) == STATUS_SUCCESS; } NTSTATUS LsaITestCall( IN LSAPR_HANDLE PolicyHandle, IN LSAPR_TEST_INTERNAL_ROUTINES Call, IN PLSAPR_TEST_INTERNAL_ARG_LIST InputArgs, OUT PLSAPR_TEST_INTERNAL_ARG_LIST *OutputArgs ) { return STATUS_NOT_IMPLEMENTED; }