2025-12-25 upload

This commit is contained in:
“shengyudong”
2025-12-25 11:16:59 +08:00
commit 322ac74336
2241 changed files with 639966 additions and 0 deletions

View File

@@ -0,0 +1,436 @@
"""
"""
# Created on 2014.10.28
#
# Author: Giovanni Cannata
#
# Copyright 2014 - 2020 Giovanni Cannata
#
# This file is part of ldap3.
#
# ldap3 is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ldap3 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ldap3 in the COPYING and COPYING.LESSER files.
# If not, see <http://www.gnu.org/licenses/>.
import re
from binascii import hexlify
from uuid import UUID
from datetime import datetime, timedelta
from ...utils.conv import to_unicode
from ...core.timezone import OffsetTzInfo
def format_unicode(raw_value):
try:
if str is not bytes: # Python 3
return str(raw_value, 'utf-8', errors='strict')
else: # Python 2
return unicode(raw_value, 'utf-8', errors='strict')
except (TypeError, UnicodeDecodeError):
pass
return raw_value
def format_integer(raw_value):
try:
return int(raw_value)
except (TypeError, ValueError): # expected exceptions
pass
except Exception: # any other exception should be investigated, anyway the formatter return the raw_value
pass
return raw_value
def format_binary(raw_value):
try:
return bytes(raw_value)
except TypeError: # expected exceptions
pass
except Exception: # any other exception should be investigated, anyway the formatter return the raw_value
pass
return raw_value
def format_uuid(raw_value):
try:
return str(UUID(bytes=raw_value))
except (TypeError, ValueError):
return format_unicode(raw_value)
except Exception: # any other exception should be investigated, anyway the formatter return the raw_value
pass
return raw_value
def format_uuid_le(raw_value):
try:
return '{' + str(UUID(bytes_le=raw_value)) + '}'
except (TypeError, ValueError):
return format_unicode(raw_value)
except Exception: # any other exception should be investigated, anyway the formatter return the raw_value
pass
return raw_value
def format_boolean(raw_value):
if raw_value in [b'TRUE', b'true', b'True']:
return True
if raw_value in [b'FALSE', b'false', b'False']:
return False
return raw_value
def format_ad_timestamp(raw_value):
"""
Active Directory stores date/time values as the number of 100-nanosecond intervals
that have elapsed since the 0 hour on January 1, 1601 till the date/time that is being stored.
The time is always stored in Greenwich Mean Time (GMT) in the Active Directory.
"""
utc_timezone = OffsetTzInfo(0, 'UTC')
if raw_value == b'9223372036854775807': # max value to be stored in a 64 bit signed int
return datetime.max.replace(tzinfo=utc_timezone) # returns datetime.datetime(9999, 12, 31, 23, 59, 59, 999999, tzinfo=OffsetTzInfo(offset=0, name='UTC'))
try:
timestamp = int(raw_value)
if timestamp < 0: # ad timestamp cannot be negative
timestamp = timestamp * -1
except Exception:
return raw_value
try:
return datetime.fromtimestamp(timestamp / 10000000.0 - 11644473600,
tz=utc_timezone) # forces true division in python 2
except (OSError, OverflowError, ValueError): # on Windows backwards timestamps are not allowed
try:
unix_epoch = datetime.fromtimestamp(0, tz=utc_timezone)
diff_seconds = timedelta(seconds=timestamp / 10000000.0 - 11644473600)
return unix_epoch + diff_seconds
except Exception:
pass
except Exception:
pass
return raw_value
try: # uses regular expressions and the timezone class (python3.2 and later)
from datetime import timezone
time_format = re.compile(
r'''
^
(?P<Year>[0-9]{4})
(?P<Month>0[1-9]|1[0-2])
(?P<Day>0[1-9]|[12][0-9]|3[01])
(?P<Hour>[01][0-9]|2[0-3])
(?:
(?P<Minute>[0-5][0-9])
(?P<Second>[0-5][0-9]|60)?
)?
(?:
[.,]
(?P<Fraction>[0-9]+)
)?
(?:
Z
|
(?:
(?P<Offset>[+-])
(?P<OffHour>[01][0-9]|2[0-3])
(?P<OffMinute>[0-5][0-9])?
)
)
$
''',
re.VERBOSE
)
def format_time(raw_value):
try:
match = time_format.fullmatch(to_unicode(raw_value))
if match is None:
return raw_value
matches = match.groupdict()
offset = timedelta(
hours=int(matches['OffHour'] or 0),
minutes=int(matches['OffMinute'] or 0)
)
if matches['Offset'] == '-':
offset *= -1
# Python does not support leap second in datetime (!)
if matches['Second'] == '60':
matches['Second'] = '59'
# According to RFC, fraction may be applied to an Hour/Minute (!)
fraction = float('0.' + (matches['Fraction'] or '0'))
if matches['Minute'] is None:
fraction *= 60
minute = int(fraction)
fraction -= minute
else:
minute = int(matches['Minute'])
if matches['Second'] is None:
fraction *= 60
second = int(fraction)
fraction -= second
else:
second = int(matches['Second'])
microseconds = int(fraction * 1000000)
return datetime(
int(matches['Year']),
int(matches['Month']),
int(matches['Day']),
int(matches['Hour']),
minute,
second,
microseconds,
timezone(offset),
)
except Exception: # exceptions should be investigated, anyway the formatter return the raw_value
pass
return raw_value
except ImportError:
def format_time(raw_value):
"""
From RFC4517:
A value of the Generalized Time syntax is a character string
representing a date and time. The LDAP-specific encoding of a value
of this syntax is a restriction of the format defined in [ISO8601],
and is described by the following ABNF:
GeneralizedTime = century year month day hour
[ minute [ second / leap-second ] ]
[ fraction ]
g-time-zone
century = 2(%x30-39) ; "00" to "99"
year = 2(%x30-39) ; "00" to "99"
month = ( %x30 %x31-39 ) ; "01" (January) to "09"
/ ( %x31 %x30-32 ) ; "10" to "12"
day = ( %x30 %x31-39 ) ; "01" to "09"
/ ( %x31-32 %x30-39 ) ; "10" to "29"
/ ( %x33 %x30-31 ) ; "30" to "31"
hour = ( %x30-31 %x30-39 ) / ( %x32 %x30-33 ) ; "00" to "23"
minute = %x30-35 %x30-39 ; "00" to "59"
second = ( %x30-35 %x30-39 ) ; "00" to "59"
leap-second = ( %x36 %x30 ) ; "60"
fraction = ( DOT / COMMA ) 1*(%x30-39)
g-time-zone = %x5A ; "Z"
/ g-differential
g-differential = ( MINUS / PLUS ) hour [ minute ]
MINUS = %x2D ; minus sign ("-")
"""
if len(raw_value) < 10 or not all((c in b'0123456789+-,.Z' for c in raw_value)) or (
b'Z' in raw_value and not raw_value.endswith(
b'Z')): # first ten characters are mandatory and must be numeric or timezone or fraction
return raw_value
# sets position for fixed values
year = int(raw_value[0: 4])
month = int(raw_value[4: 6])
day = int(raw_value[6: 8])
hour = int(raw_value[8: 10])
minute = 0
second = 0
microsecond = 0
remain = raw_value[10:]
if remain and remain.endswith(b'Z'): # uppercase 'Z'
sep = b'Z'
elif b'+' in remain: # timezone can be specified with +hh[mm] or -hh[mm]
sep = b'+'
elif b'-' in remain:
sep = b'-'
else: # timezone not specified
return raw_value
time, _, offset = remain.partition(sep)
if time and (b'.' in time or b',' in time):
# fraction time
if time[0] in b',.':
minute = 6 * int(time[1] if str is bytes else chr(time[1])) # Python 2 / Python 3
elif time[2] in b',.':
minute = int(raw_value[10: 12])
second = 6 * int(time[3] if str is bytes else chr(time[3])) # Python 2 / Python 3
elif time[4] in b',.':
minute = int(raw_value[10: 12])
second = int(raw_value[12: 14])
microsecond = 100000 * int(time[5] if str is bytes else chr(time[5])) # Python 2 / Python 3
elif len(time) == 2: # mmZ format
minute = int(raw_value[10: 12])
elif len(time) == 0: # Z format
pass
elif len(time) == 4: # mmssZ
minute = int(raw_value[10: 12])
second = int(raw_value[12: 14])
else:
return raw_value
if sep == b'Z': # UTC
timezone = OffsetTzInfo(0, 'UTC')
else: # build timezone
try:
if len(offset) == 2:
timezone_hour = int(offset[:2])
timezone_minute = 0
elif len(offset) == 4:
timezone_hour = int(offset[:2])
timezone_minute = int(offset[2:4])
else: # malformed timezone
raise ValueError
except ValueError:
return raw_value
if timezone_hour > 23 or timezone_minute > 59: # invalid timezone
return raw_value
if str is not bytes: # Python 3
timezone = OffsetTzInfo((timezone_hour * 60 + timezone_minute) * (1 if sep == b'+' else -1),
'UTC' + str(sep + offset, encoding='utf-8'))
else: # Python 2
timezone = OffsetTzInfo((timezone_hour * 60 + timezone_minute) * (1 if sep == b'+' else -1),
unicode('UTC' + sep + offset, encoding='utf-8'))
try:
return datetime(year=year,
month=month,
day=day,
hour=hour,
minute=minute,
second=second,
microsecond=microsecond,
tzinfo=timezone)
except (TypeError, ValueError):
pass
return raw_value
def format_ad_timedelta(raw_value):
"""
Convert a negative filetime value to a timedelta.
"""
# Active Directory stores attributes like "minPwdAge" as a negative
# "filetime" timestamp, which is the number of 100-nanosecond intervals that
# have elapsed since the 0 hour on January 1, 1601.
#
# Handle the minimum value that can be stored in a 64 bit signed integer.
# See https://docs.microsoft.com/en-us/dotnet/api/system.int64.minvalue
# In attributes like "maxPwdAge", this signifies never.
if raw_value == b'-9223372036854775808':
return timedelta.max
# We can reuse format_ad_timestamp to get a datetime object from the
# timestamp. Afterwards, we can subtract a datetime representing 0 hour on
# January 1, 1601 from the returned datetime to get the timedelta.
return format_ad_timestamp(raw_value) - format_ad_timestamp(0)
def format_time_with_0_year(raw_value):
try:
if raw_value.startswith(b'0000'):
return raw_value
except Exception:
try:
if raw_value.startswith('0000'):
return raw_value
except Exception:
pass
return format_time(raw_value)
def format_sid(raw_value):
"""
SID= "S-1-" IdentifierAuthority 1*SubAuthority
IdentifierAuthority= IdentifierAuthorityDec / IdentifierAuthorityHex
; If the identifier authority is < 2^32, the
; identifier authority is represented as a decimal
; number
; If the identifier authority is >= 2^32,
; the identifier authority is represented in
; hexadecimal
IdentifierAuthorityDec = 1*10DIGIT
; IdentifierAuthorityDec, top level authority of a
; security identifier is represented as a decimal number
IdentifierAuthorityHex = "0x" 12HEXDIG
; IdentifierAuthorityHex, the top-level authority of a
; security identifier is represented as a hexadecimal number
SubAuthority= "-" 1*10DIGIT
; Sub-Authority is always represented as a decimal number
; No leading "0" characters are allowed when IdentifierAuthority
; or SubAuthority is represented as a decimal number
; All hexadecimal digits must be output in string format,
; pre-pended by "0x"
Revision (1 byte): An 8-bit unsigned integer that specifies the revision level of the SID. This value MUST be set to 0x01.
SubAuthorityCount (1 byte): An 8-bit unsigned integer that specifies the number of elements in the SubAuthority array. The maximum number of elements allowed is 15.
IdentifierAuthority (6 bytes): A SID_IDENTIFIER_AUTHORITY structure that indicates the authority under which the SID was created. It describes the entity that created the SID. The Identifier Authority value {0,0,0,0,0,5} denotes SIDs created by the NT SID authority.
SubAuthority (variable): A variable length array of unsigned 32-bit integers that uniquely identifies a principal relative to the IdentifierAuthority. Its length is determined by SubAuthorityCount.
"""
try:
if raw_value.startswith(b'S-1-'):
return raw_value
except Exception:
try:
if raw_value.startswith('S-1-'):
return raw_value
except Exception:
pass
try:
if str is not bytes: # Python 3
revision = int(raw_value[0])
sub_authority_count = int(raw_value[1])
identifier_authority = int.from_bytes(raw_value[2:8], byteorder='big')
if identifier_authority >= 4294967296: # 2 ^ 32
identifier_authority = hex(identifier_authority)
sub_authority = ''
i = 0
while i < sub_authority_count:
sub_authority += '-' + str(
int.from_bytes(raw_value[8 + (i * 4): 12 + (i * 4)], byteorder='little')) # little endian
i += 1
else: # Python 2
revision = int(ord(raw_value[0]))
sub_authority_count = int(ord(raw_value[1]))
identifier_authority = int(hexlify(raw_value[2:8]), 16)
if identifier_authority >= 4294967296: # 2 ^ 32
identifier_authority = hex(identifier_authority)
sub_authority = ''
i = 0
while i < sub_authority_count:
sub_authority += '-' + str(int(hexlify(raw_value[11 + (i * 4): 7 + (i * 4): -1]), 16)) # little endian
i += 1
return 'S-' + str(revision) + '-' + str(identifier_authority) + sub_authority
except Exception: # any exception should be investigated, anyway the formatter return the raw_value
pass
return raw_value

View File

@@ -0,0 +1,238 @@
"""
"""
# Created on 2014.10.28
#
# Author: Giovanni Cannata
#
# Copyright 2014 - 2020 Giovanni Cannata
#
# This file is part of ldap3.
#
# ldap3 is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ldap3 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ldap3 in the COPYING and COPYING.LESSER files.
# If not, see <http://www.gnu.org/licenses/>.
from ... import SEQUENCE_TYPES
from .formatters import format_ad_timestamp, format_binary, format_boolean,\
format_integer, format_sid, format_time, format_unicode, format_uuid, format_uuid_le, format_time_with_0_year,\
format_ad_timedelta
from .validators import validate_integer, validate_time, always_valid,\
validate_generic_single_value, validate_boolean, validate_ad_timestamp, validate_sid,\
validate_uuid_le, validate_uuid, validate_zero_and_minus_one_and_positive_int, validate_guid, validate_time_with_0_year,\
validate_ad_timedelta
# for each syntax can be specified a format function and a input validation function
standard_formatter = {
'1.2.840.113556.1.4.903': (format_binary, None), # Object (DN-binary) - Microsoft
'1.2.840.113556.1.4.904': (format_unicode, None), # Object (DN-string) - Microsoft
'1.2.840.113556.1.4.905': (format_unicode, None), # String (Teletex) - Microsoft
'1.2.840.113556.1.4.906': (format_integer, validate_integer), # Large integer - Microsoft
'1.2.840.113556.1.4.907': (format_binary, None), # String (NT-sec-desc) - Microsoft
'1.2.840.113556.1.4.1221': (format_binary, None), # Object (OR-name) - Microsoft
'1.2.840.113556.1.4.1362': (format_unicode, None), # String (Case) - Microsoft
'1.3.6.1.4.1.1466.115.121.1.1': (format_binary, None), # ACI item [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.2': (format_binary, None), # Access point [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.3': (format_unicode, None), # Attribute type description
'1.3.6.1.4.1.1466.115.121.1.4': (format_binary, None), # Audio [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.5': (format_binary, None), # Binary [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.6': (format_unicode, None), # Bit String
'1.3.6.1.4.1.1466.115.121.1.7': (format_boolean, validate_boolean), # Boolean
'1.3.6.1.4.1.1466.115.121.1.8': (format_binary, None), # Certificate [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.9': (format_binary, None), # Certificate List [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.10': (format_binary, None), # Certificate Pair [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.11': (format_unicode, None), # Country String
'1.3.6.1.4.1.1466.115.121.1.12': (format_unicode, None), # Distinguished name (DN)
'1.3.6.1.4.1.1466.115.121.1.13': (format_binary, None), # Data Quality Syntax [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.14': (format_unicode, None), # Delivery method
'1.3.6.1.4.1.1466.115.121.1.15': (format_unicode, None), # Directory string
'1.3.6.1.4.1.1466.115.121.1.16': (format_unicode, None), # DIT Content Rule Description
'1.3.6.1.4.1.1466.115.121.1.17': (format_unicode, None), # DIT Structure Rule Description
'1.3.6.1.4.1.1466.115.121.1.18': (format_binary, None), # DL Submit Permission [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.19': (format_binary, None), # DSA Quality Syntax [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.20': (format_binary, None), # DSE Type [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.21': (format_binary, None), # Enhanced Guide
'1.3.6.1.4.1.1466.115.121.1.22': (format_unicode, None), # Facsimile Telephone Number
'1.3.6.1.4.1.1466.115.121.1.23': (format_binary, None), # Fax
'1.3.6.1.4.1.1466.115.121.1.24': (format_time, validate_time), # Generalized time
'1.3.6.1.4.1.1466.115.121.1.25': (format_binary, None), # Guide [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.26': (format_unicode, None), # IA5 string
'1.3.6.1.4.1.1466.115.121.1.27': (format_integer, validate_integer), # Integer
'1.3.6.1.4.1.1466.115.121.1.28': (format_binary, None), # JPEG
'1.3.6.1.4.1.1466.115.121.1.29': (format_binary, None), # Master and Shadow Access Points [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.30': (format_unicode, None), # Matching rule description
'1.3.6.1.4.1.1466.115.121.1.31': (format_unicode, None), # Matching rule use description
'1.3.6.1.4.1.1466.115.121.1.32': (format_unicode, None), # Mail Preference [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.33': (format_unicode, None), # MHS OR Address [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.34': (format_unicode, None), # Name and optional UID
'1.3.6.1.4.1.1466.115.121.1.35': (format_unicode, None), # Name form description
'1.3.6.1.4.1.1466.115.121.1.36': (format_unicode, None), # Numeric string
'1.3.6.1.4.1.1466.115.121.1.37': (format_unicode, None), # Object class description
'1.3.6.1.4.1.1466.115.121.1.38': (format_unicode, None), # OID
'1.3.6.1.4.1.1466.115.121.1.39': (format_unicode, None), # Other mailbox
'1.3.6.1.4.1.1466.115.121.1.40': (format_binary, None), # Octet string
'1.3.6.1.4.1.1466.115.121.1.41': (format_unicode, None), # Postal address
'1.3.6.1.4.1.1466.115.121.1.42': (format_binary, None), # Protocol Information [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.43': (format_binary, None), # Presentation Address [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.44': (format_unicode, None), # Printable string
'1.3.6.1.4.1.1466.115.121.1.45': (format_binary, None), # Subtree specification [OBSOLETE
'1.3.6.1.4.1.1466.115.121.1.46': (format_binary, None), # Supplier Information [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.47': (format_binary, None), # Supplier Or Consumer [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.48': (format_binary, None), # Supplier And Consumer [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.49': (format_binary, None), # Supported Algorithm [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.50': (format_unicode, None), # Telephone number
'1.3.6.1.4.1.1466.115.121.1.51': (format_unicode, None), # Teletex terminal identifier
'1.3.6.1.4.1.1466.115.121.1.52': (format_unicode, None), # Teletex number
'1.3.6.1.4.1.1466.115.121.1.53': (format_time, validate_time), # Utc time (deprecated)
'1.3.6.1.4.1.1466.115.121.1.54': (format_unicode, None), # LDAP syntax description
'1.3.6.1.4.1.1466.115.121.1.55': (format_binary, None), # Modify rights [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.56': (format_binary, None), # LDAP Schema Definition [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.57': (format_unicode, None), # LDAP Schema Description [OBSOLETE]
'1.3.6.1.4.1.1466.115.121.1.58': (format_unicode, None), # Substring assertion
'1.3.6.1.1.16.1': (format_uuid, validate_uuid), # UUID
'1.3.6.1.1.16.4': (format_uuid, validate_uuid), # entryUUID (RFC 4530)
'2.16.840.1.113719.1.1.4.1.501': (format_uuid, validate_guid), # GUID (Novell)
'2.16.840.1.113719.1.1.5.1.0': (format_binary, None), # Unknown (Novell)
'2.16.840.1.113719.1.1.5.1.6': (format_unicode, None), # Case Ignore List (Novell)
'2.16.840.1.113719.1.1.5.1.12': (format_binary, None), # Tagged Data (Novell)
'2.16.840.1.113719.1.1.5.1.13': (format_binary, None), # Octet List (Novell)
'2.16.840.1.113719.1.1.5.1.14': (format_unicode, None), # Tagged String (Novell)
'2.16.840.1.113719.1.1.5.1.15': (format_unicode, None), # Tagged Name And String (Novell)
'2.16.840.1.113719.1.1.5.1.16': (format_binary, None), # NDS Replica Pointer (Novell)
'2.16.840.1.113719.1.1.5.1.17': (format_unicode, None), # NDS ACL (Novell)
'2.16.840.1.113719.1.1.5.1.19': (format_time, validate_time), # NDS Timestamp (Novell)
'2.16.840.1.113719.1.1.5.1.22': (format_integer, validate_integer), # Counter (Novell)
'2.16.840.1.113719.1.1.5.1.23': (format_unicode, None), # Tagged Name (Novell)
'2.16.840.1.113719.1.1.5.1.25': (format_unicode, None), # Typed Name (Novell)
'supportedldapversion': (format_integer, None), # supportedLdapVersion (Microsoft)
'octetstring': (format_binary, validate_uuid_le), # octect string (Microsoft)
'1.2.840.113556.1.4.2': (format_uuid_le, validate_uuid_le), # objectGUID (Microsoft)
'1.2.840.113556.1.4.13': (format_ad_timestamp, validate_ad_timestamp), # builtinCreationTime (Microsoft)
'1.2.840.113556.1.4.26': (format_ad_timestamp, validate_ad_timestamp), # creationTime (Microsoft)
'1.2.840.113556.1.4.49': (format_ad_timestamp, validate_ad_timestamp), # badPasswordTime (Microsoft)
'1.2.840.113556.1.4.51': (format_ad_timestamp, validate_ad_timestamp), # lastLogoff (Microsoft)
'1.2.840.113556.1.4.52': (format_ad_timestamp, validate_ad_timestamp), # lastLogon (Microsoft)
'1.2.840.113556.1.4.60': (format_ad_timedelta, validate_ad_timedelta), # lockoutDuration (Microsoft)
'1.2.840.113556.1.4.61': (format_ad_timedelta, validate_ad_timedelta), # lockOutObservationWindow (Microsoft)
'1.2.840.113556.1.4.74': (format_ad_timedelta, validate_ad_timedelta), # maxPwdAge (Microsoft)
'1.2.840.113556.1.4.78': (format_ad_timedelta, validate_ad_timedelta), # minPwdAge (Microsoft)
'1.2.840.113556.1.4.96': (format_ad_timestamp, validate_zero_and_minus_one_and_positive_int), # pwdLastSet (Microsoft, can be set to -1 only)
'1.2.840.113556.1.4.146': (format_sid, validate_sid), # objectSid (Microsoft)
'1.2.840.113556.1.4.159': (format_ad_timestamp, validate_ad_timestamp), # accountExpires (Microsoft)
'1.2.840.113556.1.4.662': (format_ad_timestamp, validate_ad_timestamp), # lockoutTime (Microsoft)
'1.2.840.113556.1.4.1696': (format_ad_timestamp, validate_ad_timestamp), # lastLogonTimestamp (Microsoft)
'1.3.6.1.4.1.42.2.27.8.1.17': (format_time_with_0_year, validate_time_with_0_year) # pwdAccountLockedTime (Novell)
}
def find_attribute_helpers(attr_type, name, custom_formatter):
"""
Tries to format following the OIDs info and format_helper specification.
Search for attribute oid, then attribute name (can be multiple), then attribute syntax
Precedence is:
1. attribute name
2. attribute oid(from schema)
3. attribute names (from oid_info)
4. attribute syntax (from schema)
Custom formatters can be defined in Server object and have precedence over the standard_formatters
If no formatter is found the raw_value is returned as bytes.
Attributes defined as SINGLE_VALUE in schema are returned as a single object, otherwise are returned as a list of object
Formatter functions can return any kind of object
return a tuple (formatter, validator)
"""
formatter = None
if custom_formatter and isinstance(custom_formatter, dict): # if custom formatters are defined they have precedence over the standard formatters
if name in custom_formatter: # search for attribute name, as returned by the search operation
formatter = custom_formatter[name]
if not formatter and attr_type and attr_type.oid in custom_formatter: # search for attribute oid as returned by schema
formatter = custom_formatter[attr_type.oid]
if not formatter and attr_type and attr_type.oid_info:
if isinstance(attr_type.oid_info[2], SEQUENCE_TYPES): # search for multiple names defined in oid_info
for attr_name in attr_type.oid_info[2]:
if attr_name in custom_formatter:
formatter = custom_formatter[attr_name]
break
elif attr_type.oid_info[2] in custom_formatter: # search for name defined in oid_info
formatter = custom_formatter[attr_type.oid_info[2]]
if not formatter and attr_type and attr_type.syntax in custom_formatter: # search for syntax defined in schema
formatter = custom_formatter[attr_type.syntax]
if not formatter and name in standard_formatter: # search for attribute name, as returned by the search operation
formatter = standard_formatter[name]
if not formatter and attr_type and attr_type.oid in standard_formatter: # search for attribute oid as returned by schema
formatter = standard_formatter[attr_type.oid]
if not formatter and attr_type and attr_type.oid_info:
if isinstance(attr_type.oid_info[2], SEQUENCE_TYPES): # search for multiple names defined in oid_info
for attr_name in attr_type.oid_info[2]:
if attr_name in standard_formatter:
formatter = standard_formatter[attr_name]
break
elif attr_type.oid_info[2] in standard_formatter: # search for name defined in oid_info
formatter = standard_formatter[attr_type.oid_info[2]]
if not formatter and attr_type and attr_type.syntax in standard_formatter: # search for syntax defined in schema
formatter = standard_formatter[attr_type.syntax]
if formatter is None:
return None, None
return formatter
def format_attribute_values(schema, name, values, custom_formatter):
if not values: # RFCs states that attributes must always have values, but a flaky server returns empty values too
return []
if not isinstance(values, SEQUENCE_TYPES):
values = [values]
if schema and schema.attribute_types and name in schema.attribute_types:
attr_type = schema.attribute_types[name]
else:
attr_type = None
attribute_helpers = find_attribute_helpers(attr_type, name, custom_formatter)
if not isinstance(attribute_helpers, tuple): # custom formatter
formatter = attribute_helpers
else:
formatter = format_unicode if not attribute_helpers[0] else attribute_helpers[0]
formatted_values = [formatter(raw_value) for raw_value in values] # executes formatter
if formatted_values:
return formatted_values[0] if (attr_type and attr_type.single_value) else formatted_values
else: # RFCs states that attributes must always have values, but AD return empty values in DirSync
return []
def find_attribute_validator(schema, name, custom_validator):
if schema and schema.attribute_types and name in schema.attribute_types:
attr_type = schema.attribute_types[name]
else:
attr_type = None
attribute_helpers = find_attribute_helpers(attr_type, name, custom_validator)
if not isinstance(attribute_helpers, tuple): # custom validator
validator = attribute_helpers
else:
if not attribute_helpers[1]:
if attr_type and attr_type.single_value:
validator = validate_generic_single_value # validate only single value
else:
validator = always_valid # unknown syntax, accepts single and multi value
else:
validator = attribute_helpers[1]
return validator

View File

@@ -0,0 +1,502 @@
"""
"""
# Created on 2016.08.09
#
# Author: Giovanni Cannata
#
# Copyright 2016 - 2020 Giovanni Cannata
#
# This file is part of ldap3.
#
# ldap3 is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ldap3 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ldap3 in the COPYING and COPYING.LESSER files.
# If not, see <http://www.gnu.org/licenses/>.
from binascii import a2b_hex, hexlify
from datetime import datetime
from calendar import timegm
from uuid import UUID
from struct import pack
from ... import SEQUENCE_TYPES, STRING_TYPES, NUMERIC_TYPES, INTEGER_TYPES
from .formatters import format_time, format_ad_timestamp
from ...utils.conv import to_raw, to_unicode, ldap_escape_to_bytes, escape_bytes
# Validators return True if value is valid, False if value is not valid,
# or a value different from True and False that is a valid value to substitute to the input value
def check_backslash(value):
if isinstance(value, (bytearray, bytes)):
if b'\\' in value:
value = value.replace(b'\\', b'\\5C')
elif isinstance(value, STRING_TYPES):
if '\\' in value:
value = value.replace('\\', '\\5C')
return value
def check_type(input_value, value_type):
if isinstance(input_value, value_type):
return True
if isinstance(input_value, SEQUENCE_TYPES):
for value in input_value:
if not isinstance(value, value_type):
return False
return True
return False
# noinspection PyUnusedLocal
def always_valid(input_value):
return True
def validate_generic_single_value(input_value):
if not isinstance(input_value, SEQUENCE_TYPES):
return True
try: # object couldn't have a __len__ method
if len(input_value) == 1:
return True
except Exception:
pass
return False
def validate_zero_and_minus_one_and_positive_int(input_value):
"""Accept -1 and 0 only (used by pwdLastSet in AD)
"""
if not isinstance(input_value, SEQUENCE_TYPES):
if isinstance(input_value, NUMERIC_TYPES) or isinstance(input_value, STRING_TYPES):
return True if int(input_value) >= -1 else False
return False
else:
if len(input_value) == 1 and (isinstance(input_value[0], NUMERIC_TYPES) or isinstance(input_value[0], STRING_TYPES)):
return True if int(input_value[0]) >= -1 else False
return False
def validate_integer(input_value):
if check_type(input_value, (float, bool)):
return False
if check_type(input_value, INTEGER_TYPES):
return True
if not isinstance(input_value, SEQUENCE_TYPES):
sequence = False
input_value = [input_value]
else:
sequence = True # indicates if a sequence must be returned
valid_values = [] # builds a list of valid int values
from decimal import Decimal, InvalidOperation
for element in input_value:
try: #try to convert any type to int, an invalid conversion raise TypeError or ValueError, doublecheck with Decimal type, if both are valid and equal then then int() value is used
value = to_unicode(element) if isinstance(element, bytes) else element
decimal_value = Decimal(value)
int_value = int(value)
if decimal_value == int_value:
valid_values.append(int_value)
else:
return False
except (ValueError, TypeError, InvalidOperation):
return False
if sequence:
return valid_values
else:
return valid_values[0]
def validate_bytes(input_value):
return check_type(input_value, bytes)
def validate_boolean(input_value):
# it could be a real bool or the string TRUE or FALSE, # only a single valued is allowed
if validate_generic_single_value(input_value): # valid only if a single value or a sequence with a single element
if isinstance(input_value, SEQUENCE_TYPES):
input_value = input_value[0]
if isinstance(input_value, bool):
if input_value:
return 'TRUE'
else:
return 'FALSE'
if str is not bytes and isinstance(input_value, bytes): # python3 try to converts bytes to string
input_value = to_unicode(input_value)
if isinstance(input_value, STRING_TYPES):
if input_value.lower() == 'true':
return 'TRUE'
elif input_value.lower() == 'false':
return 'FALSE'
return False
def validate_time_with_0_year(input_value):
# validates generalized time but accept a 0000 year too
# if datetime object doesn't have a timezone it's considered local time and is adjusted to UTC
if not isinstance(input_value, SEQUENCE_TYPES):
sequence = False
input_value = [input_value]
else:
sequence = True # indicates if a sequence must be returned
valid_values = []
changed = False
for element in input_value:
if str is not bytes and isinstance(element, bytes): # python3 try to converts bytes to string
element = to_unicode(element)
if isinstance(element, STRING_TYPES): # tries to check if it is already be a Generalized Time
if element.startswith('0000') or isinstance(format_time(to_raw(element)), datetime): # valid Generalized Time string
valid_values.append(element)
else:
return False
elif isinstance(element, datetime):
changed = True
if element.tzinfo: # a datetime with a timezone
valid_values.append(element.strftime('%Y%m%d%H%M%S%z'))
else: # datetime without timezone, assumed local and adjusted to UTC
offset = datetime.now() - datetime.utcnow()
valid_values.append((element - offset).strftime('%Y%m%d%H%M%SZ'))
else:
return False
if changed:
if sequence:
return valid_values
else:
return valid_values[0]
else:
return True
def validate_time(input_value):
# if datetime object doesn't have a timezone it's considered local time and is adjusted to UTC
if not isinstance(input_value, SEQUENCE_TYPES):
sequence = False
input_value = [input_value]
else:
sequence = True # indicates if a sequence must be returned
valid_values = []
changed = False
for element in input_value:
if str is not bytes and isinstance(element, bytes): # python3 try to converts bytes to string
element = to_unicode(element)
if isinstance(element, STRING_TYPES): # tries to check if it is already be a Generalized Time
if isinstance(format_time(to_raw(element)), datetime): # valid Generalized Time string
valid_values.append(element)
else:
return False
elif isinstance(element, datetime):
changed = True
if element.tzinfo: # a datetime with a timezone
valid_values.append(element.strftime('%Y%m%d%H%M%S%z'))
else: # datetime without timezone, assumed local and adjusted to UTC
offset = datetime.now() - datetime.utcnow()
valid_values.append((element - offset).strftime('%Y%m%d%H%M%SZ'))
else:
return False
if changed:
if sequence:
return valid_values
else:
return valid_values[0]
else:
return True
def validate_ad_timestamp(input_value):
"""
Active Directory stores date/time values as the number of 100-nanosecond intervals
that have elapsed since the 0 hour on January 1, 1601 till the date/time that is being stored.
The time is always stored in Greenwich Mean Time (GMT) in the Active Directory.
"""
if not isinstance(input_value, SEQUENCE_TYPES):
sequence = False
input_value = [input_value]
else:
sequence = True # indicates if a sequence must be returned
valid_values = []
changed = False
for element in input_value:
if str is not bytes and isinstance(element, bytes): # python3 try to converts bytes to string
element = to_unicode(element)
if isinstance(element, NUMERIC_TYPES):
if 0 <= element <= 9223372036854775807: # min and max for the AD timestamp starting from 12:00 AM January 1, 1601
valid_values.append(element)
else:
return False
elif isinstance(element, STRING_TYPES): # tries to check if it is already be a AD timestamp
if isinstance(format_ad_timestamp(to_raw(element)), datetime): # valid Generalized Time string
valid_values.append(element)
else:
return False
elif isinstance(element, datetime):
changed = True
if element.tzinfo: # a datetime with a timezone
valid_values.append(to_raw((timegm(element.utctimetuple()) + 11644473600) * 10000000, encoding='ascii'))
else: # datetime without timezone, assumed local and adjusted to UTC
offset = datetime.now() - datetime.utcnow()
valid_values.append(to_raw((timegm((element - offset).timetuple()) + 11644473600) * 10000000, encoding='ascii'))
else:
return False
if changed:
if sequence:
return valid_values
else:
return valid_values[0]
else:
return True
def validate_ad_timedelta(input_value):
"""
Should be validated like an AD timestamp except that since it is a time
delta, it is stored as a negative number.
"""
if not isinstance(input_value, INTEGER_TYPES) or input_value > 0:
return False
return validate_ad_timestamp(input_value * -1)
def validate_guid(input_value):
"""
object guid in uuid format (Novell eDirectory)
"""
if not isinstance(input_value, SEQUENCE_TYPES):
sequence = False
input_value = [input_value]
else:
sequence = True # indicates if a sequence must be returned
valid_values = []
changed = False
for element in input_value:
if isinstance(element, STRING_TYPES):
try:
valid_values.append(UUID(element).bytes)
changed = True
except ValueError: # try if the value is an escaped ldap byte sequence
try:
x = ldap_escape_to_bytes(element)
valid_values.append(UUID(bytes=x).bytes)
changed = True
continue
except ValueError:
if str is not bytes: # python 3
pass
else:
valid_values.append(element)
continue
return False
elif isinstance(element, (bytes, bytearray)): # assumes bytes are valid
valid_values.append(element)
else:
return False
if changed:
# valid_values = [check_backslash(value) for value in valid_values]
if sequence:
return valid_values
else:
return valid_values[0]
else:
return True
def validate_uuid(input_value):
"""
object entryUUID in uuid format
"""
if not isinstance(input_value, SEQUENCE_TYPES):
sequence = False
input_value = [input_value]
else:
sequence = True # indicates if a sequence must be returned
valid_values = []
changed = False
for element in input_value:
if isinstance(element, STRING_TYPES):
try:
valid_values.append(str(UUID(element)))
changed = True
except ValueError: # try if the value is an escaped byte sequence
try:
valid_values.append(str(UUID(element.replace('\\', ''))))
changed = True
continue
except ValueError:
if str is not bytes: # python 3
pass
else:
valid_values.append(element)
continue
return False
elif isinstance(element, (bytes, bytearray)): # assumes bytes are valid
valid_values.append(element)
else:
return False
if changed:
# valid_values = [check_backslash(value) for value in valid_values]
if sequence:
return valid_values
else:
return valid_values[0]
else:
return True
def validate_uuid_le(input_value):
r"""
Active Directory stores objectGUID in uuid_le format, follows RFC4122 and MS-DTYP:
"{07039e68-4373-264d-a0a7-07039e684373}": string representation big endian, converted to little endian (with or without brace curles)
"689e030773434d26a7a007039e684373": packet representation, already in little endian
"\68\9e\03\07\73\43\4d\26\a7\a0\07\03\9e\68\43\73": bytes representation, already in little endian
byte sequence: already in little endian
"""
if not isinstance(input_value, SEQUENCE_TYPES):
sequence = False
input_value = [input_value]
else:
sequence = True # indicates if a sequence must be returned
valid_values = []
changed = False
for element in input_value:
error = False
if isinstance(element, STRING_TYPES):
if element[0] == '{' and element[-1] == '}':
try:
valid_values.append(UUID(hex=element).bytes_le) # string representation, value in big endian, converts to little endian
changed = True
except ValueError:
error = True
elif '-' in element:
try:
valid_values.append(UUID(hex=element).bytes_le) # string representation, value in big endian, converts to little endian
changed = True
except ValueError:
error = True
elif '\\' in element:
try:
valid_values.append(UUID(bytes_le=ldap_escape_to_bytes(element)).bytes_le) # byte representation, value in little endian
changed = True
except ValueError:
error = True
elif '-' not in element: # value in little endian
try:
valid_values.append(UUID(bytes_le=a2b_hex(element)).bytes_le) # packet representation, value in little endian, converts to little endian
changed = True
except ValueError:
error = True
if error and (str is bytes): # python2 only assume value is bytes and valid
valid_values.append(element) # value is untouched, must be in little endian
elif isinstance(element, (bytes, bytearray)): # assumes bytes are valid uuid
valid_values.append(element) # value is untouched, must be in little endian
else:
return False
if changed:
# valid_values = [check_backslash(value) for value in valid_values]
if sequence:
return valid_values
else:
return valid_values[0]
else:
return True
def validate_sid(input_value):
"""
SID= "S-1-" IdentifierAuthority 1*SubAuthority
IdentifierAuthority= IdentifierAuthorityDec / IdentifierAuthorityHex
; If the identifier authority is < 2^32, the
; identifier authority is represented as a decimal
; number
; If the identifier authority is >= 2^32,
; the identifier authority is represented in
; hexadecimal
IdentifierAuthorityDec = 1*10DIGIT
; IdentifierAuthorityDec, top level authority of a
; security identifier is represented as a decimal number
IdentifierAuthorityHex = "0x" 12HEXDIG
; IdentifierAuthorityHex, the top-level authority of a
; security identifier is represented as a hexadecimal number
SubAuthority= "-" 1*10DIGIT
; Sub-Authority is always represented as a decimal number
; No leading "0" characters are allowed when IdentifierAuthority
; or SubAuthority is represented as a decimal number
; All hexadecimal digits must be output in string format,
; pre-pended by "0x"
Revision (1 byte): An 8-bit unsigned integer that specifies the revision level of the SID. This value MUST be set to 0x01.
SubAuthorityCount (1 byte): An 8-bit unsigned integer that specifies the number of elements in the SubAuthority array. The maximum number of elements allowed is 15.
IdentifierAuthority (6 bytes): A SID_IDENTIFIER_AUTHORITY structure that indicates the authority under which the SID was created. It describes the entity that created the SID. The Identifier Authority value {0,0,0,0,0,5} denotes SIDs created by the NT SID authority.
SubAuthority (variable): A variable length array of unsigned 32-bit integers that uniquely identifies a principal relative to the IdentifierAuthority. Its length is determined by SubAuthorityCount.
If you have a SID like S-a-b-c-d-e-f-g-...
Then the bytes are
a (revision)
N (number of dashes minus two)
bbbbbb (six bytes of "b" treated as a 48-bit number in big-endian format)
cccc (four bytes of "c" treated as a 32-bit number in little-endian format)
dddd (four bytes of "d" treated as a 32-bit number in little-endian format)
eeee (four bytes of "e" treated as a 32-bit number in little-endian format)
ffff (four bytes of "f" treated as a 32-bit number in little-endian format)
"""
if not isinstance(input_value, SEQUENCE_TYPES):
sequence = False
input_value = [input_value]
else:
sequence = True # indicates if a sequence must be returned
valid_values = []
changed = False
for element in input_value:
if isinstance(element, STRING_TYPES):
if element.startswith('S-'):
parts = element.split('-')
sid_bytes = pack('<q', int(parts[1]))[0:1] # revision number
sid_bytes += pack('<q', len(parts[3:]))[0:1] # number of sub authorities
if len(parts[2]) <= 10:
sid_bytes += pack('>q', int(parts[2]))[2:] # authority (in dec)
else:
sid_bytes += pack('>q', int(parts[2], 16))[2:] # authority (in hex)
for sub_auth in parts[3:]:
sid_bytes += pack('<q', int(sub_auth))[0:4] # sub-authorities
valid_values.append(sid_bytes)
changed = True
if changed:
# valid_values = [check_backslash(value) for value in valid_values]
if sequence:
return valid_values
else:
return valid_values[0]
else:
return True