python/samba3: import passdb in the manner it is used
[amitay/samba.git] / python / samba / ntacls.py
1 # Unix SMB/CIFS implementation.
2 # Copyright (C) Matthieu Patou <mat@matws.net> 2009-2010
3 #
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 #
18
19 from __future__ import print_function
20 """NT Acls."""
21
22
23 import os
24 import tarfile
25 import tempfile
26 import shutil
27
28 import samba.xattr_native
29 import samba.xattr_tdb
30 import samba.posix_eadb
31 from samba.samba3 import param as s3param
32 from samba.dcerpc import security, xattr, idmap
33 from samba.ndr import ndr_pack, ndr_unpack
34 from samba.samba3 import smbd
35 from samba import smb
36
37 # don't include volumes
38 SMB_FILE_ATTRIBUTE_FLAGS = smb.FILE_ATTRIBUTE_SYSTEM | \
39                            smb.FILE_ATTRIBUTE_DIRECTORY | \
40                            smb.FILE_ATTRIBUTE_ARCHIVE | \
41                            smb.FILE_ATTRIBUTE_HIDDEN
42
43
44 SECURITY_SECINFO_FLAGS = security.SECINFO_OWNER | \
45                          security.SECINFO_GROUP | \
46                          security.SECINFO_DACL  | \
47                          security.SECINFO_SACL
48
49
50 # SEC_FLAG_SYSTEM_SECURITY is required otherwise get Access Denied
51 SECURITY_SEC_FLAGS = security.SEC_FLAG_SYSTEM_SECURITY | \
52                      security.SEC_FLAG_MAXIMUM_ALLOWED
53
54
55 class XattrBackendError(Exception):
56     """A generic xattr backend error."""
57
58
59 def checkset_backend(lp, backend, eadbfile):
60     '''return the path to the eadb, or None'''
61     if backend is None:
62         xattr_tdb = lp.get("xattr_tdb:file")
63         if xattr_tdb is not None:
64             return (samba.xattr_tdb, lp.get("xattr_tdb:file"))
65         posix_eadb = lp.get("posix:eadb")
66         if posix_eadb is not None:
67             return (samba.posix_eadb, lp.get("posix:eadb"))
68         return (None, None)
69     elif backend == "native":
70         return (None, None)
71     elif backend == "eadb":
72         if eadbfile is not None:
73             return (samba.posix_eadb, eadbfile)
74         else:
75             return (samba.posix_eadb, os.path.abspath(os.path.join(lp.get("private dir"), "eadb.tdb")))
76     elif backend == "tdb":
77         if eadbfile is not None:
78             return (samba.xattr_tdb, eadbfile)
79         else:
80             return (samba.xattr_tdb, os.path.abspath(os.path.join(lp.get("state dir"), "xattr.tdb")))
81     else:
82         raise XattrBackendError("Invalid xattr backend choice %s" % backend)
83
84
85 def getdosinfo(lp, file):
86     try:
87         attribute = samba.xattr_native.wrap_getxattr(file,
88                                                      xattr.XATTR_DOSATTRIB_NAME_S3)
89     except Exception:
90         return
91
92     return ndr_unpack(xattr.DOSATTRIB, attribute)
93
94
95 def getntacl(lp,
96              file,
97              backend=None,
98              eadbfile=None,
99              direct_db_access=True,
100              service=None,
101              session_info=None):
102     if direct_db_access:
103         (backend_obj, dbname) = checkset_backend(lp, backend, eadbfile)
104         if dbname is not None:
105             try:
106                 attribute = backend_obj.wrap_getxattr(dbname, file,
107                                                       xattr.XATTR_NTACL_NAME)
108             except Exception:
109                 # FIXME: Don't catch all exceptions, just those related to opening
110                 # xattrdb
111                 print("Fail to open %s" % dbname)
112                 attribute = samba.xattr_native.wrap_getxattr(file,
113                                                              xattr.XATTR_NTACL_NAME)
114         else:
115             attribute = samba.xattr_native.wrap_getxattr(file,
116                                                          xattr.XATTR_NTACL_NAME)
117         ntacl = ndr_unpack(xattr.NTACL, attribute)
118         if ntacl.version == 1:
119             return ntacl.info
120         elif ntacl.version == 2:
121             return ntacl.info.sd
122         elif ntacl.version == 3:
123             return ntacl.info.sd
124         elif ntacl.version == 4:
125             return ntacl.info.sd
126     else:
127         return smbd.get_nt_acl(file,
128                                SECURITY_SECINFO_FLAGS,
129                                service=service,
130                                session_info=session_info)
131
132
133 def setntacl(lp, file, sddl, domsid,
134              backend=None, eadbfile=None,
135              use_ntvfs=True, skip_invalid_chown=False,
136              passdb=None, service=None, session_info=None):
137     """
138     A wrapper for smbd set_nt_acl api.
139
140     Args:
141         lp (LoadParam): load param from conf
142         file (str): a path to file or dir
143         sddl (str): ntacl sddl string
144         service (str): name of share service, e.g.: sysvol
145         session_info (auth_session_info): session info for authentication
146
147     Note:
148         Get `session_info` with `samba.auth.user_session`, do not use the
149         `admin_session` api.
150
151     Returns:
152         None
153     """
154
155     assert(isinstance(domsid, str) or isinstance(domsid, security.dom_sid))
156     if isinstance(domsid, str):
157         sid = security.dom_sid(domsid)
158     elif isinstance(domsid, security.dom_sid):
159         sid = domsid
160         domsid = str(sid)
161
162     assert(isinstance(sddl, str) or isinstance(sddl, security.descriptor))
163     if isinstance(sddl, str):
164         sd = security.descriptor.from_sddl(sddl, sid)
165     elif isinstance(sddl, security.descriptor):
166         sd = sddl
167         sddl = sd.as_sddl(sid)
168
169     if not use_ntvfs and skip_invalid_chown:
170         # Check if the owner can be resolved as a UID
171         (owner_id, owner_type) = passdb.sid_to_id(sd.owner_sid)
172         if ((owner_type != idmap.ID_TYPE_UID) and (owner_type != idmap.ID_TYPE_BOTH)):
173             # Check if this particular owner SID was domain admins,
174             # because we special-case this as mapping to
175             # 'administrator' instead.
176             if sd.owner_sid == security.dom_sid("%s-%d" % (domsid, security.DOMAIN_RID_ADMINS)):
177                 administrator = security.dom_sid("%s-%d" % (domsid, security.DOMAIN_RID_ADMINISTRATOR))
178                 (admin_id, admin_type) = passdb.sid_to_id(administrator)
179
180                 # Confirm we have a UID for administrator
181                 if ((admin_type == idmap.ID_TYPE_UID) or (admin_type == idmap.ID_TYPE_BOTH)):
182
183                     # Set it, changing the owner to 'administrator' rather than domain admins
184                     sd2 = sd
185                     sd2.owner_sid = administrator
186
187                     smbd.set_nt_acl(
188                         file, SECURITY_SECINFO_FLAGS, sd2,
189                         service=service, session_info=session_info)
190
191                     # and then set an NTVFS ACL (which does not set the posix ACL) to pretend the owner really was set
192                     use_ntvfs = True
193                 else:
194                     raise XattrBackendError("Unable to find UID for domain administrator %s, got id %d of type %d" % (administrator, admin_id, admin_type))
195             else:
196                 # For all other owning users, reset the owner to root
197                 # and then set the ACL without changing the owner
198                 #
199                 # This won't work in test environments, as it tries a real (rather than xattr-based fake) chown
200
201                 os.chown(file, 0, 0)
202                 smbd.set_nt_acl(
203                     file,
204                     security.SECINFO_GROUP |
205                     security.SECINFO_DACL |
206                     security.SECINFO_SACL,
207                     sd, service=service, session_info=session_info)
208
209     if use_ntvfs:
210         (backend_obj, dbname) = checkset_backend(lp, backend, eadbfile)
211         ntacl = xattr.NTACL()
212         ntacl.version = 1
213         ntacl.info = sd
214         if dbname is not None:
215             try:
216                 backend_obj.wrap_setxattr(dbname,
217                                           file, xattr.XATTR_NTACL_NAME, ndr_pack(ntacl))
218             except Exception:
219                 # FIXME: Don't catch all exceptions, just those related to opening
220                 # xattrdb
221                 print("Fail to open %s" % dbname)
222                 samba.xattr_native.wrap_setxattr(file, xattr.XATTR_NTACL_NAME,
223                                                  ndr_pack(ntacl))
224         else:
225             samba.xattr_native.wrap_setxattr(file, xattr.XATTR_NTACL_NAME,
226                                              ndr_pack(ntacl))
227     else:
228         smbd.set_nt_acl(
229             file, SECURITY_SECINFO_FLAGS, sd,
230             service=service, session_info=session_info)
231
232
233 def ldapmask2filemask(ldm):
234     """Takes the access mask of a DS ACE and transform them in a File ACE mask.
235     """
236     RIGHT_DS_CREATE_CHILD     = 0x00000001
237     RIGHT_DS_DELETE_CHILD     = 0x00000002
238     RIGHT_DS_LIST_CONTENTS    = 0x00000004
239     ACTRL_DS_SELF             = 0x00000008
240     RIGHT_DS_READ_PROPERTY    = 0x00000010
241     RIGHT_DS_WRITE_PROPERTY   = 0x00000020
242     RIGHT_DS_DELETE_TREE      = 0x00000040
243     RIGHT_DS_LIST_OBJECT      = 0x00000080
244     RIGHT_DS_CONTROL_ACCESS   = 0x00000100
245     FILE_READ_DATA            = 0x0001
246     FILE_LIST_DIRECTORY       = 0x0001
247     FILE_WRITE_DATA           = 0x0002
248     FILE_ADD_FILE             = 0x0002
249     FILE_APPEND_DATA          = 0x0004
250     FILE_ADD_SUBDIRECTORY     = 0x0004
251     FILE_CREATE_PIPE_INSTANCE = 0x0004
252     FILE_READ_EA              = 0x0008
253     FILE_WRITE_EA             = 0x0010
254     FILE_EXECUTE              = 0x0020
255     FILE_TRAVERSE             = 0x0020
256     FILE_DELETE_CHILD         = 0x0040
257     FILE_READ_ATTRIBUTES      = 0x0080
258     FILE_WRITE_ATTRIBUTES     = 0x0100
259     DELETE                    = 0x00010000
260     READ_CONTROL              = 0x00020000
261     WRITE_DAC                 = 0x00040000
262     WRITE_OWNER               = 0x00080000
263     SYNCHRONIZE               = 0x00100000
264     STANDARD_RIGHTS_ALL       = 0x001F0000
265
266     filemask = ldm & STANDARD_RIGHTS_ALL
267
268     if (ldm & RIGHT_DS_READ_PROPERTY) and (ldm & RIGHT_DS_LIST_CONTENTS):
269         filemask = filemask | (SYNCHRONIZE | FILE_LIST_DIRECTORY |
270                                FILE_READ_ATTRIBUTES | FILE_READ_EA |
271                                FILE_READ_DATA | FILE_EXECUTE)
272
273     if ldm & RIGHT_DS_WRITE_PROPERTY:
274         filemask = filemask | (SYNCHRONIZE | FILE_WRITE_DATA |
275                                FILE_APPEND_DATA | FILE_WRITE_EA |
276                                FILE_WRITE_ATTRIBUTES | FILE_ADD_FILE |
277                                FILE_ADD_SUBDIRECTORY)
278
279     if ldm & RIGHT_DS_CREATE_CHILD:
280         filemask = filemask | (FILE_ADD_SUBDIRECTORY | FILE_ADD_FILE)
281
282     if ldm & RIGHT_DS_DELETE_CHILD:
283         filemask = filemask | FILE_DELETE_CHILD
284
285     return filemask
286
287
288 def dsacl2fsacl(dssddl, sid, as_sddl=True):
289     """
290
291     This function takes an the SDDL representation of a DS
292     ACL and return the SDDL representation of this ACL adapted
293     for files. It's used for Policy object provision
294     """
295     ref = security.descriptor.from_sddl(dssddl, sid)
296     fdescr = security.descriptor()
297     fdescr.owner_sid = ref.owner_sid
298     fdescr.group_sid = ref.group_sid
299     fdescr.type = ref.type
300     fdescr.revision = ref.revision
301     aces = ref.dacl.aces
302     for i in range(0, len(aces)):
303         ace = aces[i]
304         if not ace.type & security.SEC_ACE_TYPE_ACCESS_ALLOWED_OBJECT and str(ace.trustee) != security.SID_BUILTIN_PREW2K:
305            #    if fdescr.type & security.SEC_DESC_DACL_AUTO_INHERITED:
306             ace.flags = ace.flags | security.SEC_ACE_FLAG_OBJECT_INHERIT | security.SEC_ACE_FLAG_CONTAINER_INHERIT
307             if str(ace.trustee) == security.SID_CREATOR_OWNER:
308                 # For Creator/Owner the IO flag is set as this ACE has only a sense for child objects
309                 ace.flags = ace.flags | security.SEC_ACE_FLAG_INHERIT_ONLY
310             ace.access_mask = ldapmask2filemask(ace.access_mask)
311             fdescr.dacl_add(ace)
312
313     if not as_sddl:
314         return fdescr
315
316     return fdescr.as_sddl(sid)
317
318
319 class SMBHelper:
320     """
321     A wrapper class for SMB connection
322
323     smb_path: path with separator "\\" other than "/"
324     """
325
326     def __init__(self, smb_conn, dom_sid):
327         self.smb_conn = smb_conn
328         self.dom_sid = dom_sid
329
330     def get_acl(self, smb_path, as_sddl=False):
331         assert '/' not in smb_path
332
333         ntacl_sd = self.smb_conn.get_acl(
334             smb_path, SECURITY_SECINFO_FLAGS, SECURITY_SEC_FLAGS)
335
336         return ntacl_sd.as_sddl(self.dom_sid) if as_sddl else ntacl_sd
337
338     def list(self, smb_path=''):
339         """
340         List file and dir base names in smb_path without recursive.
341         """
342         assert '/' not in smb_path
343         return self.smb_conn.list(smb_path, attribs=SMB_FILE_ATTRIBUTE_FLAGS)
344
345     def is_dir(self, attrib):
346         """
347         Check whether the attrib value is a directory.
348
349         attrib is from list method.
350         """
351         return bool(attrib & smb.FILE_ATTRIBUTE_DIRECTORY)
352
353     def join(self, root, name):
354         """
355         Join path with '\\'
356         """
357         return root + '\\' + name if root else name
358
359     def loadfile(self, smb_path):
360         assert '/' not in smb_path
361         return self.smb_conn.loadfile(smb_path)
362
363     def create_tree(self, tree, smb_path=''):
364         """
365         Create files as defined in tree
366         """
367         for name, content in tree.items():
368             fullname = self.join(smb_path, name)
369             if isinstance(content, dict):  # a dir
370                 if not self.smb_conn.chkpath(fullname):
371                     self.smb_conn.mkdir(fullname)
372                 self.create_tree(content, smb_path=fullname)
373             else:  # a file
374                 self.smb_conn.savefile(fullname, content)
375
376     def get_tree(self, smb_path=''):
377         """
378         Get the tree structure via smb conn
379
380         self.smb_conn.list example:
381
382         [
383           {
384             'attrib': 16,
385             'mtime': 1528848309,
386             'name': 'dir1',
387             'short_name': 'dir1',
388             'size': 0L
389           }, {
390             'attrib': 32,
391             'mtime': 1528848309,
392             'name': 'file0.txt',
393             'short_name': 'file0.txt',
394             'size': 10L
395           }
396         ]
397         """
398         tree = {}
399         for item in self.list(smb_path):
400             name = item['name']
401             fullname = self.join(smb_path, name)
402             if self.is_dir(item['attrib']):
403                 tree[name] = self.get_tree(smb_path=fullname)
404             else:
405                 tree[name] = self.loadfile(fullname)
406         return tree
407
408     def get_ntacls(self, smb_path=''):
409         """
410         Get ntacl for each file and dir via smb conn
411         """
412         ntacls = {}
413         for item in self.list(smb_path):
414             name = item['name']
415             fullname = self.join(smb_path, name)
416             if self.is_dir(item['attrib']):
417                 ntacls.update(self.get_ntacls(smb_path=fullname))
418             else:
419                 ntacl_sd = self.get_acl(fullname)
420                 ntacls[fullname] = ntacl_sd.as_sddl(self.dom_sid)
421         return ntacls
422
423     def delete_tree(self):
424         for item in self.list():
425             name = item['name']
426             if self.is_dir(item['attrib']):
427                 self.smb_conn.deltree(name)
428             else:
429                 self.smb_conn.unlink(name)
430
431
432 class NtaclsHelper:
433
434     def __init__(self, service, smb_conf_path, dom_sid):
435         self.service = service
436         self.dom_sid = dom_sid
437
438         # this is important to help smbd find services.
439         self.lp = s3param.get_context()
440         self.lp.load(smb_conf_path)
441
442         self.use_ntvfs = "smb" in self.lp.get("server services")
443
444     def getntacl(self, path, as_sddl=False, direct_db_access=None):
445         if direct_db_access is None:
446             direct_db_access = self.use_ntvfs
447
448         ntacl_sd = getntacl(
449             self.lp, path,
450             direct_db_access=direct_db_access,
451             service=self.service)
452
453         return ntacl_sd.as_sddl(self.dom_sid) if as_sddl else ntacl_sd
454
455     def setntacl(self, path, ntacl_sd):
456         # ntacl_sd can be obj or str
457         return setntacl(self.lp, path, ntacl_sd, self.dom_sid)
458
459
460 def _create_ntacl_file(dst, ntacl_sddl_str):
461     with open(dst + '.NTACL', 'w') as f:
462         f.write(ntacl_sddl_str)
463
464
465 def _read_ntacl_file(src):
466     with open(src + '.NTACL', 'r') as f:
467         return f.read()
468
469
470 def backup_online(smb_conn, dest_tarfile_path, dom_sid):
471     """
472     Backup all files and dirs with ntacl for the serive behind smb_conn.
473
474     1. Create a temp dir as container dir
475     2. Backup all files with dir structure into container dir
476     3. Generate file.NTACL files for each file and dir in contianer dir
477     4. Create a tar file from container dir(without top level folder)
478     5. Delete contianer dir
479     """
480
481     if isinstance(dom_sid, str):
482         dom_sid = security.dom_sid(dom_sid)
483
484     smb_helper = SMBHelper(smb_conn, dom_sid)
485
486     remotedir = ''  # root dir
487
488     localdir = tempfile.mkdtemp()
489
490     r_dirs = [remotedir]
491     l_dirs = [localdir]
492
493     while r_dirs:
494         r_dir = r_dirs.pop()
495         l_dir = l_dirs.pop()
496
497         for e in smb_helper.list(smb_path=r_dir):
498             r_name = smb_helper.join(r_dir, e['name'])
499             l_name = os.path.join(l_dir, e['name'])
500
501             if smb_helper.is_dir(e['attrib']):
502                 r_dirs.append(r_name)
503                 l_dirs.append(l_name)
504                 os.mkdir(l_name)
505             else:
506                 data = smb_helper.loadfile(r_name)
507                 with open(l_name, 'wb') as f:
508                     f.write(data)
509
510             # get ntacl for this entry and save alongside
511             ntacl_sddl_str = smb_helper.get_acl(r_name, as_sddl=True)
512             _create_ntacl_file(l_name, ntacl_sddl_str)
513
514     with tarfile.open(name=dest_tarfile_path, mode='w:gz') as tar:
515         for name in os.listdir(localdir):
516             path = os.path.join(localdir, name)
517             tar.add(path, arcname=name)
518
519     shutil.rmtree(localdir)
520
521
522 def backup_offline(src_service_path, dest_tarfile_path, samdb_conn, smb_conf_path):
523     """
524     Backup files and ntacls to a tarfile for a service
525     """
526     service = src_service_path.rstrip('/').rsplit('/', 1)[-1]
527     tempdir = tempfile.mkdtemp()
528
529     dom_sid_str = samdb_conn.get_domain_sid()
530     dom_sid = security.dom_sid(dom_sid_str)
531
532     ntacls_helper = NtaclsHelper(service, smb_conf_path, dom_sid)
533
534     for dirpath, dirnames, filenames in os.walk(src_service_path):
535         # each dir only cares about its direct children
536         rel_dirpath = os.path.relpath(dirpath, start=src_service_path)
537         dst_dirpath = os.path.join(tempdir, rel_dirpath)
538
539         # create sub dirs and NTACL file
540         for dirname in dirnames:
541             src = os.path.join(dirpath, dirname)
542             dst = os.path.join(dst_dirpath, dirname)
543             # mkdir with metadata
544             smbd.mkdir(dst, service)
545             ntacl_sddl_str = ntacls_helper.getntacl(src, as_sddl=True)
546             _create_ntacl_file(dst, ntacl_sddl_str)
547
548         # create files and NTACL file, then copy data
549         for filename in filenames:
550             src = os.path.join(dirpath, filename)
551             dst = os.path.join(dst_dirpath, filename)
552             # create an empty file with metadata
553             smbd.create_file(dst, service)
554             ntacl_sddl_str = ntacls_helper.getntacl(src, as_sddl=True)
555             _create_ntacl_file(dst, ntacl_sddl_str)
556
557             # now put data in
558             with open(src, 'rb') as src_file:
559                 data = src_file.read()
560                 with open(dst, 'wb') as dst_file:
561                     dst_file.write(data)
562
563     # add all files in tempdir to tarfile without a top folder
564     with tarfile.open(name=dest_tarfile_path, mode='w:gz') as tar:
565         for name in os.listdir(tempdir):
566             path = os.path.join(tempdir, name)
567             tar.add(path, arcname=name)
568
569     shutil.rmtree(tempdir)
570
571
572 def backup_restore(src_tarfile_path, dst_service_path, samdb_conn, smb_conf_path):
573     """
574     Restore files and ntacls from a tarfile to a service
575     """
576     service = dst_service_path.rstrip('/').rsplit('/', 1)[-1]
577     tempdir = tempfile.mkdtemp()  # src files
578
579     dom_sid_str = samdb_conn.get_domain_sid()
580     dom_sid = security.dom_sid(dom_sid_str)
581
582     ntacls_helper = NtaclsHelper(service, smb_conf_path, dom_sid)
583
584     with tarfile.open(src_tarfile_path) as f:
585         f.extractall(path=tempdir)
586         # e.g.: /tmp/tmpRNystY/{dir1,dir1.NTACL,...file1,file1.NTACL}
587
588     for dirpath, dirnames, filenames in os.walk(tempdir):
589         rel_dirpath = os.path.relpath(dirpath, start=tempdir)
590         dst_dirpath = os.path.normpath(
591             os.path.join(dst_service_path, rel_dirpath))
592
593         for dirname in dirnames:
594             if not dirname.endswith('.NTACL'):
595                 src = os.path.join(dirpath, dirname)
596                 dst = os.path.join(dst_dirpath, dirname)
597                 if not os.path.isdir(dst):
598                     # dst must be absolute path for smbd API
599                     smbd.mkdir(dst, service)
600                 ntacl_sddl_str = _read_ntacl_file(src)
601                 ntacls_helper.setntacl(dst, ntacl_sddl_str)
602
603         for filename in filenames:
604             if not filename.endswith('.NTACL'):
605                 src = os.path.join(dirpath, filename)
606                 dst = os.path.join(dst_dirpath, filename)
607                 if not os.path.isfile(dst):
608                     # dst must be absolute path for smbd API
609                     smbd.create_file(dst, service)
610                 ntacl_sddl_str = _read_ntacl_file(src)
611                 ntacls_helper.setntacl(dst, ntacl_sddl_str)
612
613                 # now put data in
614                 with open(src, 'rb') as src_file:
615                     data = src_file.read()
616                     with open(dst, 'wb') as dst_file:
617                         dst_file.write(data)
618
619     shutil.rmtree(tempdir)