CVE-2023-3347: CI: add a test for server-side mandatory signing
[Samba.git] / python / samba / uptodateness.py
blob49c984a5828bfc352372eedce79626eeaa0c2479
1 # Uptodateness utils
3 # Copyright (C) Andrew Bartlett 2015, 2018
4 # Copyright (C) Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
5 # Copyright (C) Joe Guo <joeg@catalyst.net.nz>
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 import sys
21 import time
23 from ldb import SCOPE_BASE, LdbError
25 from samba import nttime2unix, dsdb
26 from samba.netcmd import CommandError
27 from samba.samdb import SamDB
28 from samba.kcc import KCC
31 def get_kcc_and_dsas(url, lp, creds):
32 """Get a readonly KCC object and the list of DSAs it knows about."""
33 unix_now = int(time.time())
34 kcc = KCC(unix_now, readonly=True)
35 kcc.load_samdb(url, lp, creds)
37 dsa_list = kcc.list_dsas()
38 dsas = set(dsa_list)
39 if len(dsas) != len(dsa_list):
40 print("There seem to be duplicate dsas", file=sys.stderr)
42 return kcc, dsas
45 def get_partition_maps(samdb):
46 """Generate dictionaries mapping short partition names to the
47 appropriate DNs."""
48 base_dn = samdb.domain_dn()
49 short_to_long = {
50 "DOMAIN": base_dn,
51 "CONFIGURATION": str(samdb.get_config_basedn()),
52 "SCHEMA": "CN=Schema,%s" % samdb.get_config_basedn(),
53 "DNSDOMAIN": "DC=DomainDnsZones,%s" % base_dn,
54 "DNSFOREST": "DC=ForestDnsZones,%s" % base_dn
57 long_to_short = {}
58 for s, l in short_to_long.items():
59 long_to_short[l] = s
61 return short_to_long, long_to_short
64 def get_partition(samdb, part):
65 # Allow people to say "--partition=DOMAIN" rather than
66 # "--partition=DC=blah,DC=..."
67 if part is not None:
68 short_partitions, long_partitions = get_partition_maps(samdb)
69 part = short_partitions.get(part.upper(), part)
70 if part not in long_partitions:
71 raise CommandError("unknown partition %s" % part)
72 return part
75 def get_utdv(samdb, dn):
76 """This finds the uptodateness vector in the database."""
77 cursors = []
78 config_dn = samdb.get_config_basedn()
79 for c in dsdb._dsdb_load_udv_v2(samdb, dn):
80 inv_id = str(c.source_dsa_invocation_id)
81 res = samdb.search(base=config_dn,
82 expression=("(&(invocationId=%s)"
83 "(objectClass=nTDSDSA))" % inv_id),
84 attrs=["distinguishedName", "invocationId"])
85 try:
86 settings_dn = str(res[0]["distinguishedName"][0])
87 prefix, dsa_dn = settings_dn.split(',', 1)
88 except IndexError as e:
89 continue
90 if prefix != 'CN=NTDS Settings':
91 raise CommandError("Expected NTDS Settings DN, got %s" %
92 settings_dn)
94 cursors.append((dsa_dn,
95 inv_id,
96 int(c.highest_usn),
97 nttime2unix(c.last_sync_success)))
98 return cursors
101 def get_own_cursor(samdb):
102 res = samdb.search(base="",
103 scope=SCOPE_BASE,
104 attrs=["highestCommittedUSN"])
105 usn = int(res[0]["highestCommittedUSN"][0])
106 now = int(time.time())
107 return (usn, now)
110 def get_utdv_edges(local_kcc, dsas, part_dn, lp, creds):
111 # we talk to each remote and make a matrix of the vectors
112 # for each partition
113 # normalise by oldest
114 utdv_edges = {}
115 for dsa_dn in dsas:
116 res = local_kcc.samdb.search(dsa_dn,
117 scope=SCOPE_BASE,
118 attrs=["dNSHostName"])
119 ldap_url = "ldap://%s" % res[0]["dNSHostName"][0]
120 try:
121 samdb = SamDB(url=ldap_url, credentials=creds, lp=lp)
122 cursors = get_utdv(samdb, part_dn)
123 own_usn, own_time = get_own_cursor(samdb)
124 remotes = {dsa_dn: own_usn}
125 for dn, guid, usn, t in cursors:
126 remotes[dn] = usn
127 except LdbError as e:
128 print("Could not contact %s (%s)" % (ldap_url, e),
129 file=sys.stderr)
130 continue
131 utdv_edges[dsa_dn] = remotes
132 return utdv_edges
135 def get_utdv_distances(utdv_edges, dsas):
136 distances = {}
137 for dn1 in dsas:
138 try:
139 peak = utdv_edges[dn1][dn1]
140 except KeyError as e:
141 peak = 0
142 d = {}
143 distances[dn1] = d
144 for dn2 in dsas:
145 if dn2 in utdv_edges:
146 if dn1 in utdv_edges[dn2]:
147 dist = peak - utdv_edges[dn2][dn1]
148 d[dn2] = dist
149 else:
150 print(f"Missing dn {dn1} from UTD vector for dsa {dn2}",
151 file=sys.stderr)
152 else:
153 print("missing dn %s from UTD vector list" % dn2,
154 file=sys.stderr)
155 return distances
158 def get_utdv_max_distance(distances):
159 max_distance = 0
160 for vector in distances.values():
161 for distance in vector.values():
162 max_distance = max(max_distance, distance)
163 return max_distance
166 def get_utdv_summary(distances, filters=None):
167 maximum = failure = 0
168 median = 0.0 # could be average of 2 median values
169 values = []
170 # put all values into a list, exclude self to self ones
171 for dn_outer, vector in distances.items():
172 for dn_inner, distance in vector.items():
173 if dn_outer != dn_inner:
174 values.append(distance)
176 if values:
177 values.sort()
178 maximum = values[-1]
179 length = len(values)
180 if length % 2 == 0:
181 index = length//2 - 1
182 median = (values[index] + values[index+1])/2.0
183 median = round(median, 1) # keep only 1 decimal digit like 2.5
184 else:
185 index = (length - 1)//2
186 median = values[index]
187 median = float(median) # ensure median is always a float like 1.0
188 # if value not exist, that's a failure
189 expected_length = len(distances) * (len(distances) - 1)
190 failure = expected_length - length
192 summary = {
193 'maximum': maximum,
194 'median': median,
195 'failure': failure,
198 if filters:
199 return {key: summary[key] for key in filters}
200 else:
201 return summary