#include "app.h" bool hasRole(const String &roles, const String &role) { if (role.length() == 0) return true; int start = 0; while (start <= (int)roles.length()) { int end = roles.indexOf('|', start); if (end < 0) end = roles.length(); if (roles.substring(start, end) == role) return true; start = end + 1; } return false; } bool validName(const String &s) { if (s.length() == 0 || s.length() > 31) return false; for (size_t i = 0; i < s.length(); i++) { char c = s[i]; if (!(isalnum(c) || c == '_' || c == '-' || c == '.')) return false; } return true; } String cleanRoles(const String &roles) { String out; String known = allRoles(); int start = 0; while (start <= (int)known.length()) { int end = known.indexOf('|', start); if (end < 0) end = known.length(); String role = known.substring(start, end); if (role.length() && hasRole(roles, role)) { if (out.length()) out += "|"; out += role; } start = end + 1; } return out; } bool isKnownRole(const String &role) { return role == "PUBLIC" || hasRole(allRoles(), role); } bool isSystemRole(const String &role) { return role == "Sysadmin" || role == "UserAdmin" || role == "WebUIConnect" || role == "Debugger"; } String allRoles() { String roles = "Sysadmin|UserAdmin|WebUIConnect|Debugger"; String custom = prefString("roles", ""); int start = 0; while (start <= (int)custom.length()) { int end = custom.indexOf('|', start); if (end < 0) end = custom.length(); String role = custom.substring(start, end); if (role.length() && !hasRole(roles, role)) roles += "|" + role; start = end + 1; if (!custom.length()) break; } return roles; } String rolesJson() { String roles = allRoles(); String out = "\"roles\":["; int start = 0; bool first = true; while (start <= (int)roles.length()) { int end = roles.indexOf('|', start); if (end < 0) end = roles.length(); String role = roles.substring(start, end); if (role.length()) { if (!first) out += ","; out += "{\"name\":\"" + jsonEscape(role) + "\",\"system\":" + String(isSystemRole(role) ? "true" : "false") + "}"; first = false; } start = end + 1; } out += "]"; return out; } bool addCustomRole(const String &role) { if (!validName(role) || isKnownRole(role)) return false; String custom = prefString("roles", ""); if (custom.length()) custom += "|"; custom += role; prefs.putString("roles", custom); return true; } bool deleteCustomRole(const String &role) { if (!validName(role) || isSystemRole(role) || !hasRole(prefString("roles", ""), role)) return false; String custom = prefString("roles", ""); String kept; int start = 0; while (start <= (int)custom.length()) { int end = custom.indexOf('|', start); if (end < 0) end = custom.length(); String item = custom.substring(start, end); if (item.length() && item != role) { if (kept.length()) kept += "|"; kept += item; } start = end + 1; } prefs.putString("roles", kept); User users[8]; size_t count = parseUsers(users, MAX_USERS); for (size_t i = 0; i < count; i++) users[i].roles = cleanRoles(users[i].roles); saveUsers(users, count); for (size_t i = 0; i < API_DEF_COUNT + CUSTOM_API_DEF_COUNT; i++) { ApiDef *api = i < API_DEF_COUNT ? &apiDefs[i] : &customApiDefs[i - API_DEF_COUNT]; if (configuredRole(api) == role) { String fallback = api->publicByDefault ? "PUBLIC" : api->defaultRole; String key = "acl"; key += api->method[0]; for (size_t j = 0; j < strlen(api->path); j++) { char c = api->path[j]; if (isalnum(c)) key += c; } prefs.putString(key.substring(0, 15).c_str(), fallback); } } return true; } static String defaultUsers() { return String(DEFAULT_ADMIN) + "\t" + passwordHash("") + "\tSysadmin|UserAdmin|WebUIConnect|Debugger\t1\n"; } static String usersText() { String text = prefString("users", ""); return text.length() ? text : defaultUsers(); } size_t parseUsers(User *users, size_t maxUsers) { String text = usersText(); size_t count = 0; int start = 0; while (start < (int)text.length() && count < maxUsers) { int end = text.indexOf('\n', start); if (end < 0) end = text.length(); String line = text.substring(start, end); int a = line.indexOf('\t'); int b = line.indexOf('\t', a + 1); int c = line.indexOf('\t', b + 1); if (a > 0 && b > a) { users[count].name = line.substring(0, a); users[count].passwordHash = line.substring(a + 1, b); users[count].roles = cleanRoles(c > b ? line.substring(b + 1, c) : line.substring(b + 1)); users[count].active = c > b ? line.substring(c + 1).toInt() != 0 : true; count++; } start = end + 1; } return count; } void saveUsers(User *users, size_t count) { String text; for (size_t i = 0; i < count; i++) { text += users[i].name + "\t" + users[i].passwordHash + "\t" + cleanRoles(users[i].roles) + "\t" + String(users[i].active ? "1" : "0") + "\n"; } prefs.putString("users", text); } bool findUser(const String &name, User &user) { User users[8]; size_t count = parseUsers(users, MAX_USERS); for (size_t i = 0; i < count; i++) { if (users[i].name == name) { user = users[i]; return true; } } return false; } String createToken(const User &user) { String value = sha256(String(esp_random(), HEX) + ":" + user.name + ":" + String(millis())); int slot = 0; uint32_t oldest = tokens[0].lastSeen; for (int i = 0; i < (int)MAX_TOKENS; i++) { if (!tokens[i].value.length()) { slot = i; break; } if (tokens[i].lastSeen < oldest) { oldest = tokens[i].lastSeen; slot = i; } } tokens[slot].value = value; tokens[slot].user = user.name; tokens[slot].roles = user.roles; tokens[slot].lastSeen = millis(); return value; } static String bearerToken() { String auth = server.header("Authorization"); if (auth.startsWith("Bearer ")) return auth.substring(7); if (server.hasHeader("X-Auth-Token")) return server.header("X-Auth-Token"); if (server.hasArg("token")) return server.arg("token"); return ""; } static String requestMethodName(); Token *currentToken() { String value = bearerToken(); if (!value.length()) return nullptr; for (size_t i = 0; i < MAX_TOKENS; i++) { if (tokens[i].value == value) { tokens[i].lastSeen = millis(); return &tokens[i]; } } appendLog(LOG_SECURITY_AUDIT, "invalid bearer token for " + requestMethodName() + " " + server.uri()); return nullptr; } static String apiKey(const String &path, const String &method) { String key = "acl"; key += method[0]; for (size_t i = 0; i < path.length(); i++) { char c = path[i]; if (isalnum(c)) key += c; } return key.substring(0, 15); } ApiDef *findApi(const String &path, const String &method) { for (size_t i = 0; i < API_DEF_COUNT; i++) { if (path == apiDefs[i].path && method == apiDefs[i].method) return &apiDefs[i]; } for (size_t i = 0; i < CUSTOM_API_DEF_COUNT; i++) { if (path == customApiDefs[i].path && method == customApiDefs[i].method) return &customApiDefs[i]; } return nullptr; } String configuredRole(ApiDef *api) { if (!api) return ""; String key = apiKey(api->path, api->method); return prefString(key.c_str(), api->publicByDefault ? "PUBLIC" : api->defaultRole); } static String requestMethodName() { switch (server.method()) { case HTTP_GET: return "GET"; case HTTP_POST: return "POST"; case HTTP_DELETE: return "DELETE"; case HTTP_PUT: return "PUT"; case HTTP_PATCH: return "PATCH"; default: return ""; } } bool authorize() { String path = server.uri(); String method = requestMethodName(); ApiDef *api = findApi(path, method); String required = configuredRole(api); if (required == "PUBLIC" || (api && api->publicByDefault && !required.length())) return true; Token *token = currentToken(); if (!token) { if (!bearerToken().length()) appendLog(LOG_SECURITY_AUDIT, "authentication required for " + method + " " + path); sendJson(401, jsonError("Authentication required")); return false; } User user; if (!findUser(token->user, user) || !user.active) { appendLog(LOG_SECURITY_AUDIT, "inactive or unknown user token for " + token->user + " on " + method + " " + path); sendJson(401, jsonError("User is inactive")); return false; } token->roles = user.roles; if (!hasRole(user.roles, required)) { appendLog(LOG_SECURITY_AUDIT, "authorization denied for " + user.name + " on " + method + " " + path + " requires " + required); sendJson(403, jsonError("Missing role " + required)); return false; } return true; }