feature: service accounts have a display name and a description, which the router sets for its internal ones.
Release / release (push) Successful in 20s

This commit is contained in:
slawek committed 2026-10-25 18:34:59 +01:00
1 parent 68ebd541b2
commit 25c151841b
8 files changed
+139 -52

No files matched your search

+13 -8
View File
@@ -465,11 +465,14 @@ Decision: The package defines its schema, `cn=vpn-router,cn=schema,cn=config`, w
`vpnrouter_ldap.provision` adds when missing and keeps as `SCHEMA_ATTRIBUTE_TYPES` and `vpnrouter_ldap.provision` adds when missing and keeps as `SCHEMA_ATTRIBUTE_TYPES` and
`SCHEMA_OBJECT_CLASSES`. Its OID is `2.25.98477352595735834532404256531620013001`, made from a `SCHEMA_OBJECT_CLASSES`. Its OID is `2.25.98477352595735834532404256531620013001`, made from a
random UUID under the arc 2.25 (ITU-T X.667), which needs no registration; attribute types are random UUID under the arc 2.25 (ITU-T X.667), which needs no registration; attribute types are
under `.1` and object classes under `.2`. The first version has the attribute `vpnRouterScope` under `.1` and object classes under `.2`. It has the attribute `vpnRouterScope` (multi-valued,
(multi-valued, case-insensitive strings) and the auxiliary class `vpnRouterAccount`, case-insensitive strings) and the auxiliary class `vpnRouterAccount`,
`MAY ( mail $ vpnRouterScope )`. Manager-only accounts have the class from their creation, so an `MAY ( mail $ vpnRouterScope $ displayName $ description )`; `displayName` and `description`, the
e-mail address can be added later without changing their classes; VPN users get it once scopes are standard attributes, are the display name and the description of a service account, which
used. `account` cannot hold. Manager-only accounts have the class from their creation, so an e-mail
address can be added later without changing their classes; VPN users get it once scopes are used.
`vpnrouter_ldap.provision` replaces the definitions in `cn=config` that differ, so an extended class
reaches an existing directory at the next setup run.
Consequences: The schema check stays strict. New attributes extend `vpnRouterAccount`, and their Consequences: The schema check stays strict. New attributes extend `vpnRouterAccount`, and their
OIDs continue the arcs. The scope `service` marks a service account (ADR-23). The OID must never OIDs continue the arcs. The scope `service` marks a service account (ADR-23). The OID must never
@@ -572,9 +575,11 @@ which another system signs in with to use a router service. Administrators add s
with `vpn-router-user add --service` and the web service's `/service-accounts`. with `vpn-router-user add --service` and the web service's `/service-accounts`.
`/service-accounts` is a path of its own, apart from `/users`, which does not list or serve them: `/service-accounts` is a path of its own, apart from `/users`, which does not list or serve them:
a service account has no names, certificate, profile or TOTP, and is not a person an a service account has no names, certificate, profile or TOTP, and is not a person an
administrator manages with the users. Internal service accounts are those the router creates and administrator manages with the users. A service account has a display name and a description
manages for its own services (`vpnrouter_users.internal_accounts`), so the system, not an besides its name, so an administrator can tell what each is for. Internal service accounts are
administrator, owns them; at present that is the metrics service's. `/service-accounts` marks those the router creates and manages for its own services (`vpnrouter_users.internal_accounts`),
with every property, display name and description included, so the system, not an
administrator, owns them; at present that is the metrics service's, which setup names Metrics. `/service-accounts` marks
them as `internal` and only lists them: every other request on one, and putting one in or taking them as `internal` and only lists them: every other request on one, and putting one in or taking
one out of a group at `/groups`, gets 403, whatever the token. The router changes them through the one out of a group at `/groups`, gets 403, whatever the token. The router changes them through the
directory's user-manager account, not the API, and root may still with `vpn-router-user`. For the directory's user-manager account, not the API, and root may still with `vpn-router-user`. For the
+19 -14
View File
@@ -393,9 +393,10 @@ is switched on:
`account` for a Manager-only account (see Managing users); `account` for a Manager-only account (see Managing users);
- the package's own schema, `cn=vpn-router,cn=schema,cn=config`, under the OID - the package's own schema, `cn=vpn-router,cn=schema,cn=config`, under the OID
`2.25.98477352595735834532404256531620013001` of the UUID arc: the auxiliary class `2.25.98477352595735834532404256531620013001` of the UUID arc: the auxiliary class
`vpnRouterAccount` with `MAY ( mail $ vpnRouterScope )`, which Manager-only and service accounts `vpnRouterAccount` with `MAY ( mail $ vpnRouterScope $ displayName $ description )`, which
have, and the attribute `vpnRouterScope`, authorization scopes, of which `service` marks a Manager-only and service accounts have, and the attribute `vpnRouterScope`, authorization
service account; scopes, of which `service` marks a service account; `displayName` and `description` are the
display name and the description of a service account;
- `ou=groups` holds one `groupOfNames` per group, named by `cn`, with its members' DNs in - `ou=groups` holds one `groupOfNames` per group, named by `cn`, with its members' DNs in
`member` and an optional `description`; `member` and an optional `description`;
- `cn=user-manager,ou=services` is the account that changes users and groups; it may write - `cn=user-manager,ou=services` is the account that changes users and groups; it may write
@@ -429,10 +430,12 @@ it is added:
the router. `add` without the first and the last name adds one; its names cannot be set later; the router. `add` without the first and the last name adds one; its names cannot be set later;
- a service account, a Manager-only account whose `vpnRouterScope` is `service`, which another - a service account, a Manager-only account whose `vpnRouterScope` is `service`, which another
system signs in with to use a router service, such as an OpenTelemetry Collector reading system signs in with to use a router service, such as an OpenTelemetry Collector reading
`/metrics`; it cannot sign in to `/auth`. `add --service` adds one, and the web service manages `/metrics`; it cannot sign in to `/auth`. Besides its name, it has a display name and a
them at `/service-accounts`, not at `/users`. Internal service accounts are those the router description. `add --service` adds one, and the web service manages them at `/service-accounts`,
creates and manages for its own services; at present that is the metrics service's, the account not at `/users`. Internal service accounts are those the router creates and manages for its own
`metrics.service_account` names while metrics are enabled (see Metrics). The API only lists services, display name and description included; at present that is the metrics service's, the
account `metrics.service_account` names while metrics are enabled (see Metrics), which
`vpn-router-setup` names Metrics. The API only lists
them: `/service-accounts` refuses every other request on one, and `/groups` refuses to put one them: `/service-accounts` refuses every other request on one, and `/groups` refuses to put one
in or take one out of a group, with 403. in or take one out of a group, with 403.
@@ -466,8 +469,9 @@ vpn-router-user otp-remove alice
| `list` | The users with their kind, names and e-mail addresses, whether each is enabled or locked, has a certificate and is enrolled for TOTP | | `list` | The users with their kind, names and e-mail addresses, whether each is enabled or locked, has a certificate and is enrolled for TOTP |
| `add <name> --first-name F --last-name L --email E` | Adds a VPN user and issues the certificate | | `add <name> --first-name F --last-name L --email E` | Adds a VPN user and issues the certificate |
| `add <name> [--email E]` | Adds a Manager-only account, with the e-mail address when given; one name without the other two options is refused | | `add <name> [--email E]` | Adds a Manager-only account, with the e-mail address when given; one name without the other two options is refused |
| `add <name> --service [--email E]` | Adds a service account, with the e-mail address when given; the names are refused | | `add <name> --service [--display-name N] [--description D] [--email E]` | Adds a service account, with its display name, description and e-mail address when given; the names are refused |
| `update <name> [--first-name F] [--last-name L] [--email E]` | Changes the names or the e-mail address | | `update <name> [--first-name F] [--last-name L] [--email E]` | Changes the names or the e-mail address |
| `update <name> [--display-name N] [--description D] [--email E]` | Changes the display name, the description or the e-mail address of a service account; `''` removes one |
| `passwd <name>` | Sets the password | | `passwd <name>` | Sets the password |
| `disable <name>`, `enable <name>` | Locks the user out of OpenVPN and `/auth`, and lets the user in again | | `disable <name>`, `enable <name>` | Locks the user out of OpenVPN and `/auth`, and lets the user in again |
| `unlock <name>` | Ends a lockout after failed logins and clears the failure count; a disabled user stays disabled | | `unlock <name>` | Ends a lockout after failed logins and clears the failure count; a disabled user stays disabled |
@@ -752,18 +756,19 @@ curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
| Request | Body | Answer | | Request | Body | Answer |
| --- | --- | --- | | --- | --- | --- |
| `GET /service-accounts` | | `{"service_accounts": [{"name", "internal", "enabled", "locked", "email", ...}, ...]}`, each as `/users` shows a user | | `GET /service-accounts` | | `{"service_accounts": [{"name", "display_name", "description", "internal", "enabled", "locked", "email", ...}, ...]}`, each as `/users` shows a user |
| `POST /service-accounts` | `{"name", "password", "email"?}` | 201 and the account | | `POST /service-accounts` | `{"name", "password", "display_name"?, "description"?, "email"?}` | 201 and the account |
| `PUT /service-accounts/<name>` | `{"email"}`; `""` removes it | the account | | `PUT /service-accounts/<name>` | any of `{"display_name", "description", "email"}`; `""` removes one | the account |
| `PUT /service-accounts/<name>/password` | `{"password": ...}` | the account | | `PUT /service-accounts/<name>/password` | `{"password": ...}` | the account |
| `POST /service-accounts/<name>/disable` | | the account | | `POST /service-accounts/<name>/disable` | | the account |
| `POST /service-accounts/<name>/enable` | | the account | | `POST /service-accounts/<name>/enable` | | the account |
| `POST /service-accounts/<name>/unlock` | | the account | | `POST /service-accounts/<name>/unlock` | | the account |
| `DELETE /service-accounts/<name>` | | `{"name", "removed": true}` | | `DELETE /service-accounts/<name>` | | `{"name", "removed": true}` |
The internal service accounts, `internal` true, are only listed: every other request on one gets A display name has at most 64 printable characters and a description at most 256. The internal
403, as the router manages them. A failure is otherwise as for `/users`, with 404 also for a service accounts, `internal` true, are only listed: every other request on one gets 403, as the
name that is not a service account. router manages them, their display name and description included. A failure is otherwise as for
`/users`, with 404 also for a name that is not a service account.
### The /account API ### The /account API
+18
View File
@@ -1,3 +1,21 @@
vpn-router (0.19.0-1) unstable; urgency=medium
feature: service accounts have a display name and a description, which the router sets for its internal ones.
* vpnRouterAccount of the package's schema also takes displayName and
description; vpn-router-setup updates the schema of an existing
directory.
* /service-accounts shows display_name and description, takes them when
adding a service account and changes them with PUT; "" removes one.
/users shows them as null.
* vpn-router-user add --service and update take --display-name and
--description.
* vpn-router-setup gives the metrics service's internal account the
display name Metrics and its description, and sets them again when
they differ.
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Mon, 26 Oct 2026 20:00:00 +0100
vpn-router (0.18.3-1) unstable; urgency=medium 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. 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.
+9 -5
View File
@@ -458,14 +458,16 @@ def manage_admin():
def manage_metrics_account(ctx): def manage_metrics_account(ctx):
"""Keep the service account metrics.service_account names, whose password is in METRICS_SECRET; the problem, or ''. """Keep the service account metrics.service_account names, whose password is in METRICS_SECRET; the problem, or ''.
While metrics are enabled, setup owns the account: it adds it as a service account with a random password when While metrics are enabled, setup owns the account, an internal service account: it adds it as a service account
it is missing, and gives it a new random password, ending any lockout, when METRICS_SECRET is missing or does not with a random password and the labels of vpnrouter_users.internal_accounts when it is missing, sets those labels
hold its password. A disabled account is left as it is. An account of that name that is not a service account is when they differ, and gives it a new random password, ending any lockout, when METRICS_SECRET is missing or does
left alone and is a problem. not hold its password. The password of a disabled account is left as it is. An account of that name that is not a
service account is left alone and is a problem.
""" """
if not vpnrouter.metrics_ready(ctx): if not vpnrouter.metrics_ready(ctx):
return '' return ''
name = ctx['metrics_service_account'] name = ctx['metrics_service_account']
wanted = vpnrouter_users.internal_accounts().get(name, {})
try: try:
vpnrouter_users.check_name(name) vpnrouter_users.check_name(name)
try: try:
@@ -481,6 +483,8 @@ def manage_metrics_account(ctx):
current = None current = None
if current is not None and not vpnrouter_users.service(current): if current is not None and not vpnrouter_users.service(current):
return f'metrics.service_account: {name} is a user, not a service account' return f'metrics.service_account: {name} is a user, not a service account'
if current is not None and any(vpnrouter_users.summary(current)[key] != value for key, value in wanted.items()):
vpnrouter_users.update_service(connection, name, wanted)
# An administrator who disabled the account meant it; its password is left as it is. # An administrator who disabled the account meant it; its password is left as it is.
if current is not None and not vpnrouter_users.summary(current)['enabled']: if current is not None and not vpnrouter_users.summary(current)['enabled']:
return '' return ''
@@ -497,7 +501,7 @@ def manage_metrics_account(ctx):
METRICS_SECRET.chmod(0o600) METRICS_SECRET.chmod(0o600)
METRICS_SECRET.write_text(password + '\n', encoding='utf-8') METRICS_SECRET.write_text(password + '\n', encoding='utf-8')
if current is None: if current is None:
vpnrouter_users.add(connection, name, password, {}, service=True) vpnrouter_users.add(connection, name, password, wanted, service=True)
else: else:
vpnrouter_users.passwd(connection, name, password) vpnrouter_users.passwd(connection, name, password)
vpnrouter_users.unlock(connection, name) vpnrouter_users.unlock(connection, name)
@@ -52,8 +52,9 @@ OVERLAYS = ('ppolicy', 'otp')
#: The package's own schema, under an OID of the UUID arc 2.25 (ITU-T X.667): attribute types #: The package's own schema, under an OID of the UUID arc 2.25 (ITU-T X.667): attribute types
#: under .1 and object classes under .2. vpnRouterAccount, auxiliary, adds properties of the #: under .1 and object classes under .2. vpnRouterAccount, auxiliary, adds properties of the
#: package to a user of either kind: mail, for a Manager-only account, and vpnRouterScope, #: package to a user of either kind: mail, for a Manager-only account, vpnRouterScope,
#: authorization scopes, of which service marks a service account. #: authorization scopes, of which service marks a service account, and displayName and
#: description, the labels of a service account.
SCHEMA_OID = '2.25.98477352595735834532404256531620013001' SCHEMA_OID = '2.25.98477352595735834532404256531620013001'
SCHEMA_NAME = 'vpn-router' SCHEMA_NAME = 'vpn-router'
SCHEMA_ATTRIBUTE_TYPES = ( SCHEMA_ATTRIBUTE_TYPES = (
@@ -62,7 +63,7 @@ SCHEMA_ATTRIBUTE_TYPES = (
) )
SCHEMA_OBJECT_CLASSES = ( SCHEMA_OBJECT_CLASSES = (
f"( {SCHEMA_OID}.2.1 NAME 'vpnRouterAccount' DESC 'VPN router: properties of a user or a Manager-only account' " f"( {SCHEMA_OID}.2.1 NAME 'vpnRouterAccount' DESC 'VPN router: properties of a user or a Manager-only account' "
"SUP top AUXILIARY MAY ( mail $ vpnRouterScope ) )", "SUP top AUXILIARY MAY ( mail $ vpnRouterScope $ displayName $ description ) )",
) )
#: The TOTP parameters: digits, the OID of HMAC-SHA1, seconds per time step, and the time steps accepted before and after now. #: The TOTP parameters: digits, the OID of HMAC-SHA1, seconds per time step, and the time steps accepted before and after now.
@@ -8,9 +8,10 @@ simpleSecurityObject and vpnRouterAccount (the package's schema) with a name, a
optionally an e-mail address, no names, no certificate and so no VPN access; it is added when 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 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 another 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 system signs in with to use a router service, and which cannot sign in to the web service; it has
internal service accounts are those the router creates and manages for its own services a display name and a description (labels), in displayName and description of vpnRouterAccount. The
(internal_accounts); vpn-router-setup keeps the metrics service's. internal service accounts are those the router creates and manages for its own services, labels
included (internal_accounts); vpn-router-setup keeps the metrics service's.
Users exist whether or not a P2S system is enabled. 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 A group is a groupOfNames in ou=groups with a name, which follows the rules of user names, an
@@ -65,8 +66,11 @@ EMAIL = re.compile(r'^[^@\s]+@[^@\s]+\.[^@\s]+$')
EMAIL_MAX = 254 EMAIL_MAX = 254
#: The keys of the person attributes and their LDAP attributes. #: The keys of the person attributes and their LDAP attributes.
PERSON = (('first_name', 'givenName'), ('last_name', 'sn'), ('email', 'mail')) PERSON = (('first_name', 'givenName'), ('last_name', 'sn'), ('email', 'mail'))
#: The keys of the labels of a service account and their LDAP attributes.
LABELS = (('display_name', 'displayName'), ('description', 'description'))
#: The attributes read for a user. #: The attributes read for a user.
ATTRIBUTES = ['uid', 'objectClass', 'pwdAccountLockedTime', 'givenName', 'sn', 'mail', 'oathTOTPToken', 'vpnRouterScope'] ATTRIBUTES = ['uid', 'objectClass', 'pwdAccountLockedTime', 'givenName', 'sn', 'mail', 'oathTOTPToken', 'vpnRouterScope',
'displayName', 'description']
#: The vpnRouterScope of a service account. #: The vpnRouterScope of a service account.
SERVICE_SCOPE = 'service' SERVICE_SCOPE = 'service'
#: The object classes of a VPN user and of a Manager-only account. #: The object classes of a VPN user and of a Manager-only account.
@@ -147,6 +151,26 @@ def person(values, required):
return found return found
def labels(values):
"""The labels of a service account in values ({display_name, description}), checked, as {LDAP attribute: value}.
A key whose value is None is not given; '' means none. A display name has at most PERSON_NAME_MAX printable
characters and a description at most DESCRIPTION_MAX.
"""
found = {}
name = values.get('display_name')
if name is not None:
if not isinstance(name, str):
raise Failure('display_name must be a string', 2)
name = name.strip()
if len(name) > PERSON_NAME_MAX or any(not character.isprintable() for character in name):
raise Failure(f'the display name must have at most {PERSON_NAME_MAX} printable characters', 2)
found['displayName'] = name
if values.get('description') is not None:
found['description'] = check_description(values['description'])
return found
def dn_of(name): def dn_of(name):
return f'uid={name},{vpnrouter_ldap.USERS}' return f'uid={name},{vpnrouter_ldap.USERS}'
@@ -195,17 +219,23 @@ def service(attributes):
def internal_accounts(): def internal_accounts():
"""The names of the internal service accounts, which the router creates and manages for its own services: the one """The internal service accounts, which the router creates and manages for its own services, by name, each with
metrics.service_account names while metrics are enabled.""" the labels the router gives it ({display_name, description}): the one metrics.service_account names while metrics
are enabled."""
try: try:
metrics = vpnrouter.load_config()['metrics'] metrics = vpnrouter.load_config()['metrics']
except SystemExit: except SystemExit:
return set() return {}
return {metrics['service_account']} if metrics['enabled'] else set() if not metrics['enabled']:
return {}
return {metrics['service_account']: {
'display_name': 'Metrics',
'description': 'The account an OpenTelemetry Collector signs in with to read /metrics of the metrics service.',
}}
def summary(attributes): def summary(attributes):
"""A user as the answers show it; a person attribute the entry does not have is None. """A user as the answers show it; a person attribute or a label the entry does not have is None.
internal is whether it is an internal service account (internal_accounts). internal is whether it is an internal service account (internal_accounts).
""" """
@@ -221,7 +251,7 @@ def summary(attributes):
'certificate': certificate(name)[0].is_file(), 'certificate': certificate(name)[0].is_file(),
'otp': 'oathTOTPToken' in attributes, 'otp': 'oathTOTPToken' in attributes,
} }
for key, attribute in PERSON: for key, attribute in PERSON + LABELS:
values = attributes.get(attribute) values = attributes.get(attribute)
user[key] = values[0].decode() if values else None user[key] = values[0].decode() if values else None
return user return user
@@ -242,12 +272,14 @@ def list_users(connection):
def add(connection, name, password, values, service=False): def add(connection, name, password, values, service=False):
"""Add a user: a VPN user with its certificate when values gives all three person attributes, a Manager-only account """Add a user: a VPN user with its certificate when values gives all three person attributes, a Manager-only account
with at most the e-mail address when it gives neither name, and, with service, a service account, with at most the with at most the e-mail address when it gives neither name, and, with service, a service account, with at most the
e-mail address too.""" e-mail address and its labels (labels)."""
check_name(name) check_name(name)
details = person(values, required=False) details = person(values, required=False)
vpn_user = 'givenName' in details or 'sn' in details vpn_user = 'givenName' in details or 'sn' in details
if service and vpn_user: if service and vpn_user:
raise Failure('a service account has no first or last name', 2) raise Failure('a service account has no first or last name', 2)
if service:
details.update({attribute: value for attribute, value in labels(values).items() if value})
if vpn_user and len(details) != len(PERSON): if vpn_user and len(details) != len(PERSON):
raise Failure('give first_name, last_name and email for a VPN user, or at most email for a Manager-only account', 2) raise Failure('give first_name, last_name and email for a VPN user, or at most email for a Manager-only account', 2)
check_password(password) check_password(password)
@@ -277,9 +309,12 @@ def add(connection, name, password, values, service=False):
def update(connection, name, values): def update(connection, name, values):
"""Change the person attributes values gives; the cn follows the first and the last name. """Change the person attributes values gives; the cn follows the first and the last name.
A Manager-only account has only the e-mail address, which '' removes. A Manager-only account has only the e-mail address, which '' removes; a service account also its labels
(update_service).
""" """
current = entry(connection, name) current = entry(connection, name)
if service(current):
return update_service(connection, name, values)
if manager_only(current): if manager_only(current):
if values.get('first_name') is not None or values.get('last_name') is not None: if values.get('first_name') is not None or values.get('last_name') is not None:
raise Failure(f'user {name} is a Manager-only account, which has no first or last name', 2) raise Failure(f'user {name} is a Manager-only account, which has no first or last name', 2)
@@ -303,6 +338,21 @@ def update(connection, name, values):
return summary(entry(connection, name)) return summary(entry(connection, name))
def update_service(connection, name, values):
"""Change any of the e-mail address and the labels (labels) of a service account that values gives; '' removes one."""
if not service(entry(connection, name)):
raise Failure(f'{name} is not a service account', 3)
changes = [(ldap.MOD_REPLACE, attribute, [value.encode()] if value else []) for attribute, value in labels(values).items()]
email = values.get('email')
if email is not None:
address = '' if isinstance(email, str) and email.strip() == '' else person({'email': email}, required=False)['mail']
changes.append((ldap.MOD_REPLACE, 'mail', [address.encode()] if address else []))
if not changes:
raise Failure('give email, display_name or description', 2)
connection.modify_s(dn_of(name), changes)
return summary(entry(connection, name))
def passwd(connection, name, password): def passwd(connection, name, password):
check_password(password) check_password(password)
entry(connection, name) entry(connection, name)
+6 -6
View File
@@ -508,8 +508,8 @@ def service_accounts(method, path, body):
"""Answer a /service-accounts request with vpnrouter_users: the HTTP status and the JSON document. """Answer a /service-accounts request with vpnrouter_users: the HTTP status and the JSON document.
GET /service-accounts the service accounts: {"service_accounts": [...]} GET /service-accounts the service accounts: {"service_accounts": [...]}
POST /service-accounts add one: {"name", "password", "email"?} POST /service-accounts add one: {"name", "password", "display_name"?, "description"?, "email"?}
PUT /service-accounts/<name> change the e-mail address: {"email"}; "" removes it PUT /service-accounts/<name> change any of {"display_name", "description", "email"}; "" removes one
PUT /service-accounts/<name>/password set the password: {"password": ...} PUT /service-accounts/<name>/password set the password: {"password": ...}
POST /service-accounts/<name>/disable lock it out POST /service-accounts/<name>/disable lock it out
POST /service-accounts/<name>/enable let it in again POST /service-accounts/<name>/enable let it in again
@@ -525,13 +525,13 @@ def service_accounts(method, path, body):
parts = path.strip('/').split('/') parts = path.strip('/').split('/')
route = (method, len(parts), parts[2] if len(parts) == 3 else None) route = (method, len(parts), parts[2] if len(parts) == 3 else None)
name = parts[1] if len(parts) > 1 else None name = parts[1] if len(parts) > 1 else None
email = {'email': body.get('email')} settings = {key: body.get(key) for key in ('email', 'display_name', 'description')}
actions = { actions = {
('GET', 1, None): lambda connection: { ('GET', 1, None): lambda connection: {
'service_accounts': [user for user in vpnrouter_users.list_users(connection)['users'] if user['service']]}, 'service_accounts': [user for user in vpnrouter_users.list_users(connection)['users'] if user['service']]},
('POST', 1, None): lambda connection: vpnrouter_users.add(connection, body.get('name'), body.get('password'), email, ('POST', 1, None): lambda connection: vpnrouter_users.add(connection, body.get('name'), body.get('password'), settings,
service=True), service=True),
('PUT', 2, None): lambda connection: vpnrouter_users.update(connection, name, email), ('PUT', 2, None): lambda connection: vpnrouter_users.update_service(connection, name, settings),
('PUT', 3, 'password'): lambda connection: vpnrouter_users.passwd(connection, name, body.get('password')), ('PUT', 3, 'password'): lambda connection: vpnrouter_users.passwd(connection, name, body.get('password')),
('POST', 3, 'disable'): lambda connection: vpnrouter_users.disable(connection, name), ('POST', 3, 'disable'): lambda connection: vpnrouter_users.disable(connection, name),
('POST', 3, 'enable'): lambda connection: vpnrouter_users.enable(connection, name), ('POST', 3, 'enable'): lambda connection: vpnrouter_users.enable(connection, name),
@@ -575,7 +575,7 @@ def groups(state, method, path, body):
route = (method, len(parts), parts[2] if len(parts) > 2 else None) route = (method, len(parts), parts[2] if len(parts) > 2 else None)
members = body.get('members') if route == ('POST', 1, None) else [body.get('user')] if route == ('POST', 3, 'members') \ 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 [] else [parts[3]] if route == ('DELETE', 4, 'members') else []
internal = sorted(vpnrouter_users.internal_accounts().intersection( internal = sorted(set(vpnrouter_users.internal_accounts()).intersection(
member for member in (members if isinstance(members, list) else []) if isinstance(member, str))) member for member in (members if isinstance(members, list) else []) if isinstance(member, str)))
if internal: if internal:
return 403, {'error': f'{", ".join(internal)}: internal service accounts, which the router manages, are not group members'} return 403, {'error': f'{", ".join(internal)}: internal service accounts, which the router manages, are not group members'}
+8 -4
View File
@@ -11,8 +11,10 @@ user exists, 1 anything else. -h or --help shows the commands and their options.
vpn-router-user [--json] list vpn-router-user [--json] list
vpn-router-user [--json] add <name> --first-name F --last-name L --email E (password on stdin) vpn-router-user [--json] add <name> --first-name F --last-name L --email E (password on stdin)
vpn-router-user [--json] add <name> [--email E] (a Manager-only account; password on stdin) vpn-router-user [--json] add <name> [--email E] (a Manager-only account; password on stdin)
vpn-router-user [--json] add <name> --service [--email E] (a service account; password on stdin) vpn-router-user [--json] add <name> --service [--display-name N] [--description D] [--email E]
(a service account; password on stdin)
vpn-router-user [--json] update <name> [--first-name F] [--last-name L] [--email E] vpn-router-user [--json] update <name> [--first-name F] [--last-name L] [--email E]
vpn-router-user [--json] update <name> [--display-name N] [--description D] [--email E] (a service account)
vpn-router-user [--json] passwd <name> (password on stdin) vpn-router-user [--json] passwd <name> (password on stdin)
vpn-router-user [--json] disable <name> vpn-router-user [--json] disable <name>
vpn-router-user [--json] enable <name> vpn-router-user [--json] enable <name>
@@ -57,7 +59,9 @@ HELP = {
} }
#: The help of the person options. #: The help of the person options.
PERSON_HELP = {'first_name': 'the first name', 'last_name': 'the last name', 'email': 'the e-mail address'} PERSON_HELP = {'first_name': 'the first name', 'last_name': 'the last name', 'email': 'the e-mail address',
'display_name': "a service account's display name; '' removes it",
'description': "a service account's description; '' removes it"}
class Parser(argparse.ArgumentParser): class Parser(argparse.ArgumentParser):
@@ -76,7 +80,7 @@ def parse(argv):
if command != 'list': if command != 'list':
sub.add_argument('name', help='the user name') sub.add_argument('name', help='the user name')
if command in ('add', 'update'): if command in ('add', 'update'):
for key, _ in vpnrouter_users.PERSON: for key, _ in vpnrouter_users.PERSON + vpnrouter_users.LABELS:
sub.add_argument(f'--{key.replace("_", "-")}', dest=key, metavar='TEXT', help=PERSON_HELP[key]) sub.add_argument(f'--{key.replace("_", "-")}', dest=key, metavar='TEXT', help=PERSON_HELP[key])
if command == 'add': if command == 'add':
sub.add_argument('--service', action='store_true', help='add a service account, which cannot sign in to the web service') sub.add_argument('--service', action='store_true', help='add a service account, which cannot sign in to the web service')
@@ -95,7 +99,7 @@ def run(connection, options, secret):
command = options.command command = options.command
if command == 'list': if command == 'list':
return vpnrouter_users.list_users(connection) return vpnrouter_users.list_users(connection)
values = {key: getattr(options, key, None) for key, _ in vpnrouter_users.PERSON} values = {key: getattr(options, key, None) for key, _ in vpnrouter_users.PERSON + vpnrouter_users.LABELS}
if command == 'add': if command == 'add':
return vpnrouter_users.add(connection, options.name, secret, values, service=options.service) return vpnrouter_users.add(connection, options.name, secret, values, service=options.service)
if command == 'update': if command == 'update':