xref: /illumos-gate/usr/src/lib/pyzfs/common/allow.py (revision e8921a52)
1#!@PYTHON@
2#
3# CDDL HEADER START
4#
5# The contents of this file are subject to the terms of the
6# Common Development and Distribution License (the "License").
7# You may not use this file except in compliance with the License.
8#
9# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10# or http://www.opensolaris.org/os/licensing.
11# See the License for the specific language governing permissions
12# and limitations under the License.
13#
14# When distributing Covered Code, include this CDDL HEADER in each
15# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16# If applicable, add the following below this CDDL HEADER, with the
17# fields enclosed by brackets "[]" replaced with your own identifying
18# information: Portions Copyright [yyyy] [name of copyright owner]
19#
20# CDDL HEADER END
21#
22# Copyright (c) 2009, 2010, Oracle and/or its affiliates. All rights reserved.
23# Copyright (c) 2013 by Delphix. All rights reserved.
24# Copyright 2018 OmniOS Community Edition (OmniOSce) Association.
25#
26
27"""This module implements the "zfs allow" and "zfs unallow" subcommands.
28The only public interface is the zfs.allow.do_allow() function."""
29
30import zfs.util
31import zfs.dataset
32import optparse
33import sys
34import pwd
35import grp
36import errno
37
38_ = zfs.util._
39
40class FSPerms(object):
41	"""This class represents all the permissions that are set on a
42	particular filesystem (not including those inherited)."""
43
44	__slots__ = "create", "sets", "local", "descend", "ld"
45	__repr__ = zfs.util.default_repr
46
47	def __init__(self, raw):
48		"""Create a FSPerms based on the dict of raw permissions
49		from zfs.ioctl.get_fsacl()."""
50		# set of perms
51		self.create = set()
52
53		# below are { "Ntype name": set(perms) }
54		# where N is a number that we just use for sorting,
55		# type is "user", "group", "everyone", or "" (for sets)
56		# name is a user, group, or set name, or "" (for everyone)
57		self.sets = dict()
58		self.local = dict()
59		self.descend = dict()
60		self.ld = dict()
61
62		# see the comment in dsl_deleg.c for the definition of whokey
63		for whokey in raw.keys():
64			perms = raw[whokey].keys()
65			whotypechr = whokey[0].lower()
66			ws = whokey[3:]
67			if whotypechr == "c":
68				self.create.update(perms)
69			elif whotypechr == "s":
70				nwho = "1" + ws
71				self.sets.setdefault(nwho, set()).update(perms)
72			else:
73				if whotypechr == "u":
74					try:
75						name = pwd.getpwuid(int(ws)).pw_name
76					except KeyError:
77						name = ws
78					nwho = "1user " + name
79				elif whotypechr == "g":
80					try:
81						name = grp.getgrgid(int(ws)).gr_name
82					except KeyError:
83						name = ws
84					nwho = "2group " + name
85				elif whotypechr == "e":
86					nwho = "3everyone"
87				else:
88					raise ValueError(whotypechr)
89
90				if whokey[1] == "l":
91					d = self.local
92				elif whokey[1] == "d":
93					d = self.descend
94				else:
95					raise ValueError(whokey[1])
96
97				d.setdefault(nwho, set()).update(perms)
98
99		# Find perms that are in both local and descend, and
100		# move them to ld.
101		for nwho in self.local:
102			if nwho not in self.descend:
103				continue
104			# note: these are set operations
105			self.ld[nwho] = self.local[nwho] & self.descend[nwho]
106			self.local[nwho] -= self.ld[nwho]
107			self.descend[nwho] -= self.ld[nwho]
108
109	@staticmethod
110	def __ldstr(d, header):
111		s = ""
112		for (nwho, perms) in sorted(d.items()):
113			# local and descend may have entries where perms
114			# is an empty set, due to consolidating all
115			# permissions into ld
116			if perms:
117				s += "\t%s %s\n" % \
118				    (nwho[1:], ",".join(sorted(perms)))
119		if s:
120			s = header + s
121		return s
122
123	def __str__(self):
124		s = self.__ldstr(self.sets, _("Permission sets:\n"))
125
126		if self.create:
127			s += _("Create time permissions:\n")
128			s += "\t%s\n" % ",".join(sorted(self.create))
129
130		s += self.__ldstr(self.local, _("Local permissions:\n"))
131		s += self.__ldstr(self.descend, _("Descendent permissions:\n"))
132		s += self.__ldstr(self.ld, _("Local+Descendent permissions:\n"))
133		return s.rstrip()
134
135def args_to_perms(parser, options, who, perms):
136	"""Return a dict of raw perms {"whostr" -> {"perm" -> None}}
137	based on the command-line input."""
138
139	# perms is not set if we are doing a "zfs unallow <who> <fs>" to
140	# remove all of someone's permissions
141	if perms:
142		setperms = dict(((p, None) for p in perms if p[0] == "@"))
143		baseperms = dict(((canonicalized_perm(p), None)
144		    for p in perms if p[0] != "@"))
145	else:
146		setperms = None
147		baseperms = None
148
149	d = dict()
150
151	def storeperm(typechr, inheritchr, arg):
152		assert typechr in "ugecs"
153		assert inheritchr in "ld-"
154
155		def mkwhokey(t):
156			return "%c%c$%s" % (t, inheritchr, arg)
157
158		if baseperms or not perms:
159			d[mkwhokey(typechr)] = baseperms
160		if setperms or not perms:
161			d[mkwhokey(typechr.upper())] = setperms
162
163	def decodeid(w, toidfunc, fmt):
164		try:
165			return int(w)
166		except ValueError:
167			try:
168				return toidfunc(w)[2]
169			except KeyError:
170				parser.error(fmt % w)
171
172	if options.set:
173		storeperm("s", "-", who)
174	elif options.create:
175		storeperm("c", "-", "")
176	else:
177		for w in who:
178			if options.user:
179				id = decodeid(w, pwd.getpwnam,
180				    _("invalid user %s"))
181				typechr = "u"
182			elif options.group:
183				id = decodeid(w, grp.getgrnam,
184				    _("invalid group %s"))
185				typechr = "g"
186			elif w == "everyone":
187				id = ""
188				typechr = "e"
189			else:
190				try:
191					id = pwd.getpwnam(w)[2]
192					typechr = "u"
193				except KeyError:
194					try:
195						id = grp.getgrnam(w)[2]
196						typechr = "g"
197					except KeyError:
198						parser.error(_("invalid user/group %s") % w)
199			if options.local:
200				storeperm(typechr, "l", id)
201			if options.descend:
202				storeperm(typechr, "d", id)
203	return d
204
205perms_subcmd = dict(
206    create=_("Must also have the 'mount' ability"),
207    destroy=_("Must also have the 'mount' ability"),
208    snapshot="",
209    rollback="",
210    clone=_("""Must also have the 'create' ability and 'mount'
211\t\t\t\tability in the origin file system"""),
212    promote=_("""Must also have the 'mount'
213\t\t\t\tand 'promote' ability in the origin file system"""),
214    rename=_("""Must also have the 'mount' and 'create'
215\t\t\t\tability in the new parent"""),
216    receive=_("Must also have the 'mount' and 'create' ability"),
217    allow=_("Must also have the permission that is being\n\t\t\t\tallowed"),
218    mount=_("Allows mount/umount of ZFS datasets"),
219    share=_("Allows sharing file systems over NFS or SMB\n\t\t\t\tprotocols"),
220    send="",
221    hold=_("Allows adding a user hold to a snapshot"),
222    release=_("Allows releasing a user hold which\n\t\t\t\tmight destroy the snapshot"),
223    diff=_("Allows lookup of paths within a dataset,\n\t\t\t\tgiven an object number. Ordinary users need this\n\t\t\t\tin order to use zfs diff"),
224    bookmark="",
225)
226
227perms_other = dict(
228    userprop=_("Allows changing any user property"),
229    userquota=_("Allows accessing any userquota@... property"),
230    groupquota=_("Allows accessing any groupquota@... property"),
231    userused=_("Allows reading any userused@... property"),
232    groupused=_("Allows reading any groupused@... property"),
233)
234
235def hasset(ds, setname):
236	"""Return True if the given setname (string) is defined for this
237	ds (Dataset)."""
238	# It would be nice to cache the result of get_fsacl().
239	for raw in ds.get_fsacl().values():
240		for whokey in raw.keys():
241			if whokey[0].lower() == "s" and whokey[3:] == setname:
242				return True
243	return False
244
245def canonicalized_perm(permname):
246	"""Return the canonical name (string) for this permission (string).
247	Raises ZFSError if it is not a valid permission."""
248	if permname in perms_subcmd.keys() or permname in perms_other.keys():
249		return permname
250	try:
251		return zfs.dataset.getpropobj(permname).name
252	except KeyError:
253		raise zfs.util.ZFSError(errno.EINVAL, permname,
254		    _("invalid permission"))
255
256def print_perms():
257	"""Print the set of supported permissions."""
258	print(_("\nThe following permissions are supported:\n"))
259	fmt = "%-16s %-14s\t%s"
260	print(fmt % (_("NAME"), _("TYPE"), _("NOTES")))
261
262	for (name, note) in sorted(perms_subcmd.items()):
263		print(fmt % (name, _("subcommand"), note))
264
265	for (name, note) in sorted(perms_other.items()):
266		print(fmt % (name, _("other"), note))
267
268	for (name, prop) in sorted(zfs.dataset.proptable.items()):
269		if prop.visible and prop.delegatable():
270			print(fmt % (name, _("property"), ""))
271
272def do_allow():
273	"""Implements the "zfs allow" and "zfs unallow" subcommands."""
274	un = (sys.argv[1] == "unallow")
275
276	def usage(msg=None):
277		parser.print_help()
278		print_perms()
279		if msg:
280			print
281			parser.exit("zfs: error: " + msg)
282		else:
283			parser.exit()
284
285	if un:
286		u = _("""unallow [-rldug] <"everyone"|user|group>[,...]
287	    [<perm|@setname>[,...]] <filesystem|volume>
288	unallow [-rld] -e [<perm|@setname>[,...]] <filesystem|volume>
289	unallow [-r] -c [<perm|@setname>[,...]] <filesystem|volume>
290	unallow [-r] -s @setname [<perm|@setname>[,...]] <filesystem|volume>""")
291		verb = _("remove")
292		sstr = _("undefine permission set")
293	else:
294		u = _("""allow <filesystem|volume>
295	allow [-ldug] <"everyone"|user|group>[,...] <perm|@setname>[,...]
296	    <filesystem|volume>
297	allow [-ld] -e <perm|@setname>[,...] <filesystem|volume>
298	allow -c <perm|@setname>[,...] <filesystem|volume>
299	allow -s @setname <perm|@setname>[,...] <filesystem|volume>""")
300		verb = _("set")
301		sstr = _("define permission set")
302
303	parser = optparse.OptionParser(usage=u, prog="zfs")
304
305	parser.add_option("-l", action="store_true", dest="local",
306	    help=_("%s permission locally") % verb)
307	parser.add_option("-d", action="store_true", dest="descend",
308	    help=_("%s permission for descendents") % verb)
309	parser.add_option("-u", action="store_true", dest="user",
310	    help=_("%s permission for user") % verb)
311	parser.add_option("-g", action="store_true", dest="group",
312	    help=_("%s permission for group") % verb)
313	parser.add_option("-e", action="store_true", dest="everyone",
314	    help=_("%s permission for everyone") % verb)
315	parser.add_option("-c", action="store_true", dest="create",
316	    help=_("%s create time permissions") % verb)
317	parser.add_option("-s", action="store_true", dest="set", help=sstr)
318	if un:
319		parser.add_option("-r", action="store_true", dest="recursive",
320		    help=_("remove permissions recursively"))
321
322	if len(sys.argv) == 3 and not un:
323		# just print the permissions on this fs
324
325		if sys.argv[2] == "-h":
326			# hack to make "zfs allow -h" work
327			usage()
328		ds = zfs.dataset.Dataset(sys.argv[2], snaps=False)
329
330		p = dict()
331		for (fs, raw) in ds.get_fsacl().items():
332			p[fs] = FSPerms(raw)
333
334		for fs in sorted(p.keys(), reverse=True):
335			s = _("---- Permissions on %s ") % fs
336			print(s + "-" * (70-len(s)))
337			print(p[fs])
338		return
339
340	(options, args) = parser.parse_args(sys.argv[2:])
341
342	if sum((bool(options.everyone), bool(options.user),
343	    bool(options.group))) > 1:
344		parser.error(_("-u, -g, and -e are mutually exclusive"))
345
346	def mungeargs(expected_len):
347		if un and len(args) == expected_len-1:
348			return (None, args[expected_len-2])
349		elif len(args) == expected_len:
350			return (args[expected_len-2].split(","),
351			    args[expected_len-1])
352		else:
353			usage(_("wrong number of parameters"))
354
355	if options.set:
356		if options.local or options.descend or options.user or \
357		    options.group or options.everyone or options.create:
358			parser.error(_("invalid option combined with -s"))
359		if args[0][0] != "@":
360			parser.error(_("invalid set name: missing '@' prefix"))
361
362		(perms, fsname) = mungeargs(3)
363		who = args[0]
364	elif options.create:
365		if options.local or options.descend or options.user or \
366		    options.group or options.everyone or options.set:
367			parser.error(_("invalid option combined with -c"))
368
369		(perms, fsname) = mungeargs(2)
370		who = None
371	elif options.everyone:
372		if options.user or options.group or \
373		    options.create or options.set:
374			parser.error(_("invalid option combined with -e"))
375
376		(perms, fsname) = mungeargs(2)
377		who = ["everyone"]
378	else:
379		(perms, fsname) = mungeargs(3)
380		who = args[0].split(",")
381
382	if not options.local and not options.descend:
383		options.local = True
384		options.descend = True
385
386	d = args_to_perms(parser, options, who, perms)
387
388	ds = zfs.dataset.Dataset(fsname, snaps=False)
389
390	if not un and perms:
391		for p in perms:
392			if p[0] == "@" and not hasset(ds, p):
393				parser.error(_("set %s is not defined") % p)
394
395	ds.set_fsacl(un, d)
396	if un and options.recursive:
397		for child in ds.descendents():
398			child.set_fsacl(un, d)
399