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