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