/* * - User Access Control * * Copyright 2009 by AVM GmbH * * This software contains free software; you can redistribute it and/or modify it under the terms * of the GNU General Public License ("License") as published by the Free Software Foundation * (version 2 of the License). This software 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 copy of the License you received along with this software for more details. * */ /* memory debugging * #include "config.h" */ #include #include #include #include /* isspace () */ #include #include #include #include #include #include #include #include "avm_acl.h" // #include "extern.h" #if 0 static void _fLog(char *fmt, ...) { FILE *fp = fopen("/dev/console", "w"); if (fp) { va_list ap; fprintf(fp, "acl(%d): ", getpid()); va_start(ap, fmt); vfprintf(fp, fmt, ap); va_end(ap); fprintf(fp, "\n"); fclose(fp); } } #define Log(x) _fLog x #else #define Log(x) #endif static char *g_username = 0; static unsigned g_uid; static short g_access_from_internet = 0; static short g_bReconfig = 0; static int g_is_samba = 0; #define COMMON_REAL_PREFIX "/var/media/ftp" struct acl_directory { struct acl_directory *next; char *real_path; /* the user always has read access in this path and all files/folders below */ int real_path_len; /* calc and store the len once to be faster */ short write_access; int common_path_len; // number of chars in real_path for which another acl_directory exists }; static struct user_context { int logged_in; // int access_from_internet; struct acl_directory *acl; char *real_root; int real_root_len; } userctx = { 0, NULL, NULL, 0 }; static int FixUserContext(void) { if (!g_bReconfig) return 0; g_bReconfig = 0; if (!userctx.acl) return -1; // reconfig with updated users.acl file if (g_username) { acl_set_user_context(g_username, g_access_from_internet); } else { acl_set_user_context_uid(g_uid, g_access_from_internet); } if (!userctx.acl) return -1; return 0; } /* ------------------------------------------------------------------------ */ // Nutzung macht nur Sinn, wenn der Interne Speicher fuer Netzwerkzugriff nicht akiviert ist. static int is_mountpoint(const char *path) { if (path != strstr(path, COMMON_REAL_PREFIX)) return 0; path += strlen(COMMON_REAL_PREFIX); if (*path != '/') return 0; path++; if (*path == '\0') return 0; // kein / mehr oder nur am ende while(*path != '\0') { if (*path == '/') { if (*(path+1) != '\0') return 0; } path++; } return 1; } static int InternalMemoryActiveForNetwork(int reset) { static int known = 0; static int active; if (reset) { // just trigger reload known = 0; return 0; } if (!known) { known = 1; char buf[PATH_MAX]; if (0 == realpath("/var/InternerSpeicher", buf)) { active = 0; } else { // as a "trick" we use the "old" /var/mountpoint_excluded_for_network if (0 == access("/var/mountpoint_excluded_for_network", R_OK)) { active = 0; } active = 1; } } return active; } static int IsHiddenLostAndFound(char *path) { static char *internal_lost_and_found = (char *)-1; static int internal_lost_and_found_len; if ((char *)-1 == internal_lost_and_found) { char buf[PATH_MAX]; if (0 == realpath("/var/InternerSpeicher", buf)) { internal_lost_and_found = 0; return 0; } else { internal_lost_and_found_len = strlen(buf) + strlen("/lost+found"); internal_lost_and_found = (char*)malloc(internal_lost_and_found_len + 1); if (!internal_lost_and_found) return 0; sprintf(internal_lost_and_found, "%s/lost+found", buf); } } if (0 == internal_lost_and_found) return 0; if (path != strstr(path, internal_lost_and_found)) return 0; if (path[internal_lost_and_found_len] != '/' && path[internal_lost_and_found_len] != '\0') return 0; return 1; } /* ------------------------------------------------------------------------ */ /** * @brief acl_is_allowed_path * check if access to a given real path is allowed for the client * all path components must exist * Return 0 if path is empty. * Return 0 if no user logged in. * Return 0 if path contains "..". TODO details * Return 1 if used by smbd and trying to read $IPC. * @param path complete filesystem path * @param pwrite_access out parameter 1 if write access else 0 * * @return 1 on sucess else 0 on error */ int acl_is_allowed_path(const char *path, int *pwrite_access) { char buf[PATH_MAX]; Log(("acl_is_allowed_path %s", path)); if (g_is_samba && (!InternalMemoryActiveForNetwork(0) || !userctx.logged_in) && !strcmp(path, COMMON_REAL_PREFIX)) { // our path of $IPC service *pwrite_access = 0; Log(("acl_is_allowed_path - Read-Only for $IPC")); return 1; } if (!userctx.logged_in) { Log(("acl_is_allowed_path - not logged in")); return 0; } if (*path == '\0') { /* realpath() with empty string would fail */ if (0 == getcwd(buf, sizeof(buf))) { Log(("acl_is_allowed_real_path - getcwd failed")); return 0; } buf[sizeof(buf)-1] = '\0'; } else { if (buf != realpath(path, buf)) { // Check if parent exists // and construct realpath char *p = &path[strlen(path)]; while (p > path && *p != '/') p--; if (*p != '/') { Log(("realpath failed and path=%s has no / p=%s", path, p)); if (buf != realpath(".", buf)) { // parent does not exists Log(("realpath of current dir failed")); return 0; } if (buf[strlen(buf)-1] != '/') { strcat(buf, "/"); } strcat(buf, path); } else { if (!strcmp(p+1, "..")) return 0; *p = '\0'; if (buf != realpath(path, buf)) { // parent does not exists Log(("realpath of parent=%s failed", path)); *p = '/'; return 0; } *p = '/'; if (buf[strlen(buf)-1] == '/') { strcat(buf, p+1); } else strcat(buf, p); } Log(("acl_is_real_path - constructed realpath %s", buf)); } } Log(("checking access to %s", buf)); #if 0 if (IsInExcludedMoutpoint(buf)) { Log(("acl_is_allowed_path - path %s - not allowed cause in excluded mountpoint", buf)); return 0; } #endif if (IsHiddenLostAndFound(buf)) { Log(("acl_is_allowed_path - path %s - not allowed cause its in lost+found in internal flash", buf)); return 0; } if (FixUserContext()) { Log(("acl_is_allowed_path - path %s - not allowed cause FixUserContext failed", buf)); return 0; } struct acl_directory *a = userctx.acl; while(a) { if (buf == strstr(buf, a->real_path)) { char c = buf[a->real_path_len]; if (c == '\0' || c == '/') break; // found } a = a->next; } if (a) { if (InternalMemoryActiveForNetwork(0)) { // /var/media/ftp is the internal memory // and all mountpoints are within the internal memory *pwrite_access = a->write_access ? 1 : 0; } else { // im smbd gibts sonst Probleme mit loeschen/umbennenn // direkt unterhalb des mountpoints if (!g_is_samba && is_mountpoint(buf)) { *pwrite_access = 0; } else *pwrite_access = a->write_access ? 1 : 0; } } else { // check for path component to a shared "partition home" // eg. the following paths are configured: // /var/media/ftp/STICK/home/sub1/sub2/sub3 // /var/media/ftp/STICK/home/sub4/sub5 // --> now we also have to allow readonly access to exactly // /var/media/ftp/STICK/home // /var/media/ftp/STICK/home/sub1 // /var/media/ftp/STICK/home/sub1/sub2 // /var/media/ftp/STICK/home/sub4 // otherwise the user would not be able to reach // /var/media/ftp/STICK/home/sub1/sub2/sub3 or // /var/media/ftp/STICK/home/sub4/sub5 // from his "partion root" /var/media/ftp/STICK/home int len = strlen(buf); for (a = userctx.acl; a; a = a->next) { Log(("checking intermediate path %s against allowed %s", buf, a->real_path)); if (0 == a->common_path_len) continue; if (len < a->common_path_len) continue; // ist unterhalb der "parition root" char c = buf[a->common_path_len]; if (c != '\0' && c != '/') continue; if (memcmp(a->real_path, buf, a->common_path_len)) continue; if (a->real_path != strstr(a->real_path, buf)) continue; c = a->real_path[len]; if (c == '/') break; // found } if (a) { *pwrite_access = 0; } } if (!a) { Log(("acl_is_allowed_path - path %s - not in any allow path for this user ", buf)); return 0; } Log(("access to %s allowed write_access=%d", buf, *pwrite_access)); return 1; } /* ------------------------------------------------------------------------ */ static void add_virt_path(char *path, int write_access) { Log(("add_virt_path %s write_access:%d", path, write_access)); if (path[0] != '/') return; if (path[1] == '\0') path = ""; struct acl_directory *a = calloc(1, sizeof(struct acl_directory)); if (!a) return; a->real_path = malloc(strlen(COMMON_REAL_PREFIX) + strlen(path) + 1); if (!a->real_path) { free(a); return; } strcpy(a->real_path, COMMON_REAL_PREFIX); strcat(a->real_path, path); a->real_path_len = strlen(a->real_path); a->write_access = write_access; // calc common_path_len -> bis zum letzten gemeinsamen Verzeichnis vom Pfad-Anfang struct acl_directory *a2; for (a2 = userctx.acl; a2; a2 = a2->next) { char *p1 = a->real_path; char *p2 = a2->real_path; while(*p1 == *p2 && *p1 != '\0') { p1++; p2++; } if ((*p1 == '\0' || *p1 == '/') && (*p2 == '\0' || *p2 == '/')) { } else { p1--; while(*p1 != '/') p1--; } // match bis einschliesslich p1-1 int len = p1 - a->real_path; if (len > a->common_path_len) { Log(("common_path_len of new %s set to %u", a->real_path, len)); a->common_path_len = len; } if (len > a2->common_path_len) { Log(("common_path_len of existing %s set to %u", a2->real_path, len)); a2->common_path_len = len; } } struct acl_directory **pp = &userctx.acl; while(*pp) { pp = &((*pp)->next); } *pp = a; } /* ----------------------------------------------------------------------- */ enum eToken { TOKEN_EOF, TOKEN_STRING, TOKEN_USER, TOKEN_ID, TOKEN_DIR, TOKEN_ACCESS, TOKEN_LOCAL_READ, TOKEN_LOCAL_WRITE, TOKEN_INTERNET_READ, TOKEN_INTERNET_WRITE, TOKEN_INTERNET_SECURED }; static struct defined_token { char *string; enum eToken token; } tokens[] = { { "user", TOKEN_USER }, { "id", TOKEN_ID }, { "dir", TOKEN_DIR }, { "access", TOKEN_ACCESS }, { "read", TOKEN_LOCAL_READ }, { "write", TOKEN_LOCAL_WRITE }, { "internet-read", TOKEN_INTERNET_READ }, { "internet-write", TOKEN_INTERNET_WRITE }, { "internet-secured", TOKEN_INTERNET_SECURED }, { 0, TOKEN_EOF } }; struct UserACLReader { FILE *fp; char *string; // current token size_t string_len; size_t string_buf_size; }; static void add_char(struct UserACLReader *r, int c) { if (!r->string) return; if (r->string_len >= r->string_buf_size-2) return; r->string[r->string_len] = c; r->string_len++; } static char *GetString(struct UserACLReader *r) { return r->string; } static enum eToken UserACLFile_GetToken(struct UserACLReader *r) { if (!r->fp) return TOKEN_EOF; int c; do { c = fgetc(r->fp); if (c == EOF) { fclose(r->fp); r->fp = 0; return TOKEN_EOF; } } while(isspace(c)); if (!r->string) { r->string_buf_size = 256; r->string = (char *)malloc(r->string_buf_size); if (!r->string) return TOKEN_EOF; } r->string_len = 0; short bQuoted; if (c == '\"') { bQuoted = 1; } else { bQuoted = 0; add_char(r, c); } short bEscaped = 0; do { c = fgetc(r->fp); if (c == EOF) break; if (bQuoted) { // end at " if (bEscaped) { bEscaped = 0; add_char(r, c); } else { if (c == '\\') bEscaped = 1; else if (c == '\"') break; // string end else add_char(r, c); } } else { // end at white space if (isspace(c)) break; // stringend add_char(r, c); } } while(1); r->string[r->string_len++] = '\0'; if (!bQuoted) { struct defined_token *dt; for (dt = &tokens[0]; dt->string; dt++) { if (!strcmp(r->string, dt->string)) return dt->token; } } return TOKEN_STRING; } static struct UserACLReader *UserACLFile_ReadOpen(void) { struct UserACLReader *r = (struct UserACLReader *)calloc(1, sizeof(struct UserACLReader)); if (!r) return 0; r->fp = fopen("/var/users.acl", "rt"); if (!r->fp) { free(r); return 0; } return r; } static void UserACLFile_ReadClose(struct UserACLReader *r) { if (!r) return; free(r->string); if (r->fp) fclose(r->fp); #if 0 struct stat sb; if (!stat("/var/users.acl", &sb)) { g_UsersACLTimestamp = sb.st_mtime; } #endif free(r); } /* user "Udo G" id 70 dir "/x" access write */ static int ReadUser(struct UserACLReader *r, int access_from_internet) { enum eToken t; short ungot_token = 0; do { if (!ungot_token) t = UserACLFile_GetToken(r); ungot_token = 0; if (t != TOKEN_DIR) break; t = UserACLFile_GetToken(r); if (t != TOKEN_STRING) break; char *path = strdup(GetString(r)); t = UserACLFile_GetToken(r); short read_access = 0; // effective read right to this path short write_access = 0; // effective write right to this path // if (!access_from_internet) read_access = 1; if (t == TOKEN_ACCESS) { short end = 0; do { t = UserACLFile_GetToken(r); switch(t) { case TOKEN_LOCAL_READ: if (!access_from_internet) read_access = 1; break; case TOKEN_LOCAL_WRITE: if (!access_from_internet) { write_access = 1; read_access = 1; } break; case TOKEN_INTERNET_READ: if (access_from_internet) read_access = 1; break; case TOKEN_INTERNET_WRITE: if (access_from_internet) { write_access = 1; read_access = 1; } break; default: ungot_token = 1; end = 1; } } while(!end); } else { ungot_token = 1; } if (read_access && path && path[0] == '/') { add_virt_path(path, write_access); } free(path); } while(1); UserACLFile_ReadClose(r); return 0; } static int ReadUsersACLFileByFriendlyName(char *username, int access_from_internet) { struct UserACLReader *r = UserACLFile_ReadOpen(); if (!r) return -1; enum eToken t = UserACLFile_GetToken(r); while(t != TOKEN_EOF) { if (t == TOKEN_USER) { #ifdef BOX_USERS short bFound = 0; do { t = UserACLFile_GetToken(r); if (!bFound && t == TOKEN_STRING && !strcasecmp(GetString(r), username)) bFound = 1; } while (t == TOKEN_STRING); if (bFound) { #else t = UserACLFile_GetToken(r); if (t == TOKEN_STRING) { if (!strcmp (GetString(r), username)) { t = UserACLFile_GetToken(r); #endif if (t != TOKEN_ID) goto fail; t = UserACLFile_GetToken(r); if (t != TOKEN_STRING) goto fail; break; #ifndef BOX_USERS } #endif } } t = UserACLFile_GetToken(r); } if (t == TOKEN_EOF) goto fail; return ReadUser(r, access_from_internet); fail: UserACLFile_ReadClose(r); return -1; } static int ReadUsersACLFileByID(unsigned id, int access_from_internet) { struct UserACLReader *r = UserACLFile_ReadOpen(); if (!r) return -1; enum eToken t = UserACLFile_GetToken(r); while(t != TOKEN_EOF) { if (t == TOKEN_USER) { #ifdef BOX_USERS do { t = UserACLFile_GetToken(r); } while(t == TOKEN_STRING); #else t = UserACLFile_GetToken(r); if (t == TOKEN_STRING) { t = UserACLFile_GetToken(r); #endif if (t == TOKEN_ID) { t = UserACLFile_GetToken(r); if (t == TOKEN_STRING) { if (atoi(GetString(r)) == id) break; } } #ifndef BOX_USERS } #endif } t = UserACLFile_GetToken(r); } if (t == TOKEN_EOF) goto fail; return ReadUser(r, access_from_internet); fail: UserACLFile_ReadClose(r); return -1; } /* ----------------------------------------------------------------------- */ static void acl_unset_user_context(void) { Log(("acl_unset_user_context")); while(userctx.acl) { struct acl_directory *a = userctx.acl; free(a->real_path); userctx.acl = a->next; free(a); } free(userctx.real_root); memset(&userctx, 0, sizeof(userctx)); } static int calc_real_root(void) { Log(("calc_real_root")); // calc real root (what is virtual /) for this user // the real root is the longest common parent path of all real directories the user can access (at least read) // ACHTUNG: TODO Das Home-Dir in /etc/passwd muss für Local Access als auch für Access aus dem Internet gelten!!!!!!! // bzw der ftp muss das homedir hieraus berechnen und nicht aus /etc/passwd lesen! struct acl_directory *root = userctx.acl; if (!root) return -1; while(root->next) root = root->next; // note that longer path with common parent are first in list, so we go to the last int root_path_len = root->real_path_len; do { // check obs gemeinsamer parent ist struct acl_directory *a = userctx.acl; if (root_path_len == 1) break; // "/" passt immer while(a != root) { if (a->real_path_len < root_path_len || memcmp(a->real_path, root->real_path, root_path_len)) break; // passt nicht char c = a->real_path[root_path_len]; if (c != '/' && c != '\0') break; // passt nicht a = a->next; } if (a == root) break; // passt! // shorten root - go up one dir if (root_path_len == strlen(COMMON_REAL_PREFIX)) break; // cant shorten more! char *p = root->real_path + root_path_len - 1; while(*p != '/' && p > root->real_path) p--; if (p == root->real_path) { root_path_len = 1; // '/' - should not happen break; } root_path_len = p - root->real_path; } while(1); userctx.real_root_len = root_path_len; userctx.real_root = malloc(root_path_len+1); if (!userctx.real_root) return -1; memcpy(userctx.real_root, root->real_path, root_path_len); userctx.real_root[root_path_len] = '\0'; Log(("calc_real_root: real_root is %s", userctx.real_root)); return 0; } /** * @brief acl_set_user_context * Returns 0 if no username is given. * Returns -1 if no access for given user. * Returns 0 if some access is configured for given user. * * @param username may be NULL * @param access_from_internet 0 for (W)LAN access else WAN access * * @return 0 or -1 */ int acl_set_user_context(char *username, int access_from_internet) { Log(("acl_set_user_context username %s access_from_internet:%d", username ? username : "", access_from_internet)); Log(("sizeof(off_t)=%u sizeof(struct stat)=%u _FILE_OFFSET_BITS=%u", sizeof(off_t), sizeof(struct stat), _FILE_OFFSET_BITS)); acl_unset_user_context(); if (!username) { return 0; } if (ReadUsersACLFileByFriendlyName(username, access_from_internet)) { // no access at all goto bad; } #if 0 // TEST // laengere pfade mit gleichen Parent-Pfaden vorher anfügen! // add_virt_path("/JetFlash-TS8GJF160-01/readonly", 0/* READ ONLY */); // add_virt_path("/JetFlash-TS8GJF160-01", 1/* write access to everything */); #endif // parameter username may be the same pointer as g_username char *old_name = g_username; g_username = strdup(username); free(old_name); g_uid = 0; g_access_from_internet = access_from_internet; userctx.logged_in = 1; return 0; bad: acl_unset_user_context(); Log(("acl_set_user_context failed")); return -1; } int acl_set_user_context_uid(unsigned uid, int access_from_internet) { Log(("acl_set_user_context_uid uid:%u access_from_internet:%d", uid, access_from_internet)); acl_unset_user_context(); if (uid <= 1000) return -1; if (ReadUsersACLFileByID(uid - 1000, access_from_internet)) { // no access at all goto bad; } free(g_username); g_username = 0; g_uid = uid; g_access_from_internet = access_from_internet; userctx.logged_in = 1; return 0; bad: acl_unset_user_context(); Log(("acl_set_user_context_uid failed")); return -1; } /** * @brief acl_set_full_access_user_context * Set write and read rights for "/" root direcotry. * for internal admin tasks ("root access") * * @return 0 */ int acl_set_full_access_user_context(void) { Log(("acl_set_full_user_context()")); acl_unset_user_context(); add_virt_path("/", 1/* write access to everything */); userctx.logged_in = 1; return 0; } char *acl_get_user_common_real_dir(void) { Log(("acl_get_user_common_real_dir")); if (!userctx.acl) return 0; if (!userctx.real_root) { if (calc_real_root()) return 0; // failed } return userctx.real_root; } void acl_set_samba_mode(void) { g_is_samba = 1; } void acl_reconfig(void) { InternalMemoryActiveForNetwork(1/*reset*/); g_bReconfig = 1; } int acl_chmod_allowed(void) { static int is_tcom = 2; if (2 == is_tcom) { char *s = getenv("OEM"); is_tcom = s && (!strcmp(s, "tcom") || !strcmp(s, "congstar")); } return !is_tcom; } int acl_force_secured_internet_access(void) { int secured = 0; struct UserACLReader *r = UserACLFile_ReadOpen(); if (!r) return 0; enum eToken t = UserACLFile_GetToken(r); while(t != TOKEN_EOF && t != TOKEN_USER) { if (t == TOKEN_INTERNET_SECURED) { secured = 1; break; } t = UserACLFile_GetToken(r); } UserACLFile_ReadClose(r); return secured; }