Commit a818ece1 authored by jan.koester's avatar jan.koester
Browse files

test

parent 63614d28
Loading
Loading
Loading
Loading
+4 −1
Changes for CMakeLists.txt: 4 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -80,7 +80,10 @@ function(webctl_module name)
    install(TARGETS webctl_mod_${name} LIBRARY DESTINATION ${WEBCTL_MODULEDIR})
endfunction()

webctl_module(users modules/users/users.cpp)
# users: the libc's databases; sources.cpp: /etc/userdb.yaml and adconnect
# (libconfplus to read, sysbus for adconnectd's unit)
webctl_module(users modules/users/users.cpp modules/users/sources.cpp)
target_link_libraries(webctl_mod_users PRIVATE confplus::confplus sysbus::sysbus)
webctl_module(network modules/network/network.cpp)
target_link_libraries(webctl_mod_network PRIVATE sysbus::sysbus)
webctl_module(services modules/services/services.cpp)
+741 −0

File added.

Preview size limit exceeded, changes collapsed.

+54 −0
Changes for modules/users/sources.h: 54 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*******************************************************************************
Copyright (c) 2026, Jan Koester jan.koester@gmx.net
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

/* The "users" module's sources half: where the libc finds users and
 * groups besides the local files (systempp's /etc/userdb.yaml, see
 * <userdb.h>), and adconnect -- Active Directory -- as the one source with
 * settings of its own (/etc/adconnect/adconnect.yaml, adconnectd).
 *
 *   sources                           -> {sources, available, moduleDir, adconnect}
 *   sourcesSave {sources: [{module, options: {}}]}
 *   adconnectSave {config: {...}, password?, enabled}
 *   adconnectTest {config: {...}, password?}   -> {ok, output}
 *   adconnectLookup {type: user|uid|group|gid|groups, name}
 *                                     -> {users, groups, gids}
 *
 * The bind password never goes back to the UI: "passwordSet" tells
 * whether there is one; an empty password keeps it. */

#pragma once

#include <string>

#include <json-c/json.h>

namespace users_sources {

// the reply to one of the actions above; nullptr for any other action
json_object *call(const std::string &action, json_object *args);

} // namespace users_sources
+86 −14
Changes for modules/users/users.cpp: 86 added lines, 14 removed lines.
Original line number Diff line number Diff line
@@ -39,6 +39,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *   groupCreate {name, gid?}
 *   groupUpdate {name, members[]}
 *   groupDelete {name}
 *   sources, sourcesSave, adconnectSave, adconnectTest, adconnectLookup
 *                                                    (sources.h)
 *
 * Without "group", create makes a group of the user's own name. A user
 * created without a password stays locked until one is set.
@@ -52,7 +54,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * read and written with the *_id_r functions, so a rewrite keeps them;
 * "list" shows such an id as text. Accounts made here get a new UUID as
 * uid and gid (their own group), when the kernel has the *_r calls; with
 * "uid" (or without them) a classic one. */
 * "uid" (or without them) a classic one.
 *
 * Only the local files are read for a rewrite (fget*ent_id_r()): users
 * and groups of the other sources in /etc/userdb.yaml (getpwent() lists
 * those too) must not end up in them. "list" shows those apart, as
 * externalUsers/externalGroups. The sources themselves: sources.cpp. */

#include <algorithm>
#include <cerrno>
@@ -76,6 +83,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include <webctl/module.h>

#include "sources.h"

#if __has_include(<sys/uid_r.h>)
#include <sys/uid_r.h>
#define WEBCTL_UID_R 1
@@ -214,41 +223,76 @@ std::string dbPath(const char *env, const char *dflt) {
    return p && *p ? p : dflt;
}

// the local files only -- what save() writes back
Db load() {
    Db db;
    std::string pwPath = dbPath("SYSTEMPP_PASSWD_YAML", _PATH_PASSWD);
    std::string grPath = dbPath("SYSTEMPP_GROUP_YAML", _PATH_GROUP);
    if (FILE *f = fopen(pwPath.c_str(), "r")) {
#ifdef WEBCTL_UID_R
    setpwent_id_r();
    while (struct passwd_id_r *p = getpwent_id_r())
        while (struct passwd_id_r *p = fgetpwent_id_r(f))
            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)});
#else
        while (struct passwd *p = fgetpwent(f))
            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});
#endif
        fclose(f);
    }
    if (FILE *f = fopen(grPath.c_str(), "r")) {
#ifdef WEBCTL_UID_R
        while (struct group_id_r *g = fgetgrent_id_r(f)) {
            Group gr{g->gr_name, g->gr_passwd, uid_r_to_legacy(&g->gr_gid), {}, idText(g->gr_gid)};
#else
        while (struct group *g = fgetgrent(f)) {
            Group gr{g->gr_name, g->gr_passwd, g->gr_gid, {}};
#endif
            for (char **m = g->gr_mem; m && *m; ++m) gr.members.push_back(*m);
            db.groups.push_back(gr);
        }
        fclose(f);
    }
    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,
                             s->sp_inact, s->sp_expire, s->sp_flag});
    endspent();
    return db;
}

// the users and groups of the other sources (those that list theirs)
void loadExternal(const Db &files, std::vector<User> &users, std::vector<Group> &groups) {
#ifdef WEBCTL_UID_R
    setpwent_id_r();
    while (struct passwd_id_r *p = getpwent_id_r())
        if (!const_cast<Db &>(files).user(p->pw_name))
            users.push_back({p->pw_name, "", 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)};
        if (const_cast<Db &>(files).group(g->gr_name)) continue;
        Group gr{g->gr_name, "", 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);
        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});
        if (!const_cast<Db &>(files).user(p->pw_name))
            users.push_back({p->pw_name, "", p->pw_gecos, p->pw_dir, p->pw_shell, p->pw_uid, p->pw_gid});
    endpwent();
    setgrent();
    while (struct group *g = getgrent()) {
        Group gr{g->gr_name, g->gr_passwd, g->gr_gid, {}};
        if (const_cast<Db &>(files).group(g->gr_name)) continue;
        Group gr{g->gr_name, "", g->gr_gid, {}};
        for (char **m = g->gr_mem; m && *m; ++m) gr.members.push_back(*m);
        db.groups.push_back(gr);
        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,
                             s->sp_inact, s->sp_expire, s->sp_flag});
    endspent();
    return db;
}

// lckpwdf() for the lifetime of the object
@@ -586,10 +630,37 @@ json_object *list() {
    json_object *sh = json_object_new_array();
    for (auto &s : shells(db)) json_object_array_add(sh, jstr(s));

    std::vector<User> extUsers;
    std::vector<Group> extGroups;
    loadExternal(db, extUsers, extGroups);
    json_object *eu = json_object_new_array();
    for (auto &u : extUsers) {
        json_object *o = json_object_new_object();
        json_object_object_add(o, "name", jstr(u.name));
        json_object_object_add(o, "uid", idJson(u.uid, u.uidR));
        json_object_object_add(o, "gid", idJson(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));
        json_object_array_add(eu, o);
    }
    json_object *eg = json_object_new_array();
    for (auto &g : extGroups) {
        json_object *o = json_object_new_object();
        json_object_object_add(o, "name", jstr(g.name));
        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_array_add(eg, o);
    }

    json_object *out = json_object_new_object();
    json_object_object_add(out, "users", users);
    json_object_object_add(out, "groups", groups);
    json_object_object_add(out, "shells", sh);
    json_object_object_add(out, "externalUsers", eu);
    json_object_object_add(out, "externalGroups", eg);
    return out;
}

@@ -821,6 +892,7 @@ json_object *call(const webctl_request *req) {
    webctl::LanguageScope lang(req->lang);
    std::string action = req->action;
    std::string self = req->user ? req->user : "";
    if (json_object *r = users_sources::call(action, req->args)) return r;
    if (action == "list") return list();
    if (action == "create") return create(req->args);
    if (action == "update") return update(req->args, self);
+17 −0
Changes for www/app.css: 17 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -105,6 +105,23 @@ label.check { color: var(--text); font-size: 1em; }
.toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 12px; }
.toolbar h2 { margin: 0 auto 0 0; }
.toolbar input[type=search] { width: 220px; }

/* tabs: one page of a module at a time */
.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--line-strong); margin-bottom: 16px; overflow-x: auto; }
.tabs button {
  background: transparent; color: var(--muted); border: 1px solid transparent; border-bottom: 0;
  border-radius: var(--radius) var(--radius) 0 0; padding: .5em 1.1em; margin-bottom: -1px;
}
.tabs button:hover { background: var(--panel); color: var(--text); border-color: transparent; }
.tabs button.active {
  background: var(--panel); color: var(--text); font-weight: 600;
  border-color: var(--line-strong); border-bottom: 1px solid var(--panel);
}
.tabs button .badge { margin-left: .4em; }
pre.output {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: .6em .8em; max-height: 16em; overflow: auto; white-space: pre-wrap; font-size: .9em;
}
.table-wrap { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; background: var(--surface); border-radius: var(--radius); }
th, td { text-align: left; padding: .5em .7em; border-bottom: 1px solid #e3e6e9; vertical-align: top; }
Loading