{"thread":{"id":"31250","subject":"[PATCH] Implement ACL module architecture and sample MySQL ACL module","startedAt":"2012-08-14T09:59:25Z","lastAt":"2012-08-21T10:57:35Z","messageCount":9,"participants":["Michal Novotny","Junio C Hamano","Shawn Pearce"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"196968","messageId":"feafacf49186d7cf0eed0002a82289b318f56ff8.1344938189.git.minovotn@redhat.com","threadId":"31250","inReplyTo":null,"subject":"[PATCH] Implement ACL module architecture and sample MySQL ACL module","fromName":"Michal Novotny","fromEmail":"minovotn@redhat.com","sentAt":"2012-08-14T09:59:25Z","receivedAt":"2012-08-14T09:59:25Z","isPatch":true,"sender":{"key":"minovotn@redhat.com","avatar":null},"body":"Hi,\nthis is the patch to introduce the ACL module architecture into git\nversioning system. The variable of GIT_BASE_DIR is being used to seek\nfor the modules if available. If variable is unset then daemon looks\nfor the /etc/git-daemon.conf file existence and reads the\n'base_path' key if it exists to determine the root path for\nthe git-daemon. This will be referred to as to $GIT_BASE_DIR\ndirectory and the ACL modules subdirectory should reside in path of\n$GIT_BASE_DIR/modules/acl. For MySQL connection you have to\nalter the modules/config/modgitacl_mysql.cfg file to be able\nto connect to the MySQL server using your credentials.\n\nFor MySQL database connection 2 new tables will get created\non your server and in the desired database. One holds the\nACLs for IP addresses and repositories and the second is\noptional logging to the history table. This is by default\ndisabled however it could be enabled by setting the \"log_history\"\ncolumn of the git_access_acl entry to 1.\n\nThe patch also introduces the global_access_rule key to the config\nwhich defines the default access policy if address and repository\nkey combination doesn't exist in the database. This automatically\ncreates the default access rule for the repository (which is\nbasically an entry with addr field set to '%' as all possible\nentries in this column) based on the key settings.\n\nThe search algorithm for the entries is as follows:\n1) Try to find the default entry for the repository\n   - If default entry doesn't exist then create it based on the\n     global_access_rule key settings (if set and valid)\n2) Try to find entry for repository and IP address to override\n   default settings\n3) Log entries access if appropriate\n\nIf the new default access rule is being created then logging is\nenabled for 'deny' global_access_rule and disabled for 'allow'\nglobal_access_rule settings.\n\nThe MySQL module is just the working example of how to use the\nACL architecture so it's not being compiled by default and user\nhas to compile it manually. Basically this module is showing how\ngit ACL infrastructure works and it's good point to start writing\nsuch modules.\n\nThe patch has been tested using following command line on one terminal:\n\n$ make && (cd modules/acl && make && cd -) && GIT_BASE_DIR=`pwd` ./git-daemon --export-all\n\nand cloning the existing repository on the second terminal with testing\nall possible options in the database (using the sample MySQL module)\nand everything was working fine.\n\nThanks,\nMichal\n\nSigned-off-by: Michal Novotny <minovotn@redhat.com>\n---\n Makefile                           |   1 +\n daemon.c                           | 123 +++++++++++++++++++++\n modules/acl/Makefile               |   6 ++\n modules/acl/modgitacl_mysql.c      | 211 +++++++++++++++++++++++++++++++++++++\n modules/config/modgitacl_mysql.cfg |  12 +++\n modules/modules.h                  |   9 ++\n 6 files changed, 362 insertions(+)\n create mode 100644 modules/acl/Makefile\n create mode 100644 modules/acl/modgitacl_mysql.c\n create mode 100644 modules/config/modgitacl_mysql.cfg\n create mode 100644 modules/modules.h\n\ndiff --git a/Makefile b/Makefile\nindex 6b0c961..6fe32a7 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -932,6 +932,7 @@ ifeq ($(uname_S),OSF1)\n \tNO_NSEC = YesPlease\n endif\n ifeq ($(uname_S),Linux)\n+\tBASIC_CFLAGS += -ldl -rdynamic\n \tNO_STRLCPY = YesPlease\n \tNO_MKSTEMPS = YesPlease\n \tHAVE_PATHS_H = YesPlease\ndiff --git a/daemon.c b/daemon.c\nindex ab21e66..8a06c05 100644\n--- a/daemon.c\n+++ b/daemon.c\n@@ -1,9 +1,14 @@\n+#define\tGIT_DAEMON_CONFIG_FILE\t\t\t\"/etc/git-daemon.conf\"\n+#define\tGIT_DAEMON_CONFIG_FILE_PATH_KEY\t\t\"base_path\"\n+#define\tBUFSIZE\t\t\t\t\t1 << 12\n+\n #include \"cache.h\"\n #include \"pkt-line.h\"\n #include \"exec_cmd.h\"\n #include \"run-command.h\"\n #include \"strbuf.h\"\n #include \"string-list.h\"\n+#include \"modules/modules.h\"\n \n #ifndef HOST_NAME_MAX\n #define HOST_NAME_MAX 256\n@@ -256,6 +261,117 @@ static int daemon_error(const char *dir, const char *msg)\n \treturn -1;\n }\n \n+static char* daemon_read_config(const char *filename, char *key)\n+{\n+\tFILE *fp;\n+\tchar line[BUFSIZE];\n+\n+\tfp = fopen(filename, \"r\");\n+\tif (fp == NULL)\n+\t\treturn NULL;\n+\n+\twhile (!feof(fp)) {\n+\t\tfgets(line, sizeof(line), fp);\n+\n+\t\tif (strncmp(line, key, strlen(key)) == 0) {\n+\t\t\treturn strdup( line + strlen(key) + 3 );\n+\t\t}\n+\t}\n+\tfclose(fp);\n+\n+\treturn NULL;\n+}\n+\n+static int check_access_addrdir(char *libname, char *base_path, char *addr, const char *dir)\n+{\n+\tint ret = -EPERM;\n+\tvoid *lib = NULL;\n+\tvoid *pCheck = NULL;\n+\ttypedef int (*tCheckFunc) (char *base_path, char *addr, const char *dir);\n+\n+\tlib = dlopen(libname, RTLD_LAZY);\n+\tif (lib == NULL) {\n+\t\tlogerror(\"%s: Cannot load ACL library '%s'\", __FUNCTION__, libname);\n+\t\tgoto cleanup;\n+\t}\n+\n+\tpCheck = dlsym(lib, \"check_access_addrdir\");\n+\tif (pCheck == NULL) {\n+\t\tlogerror(\"%s: Cannot read check_access_addrdir symbol from ACL library %s\",\n+\t\t\t__FUNCTION__, libname);\n+\t\tgoto cleanup;\n+\t}\n+\n+\ttCheckFunc fCheck = (tCheckFunc) pCheck;\n+\tif (fCheck == NULL)\n+\t\tgoto cleanup;\n+\n+\tret = fCheck(base_path, addr, dir);\n+\n+\tif (ret == -EINVAL) {\n+\t\tlogerror(\"%s: Module '%s' complained about invalid parameters. Allowing access for now\",\n+\t\t\t__FUNCTION__, libname);\n+\t\tret = 0;\n+\t\tgoto cleanup;\n+\t}\n+\n+cleanup:\n+\tif (lib != NULL)\n+\t\tdlclose(lib);\n+\n+\treturn ret;\n+}\n+\n+static int check_access_by_all_modules(const char *dir)\n+{\n+\tchar fn[NAME_MAX+1];\n+\tchar *directory = NULL;\n+\tchar *daemon_path = NULL;\n+\tstruct dirent *entry;\n+\tint ret = -EPERM;\n+\tDIR *d;\n+\t\n+\tif (getenv(GIT_BASE_DIR_ENVVAR_NAME) != NULL) {\n+\t\tdaemon_path = strdup( getenv(GIT_BASE_DIR_ENVVAR_NAME) );\n+\t\tsnprintf(fn, sizeof(fn), \"%s/modules/acl\", daemon_path);\n+\t}\n+\telse {\n+\t\tif (access(GIT_DAEMON_CONFIG_FILE, R_OK) == 0) {\n+\t\t\tif ((daemon_path = daemon_read_config(GIT_DAEMON_CONFIG_FILE,\n+\t\t\t\tGIT_DAEMON_CONFIG_FILE_PATH_KEY)) == NULL)\n+\t\t\t\treturn daemon_error(\"Cannot read config file %s\",\n+\t\t\t\t\t\t\tGIT_DAEMON_CONFIG_FILE);\n+\n+\t\t\tTRIM_LAST_CHAR_RETURN(daemon_path);\n+\n+\t\t\tsnprintf(fn, sizeof(fn), \"%s/modules/acl\", daemon_path);\n+\t\t}\n+\t\telse {\n+\t\t\tlogerror(\"%s: Cannot open module path. Please include '%s' key into '%s' file\",\n+\t\t\t\t\t__FUNCTION__, GIT_DAEMON_CONFIG_FILE_PATH_KEY, GIT_DAEMON_CONFIG_FILE);\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\n+\tdirectory = strdup(fn);\n+\n+\td = opendir(directory);\n+\tif (d == NULL)\n+\t\treturn 0;\n+\n+\tsetenv(GIT_BASE_DIR_ENVVAR_NAME, directory, 1);\n+\twhile ((entry = readdir(d)) != NULL) {\n+\t\tif (strstr(entry->d_name, \".so\") != NULL) {\n+\t\t\tsnprintf(fn, sizeof(fn), \"%s/%s\", directory, entry->d_name);\n+\t\t\tif ((ret = check_access_addrdir(fn, daemon_path, getenv(\"REMOTE_ADDR\"), dir)) < 0)\n+\t\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tclosedir(d);\n+\treturn ret;\n+}\n+\n static int run_service(char *dir, struct daemon_service *service)\n {\n \tconst char *path;\n@@ -289,6 +405,13 @@ static int run_service(char *dir, struct daemon_service *service)\n \t\treturn daemon_error(dir, \"repository not exported\");\n \t}\n \n+\t/* Address-based and repository-based access */\n+\tif (check_access_by_all_modules( dir ) != 0) {\n+\t\tlogerror(\"'%s': repository access denied\", path);\n+\t\terrno = EACCES;\n+\t\treturn daemon_error(dir, \"repository access denied\");\n+\t}\n+\n \tif (service->overridable) {\n \t\tservice_looking_at = service;\n \t\tservice_enabled = -1;\ndiff --git a/modules/acl/Makefile b/modules/acl/Makefile\nnew file mode 100644\nindex 0000000..2b5cd7e\n--- /dev/null\n+++ b/modules/acl/Makefile\n@@ -0,0 +1,6 @@\n+all:\tgitacl_mysql\n+\n+gitacl_mysql:\n+\t$(CC) -c -fPIC modgitacl_mysql.c\n+\t$(CC) -shared -fPIC -o modgitacl_mysql.so modgitacl_mysql.o `mysql_config --libs` `mysql_config --cflags`\n+\trm -f modgitacl_mysql.o\ndiff --git a/modules/acl/modgitacl_mysql.c b/modules/acl/modgitacl_mysql.c\nnew file mode 100644\nindex 0000000..f58eeba\n--- /dev/null\n+++ b/modules/acl/modgitacl_mysql.c\n@@ -0,0 +1,211 @@\n+#include <stdio.h>\n+#include <errno.h>\n+#include <string.h>\n+#include <stdlib.h>\n+#include <mysql/mysql.h>\n+#include \"../modules.h\"\n+\n+#define BUFSIZE\t\t1 << 13\n+#define DEFAULT_TABLE_ACL\t\"git_access_acl\"\n+#define DEFAULT_TABLE_HISTORY\t\"git_access_history\"\n+#define CONFIG_FILE\t\t\"modgitacl_mysql.cfg\"\n+\n+#define\tREPO_ACCESS_UNSET\t0x00\n+#define\tREPO_ACCESS_UNKNOWN\t0x01\n+#define\tREPO_ACCESS_ALLOW\t0x02\n+#define\tREPO_ACCESS_DENY\t0x04\n+\n+char\t*base_path = NULL;\n+int\tglobalRepositoryAccess = REPO_ACCESS_UNSET;\n+\n+/* MySQL Database related settings */\n+char\t*server = NULL;\n+char\t*username = NULL;\n+char\t*password = NULL;\n+char\t*database = NULL;\n+char\t*table_acl = NULL;\n+char\t*table_history = NULL;\n+\n+int ensure_table_exists(MYSQL sql)\n+{\n+\tchar qry[BUFSIZE];\n+\n+\t/* Ensure ACL table existence */\n+\tsnprintf(qry, sizeof(qry), \"CREATE TABLE IF NOT EXISTS %s (id int(11) AUTO_INCREMENT, addr varchar(128) NOT NULL, repository varchar(255) NOT NULL, \"\n+\t\t\t\t\t\"enabled tinyint(1) NOT NULL DEFAULT '1',  log_history tinyint(1) NOT NULL DEFAULT '0', added timestamp NOT NULL \"\n+\t\t\t\t\t\"DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id))\",\n+\t\t\t\t\ttable_acl ? table_acl : DEFAULT_TABLE_ACL);\n+\n+\tmysql_real_query(&sql, qry, strlen(qry));\n+\n+\t/* Ensure history table existence */\n+\tsnprintf(qry, sizeof(qry), \"CREATE TABLE IF NOT EXISTS %s (id int(11) AUTO_INCREMENT, addr varchar(128) NOT NULL, repository varchar(255) NOT NULL, \"\n+\t\t\t\t\t\" granted tinyint(1) NOT NULL, time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id))\",\n+\t\t\t\t\ttable_history ? table_history : DEFAULT_TABLE_HISTORY);\n+\n+\tmysql_real_query(&sql, qry, strlen(qry));\n+}\n+\n+int load_mysql_settings()\n+{\n+\tchar cfgfile[BUFSIZE];\n+\tchar line[BUFSIZE];\n+\tFILE *fp;\n+\n+\tif (base_path == NULL)\n+\t\treturn -EINVAL;\n+\n+\tsnprintf(cfgfile, sizeof(cfgfile), \"%s/modules/config/%s\", base_path, CONFIG_FILE);\n+\tfp = fopen(cfgfile, \"r\");\n+\tif (fp == NULL)\n+\t\treturn -EPERM;\n+\n+\twhile (!feof(fp)) {\n+\t\tfgets(line, sizeof(line), fp);\n+\n+\t\tif (strncmp(line, \"Server = \", 9) == 0)\n+\t\t\tserver = strdup( line + 9 );\n+\t\telse\n+\t\tif (strncmp(line, \"Username = \", 11) == 0)\n+\t\t\tusername = strdup( line + 11 );\n+\t\telse\n+\t\tif (strncmp(line, \"Password = \", 11) == 0)\n+\t\t\tpassword = strdup( line + 11 );\n+\t\telse\n+\t\tif (strncmp(line, \"Database = \", 11) == 0)\n+\t\t\tdatabase = strdup( line + 11 );\n+\t\telse\n+\t\tif (strncmp(line, \"Table_acl = \", 12) == 0)\n+\t\t\ttable_acl = strdup( line + 12 );\n+\t\telse\n+\t\tif (strncmp(line, \"Table_history = \", 16) == 0)\n+\t\t\ttable_history = strdup( line + 16 );\n+\t\telse\n+\t\tif (strncmp(line, \"Global_access_rule = \", 21) == 0) {\n+\t\t\tchar *tmp = strdup( line + 21 );\n+\n+\t\t\tif (strncmp(tmp, \"allow\", 5) == 0)\n+\t\t\t\tglobalRepositoryAccess = REPO_ACCESS_ALLOW;\n+\t\t\telse\n+\t\t\tif (strncmp(tmp, \"deny\", 4) == 0)\n+\t\t\t\tglobalRepositoryAccess = REPO_ACCESS_DENY;\n+\t\t\telse\n+\t\t\t\tglobalRepositoryAccess = REPO_ACCESS_UNKNOWN;\n+\n+\t\t\tfree(tmp);\n+\t\t}\n+\t}\n+\tfclose(fp);\n+\n+\tTRIM_LAST_CHAR_RETURN(server);\n+\tTRIM_LAST_CHAR_RETURN(username);\n+\tTRIM_LAST_CHAR_RETURN(password);\n+\tTRIM_LAST_CHAR_RETURN(database);\n+\tTRIM_LAST_CHAR_RETURN(table_acl);\n+\tTRIM_LAST_CHAR_RETURN(table_history);\n+\n+\treturn 0;\n+}\n+\n+void free_all(void)\n+{\n+\tfree(server);\n+\tfree(username);\n+\tfree(password);\n+\tfree(database);\n+\tfree(table_acl);\n+\tfree(table_history);\n+\n+\tserver = NULL;\n+\tusername = NULL;\n+\tpassword = NULL;\n+\tdatabase = NULL;\n+\ttable_acl = NULL;\n+\ttable_history = NULL;\n+}\n+\n+int check_access_addrdir(char *daemon_base_path, char *addr, char *dir)\n+{\n+\tMYSQL sql;\n+\tMYSQL_RES *res;\n+\tMYSQL_ROW row;\n+\tint ret = 0;\n+\tint log = 0;\n+\tchar qry[BUFSIZE];\n+\n+\tbase_path = (daemon_base_path ? daemon_base_path : getenv(GIT_BASE_DIR_ENVVAR_NAME));\n+\tif (base_path == NULL)\n+\t\treturn ret;\n+\n+\tif (mysql_init(&sql) == NULL)\n+\t\treturn ret;\n+\n+\tif (load_mysql_settings() != 0)\n+\t\tgoto cleanup;\n+\n+\tif (globalRepositoryAccess == REPO_ACCESS_UNKNOWN) {\n+\t\tret = -EINVAL;\n+\t\tgoto cleanup;\n+\t}\n+\n+\tif (!mysql_real_connect(&sql, server, username, password, database, 0, NULL, 0))\n+\t\treturn ret;\n+\n+\tensure_table_exists(sql);\n+\n+\t/* Check the default settings for repository */\n+\tsnprintf(qry, sizeof(qry), \"SELECT enabled, log_history FROM %s WHERE addr = '%%' AND repository = '%s'\",\n+\t\t\t\ttable_acl ? table_acl : DEFAULT_TABLE_ACL, dir);\n+\n+\tif (mysql_real_query(&sql, qry, strlen(qry)) != 0)\n+\t\tgoto cleanup;\n+\n+\tres = mysql_store_result(&sql);\n+\tif (mysql_num_rows(res) > 0) {\n+\t\trow = mysql_fetch_row(res);\n+\t\tret = (atoi(row[0]) == 1) ? 0 : -EPERM;\n+\t\tlog = atoi(row[1]);\n+\t}\n+\telse\n+\tif (globalRepositoryAccess != REPO_ACCESS_UNSET) {\n+\t\tsnprintf(qry, sizeof(qry), \"INSERT INTO %s(addr, repository, enabled, log_history) VALUES('%%', '%s', %d, %d)\",\n+\t\t\ttable_acl ? table_acl : DEFAULT_TABLE_ACL, dir,\n+\t\t\t(globalRepositoryAccess == REPO_ACCESS_ALLOW),\n+\t\t\t(globalRepositoryAccess == REPO_ACCESS_DENY));\n+\n+\t\tmysql_real_query(&sql, qry, strlen(qry));\n+\n+\t\tret = (globalRepositoryAccess == REPO_ACCESS_ALLOW) ? 0 : -EPERM;\n+\t\tlog = (ret < 0);\n+\t}\n+\tmysql_free_result(res);\n+\n+\t/* Check the settings for IP address of access */\n+\tsnprintf(qry, sizeof(qry), \"SELECT enabled, log_history FROM %s WHERE addr = '%s' AND repository = '%s'\",\n+\t\t\t\ttable_acl ? table_acl : DEFAULT_TABLE_ACL, addr, dir);\n+\n+\tif (mysql_real_query(&sql, qry, strlen(qry)) != 0)\n+\t\tgoto cleanup;\n+\n+\tres = mysql_store_result(&sql);\n+\tif (mysql_num_rows(res) > 0) {\n+\t\trow = mysql_fetch_row(res);\n+\t\tret = (atoi(row[0]) == 1) ? 0 : -EPERM;\n+\t\tlog = atoi(row[1]);\n+\t}\n+\tmysql_free_result(res);\n+\n+\t/* If logging is enabled then log to the history table */\n+\tif (log) {\n+\t\tsnprintf(qry, sizeof(qry), \"INSERT INTO %s(addr, repository, granted) VALUES('%s', '%s', %d)\",\n+\t\t\t\ttable_history ? table_history : DEFAULT_TABLE_HISTORY, addr, dir, (ret == 0) ? 1 : 0);\n+\t\tmysql_real_query(&sql, qry, strlen(qry));\n+\t}\n+\n+cleanup:\n+\tfree_all();\n+\tmysql_close(&sql);\n+\n+\treturn ret;\n+}\n+\ndiff --git a/modules/config/modgitacl_mysql.cfg b/modules/config/modgitacl_mysql.cfg\nnew file mode 100644\nindex 0000000..e371f4e\n--- /dev/null\n+++ b/modules/config/modgitacl_mysql.cfg\n@@ -0,0 +1,12 @@\n+# MySQL server settings\n+Server = <server>\n+Username = <username>\n+Password = <password>\n+Database = <database>\n+\n+# Table names, those are default values\n+Table_acl = git_access_acl\n+Table_history = git_access_history\n+\n+# Global access rule can be only 'allow' or 'deny'\n+Global_access_rule = allow\ndiff --git a/modules/modules.h b/modules/modules.h\nnew file mode 100644\nindex 0000000..54c61dd\n--- /dev/null\n+++ b/modules/modules.h\n@@ -0,0 +1,9 @@\n+#ifndef GIT_MODULES\n+#define GIT_MODULES\n+\n+#include <dlfcn.h>\n+#define GIT_BASE_DIR_ENVVAR_NAME\t\"GIT_BASE_DIR\"\n+#define TRIM_LAST_CHAR(x)\t\tx[ strlen(x) - 1 ] = 0;\n+#define TRIM_LAST_CHAR_RETURN(x)\tif (x[strlen(x)-1] == '\\n') x[ strlen(x) - 1 ] = 0;\n+\n+#endif\n-- \n1.7.11.2\n"},{"id":"196981","messageId":"7v1uj98nbj.fsf@alter.siamese.dyndns.org","threadId":"31250","inReplyTo":"feafacf49186d7cf0eed0002a82289b318f56ff8.1344938189.git.minovotn@redhat.com","subject":"Re: [PATCH] Implement ACL module architecture and sample MySQL ACL module","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2012-08-14T16:12:32Z","receivedAt":"2012-08-14T16:12:32Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Michal Novotny <minovotn@redhat.com> writes:\n\n> Hi,\n> this is the patch to introduce the ACL module architecture into git\n> versioning system.\n\nNo, it doesn't.  It adds something only to \"git daemon\", but does\nnot affect any other uses of Git.\n\n    Side note: I am not saying other uses of Git must be ACL\n    controlled by MySQL database.  They shouldn't be.  I am only\n    saying that the proposed commit log message must match what the\n    change does.\n\nPlease familiarize yourself with Documentation/SubmittingPatches\nfirst, and then imitate the style in existing commits in the history\nand posted patches by the \"good\" developers (you can tell who they\nare by observing the list traffic for a few weeks), by the way.\n\nAs \"git daemon\" already has a mechanism to specify what repositories\nare served with whitelist or blacklist, I am not sure if this patch\nadds enough value to the system to make us want to add further\ncomplexity only to carry more code to be audited for security.\n\nOpinions?\n"},{"id":"196982","messageId":"CAJo=hJtYz3OX1C6HS7ivhJKBOSg=Ex3rKEdTYSbcDfFT1Jh4hw@mail.gmail.com","threadId":"31250","inReplyTo":"7v1uj98nbj.fsf@alter.siamese.dyndns.org","subject":"Re: [PATCH] Implement ACL module architecture and sample MySQL ACL module","fromName":"Shawn Pearce","fromEmail":"spearce@spearce.org","sentAt":"2012-08-14T16:27:19Z","receivedAt":"2012-08-14T16:27:19Z","isPatch":true,"sender":{"key":"spearce@spearce.org","avatar":"https://avatars.githubusercontent.com/u/34844?v=4"},"body":"On Tue, Aug 14, 2012 at 9:12 AM, Junio C Hamano <gitster@pobox.com> wrote:\n> Michal Novotny <minovotn@redhat.com> writes:\n>\n>> Hi,\n>> this is the patch to introduce the ACL module architecture into git\n>> versioning system.\n>\n> No, it doesn't.  It adds something only to \"git daemon\", but does\n> not affect any other uses of Git.\n\nYes, this part of the commit message also confused me until I read\nthrough the patch further. :-(\n\n>     Side note: I am not saying other uses of Git must be ACL\n>     controlled by MySQL database.  They shouldn't be.  I am only\n>     saying that the proposed commit log message must match what the\n>     change does.\n>\n> Please familiarize yourself with Documentation/SubmittingPatches\n> first, and then imitate the style in existing commits in the history\n> and posted patches by the \"good\" developers (you can tell who they\n> are by observing the list traffic for a few weeks), by the way.\n>\n> As \"git daemon\" already has a mechanism to specify what repositories\n> are served with whitelist or blacklist, I am not sure if this patch\n> adds enough value to the system to make us want to add further\n> complexity only to carry more code to be audited for security.\n>\n> Opinions?\n\nTraditionally Git has been about providing the plumbing to handle the\nprotocol and storage, and other tools that wrap git manage access\ncontrols, e.g. UNIX filesystem or gitolite. I would strongly prefer to\nkeep that arrangement.\n\nParsing the request line of git-daemon is easy. But we could make it\neasier. An alternative arrangement would be to add a new command line\nflag to git daemon like --command-filter that names an executable\ngit-daemon will invoke after parsing the request line. It can pass\nalong the client IP address, command request, repository name, and\nresolved repository path, and tie stdin/stdout to the client. This\nbinary can decide to exec the proper git binary for the named command,\nor just exit to disconnect the client and refuse service. This makes\nit simple for a tool like gitolite to plug into the git-daemon\nauthorization path, without needing to be the network daemon itself,\nworry about number of active connection slots, etc.\n"},{"id":"196984","messageId":"7vsjbp768y.fsf@alter.siamese.dyndns.org","threadId":"31250","inReplyTo":"CAJo=hJtYz3OX1C6HS7ivhJKBOSg=Ex3rKEdTYSbcDfFT1Jh4hw@mail.gmail.com","subject":"Re: [PATCH] Implement ACL module architecture and sample MySQL ACL module","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2012-08-14T17:06:37Z","receivedAt":"2012-08-14T17:06:37Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Shawn Pearce <spearce@spearce.org> writes:\n\n> Parsing the request line of git-daemon is easy. But we could make it\n> easier. An alternative arrangement would be to add a new command line\n> flag to git daemon like --command-filter that names an executable\n> git-daemon will invoke after parsing the request line. It can pass\n> along the client IP address, command request, repository name, and\n> resolved repository path, and tie stdin/stdout to the client. This\n> binary can decide to exec the proper git binary for the named command,\n> or just exit to disconnect the client and refuse service. This makes\n> it simple for a tool like gitolite to plug into the git-daemon\n> authorization path, without needing to be the network daemon itself,\n> worry about number of active connection slots, etc.\n\nI think that is a good direction to go in, except that I am unsure\nwhat kind of conversation do you want to allow between the \"command\nfilter\" helper and the client by exposing standard input and output\nstream to to the helper.  If the client side has a matching \"pre\nnegotiate command\" helper support, then presumably the helpers can\ndiscuss what Git protocol proper does not care about before deciding\nto allow the connection go through, but until that happens, opening\nthe stdio streams up to the helper sounds like an accident waiting\nto happen to me (e.g. \"fetch-pack\" connects, the server side helper\nreads the first pkt-line from the client, says \"OK, you may proceed\"\nto the daemon, then the daemon spawns the \"upload-pack\", which will\nobviously see a corrupt request stream from \"fetch-pack\").\n"},{"id":"196989","messageId":"CAJo=hJu7W6JnNLYvahaQ43ZNqDtrurTOLCnLfZacVJKeL6VMFg@mail.gmail.com","threadId":"31250","inReplyTo":"7vsjbp768y.fsf@alter.siamese.dyndns.org","subject":"Re: [PATCH] Implement ACL module architecture and sample MySQL ACL module","fromName":"Shawn Pearce","fromEmail":"spearce@spearce.org","sentAt":"2012-08-14T17:26:30Z","receivedAt":"2012-08-14T17:26:30Z","isPatch":true,"sender":{"key":"spearce@spearce.org","avatar":"https://avatars.githubusercontent.com/u/34844?v=4"},"body":"On Tue, Aug 14, 2012 at 10:06 AM, Junio C Hamano <gitster@pobox.com> wrote:\n> Shawn Pearce <spearce@spearce.org> writes:\n>\n>> Parsing the request line of git-daemon is easy. But we could make it\n>> easier. An alternative arrangement would be to add a new command line\n>> flag to git daemon like --command-filter that names an executable\n>> git-daemon will invoke after parsing the request line. It can pass\n>> along the client IP address, command request, repository name, and\n>> resolved repository path, and tie stdin/stdout to the client. This\n>> binary can decide to exec the proper git binary for the named command,\n>> or just exit to disconnect the client and refuse service. This makes\n>> it simple for a tool like gitolite to plug into the git-daemon\n>> authorization path, without needing to be the network daemon itself,\n>> worry about number of active connection slots, etc.\n>\n> I think that is a good direction to go in, except that I am unsure\n> what kind of conversation do you want to allow between the \"command\n> filter\" helper and the client by exposing standard input and output\n> stream to to the helper.\n\nSorry, I was thinking the helper would exec the git command, and thus\npass along the stdin/stdout socket.\n\n>  If the client side has a matching \"pre\n> negotiate command\" helper support, then presumably the helpers can\n> discuss what Git protocol proper does not care about before deciding\n> to allow the connection go through, but until that happens, opening\n> the stdio streams up to the helper sounds like an accident waiting\n> to happen to me (e.g. \"fetch-pack\" connects, the server side helper\n> reads the first pkt-line from the client, says \"OK, you may proceed\"\n> to the daemon, then the daemon spawns the \"upload-pack\", which will\n> obviously see a corrupt request stream from \"fetch-pack\").\n\nBut seeing this, yes, that is a bad idea. Better to treat that like a\nhook, where exit status 0 allows the connection to continue, and exit\nstatus non-zero causes the connection to be closed. Maybe with an\nerror printed to stderr (if any) being echoed first to the client if\npossible using the ERR formatting notation.\n"},{"id":"196991","messageId":"7vpq6t9v5s.fsf@alter.siamese.dyndns.org","threadId":"31250","inReplyTo":"CAJo=hJu7W6JnNLYvahaQ43ZNqDtrurTOLCnLfZacVJKeL6VMFg@mail.gmail.com","subject":"Re: [PATCH] Implement ACL module architecture and sample MySQL ACL module","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2012-08-14T18:37:51Z","receivedAt":"2012-08-14T18:37:51Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Shawn Pearce <spearce@spearce.org> writes:\n\n> But seeing this, yes, that is a bad idea. Better to treat that like a\n> hook, where exit status 0 allows the connection to continue, and exit\n> status non-zero causes the connection to be closed. Maybe with an\n> error printed to stderr (if any) being echoed first to the client if\n> possible using the ERR formatting notation.\n\nYeah, note that we can only give a single \"ERR \" line, though.\n\nSomething like this?  Totally untested, of course ;-)\n\n daemon.c | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 79 insertions(+)\n\ndiff --git a/daemon.c b/daemon.c\nindex ab21e66..41a9679 100644\n--- a/daemon.c\n+++ b/daemon.c\n@@ -30,6 +30,7 @@ static const char daemon_usage[] =\n \"           [--interpolated-path=<path>]\\n\"\n \"           [--reuseaddr] [--pid-file=<file>]\\n\"\n \"           [--(enable|disable|allow-override|forbid-override)=<service>]\\n\"\n+\"           [--access-hook=<path>]\\n\"\n \"           [--inetd | [--listen=<host_or_ipaddr>] [--port=<n>]\\n\"\n \"                      [--detach] [--user=<user> [--group=<group>]]\\n\"\n \"           [<directory>...]\";\n@@ -256,6 +257,73 @@ static int daemon_error(const char *dir, const char *msg)\n \treturn -1;\n }\n \n+static char *access_hook;\n+\n+static int run_access_hook(struct daemon_service *service, const char *dir, const char *path)\n+{\n+\tstruct child_process child;\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tconst char *argv[8];\n+\tconst char **arg = argv;\n+\tchar *eol;\n+\tint seen_errors = 0;\n+\n+#define STRARG(x) ((x) ? (x) : \"\")\n+\t*arg++ = access_hook;\n+\t*arg++ = service->name;\n+\t*arg++ = path;\n+\t*arg++ = STRARG(hostname);\n+\t*arg++ = STRARG(canon_hostname);\n+\t*arg++ = STRARG(ip_address);\n+\t*arg++ = STRARG(tcp_port);\n+\t*arg = NULL;\n+#undef STRARG\n+\n+\tmemset(&child, 0, sizeof(child));\n+\tchild.use_shell = 1;\n+\tchild.argv = argv;\n+\tchild.no_stdin = 1;\n+\tchild.no_stderr = 1;\n+\tchild.out = -1;\n+\tif (start_command(&child)) {\n+\t\tlogerror(\"daemon access hook '%s' failed to start\",\n+\t\t\t access_hook);\n+\t\tgoto error_return;\n+\t}\n+\tif (strbuf_read(&buf, child.out, 0) < 0) {\n+\t\tlogerror(\"failed to read from pipe to daemon access hook '%s'\",\n+\t\t\t access_hook);\n+\t\tstrbuf_reset(&buf);\n+\t\tseen_errors = 1;\n+\t}\n+\tif (close(child.out) < 0) {\n+\t\tlogerror(\"failed to close pipe to daemon access hook '%s'\",\n+\t\t\t access_hook);\n+\t\tseen_errors = 1;\n+\t}\n+\tif (finish_command(&child) < 0) {\n+\t\tlogerror(\"failed to finish-command daemon access hook '%s'\",\n+\t\t\t access_hook);\n+\t\tseen_errors = 1;\n+\t}\n+\tif (!seen_errors) {\n+\t\tstrbuf_release(&buf);\n+\t\treturn 0;\n+\t}\n+\n+error_return:\n+\tstrbuf_ltrim(&buf);\n+\tif (!buf.len)\n+\t\tstrbuf_addstr(&buf, \"service rejected\");\n+\teol = strchr(buf.buf, '\\n');\n+\tif (eol)\n+\t\t*eol = '\\0';\n+\terrno = EACCES;\n+\tdaemon_error(dir, buf.buf);\n+\tstrbuf_release(&buf);\n+\treturn -1;\n+}\n+\n static int run_service(char *dir, struct daemon_service *service)\n {\n \tconst char *path;\n@@ -304,6 +372,13 @@ static int run_service(char *dir, struct daemon_service *service)\n \t}\n \n \t/*\n+\t * Optionally, a hook can choose to deny access to the\n+\t * repository depending on the phase of the moon.\n+\t */\n+\tif (access_hook && run_access_hook(service, dir, path))\n+\t\treturn -1;\n+\n+\t/*\n \t * We'll ignore SIGTERM from now on, we have a\n \t * good client.\n \t */\n@@ -1142,6 +1217,10 @@ int main(int argc, char **argv)\n \t\t\texport_all_trees = 1;\n \t\t\tcontinue;\n \t\t}\n+\t\tif (!prefixcmp(arg, \"--access-hook=\")) {\n+\t\t\taccess_hook = arg + 14;\n+\t\t\tcontinue;\n+\t\t}\n \t\tif (!prefixcmp(arg, \"--timeout=\")) {\n \t\t\ttimeout = atoi(arg+10);\n \t\t\tcontinue;\n"},{"id":"197031","messageId":"7vobmc7n80.fsf_-_@alter.siamese.dyndns.org","threadId":"31250","inReplyTo":"7vpq6t9v5s.fsf@alter.siamese.dyndns.org","subject":"[PATCH] daemon: --access-hook option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2012-08-15T05:12:15Z","receivedAt":"2012-08-15T05:12:15Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The --access-hook option to \"git daemon\" specifies an external\ncommand to be run every time a client connects, with\n\n - service name (e.g. \"upload-pack\", etc.),\n - path to the repository,\n - hostname (%H),\n - canonical hostname (%CH),\n - ip address (%IP),\n - tcp port (%P)\n\nas its command line arguments.  The external command can decide to\ndecline the service by exiting with a non-zero status (or to allow it\nby exiting with a zero status).  It can also look at the $REMOTE_ADDR\nand $REMOTE_PORT environment variables to learn about the requestor\nwhen making this decision.\n\nThe external command can optionally write a single line to its\nstandard output to be sent to the requestor as an error message when\nit declines the service.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n\n * This time, minimally tested, with a documentation update.\n\n Documentation/git-daemon.txt | 16 +++++++++\n daemon.c                     | 77 ++++++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 93 insertions(+)\n\ndiff --git a/Documentation/git-daemon.txt b/Documentation/git-daemon.txt\nindex 31b28fc..c3ba4d7 100644\n--- a/Documentation/git-daemon.txt\n+++ b/Documentation/git-daemon.txt\n@@ -16,6 +16,7 @@ SYNOPSIS\n \t     [--reuseaddr] [--detach] [--pid-file=<file>]\n \t     [--enable=<service>] [--disable=<service>]\n \t     [--allow-override=<service>] [--forbid-override=<service>]\n+\t     [--access-hook=<path>]\n \t     [--inetd | [--listen=<host_or_ipaddr>] [--port=<n>] [--user=<user> [--group=<group>]]\n \t     [<directory>...]\n \n@@ -171,6 +172,21 @@ the facility of inet daemon to achieve the same before spawning\n \terrors are not enabled, all errors report \"access denied\" to the\n \tclient. The default is --no-informative-errors.\n \n+--access-hook=<path>::\n+\tEvery time a client connects, first run an external command\n+\tspecified by the <path> with service name (e.g. \"upload-pack\"),\n+\tpath to the repository, hostname (%H), canonical hostname\n+\t(%CH), ip address (%IP), and tcp port (%P) as its command line\n+\targuments. The external command can decide to decline the\n+\tservice by exiting with a non-zero status (or to allow it by\n+\texiting with a zero status).  It can also look at the $REMOTE_ADDR\n+\tand $REMOTE_PORT environment variables to learn about the\n+\trequestor when making this decision.\n++\n+The external command can optionally write a single line to its\n+standard output to be sent to the requestor as an error message when\n+it declines the service.\n+\n <directory>::\n \tA directory to add to the whitelist of allowed directories. Unless\n \t--strict-paths is specified this will also include subdirectories\ndiff --git a/daemon.c b/daemon.c\nindex ab21e66..4602b46 100644\n--- a/daemon.c\n+++ b/daemon.c\n@@ -30,6 +30,7 @@ static const char daemon_usage[] =\n \"           [--interpolated-path=<path>]\\n\"\n \"           [--reuseaddr] [--pid-file=<file>]\\n\"\n \"           [--(enable|disable|allow-override|forbid-override)=<service>]\\n\"\n+\"           [--access-hook=<path>]\\n\"\n \"           [--inetd | [--listen=<host_or_ipaddr>] [--port=<n>]\\n\"\n \"                      [--detach] [--user=<user> [--group=<group>]]\\n\"\n \"           [<directory>...]\";\n@@ -256,6 +257,71 @@ static int daemon_error(const char *dir, const char *msg)\n \treturn -1;\n }\n \n+static char *access_hook;\n+\n+static int run_access_hook(struct daemon_service *service, const char *dir, const char *path)\n+{\n+\tstruct child_process child;\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tconst char *argv[8];\n+\tconst char **arg = argv;\n+\tchar *eol;\n+\tint seen_errors = 0;\n+\n+#define STRARG(x) ((x) ? (x) : \"\")\n+\t*arg++ = access_hook;\n+\t*arg++ = service->name;\n+\t*arg++ = path;\n+\t*arg++ = STRARG(hostname);\n+\t*arg++ = STRARG(canon_hostname);\n+\t*arg++ = STRARG(ip_address);\n+\t*arg++ = STRARG(tcp_port);\n+\t*arg = NULL;\n+#undef STRARG\n+\n+\tmemset(&child, 0, sizeof(child));\n+\tchild.use_shell = 1;\n+\tchild.argv = argv;\n+\tchild.no_stdin = 1;\n+\tchild.no_stderr = 1;\n+\tchild.out = -1;\n+\tif (start_command(&child)) {\n+\t\tlogerror(\"daemon access hook '%s' failed to start\",\n+\t\t\t access_hook);\n+\t\tgoto error_return;\n+\t}\n+\tif (strbuf_read(&buf, child.out, 0) < 0) {\n+\t\tlogerror(\"failed to read from pipe to daemon access hook '%s'\",\n+\t\t\t access_hook);\n+\t\tstrbuf_reset(&buf);\n+\t\tseen_errors = 1;\n+\t}\n+\tif (close(child.out) < 0) {\n+\t\tlogerror(\"failed to close pipe to daemon access hook '%s'\",\n+\t\t\t access_hook);\n+\t\tseen_errors = 1;\n+\t}\n+\tif (finish_command(&child))\n+\t\tseen_errors = 1;\n+\n+\tif (!seen_errors) {\n+\t\tstrbuf_release(&buf);\n+\t\treturn 0;\n+\t}\n+\n+error_return:\n+\tstrbuf_ltrim(&buf);\n+\tif (!buf.len)\n+\t\tstrbuf_addstr(&buf, \"service rejected\");\n+\teol = strchr(buf.buf, '\\n');\n+\tif (eol)\n+\t\t*eol = '\\0';\n+\terrno = EACCES;\n+\tdaemon_error(dir, buf.buf);\n+\tstrbuf_release(&buf);\n+\treturn -1;\n+}\n+\n static int run_service(char *dir, struct daemon_service *service)\n {\n \tconst char *path;\n@@ -304,6 +370,13 @@ static int run_service(char *dir, struct daemon_service *service)\n \t}\n \n \t/*\n+\t * Optionally, a hook can choose to deny access to the\n+\t * repository depending on the phase of the moon.\n+\t */\n+\tif (access_hook && run_access_hook(service, dir, path))\n+\t\treturn -1;\n+\n+\t/*\n \t * We'll ignore SIGTERM from now on, we have a\n \t * good client.\n \t */\n@@ -1142,6 +1215,10 @@ int main(int argc, char **argv)\n \t\t\texport_all_trees = 1;\n \t\t\tcontinue;\n \t\t}\n+\t\tif (!prefixcmp(arg, \"--access-hook=\")) {\n+\t\t\taccess_hook = arg + 14;\n+\t\t\tcontinue;\n+\t\t}\n \t\tif (!prefixcmp(arg, \"--timeout=\")) {\n \t\t\ttimeout = atoi(arg+10);\n \t\t\tcontinue;\n-- \n1.7.12.rc2.85.g1de7134\n"},{"id":"197041","messageId":"CAJo=hJs7cxs-4MvTovKMzLZ22a1GFPrmANZd0Srny_UKQe+u+w@mail.gmail.com","threadId":"31250","inReplyTo":"7vobmc7n80.fsf_-_@alter.siamese.dyndns.org","subject":"Re: [PATCH] daemon: --access-hook option","fromName":"Shawn Pearce","fromEmail":"spearce@spearce.org","sentAt":"2012-08-15T14:08:35Z","receivedAt":"2012-08-15T14:08:35Z","isPatch":true,"sender":{"key":"spearce@spearce.org","avatar":"https://avatars.githubusercontent.com/u/34844?v=4"},"body":"On Tue, Aug 14, 2012 at 10:12 PM, Junio C Hamano <gitster@pobox.com> wrote:\n> The --access-hook option to \"git daemon\" specifies an external\n> command to be run every time a client connects, with\n>\n>  - service name (e.g. \"upload-pack\", etc.),\n>  - path to the repository,\n>  - hostname (%H),\n>  - canonical hostname (%CH),\n>  - ip address (%IP),\n>  - tcp port (%P)\n>\n> as its command line arguments.  The external command can decide to\n> decline the service by exiting with a non-zero status (or to allow it\n> by exiting with a zero status).  It can also look at the $REMOTE_ADDR\n> and $REMOTE_PORT environment variables to learn about the requestor\n> when making this decision.\n>\n> The external command can optionally write a single line to its\n> standard output to be sent to the requestor as an error message when\n> it declines the service.\n>\n> Signed-off-by: Junio C Hamano <gitster@pobox.com>\n\nThanks Junio, this looks like the best approach.\n\nAcked-by: Shawn O. Pearce <spearce@spearce.org>\n"},{"id":"197528","messageId":"5033699F.5030207@redhat.com","threadId":"31250","inReplyTo":"7vobmc7n80.fsf_-_@alter.siamese.dyndns.org","subject":"Re: [PATCH] daemon: --access-hook option","fromName":"Michal Novotny","fromEmail":"minovotn@redhat.com","sentAt":"2012-08-21T10:57:35Z","receivedAt":"2012-08-21T10:57:35Z","isPatch":true,"sender":{"key":"minovotn@redhat.com","avatar":null},"body":"Right, this approach of having ACL using the --access-hook option looks\nmuch better. At least you got inspired this could be useful for somebody ;-)\n\nMichal\n\nOn 08/15/2012 07:12 AM, Junio C Hamano wrote:\n> The --access-hook option to \"git daemon\" specifies an external\n> command to be run every time a client connects, with\n>\n>  - service name (e.g. \"upload-pack\", etc.),\n>  - path to the repository,\n>  - hostname (%H),\n>  - canonical hostname (%CH),\n>  - ip address (%IP),\n>  - tcp port (%P)\n>\n> as its command line arguments.  The external command can decide to\n> decline the service by exiting with a non-zero status (or to allow it\n> by exiting with a zero status).  It can also look at the $REMOTE_ADDR\n> and $REMOTE_PORT environment variables to learn about the requestor\n> when making this decision.\n>\n> The external command can optionally write a single line to its\n> standard output to be sent to the requestor as an error message when\n> it declines the service.\n>\n> Signed-off-by: Junio C Hamano <gitster@pobox.com>\n> ---\n>\n>  * This time, minimally tested, with a documentation update.\n>\n>  Documentation/git-daemon.txt | 16 +++++++++\n>  daemon.c                     | 77 ++++++++++++++++++++++++++++++++++++++++++++\n>  2 files changed, 93 insertions(+)\n>\n> diff --git a/Documentation/git-daemon.txt b/Documentation/git-daemon.txt\n> index 31b28fc..c3ba4d7 100644\n> --- a/Documentation/git-daemon.txt\n> +++ b/Documentation/git-daemon.txt\n> @@ -16,6 +16,7 @@ SYNOPSIS\n>  \t     [--reuseaddr] [--detach] [--pid-file=<file>]\n>  \t     [--enable=<service>] [--disable=<service>]\n>  \t     [--allow-override=<service>] [--forbid-override=<service>]\n> +\t     [--access-hook=<path>]\n>  \t     [--inetd | [--listen=<host_or_ipaddr>] [--port=<n>] [--user=<user> [--group=<group>]]\n>  \t     [<directory>...]\n>  \n> @@ -171,6 +172,21 @@ the facility of inet daemon to achieve the same before spawning\n>  \terrors are not enabled, all errors report \"access denied\" to the\n>  \tclient. The default is --no-informative-errors.\n>  \n> +--access-hook=<path>::\n> +\tEvery time a client connects, first run an external command\n> +\tspecified by the <path> with service name (e.g. \"upload-pack\"),\n> +\tpath to the repository, hostname (%H), canonical hostname\n> +\t(%CH), ip address (%IP), and tcp port (%P) as its command line\n> +\targuments. The external command can decide to decline the\n> +\tservice by exiting with a non-zero status (or to allow it by\n> +\texiting with a zero status).  It can also look at the $REMOTE_ADDR\n> +\tand $REMOTE_PORT environment variables to learn about the\n> +\trequestor when making this decision.\n> ++\n> +The external command can optionally write a single line to its\n> +standard output to be sent to the requestor as an error message when\n> +it declines the service.\n> +\n>  <directory>::\n>  \tA directory to add to the whitelist of allowed directories. Unless\n>  \t--strict-paths is specified this will also include subdirectories\n> diff --git a/daemon.c b/daemon.c\n> index ab21e66..4602b46 100644\n> --- a/daemon.c\n> +++ b/daemon.c\n> @@ -30,6 +30,7 @@ static const char daemon_usage[] =\n>  \"           [--interpolated-path=<path>]\\n\"\n>  \"           [--reuseaddr] [--pid-file=<file>]\\n\"\n>  \"           [--(enable|disable|allow-override|forbid-override)=<service>]\\n\"\n> +\"           [--access-hook=<path>]\\n\"\n>  \"           [--inetd | [--listen=<host_or_ipaddr>] [--port=<n>]\\n\"\n>  \"                      [--detach] [--user=<user> [--group=<group>]]\\n\"\n>  \"           [<directory>...]\";\n> @@ -256,6 +257,71 @@ static int daemon_error(const char *dir, const char *msg)\n>  \treturn -1;\n>  }\n>  \n> +static char *access_hook;\n> +\n> +static int run_access_hook(struct daemon_service *service, const char *dir, const char *path)\n> +{\n> +\tstruct child_process child;\n> +\tstruct strbuf buf = STRBUF_INIT;\n> +\tconst char *argv[8];\n> +\tconst char **arg = argv;\n> +\tchar *eol;\n> +\tint seen_errors = 0;\n> +\n> +#define STRARG(x) ((x) ? (x) : \"\")\n> +\t*arg++ = access_hook;\n> +\t*arg++ = service->name;\n> +\t*arg++ = path;\n> +\t*arg++ = STRARG(hostname);\n> +\t*arg++ = STRARG(canon_hostname);\n> +\t*arg++ = STRARG(ip_address);\n> +\t*arg++ = STRARG(tcp_port);\n> +\t*arg = NULL;\n> +#undef STRARG\n> +\n> +\tmemset(&child, 0, sizeof(child));\n> +\tchild.use_shell = 1;\n> +\tchild.argv = argv;\n> +\tchild.no_stdin = 1;\n> +\tchild.no_stderr = 1;\n> +\tchild.out = -1;\n> +\tif (start_command(&child)) {\n> +\t\tlogerror(\"daemon access hook '%s' failed to start\",\n> +\t\t\t access_hook);\n> +\t\tgoto error_return;\n> +\t}\n> +\tif (strbuf_read(&buf, child.out, 0) < 0) {\n> +\t\tlogerror(\"failed to read from pipe to daemon access hook '%s'\",\n> +\t\t\t access_hook);\n> +\t\tstrbuf_reset(&buf);\n> +\t\tseen_errors = 1;\n> +\t}\n> +\tif (close(child.out) < 0) {\n> +\t\tlogerror(\"failed to close pipe to daemon access hook '%s'\",\n> +\t\t\t access_hook);\n> +\t\tseen_errors = 1;\n> +\t}\n> +\tif (finish_command(&child))\n> +\t\tseen_errors = 1;\n> +\n> +\tif (!seen_errors) {\n> +\t\tstrbuf_release(&buf);\n> +\t\treturn 0;\n> +\t}\n> +\n> +error_return:\n> +\tstrbuf_ltrim(&buf);\n> +\tif (!buf.len)\n> +\t\tstrbuf_addstr(&buf, \"service rejected\");\n> +\teol = strchr(buf.buf, '\\n');\n> +\tif (eol)\n> +\t\t*eol = '\\0';\n> +\terrno = EACCES;\n> +\tdaemon_error(dir, buf.buf);\n> +\tstrbuf_release(&buf);\n> +\treturn -1;\n> +}\n> +\n>  static int run_service(char *dir, struct daemon_service *service)\n>  {\n>  \tconst char *path;\n> @@ -304,6 +370,13 @@ static int run_service(char *dir, struct daemon_service *service)\n>  \t}\n>  \n>  \t/*\n> +\t * Optionally, a hook can choose to deny access to the\n> +\t * repository depending on the phase of the moon.\n> +\t */\n> +\tif (access_hook && run_access_hook(service, dir, path))\n> +\t\treturn -1;\n> +\n> +\t/*\n>  \t * We'll ignore SIGTERM from now on, we have a\n>  \t * good client.\n>  \t */\n> @@ -1142,6 +1215,10 @@ int main(int argc, char **argv)\n>  \t\t\texport_all_trees = 1;\n>  \t\t\tcontinue;\n>  \t\t}\n> +\t\tif (!prefixcmp(arg, \"--access-hook=\")) {\n> +\t\t\taccess_hook = arg + 14;\n> +\t\t\tcontinue;\n> +\t\t}\n>  \t\tif (!prefixcmp(arg, \"--timeout=\")) {\n>  \t\t\ttimeout = atoi(arg+10);\n>  \t\t\tcontinue;\n\n-- \nMichal Novotny <minovotn@redhat.com>, RHCE, Red Hat\nVirtualization | libvirt-php bindings | php-virt-control.org\n"}]}