Re: [PATCH] secdesc-ui.py: a UI to view the security descriptors on SMB2+ shares

From: Pavel Shilovsky
Date: Mon Apr 01 2019 - 15:24:14 EST


This is really cool! Thanks Ronnie. I will be targeting this for the
next release of cifs-utils (not the one that I am about to cut off),
so we will have time to stabilize it.

Best regards,
Pavel Shilovsky

ÐÑ, 31 ÐÐÑ. 2019 Ð. Ð 21:51, Steve French via samba-technical
<samba-technical@xxxxxxxxxxxxxxx>:

>
> The tool that Ronnie proposed below looks useful (see below) and
> attached screenshot. With this as a sample (along with 'smbinfo' tool
> in cifs-utils) and a starting point, those with python/GUI interest
> should be able to extend it in very interesting ways now that we have
> the ability to query server information much more broadly. Managing
> ACLs, quotas, snapshots, alerts and many other fun features across
> such a broad set of servers (from Samba, to Windows, Azure and the
> Cloud, Macs, NetApp and various filers).
>
> Ronnie,
> Great idea.
>
> ---------- Forwarded message ---------
> From: Ronnie Sahlberg <lsahlber@xxxxxxxxxx>
> Date: Sun, Mar 31, 2019 at 10:54 PM
> Subject: [PATCH] secdesc-ui.py: a UI to view the security descriptors
> on SMB2+ shares
> To: linux-cifs <linux-cifs@xxxxxxxxxxxxxxx>
> Cc: Steve French <smfrench@xxxxxxxxx>, Pavel Shilovsky
> <pshilov@xxxxxxxxxxxxx>, Ronnie Sahlberg <lsahlber@xxxxxxxxxx>
>
> a simple python program with a basic UI to view the security descriptor
> for SMB2+ resources.
>
> With a basic starting point like this my hope is we can get some interest
> from people with python skills that may want to make it better until
> it becomes a full-fledged utility.
>
> Signed-off-by: Ronnie Sahlberg <lsahlber@xxxxxxxxxx>
> ---
> secdesc-ui.py | 421 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 421 insertions(+)
> create mode 100755 secdesc-ui.py
>
> diff --git a/secdesc-ui.py b/secdesc-ui.py
> new file mode 100755
> index 0000000..dcb9dbf
> --- /dev/null
> +++ b/secdesc-ui.py
> @@ -0,0 +1,421 @@
> +#!/usr/bin/env python
> +# coding: utf-8
> +
> +import array
> +import enum
> +import fcntl
> +import os
> +import struct
> +import stat
> +import sys
> +from Tkinter import *
> +
> +FULL_CONTROL = 0x001f01ff
> +EWRITE = 0x00000116
> +ALL_READ_BITS = 0x00020089
> +EREAD = 0x001200a9
> +CHANGE = 0x001301bf
> +
> +TRAV_EXEC = 0x00100020
> +LIST_READ = 0x00100001
> +READ_ATTR = 0x00100080
> +READ_XATT = 0x00100008
> +CREA_WRIT = 0x00100002
> +CREA_APPE = 0x00100004
> +WRIT_ATTR = 0x00100100
> +WRIT_XATT = 0x00100010
> +DELE = 0x00110000
> +READ_PERM = 0x00120000
> +CHAN_PERM = 0x00140000
> +TAKE_OWNR = 0x00180000
> +
> +class App:
> + def __init__(self, root, sd, is_dir):
> + self.sd = sd
> + self.is_dir = is_dir
> + self.tf = Frame(bd=1)
> + self.tf.grid(columnspan=5, rowspan=5, padx=5, pady=5)
> +
> + # Owner
> + Label(self.tf, text='Owner: %s' %
> (self.sd.owner)).grid(row=0, column=0, columnspan=6, sticky='W')
> +
> + # Group
> + Label(self.tf, text='Group: %s' %
> (self.sd.group)).grid(row=1, column=0, columnspan=6, sticky='W')
> +
> + self.sb = Scrollbar(self.tf, orient=VERTICAL)
> + self.lb = Listbox(self.tf, height=5, selectmode=SINGLE,
> + yscrollcommand=self.sb.set)
> + self.sb.config(command=self.lb.yview)
> + self.sb.grid(row=2, column=1, sticky='NS')
> + self.lb.grid(row=2, column=0, sticky='W')
> +
> + max = 0
> + for idx, item in enumerate(self.sd.dacl.ace):
> + if item.type != 0 and item.type != 1:
> + continue
> + sid = '%s %s' % ("ALLOW" if item.type == 0
> else "DENY", item.sid)
> + if max > len(sid):
> + max = len(sid)
> + self.lb.insert(idx, sid)
> + if not self.lb.curselection():
> + self.lb.selection_set(idx)
> + self.lb.config(width=max)
> + self.lb.bind("<Double-Button-1>", self.select_sid)
> +
> + self.bas = Button(self.tf, text='Basic', relief=SUNKEN,
> + command=self.click_bas)
> + self.bas.grid(row=2, column=2, sticky='NW')
> +
> + self.adv = Button(self.tf, text='Advanced',
> + command=self.click_adv)
> + self.adv.grid(row=2, column=3, sticky='NW')
> +
> + # Basic Panel
> + self.bf_bas = Frame(master=self.tf, bd=1)
> + self.bf_bas.grid(row=3, column=0, columnspan=4, padx=5, pady=5)
> + self.bf_bas_name = Label(self.bf_bas, text='')
> + self.bf_bas_name.grid(row=0, column=0, columnspan=2, sticky='W')
> +
> + row = 1
> + self.bf_bas_fc=Checkbutton(self.bf_bas, text='Full Control')
> + self.bf_bas_fc.grid(row=row, column=0, sticky='W')
> + self.bf_bas_fc.config(state=DISABLED)
> + row += 1
> +
> + self.bf_bas_mo=Checkbutton(self.bf_bas, text='Modify')
> + self.bf_bas_mo.grid(row=row, column=0, sticky='W')
> + self.bf_bas_mo.config(state=DISABLED)
> + row += 1
> +
> + self.bf_bas_re=Checkbutton(self.bf_bas, text='Read & Execute')
> + self.bf_bas_re.grid(row=row, column=0, sticky='W')
> + self.bf_bas_re.config(state=DISABLED)
> + row += 1
> +
> + self.bf_bas_rd=Checkbutton(self.bf_bas, text='Read')
> + self.bf_bas_rd.grid(row=row, column=0, sticky='W')
> + self.bf_bas_rd.config(state=DISABLED)
> + row += 1
> +
> + self.bf_bas_wr=Checkbutton(self.bf_bas, text='Write')
> + self.bf_bas_wr.grid(row=row, column=0, sticky='W')
> + self.bf_bas_wr.config(state=DISABLED)
> + row += 1
> +
> + self.bf_bas_sp=Checkbutton(self.bf_bas, text='Special')
> + self.bf_bas_sp.grid(row=row, column=0, sticky='W')
> + self.bf_bas_sp.config(state=DISABLED)
> + row += 1
> +
> + self.show_bas = True
> + self.update_bf_bas()
> +
> + # Advanced Panel
> + self.bf_adv = Frame(master=self.tf, bd=1)
> + self.bf_adv.grid(row=3, column=0, columnspan=4, padx=5, pady=5)
> + self.bf_adv_name = Label(self.bf_adv, text='')
> + self.bf_adv_name.grid(row=0, column=0, columnspan=2, sticky='W')
> +
> + row = 1
> + self.bf_adv_fc=Checkbutton(self.bf_adv, text='Full Control')
> + self.bf_adv_fc.grid(row=row, column=0, sticky='W')
> + self.bf_adv_fc.config(state=DISABLED)
> + row += 1
> +
> + self.bf_adv_te=Checkbutton(self.bf_adv,
> text='Traverse-folder/execute-file')
> + self.bf_adv_te.grid(row=row, column=0, sticky='W')
> + self.bf_adv_te.config(state=DISABLED)
> + row += 1
> +
> + self.bf_adv_lr=Checkbutton(self.bf_adv,
> text='List-folder/read-data')
> + self.bf_adv_lr.grid(row=row, column=0, sticky='W')
> + self.bf_adv_lr.config(state=DISABLED)
> + row += 1
> +
> + self.bf_adv_ra=Checkbutton(self.bf_adv, text='Read-Attributes')
> + self.bf_adv_ra.grid(row=row, column=0, sticky='W')
> + self.bf_adv_ra.config(state=DISABLED)
> + row += 1
> +
> + self.bf_adv_re=Checkbutton(self.bf_adv,
> text='Read-Extended-Attributes')
> + self.bf_adv_re.grid(row=row, column=0, sticky='W')
> + self.bf_adv_re.config(state=DISABLED)
> + row += 1
> +
> + self.bf_adv_cw=Checkbutton(self.bf_adv,
> text='Create-files/write-data')
> + self.bf_adv_cw.grid(row=row, column=0, sticky='W')
> + self.bf_adv_cw.config(state=DISABLED)
> + row += 1
> +
> + self.bf_adv_ca=Checkbutton(self.bf_adv,
> text='Create-folders/append-data')
> + self.bf_adv_ca.grid(row=row, column=0, sticky='W')
> + self.bf_adv_ca.config(state=DISABLED)
> + row += 1
> +
> + row = 1
> + self.bf_adv_wa=Checkbutton(self.bf_adv, text='Write-Attributes')
> + self.bf_adv_wa.grid(row=row, column=1, sticky='W')
> + self.bf_adv_wa.config(state=DISABLED)
> + row += 1
> +
> + self.bf_adv_we=Checkbutton(self.bf_adv,
> text='Write-Extended-Attributes')
> + self.bf_adv_we.grid(row=row, column=1, sticky='W')
> + self.bf_adv_we.config(state=DISABLED)
> + row += 1
> +
> + self.bf_adv_de=Checkbutton(self.bf_adv, text='Delete')
> + self.bf_adv_de.grid(row=row, column=1, sticky='W')
> + self.bf_adv_de.config(state=DISABLED)
> + row += 1
> +
> + self.bf_adv_rp=Checkbutton(self.bf_adv, text='Read-Permissions')
> + self.bf_adv_rp.grid(row=row, column=1, sticky='W')
> + self.bf_adv_rp.config(state=DISABLED)
> + row += 1
> +
> + self.bf_adv_cp=Checkbutton(self.bf_adv,
> text='Change-Permissions')
> + self.bf_adv_cp.grid(row=row, column=1, sticky='W')
> + self.bf_adv_cp.config(state=DISABLED)
> + row += 1
> +
> + self.bf_adv_to=Checkbutton(self.bf_adv, text='Take-Ownership')
> + self.bf_adv_to.grid(row=row, column=1, sticky='W')
> + self.bf_adv_to.config(state=DISABLED)
> + row += 1
> +
> + self.bf_adv.grid_remove()
> +
> + def select_sid(self, event):
> + self.click_bas()
> +
> + def click_bas(self):
> + self.adv.config(relief=RAISED)
> + self.bas.config(relief=SUNKEN)
> + self.bf_adv.grid_remove()
> + self.update_bf_bas()
> + self.bf_bas.grid()
> + self.show_bas = True
> +
> + def click_adv(self):
> + self.adv.config(relief=SUNKEN)
> + self.bas.config(relief=RAISED)
> + self.bf_bas.grid_remove()
> + self.update_bf_adv()
> + self.bf_adv.grid()
> + self.show_bas = False
> +
> + def update_bf_adv(self):
> + ace = self.sd.dacl.ace[self.lb.curselection()[0]]
> + self.bf_adv_name.config(text='Advanced Permissions for
> %s' % (ace.sid))
> + if ace.mask == FULL_CONTROL:
> + self.bf_adv_fc.select()
> + else:
> + self.bf_adv_fc.deselect()
> + if ace.mask & TRAV_EXEC == TRAV_EXEC:
> + self.bf_adv_te.select()
> + else:
> + self.bf_adv_te.deselect()
> + if ace.mask & LIST_READ == LIST_READ:
> + self.bf_adv_lr.select()
> + else:
> + self.bf_adv_lr.deselect()
> + if ace.mask & READ_ATTR == READ_ATTR:
> + self.bf_adv_ra.select()
> + else:
> + self.bf_adv_ra.deselect()
> + if ace.mask & READ_XATT == READ_XATT:
> + self.bf_adv_re.select()
> + else:
> + self.bf_adv_re.deselect()
> + if ace.mask & CREA_WRIT == CREA_WRIT:
> + self.bf_adv_cw.select()
> + else:
> + self.bf_adv_cw.deselect()
> + if ace.mask & CREA_APPE == CREA_APPE:
> + self.bf_adv_ca.select()
> + else:
> + self.bf_adv_ca.deselect()
> + if ace.mask & WRIT_ATTR == WRIT_ATTR:
> + self.bf_adv_wa.select()
> + else:
> + self.bf_adv_wa.deselect()
> + if ace.mask & WRIT_XATT == WRIT_XATT:
> + self.bf_adv_we.select()
> + else:
> + self.bf_adv_we.deselect()
> + if ace.mask & DELE == DELE:
> + self.bf_adv_de.select()
> + else:
> + self.bf_adv_de.deselect()
> + if ace.mask & READ_PERM == READ_PERM:
> + self.bf_adv_rp.select()
> + else:
> + self.bf_adv_rp.deselect()
> + if ace.mask & CHAN_PERM == CHAN_PERM:
> + self.bf_adv_rp.select()
> + else:
> + self.bf_adv_rp.deselect()
> + if ace.mask & TAKE_OWNR == TAKE_OWNR:
> + self.bf_adv_to.select()
> + else:
> + self.bf_adv_to.deselect()
> +
> + def update_bf_bas(self):
> + ace = self.sd.dacl.ace[self.lb.curselection()[0]]
> + self.bf_bas_name.config(text='Permissions for %s' % (ace.sid))
> + tmp = ace.mask
> + if ace.mask == FULL_CONTROL:
> + self.bf_bas_fc.select()
> + tmp &= ~FULL_CONTROL
> + else:
> + self.bf_bas_fc.deselect()
> + if ace.mask & CHANGE == CHANGE:
> + self.bf_bas_mo.select()
> + tmp &= ~CHANGE
> + else:
> + self.bf_bas_mo.deselect()
> + if ace.mask & EREAD == EREAD:
> + self.bf_bas_re.select()
> + tmp &= ~EREAD
> + else:
> + self.bf_bas_re.deselect()
> + if ace.mask & ALL_READ_BITS == ALL_READ_BITS:
> + self.bf_bas_rd.select()
> + tmp &= ~ALL_READ_BITS
> + else:
> + self.bf_bas_rd.deselect()
> + if ace.mask & EWRITE == EWRITE:
> + self.bf_bas_wr.select()
> + tmp &= ~EWRITE
> + else:
> + self.bf_bas_wr.deselect()
> + if tmp:
> + self.bf_bas_sp.select()
> + else:
> + self.bf_bas_sp.deselect()
> +
> +CIFS_QUERY_INFO = 0xc018cf07
> +
> +def usage():
> + print "Usage: %s <filename>" % (sys.argv[0])
> + sys.exit()
> +
> +class SID:
> + def __init__(self, buf):
> + self.sub_authority_count = buf[1]
> + self.buffer = buf[:8 + self.sub_authority_count * 4]
> + self.revision = self.buffer[0]
> + if self.revision != 1:
> + raise ValueError('SID Revision %d not supported' %
> + (self.revision))
> + self.identifier_authority = 0
> + for x in self.buffer[2:8]:
> + self.identifier_authority =
> self.identifier_authority * 256 + x
> + self.sub_authority = []
> + for i in range(self.sub_authority_count):
> +
> self.sub_authority.append(struct.unpack_from('<I', self.buffer, 8 + 4
> * i)[0])
> +
> + def __str__(self):
> + s = "S-%u-%u" % (self.revision, self.identifier_authority)
> +
> + for x in self.sub_authority:
> + s += '-%u' % x
> + return s
> +
> +
> +class ACE:
> + def __init__(self, buf):
> + self.type = buf[0]
> + self.flags = buf[1]
> + self.size = struct.unpack_from('<H', buf, 2)[0]
> + self.raw = buf[:self.size]
> + if self.type in [0, 1]:
> + self.mask = struct.unpack_from('<I', buf, 4)[0]
> + self.sid = SID(buf[8:])
> +
> + def __str__(self):
> + s = 'Type:0x%02x ' % (self.type)
> + s += 'Flags:0x%02x ' % (self.flags)
> + if self.type in [0, 1]:
> + s += 'Mask:0x%02x SID:%s' % (self.mask, self.sid)
> + else:
> + for i in self.raw[4:]:
> + s += '%02x' % (i)
> +
> + return s
> +
> + class Type(enum.Enum):
> + ALLOWED = 0
> + DENIED = 1
> +
> + def __str__(self):
> + return self.name
> +
> +class ACL:
> + def __init__(self, buf):
> + self.revision = buf[0]
> + if self.revision != 2 and self.revision != 4:
> + raise ValueError('ACL Revision %d '
> + 'not supported' % (self.revision))
> + acl = buf[8:8 + struct.unpack_from('<H', buf, 2)[0]]
> + self.ace = []
> + for i in range(struct.unpack_from('<H', buf, 4)[0]):
> + ace = ACE(acl)
> + self.ace.append(ace)
> + acl = acl[ace.size:]
> +
> + def __str__(self):
> + s = 'Revision:0x%02x\n' % (self.revision)
> + for ace in self.ace:
> + s += '%s\n' % (ace)
> + return s
> +
> +class SecurityDescriptor:
> + def __init__(self, buf):
> + self.revision = buf[0]
> + if self.revision != 1:
> + raise ValueError('Security Descriptor Revision %d '
> + 'not supported' % (self.revision))
> + self.control = struct.unpack_from('<H', buf, 2)
> +
> + self.owner = SID(buf[struct.unpack_from('<I', buf, 4)[0]:])
> + self.group = SID(buf[struct.unpack_from('<I', buf, 8)[0]:])
> +
> + self.dacl = ACL(buf[struct.unpack_from('<I', buf, 16)[0]:])
> +
> + def __str__(self):
> + s = 'Revision:%u\n' % (self.revision)
> + s += 'Control:0x%04x\n' % (self.control)
> + s += 'Owner:%s\n' % (self.owner)
> + s += 'Group:%s\n' % (self.group)
> + s += 'DACL:\n%s' % (self.dacl)
> + return s
> +
> +def main():
> + if len(sys.argv) != 2:
> + usage()
> +
> + buf = array.array('B', [0] * 16384)
> +
> + struct.pack_into('<I', buf, 0, 3) # InfoType: Security
> + struct.pack_into('<I', buf, 8, 7) # AddInfo: Group/Owner/Dacl
> + struct.pack_into('<I', buf, 16, 16384) # InputBufferLength
> +
> + #with open(sys.argv[1], 'r') as f:
> + f = os.open(sys.argv[1], os.O_RDONLY)
> + st = os.fstat(f)
> + fcntl.ioctl(f, CIFS_QUERY_INFO, buf, 1)
> + os.close(f)
> +
> + s = struct.unpack_from('<I', buf, 16)
> +
> + sd = SecurityDescriptor(buf[24:24 + s[0]])
> +
> + root = Tk()
> + app = App(root, sd, stat.S_ISDIR(st.st_mode))
> + root.mainloop()
> +
> +
> +if __name__ == "__main__":
> + main()
> +
> --
> 2.13.6
>
>
>
> --
> Thanks,
>
> Steve