296 lines
8.6 KiB
C++
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;
|
|
}
|