1#!/usr/bin/env python2 2# -*- coding: utf-8 -*- 3# BAREOS - Backup Archiving REcovery Open Sourced 4# 5# Copyright (C) 2020-2020 Bareos GmbH & Co. KG 6# 7# This program is Free Software; you can redistribute it and/or 8# modify it under the terms of version three of the GNU Affero General Public 9# License as published by the Free Software Foundation and included 10# in the file LICENSE. 11# 12# This program is distributed in the hope that it will be useful, but 13# WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15# Affero General Public License for more details. 16# 17# You should have received a copy of the GNU Affero General Public License 18# along with this program; if not, write to the Free Software 19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 20# 02110-1301, USA. 21 22from __future__ import division, absolute_import, print_function, unicode_literals 23import io 24import ldap 25import ldap.modlist 26import ldif 27import sys 28import hashlib 29from argparse import ArgumentParser 30from ldif import LDIFWriter 31 32 33def _safe_encode(data): 34 if isinstance(data, unicode): 35 return data.encode("utf-8") 36 return data 37 38 39class BytesLDIFRecordList(ldif.LDIFRecordList): 40 """Simple encoding wrapper for LDIFRecordList that converts keys to UTF-8""" 41 42 def _next_key_and_value(self): 43 # we do not descend from object, so we cannot use super() 44 k, v = ldif.LDIFRecordList._next_key_and_value(self) 45 return k.encode("utf-8"), v 46 47 48def ldap_connect(address, binddn, password): 49 try: 50 conn = ldap.initialize("ldap://%s" % address, bytes_mode=True) 51 except TypeError: 52 conn = ldap.initialize("ldap://%s" % address) 53 54 conn.set_option(ldap.OPT_REFERRALS, 0) 55 conn.simple_bind_s(binddn, password) 56 57 return conn 58 59 60def ldap_create_or_fail(conn, dn, modlist): 61 """ 62 create an object dn with the attributes from modlist dict 63 """ 64 try: 65 conn.add_s(dn, ldap.modlist.addModlist(modlist)) 66 except ldap.ALREADY_EXISTS: 67 print("Object '%s' already exists." % dn, file=sys.stderr) 68 sys.exit(1) 69 70 71def action_clean(conn, basedn): 72 """ 73 Clean up all objects in our subtrees (ldap-variant of "rm -rf"). 74 75 find the dns ob all objects below our bases and remove them ordered from 76 longest to shortest dn, so we remove parent objects later 77 """ 78 79 for subtree_dn in [b"ou=backup,%s" % basedn, b"ou=restore,%s" % basedn]: 80 try: 81 for dn in sorted( 82 map( 83 lambda x: x[0], 84 conn.search_s(subtree_dn, ldap.SCOPE_SUBTREE, attrsonly=1), 85 ), 86 key=len, 87 reverse=True, 88 ): 89 conn.delete_s(dn) 90 except ldap.NO_SUCH_OBJECT: 91 # if the top object doesn't exist, there's nothing to remove 92 pass 93 94 95def action_populate(conn, basedn): 96 """Populate our backup data""" 97 ldap_create_or_fail( 98 conn, 99 b"ou=backup,%s" % basedn, 100 {b"objectClass": [b"organizationalUnit"], b"ou": [b"restore"]}, 101 ) 102 103 ldap_create_or_fail( 104 conn, 105 b"cn=No JPEG,ou=backup,%s" % basedn, 106 { 107 b"objectClass": [b"inetOrgPerson", b"posixAccount", b"shadowAccount"], 108 b"uid": [b"njpeg"], 109 b"sn": [b"JPEG"], 110 b"givenName": [b"No"], 111 b"cn": [b"No JPEG"], 112 b"displayName": [b"No JPEG"], 113 b"uidNumber": [b"1000"], 114 b"gidNumber": [b"1000"], 115 b"loginShell": [b"/bin/bash"], 116 b"homeDirectory": [b"/home/njpeg"], 117 }, 118 ) 119 120 ldap_create_or_fail( 121 conn, 122 b"cn=Small JPEG,ou=backup,%s" % basedn, 123 { 124 b"objectClass": [b"inetOrgPerson", b"posixAccount", b"shadowAccount"], 125 b"uid": [b"sjpeg"], 126 b"sn": [b"JPEG"], 127 b"givenName": [b"Small"], 128 b"cn": [b"Small JPEG"], 129 b"displayName": [b"Small JPEG"], 130 b"uidNumber": [b"1001"], 131 b"gidNumber": [b"1000"], 132 b"loginShell": [b"/bin/bash"], 133 b"homeDirectory": [b"/home/sjpeg"], 134 b"jpegPhoto": open("image-small.jpg", "rb").read(), 135 }, 136 ) 137 138 ldap_create_or_fail( 139 conn, 140 b"cn=Medium JPEG,ou=backup,%s" % basedn, 141 { 142 b"objectClass": [b"inetOrgPerson", b"posixAccount", b"shadowAccount"], 143 b"uid": [b"mjpeg"], 144 b"sn": [b"JPEG"], 145 b"givenName": [b"Medium"], 146 b"cn": [b"Medium JPEG"], 147 b"displayName": [b"Medium JPEG"], 148 b"uidNumber": [b"1002"], 149 b"gidNumber": [b"1000"], 150 b"loginShell": [b"/bin/bash"], 151 b"homeDirectory": [b"/home/mjpeg"], 152 b"jpegPhoto": open("image-medium.jpg", "rb").read(), 153 }, 154 ) 155 156 ldap_create_or_fail( 157 conn, 158 b"cn=Large JPEG,ou=backup,%s" % basedn, 159 { 160 b"objectClass": [b"inetOrgPerson", b"posixAccount", b"shadowAccount"], 161 b"uid": [b"ljpeg"], 162 b"sn": [b"JPEG"], 163 b"givenName": [b"Large"], 164 b"cn": [b"Large JPEG"], 165 b"displayName": [b"Large JPEG"], 166 b"uidNumber": [b"1003"], 167 b"gidNumber": [b"1000"], 168 b"loginShell": [b"/bin/bash"], 169 b"homeDirectory": [b"/home/ljpeg"], 170 b"jpegPhoto": open("image-large.jpg", "rb").read(), 171 }, 172 ) 173 174 ldap_create_or_fail( 175 conn, 176 b"o=Bareos GmbH & Co. KG,ou=backup,%s" % basedn, 177 {b"objectClass": [b"top", b"organization"], b"o": [b"Bareos GmbH & Co. KG"]}, 178 ) 179 180 ldap_create_or_fail( 181 conn, 182 b"ou=automount,ou=backup,%s" % basedn, 183 {b"objectClass": [b"top", b"organizationalUnit"], b"ou": [b"automount"]}, 184 ) 185 186 # # Objects with / in the DN are currently not supported 187 # ldap_create_or_fail( 188 # conn, 189 # "cn=/home,ou=automount,ou=backup,%s" % basedn, 190 # { 191 # "objectClass": [b"top", b"person"], 192 # "cn": [b"/home"], 193 # "sn": [b"Automount objects don't have a surname"], 194 # }, 195 # ) 196 197 ldap_create_or_fail( 198 conn, 199 b"ou=weird-names,ou=backup,%s" % basedn, 200 {b"objectClass": [b"top", b"organizationalUnit"], b"ou": [b"weird-names"]}, 201 ) 202 203 for ou in [ 204 b" leading-space", 205 b"#leading-hash", 206 b"space in middle", 207 b"trailing-space ", 208 b"with\nnewline", 209 b"with,comma", 210 b'with"quotes"', 211 b"with\\backslash", 212 b"with+plus", 213 b"with#hash", 214 b"with;semicolon", 215 b"with<less-than", 216 b"with=equals", 217 b"with>greater-than", 218 ]: 219 ldap_create_or_fail( 220 conn, 221 b"ou=%s,ou=weird-names,ou=backup,%s" 222 % (ldap.dn.escape_dn_chars(ou), basedn), 223 {b"objectClass": [b"top", b"organizationalUnit"], b"ou": [ou]}, 224 ) 225 226 # creating the DN using the normal method wouldn't work, so we create a 227 # temporary LDIF and parse that. 228 ldif_data = io.BytesIO() 229 ldif_data.write(b"dn: ou=böses encoding,ou=weird-names,ou=backup,") 230 ldif_data.write(b"%s\n" % basedn.encode("ascii")) 231 ldif_data.write(b"objectClass: top\n") 232 ldif_data.write(b"objectClass: organizationalUnit\n") 233 ldif_data.write(b"ou: böses encoding\n") 234 ldif_data.seek(0) 235 236 ldif_parser = BytesLDIFRecordList(ldif_data, max_entries=1) 237 ldif_parser.parse() 238 dn, entry = ldif_parser.all_records[0] 239 ldif_data.close() 240 241 ldap_create_or_fail(conn, _safe_encode(dn), entry) 242 243 244def abbrev_value(v): 245 """Abbreviate long values for readable LDIF output""" 246 length = len(v) 247 if length > 80: 248 digest = hashlib.sha1(v).hexdigest() 249 return "BLOB len:%d sha1:%s" % (length, digest) 250 return v 251 252 253def action_dump(conn, basedn, shorten=True, rewrite_dn=True): 254 writer = LDIFWriter(sys.stdout) 255 try: 256 for dn, attrs in conn.search_s(basedn, ldap.SCOPE_SUBTREE): 257 if rewrite_dn: 258 dn = ( 259 dn.decode("utf-8") 260 .replace(basedn, "dc=unified,dc=base,dc=dn") 261 .encode("utf-8") 262 ) 263 if shorten: 264 attrs = { 265 k: [abbrev_value(v) for v in vals] for k, vals in attrs.iteritems() 266 } 267 try: 268 writer.unparse(dn, attrs) 269 except UnicodeDecodeError: 270 writer.unparse(dn.decode("utf-8"), attrs) 271 except ldap.NO_SUCH_OBJECT: 272 print("No object '%s' in directory." % basedn, file=sys.stderr) 273 sys.exit(1) 274 275 276if __name__ == "__main__": 277 parser = ArgumentParser(description="Tool to create, remove LDAP test data") 278 parser.add_argument( 279 "--clean", action="store_true", help="remove data from LDAP server" 280 ) 281 parser.add_argument( 282 "--populate", action="store_true", help="populate LDAP server with data" 283 ) 284 parser.add_argument( 285 "--dump-backup", 286 action="store_true", 287 help="print representation of backup subtree", 288 ) 289 parser.add_argument( 290 "--dump-restore", 291 action="store_true", 292 help="print representation of restore subtree", 293 ) 294 parser.add_argument( 295 "--full-value", 296 action="store_true", 297 help="disable shortening of large values during dump", 298 ) 299 parser.add_argument( 300 "--real-dn", action="store_true", help="disable rewriting of DN during dump" 301 ) 302 parser.add_argument( 303 "--address", "--host", default="localhost", help="LDAP server address" 304 ) 305 parser.add_argument( 306 "--basedn", "-b", default=b"dc=example,dc=org", help="LDAP base dn" 307 ) 308 parser.add_argument( 309 "--binddn", "-D", default=b"cn=admin,dc=example,dc=org", help="LDAP bind dn" 310 ) 311 parser.add_argument("--password", "-w", default=b"admin", help="LDAP password") 312 313 args = parser.parse_args() 314 315 if ( 316 not args.clean 317 and not args.populate 318 and not args.dump_backup 319 and not args.dump_restore 320 ): 321 print("please select at least one action", file=sys.stderr) 322 sys.exit(1) 323 324 conn = ldap_connect(args.address, args.binddn, args.password) 325 if args.clean: 326 action_clean(conn, args.basedn) 327 if args.populate: 328 action_populate(conn, args.basedn) 329 if args.dump_backup: 330 action_dump( 331 conn, 332 b"ou=backup,%s" % args.basedn, 333 shorten=not args.full_value, 334 rewrite_dn=not args.real_dn, 335 ) 336 if args.dump_restore: 337 action_dump( 338 conn, 339 b"ou=restore,%s" % args.basedn, 340 shorten=not args.full_value, 341 rewrite_dn=not args.real_dn, 342 ) 343