Loading modules/users/users.cpp +62 −5 Changes for modules/users/users.cpp: 62 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -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) Loading @@ -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 Loading Loading @@ -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(); Loading @@ -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) { Loading Loading @@ -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(); Loading @@ -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; Loading @@ -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; Loading Loading @@ -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); } Loading www/modules/users.js +32 −4 Changes for www/modules/users.js: 32 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -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.', Loading Loading @@ -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}; Loading @@ -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: () => [ Loading @@ -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')); Loading Loading @@ -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'))), Loading Loading @@ -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, Loading Loading
modules/users/users.cpp +62 −5 Changes for modules/users/users.cpp: 62 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -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) Loading @@ -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 Loading Loading @@ -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(); Loading @@ -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) { Loading Loading @@ -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(); Loading @@ -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; Loading @@ -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; Loading Loading @@ -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); } Loading
www/modules/users.js +32 −4 Changes for www/modules/users.js: 32 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -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.', Loading Loading @@ -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}; Loading @@ -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: () => [ Loading @@ -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')); Loading Loading @@ -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'))), Loading Loading @@ -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, Loading