Files
PlatformIO_Esp32C3_BedCooler/src/core/auth.cpp
2026-06-28 13:15:53 -05:00

296 lines
8.6 KiB
C++

#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;
}