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

test

parent a1c1c89a
Loading
Loading
Loading
Loading
+62 −5
Changes for modules/users/users.cpp: 62 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -37,7 +37,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *   update  {name, gecos?, home?, shell?, groups?[], password?, locked?}
 *   delete  {name, removeHome?}
 *   groupCreate {name, gid?}
 *   groupUpdate {name, members[]}
 *   groupUpdate {name, members[]}   members: local users or ones of the
 *                                    other sources (adconnect: user@domain)
 *   externalGroups {name, groups[]}  the local groups a user of another
 *                                    source is a member of
 *   groupDelete {name}
 *   sources, sourcesSave, adconnectSave, adconnectTest, adconnectLookup
 *                                                    (sources.h)
@@ -53,8 +56,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 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 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 and gid (their own group), groups made here a new UUID as gid,
 * when the kernel has the *_r calls; with "uid"/"gid" (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
@@ -642,6 +646,11 @@ json_object *list() {
        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 *groups = json_object_new_array();
        for (auto &g : db.groups)
            if (std::find(g.members.begin(), g.members.end(), u.name) != g.members.end())
                json_object_array_add(groups, jstr(g.name));
        json_object_object_add(o, "groups", groups);
        json_object_array_add(eu, o);
    }
    json_object *eg = json_object_new_array();
@@ -664,6 +673,24 @@ json_object *list() {
    return out;
}

// a user the system knows besides the local files (adconnect, ...): one
// local groups can list as a member too
bool externalUser(Db &db, const std::string &name) {
    if (name.empty() || name.size() > 256 || !validText(name) || name.find(',') != std::string::npos) return false;
    if (db.user(name)) return false;
#ifdef WEBCTL_UID_R
    return getpwnam_id_r(name.c_str()) != nullptr;
#else
    return getpwnam(name.c_str()) != nullptr;
#endif
}

bool memberOfAny(const Db &db, const std::string &name) {
    for (auto &g : db.groups)
        if (std::find(g.members.begin(), g.members.end(), name) != g.members.end()) return true;
    return false;
}

uid_t freeId(const Db &db, bool group, uid_t wanted) {
    auto taken = [&](uid_t id) {
        if (group) {
@@ -844,7 +871,16 @@ 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, {}, ""});
    std::string gidR;  // a new UUID group (the default where the kernel can use one)
#ifdef WEBCTL_UID_R
    if (gidArg == -1 && kernelHasUidR()) {
        uid_r_t id;
        if (uid_r_uuidgen(&id) != 0) return error(tr("no UUID for the group", "keine UUID für die Gruppe"), 500);
        gidR = idText(id);
        gid = UID_R_OVERFLOW;
    }
#endif
    db.groups.push_back({name, "x", gid, {}, gidR});
    std::string err;
    if (!save(db, err)) return error(err, 500);
    return webctl::ok();
@@ -862,7 +898,9 @@ json_object *groupUpdate(json_object *a) {
    if (!g) return error(tr("no group %1", "keine Gruppe %1", {name}), 404);
    std::vector<std::string> unique;
    for (auto &m : members) {
        if (!db.user(m)) return error(tr("no user %1", "kein Benutzer %1", {m}), 404);
        // a member already listed stays, even while its source can't be asked
        bool listed = std::find(g->members.begin(), g->members.end(), m) != g->members.end();
        if (!db.user(m) && !listed && !externalUser(db, m)) return error(tr("no user %1", "kein Benutzer %1", {m}), 404);
        if (std::find(unique.begin(), unique.end(), m) == unique.end()) unique.push_back(m);
    }
    g->members = unique;
@@ -871,6 +909,24 @@ json_object *groupUpdate(json_object *a) {
    return webctl::ok();
}

json_object *externalGroups(json_object *a) {
    std::string name = webctl::str(a, "name");
    bool listOk;
    std::vector<std::string> groups = stringList(a, "groups", listOk);
    if (!listOk) return error(tr("groups must be a list of group names", "groups muss eine Liste von Gruppennamen sein"));
    DbLock lock;
    if (!lock.held) return error(tr("the user database is locked by another program", "die Benutzerdatenbank ist von einem anderen Programm gesperrt"), 503);
    Db db = load();
    if (db.user(name)) return error(tr("%1 is a local user", "%1 ist ein lokaler Benutzer", {name}), 409);
    // one no longer found may still leave groups
    if (!externalUser(db, name) && !(groups.empty() && memberOfAny(db, name)))
        return error(tr("no user %1", "kein Benutzer %1", {name}), 404);
    if (json_object *e = setMemberships(db, name, groups)) return e;
    std::string err;
    if (!save(db, err)) return error(err, 500);
    return webctl::ok();
}

json_object *groupDelete(json_object *a) {
    std::string name = webctl::str(a, "name");
    DbLock lock;
@@ -900,6 +956,7 @@ json_object *call(const webctl_request *req) {
    if (action == "groupCreate") return groupCreate(req->args);
    if (action == "groupUpdate") return groupUpdate(req->args);
    if (action == "groupDelete") return groupDelete(req->args);
    if (action == "externalGroups") return externalGroups(req->args);
    return error(tr("unknown action %1", "unbekannte Aktion %1", {action}), 404);
}

+32 −4
Changes for www/modules/users.js: 32 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -38,6 +38,9 @@
    'Stores an extra NT hash of the same password, which SMB needs.':
      'Speichert zusätzlich einen NT-Hash desselben Passworts, den SMB braucht.',
    'Users & groups': 'Benutzer & Gruppen', 'Sources': 'Quellen',
    'Add users from other sources': 'Benutzer aus anderen Quellen hinzufügen',
    'e.g. user@domain; several separated by commas': 'z. B. benutzer@domain; mehrere durch Kommas getrennt',
    'Local groups': 'Lokale Gruppen', 'Local groups of {0}': 'Lokale Gruppen von {0}', '{0} saved.': '{0} gespeichert.',
    'Users from other sources': 'Benutzer aus anderen Quellen', 'Groups from other sources': 'Gruppen aus anderen Quellen',
    'Managed by their source; shown here as the system sees them.':
      'Werden von ihrer Quelle verwaltet; hier so angezeigt, wie das System sie sieht.',
@@ -294,7 +297,7 @@
      fields: () => [
        h('div', {class: 'row'},
          h('label', {}, t('Name'), h('input', {name: 'name', required: true, pattern: '[a-z_][a-z0-9_\\-]{0,31}'})),
          h('label', {}, t('GID (optional)'), h('input', {name: 'gid', type: 'number', min: 1, max: 60000}))),
          h('label', {}, t('GID (optional)'), h('input', {name: 'gid', type: 'number', min: 1, max: 60000, placeholder: t('empty: a new UUID')}))),
      ],
      onSubmit: async d => {
        const args = {name: d.name};
@@ -307,6 +310,10 @@
  }

  function editMembers(g) {
    // members that aren't local users: ones of adconnect & co. (user@domain)
    const local = new Set(data.users.map(u => u.name));
    const external = g.members.filter(m => !local.has(m));
    const offer = (data.externalUsers || []).map(u => u.name).filter(n => !external.includes(n));
    dialog({
      title: t('Members of {0}', g.name),
      fields: () => [
@@ -314,16 +321,35 @@
          h('label', {class: 'check'},
            h('input', {type: 'checkbox', name: 'members', value: u.name, checked: g.members.includes(u.name)}),
            u.name))),
        external.length ? h('div', {class: 'checklist'}, external.map(m =>
          h('label', {class: 'check'}, h('input', {type: 'checkbox', name: 'members', value: m, checked: true}), m))) : null,
        h('label', {}, t('Add users from other sources'),
          h('input', {name: 'add', list: 'external-users', autocomplete: 'off',
                      placeholder: t('e.g. user@domain; several separated by commas')})),
        h('datalist', {id: 'external-users'}, offer.map(n => h('option', {value: n}))),
        g.primaryOf.length ? h('p', {class: 'hint'}, t('Primary group of: {0}', g.primaryOf.join(', '))) : null,
      ],
      onSubmit: async d => {
        await call('users', 'groupUpdate', {name: g.name, members: d.members || []});
        const add = (d.add || '').split(/[,;\n]/).map(s => s.trim()).filter(Boolean);
        await call('users', 'groupUpdate', {name: g.name, members: [...(d.members || []), ...add]});
        toast(t('Group {0} saved.', g.name));
        load();
      },
    });
  }

  function editExternalGroups(u) {
    dialog({
      title: t('Local groups of {0}', u.name),
      fields: () => [groupChecklist(u.groups || [])],
      onSubmit: async d => {
        await call('users', 'externalGroups', {name: u.name, groups: d.groups || []});
        toast(t('{0} saved.', u.name));
        load();
      },
    });
  }

  async function deleteGroup(g) {
    const ok = await confirmAction(t('Delete group {0}', g.name),
                                   t('The group {0} (GID {1}) will be deleted.', g.name, g.gid), t('Delete'));
@@ -401,6 +427,8 @@
          {title: t('Full name'), render: u => u.gecos},
          {title: 'Home', render: u => u.home, class: 'mono'},
          {title: t('Shell'), render: u => u.shell, class: 'mono'},
          {title: t('Local groups'), render: u => h('span', {class: 'chips'}, (u.groups || []).map(g => h('span', {}, g)))},
          {title: '', class: 'actions', render: u => h('button', {class: 'small', onclick: () => editExternalGroups(u)}, t('Groups'))},
        ], data.externalUsers)) : null,
      (data.externalGroups || []).length ? h('section', {class: 'panel'},
        h('div', {class: 'toolbar'}, h('h2', {}, t('Groups from other sources'))),
@@ -483,9 +511,9 @@
  function adConfigFrom(d) {
    const num = v => v === '' || v === undefined ? '' : Number(v);
    return {
      domain: d.domain.trim().toLowerCase(), base: d.base.trim(), servers: d.servers.split(/[\s,]+/).filter(Boolean),
      domain: d.domain.trim().toLowerCase(), base: d.base.trim(), servers: d.servers.split(/[,;\n]/).map(s => s.trim()).filter(Boolean),
      auth: d.auth, kerberos_principal: d.kerberos_principal.trim(), keytab: d.keytab.trim(),
      kdcs: d.kdcs.split(/[\s,]+/).filter(Boolean),
      kdcs: d.kdcs.split(/[,;\n]/).map(s => s.trim()).filter(Boolean),
      tls: d.tls, port: num(d.port), insecure: !!d.insecure, ca_file: d.ca_file.trim(), bind_dn: d.bind_dn.trim(),
      id_mapping: d.id_mapping, id_base: num(d.id_base), id_range: num(d.id_range),
      home: d.home.trim(), shell: d.shell.trim(), lowercase_names: !!d.lowercase_names,