Add the ability to supply filters to watches set with keyctl

Add the ability to supply filters to watches set with "keyctl watch" and
"keyctl watch_session".

Signed-off-by: David Howells <dhowells@redhat.com>
diff --git a/keyctl.c b/keyctl.c
index cc25ac0..b1e100e 100644
--- a/keyctl.c
+++ b/keyctl.c
@@ -139,10 +139,10 @@
 	{ act_keyctl_timeout,	"timeout",	"<key> <timeout>" },
 	{ act_keyctl_unlink,	"unlink",	"<key> [<keyring>]" },
 	{ act_keyctl_update,	"update",	"[-x] <key> <data>" },
-	{ act_keyctl_watch,	"watch",	"<key>" },
+	{ act_keyctl_watch,	"watch",	"[-f<filters>] <key>" },
 	{ act_keyctl_watch_add,	"watch_add",	"<fd> <key>" },
 	{ act_keyctl_watch_rm,	"watch_rm",	"<fd> <key>" },
-	{ act_keyctl_watch_session, "watch_session", "[-n <name>] <notifylog> <gclog> <fd> <prog> [<arg1> <arg2> ...]" },
+	{ act_keyctl_watch_session, "watch_session", "[-f<filters>] [-n <name>] <notifylog> <gclog> <fd> <prog> [<arg1> <arg2> ...]" },
 	{ act_keyctl_watch_sync, "watch_sync",	"<fd>" },
 	{ act_keyctl_test,	"--test",	"..." },
 	{ NULL,			NULL,		NULL }
diff --git a/keyctl_watch.c b/keyctl_watch.c
index 191fe51..5415d4c 100644
--- a/keyctl_watch.c
+++ b/keyctl_watch.c
@@ -38,6 +38,16 @@
 static int watch_fd;
 static int debug;
 
+static struct watch_notification_filter filter = {
+	.nr_filters	= 0,
+	.filters = {
+		/* Reserve a slot */
+		[0]	= {
+			.type			= WATCH_TYPE_KEY_NOTIFY,
+		},
+	},
+};
+
 static inline bool after_eq(unsigned int a, unsigned int b)
 {
         return (signed int)(a - b) >= 0;
@@ -172,17 +182,6 @@
 	exit(0);
 }
 
-static struct watch_notification_filter filter = {
-	.nr_filters	= 1,
-	.__reserved	= 0,
-	.filters = {
-		[0]	= {
-			.type			= WATCH_TYPE_KEY_NOTIFY,
-			.subtype_filter[0]	= UINT_MAX,
-		},
-	},
-};
-
 /*
  * Open the watch device and allocate a buffer.
  */
@@ -199,7 +198,8 @@
 	if (ioctl(fd, IOC_WATCH_QUEUE_SET_SIZE, BUF_SIZE) == -1)
 		error("/dev/watch_queue(size)");
 
-	if (ioctl(fd, IOC_WATCH_QUEUE_SET_FILTER, &filter) == -1)
+	if (filter.nr_filters &&
+	    ioctl(fd, IOC_WATCH_QUEUE_SET_FILTER, &filter) == -1)
 		error("/dev/watch_queue(filter)");
 
 	page_size = sysconf(_SC_PAGESIZE);
@@ -213,18 +213,84 @@
 }
 
 /*
+ * Parse a filter character representation into a subtype number.
+ */
+static bool parse_subtype(struct watch_notification_type_filter *t, char filter)
+{
+	static const char filter_mapping[] =
+		"i" /* 0 NOTIFY_KEY_INSTANTIATED */
+		"p" /* 1 NOTIFY_KEY_UPDATED */
+		"l" /* 2 NOTIFY_KEY_LINKED */
+		"n" /* 3 NOTIFY_KEY_UNLINKED */
+		"c" /* 4 NOTIFY_KEY_CLEARED */
+		"r" /* 5 NOTIFY_KEY_REVOKED */
+		"v" /* 6 NOTIFY_KEY_INVALIDATED */
+		"s" /* 7 NOTIFY_KEY_SETATTR */
+		;
+	const char *p;
+	unsigned int st_bits;
+	unsigned int st_index;
+	unsigned int st_bit;
+	int subtype;
+
+	p = strchr(filter_mapping, filter);
+	if (!p)
+		return false;
+
+	subtype = p - filter_mapping;
+	st_bits = sizeof(t->subtype_filter[0]) * 8;
+	st_index = subtype / st_bits;
+	st_bit = 1U << (subtype % st_bits);
+	t->subtype_filter[st_index] |= st_bit;
+	return true;
+}
+
+/*
+ * Parse filters.
+ */
+static void parse_watch_filter(char *str)
+{
+	struct watch_notification_filter *f = &filter;
+	struct watch_notification_type_filter *t0 = &f->filters[0];
+
+	f->nr_filters	= 1;
+	t0->type	= WATCH_TYPE_KEY_NOTIFY;
+
+	for (; *str; str++) {
+		if (parse_subtype(t0, *str))
+			continue;
+		fprintf(stderr, "Unknown filter character '%c'\n", *str);
+		exit(2);
+	}
+}
+
+/*
  * Watch a key or keyring for changes.
  */
 void act_keyctl_watch(int argc, char *argv[])
 {
 	struct watch_queue_buffer *buf;
 	key_serial_t key;
-	int wfd;
+	int wfd, opt;
 
-	if (argc != 2)
+	while (opt = getopt(argc, argv, "f:"),
+	       opt != -1) {
+		switch (opt) {
+		case 'f':
+			parse_watch_filter(optarg);
+			break;
+		default:
+			fprintf(stderr, "Unknown option\n");
+			exit(2);
+		}
+	}
+
+	argv += optind;
+	argc -= optind;
+	if (argc != 1)
 		format();
 
-	key = get_key_id(argv[1]);
+	key = get_key_id(argv[0]);
 	wfd = open_watch(&buf);
 
 	if (keyctl_watch_key(key, wfd, 0x01) == -1)
@@ -356,12 +422,15 @@
 	FILE *log, *gc;
 	int wfd, tfd, opt, w, e = 0, e2 = 0;
 
-	while (opt = getopt(argc, argv, "+dn:"),
+	while (opt = getopt(argc, argv, "+df:n:"),
 	       opt != -1) {
 		switch (opt) {
 		case 'd':
 			debug = 1;
 			break;
+		case 'f':
+			parse_watch_filter(optarg);
+			break;
 		case 'n':
 			session_name = optarg;
 			break;
diff --git a/man/keyctl.1 b/man/keyctl.1
index f18f92d..2343762 100644
--- a/man/keyctl.1
+++ b/man/keyctl.1
@@ -115,13 +115,13 @@
 .br
 \fBkeyctl\fR pkey_decrypt <key> <pass> <datafile> <sigfile> [k=v]*
 .br
-\fBkeyctl\fR watch <key>
+\fBkeyctl\fR watch [\-f<filters>] <key>
 .br
 \fBkeyctl\fR watch_add <fd> <key>
 .br
 \fBkeyctl\fR watch_rm <fd> <key>
 .br
-\fBkeyctl\fR watch_session [-n <name>] \\
+\fBkeyctl\fR watch_session [\-f <filters>] [-n <name>] \\
                 <notifylog> <gclog> <fd> <prog> [<arg1> <arg2> ...]
 .SH DESCRIPTION
 This program is used to control the key management facility in various ways
@@ -954,9 +954,9 @@
 See asymmetric-key(7) for more information.
 
 .SS Change notifications
-\fBkeyctl\fR watch <key>
+\fBkeyctl\fR watch [\-f<filters>] <key>
 .br
-\fBkeyctl\fR watch_session [-n <name>] \\
+\fBkeyctl\fR watch_session [\-f <filters>] [-n <name>] \\
                 <notifylog> <gclog> <fd> <prog> [<arg1> <arg2> ...]
 \fBkeyctl\fR watch_add <fd> <key>
 .br
@@ -966,7 +966,24 @@
 The
 .B watch
 command watches a single key, printing notifications to stdout until the key
-is destroyed.
+is destroyed.  Filters can be employed to cut down the events that will be
+delivered.  The
+.I filter
+string is a series of letters, each one of which enables a particular event
+subtype:
+.PP
+.RS
+.nf
+.BR i " - The key has been instantiated"
+.BR p " - The key has been updated"
+.BR l " - A link has been added to a keyring"
+.BR n " - A link has been removed from a keyring"
+.BR c " - A keyring has been cleared"
+.BR r " - A key has been revoked"
+.BR v " - A key has been invalidated"
+.BR s " - A key has had its attributes changed"
+.fi
+.RE
 .PP
 The output of the command looks like:
 .PP
diff --git a/tests/keyctl/watch/bad-args/runtest.sh b/tests/keyctl/watch/bad-args/runtest.sh
new file mode 100644
index 0000000..8bbd8c0
--- /dev/null
+++ b/tests/keyctl/watch/bad-args/runtest.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+
+. ../../../prepare.inc.sh
+. ../../../toolbox.inc.sh
+
+
+# ---- do the actual testing ----
+
+result=PASS
+echo "++++ BEGINNING TEST" >$OUTPUTFILE
+
+# Attempt to watch an invalid key
+marker "CHECK WATCH INVALID KEY"
+watch_key --fail 0
+expect_error EINVAL
+
+# Add a user key to the session keyring for us to play with
+marker "ADD USER KEY"
+create_key --new=keyid user wibble stuff @s
+
+# Remove the key we just added
+marker "UNLINK KEY"
+unlink_key --wait $keyid @s
+
+# It should fail when we attempt to watch it
+marker "UPDATE UNLINKED KEY"
+watch_key --fail $keyid
+expect_error ENOKEY
+
+# Try a number of dodgy filters
+marker "CHECK DODGY FILTERS"
+watch_key --fail2 -fZ @s
+watch_key --fail2 -fZ -fQ @s
+watch_key --fail2 -f: @s
+
+echo "++++ FINISHED TEST: $result" >>$OUTPUTFILE
+
+# --- then report the results in the database ---
+toolbox_report_result $TEST $result
diff --git a/tests/keyctl/watch/noargs/runtest.sh b/tests/keyctl/watch/noargs/runtest.sh
new file mode 100644
index 0000000..e2bc7d7
--- /dev/null
+++ b/tests/keyctl/watch/noargs/runtest.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+. ../../../prepare.inc.sh
+. ../../../toolbox.inc.sh
+
+
+# ---- do the actual testing ----
+
+result=PASS
+echo "++++ BEGINNING TEST" >$OUTPUTFILE
+
+# check that no arguments fails correctly
+marker "WATCH NO ARGS"
+expect_args_error keyctl watch
+
+# check that two arguments fail correctly
+marker "WATCH TWO ARGS"
+expect_args_error keyctl watch @s @s
+
+# Try a dodgy filter
+marker "CHECK BAD FILTER"
+expect_args_error keyctl watch_key -f 0
+
+echo "++++ FINISHED TEST: $result" >>$OUTPUTFILE
+
+# --- then report the results in the database ---
+toolbox_report_result $TEST $result
diff --git a/tests/toolbox.inc.sh b/tests/toolbox.inc.sh
index 81d639c..609a6c7 100644
--- a/tests/toolbox.inc.sh
+++ b/tests/toolbox.inc.sh
@@ -1834,6 +1834,100 @@
 
 ###############################################################################
 #
+# watch a key
+#
+###############################################################################
+function watch_key ()
+{
+    my_exitval=0
+    if [ "x$1" = "x--fail" ]
+    then
+	my_exitval=1
+	shift
+    elif [ "x$1" = "x--fail2" ]
+    then
+	my_exitval=2
+	shift
+    fi
+
+    echo keyctl watch "$@" >>$OUTPUTFILE
+    nice --adjustment=-3 keyctl watch "$@" >>$PWD/notify.log 2>>$OUTPUTFILE
+    if [ $? != $my_exitval ]
+    then
+	failed
+    fi
+}
+
+###############################################################################
+#
+# Check for a notification
+#
+#	expect_notification [--filter=[i|p|l|n|c|r|v|s]] <keyid> <op> [<alt>]
+#
+###############################################################################
+function expect_notification ()
+{
+    local want
+
+    local filter=""
+    case "x$1" in
+	x--filter*)
+	    case $1 in
+		--filter=)  filter=;;
+		--filter=i) filter=inst;;
+		--filter=p) filter=upd;;
+		--filter=l) filter=link;;
+		--filter=n) filter=unlk;;
+		--filter=c) filter=clr;;
+		--filter=r) filter=rev;;
+		--filter=v) filter=inv;;
+		--filter=s) filter=attr;;
+		*)
+		    echo "Unknown param $1 to expect_notification()" >&2
+		    exit 2
+		    ;;
+	    esac
+	    shift
+	    ;;
+    esac
+    
+    if [ $# = 2 ]
+    then
+	want="$1 $2"
+	op=$2
+    elif [ $# = 3 ]
+    then
+	want="$1 $2 $3"
+	op=$2
+    else
+	echo "Wrong parameters to expect_notification" >&2
+	exit 2
+    fi
+
+    if tail -3 $PWD/notify.log | grep "^${want}\$" >/dev/null
+    then
+	echo "Found notification '$*'" >>$OUTPUTFILE
+	if [ "$filter" != "" -a $op != "$filter" ]
+	then
+	    echo "Notification '$want' should be filtered" >&2
+	    failed
+	fi
+    else
+	echo "Notification '$*' not present" >>$OUTPUTFILE
+	if [ "$filter" = "" ]
+	then
+	    echo "Missing notification '$want'" >&2
+	    failed
+	elif [ $op = "$filter" ]
+	then
+	    echo "Notification unexpectedly filtered '$want' $filter" >&2
+	    failed
+	fi
+    fi
+}
+
+###############################################################################
+#
 # Note the creation of a new key
 #
 #	expect_new_key <variable_name> <keyring> [<expected_id>]