Unify service accounts to one kind, add manager field, make homeDirectory/loginShell editable

Removes the LDAP bind-only service account type in favor of a single
Unix/POSIX account model, surfaced in a new Users > Service Accounts tab.
Adds a multi-valued `manager` field to every account (defaults to the
creator, editable, and grants edit rights on the accounts a person manages
without needing app_sso_admin). homeDirectory and loginShell are now
editable from the profile edit form.

Bumps to v1.1.7.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
This commit is contained in:
2026-07-17 00:32:19 -04:00
parent 5fc65d6fb3
commit cdc5d1528c
14 changed files with 376 additions and 406 deletions
-54
View File
@@ -1,54 +0,0 @@
'use strict';
const router = require('express').Router();
const {ServiceAccount} = require('../models/service_account');
const permission = require('../utils/permission');
const ADMIN_GROUP = 'app_sso_admin';
router.get('/', async function(req, res, next) {
try {
await permission.byGroup(req.user, [ADMIN_GROUP]);
return res.json({results: await ServiceAccount.list()});
} catch(error) {
next(error);
}
});
router.post('/', async function(req, res, next) {
try {
await permission.byGroup(req.user, [ADMIN_GROUP]);
const result = await ServiceAccount.create({cn: req.body.cn, description: req.body.description});
return res.json({
results: result,
message: `Service account "${result.cn}" created. Save the password now — it will not be shown again.`,
});
} catch(error) {
next(error);
}
});
router.put('/:cn/password', async function(req, res, next) {
try {
await permission.byGroup(req.user, [ADMIN_GROUP]);
const result = await ServiceAccount.setPassword(req.params.cn, req.body.password);
return res.json({
results: result,
message: `Password rotated for "${req.params.cn}". Save it now — it will not be shown again.`,
});
} catch(error) {
next(error);
}
});
router.delete('/:cn', async function(req, res, next) {
try {
await permission.byGroup(req.user, [ADMIN_GROUP]);
await ServiceAccount.remove(req.params.cn);
return res.json({message: `Service account "${req.params.cn}" deleted.`});
} catch(error) {
next(error);
}
});
module.exports = router;
+10 -1
View File
@@ -23,6 +23,7 @@ router.post('/', async function(req, res, next){
await permission.byGroup(req.user, ['app_sso_admin'])
req.body.created_by = req.user.uid
req.body.manager = [req.user.dn];
const user = await User.add(req.body);
const verif = await UserVerification.getOrCreate(user.uid);
@@ -145,7 +146,15 @@ router.put('/:uid', async function(req, res, next){
user = req.user;
}else{
user = await User.get(req.params.uid);
await permission.byGroup(req.user, ['app_sso_admin'])
const isManager = (user.manager || []).includes(req.user.dn);
if(!isManager) await permission.byGroup(req.user, ['app_sso_admin'])
}
// The manager picker is a tag widget backed by a single newline-separated
// hidden input (see public/js/app.js app.ui.userSelect), same convention
// as oauth_client.js's allowed_groups.
if (typeof req.body.manager === 'string') {
req.body.manager = req.body.manager.split('\n').map(s => s.trim()).filter(Boolean);
}
return res.json({