You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1650 lines
46 KiB
1650 lines
46 KiB
/*
|
|
|
|
Copyright (c) 1992 Microsoft Corporation
|
|
|
|
Module Name:
|
|
|
|
access.c
|
|
|
|
Abstract:
|
|
|
|
This module contains the routines for handling access related stuff.
|
|
|
|
Author:
|
|
|
|
Jameel Hyder (microsoft!jameelh)
|
|
|
|
|
|
Revision History:
|
|
20 Sep 1992 Initial Version
|
|
|
|
Notes: Tab stop: 4
|
|
--*/
|
|
|
|
#define FILENUM FILE_ACCESS
|
|
|
|
#include <afp.h>
|
|
#include <fdparm.h>
|
|
#include <pathmap.h>
|
|
#define _ACCESS_LOCALS
|
|
#include <access.h>
|
|
#include <client.h>
|
|
#include <secutil.h>
|
|
#include <seposix.h>
|
|
|
|
#ifdef ALLOC_PRAGMA
|
|
#pragma alloc_text( PAGE, AfpGetUserAndPrimaryGroupSids)
|
|
#pragma alloc_text( PAGE, AfpMakeSecurityDescriptorForUser)
|
|
#pragma alloc_text( PAGE, AfpGetAfpPermissions)
|
|
#pragma alloc_text( PAGE, afpMoveAces)
|
|
#pragma alloc_text( PAGE, AfpSetAfpPermissions)
|
|
#pragma alloc_text( PAGE, afpPermissions2NtMask)
|
|
#pragma alloc_text( PAGE, afpAddAceToAcl)
|
|
#if DBG
|
|
#pragma alloc_text( PAGE, AfpDumpSid)
|
|
#pragma alloc_text( PAGE, AfpDumpSidnMask)
|
|
#endif
|
|
#endif
|
|
|
|
#define ACCESS_CHECK_ACCESS_MASK 0x01
|
|
#define GRPS_BUFFER_SIZE 1024
|
|
|
|
|
|
/*** AfpMakeSecDescForAccessCheck
|
|
*
|
|
* Create a security descriptor for a SID. The security descriptor has the
|
|
* Aces for the User alone.
|
|
*/
|
|
AFPSTATUS
|
|
AfpMakeSecDescForAccessCheck(
|
|
IN PSID OwnerSid,
|
|
OUT PISECURITY_DESCRIPTOR * ppSecDesc
|
|
)
|
|
{
|
|
AFPSTATUS Status = AFP_ERR_MISC;
|
|
PISECURITY_DESCRIPTOR pSecDesc;
|
|
int DaclSize;
|
|
PACCESS_ALLOWED_ACE pAce;
|
|
|
|
PAGED_CODE( );
|
|
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("AfpMakeSecDescForAccessCheck: Entered\n"));
|
|
|
|
do
|
|
{
|
|
// Allocate a security descriptor
|
|
pSecDesc = (PISECURITY_DESCRIPTOR)ALLOC_ACCESS_MEM(sizeof(SECURITY_DESCRIPTOR));
|
|
|
|
*ppSecDesc = pSecDesc;
|
|
if (pSecDesc == NULL)
|
|
{
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_ERR,
|
|
("ALLOC_ACCESS_MEM error for pSecDesc\n"));
|
|
break;
|
|
}
|
|
|
|
// Initialize the security descriptor
|
|
RtlCreateSecurityDescriptor(pSecDesc, SECURITY_DESCRIPTOR_REVISION);
|
|
pSecDesc->Control = SE_DACL_PRESENT;
|
|
|
|
// Set the owner and group Ids in the descriptor
|
|
pSecDesc->Owner = OwnerSid;
|
|
|
|
// Determine the size of the Dacl needed. The sizeof(DWORD) offsets the
|
|
// SidStart field in the ACE.
|
|
//
|
|
// 2 ACEs for the owner (owner+inherit for owner)
|
|
DaclSize = sizeof(ACL) + 2*(sizeof(ACCESS_ALLOWED_ACE) - sizeof(DWORD) +
|
|
RtlLengthSid(OwnerSid));
|
|
|
|
if ((pSecDesc->Dacl = (PACL)ALLOC_ACCESS_MEM(DaclSize)) == NULL)
|
|
{
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_ERR,
|
|
("ALLOC_ACCESS_MEM error for pSecDesc->Dacl\n"));
|
|
break;
|
|
}
|
|
|
|
// Initialize the ACL with one ACE corres. to Owner getting all the
|
|
// privileges. Add another ace which is identical to the first ace but is
|
|
// a inheritance ACE.
|
|
RtlCreateAcl(pSecDesc->Dacl, DaclSize, ACL_REVISION);
|
|
|
|
// we will be adding to this as we add aces, so set it to the min here
|
|
pSecDesc->Dacl->AclSize = sizeof(ACL);
|
|
|
|
pAce = (PACCESS_ALLOWED_ACE)((PBYTE)pSecDesc->Dacl + sizeof(ACL));
|
|
|
|
// Add the ALLOWED_ACE and the corres. inherit Ace for owner
|
|
pAce = afpAddAceToAcl(pSecDesc->Dacl,
|
|
pAce,
|
|
ACCESS_CHECK_ACCESS_MASK,
|
|
OwnerSid,
|
|
True);
|
|
|
|
Status = AFP_ERR_NONE;
|
|
} while (False);
|
|
|
|
// Do any cleanup on error
|
|
if (!NT_SUCCESS(Status) && (pSecDesc != NULL))
|
|
{
|
|
if (pSecDesc->Dacl != NULL)
|
|
AfpFreeMemory(pSecDesc->Dacl);
|
|
AfpFreeMemory(pSecDesc);
|
|
pSecDesc = NULL;
|
|
}
|
|
|
|
return Status;
|
|
}
|
|
|
|
|
|
/*** afpCheckUserMemberOfGroup
|
|
*
|
|
* Determine if the User is member of the given group, if it is a group.
|
|
*/
|
|
LOCAL BOOLEAN
|
|
afpCheckUserMemberOfGroup(
|
|
IN PSDA pSda,
|
|
IN PSID pSidGroup
|
|
)
|
|
{
|
|
DWORD i;
|
|
BOOLEAN IsAMember = False;
|
|
PISECURITY_DESCRIPTOR pSecDesc = NULL;
|
|
SECURITY_SUBJECT_CONTEXT SecSubjectContext = {0};
|
|
ACCESS_MASK CheckAccessMaskIn = 0, CheckAccessMaskOut = 0;
|
|
BOOLEAN fAccessCheckSuccess = False;
|
|
BOOLEAN fRevertImpersonation = False;
|
|
NTSTATUS Status = STATUS_UNSUCCESSFUL;
|
|
|
|
PAGED_CODE( );
|
|
|
|
ASSERT ((pSda != NULL) && (pSidGroup != NULL));
|
|
|
|
AfpDumpSid("afpCheckUserMemberOfGroup: Checking", pSidGroup);
|
|
|
|
do
|
|
{
|
|
|
|
// Create SecurityDescriptor out of the Sid provided
|
|
Status = AfpMakeSecDescForAccessCheck(pSidGroup, &pSecDesc);
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_ERR,
|
|
("AfpMakeSecDescForAccessCheck failed error (%0xld)\n", Status));
|
|
break;
|
|
}
|
|
|
|
AfpImpersonateClient(pSda);
|
|
fRevertImpersonation = True;
|
|
|
|
SeCaptureSubjectContext(&SecSubjectContext);
|
|
CheckAccessMaskIn = ACCESS_CHECK_ACCESS_MASK;
|
|
fAccessCheckSuccess = SeAccessCheck (
|
|
pSecDesc,
|
|
&SecSubjectContext,
|
|
FALSE,
|
|
CheckAccessMaskIn,
|
|
0,
|
|
NULL,
|
|
IoGetFileObjectGenericMapping(),
|
|
UserMode,
|
|
&CheckAccessMaskOut,
|
|
&Status
|
|
);
|
|
|
|
if (fAccessCheckSuccess && NT_SUCCESS(Status))
|
|
{
|
|
IsAMember = True;
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("SeAccessCheck good : Status= (%0x), RetCode= (%ld), AccessOut= (%ld)\n", Status, fAccessCheckSuccess, CheckAccessMaskOut));
|
|
}
|
|
else
|
|
{
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("SeAccessCheck failed: Status = (%0x), RetCode= (%ld)\n", Status, fAccessCheckSuccess));
|
|
}
|
|
SeReleaseSubjectContext(&SecSubjectContext);
|
|
}
|
|
while (FALSE);
|
|
|
|
if (fRevertImpersonation)
|
|
AfpRevertBack();
|
|
|
|
if (pSecDesc != NULL)
|
|
{
|
|
if (pSecDesc->Dacl != NULL)
|
|
AfpFreeMemory(pSecDesc->Dacl);
|
|
AfpFreeMemory(pSecDesc);
|
|
}
|
|
|
|
return IsAMember;
|
|
}
|
|
|
|
|
|
/*** afpGetUserAccess
|
|
*
|
|
* Determine the Access that is permitted for the user
|
|
*/
|
|
LOCAL NTSTATUS
|
|
afpGetUserAccess(
|
|
IN PSDA pSda,
|
|
IN PISECURITY_DESCRIPTOR pSecDesc,
|
|
OUT PACCESS_MASK pGrantedAccess
|
|
)
|
|
{
|
|
DWORD i;
|
|
SECURITY_SUBJECT_CONTEXT SecSubjectContext = {0};
|
|
ACCESS_MASK DesiredAccess = 0;
|
|
BOOLEAN fAccessCheckSuccess = False;
|
|
BOOLEAN fRevertImpersonation = False;
|
|
NTSTATUS Status = STATUS_UNSUCCESSFUL;
|
|
|
|
PAGED_CODE( );
|
|
|
|
ASSERT ((pSda != NULL) && (pSecDesc != NULL));
|
|
|
|
do
|
|
{
|
|
AfpImpersonateClient(pSda);
|
|
fRevertImpersonation = True;
|
|
|
|
SeCaptureSubjectContext(&SecSubjectContext);
|
|
DesiredAccess = MAXIMUM_ALLOWED;
|
|
fAccessCheckSuccess = SeAccessCheck (
|
|
pSecDesc,
|
|
&SecSubjectContext,
|
|
FALSE,
|
|
DesiredAccess,
|
|
0,
|
|
NULL,
|
|
IoGetFileObjectGenericMapping(),
|
|
UserMode,
|
|
pGrantedAccess,
|
|
&Status
|
|
);
|
|
|
|
if (fAccessCheckSuccess && NT_SUCCESS(Status))
|
|
{
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("SeAccessCheck good : Status= (%0x), RetCode= (%ld), AccessOut= (%0x)\n", Status, fAccessCheckSuccess, *pGrantedAccess));
|
|
}
|
|
else
|
|
{
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("SeAccessCheck failed: Status = (%0x), RetCode= (%0x)\n", Status, fAccessCheckSuccess));
|
|
}
|
|
SeReleaseSubjectContext(&SecSubjectContext);
|
|
}
|
|
while (FALSE);
|
|
|
|
if (fRevertImpersonation)
|
|
AfpRevertBack();
|
|
|
|
return Status;
|
|
}
|
|
|
|
|
|
/*** AfpGetUserAndPrimaryGroupSids
|
|
*
|
|
* Get the Sids corres. to the user and his primary group.
|
|
*/
|
|
NTSTATUS
|
|
AfpGetUserAndPrimaryGroupSids(
|
|
IN PSDA pSda
|
|
)
|
|
{
|
|
DWORD i, j;
|
|
NTSTATUS Status = STATUS_SUCCESS;
|
|
DWORD SidLength, SizeNeeded, ExtraSpace, Offset;
|
|
PSID_AND_ATTRIBUTES pSidnAttr;
|
|
PTOKEN_GROUPS pGroups = NULL;
|
|
PBYTE pGrpsBuffer = NULL;
|
|
BYTE Buffer[256]; // We should not need a buffer larger
|
|
// than this for User SID_AND_ATTRIBUTES
|
|
|
|
PAGED_CODE( );
|
|
|
|
do
|
|
{
|
|
pGrpsBuffer = (PBYTE)ALLOC_ACCESS_MEM(GRPS_BUFFER_SIZE);
|
|
if (pGrpsBuffer == NULL)
|
|
{
|
|
Status = STATUS_INSUFFICIENT_RESOURCES;
|
|
break;
|
|
}
|
|
|
|
pGroups = (PTOKEN_GROUPS)pGrpsBuffer;
|
|
pSda->sda_pGroups = NULL;
|
|
if (pSda->sda_ClientType == SDA_CLIENT_GUEST)
|
|
{
|
|
pSda->sda_UserSid = &AfpSidWorld;
|
|
pSda->sda_GroupSid = &AfpSidWorld; // Primary group of Guest is also 'World'
|
|
break;
|
|
}
|
|
|
|
pSidnAttr = (PSID_AND_ATTRIBUTES)Buffer;
|
|
|
|
// Get the Owner Sid out of the User token and copy it into the Sda
|
|
Status = NtQueryInformationToken(pSda->sda_UserToken,
|
|
TokenOwner,
|
|
pSidnAttr,
|
|
sizeof(Buffer),
|
|
&SizeNeeded);
|
|
|
|
ASSERT (NT_SUCCESS(Status));
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
break;
|
|
}
|
|
|
|
AfpDumpSid("AfpGetUserAndPrimaryGroupSids: LOGON Owner Sid", pSidnAttr->Sid);
|
|
|
|
SidLength = RtlLengthSid(pSidnAttr->Sid);
|
|
|
|
pSda->sda_UserSid = (PSID)ALLOC_ACCESS_MEM(SidLength);
|
|
if (pSda->sda_UserSid == NULL)
|
|
{
|
|
Status = STATUS_INSUFFICIENT_RESOURCES;
|
|
break;
|
|
}
|
|
RtlCopyMemory(pSda->sda_UserSid, pSidnAttr->Sid, SidLength);
|
|
|
|
// Get the primary group of this user
|
|
Status = NtQueryInformationToken(pSda->sda_UserToken,
|
|
TokenPrimaryGroup,
|
|
pSidnAttr,
|
|
sizeof(Buffer),
|
|
&SizeNeeded);
|
|
|
|
ASSERT (NT_SUCCESS(Status));
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
break;
|
|
}
|
|
|
|
AfpDumpSid("AfpGetUserAndPrimaryGroupSids: LOGON Group Sid", pSidnAttr->Sid);
|
|
|
|
SidLength = RtlLengthSid(pSidnAttr->Sid);
|
|
pSda->sda_GroupSid = (PSID)ALLOC_ACCESS_MEM(SidLength);
|
|
if (pSda->sda_GroupSid == NULL)
|
|
{
|
|
Status = STATUS_INSUFFICIENT_RESOURCES;
|
|
break;
|
|
}
|
|
RtlCopyMemory(pSda->sda_GroupSid, pSidnAttr->Sid, SidLength);
|
|
|
|
// Get the User Sid out of the User token. This will be added to the
|
|
// list of groups that we query later, if this is different from
|
|
// the Owner Sid (which is now in sda_UserSid).
|
|
Status = NtQueryInformationToken(pSda->sda_UserToken,
|
|
TokenUser,
|
|
pSidnAttr,
|
|
sizeof(Buffer),
|
|
&SizeNeeded);
|
|
|
|
ASSERT (NT_SUCCESS(Status));
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
break;
|
|
}
|
|
|
|
AfpDumpSid("AfpGetUserAndPrimaryGroupSids: LOGON User Sid", pSidnAttr->Sid);
|
|
|
|
// Get the list of groups this user is member of
|
|
SizeNeeded = GRPS_BUFFER_SIZE;
|
|
do
|
|
{
|
|
if (Status != STATUS_SUCCESS)
|
|
{
|
|
if (pGroups != (PTOKEN_GROUPS)pGrpsBuffer)
|
|
AfpFreeMemory(pGroups);
|
|
|
|
if ((pGroups = (PTOKEN_GROUPS)ALLOC_ACCESS_MEM(SizeNeeded)) == NULL)
|
|
{
|
|
Status = AFP_ERR_MISC;
|
|
if (pSda->sda_ClientType == SDA_CLIENT_ADMIN)
|
|
{
|
|
Status = STATUS_INSUFFICIENT_RESOURCES;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
Status = NtQueryInformationToken(pSda->sda_UserToken,
|
|
TokenGroups,
|
|
pGroups,
|
|
SizeNeeded,
|
|
&SizeNeeded);
|
|
} while ((Status != STATUS_SUCCESS) &&
|
|
((Status == STATUS_BUFFER_TOO_SMALL) ||
|
|
(Status == STATUS_BUFFER_OVERFLOW) ||
|
|
(Status == STATUS_MORE_ENTRIES)));
|
|
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
AFPLOG_ERROR(AFPSRVMSG_USER_GROUPS, Status, NULL, 0, NULL);
|
|
break;
|
|
}
|
|
|
|
// Allocate enough memory to copy the group information in the sda. If
|
|
// the User and Owner Sids in the user token are not the same then we
|
|
// want to add the user sid to the list of groups. This is especially
|
|
// the case where an ADMIN logs on but his Owner Sid is Administrators.
|
|
// Also fix up the pointers appropriately !!!
|
|
|
|
ExtraSpace = 0; Offset = 0; j = 0;
|
|
if (!RtlEqualSid(pSidnAttr->Sid, pSda->sda_UserSid))
|
|
{
|
|
ExtraSpace = (RtlLengthSid(pSidnAttr->Sid) + sizeof(pSidnAttr->Attributes));
|
|
Offset = sizeof(SID_AND_ATTRIBUTES);
|
|
j = 1;
|
|
}
|
|
|
|
if ((pSda->sda_pGroups = (PTOKEN_GROUPS)AfpAllocPagedMemory(2*SizeNeeded+2*ExtraSpace)) == NULL)
|
|
{
|
|
Status = STATUS_INSUFFICIENT_RESOURCES;
|
|
break;
|
|
}
|
|
|
|
// If we are not copying the User Sid in sda_pGroups, then copy pGroups to sda_pGroups
|
|
// directly and then fixup the individual pSid pointers. If we are then make the User
|
|
// Sid as the first one in the list and copy the actual sid at the tail end of the
|
|
// buffer.
|
|
pSda->sda_pGroups->GroupCount = pGroups->GroupCount;
|
|
RtlCopyMemory(&pSda->sda_pGroups->Groups[j],
|
|
&pGroups->Groups[0],
|
|
SizeNeeded - sizeof(DWORD)); // DWORD accounts for GroupCount
|
|
if (ExtraSpace > 0)
|
|
{
|
|
pSda->sda_pGroups->Groups[0].Sid = (PSID)((PBYTE)(pSda->sda_pGroups) + SizeNeeded);
|
|
RtlCopyMemory(pSda->sda_pGroups->Groups[0].Sid,
|
|
pSidnAttr->Sid,
|
|
RtlLengthSid(pSidnAttr->Sid));
|
|
|
|
pSda->sda_pGroups->Groups[0].Attributes = pSidnAttr->Attributes;
|
|
pSda->sda_pGroups->GroupCount ++;
|
|
|
|
AfpDumpSid("AfpGetUserAndPrimaryGroupSids: Member of ",
|
|
pSda->sda_pGroups->Groups[0].Sid);
|
|
}
|
|
for (i = 0; i < pGroups->GroupCount; i++, j++)
|
|
{
|
|
pSda->sda_pGroups->Groups[j].Sid = (PSID)((PBYTE)(pGroups->Groups[i].Sid) -
|
|
(PBYTE)pGroups +
|
|
(PBYTE)(pSda->sda_pGroups) +
|
|
Offset);
|
|
AfpDumpSid("AfpGetUserAndPrimaryGroupSids: Member of ",
|
|
pSda->sda_pGroups->Groups[j].Sid);
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("AfpGetUserAndPrimaryGroupSids: Attributes %lx\n",
|
|
pSda->sda_pGroups->Groups[j].Attributes));
|
|
}
|
|
} while (False);
|
|
|
|
if (pGroups != (PTOKEN_GROUPS)pGrpsBuffer)
|
|
if (pGroups)
|
|
AfpFreeMemory(pGroups);
|
|
|
|
if (pGrpsBuffer != NULL)
|
|
AfpFreeMemory(pGrpsBuffer);
|
|
|
|
return Status;
|
|
}
|
|
|
|
|
|
|
|
/*** AfpMakeSecurityDescriptorForUser
|
|
*
|
|
* Create a security descriptor for a user. The security descriptor has the
|
|
* Owner Sid, Primary Group Sid and Aces for the User alone.
|
|
*/
|
|
AFPSTATUS
|
|
AfpMakeSecurityDescriptorForUser(
|
|
IN PSID OwnerSid,
|
|
IN PSID GroupSid,
|
|
OUT PISECURITY_DESCRIPTOR * ppSecDesc
|
|
)
|
|
{
|
|
AFPSTATUS Status = AFP_ERR_MISC;
|
|
PISECURITY_DESCRIPTOR pSecDesc;
|
|
int DaclSize;
|
|
PACCESS_ALLOWED_ACE pAce;
|
|
|
|
PAGED_CODE( );
|
|
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("AfpMakeSecurityDescriptorForUser: Entered\n"));
|
|
|
|
do
|
|
{
|
|
// Allocate a security descriptor
|
|
pSecDesc = (PISECURITY_DESCRIPTOR)ALLOC_ACCESS_MEM(sizeof(SECURITY_DESCRIPTOR));
|
|
|
|
*ppSecDesc = pSecDesc;
|
|
if (pSecDesc == NULL)
|
|
break;
|
|
|
|
// Initialize the security descriptor
|
|
RtlCreateSecurityDescriptor(pSecDesc, SECURITY_DESCRIPTOR_REVISION);
|
|
|
|
pSecDesc->Control = SE_DACL_PRESENT;
|
|
|
|
// Set the owner and group Ids in the descriptor
|
|
pSecDesc->Owner = OwnerSid;
|
|
pSecDesc->Group = GroupSid;
|
|
|
|
// Determine the size of the Dacl needed. The sizeof(DWORD) offsets the
|
|
// SidStart field in the ACE. There are 7 aces in this security descriptor:
|
|
//
|
|
// 2 for the owner (owner+inherit for owner)
|
|
// 2 for world (1 for world and 1 inherit for world).
|
|
// 2 for system
|
|
DaclSize = sizeof(ACL) + 2*(sizeof(ACCESS_ALLOWED_ACE) - sizeof(DWORD) +
|
|
RtlLengthSid(OwnerSid)) +
|
|
2*(sizeof(ACCESS_ALLOWED_ACE) - sizeof(DWORD) +
|
|
sizeof(AfpSidWorld)) +
|
|
2*(sizeof(ACCESS_ALLOWED_ACE) - sizeof(DWORD) +
|
|
AfpSizeSidAdmins) +
|
|
2*(sizeof(ACCESS_ALLOWED_ACE) - sizeof(DWORD) +
|
|
RtlLengthSid(&AfpSidSystem));
|
|
|
|
if ((pSecDesc->Dacl = (PACL)ALLOC_ACCESS_MEM(DaclSize)) == NULL)
|
|
break;
|
|
|
|
// Initialize the ACL with one ACE corres. to Owner getting all the
|
|
// privileges. Add another ace which is identical to the first ace but is
|
|
// a inheritance ACE.
|
|
// JH - Add another ace for world with minumum permissions and for administrators
|
|
// with FullControl
|
|
RtlCreateAcl(pSecDesc->Dacl, DaclSize, ACL_REVISION);
|
|
|
|
// we will be adding to this as we add aces, so set it to the min here
|
|
pSecDesc->Dacl->AclSize = sizeof(ACL);
|
|
|
|
pAce = (PACCESS_ALLOWED_ACE)((PBYTE)pSecDesc->Dacl + sizeof(ACL));
|
|
|
|
// Add the ALLOWED_ACE and the corres. inherit Ace for owner
|
|
pAce = afpAddAceToAcl(pSecDesc->Dacl,
|
|
pAce,
|
|
(AFP_READ_ACCESS | AFP_WRITE_ACCESS | AFP_OWNER_ACCESS | FILE_DELETE_CHILD),
|
|
OwnerSid,
|
|
True);
|
|
|
|
if (AfpSidAdmins != NULL)
|
|
{
|
|
// Add the ALLOWED_ACE and the corres. inherit Ace for 'Administrators'
|
|
pAce = afpAddAceToAcl(pSecDesc->Dacl,
|
|
pAce,
|
|
(AFP_READ_ACCESS | AFP_WRITE_ACCESS | AFP_OWNER_ACCESS | FILE_DELETE_CHILD),
|
|
AfpSidAdmins,
|
|
True);
|
|
}
|
|
|
|
// Add a min. permission ace for world, but only if the owner is
|
|
// not world already
|
|
if (!RtlEqualSid(OwnerSid, &AfpSidWorld))
|
|
{
|
|
pAce = afpAddAceToAcl(pSecDesc->Dacl,
|
|
pAce,
|
|
(AFP_MIN_ACCESS),
|
|
&AfpSidWorld,
|
|
True);
|
|
}
|
|
|
|
// Now add Aces for System
|
|
pAce = afpAddAceToAcl(pSecDesc->Dacl,
|
|
pAce,
|
|
AFP_READ_ACCESS | AFP_WRITE_ACCESS | AFP_OWNER_ACCESS,
|
|
&AfpSidSystem,
|
|
True);
|
|
Status = AFP_ERR_NONE;
|
|
} while (False);
|
|
|
|
// Do any cleanup on error
|
|
if (!NT_SUCCESS(Status) && (pSecDesc != NULL))
|
|
{
|
|
if (pSecDesc->Dacl != NULL)
|
|
AfpFreeMemory(pSecDesc->Dacl);
|
|
AfpFreeMemory(pSecDesc);
|
|
}
|
|
|
|
return Status;
|
|
|
|
}
|
|
|
|
|
|
/*** AfpGetAfpPermissions
|
|
*
|
|
* Read the security descriptor for this directory and obtain the SIDs for
|
|
* Owner and Primary group. Determine if this user is a member of the directory
|
|
* primary group. Finally obtain Owner,Group and World permissions.
|
|
*
|
|
* OwnerId, GroupId and permissions will always be valid if this call succeeds.
|
|
*/
|
|
NTSTATUS
|
|
AfpGetAfpPermissions(
|
|
IN PSDA pSda,
|
|
IN HANDLE DirHandle,
|
|
IN OUT PFILEDIRPARM pFDParm
|
|
)
|
|
{
|
|
NTSTATUS Status = STATUS_SUCCESS;
|
|
DWORD SizeNeeded;
|
|
PISECURITY_DESCRIPTOR pSecDesc = NULL;
|
|
PBYTE pAbsSecDesc = NULL; // Used in conversion of
|
|
// sec descriptor to
|
|
// absolute format
|
|
BOOLEAN SawOwnerAce = False,
|
|
SawGroupAce = False,
|
|
SawWorldAce = False,
|
|
CheckUserRights = False;
|
|
ACCESS_MASK OwnerGranted = 0, OwnerDenied = 0,
|
|
GroupGranted = 0, GroupDenied = 0,
|
|
WorldGranted = 0, WorldDenied = 0,
|
|
UserGranted = 0, UserDenied = 0,
|
|
UserAccess = 0;
|
|
|
|
|
|
#ifdef PROFILING
|
|
TIME TimeS, TimeE, TimeD;
|
|
#endif
|
|
|
|
PAGED_CODE( );
|
|
|
|
#ifdef PROFILING
|
|
INTERLOCKED_INCREMENT_LONG(&AfpServerProfile->perf_GetPermsCount);
|
|
AfpGetPerfCounter(&TimeS);
|
|
#endif
|
|
|
|
// Read the security descriptor for this directory and determine the
|
|
// rights for owner/group/world.We want to optimize on how much memory
|
|
// we need to read this in. Its a pain to make a call just to get that.
|
|
// So just make a guess. If that turns out to be short then do the exact
|
|
// allocation.
|
|
do
|
|
{
|
|
// 4096 has been emperically chosen
|
|
SizeNeeded = 4096 - POOL_OVERHEAD;
|
|
do
|
|
{
|
|
if (pSecDesc != NULL)
|
|
{
|
|
AfpFreeMemory(pSecDesc);
|
|
}
|
|
if ((pSecDesc = (PSECURITY_DESCRIPTOR)ALLOC_ACCESS_MEM(SizeNeeded)) == NULL)
|
|
{
|
|
Status = AFP_ERR_MISC;
|
|
if (pSda->sda_ClientType == SDA_CLIENT_ADMIN)
|
|
{
|
|
Status = STATUS_INSUFFICIENT_RESOURCES;
|
|
}
|
|
break;
|
|
}
|
|
Status = NtQuerySecurityObject(DirHandle,
|
|
OWNER_SECURITY_INFORMATION |
|
|
GROUP_SECURITY_INFORMATION |
|
|
DACL_SECURITY_INFORMATION,
|
|
pSecDesc,
|
|
SizeNeeded,
|
|
&SizeNeeded);
|
|
} while ((Status != STATUS_SUCCESS) &&
|
|
((Status == STATUS_BUFFER_TOO_SMALL) ||
|
|
(Status == STATUS_BUFFER_OVERFLOW) ||
|
|
(Status == STATUS_MORE_ENTRIES)));
|
|
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
break;
|
|
}
|
|
|
|
// If the security descriptor is in self-relative form, convert to absolute
|
|
|
|
pSecDesc = (PISECURITY_DESCRIPTOR)((PBYTE)pSecDesc);
|
|
if (pSecDesc->Control & SE_SELF_RELATIVE)
|
|
{
|
|
|
|
DWORD AbsoluteSizeNeeded;
|
|
|
|
// An absolute SD is not necessarily the same size as a relative
|
|
// SD, so an in-place conversion may not be possible.
|
|
AbsoluteSizeNeeded = SizeNeeded;
|
|
Status = RtlSelfRelativeToAbsoluteSD2(pSecDesc, &AbsoluteSizeNeeded);
|
|
if (Status == STATUS_BUFFER_TOO_SMALL)
|
|
{
|
|
// Allocate a new buffer in which to store the absolute
|
|
// security descriptor, copy the contents of the relative
|
|
// descriptor in and try again
|
|
|
|
pAbsSecDesc = (PBYTE)ALLOC_ACCESS_MEM(AbsoluteSizeNeeded);
|
|
if (pAbsSecDesc == NULL)
|
|
{
|
|
Status = STATUS_NO_MEMORY;
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_ERR,
|
|
("AfpGetAfpPermissions: ALLOC_ACCESS_MEM error\n"));
|
|
}
|
|
else
|
|
{
|
|
RtlCopyMemory(pAbsSecDesc, pSecDesc, SizeNeeded);
|
|
Status = RtlSelfRelativeToAbsoluteSD2 (pAbsSecDesc,
|
|
&AbsoluteSizeNeeded);
|
|
if (NT_SUCCESS(Status))
|
|
{
|
|
// We don't need relative form anymore,
|
|
// we will work with the Absolute form
|
|
if (pSecDesc != NULL)
|
|
{
|
|
AfpFreeMemory(pSecDesc);
|
|
}
|
|
pSecDesc = (PISECURITY_DESCRIPTOR)pAbsSecDesc;
|
|
}
|
|
else
|
|
{
|
|
// We cannot use Absolute Form, throw it away
|
|
AfpFreeMemory(pAbsSecDesc);
|
|
pAbsSecDesc = NULL;
|
|
}
|
|
}
|
|
|
|
}
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_ERR,
|
|
("AfpGetAfpPermissions: RtlSelfRelativeToAbsoluteSD2: returned error %lx\n", Status));
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Now determine if the user is a member of the directories primary group.
|
|
pFDParm->_fdp_OwnerId = 0;
|
|
pFDParm->_fdp_GroupId = 0;
|
|
pFDParm->_fdp_UserIsOwner = False;
|
|
pFDParm->_fdp_UserIsMemberOfDirGroup = False;
|
|
|
|
if (pSecDesc->Owner != NULL)
|
|
{
|
|
AfpDumpSid("AfpGetAfpPermissions: OwnerSid", pSecDesc->Owner);
|
|
|
|
pFDParm->_fdp_UserIsOwner =
|
|
(RtlEqualSid(pSecDesc->Owner, pSda->sda_UserSid) ||
|
|
((pSda->sda_ClientType != SDA_CLIENT_GUEST) &&
|
|
(pSda->sda_ClientType != SDA_CLIENT_ADMIN) &&
|
|
afpCheckUserMemberOfGroup(pSda,
|
|
pSecDesc->Owner)));
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("AfpGetAfpPermissions: User %s Owner\n",
|
|
pFDParm->_fdp_UserIsOwner ? "is" : "isnt"));
|
|
|
|
if (!NT_SUCCESS(Status = AfpSidToMacId(pSecDesc->Owner,
|
|
&pFDParm->_fdp_OwnerId)))
|
|
{
|
|
// If we cant map the Sid, return Id SE_NULL_POSIX_ID
|
|
pFDParm->_fdp_OwnerId = SE_NULL_POSIX_ID;
|
|
Status = AFP_ERR_NONE;
|
|
}
|
|
}
|
|
|
|
if (pSecDesc->Group != NULL)
|
|
{
|
|
AfpDumpSid("AfpGetAfpPermissions: GroupSid", pSecDesc->Group);
|
|
|
|
if (!pFDParm->_fdp_UserIsOwner)
|
|
pFDParm->_fdp_UserIsMemberOfDirGroup =
|
|
(RtlEqualSid(pSecDesc->Group, pSda->sda_UserSid) ||
|
|
((pSda->sda_ClientType != SDA_CLIENT_GUEST) &&
|
|
(pSda->sda_ClientType != SDA_CLIENT_ADMIN) &&
|
|
afpCheckUserMemberOfGroup(pSda,
|
|
pSecDesc->Group)));
|
|
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("AfpGetAfpPermissions: Group %s member of PrimaryGroup\n",
|
|
pFDParm->_fdp_UserIsMemberOfDirGroup ? "is" : "isnt"));
|
|
|
|
if (!NT_SUCCESS(Status = AfpSidToMacId(pSecDesc->Group,
|
|
&pFDParm->_fdp_GroupId)))
|
|
{
|
|
// If we cant map the Sid, return Id SE_NULL_POSIX_ID
|
|
pFDParm->_fdp_GroupId = SE_NULL_POSIX_ID;
|
|
Status = AFP_ERR_NONE;
|
|
}
|
|
}
|
|
|
|
// Walk through the ACL list and determine Owner/Group/World and User
|
|
// permissions. For Owner/Group and User, if the specific ace's are
|
|
// not present then they inherit the world permissions.
|
|
//
|
|
// A NULL Acl => All rights to everyone. An empty Acl on the other
|
|
// hand => no access for anyone.
|
|
|
|
pFDParm->_fdp_UserRights = 0;
|
|
pFDParm->_fdp_WorldRights = 0;
|
|
|
|
if ((pSecDesc->Control & SE_DACL_PRESENT) &&
|
|
(pSecDesc->Dacl != NULL))
|
|
{
|
|
USHORT i;
|
|
PSID pSid;
|
|
PACL pAcl;
|
|
PACCESS_ALLOWED_ACE pAce;
|
|
|
|
pAcl = pSecDesc->Dacl;
|
|
pAce = (PACCESS_ALLOWED_ACE)((PBYTE)pAcl + sizeof(ACL));
|
|
CheckUserRights = ((pSda->sda_ClientType != SDA_CLIENT_GUEST) &&
|
|
(pSda->sda_ClientType != SDA_CLIENT_ADMIN));
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("AfpGetAfpPermissions: # of aces %d\n", pSecDesc->Dacl->AceCount));
|
|
for (i = 0; i < pSecDesc->Dacl->AceCount; i++)
|
|
{
|
|
if ((pAce->Header.AceType != ACCESS_ALLOWED_ACE_TYPE) &&
|
|
(pAce->Header.AceType != ACCESS_DENIED_ACE_TYPE))
|
|
{
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("AfpGetAfpPermissions: Skipping ACE of type (%ld)\n", pAce->Header.AceType));
|
|
pAce = (PACCESS_ALLOWED_ACE)((PBYTE)pAce + pAce->Header.AceSize);
|
|
continue;
|
|
}
|
|
|
|
pSid = (PSID)(&pAce->SidStart);
|
|
|
|
// Ignore inherit-only aces, & system
|
|
if (pAce->Header.AceFlags & INHERIT_ONLY_ACE)
|
|
{
|
|
AfpDumpSidnMask("AfpGetAfpPermissions: Skipping",
|
|
pSid,
|
|
pAce->Mask,
|
|
pAce->Header.AceType,
|
|
pAce->Header.AceFlags);
|
|
}
|
|
else
|
|
{
|
|
AfpDumpSidnMask("AfpGetAfpPermissions: ACE",
|
|
pSid,
|
|
pAce->Mask,
|
|
pAce->Header.AceType,
|
|
pAce->Header.AceFlags);
|
|
|
|
if ((pSecDesc->Owner != NULL) &&
|
|
RtlEqualSid(pSid, pSecDesc->Owner))
|
|
{
|
|
if (pAce->Header.AceType == ACCESS_ALLOWED_ACE_TYPE)
|
|
{
|
|
OwnerGranted |= (pAce->Mask & ~OwnerDenied);
|
|
}
|
|
else
|
|
{
|
|
OwnerDenied |= (pAce->Mask & ~OwnerGranted);
|
|
}
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("Owner: %s Ace Mask %lx\n",
|
|
(pAce->Header.AceType == ACCESS_ALLOWED_ACE_TYPE) ?
|
|
"Allow" : "Deny",
|
|
pAce->Mask));
|
|
SawOwnerAce = True;
|
|
}
|
|
|
|
if ((pSecDesc->Group != NULL) &&
|
|
RtlEqualSid(pSid, pSecDesc->Group))
|
|
{
|
|
if (pAce->Header.AceType == ACCESS_ALLOWED_ACE_TYPE)
|
|
{
|
|
GroupGranted |= (pAce->Mask & ~GroupDenied);
|
|
}
|
|
else
|
|
{
|
|
GroupDenied |= (pAce->Mask & ~GroupGranted);
|
|
}
|
|
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("Group: %s Ace Mask %lx\n",
|
|
(pAce->Header.AceType == ACCESS_ALLOWED_ACE_TYPE) ?
|
|
"Allow" : "Deny",
|
|
pAce->Mask));
|
|
SawGroupAce = True;
|
|
}
|
|
|
|
if ((RtlEqualSid(pSid, (PSID)&AfpSidWorld)))
|
|
{
|
|
if (pAce->Header.AceType == ACCESS_ALLOWED_ACE_TYPE)
|
|
{
|
|
WorldGranted |= (pAce->Mask & ~WorldDenied);
|
|
}
|
|
else
|
|
{
|
|
WorldDenied |= (pAce->Mask & ~WorldGranted);
|
|
}
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("World: %s Ace Mask %lx\n",
|
|
(pAce->Header.AceType == ACCESS_ALLOWED_ACE_TYPE) ?
|
|
"Allow" : "Deny",
|
|
pAce->Mask));
|
|
SawWorldAce = True;
|
|
}
|
|
}
|
|
|
|
pAce = (PACCESS_ALLOWED_ACE)((PBYTE)pAce + pAce->Header.AceSize);
|
|
}
|
|
}
|
|
else // Security descriptor not present, party time
|
|
{
|
|
pFDParm->_fdp_WorldRights = DIR_ACCESS_ALL;
|
|
pFDParm->_fdp_UserRights = DIR_ACCESS_ALL | DIR_ACCESS_OWNER;
|
|
}
|
|
|
|
if (!SawGroupAce)
|
|
pFDParm->_fdp_GroupRights = pFDParm->_fdp_WorldRights;
|
|
|
|
if (SawOwnerAce)
|
|
{
|
|
AfpAccessMask2AfpPermissions(pFDParm->_fdp_OwnerRights,
|
|
OwnerGranted,
|
|
ACCESS_ALLOWED_ACE_TYPE);
|
|
}
|
|
|
|
if (SawGroupAce)
|
|
{
|
|
AfpAccessMask2AfpPermissions(pFDParm->_fdp_GroupRights,
|
|
GroupGranted,
|
|
ACCESS_ALLOWED_ACE_TYPE);
|
|
}
|
|
|
|
if (SawWorldAce)
|
|
{
|
|
AfpAccessMask2AfpPermissions(pFDParm->_fdp_WorldRights,
|
|
WorldGranted,
|
|
ACCESS_ALLOWED_ACE_TYPE);
|
|
}
|
|
|
|
// Get the ACCESS_MASK allowed for the user
|
|
|
|
Status = afpGetUserAccess(
|
|
pSda,
|
|
pSecDesc,
|
|
&UserAccess);
|
|
if (NT_SUCCESS(Status))
|
|
{
|
|
UserGranted = UserAccess;
|
|
}
|
|
else
|
|
{
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_ERR,
|
|
("afpGetUserAccess failed with error %0x\n",
|
|
Status));
|
|
Status = AFP_ERR_NONE;
|
|
}
|
|
|
|
AfpAccessMask2AfpPermissions(pFDParm->_fdp_UserRights,
|
|
UserGranted,
|
|
ACCESS_ALLOWED_ACE_TYPE);
|
|
|
|
// If this is a standalone server and the primary group of the
|
|
// directory is MACHINE\None, do not return this information to
|
|
// the caller.
|
|
if (AfpServerIsStandalone &&
|
|
(pSecDesc->Group != NULL) &&
|
|
RtlEqualSid(pSecDesc->Group, AfpSidNone))
|
|
{
|
|
pFDParm->_fdp_GroupRights = 0;
|
|
pFDParm->_fdp_GroupId = 0;
|
|
}
|
|
|
|
if (pSda->sda_ClientType == SDA_CLIENT_GUEST)
|
|
pFDParm->_fdp_UserRights = pFDParm->_fdp_WorldRights;
|
|
|
|
} while (False);
|
|
|
|
if (pSecDesc != NULL)
|
|
AfpFreeMemory(pSecDesc);
|
|
|
|
#ifdef PROFILING
|
|
AfpGetPerfCounter(&TimeE);
|
|
TimeD.QuadPart = TimeE.QuadPart - TimeS.QuadPart;
|
|
INTERLOCKED_ADD_LARGE_INTGR(&AfpServerProfile->perf_GetPermsTime,
|
|
TimeD,
|
|
&AfpStatisticsLock);
|
|
#endif
|
|
return Status;
|
|
}
|
|
|
|
|
|
|
|
/*** afpMoveAces
|
|
*
|
|
* Move a bunch of aces from the old security descriptor to the new security
|
|
* descriptor.
|
|
*/
|
|
LOCAL PACCESS_ALLOWED_ACE
|
|
afpMoveAces(
|
|
IN PACL pOldDacl,
|
|
IN PACCESS_ALLOWED_ACE pAceStart,
|
|
IN PSID pSidOldOwner,
|
|
IN PSID pSidNewOwner,
|
|
IN PSID pSidOldGroup,
|
|
IN PSID pSidNewGroup,
|
|
IN BOOLEAN DenyAces,
|
|
IN BOOLEAN InheritedAces,
|
|
IN OUT PACL pNewDacl
|
|
)
|
|
{
|
|
USHORT i;
|
|
PACCESS_ALLOWED_ACE pAceOld;
|
|
PSID pSidAce;
|
|
|
|
PAGED_CODE( );
|
|
|
|
for (i = 0, pAceOld = (PACCESS_ALLOWED_ACE)((PBYTE)pOldDacl + sizeof(ACL));
|
|
i < pOldDacl->AceCount;
|
|
i++, pAceOld = (PACCESS_ALLOWED_ACE)((PBYTE)pAceOld + pAceOld->Header.AceSize))
|
|
{
|
|
if (InheritedAces && ((pAceOld->Header.AceFlags & INHERITED_ACE) != INHERITED_ACE))
|
|
continue;
|
|
|
|
if ((!InheritedAces) && ((pAceOld->Header.AceFlags & INHERITED_ACE) == INHERITED_ACE))
|
|
continue;
|
|
|
|
// Note: All deny aces are ahead of the grant aces.
|
|
if (DenyAces && (pAceOld->Header.AceType != ACCESS_DENIED_ACE_TYPE))
|
|
break;
|
|
|
|
if (!DenyAces && (pAceOld->Header.AceType == ACCESS_DENIED_ACE_TYPE))
|
|
continue;
|
|
|
|
pSidAce = (PSID)(&pAceOld->SidStart);
|
|
if (!RtlEqualSid(pSidAce, &AfpSidWorld) &&
|
|
!RtlEqualSid(pSidAce, &AfpSidSystem) &&
|
|
!RtlEqualSid(pSidAce, pSidOldOwner) &&
|
|
!RtlEqualSid(pSidAce, pSidNewOwner) &&
|
|
!RtlEqualSid(pSidAce, pSidOldGroup) &&
|
|
!RtlEqualSid(pSidAce, pSidNewGroup))
|
|
{
|
|
RtlCopyMemory(pAceStart, pAceOld, pAceOld->Header.AceSize);
|
|
pNewDacl->AclSize += pAceOld->Header.AceSize;
|
|
pNewDacl->AceCount ++;
|
|
pAceStart = (PACCESS_ALLOWED_ACE)((PBYTE)pAceStart +
|
|
pAceStart->Header.AceSize);
|
|
}
|
|
}
|
|
return pAceStart;
|
|
}
|
|
|
|
|
|
/*** AfpSetAfpPermissions
|
|
*
|
|
* Set the permissions on this directory. Also optionally set the owner and
|
|
* group ids. For setting the owner and group ids verify if the user has the
|
|
* needed access. This access is however not good enough. We check for this
|
|
* access but do the actual setting of the permissions in the special server
|
|
* context (RESTORE privilege is needed).
|
|
*/
|
|
AFPSTATUS
|
|
AfpSetAfpPermissions(
|
|
IN HANDLE DirHandle,
|
|
IN DWORD Bitmap,
|
|
IN PFILEDIRPARM pFDParm
|
|
)
|
|
{
|
|
AFPSTATUS Status = STATUS_SUCCESS;
|
|
DWORD SizeNeeded;
|
|
PISECURITY_DESCRIPTOR pSecDesc;
|
|
PBYTE pAbsSecDesc = NULL; // Used in conversion of
|
|
// sec descriptor to
|
|
// absolute format
|
|
SECURITY_INFORMATION SecInfo = DACL_SECURITY_INFORMATION;
|
|
PSID pSidOwner = NULL, pSidGroup = NULL;
|
|
PSID pSidOldOwner, pSidOldGroup;
|
|
BOOLEAN SawOwnerAce = False, SawGroupAce = False;
|
|
BOOLEAN OwnerIsWorld = False, GroupIsWorld = False;
|
|
BOOLEAN fDir = IsDir(pFDParm);
|
|
PACL pDaclNew = NULL;
|
|
PACCESS_ALLOWED_ACE pAce;
|
|
LONG SizeNewDacl;
|
|
#ifdef PROFILING
|
|
TIME TimeS, TimeE, TimeD;
|
|
#endif
|
|
|
|
PAGED_CODE( );
|
|
|
|
#ifdef PROFILING
|
|
INTERLOCKED_INCREMENT_LONG(&AfpServerProfile->perf_SetPermsCount);
|
|
AfpGetPerfCounter(&TimeS);
|
|
#endif
|
|
do
|
|
{
|
|
// Read the security descriptor for this directory
|
|
SizeNeeded = 4096 - POOL_OVERHEAD;
|
|
pSecDesc = NULL;
|
|
|
|
do
|
|
{
|
|
if (pSecDesc != NULL)
|
|
{
|
|
AfpFreeMemory(pSecDesc);
|
|
}
|
|
|
|
SizeNewDacl = SizeNeeded;
|
|
if ((pSecDesc = (PSECURITY_DESCRIPTOR)ALLOC_ACCESS_MEM(SizeNeeded)) == NULL)
|
|
{
|
|
Status = AFP_ERR_MISC;
|
|
break;
|
|
}
|
|
|
|
Status = NtQuerySecurityObject(DirHandle,
|
|
OWNER_SECURITY_INFORMATION |
|
|
GROUP_SECURITY_INFORMATION |
|
|
DACL_SECURITY_INFORMATION,
|
|
pSecDesc,
|
|
SizeNeeded,
|
|
&SizeNeeded);
|
|
} while ((Status != STATUS_SUCCESS) &&
|
|
((Status == STATUS_BUFFER_TOO_SMALL) ||
|
|
(Status == STATUS_BUFFER_OVERFLOW) ||
|
|
(Status == STATUS_MORE_ENTRIES)));
|
|
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
Status = AfpIoConvertNTStatusToAfpStatus(Status);
|
|
break;
|
|
}
|
|
|
|
|
|
pSecDesc = (PISECURITY_DESCRIPTOR)((PBYTE)pSecDesc);
|
|
// If the security descriptor is in self-relative form, convert to absolute
|
|
if (pSecDesc->Control & SE_SELF_RELATIVE)
|
|
{
|
|
DWORD AbsoluteSizeNeeded;
|
|
|
|
// An absolute SD is not necessarily the same size as a relative
|
|
// SD, so an in-place conversion may not be possible.
|
|
|
|
AbsoluteSizeNeeded = SizeNeeded;
|
|
Status = RtlSelfRelativeToAbsoluteSD2(pSecDesc, &AbsoluteSizeNeeded);
|
|
if (Status == STATUS_BUFFER_TOO_SMALL)
|
|
{
|
|
// Allocate a new buffer in which to store the absolute
|
|
// security descriptor, copy the contents of the relative
|
|
// descriptor in and try again
|
|
|
|
pAbsSecDesc = (PBYTE)ALLOC_ACCESS_MEM(AbsoluteSizeNeeded);
|
|
if (pAbsSecDesc == NULL)
|
|
{
|
|
Status = STATUS_NO_MEMORY;
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_ERR,
|
|
("AfpSetAfpPermissions: ALLOC_ACCESS_MEM 2 error\n"));
|
|
}
|
|
else
|
|
{
|
|
RtlCopyMemory(pAbsSecDesc, pSecDesc, SizeNeeded);
|
|
Status = RtlSelfRelativeToAbsoluteSD2 (pAbsSecDesc,
|
|
&AbsoluteSizeNeeded);
|
|
if (NT_SUCCESS(Status))
|
|
{
|
|
// We don't need relative form anymore,
|
|
// we will work with the Absolute form
|
|
if (pSecDesc != NULL)
|
|
{
|
|
AfpFreeMemory(pSecDesc);
|
|
}
|
|
pSecDesc = (PISECURITY_DESCRIPTOR)pAbsSecDesc;
|
|
}
|
|
else
|
|
{
|
|
// We cannot use Absolute Form, throw it away
|
|
AfpFreeMemory(pAbsSecDesc);
|
|
pAbsSecDesc = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_ERR,
|
|
("AfpSetAfpPermissions: RtlSelfRelativeToAbsoluteSD2: returned error %lx\n", Status));
|
|
break;
|
|
}
|
|
SizeNeeded = AbsoluteSizeNeeded;
|
|
}
|
|
SizeNewDacl = SizeNeeded;
|
|
|
|
// Add SE_DACL_AUTO_INHERIT_REQ
|
|
pSecDesc->Control |= SE_DACL_AUTO_INHERIT_REQ;
|
|
|
|
// Save the old Owner and Group Sids
|
|
pSidOldOwner = pSecDesc->Owner;
|
|
pSidOldGroup = pSecDesc->Group;
|
|
|
|
// Convert the owner/group ids, if any to be set to their corres. sids
|
|
if (Bitmap & DIR_BITMAP_OWNERID)
|
|
{
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("AfpSetAfpPermissions: Setting Owner to ID %lx\n",
|
|
pFDParm->_fdp_OwnerId));
|
|
|
|
if (AfpMacIdToSid(pFDParm->_fdp_OwnerId, &pSidOwner) != STATUS_SUCCESS)
|
|
{
|
|
Status = AFP_ERR_MISC;
|
|
break;
|
|
}
|
|
|
|
// Don't allow owner sid to be set as the NULL sid, or
|
|
// to what it is presently set to
|
|
if (!RtlEqualSid(pSecDesc->Owner, pSidOwner) &&
|
|
!RtlEqualSid(&AfpSidNull, pSidOwner))
|
|
{
|
|
AfpDumpSid("AfpSetAfpPermissions: Setting Owner Sid to ", pSidOwner);
|
|
pSecDesc->Owner = pSidOwner;
|
|
SecInfo |= OWNER_SECURITY_INFORMATION;
|
|
}
|
|
}
|
|
|
|
if (Bitmap & DIR_BITMAP_GROUPID)
|
|
{
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("AfpSetAfpPermissions: Setting Group to ID %lx\n",
|
|
pFDParm->_fdp_GroupId));
|
|
|
|
if (AfpMacIdToSid(pFDParm->_fdp_GroupId, &pSidGroup) != STATUS_SUCCESS)
|
|
{
|
|
Status = AFP_ERR_MISC;
|
|
break;
|
|
}
|
|
|
|
// Don't allow group sid to be set as the NULL or None sid, or
|
|
// to what it is presently set to
|
|
if (!RtlEqualSid(pSecDesc->Group, pSidGroup) &&
|
|
!RtlEqualSid(&AfpSidNull, pSidGroup) &&
|
|
(!AfpServerIsStandalone || !RtlEqualSid(AfpSidNone, pSidGroup)))
|
|
{
|
|
AfpDumpSid("AfpSetAfpPermissions: Setting Group Sid to ", pSidGroup);
|
|
pSecDesc->Group = pSidGroup;
|
|
SecInfo |= GROUP_SECURITY_INFORMATION;
|
|
}
|
|
|
|
}
|
|
|
|
// If either the owner or group or both is 'EveryOne' then coalesce the
|
|
// permissions
|
|
if (RtlEqualSid(pSecDesc->Owner, pSecDesc->Group))
|
|
{
|
|
pFDParm->_fdp_OwnerRights |= pFDParm->_fdp_GroupRights;
|
|
pFDParm->_fdp_GroupRights |= pFDParm->_fdp_OwnerRights;
|
|
}
|
|
|
|
if (RtlEqualSid(pSecDesc->Owner, &AfpSidWorld))
|
|
{
|
|
pFDParm->_fdp_WorldRights |= (pFDParm->_fdp_OwnerRights | DIR_ACCESS_OWNER);
|
|
OwnerIsWorld = True;
|
|
}
|
|
|
|
if (RtlEqualSid(pSecDesc->Group, &AfpSidWorld))
|
|
{
|
|
pFDParm->_fdp_WorldRights |= pFDParm->_fdp_GroupRights;
|
|
GroupIsWorld = True;
|
|
}
|
|
|
|
// Construct the new Dacl. This consists of Aces for World, Owner and Group
|
|
// followed by Old Aces for everybody else, but with Aces for World, OldOwner
|
|
// and OldGroup stripped out. First determine space for the new Dacl and
|
|
// allocated space for the new Dacl. Lets be exteremely conservative. We
|
|
// have two aces each for owner/group/world.
|
|
|
|
SizeNewDacl +=
|
|
(RtlLengthSid(pSecDesc->Owner) + sizeof(ACCESS_ALLOWED_ACE) +
|
|
RtlLengthSid(pSecDesc->Group) + sizeof(ACCESS_ALLOWED_ACE) +
|
|
sizeof(AfpSidSystem) + sizeof(ACCESS_ALLOWED_ACE) +
|
|
sizeof(AfpSidWorld) + sizeof(ACCESS_ALLOWED_ACE)) * 2;
|
|
|
|
if ((pDaclNew = (PACL)ALLOC_ACCESS_MEM(SizeNewDacl)) == NULL)
|
|
{
|
|
Status = AFP_ERR_MISC;
|
|
break;
|
|
}
|
|
|
|
RtlCreateAcl(pDaclNew, SizeNewDacl, ACL_REVISION);
|
|
|
|
// we will be adding to this as we add aces, so set it to the min here
|
|
pDaclNew->AclSize = sizeof(ACL);
|
|
|
|
pAce = (PACCESS_ALLOWED_ACE)((PBYTE)pDaclNew + sizeof(ACL));
|
|
|
|
// At this time the Acl list is empty, i.e. no access for anybody
|
|
|
|
// Start off by copying the Explicit/Non-inherited Deny Aces from
|
|
// the original Dacl list
|
|
// weeding out the Aces for World, old and new owner, new and old
|
|
// group, creator owner and creator group
|
|
if (pSecDesc->Dacl != NULL)
|
|
{
|
|
pAce = afpMoveAces(pSecDesc->Dacl,
|
|
pAce,
|
|
pSidOldOwner,
|
|
pSecDesc->Owner,
|
|
pSidOldGroup,
|
|
pSecDesc->Group,
|
|
True,
|
|
False,
|
|
pDaclNew);
|
|
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("AfpSetAfpPermissions: Added (%d) old Non-inherited Deny Aces\n",
|
|
pDaclNew->AceCount));
|
|
|
|
ASSERT(((PBYTE)pAce - (PBYTE)pDaclNew) < SizeNewDacl);
|
|
}
|
|
|
|
// Now add Allowed Aces for System, World, Group & Owner - in that order
|
|
|
|
pAce = afpAddAceToAcl(pDaclNew,
|
|
pAce,
|
|
AFP_READ_ACCESS | AFP_WRITE_ACCESS | AFP_OWNER_ACCESS,
|
|
&AfpSidSystem,
|
|
fDir);
|
|
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("AfpSetAfpPermissions: Added Aces for System (%d)\n",
|
|
pDaclNew->AceCount));
|
|
|
|
ASSERT(((PBYTE)pAce - (PBYTE)pDaclNew) < SizeNewDacl);
|
|
|
|
// Now add Ace for World
|
|
pAce = afpAddAceToAcl(pDaclNew,
|
|
pAce,
|
|
afpPermissions2NtMask(pFDParm->_fdp_WorldRights),
|
|
&AfpSidWorld,
|
|
fDir);
|
|
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("AfpSetAfpPermissions: Added Aces for World (%d)\n",
|
|
pDaclNew->AceCount));
|
|
|
|
ASSERT(((PBYTE)pAce - (PBYTE)pDaclNew) < SizeNewDacl);
|
|
|
|
// Now add Ace for Group
|
|
if (!GroupIsWorld &&
|
|
!RtlEqualSid(pSecDesc->Group, &AfpSidNull) &&
|
|
(!AfpServerIsStandalone || !RtlEqualSid(pSecDesc->Group, AfpSidNone)))
|
|
{
|
|
pAce = afpAddAceToAcl(pDaclNew,
|
|
pAce,
|
|
afpPermissions2NtMask(pFDParm->_fdp_GroupRights),
|
|
pSecDesc->Group,
|
|
fDir);
|
|
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("AfpSetAfpPermissions: Added Aces for Group (%d)\n",
|
|
pDaclNew->AceCount));
|
|
|
|
ASSERT(((PBYTE)pAce - (PBYTE)pDaclNew) < SizeNewDacl);
|
|
}
|
|
|
|
if (!OwnerIsWorld && !RtlEqualSid(pSecDesc->Owner, &AfpSidNull))
|
|
{
|
|
pFDParm->_fdp_OwnerRights |= DIR_ACCESS_OWNER;
|
|
pAce = afpAddAceToAcl(pDaclNew,
|
|
pAce,
|
|
afpPermissions2NtMask(pFDParm->_fdp_OwnerRights),
|
|
pSecDesc->Owner,
|
|
fDir);
|
|
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("AfpSetAfpPermissions: Added Aces for Owner (%d)\n",
|
|
pDaclNew->AceCount));
|
|
|
|
ASSERT(((PBYTE)pAce - (PBYTE)pDaclNew) < SizeNewDacl);
|
|
}
|
|
|
|
|
|
// Now add in the Explicit/Non-inherited Grant Aces from the
|
|
// original Dacl list weeding out
|
|
// the Aces for World, old and new owner, new and old group, creator
|
|
// owner and creator group
|
|
if (pSecDesc->Dacl != NULL)
|
|
{
|
|
pAce = afpMoveAces(pSecDesc->Dacl,
|
|
pAce,
|
|
pSidOldOwner,
|
|
pSecDesc->Owner,
|
|
pSidOldGroup,
|
|
pSecDesc->Group,
|
|
False,
|
|
False,
|
|
pDaclNew);
|
|
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("AfpSetAfpPermissions: Added (%d) old Non-inherited Grant Aces\n",
|
|
pDaclNew->AceCount));
|
|
|
|
ASSERT(((PBYTE)pAce - (PBYTE)pDaclNew) < SizeNewDacl);
|
|
}
|
|
|
|
// Now add in the Non-explicit/Inherited Deny Aces from
|
|
// the original Dacl list
|
|
// weeding out the Aces for World, old and new owner, new and old
|
|
// group, creator owner and creator group
|
|
if (pSecDesc->Dacl != NULL)
|
|
{
|
|
pAce = afpMoveAces(pSecDesc->Dacl,
|
|
pAce,
|
|
pSidOldOwner,
|
|
pSecDesc->Owner,
|
|
pSidOldGroup,
|
|
pSecDesc->Group,
|
|
True,
|
|
True,
|
|
pDaclNew);
|
|
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("AfpSetAfpPermissions: Added (%d) old Inherited Deny Aces\n",
|
|
pDaclNew->AceCount));
|
|
|
|
ASSERT(((PBYTE)pAce - (PBYTE)pDaclNew) < SizeNewDacl);
|
|
}
|
|
|
|
// Now add in the Explicit/Non-inherited Grant Aces from the
|
|
// original Dacl list weeding out
|
|
// the Aces for World, old and new owner, new and old group, creator
|
|
// owner and creator group
|
|
if (pSecDesc->Dacl != NULL)
|
|
{
|
|
pAce = afpMoveAces(pSecDesc->Dacl,
|
|
pAce,
|
|
pSidOldOwner,
|
|
pSecDesc->Owner,
|
|
pSidOldGroup,
|
|
pSecDesc->Group,
|
|
False,
|
|
True,
|
|
pDaclNew);
|
|
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("AfpSetAfpPermissions: Added (%d) old Inherited Grant Aces (%d)\n",
|
|
pDaclNew->AceCount));
|
|
|
|
ASSERT(((PBYTE)pAce - (PBYTE)pDaclNew) < SizeNewDacl);
|
|
}
|
|
|
|
// Now set the new security descriptor
|
|
pSecDesc->Dacl = pDaclNew;
|
|
|
|
// We need to impersonate the FspToken while we do this
|
|
AfpImpersonateClient(NULL);
|
|
Status = NtSetSecurityObject(DirHandle, SecInfo, pSecDesc);
|
|
if (!NT_SUCCESS(Status))
|
|
Status = AfpIoConvertNTStatusToAfpStatus(Status);
|
|
AfpRevertBack();
|
|
} while (False);
|
|
|
|
// Free the allocated buffers before we return
|
|
if (pSecDesc != NULL)
|
|
AfpFreeMemory(pSecDesc);
|
|
if (pDaclNew != NULL)
|
|
AfpFreeMemory(pDaclNew);
|
|
|
|
|
|
#ifdef PROFILING
|
|
AfpGetPerfCounter(&TimeE);
|
|
TimeD.QuadPart = TimeE.QuadPart - TimeS.QuadPart;
|
|
INTERLOCKED_ADD_LARGE_INTGR(&AfpServerProfile->perf_SetPermsTime,
|
|
TimeD,
|
|
&AfpStatisticsLock);
|
|
#endif
|
|
return Status;
|
|
}
|
|
|
|
|
|
/*** afpPermissions2NtMask
|
|
*
|
|
* Map Afp permissions to Nt access mask. FILE_DELETE_CHILD is added ONLY
|
|
* when all the Afp bits are set. This is in line with the FileManager
|
|
* which only sets this bit if "Full Control" is specified. Also under
|
|
* NT security model, FILE_DELETE_CHILD overrides any child access control
|
|
* as far as the ability to delete that entity goes.
|
|
*/
|
|
LOCAL ACCESS_MASK
|
|
afpPermissions2NtMask(
|
|
IN BYTE AfpPermissions
|
|
)
|
|
{
|
|
ACCESS_MASK NtAccess = 0;
|
|
|
|
PAGED_CODE( );
|
|
|
|
if (AfpPermissions & DIR_ACCESS_OWNER)
|
|
NtAccess |= AFP_OWNER_ACCESS;
|
|
|
|
if ((AfpPermissions & DIR_ACCESS_ALL) == DIR_ACCESS_ALL)
|
|
NtAccess |= AFP_READ_ACCESS | AFP_WRITE_ACCESS | FILE_DELETE_CHILD;
|
|
else
|
|
{
|
|
if (AfpPermissions & (DIR_ACCESS_READ | DIR_ACCESS_SEARCH))
|
|
NtAccess |= AFP_READ_ACCESS;
|
|
|
|
if (AfpPermissions & DIR_ACCESS_WRITE)
|
|
NtAccess |= AFP_WRITE_ACCESS;
|
|
}
|
|
return NtAccess;
|
|
}
|
|
|
|
|
|
/*** afpAddAceToAcl
|
|
*
|
|
* Build an Ace corres. to the Sid(s) and mask and add these to the Acl. It is
|
|
* assumed that the Acl has space for the Aces. If the mask is 0 i.e. no access
|
|
* we give AFP_MIN_ACCESS. This is so that the file/dir permissions can be
|
|
* queried and a belted icon is generated instead of nothing.
|
|
*/
|
|
LOCAL PACCESS_ALLOWED_ACE
|
|
afpAddAceToAcl(
|
|
IN PACL pAcl,
|
|
IN PACCESS_ALLOWED_ACE pAce,
|
|
IN ACCESS_MASK Mask,
|
|
IN PSID pSid,
|
|
IN BOOLEAN fInherit
|
|
)
|
|
{
|
|
USHORT SidLen;
|
|
|
|
PAGED_CODE( );
|
|
|
|
SidLen = (USHORT)RtlLengthSid(pSid);
|
|
|
|
// Add a vanilla ace
|
|
pAcl->AceCount ++;
|
|
pAce->Mask = Mask | SYNCHRONIZE | AFP_MIN_ACCESS;
|
|
pAce->Header.AceFlags = 0;
|
|
pAce->Header.AceType = ACCESS_ALLOWED_ACE_TYPE;
|
|
pAce->Header.AceSize = (USHORT)(sizeof(ACE_HEADER) +
|
|
sizeof(ACCESS_MASK) +
|
|
SidLen);
|
|
|
|
RtlCopyMemory((PSID)(&pAce->SidStart), pSid, SidLen);
|
|
|
|
pAcl->AclSize += pAce->Header.AceSize;
|
|
|
|
AfpDumpSidnMask("afpAddAceToAcl ",
|
|
pSid,
|
|
pAce->Mask,
|
|
ACCESS_ALLOWED_ACE_TYPE,
|
|
pAce->Header.AceFlags);
|
|
|
|
// Now add an inherit ace
|
|
if (fInherit)
|
|
{
|
|
pAce = (PACCESS_ALLOWED_ACE)((PBYTE)pAce + pAce->Header.AceSize);
|
|
pAcl->AceCount ++;
|
|
pAce->Mask = Mask | SYNCHRONIZE | AFP_MIN_ACCESS;
|
|
pAce->Header.AceFlags = CONTAINER_INHERIT_ACE |
|
|
OBJECT_INHERIT_ACE |
|
|
INHERIT_ONLY_ACE;
|
|
pAce->Header.AceType = ACCESS_ALLOWED_ACE_TYPE;
|
|
pAce->Header.AceSize = (USHORT)(sizeof(ACE_HEADER) +
|
|
sizeof(ACCESS_MASK) +
|
|
SidLen);
|
|
RtlCopyMemory((PSID)(&pAce->SidStart), pSid, SidLen);
|
|
|
|
pAcl->AclSize += pAce->Header.AceSize;
|
|
|
|
AfpDumpSidnMask("afpAddAceToAcl (Inherit) ",
|
|
pSid,
|
|
pAce->Mask,
|
|
ACCESS_ALLOWED_ACE_TYPE,
|
|
pAce->Header.AceFlags);
|
|
}
|
|
|
|
return ((PACCESS_ALLOWED_ACE)((PBYTE)pAce + pAce->Header.AceSize));
|
|
}
|
|
|
|
|
|
#if DBG
|
|
|
|
/*** AfpDumpSid
|
|
*
|
|
*/
|
|
VOID
|
|
AfpDumpSid(
|
|
IN PBYTE pString,
|
|
IN PISID pSid
|
|
)
|
|
{
|
|
WCHAR Buffer[128];
|
|
UNICODE_STRING SidStr;
|
|
|
|
PAGED_CODE( );
|
|
|
|
AfpSetEmptyUnicodeString(&SidStr, sizeof(Buffer), Buffer);
|
|
if ((AfpDebugComponent & DBG_COMP_SECURITY) && (DBG_LEVEL_INFO >= AfpDebugLevel))
|
|
{
|
|
RtlConvertSidToUnicodeString(&SidStr, pSid, False);
|
|
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("%s %ws\n", pString, SidStr.Buffer));
|
|
}
|
|
}
|
|
|
|
/*** AfpDumpSidnMask
|
|
*
|
|
*/
|
|
VOID
|
|
AfpDumpSidnMask(
|
|
IN PBYTE pString,
|
|
IN PISID pSid,
|
|
IN DWORD Mask,
|
|
IN UCHAR Type,
|
|
IN UCHAR Flags
|
|
)
|
|
{
|
|
WCHAR Buffer[128];
|
|
UNICODE_STRING SidStr;
|
|
|
|
PAGED_CODE( );
|
|
|
|
AfpSetEmptyUnicodeString(&SidStr, sizeof(Buffer), Buffer);
|
|
if ((AfpDebugComponent & DBG_COMP_SECURITY) && (DBG_LEVEL_INFO >= AfpDebugLevel))
|
|
{
|
|
RtlConvertSidToUnicodeString(&SidStr, pSid, False);
|
|
|
|
DBGPRINT(DBG_COMP_SECURITY, DBG_LEVEL_INFO,
|
|
("%s Sid %ws, Mask %lx, Type %x, Flags %x\n",
|
|
pString, SidStr.Buffer, Mask, Type, Flags));
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|