getncchanges.py: Add some GET_TGT test cases
[nivanova/samba-autobuild/.git] / source4 / torture / drs / python / drs_base.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # Unix SMB/CIFS implementation.
5 # Copyright (C) Kamen Mazdrashki <kamenim@samba.org> 2011
6 # Copyright (C) Andrew Bartlett <abartlet@samba.org> 2016
7 # Copyright (C) Catalyst IT Ltd. 2016
8 #
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
21 #
22
23 import sys
24 import time
25 import os
26 import ldb
27
28 sys.path.insert(0, "bin/python")
29 import samba.tests
30 from samba.tests.samba_tool.base import SambaToolCmdTest
31 from samba import dsdb
32 from samba.dcerpc import drsuapi, misc, drsblobs, security
33 from samba.ndr import ndr_unpack, ndr_pack
34 from samba.drs_utils import drs_DsBind
35 from samba import gensec
36 from ldb import (
37     SCOPE_BASE,
38     Message,
39     FLAG_MOD_REPLACE,
40     )
41
42
43 class DrsBaseTestCase(SambaToolCmdTest):
44     """Base class implementation for all DRS python tests.
45        It is intended to provide common initialization and
46        and functionality used by all DRS tests in drs/python
47        test package. For instance, DC1 and DC2 are always used
48        to pass URLs for DCs to test against"""
49
50     def setUp(self):
51         super(DrsBaseTestCase, self).setUp()
52         creds = self.get_credentials()
53         creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
54
55         # connect to DCs
56         url_dc = samba.tests.env_get_var_value("DC1")
57         (self.ldb_dc1, self.info_dc1) = samba.tests.connect_samdb_ex(url_dc,
58                                                                      ldap_only=True)
59         url_dc = samba.tests.env_get_var_value("DC2")
60         (self.ldb_dc2, self.info_dc2) = samba.tests.connect_samdb_ex(url_dc,
61                                                                      ldap_only=True)
62         self.test_ldb_dc = self.ldb_dc1
63
64         # cache some of RootDSE props
65         self.schema_dn = self.info_dc1["schemaNamingContext"][0]
66         self.domain_dn = self.info_dc1["defaultNamingContext"][0]
67         self.config_dn = self.info_dc1["configurationNamingContext"][0]
68         self.forest_level = int(self.info_dc1["forestFunctionality"][0])
69
70         # we will need DCs DNS names for 'samba-tool drs' command
71         self.dnsname_dc1 = self.info_dc1["dnsHostName"][0]
72         self.dnsname_dc2 = self.info_dc2["dnsHostName"][0]
73
74         # for debugging the test code
75         self._debug = False
76
77     def tearDown(self):
78         super(DrsBaseTestCase, self).tearDown()
79
80     def set_test_ldb_dc(self, ldb_dc):
81         """Sets which DC's LDB we perform operations on during the test"""
82         self.test_ldb_dc = ldb_dc
83
84     def _GUID_string(self, guid):
85         return self.test_ldb_dc.schema_format_value("objectGUID", guid)
86
87     def _ldap_schemaUpdateNow(self, sam_db):
88         rec = {"dn": "",
89                "schemaUpdateNow": "1"}
90         m = Message.from_dict(sam_db, rec, FLAG_MOD_REPLACE)
91         sam_db.modify(m)
92
93     def _deleted_objects_dn(self, sam_ldb):
94         wkdn = "<WKGUID=18E2EA80684F11D2B9AA00C04F79F805,%s>" % self.domain_dn
95         res = sam_ldb.search(base=wkdn,
96                              scope=SCOPE_BASE,
97                              controls=["show_deleted:1"])
98         self.assertEquals(len(res), 1)
99         return str(res[0]["dn"])
100
101     def _lost_and_found_dn(self, sam_ldb, nc):
102         wkdn = "<WKGUID=%s,%s>" % (dsdb.DS_GUID_LOSTANDFOUND_CONTAINER, nc)
103         res = sam_ldb.search(base=wkdn,
104                              scope=SCOPE_BASE)
105         self.assertEquals(len(res), 1)
106         return str(res[0]["dn"])
107
108     def _make_obj_name(self, prefix):
109         return prefix + time.strftime("%s", time.gmtime())
110
111     def _samba_tool_cmd_list(self, drs_command):
112         # make command line credentials string
113
114         ccache_name = self.get_creds_ccache_name()
115
116         # Tunnel the command line credentials down to the
117         # subcommand to avoid a new kinit
118         cmdline_auth = "--krb5-ccache=%s" % ccache_name
119
120         # bin/samba-tool drs <drs_command> <cmdline_auth>
121         return ["drs", drs_command, cmdline_auth]
122
123     def _net_drs_replicate(self, DC, fromDC, nc_dn=None, forced=True, local=False, full_sync=False):
124         if nc_dn is None:
125             nc_dn = self.domain_dn
126         # make base command line
127         samba_tool_cmdline = self._samba_tool_cmd_list("replicate")
128         # bin/samba-tool drs replicate <Dest_DC_NAME> <Src_DC_NAME> <Naming Context>
129         samba_tool_cmdline += [DC, fromDC, nc_dn]
130
131         if forced:
132             samba_tool_cmdline += ["--sync-forced"]
133         if local:
134             samba_tool_cmdline += ["--local"]
135         if full_sync:
136             samba_tool_cmdline += ["--full-sync"]
137
138         (result, out, err) = self.runsubcmd(*samba_tool_cmdline)
139         self.assertCmdSuccess(result, out, err)
140         self.assertEquals(err,"","Shouldn't be any error messages")
141
142     def _enable_inbound_repl(self, DC):
143         # make base command line
144         samba_tool_cmd = self._samba_tool_cmd_list("options")
145         # disable replication
146         samba_tool_cmd += [DC, "--dsa-option=-DISABLE_INBOUND_REPL"]
147         (result, out, err) = self.runsubcmd(*samba_tool_cmd)
148         self.assertCmdSuccess(result, out, err)
149         self.assertEquals(err,"","Shouldn't be any error messages")
150
151     def _disable_inbound_repl(self, DC):
152         # make base command line
153         samba_tool_cmd = self._samba_tool_cmd_list("options")
154         # disable replication
155         samba_tool_cmd += [DC, "--dsa-option=+DISABLE_INBOUND_REPL"]
156         (result, out, err) = self.runsubcmd(*samba_tool_cmd)
157         self.assertCmdSuccess(result, out, err)
158         self.assertEquals(err,"","Shouldn't be any error messages")
159
160     def _enable_all_repl(self, DC):
161         self._enable_inbound_repl(DC)
162         # make base command line
163         samba_tool_cmd = self._samba_tool_cmd_list("options")
164         # enable replication
165         samba_tool_cmd += [DC, "--dsa-option=-DISABLE_OUTBOUND_REPL"]
166         (result, out, err) = self.runsubcmd(*samba_tool_cmd)
167         self.assertCmdSuccess(result, out, err)
168         self.assertEquals(err,"","Shouldn't be any error messages")
169
170     def _disable_all_repl(self, DC):
171         self._disable_inbound_repl(DC)
172         # make base command line
173         samba_tool_cmd = self._samba_tool_cmd_list("options")
174         # disable replication
175         samba_tool_cmd += [DC, "--dsa-option=+DISABLE_OUTBOUND_REPL"]
176         (result, out, err) = self.runsubcmd(*samba_tool_cmd)
177         self.assertCmdSuccess(result, out, err)
178         self.assertEquals(err,"","Shouldn't be any error messages")
179
180     def _get_highest_hwm_utdv(self, ldb_conn):
181         res = ldb_conn.search("", scope=ldb.SCOPE_BASE, attrs=["highestCommittedUSN"])
182         hwm = drsuapi.DsReplicaHighWaterMark()
183         hwm.tmp_highest_usn = long(res[0]["highestCommittedUSN"][0])
184         hwm.reserved_usn = 0
185         hwm.highest_usn = hwm.tmp_highest_usn
186
187         utdv = drsuapi.DsReplicaCursorCtrEx()
188         cursors = []
189         c1 = drsuapi.DsReplicaCursor()
190         c1.source_dsa_invocation_id = misc.GUID(ldb_conn.get_invocation_id())
191         c1.highest_usn = hwm.highest_usn
192         cursors.append(c1)
193         utdv.count = len(cursors)
194         utdv.cursors = cursors
195         return (hwm, utdv)
196
197     def _get_identifier(self, ldb_conn, dn):
198         res = ldb_conn.search(dn, scope=ldb.SCOPE_BASE,
199                 attrs=["objectGUID", "objectSid"])
200         id = drsuapi.DsReplicaObjectIdentifier()
201         id.guid = ndr_unpack(misc.GUID, res[0]['objectGUID'][0])
202         if "objectSid" in res[0]:
203             id.sid = ndr_unpack(security.dom_sid, res[0]['objectSid'][0])
204         id.dn = str(res[0].dn)
205         return id
206
207     def _get_ctr6_links(self, ctr6):
208         """
209         Unpacks the linked attributes from a DsGetNCChanges response
210         and returns them as a list.
211         """
212         ctr6_links = []
213         for lidx in range(0, ctr6.linked_attributes_count):
214             l = ctr6.linked_attributes[lidx]
215             try:
216                 target = ndr_unpack(drsuapi.DsReplicaObjectIdentifier3,
217                                     l.value.blob)
218             except:
219                 target = ndr_unpack(drsuapi.DsReplicaObjectIdentifier3Binary,
220                                     l.value.blob)
221             al = AbstractLink(l.attid, l.flags,
222                               l.identifier.guid,
223                               target.guid, target.dn)
224             ctr6_links.append(al)
225
226         return ctr6_links
227
228     def _get_ctr6_object_guids(self, ctr6):
229         """Returns all the object GUIDs in a GetNCChanges response"""
230         guid_list = []
231
232         obj = ctr6.first_object
233         for i in range(0, ctr6.object_count):
234             guid_list.append(str(obj.object.identifier.guid))
235             obj = obj.next_object
236
237         return guid_list
238
239     def _ctr6_debug(self, ctr6):
240         """
241         Displays basic info contained in a DsGetNCChanges response.
242         Having this debug code allows us to see the difference in behaviour
243         between Samba and Windows easier. Turn on the self._debug flag to see it.
244         """
245
246         if self._debug:
247             print("------------ recvd CTR6 -------------")
248
249             next_object = ctr6.first_object
250             for i in range(0, ctr6.object_count):
251                 print("Obj %d: %s %s" %(i, next_object.object.identifier.dn[:22],
252                                         next_object.object.identifier.guid))
253                 next_object = next_object.next_object
254
255             print("Linked Attributes: %d" % ctr6.linked_attributes_count)
256             ctr6_links = self._get_ctr6_links(ctr6)
257             for link in ctr6_links:
258                 print("Link Tgt %s... <-- Src %s"
259                       %(link.targetDN[:22], link.identifier))
260
261             print("HWM:     %d" %(ctr6.new_highwatermark.highest_usn))
262             print("Tmp HWM: %d" %(ctr6.new_highwatermark.tmp_highest_usn))
263             print("More data: %d" %(ctr6.more_data))
264
265     def _get_replication(self, replica_flags,
266                           drs_error=drsuapi.DRSUAPI_EXOP_ERR_NONE, drs=None, drs_handle=None,
267                           highwatermark=None, uptodateness_vector=None,
268                           more_flags=0, max_objects=133, exop=0,
269                           dest_dsa=drsuapi.DRSUAPI_DS_BIND_GUID_W2K3,
270                           source_dsa=None, invocation_id=None, nc_dn_str=None):
271         """
272         Builds a DsGetNCChanges request based on the information provided
273         and returns the response received from the DC.
274         """
275         if source_dsa is None:
276             source_dsa = self.test_ldb_dc.get_ntds_GUID()
277         if invocation_id is None:
278             invocation_id = self.test_ldb_dc.get_invocation_id()
279         if nc_dn_str is None:
280             nc_dn_str = self.test_ldb_dc.domain_dn()
281
282         if highwatermark is None:
283             if self.default_hwm is None:
284                 (highwatermark, _) = self._get_highest_hwm_utdv(self.test_ldb_dc)
285             else:
286                 highwatermark = self.default_hwm
287
288         if drs is None:
289             drs = self.drs
290         if drs_handle is None:
291             drs_handle = self.drs_handle
292
293         req10 = self._getnc_req10(dest_dsa=dest_dsa,
294                                   invocation_id=invocation_id,
295                                   nc_dn_str=nc_dn_str,
296                                   exop=exop,
297                                   max_objects=max_objects,
298                                   replica_flags=replica_flags,
299                                   more_flags=more_flags)
300         req10.highwatermark = highwatermark
301         if uptodateness_vector is not None:
302             uptodateness_vector_v1 = drsuapi.DsReplicaCursorCtrEx()
303             cursors = []
304             for i in xrange(0, uptodateness_vector.count):
305                 c = uptodateness_vector.cursors[i]
306                 c1 = drsuapi.DsReplicaCursor()
307                 c1.source_dsa_invocation_id = c.source_dsa_invocation_id
308                 c1.highest_usn = c.highest_usn
309                 cursors.append(c1)
310             uptodateness_vector_v1.count = len(cursors)
311             uptodateness_vector_v1.cursors = cursors
312             req10.uptodateness_vector = uptodateness_vector_v1
313         (level, ctr) = drs.DsGetNCChanges(drs_handle, 10, req10)
314         self._ctr6_debug(ctr)
315
316         self.assertEqual(level, 6, "expected level 6 response!")
317         self.assertEqual(ctr.source_dsa_guid, misc.GUID(source_dsa))
318         self.assertEqual(ctr.source_dsa_invocation_id, misc.GUID(invocation_id))
319         self.assertEqual(ctr.extended_ret, drs_error)
320
321         return ctr
322
323     def _check_replication(self, expected_dns, replica_flags, expected_links=[],
324                            drs_error=drsuapi.DRSUAPI_EXOP_ERR_NONE, drs=None, drs_handle=None,
325                            highwatermark=None, uptodateness_vector=None,
326                            more_flags=0, more_data=False,
327                            dn_ordered=True, links_ordered=True,
328                            max_objects=133, exop=0,
329                            dest_dsa=drsuapi.DRSUAPI_DS_BIND_GUID_W2K3,
330                            source_dsa=None, invocation_id=None, nc_dn_str=None,
331                            nc_object_count=0, nc_linked_attributes_count=0):
332         """
333         Makes sure that replication returns the specific error given.
334         """
335
336         # send a DsGetNCChanges to the DC
337         ctr6 = self._get_replication(replica_flags,
338                                      drs_error, drs, drs_handle,
339                                      highwatermark, uptodateness_vector,
340                                      more_flags, max_objects, exop, dest_dsa,
341                                      source_dsa, invocation_id, nc_dn_str)
342
343         # check the response is what we expect
344         self._check_ctr6(ctr6, expected_dns, expected_links,
345                          nc_object_count=nc_object_count, more_data=more_data,
346                          dn_ordered=dn_ordered)
347         return (ctr6.new_highwatermark, ctr6.uptodateness_vector)
348
349
350     def _get_ctr6_dn_list(self, ctr6):
351         """
352         Returns the DNs contained in a DsGetNCChanges response.
353         """
354         dn_list = []
355         next_object = ctr6.first_object
356         for i in range(0, ctr6.object_count):
357             dn_list.append(next_object.object.identifier.dn)
358             next_object = next_object.next_object
359         self.assertEqual(next_object, None)
360
361         return dn_list
362
363
364     def _check_ctr6(self, ctr6, expected_dns=[], expected_links=[],
365                     dn_ordered=True, links_ordered=True,
366                     more_data=False, nc_object_count=0,
367                     nc_linked_attributes_count=0, drs_error=0):
368         """
369         Check that a ctr6 matches the specified parameters.
370         """
371         self.assertEqual(ctr6.object_count, len(expected_dns))
372         self.assertEqual(ctr6.linked_attributes_count, len(expected_links))
373         self.assertEqual(ctr6.more_data, more_data)
374         self.assertEqual(ctr6.nc_object_count, nc_object_count)
375         self.assertEqual(ctr6.nc_linked_attributes_count, nc_linked_attributes_count)
376         self.assertEqual(ctr6.drs_error[0], drs_error)
377
378         ctr6_dns = self._get_ctr6_dn_list(ctr6)
379
380         i = 0
381         for dn in expected_dns:
382             # Expect them back in the exact same order as specified.
383             if dn_ordered:
384                 self.assertNotEqual(ctr6_dns[i], None)
385                 self.assertEqual(ctr6_dns[i], dn)
386                 i = i + 1
387             # Don't care what order
388             else:
389                 self.assertTrue(dn in ctr6_dns, "Couldn't find DN '%s' anywhere in ctr6 response." % dn)
390
391         # Extract the links from the response
392         ctr6_links = self._get_ctr6_links(ctr6)
393         expected_links.sort()
394
395         lidx = 0
396         for el in expected_links:
397             if links_ordered:
398                 self.assertEqual(el, ctr6_links[lidx])
399                 lidx += 1
400             else:
401                 self.assertTrue(el in ctr6_links, "Couldn't find link '%s' anywhere in ctr6 response." % el)
402
403     def _exop_req8(self, dest_dsa, invocation_id, nc_dn_str, exop,
404                    replica_flags=0, max_objects=0, partial_attribute_set=None,
405                    partial_attribute_set_ex=None, mapping_ctr=None):
406         req8 = drsuapi.DsGetNCChangesRequest8()
407
408         req8.destination_dsa_guid = misc.GUID(dest_dsa) if dest_dsa else misc.GUID()
409         req8.source_dsa_invocation_id = misc.GUID(invocation_id)
410         req8.naming_context = drsuapi.DsReplicaObjectIdentifier()
411         req8.naming_context.dn = unicode(nc_dn_str)
412         req8.highwatermark = drsuapi.DsReplicaHighWaterMark()
413         req8.highwatermark.tmp_highest_usn = 0
414         req8.highwatermark.reserved_usn = 0
415         req8.highwatermark.highest_usn = 0
416         req8.uptodateness_vector = None
417         req8.replica_flags = replica_flags
418         req8.max_object_count = max_objects
419         req8.max_ndr_size = 402116
420         req8.extended_op = exop
421         req8.fsmo_info = 0
422         req8.partial_attribute_set = partial_attribute_set
423         req8.partial_attribute_set_ex = partial_attribute_set_ex
424         if mapping_ctr:
425             req8.mapping_ctr = mapping_ctr
426         else:
427             req8.mapping_ctr.num_mappings = 0
428             req8.mapping_ctr.mappings = None
429
430         return req8
431
432     def _getnc_req10(self, dest_dsa, invocation_id, nc_dn_str, exop,
433                      replica_flags=0, max_objects=0, partial_attribute_set=None,
434                      partial_attribute_set_ex=None, mapping_ctr=None,
435                      more_flags=0):
436         req10 = drsuapi.DsGetNCChangesRequest10()
437
438         req10.destination_dsa_guid = misc.GUID(dest_dsa) if dest_dsa else misc.GUID()
439         req10.source_dsa_invocation_id = misc.GUID(invocation_id)
440         req10.naming_context = drsuapi.DsReplicaObjectIdentifier()
441         req10.naming_context.dn = unicode(nc_dn_str)
442         req10.highwatermark = drsuapi.DsReplicaHighWaterMark()
443         req10.highwatermark.tmp_highest_usn = 0
444         req10.highwatermark.reserved_usn = 0
445         req10.highwatermark.highest_usn = 0
446         req10.uptodateness_vector = None
447         req10.replica_flags = replica_flags
448         req10.max_object_count = max_objects
449         req10.max_ndr_size = 402116
450         req10.extended_op = exop
451         req10.fsmo_info = 0
452         req10.partial_attribute_set = partial_attribute_set
453         req10.partial_attribute_set_ex = partial_attribute_set_ex
454         if mapping_ctr:
455             req10.mapping_ctr = mapping_ctr
456         else:
457             req10.mapping_ctr.num_mappings = 0
458             req10.mapping_ctr.mappings = None
459         req10.more_flags = more_flags
460
461         return req10
462
463     def _ds_bind(self, server_name, creds=None):
464         binding_str = "ncacn_ip_tcp:%s[seal]" % server_name
465
466         if creds is None:
467             creds = self.get_credentials()
468         drs = drsuapi.drsuapi(binding_str, self.get_loadparm(), creds)
469         (drs_handle, supported_extensions) = drs_DsBind(drs)
470         return (drs, drs_handle)
471
472     def get_partial_attribute_set(self, attids=[drsuapi.DRSUAPI_ATTID_objectClass]):
473         partial_attribute_set = drsuapi.DsPartialAttributeSet()
474         partial_attribute_set.attids = attids
475         partial_attribute_set.num_attids = len(attids)
476         return partial_attribute_set
477
478
479
480 class AbstractLink:
481     def __init__(self, attid, flags, identifier, targetGUID,
482                  targetDN=""):
483         self.attid = attid
484         self.flags = flags
485         self.identifier = str(identifier)
486         self.selfGUID_blob = ndr_pack(identifier)
487         self.targetGUID = str(targetGUID)
488         self.targetGUID_blob = ndr_pack(targetGUID)
489         self.targetDN = targetDN
490
491     def __repr__(self):
492         return "AbstractLink(0x%08x, 0x%08x, %s, %s)" % (
493                 self.attid, self.flags, self.identifier, self.targetGUID)
494
495     def __internal_cmp__(self, other, verbose=False):
496         """See CompareLinks() in MS-DRSR section 4.1.10.5.17"""
497         if not isinstance(other, AbstractLink):
498             if verbose:
499                 print "AbstractLink.__internal_cmp__(%r, %r) => wrong type" % (self, other)
500             return NotImplemented
501
502         c = cmp(self.selfGUID_blob, other.selfGUID_blob)
503         if c != 0:
504             if verbose:
505                 print "AbstractLink.__internal_cmp__(%r, %r) => %d different identifier" % (self, other, c)
506             return c
507
508         c = other.attid - self.attid
509         if c != 0:
510             if verbose:
511                 print "AbstractLink.__internal_cmp__(%r, %r) => %d different attid" % (self, other, c)
512             return c
513
514         self_active = self.flags & drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE
515         other_active = other.flags & drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE
516
517         c = self_active - other_active
518         if c != 0:
519             if verbose:
520                 print "AbstractLink.__internal_cmp__(%r, %r) => %d different FLAG_ACTIVE" % (self, other, c)
521             return c
522
523         c = cmp(self.targetGUID_blob, other.targetGUID_blob)
524         if c != 0:
525             if verbose:
526                 print "AbstractLink.__internal_cmp__(%r, %r) => %d different target" % (self, other, c)
527             return c
528
529         c = self.flags - other.flags
530         if c != 0:
531             if verbose:
532                 print "AbstractLink.__internal_cmp__(%r, %r) => %d different flags" % (self, other, c)
533             return c
534
535         return 0
536
537     def __lt__(self, other):
538         c = self.__internal_cmp__(other)
539         if c == NotImplemented:
540             return NotImplemented
541         if c < 0:
542             return True
543         return False
544
545     def __le__(self, other):
546         c = self.__internal_cmp__(other)
547         if c == NotImplemented:
548             return NotImplemented
549         if c <= 0:
550             return True
551         return False
552
553     def __eq__(self, other):
554         c = self.__internal_cmp__(other, verbose=True)
555         if c == NotImplemented:
556             return NotImplemented
557         if c == 0:
558             return True
559         return False
560
561     def __ne__(self, other):
562         c = self.__internal_cmp__(other)
563         if c == NotImplemented:
564             return NotImplemented
565         if c != 0:
566             return True
567         return False
568
569     def __gt__(self, other):
570         c = self.__internal_cmp__(other)
571         if c == NotImplemented:
572             return NotImplemented
573         if c > 0:
574             return True
575         return False
576
577     def __ge__(self, other):
578         c = self.__internal_cmp__(other)
579         if c == NotImplemented:
580             return NotImplemented
581         if c >= 0:
582             return True
583         return False
584
585     def __hash__(self):
586         return hash((self.attid, self.flags, self.identifier, self.targetGUID))