diff --git a/authz/Makefile.objs b/authz/Makefile.objs
index 921fa624d7..8351bf181d 100644
--- a/authz/Makefile.objs
+++ b/authz/Makefile.objs
@@ -1,3 +1,4 @@
authz-obj-y += base.o
authz-obj-y += simple.o
authz-obj-y += list.o
+authz-obj-y += listfile.o
diff --git a/authz/listfile.c b/authz/listfile.c
new file mode 100644
index 0000000000..d4579767e7
--- /dev/null
+++ b/authz/listfile.c
@@ -0,0 +1,283 @@
+/*
+ * QEMU access control list file authorization driver
+ *
+ * Copyright (c) 2018 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ *
+ */
+
+#include "qemu/osdep.h"
+#include "authz/listfile.h"
+#include "authz/trace.h"
+#include "qemu/error-report.h"
+#include "qemu/main-loop.h"
+#include "qemu/sockets.h"
+#include "qemu/filemonitor.h"
+#include "qom/object_interfaces.h"
+#include "qapi/qapi-visit-authz.h"
+#include "qapi/qmp/qjson.h"
+#include "qapi/qmp/qobject.h"
+#include "qapi/qmp/qerror.h"
+#include "qapi/qobject-input-visitor.h"
+
+
+static bool
+qauthz_list_file_is_allowed(QAuthZ *authz,
+ const char *identity,
+ Error **errp)
+{
+ QAuthZListFile *fauthz = QAUTHZ_LIST_FILE(authz);
+ if (fauthz->list) {
+ return qauthz_is_allowed(fauthz->list, identity, errp);
+ }
+
+ return false;
+}
+
+
+static QAuthZ *
+qauthz_list_file_load(QAuthZListFile *fauthz, Error **errp)
+{
+ GError *err = NULL;
+ gchar *content = NULL;
+ gsize len;
+ QObject *obj = NULL;
+ QDict *pdict;
+ Visitor *v = NULL;
+ QAuthZ *ret = NULL;
+
+ trace_qauthz_list_file_load(fauthz, fauthz->filename);
+ if (!g_file_get_contents(fauthz->filename, &content, &len, &err)) {
+ error_setg(errp, "Unable to read '%s': %s",
+ fauthz->filename, err->message);
+ goto cleanup;
+ }
+
+ obj = qobject_from_json(content, errp);
+ if (!obj) {
+ goto cleanup;
+ }
+
+ pdict = qobject_to(QDict, obj);
+ if (!pdict) {
+ error_setg(errp, QERR_INVALID_PARAMETER_TYPE, "obj", "dict");
+ goto cleanup;
+ }
+
+ v = qobject_input_visitor_new(obj);
+
+ ret = (QAuthZ *)user_creatable_add_type(TYPE_QAUTHZ_LIST,
+ NULL, pdict, v, errp);
+
+ cleanup:
+ visit_free(v);
+ qobject_unref(obj);
+ if (err) {
+ g_error_free(err);
+ }
+ g_free(content);
+ return ret;
+}
+
+
+static void
+qauthz_list_file_event(int wd G_GNUC_UNUSED,
+ QFileMonitorEvent ev G_GNUC_UNUSED,
+ const char *name G_GNUC_UNUSED,
+ void *opaque)
+{
+ QAuthZListFile *fauthz = opaque;
+ Error *err = NULL;
+
+ if (ev != QFILE_MONITOR_EVENT_MODIFIED &&
+ ev != QFILE_MONITOR_EVENT_CREATED) {
+ return;
+ }
+
+ object_unref(OBJECT(fauthz->list));
+ fauthz->list = qauthz_list_file_load(fauthz, &err);
+ trace_qauthz_list_file_refresh(fauthz,
+ fauthz->filename, fauthz->list ? 1 : 0);
+ if (!fauthz->list) {
+ error_report_err(err);
+ }
+}
+
+static void
+qauthz_list_file_complete(UserCreatable *uc, Error **errp)
+{
+ QAuthZListFile *fauthz = QAUTHZ_LIST_FILE(uc);
+ gchar *dir = NULL, *file = NULL;
+
+ fauthz->list = qauthz_list_file_load(fauthz, errp);
+
+ if (!fauthz->refresh) {
+ return;
+ }
+
+ fauthz->file_monitor = qemu_file_monitor_new(errp);
+ if (!fauthz->file_monitor) {
+ return;
+ }
+
+ dir = g_path_get_dirname(fauthz->filename);
+ if (g_str_equal(dir, ".")) {
+ error_setg(errp, "Filename must be an absolute path");
+ goto cleanup;
+ }
+ file = g_path_get_basename(fauthz->filename);
+ if (g_str_equal(file, ".")) {
+ error_setg(errp, "Path has no trailing filename component");
+ goto cleanup;
+ }
+
+ fauthz->file_watch = qemu_file_monitor_add_watch(
+ fauthz->file_monitor, dir, file,
+ qauthz_list_file_event, fauthz, errp);
+ if (fauthz->file_watch < 0) {
+ goto cleanup;
+ }
+
+ cleanup:
+ g_free(file);
+ g_free(dir);
+}
+
+
+static void
+qauthz_list_file_prop_set_filename(Object *obj,
+ const char *value,
+ Error **errp G_GNUC_UNUSED)
+{
+ QAuthZListFile *fauthz = QAUTHZ_LIST_FILE(obj);
+
+ g_free(fauthz->filename);
+ fauthz->filename = g_strdup(value);
+}
+
+
+static char *
+qauthz_list_file_prop_get_filename(Object *obj,
+ Error **errp G_GNUC_UNUSED)
+{
+ QAuthZListFile *fauthz = QAUTHZ_LIST_FILE(obj);
+
+ return g_strdup(fauthz->filename);
+}
+
+
+static void
+qauthz_list_file_prop_set_refresh(Object *obj,
+ bool value,
+ Error **errp G_GNUC_UNUSED)
+{
+ QAuthZListFile *fauthz = QAUTHZ_LIST_FILE(obj);
+
+ fauthz->refresh = value;
+}
+
+
+static bool
+qauthz_list_file_prop_get_refresh(Object *obj,
+ Error **errp G_GNUC_UNUSED)
+{
+ QAuthZListFile *fauthz = QAUTHZ_LIST_FILE(obj);
+
+ return fauthz->refresh;
+}
+
+
+static void
+qauthz_list_file_finalize(Object *obj)
+{
+ QAuthZListFile *fauthz = QAUTHZ_LIST_FILE(obj);
+
+ object_unref(OBJECT(fauthz->list));
+ g_free(fauthz->filename);
+ qemu_file_monitor_free(fauthz->file_monitor);
+}
+
+
+static void
+qauthz_list_file_class_init(ObjectClass *oc, void *data)
+{
+ UserCreatableClass *ucc = USER_CREATABLE_CLASS(oc);
+ QAuthZClass *authz = QAUTHZ_CLASS(oc);
+
+ ucc->complete = qauthz_list_file_complete;
+
+ object_class_property_add_str(oc, "filename",
+ qauthz_list_file_prop_get_filename,
+ qauthz_list_file_prop_set_filename,
+ NULL);
+ object_class_property_add_bool(oc, "refresh",
+ qauthz_list_file_prop_get_refresh,
+ qauthz_list_file_prop_set_refresh,
+ NULL);
+
+ authz->is_allowed = qauthz_list_file_is_allowed;
+}
+
+
+static void
+qauthz_list_file_init(Object *obj)
+{
+ QAuthZListFile *authz = QAUTHZ_LIST_FILE(obj);
+
+ authz->file_watch = -1;
+#ifdef CONFIG_INOTIFY1
+ authz->refresh = TRUE;
+#endif
+}
+
+
+QAuthZListFile *qauthz_list_file_new(const char *id,
+ const char *filename,
+ bool refresh,
+ Error **errp)
+{
+ return QAUTHZ_LIST_FILE(
+ object_new_with_props(TYPE_QAUTHZ_LIST_FILE,
+ object_get_objects_root(),
+ id, errp,
+ "filename", filename,
+ "refresh", refresh ? "yes" : "no",
+ NULL));
+}
+
+
+static const TypeInfo qauthz_list_file_info = {
+ .parent = TYPE_QAUTHZ,
+ .name = TYPE_QAUTHZ_LIST_FILE,
+ .instance_init = qauthz_list_file_init,
+ .instance_size = sizeof(QAuthZListFile),
+ .instance_finalize = qauthz_list_file_finalize,
+ .class_size = sizeof(QAuthZListFileClass),
+ .class_init = qauthz_list_file_class_init,
+ .interfaces = (InterfaceInfo[]) {
+ { TYPE_USER_CREATABLE },
+ { }
+ }
+};
+
+
+static void
+qauthz_list_file_register_types(void)
+{
+ type_register_static(&qauthz_list_file_info);
+}
+
+
+type_init(qauthz_list_file_register_types);
diff --git a/authz/trace-events b/authz/trace-events
index a896d876e8..fb65349a90 100644
--- a/authz/trace-events
+++ b/authz/trace-events
@@ -9,3 +9,7 @@ qauthz_simple_is_allowed(void *authz, const char *wantidentity, const char *goti
# auth/list.c
qauthz_list_check_rule(void *authz, const char *identity, const char *rule, int format, int policy) "AuthZ list %p check rule=%s identity=%s format=%d policy=%d"
qauthz_list_default_policy(void *authz, const char *identity, int policy) "AuthZ list %p default identity=%s policy=%d"
+
+# auth/listfile.c
+qauthz_list_file_load(void *authz, const char *filename) "AuthZ file %p load filename=%s"
+qauthz_list_file_refresh(void *authz, const char *filename, int success) "AuthZ file %p load filename=%s success=%d"
diff --git a/include/authz/listfile.h b/include/authz/listfile.h
new file mode 100644
index 0000000000..bcc8d80743
--- /dev/null
+++ b/include/authz/listfile.h
@@ -0,0 +1,111 @@
+/*
+ * QEMU list file authorization driver
+ *
+ * Copyright (c) 2018 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ *
+ */
+
+#ifndef QAUTHZ_LIST_FILE_H__
+#define QAUTHZ_LIST_FILE_H__
+
+#include "authz/list.h"
+#include "qapi/qapi-types-authz.h"
+#include "qemu/filemonitor.h"
+
+#define TYPE_QAUTHZ_LIST_FILE "authz-list-file"
+
+#define QAUTHZ_LIST_FILE_CLASS(klass) \
+ OBJECT_CLASS_CHECK(QAuthZListFileClass, (klass), \
+ TYPE_QAUTHZ_LIST_FILE)
+#define QAUTHZ_LIST_FILE_GET_CLASS(obj) \
+ OBJECT_GET_CLASS(QAuthZListFileClass, (obj), \
+ TYPE_QAUTHZ_LIST_FILE)
+#define QAUTHZ_LIST_FILE(obj) \
+ INTERFACE_CHECK(QAuthZListFile, (obj), \
+ TYPE_QAUTHZ_LIST_FILE)
+
+typedef struct QAuthZListFile QAuthZListFile;
+typedef struct QAuthZListFileClass QAuthZListFileClass;
+
+
+/**
+ * QAuthZListFile:
+ *
+ * This authorization driver provides a file mechanism
+ * for granting access by matching user names against a
+ * file of globs. Each match rule has an associated policy
+ * and a catch all policy applies if no rule matches
+ *
+ * To create an instance of this class via QMP:
+ *
+ * {
+ * "execute": "object-add",
+ * "arguments": {
+ * "qom-type": "authz-list-file",
+ * "id": "authz0",
+ * "props": {
+ * "filename": "/etc/qemu/myvm-vnc.acl",
+ * "refresh": true
+ * }
+ * }
+ * }
+ *
+ * If 'refresh' is 'yes', inotify is used to monitor for changes
+ * to the file and auto-reload the rules.
+ *
+ * The myvm-vnc.acl file should contain the parameters for
+ * the QAuthZList object in JSON format:
+ *
+ * {
+ * "rules": [
+ * { "match": "fred", "policy": "allow", "format": "exact" },
+ * { "match": "bob", "policy": "allow", "format": "exact" },
+ * { "match": "danb", "policy": "deny", "format": "exact" },
+ * { "match": "dan*", "policy": "allow", "format": "glob" }
+ * ],
+ * "policy": "deny"
+ * }
+ *
+ * The object can be created on the command line using
+ *
+ * -object authz-list-file,id=authz0,\
+ * filename=/etc/qemu/myvm-vnc.acl,refresh=yes
+ *
+ */
+struct QAuthZListFile {
+ QAuthZ parent_obj;
+
+ QAuthZ *list;
+ char *filename;
+ bool refresh;
+ QFileMonitor *file_monitor;
+ int file_watch;
+};
+
+
+struct QAuthZListFileClass {
+ QAuthZClass parent_class;
+};
+
+
+QAuthZListFile *qauthz_list_file_new(const char *id,
+ const char *filename,
+ bool refresh,
+ Error **errp);
+
+
+#endif /* QAUTHZ_LIST_FILE_H__ */
+
diff --git a/qemu-options.hx b/qemu-options.hx
index 6d9344792d..217662acb0 100644
--- a/qemu-options.hx
+++ b/qemu-options.hx
@@ -4389,6 +4389,52 @@ would look like:
Note the use of quotes due to the x509 distinguished name containing
whitespace, and escaping of ','.
+@item -object authz-listfile,id=@var{id},filename=@var{path},refresh=@var{yes|no}
+
+Create an authorization object that will control access to network services.
+
+The @option{filename} parameter is the fully qualified path to a file
+containing the access control list rules in JSON format.
+
+An example set of rules that match against SASL usernames might look
+like:
+
+@example
+ @{
+ "rules": [
+ @{ "match": "fred", "policy": "allow", "format": "exact" @},
+ @{ "match": "bob", "policy": "allow", "format": "exact" @},
+ @{ "match": "danb", "policy": "deny", "format": "glob" @},
+ @{ "match": "dan*", "policy": "allow", "format": "exact" @},
+ ],
+ "policy": "deny"
+ @}
+@end example
+
+When checking access the object will iterate over all the rules and
+the first rule to match will have its @option{policy} value returned
+as the result. If no rules match, then the default @option{policy}
+value is returned.
+
+The rules can either be an exact string match, or they can use the
+simple UNIX glob pattern matching to allow wildcards to be used.
+
+If @option{refresh} is set to true the file will be monitored
+and automatically reloaded whenever its content changes.
+
+As with the @code{authz-simple} object, the format of the identity
+strings being matched depends on the network service, but is usually
+a TLS x509 distinguished name, or a SASL username.
+
+An example authorization object to validate a SASL username
+would look like:
+@example
+ # $QEMU \
+ ...
+ -object authz-simple,id=auth0,filename=/etc/qemu/vnc-sasl.acl,refresh=yes
+ ...
+@end example
+
@end table
ETEXI
diff --git a/tests/Makefile.include b/tests/Makefile.include
index 77a8c48c9b..9b63d4743c 100644
--- a/tests/Makefile.include
+++ b/tests/Makefile.include
@@ -118,6 +118,7 @@ check-unit-$(CONFIG_INOTIFY1) += tests/test-util-filemonitor$(EXESUF)
check-unit-y += tests/test-util-sockets$(EXESUF)
check-unit-y += tests/test-authz-simple$(EXESUF)
check-unit-y += tests/test-authz-list$(EXESUF)
+check-unit-y += tests/test-authz-listfile$(EXESUF)
check-unit-y += tests/test-io-task$(EXESUF)
check-unit-y += tests/test-io-channel-socket$(EXESUF)
check-unit-y += tests/test-io-channel-file$(EXESUF)
@@ -667,6 +668,7 @@ tests/test-util-sockets$(EXESUF): tests/test-util-sockets.o \
tests/socket-helpers.o $(test-util-obj-y)
tests/test-authz-simple$(EXESUF): tests/test-authz-simple.o $(test-authz-obj-y)
tests/test-authz-list$(EXESUF): tests/test-authz-list.o $(test-authz-obj-y)
+tests/test-authz-listfile$(EXESUF): tests/test-authz-listfile.o $(test-authz-obj-y)
tests/test-io-task$(EXESUF): tests/test-io-task.o $(test-io-obj-y)
tests/test-io-channel-socket$(EXESUF): tests/test-io-channel-socket.o \
tests/io-channel-helpers.o tests/socket-helpers.o $(test-io-obj-y)
diff --git a/tests/test-authz-listfile.c b/tests/test-authz-listfile.c
new file mode 100644
index 0000000000..1e452fef6d
--- /dev/null
+++ b/tests/test-authz-listfile.c
@@ -0,0 +1,195 @@
+/*
+ * QEMU list authorization object tests
+ *
+ * Copyright (c) 2018 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ *
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/main-loop.h"
+#include "authz/listfile.h"
+
+static char *workdir;
+
+static gchar *qemu_authz_listfile_test_save(const gchar *name,
+ const gchar *cfg)
+{
+ gchar *path = g_strdup_printf("%s/default-deny.cfg", workdir);
+ GError *gerr = NULL;
+
+ if (!g_file_set_contents(path, cfg, -1, &gerr)) {
+ g_printerr("Unable to save config %s: %s\n",
+ path, gerr->message);
+ g_error_free(gerr);
+ g_free(path);
+ rmdir(workdir);
+ abort();
+ }
+
+ return path;
+}
+
+static void test_authz_default_deny(void)
+{
+ gchar *file = qemu_authz_listfile_test_save(
+ "default-deny.cfg",
+ "{ \"policy\": \"deny\" }");
+ Error *local_err = NULL;
+
+ QAuthZListFile *auth = qauthz_list_file_new("auth0",
+ file, false,
+ &local_err);
+ unlink(file);
+ g_free(file);
+ g_assert(local_err == NULL);
+ g_assert(!qauthz_is_allowed(QAUTHZ(auth), "fred", &error_abort));
+
+ object_unparent(OBJECT(auth));
+}
+
+static void test_authz_default_allow(void)
+{
+ gchar *file = qemu_authz_listfile_test_save(
+ "default-allow.cfg",
+ "{ \"policy\": \"allow\" }");
+ Error *local_err = NULL;
+
+ QAuthZListFile *auth = qauthz_list_file_new("auth0",
+ file, false,
+ &local_err);
+ unlink(file);
+ g_free(file);
+ g_assert(local_err == NULL);
+ g_assert(qauthz_is_allowed(QAUTHZ(auth), "fred", &error_abort));
+
+ object_unparent(OBJECT(auth));
+}
+
+static void test_authz_explicit_deny(void)
+{
+ gchar *file = qemu_authz_listfile_test_save(
+ "explicit-deny.cfg",
+ "{ \"rules\": [ "
+ " { \"match\": \"fred\","
+ " \"policy\": \"deny\","
+ " \"format\": \"exact\" } ],"
+ " \"policy\": \"allow\" }");
+ Error *local_err = NULL;
+
+ QAuthZListFile *auth = qauthz_list_file_new("auth0",
+ file, false,
+ &local_err);
+ unlink(file);
+ g_free(file);
+ g_assert(local_err == NULL);
+
+ g_assert(!qauthz_is_allowed(QAUTHZ(auth), "fred", &error_abort));
+
+ object_unparent(OBJECT(auth));
+}
+
+static void test_authz_explicit_allow(void)
+{
+ gchar *file = qemu_authz_listfile_test_save(
+ "explicit-allow.cfg",
+ "{ \"rules\": [ "
+ " { \"match\": \"fred\","
+ " \"policy\": \"allow\","
+ " \"format\": \"exact\" } ],"
+ " \"policy\": \"deny\" }");
+ Error *local_err = NULL;
+
+ QAuthZListFile *auth = qauthz_list_file_new("auth0",
+ file, false,
+ &local_err);
+ unlink(file);
+ g_free(file);
+ g_assert(local_err == NULL);
+
+ g_assert(qauthz_is_allowed(QAUTHZ(auth), "fred", &error_abort));
+
+ object_unparent(OBJECT(auth));
+}
+
+
+static void test_authz_complex(void)
+{
+ gchar *file = qemu_authz_listfile_test_save(
+ "complex.cfg",
+ "{ \"rules\": [ "
+ " { \"match\": \"fred\","
+ " \"policy\": \"allow\","
+ " \"format\": \"exact\" },"
+ " { \"match\": \"bob\","
+ " \"policy\": \"allow\","
+ " \"format\": \"exact\" },"
+ " { \"match\": \"dan\","
+ " \"policy\": \"deny\","
+ " \"format\": \"exact\" },"
+ " { \"match\": \"dan*\","
+ " \"policy\": \"allow\","
+ " \"format\": \"glob\" } ],"
+ " \"policy\": \"deny\" }");
+
+ Error *local_err = NULL;
+
+ QAuthZListFile *auth = qauthz_list_file_new("auth0",
+ file, false,
+ &local_err);
+ unlink(file);
+ g_free(file);
+ g_assert(local_err == NULL);
+
+ g_assert(qauthz_is_allowed(QAUTHZ(auth), "fred", &error_abort));
+ g_assert(qauthz_is_allowed(QAUTHZ(auth), "bob", &error_abort));
+ g_assert(!qauthz_is_allowed(QAUTHZ(auth), "dan", &error_abort));
+ g_assert(qauthz_is_allowed(QAUTHZ(auth), "danb", &error_abort));
+
+ object_unparent(OBJECT(auth));
+}
+
+
+int main(int argc, char **argv)
+{
+ int ret;
+ GError *gerr = NULL;
+
+ g_test_init(&argc, &argv, NULL);
+
+ module_call_init(MODULE_INIT_QOM);
+
+ workdir = g_dir_make_tmp("qemu-test-authz-listfile-XXXXXX",
+ &gerr);
+ if (!workdir) {
+ g_printerr("Unable to create temporary dir: %s\n",
+ gerr->message);
+ g_error_free(gerr);
+ abort();
+ }
+
+ g_test_add_func("/auth/list/default/deny", test_authz_default_deny);
+ g_test_add_func("/auth/list/default/allow", test_authz_default_allow);
+ g_test_add_func("/auth/list/explicit/deny", test_authz_explicit_deny);
+ g_test_add_func("/auth/list/explicit/allow", test_authz_explicit_allow);
+ g_test_add_func("/auth/list/complex", test_authz_complex);
+
+ ret = g_test_run();
+
+ rmdir(workdir);
+ g_free(workdir);
+
+ return ret;
+}