add bed cooler project.
This commit is contained in:
295
src/core/auth.cpp
Normal file
295
src/core/auth.cpp
Normal file
@@ -0,0 +1,295 @@
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user