fix: internal service accounts are the router's own, of which the metrics service's is the one at present, and the documentation says what a service account is.
Release / release (push) Successful in 13s

This commit is contained in:
slawek committed 2026-10-25 18:27:10 +01:00
1 parent f01be41191
commit 68ebd541b2
5 files changed
+60 -40

No files matched your search

+15
View File
@@ -1,3 +1,18 @@
vpn-router (0.18.3-1) unstable; urgency=medium
fix: internal service accounts are the router's own, of which the metrics service's is the one at present, and the documentation says what a service account is.
* /service-accounts, /groups and the internal field of the answers
take every internal service account, the accounts the router creates
and manages for its own services, instead of only the metrics
service's; the 403 answers say that the router manages them.
* The README, the ADR and the users module describe a service account
as the account another system, such as an OpenTelemetry Collector
reading /metrics, signs in with, and the internal ones as managed by
the router.
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Mon, 26 Oct 2026 19:00:00 +0100
vpn-router (0.18.2-1) unstable; urgency=medium
fix: service accounts are managed at /service-accounts, not /users, and the internal one of the metrics service is only listed.
@@ -7,10 +7,10 @@ common name, which OpenVPN and IKEv2 accept. A Manager-only account is an accoun
simpleSecurityObject and vpnRouterAccount (the package's schema) with a name, a password and
optionally an e-mail address, no names, no certificate and so no VPN access; it is added when
neither the first nor the last name is given, and can sign in to the web service. A service
account is a Manager-only account with the scope SERVICE_SCOPE in vpnRouterScope, which cannot
sign in to the web service; an administrator adds one for a service that checks passwords
against the directory. The internal one is the account metrics.service_account names while
metrics are enabled, which vpn-router-setup adds and whose password it keeps (metrics_account).
account is a Manager-only account with the scope SERVICE_SCOPE in vpnRouterScope, which another
system signs in with to use a router service, and which cannot sign in to the web service. The
internal service accounts are those the router creates and manages for its own services
(internal_accounts); vpn-router-setup keeps the metrics service's.
Users exist whether or not a P2S system is enabled.
A group is a groupOfNames in ou=groups with a name, which follows the rules of user names, an
@@ -194,19 +194,20 @@ def service(attributes):
return SERVICE_SCOPE.encode() in (value.lower() for value in attributes.get('vpnRouterScope', []))
def metrics_account():
"""The internal service account: the one metrics.service_account names while metrics are enabled, else None."""
def internal_accounts():
"""The names of the internal service accounts, which the router creates and manages for its own services: the one
metrics.service_account names while metrics are enabled."""
try:
metrics = vpnrouter.load_config()['metrics']
except SystemExit:
return None
return metrics['service_account'] if metrics['enabled'] else None
return set()
return {metrics['service_account']} if metrics['enabled'] else set()
def summary(attributes):
"""A user as the answers show it; a person attribute the entry does not have is None.
internal is whether it is the internal service account (metrics_account).
internal is whether it is an internal service account (internal_accounts).
"""
name = attributes['uid'][0].decode()
lock = attributes.get('pwdAccountLockedTime', [None])[0]
@@ -214,7 +215,7 @@ def summary(attributes):
'name': name,
'manager_only': manager_only(attributes),
'service': service(attributes),
'internal': service(attributes) and name == metrics_account(),
'internal': service(attributes) and name in internal_accounts(),
'enabled': lock != LOCKED,
'locked': lock not in (None, LOCKED) and locked_since(lock) > time.time() - vpnrouter_ldap.LOCKOUT_SECONDS,
'certificate': certificate(name)[0].is_file(),
+10 -9
View File
@@ -15,7 +15,7 @@ Pages:
profile, and TOTP enrolment (see users()); service accounts are not among them
/service-accounts
the service accounts: list, add, change the e-mail address, set the password,
disable, enable, unlock and remove; the internal one is only listed (see
disable, enable, unlock and remove; the internal ones are only listed (see
service_accounts())
/groups the groups of the router's directory: list, add, rename or describe, remove, and
add and take out members (see groups())
@@ -516,8 +516,8 @@ def service_accounts(method, path, body):
POST /service-accounts/<name>/unlock end a lockout after failed logins
DELETE /service-accounts/<name> remove it
A name that is not a service account is 404. The internal one (vpnrouter_users.metrics_account), which
vpn-router-setup adds and keeps, is only listed: every other request on it is refused with 403.
A name that is not a service account is 404. The internal service accounts (vpnrouter_users.internal_accounts),
which the router manages, are only listed: every other request on one is refused with 403.
"""
if body == 'invalid' or (body is not None and not isinstance(body, dict)):
return 400, {'error': 'the body must be a JSON object'}
@@ -540,8 +540,8 @@ def service_accounts(method, path, body):
}
if route not in actions:
return 404, {'error': f'no route {method} {path}'}
if name is not None and name == vpnrouter_users.metrics_account():
return 403, {'error': f'{name} is the internal service account of the metrics service; only vpn-router-setup changes it'}
if name is not None and name in vpnrouter_users.internal_accounts():
return 403, {'error': f'{name} is an internal service account, which the router manages'}
try:
with vpnrouter_users.session() as connection:
if name is not None and not vpnrouter_users.service(vpnrouter_users.entry(connection, name)):
@@ -564,7 +564,7 @@ def groups(state, method, path, body):
POST /groups/<name>/members add a member: {"user": ...}
DELETE /groups/<name>/members/<user> take a member out; the last member cannot leave
The internal service account (vpnrouter_users.metrics_account) is not put in or taken out of a group: such a
An internal service account (vpnrouter_users.internal_accounts) is not put in or taken out of a group: such a
request is refused with 403.
"""
if body == 'invalid' or (body is not None and not isinstance(body, dict)):
@@ -573,11 +573,12 @@ def groups(state, method, path, body):
parts = path.strip('/').split('/')
name = parts[1] if len(parts) > 1 else None
route = (method, len(parts), parts[2] if len(parts) > 2 else None)
internal = vpnrouter_users.metrics_account()
members = body.get('members') if route == ('POST', 1, None) else [body.get('user')] if route == ('POST', 3, 'members') \
else [parts[3]] if route == ('DELETE', 4, 'members') else []
if internal is not None and isinstance(members, list) and internal in members:
return 403, {'error': f'{internal} is the internal service account of the metrics service; only vpn-router-setup changes it'}
internal = sorted(vpnrouter_users.internal_accounts().intersection(
member for member in (members if isinstance(members, list) else []) if isinstance(member, str)))
if internal:
return 403, {'error': f'{", ".join(internal)}: internal service accounts, which the router manages, are not group members'}
actions = {
('GET', 1, None): lambda connection: vpnrouter_users.list_groups(connection),
('POST', 1, None): lambda connection: vpnrouter_users.group_add(