Commit 49a0bed0 authored by jan.koester's avatar jan.koester
Browse files

test

parent ecc06f71
Loading
Loading
Loading
Loading
+109 −23
Changes for modules/users/users.cpp: 109 added lines, 23 removed lines.
Original line number Diff line number Diff line
@@ -46,7 +46,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * A password set here can also be the user's SMB password (smb: true):
 * shadow_set_password() then keeps an NT hash next to the crypt() hash,
 * for the same password (see <shadow.h>); smb: false ends SMB logins.
 * Never for root. "list" tells per user whether it has SMB access. */
 * Never for root. "list" tells per user whether it has SMB access.
 *
 * Users and groups with a UUID or GUID id (systempp's <sys/uid_r.h>) are
 * read and written with the *_id_r functions, so a rewrite keeps them;
 * "list" shows such an id as text. Accounts made here get classic ids. */

#include <algorithm>
#include <cerrno>
@@ -70,6 +74,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include <webctl/module.h>

#if __has_include(<sys/uid_r.h>)
#include <sys/uid_r.h>
#define WEBCTL_UID_R 1
#endif

#ifndef _PATH_PASSWD
#define _PATH_PASSWD "/etc/passwd"
#endif
@@ -89,18 +98,56 @@ namespace {
const uid_t FirstUid = 1000;
const size_t MinPassword = 8;

// uid/gid are the classic ids; a UUID or GUID id is in uidR/gidR as text
// (uid/gid are the overflow id then, never compare those alone: idKey())
struct User {
    std::string name, passwd, gecos, dir, shell;
    uid_t uid;
    gid_t gid;
    std::string uidR, gidR;
};

struct Group {
    std::string name, passwd;
    gid_t gid;
    std::vector<std::string> members;
    std::string gidR;
};

// the id as "list" shows it and as ids are compared
std::string idKey(unsigned long id, const std::string &r) { return r.empty() ? std::to_string(id) : r; }
json_object *idJson(unsigned long id, const std::string &r) {
    return r.empty() ? json_object_new_int64((int64_t)id) : json_object_new_string(r.c_str());
}

#ifdef WEBCTL_UID_R
std::string idText(const uid_r_t &id) {
    char buf[UID_R_STR_LEN];
    return uid_r_is_legacy(&id) ? std::string() : std::string(uid_r_to_str(&id, buf));
}
uid_r_t idOf(unsigned long id, const std::string &r) {
    uid_r_t out = uid_r_legacy((unsigned)id);
    if (!r.empty()) uid_r_from_str(r.c_str(), &out);
    return out;
}
#endif

// the owner of path (not following a symlink), in idKey() form
bool ownerKey(const std::string &path, std::string &key) {
#ifdef WEBCTL_UID_R
    uid_r_t owner;
    if (fstatat_id_r(AT_FDCWD, path.c_str(), &owner, nullptr, AT_SYMLINK_NOFOLLOW) == 0) {
        key = idKey(uid_r_to_legacy(&owner), idText(owner));
        return true;
    }
    if (errno != ENOSYS) return false;
#endif
    struct stat st;
    if (lstat(path.c_str(), &st) != 0) return false;
    key = std::to_string(st.st_uid);
    return true;
}

struct Shadow {
    std::string name, pwd;
    long lstchg, min, max, warn, inact, expire;
@@ -121,9 +168,16 @@ struct Db {
        return nullptr;
    }
    Group *groupById(gid_t gid) {
        for (auto &g : groups) if (g.gid == gid) return &g;
        for (auto &g : groups) if (g.gidR.empty() && g.gid == gid) return &g;
        return nullptr;
    }
    Group *primaryOf(const User &u) {
        for (auto &g : groups) if (isPrimary(u, g)) return &g;
        return nullptr;
    }
    static bool isPrimary(const User &u, const Group &g) {
        return idKey(u.gid, u.gidR) == idKey(g.gid, g.gidR);
    }
    Shadow *shadowOf(const std::string &n) {
        for (auto &s : shadow) if (s.name == n) return &s;
        return nullptr;
@@ -138,6 +192,21 @@ std::string dbPath(const char *env, const char *dflt) {

Db load() {
    Db db;
#ifdef WEBCTL_UID_R
    setpwent_id_r();
    while (struct passwd_id_r *p = getpwent_id_r())
        db.users.push_back({p->pw_name, p->pw_passwd, p->pw_gecos, p->pw_dir, p->pw_shell,
                            uid_r_to_legacy(&p->pw_uid), uid_r_to_legacy(&p->pw_gid),
                            idText(p->pw_uid), idText(p->pw_gid)});
    endpwent_id_r();
    setgrent_id_r();
    while (struct group_id_r *g = getgrent_id_r()) {
        Group gr{g->gr_name, g->gr_passwd, uid_r_to_legacy(&g->gr_gid), {}, idText(g->gr_gid)};
        for (char **m = g->gr_mem; m && *m; ++m) gr.members.push_back(*m);
        db.groups.push_back(gr);
    }
    endgrent_id_r();
#else
    setpwent();
    while (struct passwd *p = getpwent())
        db.users.push_back({p->pw_name, p->pw_passwd, p->pw_gecos, p->pw_dir, p->pw_shell, p->pw_uid, p->pw_gid});
@@ -149,6 +218,7 @@ Db load() {
        db.groups.push_back(gr);
    }
    endgrent();
#endif
    setspent();
    while (struct spwd *s = getspent())
        db.shadow.push_back({s->sp_namp, s->sp_pwdp, s->sp_lstchg, s->sp_min, s->sp_max, s->sp_warn,
@@ -193,9 +263,16 @@ bool save(Db &db, std::string &err) {

    bool ok = writeTemp(pw, 0644, [&](FILE *f) {
        for (auto &u : db.users) {
#ifdef WEBCTL_UID_R
            struct passwd_id_r p = {(char *)u.name.c_str(), (char *)u.passwd.c_str(), idOf(u.uid, u.uidR),
                                    idOf(u.gid, u.gidR), (char *)u.gecos.c_str(), (char *)u.dir.c_str(),
                                    (char *)u.shell.c_str()};
            if (putpwent_id_r(&p, f) != 0) return false;
#else
            struct passwd p = {(char *)u.name.c_str(), (char *)u.passwd.c_str(), u.uid, u.gid,
                               (char *)u.gecos.c_str(), (char *)u.dir.c_str(), (char *)u.shell.c_str()};
            if (putpwent(&p, f) != 0) return false;
#endif
        }
        return true;
    }, err);
@@ -204,8 +281,13 @@ bool save(Db &db, std::string &err) {
            std::vector<char *> mem;
            for (auto &m : g.members) mem.push_back((char *)m.c_str());
            mem.push_back(nullptr);
#ifdef WEBCTL_UID_R
            struct group_id_r e = {(char *)g.name.c_str(), (char *)g.passwd.c_str(), idOf(g.gid, g.gidR), mem.data()};
            if (putgrent_id_r(&e, f) != 0) return false;
#else
            struct group e = {(char *)g.name.c_str(), (char *)g.passwd.c_str(), g.gid, mem.data()};
            if (putgrent(&e, f) != 0) return false;
#endif
        }
        return true;
    }, err);
@@ -406,8 +488,10 @@ json_object *removeHome(const Db &db, const User &u) {
    for (auto &o : db.users)
        if (o.name != u.name && o.dir == u.dir) return error(tr("%1 is also %2's home", "%1 ist auch das Home von %2", {u.dir, o.name}));
    struct stat st;
    std::string owner;
    if (lstat(u.dir.c_str(), &st) != 0) return errno == ENOENT ? nullptr : error(u.dir + ": " + strerror(errno), 500);
    if (!S_ISDIR(st.st_mode) || st.st_uid != u.uid)
    if (!ownerKey(u.dir, owner)) return error(u.dir + ": " + strerror(errno), 500);
    if (!S_ISDIR(st.st_mode) || owner != idKey(u.uid, u.uidR))
        return error(tr("%1 isn't a directory owned by %2, left alone", "%1 ist kein Verzeichnis von %2, bleibt erhalten", {u.dir, u.name}));
    int fd = open(u.dir.c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (fd < 0) return error(u.dir + ": " + strerror(errno), 500);
@@ -436,10 +520,10 @@ json_object *list() {
    for (auto &u : db.users) {
        json_object *o = json_object_new_object();
        json_object_object_add(o, "name", jstr(u.name));
        json_object_object_add(o, "uid", json_object_new_int64(u.uid));
        json_object_object_add(o, "gid", json_object_new_int64(u.gid));
        Group *pg = db.groupById(u.gid);
        json_object_object_add(o, "group", jstr(pg ? pg->name : std::to_string(u.gid)));
        json_object_object_add(o, "uid", idJson(u.uid, u.uidR));
        json_object_object_add(o, "gid", idJson(u.gid, u.gidR));
        Group *pg = db.primaryOf(u);
        json_object_object_add(o, "group", jstr(pg ? pg->name : idKey(u.gid, u.gidR)));
        json_object_object_add(o, "gecos", jstr(u.gecos));
        json_object_object_add(o, "home", jstr(u.dir));
        json_object_object_add(o, "shell", jstr(u.shell));
@@ -454,7 +538,7 @@ json_object *list() {
        json_object_object_add(o, "locked", json_object_new_boolean(locked));
        json_object_object_add(o, "password", json_object_new_boolean(!hash.empty() && hash != "!" && hash != "*"));
        json_object_object_add(o, "lastChange", json_object_new_int64(s ? s->lstchg : -1));
        json_object_object_add(o, "system", json_object_new_boolean(u.uid != 0 && u.uid < FirstUid));
        json_object_object_add(o, "system", json_object_new_boolean(u.uidR.empty() && u.uid != 0 && u.uid < FirstUid));
        unsigned char nt[16];
        json_object_object_add(o, "smb", json_object_new_boolean(shadow_get_nthash(u.name.c_str(), nt) == 1));
        memset(nt, 0, sizeof(nt));
@@ -464,15 +548,15 @@ json_object *list() {
    for (auto &g : db.groups) {
        json_object *o = json_object_new_object();
        json_object_object_add(o, "name", jstr(g.name));
        json_object_object_add(o, "gid", json_object_new_int64(g.gid));
        json_object_object_add(o, "gid", idJson(g.gid, g.gidR));
        json_object *mem = json_object_new_array();
        for (auto &m : g.members) json_object_array_add(mem, jstr(m));
        json_object_object_add(o, "members", mem);
        json_object *primary = json_object_new_array();
        for (auto &u : db.users)
            if (u.gid == g.gid) json_object_array_add(primary, jstr(u.name));
            if (Db::isPrimary(u, g)) json_object_array_add(primary, jstr(u.name));
        json_object_object_add(o, "primaryOf", primary);
        json_object_object_add(o, "system", json_object_new_boolean(g.gid != 0 && g.gid < FirstUid));
        json_object_object_add(o, "system", json_object_new_boolean(g.gidR.empty() && g.gid != 0 && g.gid < FirstUid));
        json_object_array_add(groups, o);
    }
    json_object *sh = json_object_new_array();
@@ -488,9 +572,9 @@ json_object *list() {
uid_t freeId(const Db &db, bool group, uid_t wanted) {
    auto taken = [&](uid_t id) {
        if (group) {
            for (auto &g : db.groups) if (g.gid == id) return true;
            for (auto &g : db.groups) if (g.gidR.empty() && g.gid == id) return true;
        } else {
            for (auto &u : db.users) if (u.uid == id) return true;
            for (auto &u : db.users) if (u.uidR.empty() && u.uid == id) return true;
        }
        return false;
    };
@@ -523,22 +607,24 @@ json_object *create(json_object *a) {
    if (uidArg != -1 && (uidArg < 1 || uidArg > 60000)) return error(tr("invalid uid", "ungültige UID"));
    if (uidArg != -1)
        for (auto &u : db.users)
            if (u.uid == (uid_t)uidArg) return error(tr("uid %1 belongs to %2", "UID %1 gehört bereits %2", {std::to_string(uidArg), u.name}), 409);
            if (u.uidR.empty() && u.uid == (uid_t)uidArg) return error(tr("uid %1 belongs to %2", "UID %1 gehört bereits %2", {std::to_string(uidArg), u.name}), 409);
    uid_t uid = uidArg != -1 ? (uid_t)uidArg : freeId(db, false, FirstUid);

    gid_t gid;
    std::string gidR;
    std::string groupName = webctl::str(a, "group");
    if (!groupName.empty()) {
        Group *g = db.group(groupName);
        if (!g) return error(tr("no group %1", "keine Gruppe %1", {groupName}), 404);
        gid = g->gid;
        gidR = g->gidR;
    } else {
        if (db.group(name)) return error(tr("a group %1 exists already; choose it as the primary group", "eine Gruppe %1 gibt es schon; bitte als primäre Gruppe auswählen", {name}), 409);
        gid = freeId(db, true, uid);
        db.groups.push_back({name, "x", gid, {}});
        db.groups.push_back({name, "x", gid, {}, ""});
    }

    User u{name, "x", gecos, home, shell, uid, gid};
    User u{name, "x", gecos, home, shell, uid, gid, "", gidR};
    db.users.push_back(u);
    db.shadow.push_back({name, "!", today(), 0, 99999, 7, -1, -1, 0});
    if (json_object *e = setMemberships(db, name, groups)) return e;
@@ -588,7 +674,7 @@ json_object *update(json_object *a, const std::string &self) {
        if (json_object *e = setPassword(db, name, password)) return e;
    if (webctl::has(a, "locked")) {
        bool lockIt = webctl::boolean(a, "locked");
        if (lockIt && (u->uid == 0 || name == self)) return error(tr("root can't be locked", "root kann nicht gesperrt werden"), 403);
        if (lockIt && ((u->uidR.empty() && u->uid == 0) || name == self)) return error(tr("root can't be locked", "root kann nicht gesperrt werden"), 403);
        Shadow *s = db.shadowOf(name);
        if (!s) return error(tr("%1 has no shadow entry", "%1 hat keinen Shadow-Eintrag", {name}), 500);
        bool isLocked = !s->pwd.empty() && s->pwd[0] == '!';
@@ -613,7 +699,7 @@ json_object *remove(json_object *a, const std::string &self) {
    Db db = load();
    User *u = db.user(name);
    if (!u) return error(tr("no user %1", "kein Benutzer %1", {name}), 404);
    if (u->uid == 0 || name == self) return error(tr("root can't be deleted", "root kann nicht gelöscht werden"), 403);
    if ((u->uidR.empty() && u->uid == 0) || name == self) return error(tr("root can't be deleted", "root kann nicht gelöscht werden"), 403);
    User gone = *u;

    if (webctl::boolean(a, "removeHome"))
@@ -627,8 +713,8 @@ json_object *remove(json_object *a, const std::string &self) {
        g.members.erase(std::remove(g.members.begin(), g.members.end(), name), g.members.end());
    // the user's own group goes too, unless someone else still uses it
    db.groups.erase(std::remove_if(db.groups.begin(), db.groups.end(), [&](const Group &g) {
        if (g.name != name || g.gid != gone.gid || !g.members.empty()) return false;
        for (auto &o : db.users) if (o.gid == g.gid) return false;
        if (g.name != name || !Db::isPrimary(gone, g) || !g.members.empty()) return false;
        for (auto &o : db.users) if (Db::isPrimary(o, g)) return false;
        return true;
    }), db.groups.end());

@@ -648,7 +734,7 @@ json_object *groupCreate(json_object *a) {
    if (gidArg != -1 && (gidArg < 1 || gidArg > 60000)) return error(tr("invalid gid", "ungültige GID"));
    if (gidArg != -1 && db.groupById((gid_t)gidArg)) return error(tr("gid %1 is taken", "GID %1 ist vergeben", {std::to_string(gidArg)}), 409);
    gid_t gid = gidArg != -1 ? (gid_t)gidArg : freeId(db, true, FirstUid);
    db.groups.push_back({name, "x", gid, {}});
    db.groups.push_back({name, "x", gid, {}, ""});
    std::string err;
    if (!save(db, err)) return error(err, 500);
    return webctl::ok();
@@ -682,9 +768,9 @@ json_object *groupDelete(json_object *a) {
    Db db = load();
    Group *g = db.group(name);
    if (!g) return error(tr("no group %1", "keine Gruppe %1", {name}), 404);
    if (g->gid == 0) return error(tr("the root group can't be deleted", "die Gruppe root kann nicht gelöscht werden"), 403);
    if (g->gidR.empty() && g->gid == 0) return error(tr("the root group can't be deleted", "die Gruppe root kann nicht gelöscht werden"), 403);
    for (auto &u : db.users)
        if (u.gid == g->gid) return error(tr("%1 is %2's primary group", "%1 ist die primäre Gruppe von %2", {name, u.name}), 409);
        if (Db::isPrimary(u, *g)) return error(tr("%1 is %2's primary group", "%1 ist die primäre Gruppe von %2", {name, u.name}), 409);
    db.groups.erase(std::remove_if(db.groups.begin(), db.groups.end(), [&](const Group &x) { return x.name == name; }),
                    db.groups.end());
    std::string err;
+6 −1
Changes for src/backend/main.cpp: 6 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -97,7 +97,12 @@ private:
};

void Backend::start() {
    if (struct passwd *pw = getpwnam(_cfg.user.c_str())) {
    struct passwd *pw = getpwnam(_cfg.user.c_str());
    if (pw && pw->pw_uid == 65534) {
        // the overflow id: SO_PEERCRED shows every UUID or GUID user
        // (FenrirOS uid_r) as it, so it can't tell who connects
        std::cerr << "webctl-backend: " << _cfg.user << " has the overflow uid 65534, only root may connect" << std::endl;
    } else if (pw) {
        _frontendUid = pw->pw_uid;
        _frontendGid = pw->pw_gid;
    } else {