5 # Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
6 # Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
31 from samba
import credentials
, nttime2float
32 from samba
.auth
import system_session
33 from samba
.common
import get_bytes
, get_string
34 from samba
.dcerpc
import drsblobs
, security
, gmsa
35 from samba
.ndr
import ndr_unpack
36 from samba
.netcmd
import Command
, CommandError
37 from samba
.samdb
import SamDB
38 from samba
.nt_time
import timedelta_from_nt_time_delta
, nt_time_from_datetime
39 from samba
.gkdi
import MAX_CLOCK_SKEW
41 # python[3]-gpgme is abandoned since ubuntu 1804 and debian 9
42 # have to use python[3]-gpg instead
43 # The API is different, need to adapt.
45 def _gpgme_decrypt(encrypted_bytes
):
47 Use python[3]-gpgme to decrypt GPG.
50 ctx
.armor
= True # use ASCII-armored
52 ctx
.decrypt(io
.BytesIO(encrypted_bytes
), out
)
56 def _gpg_decrypt(encrypted_bytes
):
58 Use python[3]-gpg to decrypt GPG.
60 ciphertext
= gpg
.Data(string
=encrypted_bytes
)
61 ctx
= gpg
.Context(armor
=True)
62 # plaintext, result, verify_result
63 plaintext
, _
, _
= ctx
.decrypt(ciphertext
)
72 gpg_decrypt
= _gpgme_decrypt
79 gpg_decrypt
= _gpg_decrypt
84 decrypt_samba_gpg_help
= ("Decrypt the SambaGPG password as "
87 decrypt_samba_gpg_help
= ("Decrypt the SambaGPG password not supported, "
88 "python[3]-gpgme or python[3]-gpg required")
91 disabled_virtual_attributes
= {
94 virtual_attributes
= {
95 "virtualClearTextUTF8": {
96 "flags": ldb
.ATTR_FLAG_FORCE_BASE64_LDIF
,
98 "virtualClearTextUTF16": {
99 "flags": ldb
.ATTR_FLAG_FORCE_BASE64_LDIF
,
102 "flags": ldb
.ATTR_FLAG_FORCE_BASE64_LDIF
,
105 "flags": ldb
.ATTR_FLAG_FORCE_BASE64_LDIF
,
107 "virtualManagedPasswordQueryTime": {
112 def get_crypt_value(alg
, utf8pw
, rounds
=0):
118 raise ValueError(f
"invalid algorithm code: {alg}"
119 f
"(expected one of {','.join(algs.keys())})")
121 salt
= os
.urandom(16)
122 # The salt needs to be in [A-Za-z0-9./]
123 # base64 is close enough and as we had 16
124 # random bytes but only need 16 characters
125 # we can ignore the possible == at the end
126 # of the base64 string
127 # we just need to replace '+' by '.'
128 b64salt
= base64
.b64encode(salt
)[0:16].replace(b
'+', b
'.').decode('utf8')
131 crypt_salt
= "$%s$rounds=%s$%s$" % (alg
, rounds
, b64salt
)
133 crypt_salt
= "$%s$%s$" % (alg
, b64salt
)
135 crypt_value
= crypt
.crypt(utf8pw
, crypt_salt
)
136 if crypt_value
is None:
137 raise NotImplementedError("crypt.crypt(%s) returned None" % (crypt_salt
))
138 expected_len
= len(crypt_salt
) + algs
[alg
]["length"]
139 if len(crypt_value
) != expected_len
:
140 raise NotImplementedError("crypt.crypt(%s) returned a value with length %d, expected length is %d" % (
141 crypt_salt
, len(crypt_value
), expected_len
))
148 virtual_attributes
["virtualSSHA"] = {
150 except ImportError as e
:
151 reason
= "hashlib.sha1()"
152 reason
+= " required"
153 disabled_virtual_attributes
["virtualSSHA"] = {
157 for (alg
, attr
) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
160 get_crypt_value(alg
, "")
161 virtual_attributes
[attr
] = {
163 except ImportError as e
:
165 reason
+= " required"
166 disabled_virtual_attributes
[attr
] = {
169 except NotImplementedError as e
:
170 reason
= "modern '$%s$' salt in crypt(3) required" % (alg
)
171 disabled_virtual_attributes
[attr
] = {
175 # Add the wDigest virtual attributes, virtualWDigest01 to virtualWDigest29
176 for x
in range(1, 30):
177 virtual_attributes
["virtualWDigest%02d" % x
] = {}
179 # Add Kerberos virtual attributes
180 virtual_attributes
["virtualKerberosSalt"] = {}
182 virtual_attributes_help
= "The attributes to display (comma separated). "
183 virtual_attributes_help
+= "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes
.keys()))
184 if len(disabled_virtual_attributes
) != 0:
185 virtual_attributes_help
+= "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes
.keys()))
188 class GetPasswordCommand(Command
):
194 def inject_virtual_attributes(self
, samdb
):
195 # We use sort here in order to have a predictable processing order
196 # this might not be strictly needed, but also doesn't hurt here
197 for a
in sorted(virtual_attributes
.keys()):
198 flags
= ldb
.ATTR_FLAG_HIDDEN | virtual_attributes
[a
].get("flags", 0)
199 samdb
.schema_attribute_add(a
, flags
, ldb
.SYNTAX_OCTET_STRING
)
201 def connect_for_passwords(self
, url
,
206 # using anonymous here, results in no authentication
207 # which means we can get system privileges via
208 # the privileged ldapi socket
209 anon_creds
= credentials
.Credentials()
210 anon_creds
.set_anonymous()
212 if url
is None and not require_ldapi
:
214 elif url
.lower().startswith("ldapi://"):
218 raise CommandError("--url requires an ldapi:// url for this command")
221 self
.outf
.write("Connecting to '%s'\n" % url
)
223 samdb
= SamDB(url
=url
, session_info
=system_session(),
224 credentials
=creds
, lp
=self
.lp
)
226 if require_ldapi
or url
is None:
229 # Make sure we're connected as SYSTEM
231 res
= samdb
.search(base
='', scope
=ldb
.SCOPE_BASE
, attrs
=["tokenGroups"])
233 sids
= res
[0].get("tokenGroups")
234 assert len(sids
) == 1
235 sid
= ndr_unpack(security
.dom_sid
, sids
[0])
236 assert str(sid
) == security
.SID_NT_SYSTEM
237 except Exception as msg
:
238 raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
239 (security
.SID_NT_SYSTEM
))
241 self
.inject_virtual_attributes(samdb
)
245 def get_account_attributes(self
, samdb
, username
, basedn
, filter, scope
,
246 attrs
, decrypt
, support_pw_attrs
=True):
248 def get_option(opts
, name
):
252 if o
.lower().startswith("%s=" % name
.lower()):
253 (key
, _
, val
) = o
.partition('=')
257 def get_virtual_attr_definition(attr
):
258 for van
in sorted(virtual_attributes
.keys()):
259 if van
.lower() != attr
.lower():
261 return virtual_attributes
[van
]
270 def get_virtual_format_definition(opts
):
271 formatname
= get_option(opts
, "format")
272 if formatname
is None:
275 if fm
.lower() != formatname
.lower():
280 def parse_raw_attr(raw_attr
, is_hidden
=False):
281 (attr
, _
, fullopts
) = raw_attr
.partition(';')
283 opts
= fullopts
.split(';')
287 a
["raw_attr"] = raw_attr
290 a
["vattr"] = get_virtual_attr_definition(attr
)
291 a
["vformat"] = get_virtual_format_definition(opts
)
292 a
["is_hidden"] = is_hidden
296 has_wildcard_attr
= "*" in raw_attrs
297 has_virtual_attrs
= False
301 for raw_attr
in raw_attrs
:
302 a
= parse_raw_attr(raw_attr
)
303 requested_attrs
.append(a
)
306 has_virtual_attrs
= False
307 for a
in requested_attrs
:
308 if a
["vattr"] is not None:
309 has_virtual_attrs
= True
311 if a
["vformat"] is not None:
312 # also add it as implicit attr,
314 # search_attrs.append(a["attr"])
316 implicit_attrs
.append(a
)
318 if a
["raw_attr"] in search_attrs
:
320 search_attrs
.append(a
["raw_attr"])
322 if not has_wildcard_attr
:
327 for required_attr
in required_attrs
:
328 a
= parse_raw_attr(required_attr
)
329 implicit_attrs
.append(a
)
331 if has_virtual_attrs
:
334 "supplementalCredentials",
336 "msDS-ManagedPassword",
338 for required_attr
in required_attrs
:
339 a
= parse_raw_attr(required_attr
, is_hidden
=True)
340 implicit_attrs
.append(a
)
342 for a
in implicit_attrs
:
343 if a
["attr"] in search_attrs
:
345 search_attrs
.append(a
["attr"])
347 if scope
== ldb
.SCOPE_BASE
:
348 search_controls
= ["show_deleted:1", "show_recycled:1"]
352 res
= samdb
.search(base
=basedn
, expression
=filter,
353 scope
=scope
, attrs
=search_attrs
,
354 controls
=search_controls
)
356 raise Exception('Unable to find user "%s"' % (username
or filter))
358 raise Exception('Matched %u multiple users with filter "%s"' % (len(res
), filter))
359 except Exception as msg
:
360 # FIXME: catch more specific exception
361 raise CommandError("Failed to get password for user '%s': %s" % (username
or filter, msg
))
368 if "supplementalCredentials" in obj
:
369 sc_blob
= obj
["supplementalCredentials"][0]
370 sc
= ndr_unpack(drsblobs
.supplementalCredentialsBlob
, sc_blob
)
371 if "unicodePwd" in obj
:
372 unicodePwd
= obj
["unicodePwd"][0]
373 if "msDS-ManagedPassword" in obj
:
374 # unpack a GMSA managed password as if we could read the
375 # hidden password attributes.
376 managed_password
= obj
["msDS-ManagedPassword"][0]
377 unpacked_managed_password
= ndr_unpack(gmsa
.MANAGEDPASSWORD_BLOB
,
379 calculated
["OLDCLEARTEXT"] = \
380 unpacked_managed_password
.passwords
.previous
381 query_interval
= unpacked_managed_password
.passwords
.query_interval
382 calculated
["GMSA:query_interval"] = \
385 query_time_datetime
= \
386 timedelta_from_nt_time_delta(query_interval
) + datetime
.datetime
.now(tz
=datetime
.timezone
.utc
)
388 query_time_nttime
= nt_time_from_datetime(query_time_datetime
)
390 calculated
["GMSA:query_time"] = query_time_nttime
392 # This password is useful for a keytab, but not for
393 # authentication, so don't show or provide it as the new password
395 if calculated
["GMSA:query_interval"] <= MAX_CLOCK_SKEW
:
396 calculated
["Primary:CLEARTEXT"] = \
397 unpacked_managed_password
.passwords
.previous
399 calculated
["Primary:CLEARTEXT"] = \
400 unpacked_managed_password
.passwords
.current
402 account_name
= str(obj
["sAMAccountName"][0])
403 if "userPrincipalName" in obj
:
404 account_upn
= str(obj
["userPrincipalName"][0])
406 realm
= samdb
.domain_dns_name()
407 account_upn
= "%s@%s" % (account_name
, realm
.lower())
409 def get_package(name
, min_idx
=0):
410 if name
in calculated
:
411 return calculated
[name
]
415 min_idx
= len(sc
.sub
.packages
) + min_idx
417 for p
in sc
.sub
.packages
:
424 return binascii
.a2b_hex(p
.data
)
427 def get_cleartext(attr_opts
):
428 param
= get_option(attr_opts
, "previous")
432 f
"Invalid attribute parameter ;previous={param}, "
433 "only supported value is previous=1")
434 return calculated
.get("OLDCLEARTEXT")
436 return get_package("Primary:CLEARTEXT")
438 def get_kerberos_ctr():
439 primary_krb5
= get_package("Primary:Kerberos-Newer-Keys")
440 if primary_krb5
is None:
441 primary_krb5
= get_package("Primary:Kerberos")
442 if primary_krb5
is None:
444 krb5_blob
= ndr_unpack(drsblobs
.package_PrimaryKerberosBlob
,
446 return (krb5_blob
.version
, krb5_blob
.ctr
)
451 (krb5_v
, krb5_ctr
) = get_kerberos_ctr()
453 kerberos_salt
= krb5_ctr
.salt
.string
457 return k
.keytype
== 18
458 aes256_key
= next(builtins
.filter(is_aes256
, krb5_ctr
.keys
),
463 # Samba adds 'Primary:SambaGPG' at the end.
464 # When Windows sets the password it keeps
465 # 'Primary:SambaGPG' and rotates it to
466 # the beginning. So we can only use the value,
467 # if it is the last one.
469 # In order to get more protection we verify
470 # the nthash of the decrypted utf16 password
471 # against the stored nthash in unicodePwd if
472 # available, otherwise against the first 16
473 # bytes of the AES256 key.
475 sgv
= get_package("Primary:SambaGPG", min_idx
=-1)
478 cv
= gpg_decrypt(sgv
)
480 # We only use the password if it matches
481 # the current nthash stored in the unicodePwd
482 # attribute, or the current AES256 key.
484 tmp
= credentials
.Credentials()
486 tmp
.set_utf16_password(cv
)
491 if unicodePwd
is not None:
492 decrypted
= tmp
.get_nt_hash()
493 current_hash
= unicodePwd
494 elif aes256_key
is not None and kerberos_salt
is not None:
495 tmp
.set_kerberos_salt_principal(kerberos_salt
)
496 decrypted
= tmp
.get_kerberos_key(credentials
.ENCTYPE_AES256_CTS_HMAC_SHA1_96
)
497 current_hash
= aes256_key
.value
499 if current_hash
is not None and current_hash
== decrypted
:
500 calculated
["Primary:CLEARTEXT"] = cv
502 except Exception as e
:
503 if gpg_decrypt
is None:
504 message
= decrypt_samba_gpg_help
508 "WARNING: '%s': SambaGPG can't be decrypted "
509 "into CLEARTEXT: %s\n" % (
510 username
or account_name
, message
))
512 def get_utf8(a
, b
, username
):
513 creds_for_charcnv
= credentials
.Credentials()
514 creds_for_charcnv
.set_anonymous()
515 creds_for_charcnv
.set_utf16_password(get_bytes(b
))
517 # This can't fail due to character conversion issues as it
518 # includes a built-in fallback (UTF16_MUNGED) matching
519 # exactly what we need.
520 return creds_for_charcnv
.get_password().encode()
522 # Extract the WDigest hash for the value specified by i.
523 # Builds an htdigest compatible value
526 def get_wDigest(i
, primary_wdigest
, account_name
, account_upn
,
532 user
= account_name
.lower()
533 realm
= domain
.lower()
535 user
= account_name
.upper()
536 realm
= domain
.upper()
539 realm
= domain
.upper()
542 realm
= domain
.lower()
544 user
= account_name
.upper()
545 realm
= domain
.lower()
547 user
= account_name
.lower()
548 realm
= domain
.upper()
551 realm
= dns_domain
.lower()
553 user
= account_name
.lower()
554 realm
= dns_domain
.lower()
556 user
= account_name
.upper()
557 realm
= dns_domain
.upper()
560 realm
= dns_domain
.upper()
563 realm
= dns_domain
.lower()
565 user
= account_name
.upper()
566 realm
= dns_domain
.lower()
568 user
= account_name
.lower()
569 realm
= dns_domain
.upper()
574 user
= account_upn
.lower()
577 user
= account_upn
.upper()
580 user
= "%s\\%s" % (domain
, account_name
)
583 user
= "%s\\%s" % (domain
.lower(), account_name
.lower())
586 user
= "%s\\%s" % (domain
.upper(), account_name
.upper())
592 user
= account_name
.lower()
595 user
= account_name
.upper()
601 user
= account_upn
.lower()
604 user
= account_upn
.upper()
607 user
= "%s\\%s" % (domain
, account_name
)
610 # Differs from spec, see tests
611 user
= "%s\\%s" % (domain
.lower(), account_name
.lower())
614 # Differs from spec, see tests
615 user
= "%s\\%s" % (domain
.upper(), account_name
.upper())
620 digests
= ndr_unpack(drsblobs
.package_PrimaryWDigestBlob
,
623 digest
= binascii
.hexlify(bytearray(digests
.hashes
[i
- 1].hash))
624 return "%s:%s:%s" % (user
, realm
, get_string(digest
))
628 # get the value for a virtualCrypt attribute.
629 # look for an exact match on algorithm and rounds in supplemental creds
630 # if not found calculate using Primary:CLEARTEXT
631 # if no Primary:CLEARTEXT return the first supplementalCredential
632 # that matches the algorithm.
633 def get_virtual_crypt_value(a
, algorithm
, rounds
, username
, account_name
):
636 b
= get_package("Primary:userPassword")
638 (sv
, fb
) = get_userPassword_hash(b
, algorithm
, rounds
)
640 # No exact match on algorithm and number of rounds
641 # try and calculate one from the Primary:CLEARTEXT
642 b
= get_cleartext(attr_opts
)
644 u8
= get_utf8(a
, b
, username
or account_name
)
646 # in py2 using get_bytes should ensure u8 is unmodified
647 # in py3 it will be decoded
648 sv
= get_crypt_value(str(algorithm
), get_string(u8
), rounds
)
650 # Unable to calculate a hash with the specified
651 # number of rounds, fall back to the first hash using
652 # the specified algorithm
656 return "{CRYPT}" + sv
658 def get_userPassword_hash(blob
, algorithm
, rounds
):
659 up
= ndr_unpack(drsblobs
.package_PrimaryUserPasswordBlob
, blob
)
662 # Check that the NT hash or AES256 key have not been changed
663 # without updating the user password hashes. This indicates that
664 # password has been changed without updating the supplemental
666 if unicodePwd
is not None:
667 current_hash
= unicodePwd
668 elif aes256_key
is not None:
669 current_hash
= aes256_key
.value
[:16]
673 if current_hash
!= bytearray(up
.current_nt_hash
.hash):
676 scheme_prefix
= "$%d$" % algorithm
677 prefix
= scheme_prefix
679 prefix
= "$%d$rounds=%d" % (algorithm
, rounds
)
683 # in PY2 this should just do nothing and in PY3 if bytes
684 # it will decode them
685 h_value
= get_string(h
.value
)
686 if (scheme_match
is None and
687 h
.scheme
== SCHEME
and
688 h_value
.startswith(scheme_prefix
)):
689 scheme_match
= h_value
690 if h
.scheme
== SCHEME
and h_value
.startswith(prefix
):
691 return (h_value
, scheme_match
)
693 # No match on the number of rounds, return the value of the
694 # first matching scheme
695 return (None, scheme_match
)
697 # Extract the rounds value from the options of a virtualCrypt attribute
698 # i.e. options = "rounds=20;other=ignored;" will return 20
699 # if the rounds option is not found or the value is not a number, 0 is returned
700 # which indicates that the default number of rounds should be used.
701 def get_rounds(opts
):
702 val
= get_option(opts
, "rounds")
710 def get_unicode_pwd_hash(pwd
):
711 # We can't read unicodePwd directly, but we can regenerate
712 # it from msDS-ManagedPassword
713 tmp
= credentials
.Credentials()
715 tmp
.set_utf16_password(pwd
)
716 return tmp
.get_nt_hash()
718 # We use sort here in order to have a predictable processing order
719 for a
in sorted(virtual_attributes
.keys()):
721 for ra
in requested_attrs
:
722 if ra
["vattr"] is None:
724 if ra
["attr"].lower() != a
.lower():
730 attr_opts
= vattr
["opts"]
732 if a
== "virtualClearTextUTF8":
733 b
= get_cleartext(attr_opts
)
736 u8
= get_utf8(a
, b
, username
or account_name
)
740 elif a
== "virtualClearTextUTF16":
741 v
= get_cleartext(attr_opts
)
744 elif a
== "virtualSSHA":
745 b
= get_cleartext(attr_opts
)
748 u8
= get_utf8(a
, b
, username
or account_name
)
755 bv
= h
.digest() + salt
756 v
= "{SSHA}" + base64
.b64encode(bv
).decode('utf8')
757 elif a
== "virtualCryptSHA256":
758 rounds
= get_rounds(attr_opts
)
759 x
= get_virtual_crypt_value(a
, 5, rounds
, username
, account_name
)
763 elif a
== "virtualCryptSHA512":
764 rounds
= get_rounds(attr_opts
)
765 x
= get_virtual_crypt_value(a
, 6, rounds
, username
, account_name
)
769 elif a
== "virtualSambaGPG":
770 # Samba adds 'Primary:SambaGPG' at the end.
771 # When Windows sets the password it keeps
772 # 'Primary:SambaGPG' and rotates it to
773 # the beginning. So we can only use the value,
774 # if it is the last one.
775 v
= get_package("Primary:SambaGPG", min_idx
=-1)
778 elif a
== "virtualKerberosSalt":
782 elif a
== "unicodePwd" and unicodePwd
is None:
783 if "Primary:CLEARTEXT" in calculated
and not get_option(attr_opts
, "previous"):
784 v
= get_unicode_pwd_hash(calculated
["Primary:CLEARTEXT"])
785 elif "OLDCLEARTEXT" in calculated
and get_option(attr_opts
, "previous"):
786 v
= get_unicode_pwd_hash(calculated
["OLDCLEARTEXT"])
789 elif a
.startswith("virtualWDigest"):
790 primary_wdigest
= get_package("Primary:WDigest")
791 if primary_wdigest
is None:
793 x
= a
[len("virtualWDigest"):]
798 domain
= samdb
.domain_netbios_name()
799 dns_domain
= samdb
.domain_dns_name()
800 v
= get_wDigest(i
, primary_wdigest
, account_name
, account_upn
, domain
, dns_domain
)
803 elif a
== "virtualManagedPasswordQueryTime":
804 if "GMSA:query_time" not in calculated
:
806 v
= str(calculated
["GMSA:query_time"])
809 obj
[a
] = ldb
.MessageElement(v
, ldb
.FLAG_MOD_REPLACE
, vattr
["raw_attr"])
811 def get_src_attrname(srcattrg
):
812 srcattrl
= srcattrg
.lower()
815 if srcattrl
!= k
.lower():
821 def get_src_time_float(srcattr
):
822 if srcattr
not in obj
:
824 vstr
= str(obj
[srcattr
][0])
825 if vstr
.endswith(".0Z"):
826 vut
= ldb
.string_to_time(vstr
)
832 except ValueError as e
:
834 # 0 or 9223372036854775807 mean no value too
837 if vnt
>= 0x7FFFFFFFFFFFFFFF:
839 vfl
= nttime2float(vnt
)
842 def get_generalizedtime(srcattr
):
843 vfl
= get_src_time_float(srcattr
)
848 v
= "%s" % ldb
.timestring(vut
)
850 if e
.errno
== errno
.EOVERFLOW
:
855 def get_unixepoch(srcattr
):
856 vfl
= get_src_time_float(srcattr
)
863 def get_timespec(srcattr
):
864 vfl
= get_src_time_float(srcattr
)
870 generated_formats
= {}
872 for ra
in requested_attrs
:
873 if ra
["vformat"] is None:
875 if ra
["vformat"] != fm
:
878 srcattr
= get_src_attrname(ra
["attr"])
879 if srcattr
is not None:
880 an
= "%s;format=%s" % (srcattr
, fm
)
882 srcattr
= an
= get_src_attrname(ra
["raw_attr"])
885 if an
in generated_formats
:
887 generated_formats
[an
] = fm
890 if fm
== "GeneralizedTime":
891 v
= get_generalizedtime(srcattr
)
892 elif fm
== "UnixTime":
893 v
= get_unixepoch(srcattr
)
894 elif fm
== "TimeSpec":
895 v
= get_timespec(srcattr
)
898 obj
[an
] = ldb
.MessageElement(v
, ldb
.FLAG_MOD_REPLACE
, an
)
900 # Now filter out implicit attributes
901 for delname
in obj
.keys():
903 for ra
in requested_attrs
:
904 if delname
.lower() != ra
["raw_attr"].lower():
912 for ia
in implicit_attrs
:
913 if delname
.lower() != ia
["attr"].lower():
920 if has_wildcard_attr
and not dattr
["is_hidden"]:
925 def parse_attributes(self
, attributes
):
927 if attributes
is None:
928 raise CommandError("Please specify --attributes")
929 attrs
= attributes
.split(',')
932 pa
= pa
.lstrip().rstrip()
933 for da
in disabled_virtual_attributes
.keys():
934 if pa
.lower() == da
.lower():
935 r
= disabled_virtual_attributes
[da
]["reason"]
936 raise CommandError("Virtual attribute '%s' not supported: %s" % (
938 for va
in virtual_attributes
.keys():
939 if pa
.lower() == va
.lower():
943 password_attrs
+= [pa
]
945 return password_attrs