mirror of
https://github.com/reactos/reactos.git
synced 2025-06-09 11:20:39 +00:00
[SAMSRV]
- Store all fixed size user attributes in the registry value "F". - Initialize fixed size domain attributes in SampCreateUserAccount and SamrCreateUserInDomain. - Implement most information classes of SamrQueryInformationUser. - Disable all informationclasses of SamrSetInformationUser that store fixed size attributes. svn path=/trunk/; revision=56820
This commit is contained in:
parent
470e9cf3bf
commit
2f6a92ccd0
3 changed files with 846 additions and 3 deletions
|
@ -1712,6 +1712,7 @@ SamrCreateUserInDomain(IN SAMPR_HANDLE DomainHandle,
|
||||||
OUT unsigned long *RelativeId)
|
OUT unsigned long *RelativeId)
|
||||||
{
|
{
|
||||||
SAM_DOMAIN_FIXED_DATA FixedDomainData;
|
SAM_DOMAIN_FIXED_DATA FixedDomainData;
|
||||||
|
SAM_USER_FIXED_DATA FixedUserData;
|
||||||
PSAM_DB_OBJECT DomainObject;
|
PSAM_DB_OBJECT DomainObject;
|
||||||
PSAM_DB_OBJECT UserObject;
|
PSAM_DB_OBJECT UserObject;
|
||||||
ULONG ulSize;
|
ULONG ulSize;
|
||||||
|
@ -1809,6 +1810,24 @@ SamrCreateUserInDomain(IN SAMPR_HANDLE DomainHandle,
|
||||||
return Status;
|
return Status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Initialize fixed user data */
|
||||||
|
memset(&FixedUserData, 0, sizeof(SAM_USER_FIXED_DATA));
|
||||||
|
FixedUserData.Version = 1;
|
||||||
|
|
||||||
|
FixedUserData.UserId = ulRid;
|
||||||
|
|
||||||
|
/* Set fixed user data attribute */
|
||||||
|
Status = SampSetObjectAttribute(UserObject,
|
||||||
|
L"F",
|
||||||
|
REG_BINARY,
|
||||||
|
(LPVOID)&FixedUserData,
|
||||||
|
sizeof(SAM_USER_FIXED_DATA));
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
TRACE("failed with status 0x%08lx\n", Status);
|
||||||
|
return Status;
|
||||||
|
}
|
||||||
|
|
||||||
/* Set the name attribute */
|
/* Set the name attribute */
|
||||||
Status = SampSetObjectAttribute(UserObject,
|
Status = SampSetObjectAttribute(UserObject,
|
||||||
L"Name",
|
L"Name",
|
||||||
|
@ -3158,15 +3177,720 @@ SamrDeleteUser(IN OUT SAMPR_HANDLE *UserHandle)
|
||||||
return STATUS_NOT_IMPLEMENTED;
|
return STATUS_NOT_IMPLEMENTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static
|
static
|
||||||
NTSTATUS
|
NTSTATUS
|
||||||
SampQueryUserName(PSAM_DB_OBJECT UserObject,
|
SampQueryUserName(PSAM_DB_OBJECT UserObject,
|
||||||
PSAMPR_USER_INFO_BUFFER *Buffer)
|
PSAMPR_USER_INFO_BUFFER *Buffer)
|
||||||
{
|
{
|
||||||
UNIMPLEMENTED;
|
PSAMPR_USER_INFO_BUFFER InfoBuffer = NULL;
|
||||||
return STATUS_NOT_IMPLEMENTED;
|
ULONG Length = 0;
|
||||||
|
NTSTATUS Status;
|
||||||
|
|
||||||
|
*Buffer = NULL;
|
||||||
|
|
||||||
|
InfoBuffer = midl_user_allocate(sizeof(SAMPR_USER_INFO_BUFFER));
|
||||||
|
if (InfoBuffer == NULL)
|
||||||
|
return STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"Name",
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status) && Status != STATUS_BUFFER_OVERFLOW)
|
||||||
|
{
|
||||||
|
TRACE("Status 0x%08lx\n", Status);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoBuffer->Name.UserName.Length = Length - sizeof(WCHAR);
|
||||||
|
InfoBuffer->Name.UserName.MaximumLength = Length;
|
||||||
|
InfoBuffer->Name.UserName.Buffer = midl_user_allocate(Length);
|
||||||
|
if (InfoBuffer->Name.UserName.Buffer == NULL)
|
||||||
|
{
|
||||||
|
Status = STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
TRACE("Length: %lu\n", Length);
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"Name",
|
||||||
|
NULL,
|
||||||
|
(PVOID)InfoBuffer->Name.UserName.Buffer,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
TRACE("Status 0x%08lx\n", Status);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
Length = 0;
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"FullName",
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status) && Status != STATUS_BUFFER_OVERFLOW)
|
||||||
|
{
|
||||||
|
TRACE("Status 0x%08lx\n", Status);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoBuffer->Name.FullName.Length = Length - sizeof(WCHAR);
|
||||||
|
InfoBuffer->Name.FullName.MaximumLength = Length;
|
||||||
|
InfoBuffer->Name.FullName.Buffer = midl_user_allocate(Length);
|
||||||
|
if (InfoBuffer->Name.FullName.Buffer == NULL)
|
||||||
|
{
|
||||||
|
Status = STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
TRACE("Length: %lu\n", Length);
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"FullName",
|
||||||
|
NULL,
|
||||||
|
(PVOID)InfoBuffer->Name.FullName.Buffer,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
TRACE("Status 0x%08lx\n", Status);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
*Buffer = InfoBuffer;
|
||||||
|
|
||||||
|
done:
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
if (InfoBuffer != NULL)
|
||||||
|
{
|
||||||
|
if (InfoBuffer->Name.UserName.Buffer != NULL)
|
||||||
|
midl_user_free(InfoBuffer->Name.UserName.Buffer);
|
||||||
|
|
||||||
|
if (InfoBuffer->Name.FullName.Buffer != NULL)
|
||||||
|
midl_user_free(InfoBuffer->Name.FullName.Buffer);
|
||||||
|
|
||||||
|
midl_user_free(InfoBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static NTSTATUS
|
||||||
|
SampQueryUserAccountName(PSAM_DB_OBJECT UserObject,
|
||||||
|
PSAMPR_USER_INFO_BUFFER *Buffer)
|
||||||
|
{
|
||||||
|
PSAMPR_USER_INFO_BUFFER InfoBuffer = NULL;
|
||||||
|
ULONG Length = 0;
|
||||||
|
NTSTATUS Status;
|
||||||
|
|
||||||
|
*Buffer = NULL;
|
||||||
|
|
||||||
|
InfoBuffer = midl_user_allocate(sizeof(SAMPR_USER_INFO_BUFFER));
|
||||||
|
if (InfoBuffer == NULL)
|
||||||
|
return STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"Name",
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status) && Status != STATUS_BUFFER_OVERFLOW)
|
||||||
|
{
|
||||||
|
TRACE("Status 0x%08lx\n", Status);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoBuffer->AccountName.UserName.Length = Length - sizeof(WCHAR);
|
||||||
|
InfoBuffer->AccountName.UserName.MaximumLength = Length;
|
||||||
|
InfoBuffer->AccountName.UserName.Buffer = midl_user_allocate(Length);
|
||||||
|
if (InfoBuffer->AccountName.UserName.Buffer == NULL)
|
||||||
|
{
|
||||||
|
Status = STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
TRACE("Length: %lu\n", Length);
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"Name",
|
||||||
|
NULL,
|
||||||
|
(PVOID)InfoBuffer->AccountName.UserName.Buffer,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
TRACE("Status 0x%08lx\n", Status);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
*Buffer = InfoBuffer;
|
||||||
|
|
||||||
|
done:
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
if (InfoBuffer != NULL)
|
||||||
|
{
|
||||||
|
if (InfoBuffer->AccountName.UserName.Buffer != NULL)
|
||||||
|
midl_user_free(InfoBuffer->AccountName.UserName.Buffer);
|
||||||
|
|
||||||
|
midl_user_free(InfoBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static NTSTATUS
|
||||||
|
SampQueryUserFullName(PSAM_DB_OBJECT UserObject,
|
||||||
|
PSAMPR_USER_INFO_BUFFER *Buffer)
|
||||||
|
{
|
||||||
|
PSAMPR_USER_INFO_BUFFER InfoBuffer = NULL;
|
||||||
|
ULONG Length = 0;
|
||||||
|
NTSTATUS Status;
|
||||||
|
|
||||||
|
*Buffer = NULL;
|
||||||
|
|
||||||
|
InfoBuffer = midl_user_allocate(sizeof(SAMPR_USER_INFO_BUFFER));
|
||||||
|
if (InfoBuffer == NULL)
|
||||||
|
return STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"FullName",
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status) && Status != STATUS_BUFFER_OVERFLOW)
|
||||||
|
{
|
||||||
|
TRACE("Status 0x%08lx\n", Status);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoBuffer->FullName.FullName.Length = Length - sizeof(WCHAR);
|
||||||
|
InfoBuffer->FullName.FullName.MaximumLength = Length;
|
||||||
|
InfoBuffer->FullName.FullName.Buffer = midl_user_allocate(Length);
|
||||||
|
if (InfoBuffer->FullName.FullName.Buffer == NULL)
|
||||||
|
{
|
||||||
|
Status = STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
TRACE("Length: %lu\n", Length);
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"FullName",
|
||||||
|
NULL,
|
||||||
|
(PVOID)InfoBuffer->FullName.FullName.Buffer,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
TRACE("Status 0x%08lx\n", Status);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
*Buffer = InfoBuffer;
|
||||||
|
|
||||||
|
done:
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
if (InfoBuffer != NULL)
|
||||||
|
{
|
||||||
|
if (InfoBuffer->FullName.FullName.Buffer != NULL)
|
||||||
|
midl_user_free(InfoBuffer->FullName.FullName.Buffer);
|
||||||
|
|
||||||
|
midl_user_free(InfoBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static
|
||||||
|
NTSTATUS
|
||||||
|
SampQueryUserPrimaryGroup(PSAM_DB_OBJECT UserObject,
|
||||||
|
PSAMPR_USER_INFO_BUFFER *Buffer)
|
||||||
|
{
|
||||||
|
PSAMPR_USER_INFO_BUFFER InfoBuffer = NULL;
|
||||||
|
SAM_USER_FIXED_DATA FixedData;
|
||||||
|
ULONG Length = 0;
|
||||||
|
NTSTATUS Status;
|
||||||
|
|
||||||
|
*Buffer = NULL;
|
||||||
|
|
||||||
|
InfoBuffer = midl_user_allocate(sizeof(SAMPR_USER_INFO_BUFFER));
|
||||||
|
if (InfoBuffer == NULL)
|
||||||
|
return STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
|
||||||
|
Length = sizeof(SAM_USER_FIXED_DATA);
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"F",
|
||||||
|
NULL,
|
||||||
|
(PVOID)&FixedData,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
goto done;
|
||||||
|
|
||||||
|
InfoBuffer->PrimaryGroup.PrimaryGroupId = FixedData.PrimaryGroupId;
|
||||||
|
|
||||||
|
*Buffer = InfoBuffer;
|
||||||
|
|
||||||
|
done:
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
if (InfoBuffer != NULL)
|
||||||
|
{
|
||||||
|
midl_user_free(InfoBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static NTSTATUS
|
||||||
|
SampQueryUserHome(PSAM_DB_OBJECT UserObject,
|
||||||
|
PSAMPR_USER_INFO_BUFFER *Buffer)
|
||||||
|
{
|
||||||
|
PSAMPR_USER_INFO_BUFFER InfoBuffer = NULL;
|
||||||
|
ULONG Length = 0;
|
||||||
|
NTSTATUS Status;
|
||||||
|
|
||||||
|
*Buffer = NULL;
|
||||||
|
|
||||||
|
InfoBuffer = midl_user_allocate(sizeof(SAMPR_USER_INFO_BUFFER));
|
||||||
|
if (InfoBuffer == NULL)
|
||||||
|
return STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"HomeDirectory",
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status) && Status != STATUS_BUFFER_OVERFLOW)
|
||||||
|
{
|
||||||
|
TRACE("Status 0x%08lx\n", Status);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoBuffer->Home.HomeDirectory.Length = Length - sizeof(WCHAR);
|
||||||
|
InfoBuffer->Home.HomeDirectory.MaximumLength = Length;
|
||||||
|
InfoBuffer->Home.HomeDirectory.Buffer = midl_user_allocate(Length);
|
||||||
|
if (InfoBuffer->Home.HomeDirectory.Buffer == NULL)
|
||||||
|
{
|
||||||
|
Status = STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
TRACE("Length: %lu\n", Length);
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"HomeDirectory",
|
||||||
|
NULL,
|
||||||
|
(PVOID)InfoBuffer->Home.HomeDirectory.Buffer,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
TRACE("Status 0x%08lx\n", Status);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
Length = 0;
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"HomeDirectoryDrive",
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status) && Status != STATUS_BUFFER_OVERFLOW)
|
||||||
|
{
|
||||||
|
TRACE("Status 0x%08lx\n", Status);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoBuffer->Home.HomeDirectoryDrive.Length = Length - sizeof(WCHAR);
|
||||||
|
InfoBuffer->Home.HomeDirectoryDrive.MaximumLength = Length;
|
||||||
|
InfoBuffer->Home.HomeDirectoryDrive.Buffer = midl_user_allocate(Length);
|
||||||
|
if (InfoBuffer->Home.HomeDirectoryDrive.Buffer == NULL)
|
||||||
|
{
|
||||||
|
Status = STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
TRACE("Length: %lu\n", Length);
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"HomeDirectoryDrive",
|
||||||
|
NULL,
|
||||||
|
(PVOID)InfoBuffer->Home.HomeDirectoryDrive.Buffer,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
TRACE("Status 0x%08lx\n", Status);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
*Buffer = InfoBuffer;
|
||||||
|
|
||||||
|
done:
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
if (InfoBuffer != NULL)
|
||||||
|
{
|
||||||
|
if (InfoBuffer->Home.HomeDirectory.Buffer != NULL)
|
||||||
|
midl_user_free(InfoBuffer->Home.HomeDirectory.Buffer);
|
||||||
|
|
||||||
|
if (InfoBuffer->Home.HomeDirectoryDrive.Buffer != NULL)
|
||||||
|
midl_user_free(InfoBuffer->Home.HomeDirectoryDrive.Buffer);
|
||||||
|
|
||||||
|
midl_user_free(InfoBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static NTSTATUS
|
||||||
|
SampQueryUserScript(PSAM_DB_OBJECT UserObject,
|
||||||
|
PSAMPR_USER_INFO_BUFFER *Buffer)
|
||||||
|
{
|
||||||
|
PSAMPR_USER_INFO_BUFFER InfoBuffer = NULL;
|
||||||
|
ULONG Length = 0;
|
||||||
|
NTSTATUS Status;
|
||||||
|
|
||||||
|
*Buffer = NULL;
|
||||||
|
|
||||||
|
InfoBuffer = midl_user_allocate(sizeof(SAMPR_USER_INFO_BUFFER));
|
||||||
|
if (InfoBuffer == NULL)
|
||||||
|
return STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"ScriptPath",
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status) && Status != STATUS_BUFFER_OVERFLOW)
|
||||||
|
{
|
||||||
|
TRACE("Status 0x%08lx\n", Status);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoBuffer->Script.ScriptPath.Length = Length - sizeof(WCHAR);
|
||||||
|
InfoBuffer->Script.ScriptPath.MaximumLength = Length;
|
||||||
|
InfoBuffer->Script.ScriptPath.Buffer = midl_user_allocate(Length);
|
||||||
|
if (InfoBuffer->Script.ScriptPath.Buffer == NULL)
|
||||||
|
{
|
||||||
|
Status = STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
TRACE("Length: %lu\n", Length);
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"ScriptPath",
|
||||||
|
NULL,
|
||||||
|
(PVOID)InfoBuffer->Script.ScriptPath.Buffer,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
TRACE("Status 0x%08lx\n", Status);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
*Buffer = InfoBuffer;
|
||||||
|
|
||||||
|
done:
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
if (InfoBuffer != NULL)
|
||||||
|
{
|
||||||
|
if (InfoBuffer->Script.ScriptPath.Buffer != NULL)
|
||||||
|
midl_user_free(InfoBuffer->Script.ScriptPath.Buffer);
|
||||||
|
|
||||||
|
midl_user_free(InfoBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static NTSTATUS
|
||||||
|
SampQueryUserProfile(PSAM_DB_OBJECT UserObject,
|
||||||
|
PSAMPR_USER_INFO_BUFFER *Buffer)
|
||||||
|
{
|
||||||
|
PSAMPR_USER_INFO_BUFFER InfoBuffer = NULL;
|
||||||
|
ULONG Length = 0;
|
||||||
|
NTSTATUS Status;
|
||||||
|
|
||||||
|
*Buffer = NULL;
|
||||||
|
|
||||||
|
InfoBuffer = midl_user_allocate(sizeof(SAMPR_USER_INFO_BUFFER));
|
||||||
|
if (InfoBuffer == NULL)
|
||||||
|
return STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"ProfilePath",
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status) && Status != STATUS_BUFFER_OVERFLOW)
|
||||||
|
{
|
||||||
|
TRACE("Status 0x%08lx\n", Status);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoBuffer->Profile.ProfilePath.Length = Length - sizeof(WCHAR);
|
||||||
|
InfoBuffer->Profile.ProfilePath.MaximumLength = Length;
|
||||||
|
InfoBuffer->Profile.ProfilePath.Buffer = midl_user_allocate(Length);
|
||||||
|
if (InfoBuffer->Profile.ProfilePath.Buffer == NULL)
|
||||||
|
{
|
||||||
|
Status = STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
TRACE("Length: %lu\n", Length);
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"ProfilePath",
|
||||||
|
NULL,
|
||||||
|
(PVOID)InfoBuffer->Profile.ProfilePath.Buffer,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
TRACE("Status 0x%08lx\n", Status);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
*Buffer = InfoBuffer;
|
||||||
|
|
||||||
|
done:
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
if (InfoBuffer != NULL)
|
||||||
|
{
|
||||||
|
if (InfoBuffer->Profile.ProfilePath.Buffer != NULL)
|
||||||
|
midl_user_free(InfoBuffer->Profile.ProfilePath.Buffer);
|
||||||
|
|
||||||
|
midl_user_free(InfoBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static NTSTATUS
|
||||||
|
SampQueryUserAdminComment(PSAM_DB_OBJECT UserObject,
|
||||||
|
PSAMPR_USER_INFO_BUFFER *Buffer)
|
||||||
|
{
|
||||||
|
PSAMPR_USER_INFO_BUFFER InfoBuffer = NULL;
|
||||||
|
ULONG Length = 0;
|
||||||
|
NTSTATUS Status;
|
||||||
|
|
||||||
|
*Buffer = NULL;
|
||||||
|
|
||||||
|
InfoBuffer = midl_user_allocate(sizeof(SAMPR_USER_INFO_BUFFER));
|
||||||
|
if (InfoBuffer == NULL)
|
||||||
|
return STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"AdminComment",
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status) && Status != STATUS_BUFFER_OVERFLOW)
|
||||||
|
{
|
||||||
|
TRACE("Status 0x%08lx\n", Status);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoBuffer->AdminComment.AdminComment.Length = Length - sizeof(WCHAR);
|
||||||
|
InfoBuffer->AdminComment.AdminComment.MaximumLength = Length;
|
||||||
|
InfoBuffer->AdminComment.AdminComment.Buffer = midl_user_allocate(Length);
|
||||||
|
if (InfoBuffer->AdminComment.AdminComment.Buffer == NULL)
|
||||||
|
{
|
||||||
|
Status = STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
TRACE("Length: %lu\n", Length);
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"AdminComment",
|
||||||
|
NULL,
|
||||||
|
(PVOID)InfoBuffer->AdminComment.AdminComment.Buffer,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
TRACE("Status 0x%08lx\n", Status);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
*Buffer = InfoBuffer;
|
||||||
|
|
||||||
|
done:
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
if (InfoBuffer != NULL)
|
||||||
|
{
|
||||||
|
if (InfoBuffer->AdminComment.AdminComment.Buffer != NULL)
|
||||||
|
midl_user_free(InfoBuffer->AdminComment.AdminComment.Buffer);
|
||||||
|
|
||||||
|
midl_user_free(InfoBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
static NTSTATUS
|
||||||
|
SampQueryUserWorkStations(PSAM_DB_OBJECT UserObject,
|
||||||
|
PSAMPR_USER_INFO_BUFFER *Buffer)
|
||||||
|
{
|
||||||
|
PSAMPR_USER_INFO_BUFFER InfoBuffer = NULL;
|
||||||
|
ULONG Length = 0;
|
||||||
|
NTSTATUS Status;
|
||||||
|
|
||||||
|
*Buffer = NULL;
|
||||||
|
|
||||||
|
InfoBuffer = midl_user_allocate(sizeof(SAMPR_USER_INFO_BUFFER));
|
||||||
|
if (InfoBuffer == NULL)
|
||||||
|
return STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"WorkStations",
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status) && Status != STATUS_BUFFER_OVERFLOW)
|
||||||
|
{
|
||||||
|
TRACE("Status 0x%08lx\n", Status);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoBuffer->WorkStations.WorkStations.Length = Length - sizeof(WCHAR);
|
||||||
|
InfoBuffer->WorkStations.WorkStations.MaximumLength = Length;
|
||||||
|
InfoBuffer->WorkStations.WorkStations.Buffer = midl_user_allocate(Length);
|
||||||
|
if (InfoBuffer->WorkStations.WorkStations.Buffer == NULL)
|
||||||
|
{
|
||||||
|
Status = STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
TRACE("Length: %lu\n", Length);
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"WorkStations",
|
||||||
|
NULL,
|
||||||
|
(PVOID)InfoBuffer->WorkStations.WorkStations.Buffer,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
TRACE("Status 0x%08lx\n", Status);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
*Buffer = InfoBuffer;
|
||||||
|
|
||||||
|
done:
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
if (InfoBuffer != NULL)
|
||||||
|
{
|
||||||
|
if (InfoBuffer->WorkStations.WorkStations.Buffer != NULL)
|
||||||
|
midl_user_free(InfoBuffer->WorkStations.WorkStations.Buffer);
|
||||||
|
|
||||||
|
midl_user_free(InfoBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static
|
||||||
|
NTSTATUS
|
||||||
|
SampQueryUserControl(PSAM_DB_OBJECT UserObject,
|
||||||
|
PSAMPR_USER_INFO_BUFFER *Buffer)
|
||||||
|
{
|
||||||
|
PSAMPR_USER_INFO_BUFFER InfoBuffer = NULL;
|
||||||
|
SAM_USER_FIXED_DATA FixedData;
|
||||||
|
ULONG Length = 0;
|
||||||
|
NTSTATUS Status;
|
||||||
|
|
||||||
|
*Buffer = NULL;
|
||||||
|
|
||||||
|
InfoBuffer = midl_user_allocate(sizeof(SAMPR_USER_INFO_BUFFER));
|
||||||
|
if (InfoBuffer == NULL)
|
||||||
|
return STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
|
||||||
|
Length = sizeof(SAM_USER_FIXED_DATA);
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"F",
|
||||||
|
NULL,
|
||||||
|
(PVOID)&FixedData,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
goto done;
|
||||||
|
|
||||||
|
InfoBuffer->Control.UserAccountControl = FixedData.UserAccountControl;
|
||||||
|
|
||||||
|
*Buffer = InfoBuffer;
|
||||||
|
|
||||||
|
done:
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
if (InfoBuffer != NULL)
|
||||||
|
{
|
||||||
|
midl_user_free(InfoBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static
|
||||||
|
NTSTATUS
|
||||||
|
SampQueryUserExpires(PSAM_DB_OBJECT UserObject,
|
||||||
|
PSAMPR_USER_INFO_BUFFER *Buffer)
|
||||||
|
{
|
||||||
|
PSAMPR_USER_INFO_BUFFER InfoBuffer = NULL;
|
||||||
|
SAM_USER_FIXED_DATA FixedData;
|
||||||
|
ULONG Length = 0;
|
||||||
|
NTSTATUS Status;
|
||||||
|
|
||||||
|
*Buffer = NULL;
|
||||||
|
|
||||||
|
InfoBuffer = midl_user_allocate(sizeof(SAMPR_USER_INFO_BUFFER));
|
||||||
|
if (InfoBuffer == NULL)
|
||||||
|
return STATUS_INSUFFICIENT_RESOURCES;
|
||||||
|
|
||||||
|
Length = sizeof(SAM_USER_FIXED_DATA);
|
||||||
|
Status = SampGetObjectAttribute(UserObject,
|
||||||
|
L"F",
|
||||||
|
NULL,
|
||||||
|
(PVOID)&FixedData,
|
||||||
|
&Length);
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
goto done;
|
||||||
|
|
||||||
|
InfoBuffer->Expires.AccountExpires.LowPart = FixedData.AccountExpires.LowPart;
|
||||||
|
InfoBuffer->Expires.AccountExpires.HighPart = FixedData.AccountExpires.HighPart;
|
||||||
|
|
||||||
|
*Buffer = InfoBuffer;
|
||||||
|
|
||||||
|
done:
|
||||||
|
if (!NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
if (InfoBuffer != NULL)
|
||||||
|
{
|
||||||
|
midl_user_free(InfoBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Function 36 */
|
/* Function 36 */
|
||||||
NTSTATUS
|
NTSTATUS
|
||||||
NTAPI
|
NTAPI
|
||||||
|
@ -3192,6 +3916,31 @@ SamrQueryInformationUser(IN SAMPR_HANDLE UserHandle,
|
||||||
DesiredAccess = USER_READ_GENERAL;
|
DesiredAccess = USER_READ_GENERAL;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case UserLogonHoursInformation:
|
||||||
|
case UserHomeInformation:
|
||||||
|
case UserScriptInformation:
|
||||||
|
case UserProfileInformation:
|
||||||
|
case UserWorkStationsInformation:
|
||||||
|
DesiredAccess = USER_READ_LOGON;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UserControlInformation:
|
||||||
|
case UserExpiresInformation:
|
||||||
|
DesiredAccess = USER_READ_ACCOUNT;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UserPreferencesInformation:
|
||||||
|
DesiredAccess = USER_READ_GENERAL |
|
||||||
|
USER_READ_PREFERENCES;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UserLogonInformation:
|
||||||
|
case UserAccountInformation:
|
||||||
|
DesiredAccess = USER_READ_GENERAL |
|
||||||
|
USER_READ_PREFERENCES |
|
||||||
|
USER_READ_LOGON |
|
||||||
|
USER_READ_ACCOUNT;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return STATUS_INVALID_INFO_CLASS;
|
return STATUS_INVALID_INFO_CLASS;
|
||||||
|
@ -3210,11 +3959,66 @@ SamrQueryInformationUser(IN SAMPR_HANDLE UserHandle,
|
||||||
|
|
||||||
switch (UserInformationClass)
|
switch (UserInformationClass)
|
||||||
{
|
{
|
||||||
|
// case UserGeneralInformation:
|
||||||
|
// case UserPreferencesInformation:
|
||||||
|
// case UserLogonInformation:
|
||||||
|
// case UserLogonHoursInformation:
|
||||||
|
// case UserAccountInformation:
|
||||||
|
|
||||||
case UserNameInformation:
|
case UserNameInformation:
|
||||||
Status = SampQueryUserName(UserObject,
|
Status = SampQueryUserName(UserObject,
|
||||||
Buffer);
|
Buffer);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case UserAccountNameInformation:
|
||||||
|
Status = SampQueryUserAccountName(UserObject,
|
||||||
|
Buffer);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UserFullNameInformation:
|
||||||
|
Status = SampQueryUserFullName(UserObject,
|
||||||
|
Buffer);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UserPrimaryGroupInformation:
|
||||||
|
Status = SampQueryUserPrimaryGroup(UserObject,
|
||||||
|
Buffer);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UserHomeInformation:
|
||||||
|
Status = SampQueryUserHome(UserObject,
|
||||||
|
Buffer);
|
||||||
|
|
||||||
|
case UserScriptInformation:
|
||||||
|
Status = SampQueryUserScript(UserObject,
|
||||||
|
Buffer);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UserProfileInformation:
|
||||||
|
Status = SampQueryUserProfile(UserObject,
|
||||||
|
Buffer);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UserAdminCommentInformation:
|
||||||
|
Status = SampQueryUserAdminComment(UserObject,
|
||||||
|
Buffer);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UserWorkStationsInformation:
|
||||||
|
Status = SampQueryUserWorkStations(UserObject,
|
||||||
|
Buffer);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UserControlInformation:
|
||||||
|
Status = SampQueryUserControl(UserObject,
|
||||||
|
Buffer);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UserExpiresInformation:
|
||||||
|
Status = SampQueryUserExpires(UserObject,
|
||||||
|
Buffer);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
Status = STATUS_INVALID_INFO_CLASS;
|
Status = STATUS_INVALID_INFO_CLASS;
|
||||||
}
|
}
|
||||||
|
@ -3309,6 +4113,7 @@ SamrSetInformationUser(IN SAMPR_HANDLE UserHandle,
|
||||||
Buffer->FullName.FullName.MaximumLength);
|
Buffer->FullName.FullName.MaximumLength);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
/*
|
||||||
case UserPrimaryGroupInformation:
|
case UserPrimaryGroupInformation:
|
||||||
Status = SampSetObjectAttribute(UserObject,
|
Status = SampSetObjectAttribute(UserObject,
|
||||||
L"PrimaryGroupId",
|
L"PrimaryGroupId",
|
||||||
|
@ -3316,6 +4121,7 @@ SamrSetInformationUser(IN SAMPR_HANDLE UserHandle,
|
||||||
&Buffer->PrimaryGroup.PrimaryGroupId,
|
&Buffer->PrimaryGroup.PrimaryGroupId,
|
||||||
sizeof(ULONG));
|
sizeof(ULONG));
|
||||||
break;
|
break;
|
||||||
|
*/
|
||||||
|
|
||||||
case UserHomeInformation:
|
case UserHomeInformation:
|
||||||
Status = SampSetObjectAttribute(UserObject,
|
Status = SampSetObjectAttribute(UserObject,
|
||||||
|
@ -3376,6 +4182,7 @@ SamrSetInformationUser(IN SAMPR_HANDLE UserHandle,
|
||||||
Buffer->SetPassword.Password.MaximumLength);
|
Buffer->SetPassword.Password.MaximumLength);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
/*
|
||||||
case UserControlInformation:
|
case UserControlInformation:
|
||||||
Status = SampSetObjectAttribute(UserObject,
|
Status = SampSetObjectAttribute(UserObject,
|
||||||
L"UserAccountControl",
|
L"UserAccountControl",
|
||||||
|
@ -3383,7 +4190,8 @@ SamrSetInformationUser(IN SAMPR_HANDLE UserHandle,
|
||||||
&Buffer->Control.UserAccountControl,
|
&Buffer->Control.UserAccountControl,
|
||||||
sizeof(ULONG));
|
sizeof(ULONG));
|
||||||
break;
|
break;
|
||||||
|
*/
|
||||||
|
/*
|
||||||
case UserExpiresInformation:
|
case UserExpiresInformation:
|
||||||
Status = SampSetObjectAttribute(UserObject,
|
Status = SampSetObjectAttribute(UserObject,
|
||||||
L"AccountExpires",
|
L"AccountExpires",
|
||||||
|
@ -3391,6 +4199,7 @@ SamrSetInformationUser(IN SAMPR_HANDLE UserHandle,
|
||||||
&Buffer->Expires.AccountExpires,
|
&Buffer->Expires.AccountExpires,
|
||||||
sizeof(OLD_LARGE_INTEGER));
|
sizeof(OLD_LARGE_INTEGER));
|
||||||
break;
|
break;
|
||||||
|
*/
|
||||||
|
|
||||||
// case UserInternal1Information:
|
// case UserInternal1Information:
|
||||||
// case UserParametersInformation:
|
// case UserParametersInformation:
|
||||||
|
|
|
@ -73,6 +73,26 @@ typedef struct _SAM_DOMAIN_FIXED_DATA
|
||||||
BOOLEAN UasCompatibilityRequired;
|
BOOLEAN UasCompatibilityRequired;
|
||||||
} SAM_DOMAIN_FIXED_DATA, *PSAM_DOMAIN_FIXED_DATA;
|
} SAM_DOMAIN_FIXED_DATA, *PSAM_DOMAIN_FIXED_DATA;
|
||||||
|
|
||||||
|
typedef struct _SAM_USER_FIXED_DATA
|
||||||
|
{
|
||||||
|
ULONG Version;
|
||||||
|
ULONG Reserved;
|
||||||
|
LARGE_INTEGER LastLogon;
|
||||||
|
LARGE_INTEGER LastLogoff;
|
||||||
|
LARGE_INTEGER PasswordLastSet;
|
||||||
|
LARGE_INTEGER AccountExpires;
|
||||||
|
LARGE_INTEGER LastBadPasswordTime;
|
||||||
|
ULONG UserId;
|
||||||
|
ULONG PrimaryGroupId;
|
||||||
|
ULONG UserAccountControl;
|
||||||
|
USHORT CountryCode;
|
||||||
|
USHORT CodePage;
|
||||||
|
USHORT BadPasswordCount;
|
||||||
|
USHORT LogonCount;
|
||||||
|
USHORT AdminCount;
|
||||||
|
USHORT OperatorCount;
|
||||||
|
} SAM_USER_FIXED_DATA, *PSAM_USER_FIXED_DATA;
|
||||||
|
|
||||||
/* database.c */
|
/* database.c */
|
||||||
|
|
||||||
NTSTATUS
|
NTSTATUS
|
||||||
|
|
|
@ -223,11 +223,18 @@ SampCreateUserAccount(HKEY hDomainKey,
|
||||||
LPCWSTR lpAccountName,
|
LPCWSTR lpAccountName,
|
||||||
ULONG ulRelativeId)
|
ULONG ulRelativeId)
|
||||||
{
|
{
|
||||||
|
SAM_USER_FIXED_DATA FixedUserData;
|
||||||
DWORD dwDisposition;
|
DWORD dwDisposition;
|
||||||
WCHAR szAccountKeyName[32];
|
WCHAR szAccountKeyName[32];
|
||||||
HKEY hAccountKey = NULL;
|
HKEY hAccountKey = NULL;
|
||||||
HKEY hNamesKey = NULL;
|
HKEY hNamesKey = NULL;
|
||||||
|
|
||||||
|
/* Initialize fixed user data */
|
||||||
|
memset(&FixedUserData, 0, sizeof(SAM_USER_FIXED_DATA));
|
||||||
|
FixedUserData.Version = 1;
|
||||||
|
|
||||||
|
FixedUserData.UserId = ulRelativeId;
|
||||||
|
|
||||||
swprintf(szAccountKeyName, L"Users\\%08lX", ulRelativeId);
|
swprintf(szAccountKeyName, L"Users\\%08lX", ulRelativeId);
|
||||||
|
|
||||||
if (!RegCreateKeyExW(hDomainKey,
|
if (!RegCreateKeyExW(hDomainKey,
|
||||||
|
@ -240,6 +247,13 @@ SampCreateUserAccount(HKEY hDomainKey,
|
||||||
&hAccountKey,
|
&hAccountKey,
|
||||||
&dwDisposition))
|
&dwDisposition))
|
||||||
{
|
{
|
||||||
|
RegSetValueEx(hAccountKey,
|
||||||
|
L"F",
|
||||||
|
0,
|
||||||
|
REG_BINARY,
|
||||||
|
(LPVOID)&FixedUserData,
|
||||||
|
sizeof(SAM_USER_FIXED_DATA));
|
||||||
|
|
||||||
RegSetValueEx(hAccountKey,
|
RegSetValueEx(hAccountKey,
|
||||||
L"Name",
|
L"Name",
|
||||||
0,
|
0,
|
||||||
|
|
Loading…
Reference in a new issue