fix: service accounts are managed at /service-accounts, not /users, and the internal one of the metrics service is only listed.
Release / release (push) Successful in 12s
Release / release (push) Successful in 12s
This commit is contained in:
1 parent
d1970b77b7
commit
f01be41191
4 files changed
+143
-34
No files matched your search
@@ -568,22 +568,27 @@ API or the Manager, and its password should be checked and locked out like any o
|
|||||||
belongs to the package's metrics, not to the people using the router.
|
belongs to the package's metrics, not to the people using the router.
|
||||||
|
|
||||||
Decision: A service account is a Manager-only account (ADR-18) with `vpnRouterScope` `service`.
|
Decision: A service account is a Manager-only account (ADR-18) with `vpnRouterScope` `service`.
|
||||||
Administrators add service accounts with `vpn-router-user add --service` and `/users`, for
|
Administrators add service accounts with `vpn-router-user add --service` and the web service's
|
||||||
services of their own that check passwords against the directory, and manage them like other
|
`/service-accounts`, for services of their own that check passwords against the directory.
|
||||||
accounts. The metrics service's is the internal one, created by `vpn-router-setup`: while metrics
|
`/service-accounts` is a path of its own, apart from `/users`, which does not list or serve them:
|
||||||
are enabled it adds the account `metrics.service_account` names, `metrics` by default, with a
|
a service account has no names, certificate, profile or TOTP, and is not a person an
|
||||||
random password it writes to `/etc/vpn-router/metrics.secret` (root only), and sets a new random
|
administrator manages with the users. The metrics service's is the internal one, created by
|
||||||
password, ending a lockout, when the file is missing or no longer holds the password. The answers
|
`vpn-router-setup`: while metrics are enabled it adds the account `metrics.service_account` names,
|
||||||
of `/users` mark it as `internal`, and `/users` refuses to set its password, which setup would
|
`metrics` by default, with a random password it writes to `/etc/vpn-router/metrics.secret` (root
|
||||||
replace. `/auth` refuses every service account with the message of a wrong password. `/metrics` accepts HTTP Basic only for that account, and
|
only), and sets a new random password, ending a lockout, when the file is missing or no longer
|
||||||
|
holds the password. `/service-accounts` marks it as `internal` and only lists it: every other
|
||||||
|
request on it, and putting it in or taking it out of a group at `/groups`, gets 403, whatever the
|
||||||
|
token. Setup
|
||||||
|
changes it through the directory's user-manager account, not the API, and root may still with
|
||||||
|
`vpn-router-user`. `/auth` refuses every service account with the message of a wrong password. `/metrics` accepts HTTP Basic only for that account, and
|
||||||
only when it is a service account; the password is checked with a bind as the account, so the
|
only when it is a service account; the password is checked with a bind as the account, so the
|
||||||
password policy's lockout applies (ADR-14). TLS is Caddy's, on `metrics.https_port` (ADR-21);
|
password policy's lockout applies (ADR-14). TLS is Caddy's, on `metrics.https_port` (ADR-21);
|
||||||
plain HTTP is served on the interfaces `metrics.listen` names, `internal` by default.
|
plain HTTP is served on the interfaces `metrics.listen` names, `internal` by default.
|
||||||
|
|
||||||
Consequences: Every scrape binds to slapd once. The collector's operator copies the password from
|
Consequences: Every scrape binds to slapd once. The collector's operator copies the password from
|
||||||
the router; deleting the file and running setup changes it. A user who already has the name is not
|
the router; deleting the file and running setup changes it. A user who already has the name is not
|
||||||
taken over; setup fails until `metrics.service_account` names another. A disabled account stays
|
taken over; setup fails until `metrics.service_account` names another. An account root disabled
|
||||||
disabled. A scraper on the external interface over plain HTTP sends the password in the clear, so
|
stays disabled. A scraper on the external interface over plain HTTP sends the password in the clear, so
|
||||||
`metrics.listen` stays `internal` unless HTTPS is used.
|
`metrics.listen` stays `internal` unless HTTPS is used.
|
||||||
|
|
||||||
## ADR-24: The Router Manager upgrades the router's packages in a transient unit outside its own
|
## ADR-24: The Router Manager upgrades the router's packages in a transient unit outside its own
|
||||||
|
|||||||
@@ -428,10 +428,11 @@ it is added:
|
|||||||
certificate, so no VPN access. It can sign in to `/auth` and, as a member of `admins`, manage
|
certificate, so no VPN access. It can sign in to `/auth` and, as a member of `admins`, manage
|
||||||
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`, for a service
|
- a service account, a Manager-only account whose `vpnRouterScope` is `service`, for a service
|
||||||
that checks passwords against the directory; it cannot sign in to `/auth`. `add --service` and
|
that checks passwords against the directory; it cannot sign in to `/auth`. `add --service` adds
|
||||||
`POST /users` with `"service": true` add one. The internal one is the account
|
one, and the web service manages them at `/service-accounts`, not at `/users`. The internal one
|
||||||
`metrics.service_account` names while metrics are enabled: `vpn-router-setup` adds it and keeps
|
is the account `metrics.service_account` names while metrics are enabled: `vpn-router-setup` adds
|
||||||
its password (see Metrics), so `/users` refuses to set its password.
|
it and keeps its password (see Metrics). The API only lists it: `/service-accounts` refuses every
|
||||||
|
other request on it, and `/groups` refuses to put it in or take it out of a group, with 403.
|
||||||
|
|
||||||
Passwords and TOTP secrets are read from the first line of standard input. The answer is text: a
|
Passwords and TOTP secrets are read from the first line of standard input. The answer is text: a
|
||||||
table of the users, the client profile, a TOTP secret or a short message, and a failure is a
|
table of the users, the client profile, a TOTP secret or a short message, and a failure is a
|
||||||
@@ -610,6 +611,7 @@ curl http://10.0.0.4:17081/
|
|||||||
| `/auth` | Signing in with a user's password and TOTP code, refreshing and signing out |
|
| `/auth` | Signing in with a user's password and TOTP code, refreshing and signing out |
|
||||||
| `/config` | The configuration without its secret options, and what the router resolved and runs, as JSON; needs a token |
|
| `/config` | The configuration without its secret options, and what the router resolved and runs, as JSON; needs a token |
|
||||||
| `/users` | The users of OpenVPN and road-warrior access; needs a token |
|
| `/users` | The users of OpenVPN and road-warrior access; needs a token |
|
||||||
|
| `/service-accounts` | The service accounts; needs a token |
|
||||||
| `/groups` | The groups of users; needs a token |
|
| `/groups` | The groups of users; needs a token |
|
||||||
| `/site-to-site` | The site-to-site connections: list, add, change and remove; needs a token |
|
| `/site-to-site` | The site-to-site connections: list, add, change and remove; needs a token |
|
||||||
| `/account` | The account of the signed-in user: its details, its password and its TOTP enrolment; needs a user's token |
|
| `/account` | The account of the signed-in user: its details, its password and its TOTP enrolment; needs a user's token |
|
||||||
@@ -634,7 +636,7 @@ curl http://10.0.0.4:7081/status
|
|||||||
|
|
||||||
### Tokens and signing in
|
### Tokens and signing in
|
||||||
|
|
||||||
`/config`, `/users`, `/groups`, `/site-to-site` and `/account` need a JWT in an `Authorization: Bearer` header, signed with
|
`/config`, `/users`, `/service-accounts`, `/groups`, `/site-to-site` and `/account` need a JWT in an `Authorization: Bearer` header, signed with
|
||||||
HS256 by the key the web service makes on its first run, `/etc/vpn-router/web.key` (root only).
|
HS256 by the key the web service makes on its first run, `/etc/vpn-router/web.key` (root only).
|
||||||
The key never leaves the router; while it does not exist these paths and `/auth` are not served
|
The key never leaves the router; while it does not exist these paths and `/auth` are not served
|
||||||
(404). A missing, expired or wrongly signed token, or one of another algorithm, gets 401 with
|
(404). A missing, expired or wrongly signed token, or one of another algorithm, gets 401 with
|
||||||
@@ -717,9 +719,9 @@ and answer bodies are JSON; a request body is at most 4096 bytes.
|
|||||||
| Request | Body | Answer |
|
| Request | Body | Answer |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `GET /users` | | `{"users": [{"name", "manager_only", "service", "internal", "enabled", "locked", "certificate", "otp", "first_name", "last_name", "email"}, ...]}` |
|
| `GET /users` | | `{"users": [{"name", "manager_only", "service", "internal", "enabled", "locked", "certificate", "otp", "first_name", "last_name", "email"}, ...]}` |
|
||||||
| `POST /users` | `{"name", "password", "first_name", "last_name", "email"}` for a VPN user; `{"name", "password", "email"?}` for a Manager-only account; `{"name", "password", "service": true, "email"?}` for a service account | 201 and the user |
|
| `POST /users` | `{"name", "password", "first_name", "last_name", "email"}` for a VPN user; `{"name", "password", "email"?}` for a Manager-only account | 201 and the user |
|
||||||
| `PUT /users/<name>` | any of `{"first_name", "last_name", "email"}`; for a Manager-only account only `{"email"}`, `""` removing it | the user |
|
| `PUT /users/<name>` | any of `{"first_name", "last_name", "email"}`; for a Manager-only account only `{"email"}`, `""` removing it | the user |
|
||||||
| `PUT /users/<name>/password` | `{"password": ...}`; refused with 400 for the internal service account | the user |
|
| `PUT /users/<name>/password` | `{"password": ...}` | the user |
|
||||||
| `POST /users/<name>/disable` | | the user |
|
| `POST /users/<name>/disable` | | the user |
|
||||||
| `POST /users/<name>/enable` | | the user |
|
| `POST /users/<name>/enable` | | the user |
|
||||||
| `POST /users/<name>/unlock` | | the user |
|
| `POST /users/<name>/unlock` | | the user |
|
||||||
@@ -730,7 +732,8 @@ and answer bodies are JSON; a request body is at most 4096 bytes.
|
|||||||
| `DELETE /users/<name>/otp` | | the user |
|
| `DELETE /users/<name>/otp` | | the user |
|
||||||
|
|
||||||
A failure is `{"error": ...}` with 400 for an invalid request, 404 for a user that does not
|
A failure is `{"error": ...}` with 400 for an invalid request, 404 for a user that does not
|
||||||
exist, 409 for a user that exists and 500 otherwise. `/users` works whether or not OpenVPN or
|
exist or is a service account, 409 for a user that exists and 500 otherwise. `/users` does not
|
||||||
|
list service accounts; `/service-accounts` serves them. `/users` works whether or not OpenVPN or
|
||||||
road-warrior access is enabled; the profile needs OpenVPN. Passwords, private keys and TOTP
|
road-warrior access is enabled; the profile needs OpenVPN. Passwords, private keys and TOTP
|
||||||
secrets cross the internal network in plain HTTP, like the token.
|
secrets cross the internal network in plain HTTP, like the token.
|
||||||
|
|
||||||
@@ -740,6 +743,26 @@ curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
|||||||
http://10.0.0.4:7081/users
|
http://10.0.0.4:7081/users
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### The /service-accounts API
|
||||||
|
|
||||||
|
`/service-accounts` manages the service accounts with the same token and the same module as
|
||||||
|
`/users`.
|
||||||
|
|
||||||
|
| Request | Body | Answer |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `GET /service-accounts` | | `{"service_accounts": [{"name", "internal", "enabled", "locked", "email", ...}, ...]}`, each as `/users` shows a user |
|
||||||
|
| `POST /service-accounts` | `{"name", "password", "email"?}` | 201 and the account |
|
||||||
|
| `PUT /service-accounts/<name>` | `{"email"}`; `""` removes it | the account |
|
||||||
|
| `PUT /service-accounts/<name>/password` | `{"password": ...}` | the account |
|
||||||
|
| `POST /service-accounts/<name>/disable` | | the account |
|
||||||
|
| `POST /service-accounts/<name>/enable` | | the account |
|
||||||
|
| `POST /service-accounts/<name>/unlock` | | the account |
|
||||||
|
| `DELETE /service-accounts/<name>` | | `{"name", "removed": true}` |
|
||||||
|
|
||||||
|
The internal account, `internal` true, is only listed: every other request on it gets 403, as
|
||||||
|
`vpn-router-setup` adds and keeps it. 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
|
||||||
|
|
||||||
`/account` serves the user the access token names, whether or not the user is a member of
|
`/account` serves the user the access token names, whether or not the user is a member of
|
||||||
|
|||||||
@@ -1,3 +1,20 @@
|
|||||||
|
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.
|
||||||
|
|
||||||
|
* /service-accounts, for members of admins and admin tokens, lists the
|
||||||
|
service accounts, adds one with a password and an optional e-mail
|
||||||
|
address, changes the e-mail address, sets the password, disables,
|
||||||
|
enables, unlocks and removes them.
|
||||||
|
* /users no longer lists service accounts or takes "service" on POST,
|
||||||
|
and answers 404 to a request naming one.
|
||||||
|
* /service-accounts answers 403 to every request but the list on the
|
||||||
|
account metrics.service_account names while metrics are enabled, and
|
||||||
|
/groups to putting it in or taking it out of a group;
|
||||||
|
vpn-router-setup and root's vpn-router-user still change it.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Mon, 26 Oct 2026 18:00:00 +0100
|
||||||
|
|
||||||
vpn-router (0.18.1-1) unstable; urgency=medium
|
vpn-router (0.18.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
fix: vpn-router-setup owns only the creation of the metrics service's account, which is marked internal, and administrators add service accounts with vpn-router-user add --service and /users.
|
fix: vpn-router-setup owns only the creation of the metrics service's account, which is marked internal, and administrators add service accounts with vpn-router-user add --service and /users.
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ Pages:
|
|||||||
runs, as JSON
|
runs, as JSON
|
||||||
/users the users in the router's directory, whether or not a P2S system is enabled: list,
|
/users the users in the router's directory, whether or not a P2S system is enabled: list,
|
||||||
add, change, set the password, disable, enable, remove, the OpenVPN client
|
add, change, set the password, disable, enable, remove, the OpenVPN client
|
||||||
profile, and TOTP enrolment (see users())
|
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
|
||||||
|
service_accounts())
|
||||||
/groups the groups of the router's directory: list, add, rename or describe, remove, and
|
/groups the groups of the router's directory: list, add, rename or describe, remove, and
|
||||||
add and take out members (see groups())
|
add and take out members (see groups())
|
||||||
/account the account of the signed-in user: its details, its password and its TOTP
|
/account the account of the signed-in user: its details, its password and its TOTP
|
||||||
@@ -28,7 +32,7 @@ Pages:
|
|||||||
the databases of each PostgreSQL cluster on the router with their sizes, as JSON (see
|
the databases of each PostgreSQL cluster on the router with their sizes, as JSON (see
|
||||||
postgresql_databases()); /config has the clusters' settings
|
postgresql_databases()); /config has the clusters' settings
|
||||||
|
|
||||||
/config, /metrics/history, /postgresql, /users, /groups, /site-to-site and /account need an HS256 JWT in an Authorization: Bearer header signed
|
/config, /metrics/history, /postgresql, /users, /service-accounts, /groups, /site-to-site and /account need an HS256 JWT in an Authorization: Bearer header signed
|
||||||
with vpnrouter_auth.WEB_KEY (see vpnrouter_auth.verify). A token of /auth names a user in sub;
|
with vpnrouter_auth.WEB_KEY (see vpnrouter_auth.verify). A token of /auth names a user in sub;
|
||||||
/account serves that user, and the other paths need a member of vpnrouter_users.ADMIN_GROUP. A
|
/account serves that user, and the other paths need a member of vpnrouter_users.ADMIN_GROUP. A
|
||||||
token of vpn-router-token has the admin claim and names no user; it may call every path but
|
token of vpn-router-token has the admin claim and names no user; it may call every path but
|
||||||
@@ -441,11 +445,9 @@ def users(state, method, path, body):
|
|||||||
|
|
||||||
GET /users the users
|
GET /users the users
|
||||||
POST /users add a user: {"name", "password", "first_name", "last_name", "email"};
|
POST /users add a user: {"name", "password", "first_name", "last_name", "email"};
|
||||||
without the names a Manager-only account, and with "service": true a
|
without the names a Manager-only account
|
||||||
service account
|
|
||||||
PUT /users/<name> change any of {"first_name", "last_name", "email"}
|
PUT /users/<name> change any of {"first_name", "last_name", "email"}
|
||||||
PUT /users/<name>/password set the password: {"password": ...}; refused for the internal service
|
PUT /users/<name>/password set the password: {"password": ...}
|
||||||
account, whose password vpn-router-setup keeps
|
|
||||||
POST /users/<name>/disable lock the user out of OpenVPN
|
POST /users/<name>/disable lock the user out of OpenVPN
|
||||||
POST /users/<name>/enable let the user in again
|
POST /users/<name>/enable let the user in again
|
||||||
POST /users/<name>/unlock end a lockout after failed logins
|
POST /users/<name>/unlock end a lockout after failed logins
|
||||||
@@ -454,6 +456,8 @@ def users(state, method, path, body):
|
|||||||
POST /users/<name>/otp a new TOTP secret and its otpauth URI; nothing is stored
|
POST /users/<name>/otp a new TOTP secret and its otpauth URI; nothing is stored
|
||||||
PUT /users/<name>/otp store a TOTP secret: {"secret", "code"}, code a code of the secret
|
PUT /users/<name>/otp store a TOTP secret: {"secret", "code"}, code a code of the secret
|
||||||
DELETE /users/<name>/otp remove the TOTP token
|
DELETE /users/<name>/otp remove the TOTP token
|
||||||
|
|
||||||
|
Service accounts are served by /service-accounts: they are not listed, and a request naming one is 404.
|
||||||
"""
|
"""
|
||||||
if body == 'invalid' or (body is not None and not isinstance(body, dict)):
|
if body == 'invalid' or (body is not None and not isinstance(body, dict)):
|
||||||
return 400, {'error': 'the body must be a JSON object'}
|
return 400, {'error': 'the body must be a JSON object'}
|
||||||
@@ -462,9 +466,9 @@ def users(state, method, path, body):
|
|||||||
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
|
||||||
actions = {
|
actions = {
|
||||||
('GET', 1, None): lambda connection: vpnrouter_users.list_users(connection),
|
('GET', 1, None): lambda connection: {
|
||||||
('POST', 1, None): lambda connection: vpnrouter_users.add(connection, body.get('name'), body.get('password'), body,
|
'users': [user for user in vpnrouter_users.list_users(connection)['users'] if not user['service']]},
|
||||||
service=body.get('service') is True),
|
('POST', 1, None): lambda connection: vpnrouter_users.add(connection, body.get('name'), body.get('password'), body),
|
||||||
('PUT', 2, None): lambda connection: vpnrouter_users.update(connection, name, body),
|
('PUT', 2, None): lambda connection: vpnrouter_users.update(connection, name, body),
|
||||||
('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),
|
||||||
@@ -481,12 +485,10 @@ def users(state, method, path, body):
|
|||||||
('DELETE', 2, None): 'remove'}
|
('DELETE', 2, None): 'remove'}
|
||||||
if route not in actions:
|
if route not in actions:
|
||||||
return 404, {'error': f'no route {method} {path}'}
|
return 404, {'error': f'no route {method} {path}'}
|
||||||
if route == ('POST', 1, None) and not isinstance(body.get('service', False), bool):
|
|
||||||
return 400, {'error': 'service must be true or false'}
|
|
||||||
if route == ('PUT', 3, 'password') and name == vpnrouter_users.metrics_account():
|
|
||||||
return 400, {'error': f'{name} is the internal service account of the metrics service; vpn-router-setup keeps its password'}
|
|
||||||
try:
|
try:
|
||||||
with vpnrouter_users.session() as connection:
|
with vpnrouter_users.session() as connection:
|
||||||
|
if name is not None and vpnrouter_users.service(vpnrouter_users.entry(connection, name)):
|
||||||
|
raise vpnrouter_users.Failure(f'{name} is a service account; see /service-accounts', 3)
|
||||||
document = actions[route](connection)
|
document = actions[route](connection)
|
||||||
if operations.get(route) in vpnrouter_auth.REVOKING:
|
if operations.get(route) in vpnrouter_auth.REVOKING:
|
||||||
vpnrouter_auth.revoke(vpnrouter.load_config(), name)
|
vpnrouter_auth.revoke(vpnrouter.load_config(), name)
|
||||||
@@ -502,6 +504,56 @@ def users(state, method, path, body):
|
|||||||
return (201 if route == ('POST', 1, None) else 200), document
|
return (201 if route == ('POST', 1, None) else 200), document
|
||||||
|
|
||||||
|
|
||||||
|
def service_accounts(method, path, body):
|
||||||
|
"""Answer a /service-accounts request with vpnrouter_users: the HTTP status and the JSON document.
|
||||||
|
|
||||||
|
GET /service-accounts the service accounts: {"service_accounts": [...]}
|
||||||
|
POST /service-accounts add one: {"name", "password", "email"?}
|
||||||
|
PUT /service-accounts/<name> change the e-mail address: {"email"}; "" removes it
|
||||||
|
PUT /service-accounts/<name>/password set the password: {"password": ...}
|
||||||
|
POST /service-accounts/<name>/disable lock it out
|
||||||
|
POST /service-accounts/<name>/enable let it in again
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
if body == 'invalid' or (body is not None and not isinstance(body, dict)):
|
||||||
|
return 400, {'error': 'the body must be a JSON object'}
|
||||||
|
body = body or {}
|
||||||
|
parts = path.strip('/').split('/')
|
||||||
|
route = (method, len(parts), parts[2] if len(parts) == 3 else None)
|
||||||
|
name = parts[1] if len(parts) > 1 else None
|
||||||
|
email = {'email': body.get('email')}
|
||||||
|
actions = {
|
||||||
|
('GET', 1, None): lambda connection: {
|
||||||
|
'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,
|
||||||
|
service=True),
|
||||||
|
('PUT', 2, None): lambda connection: vpnrouter_users.update(connection, name, email),
|
||||||
|
('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, 'enable'): lambda connection: vpnrouter_users.enable(connection, name),
|
||||||
|
('POST', 3, 'unlock'): lambda connection: vpnrouter_users.unlock(connection, name),
|
||||||
|
('DELETE', 2, None): lambda connection: vpnrouter_users.remove(connection, name),
|
||||||
|
}
|
||||||
|
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'}
|
||||||
|
try:
|
||||||
|
with vpnrouter_users.session() as connection:
|
||||||
|
if name is not None and not vpnrouter_users.service(vpnrouter_users.entry(connection, name)):
|
||||||
|
raise vpnrouter_users.Failure(f'{name} is not a service account', 3)
|
||||||
|
document = actions[route](connection)
|
||||||
|
except vpnrouter_users.Failure as failure:
|
||||||
|
return USER_STATUS.get(failure.status, 500), {'error': str(failure)}
|
||||||
|
except (ldap.LDAPError, OSError) as exc:
|
||||||
|
return 500, {'error': f'the user directory failed: {exc}'}
|
||||||
|
return (201 if route == ('POST', 1, None) else 200), document
|
||||||
|
|
||||||
|
|
||||||
def groups(state, method, path, body):
|
def groups(state, method, path, body):
|
||||||
"""Answer a /groups request with vpnrouter_users: the HTTP status and the JSON document.
|
"""Answer a /groups request with vpnrouter_users: the HTTP status and the JSON document.
|
||||||
|
|
||||||
@@ -511,6 +563,9 @@ def groups(state, method, path, body):
|
|||||||
DELETE /groups/<name> remove the group; its members are kept
|
DELETE /groups/<name> remove the group; its members are kept
|
||||||
POST /groups/<name>/members add a member: {"user": ...}
|
POST /groups/<name>/members add a member: {"user": ...}
|
||||||
DELETE /groups/<name>/members/<user> take a member out; the last member cannot leave
|
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
|
||||||
|
request is refused with 403.
|
||||||
"""
|
"""
|
||||||
if body == 'invalid' or (body is not None and not isinstance(body, dict)):
|
if body == 'invalid' or (body is not None and not isinstance(body, dict)):
|
||||||
return 400, {'error': 'the body must be a JSON object'}
|
return 400, {'error': 'the body must be a JSON object'}
|
||||||
@@ -518,6 +573,11 @@ def groups(state, method, path, body):
|
|||||||
parts = path.strip('/').split('/')
|
parts = path.strip('/').split('/')
|
||||||
name = parts[1] if len(parts) > 1 else None
|
name = parts[1] if len(parts) > 1 else None
|
||||||
route = (method, len(parts), parts[2] if len(parts) > 2 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'}
|
||||||
actions = {
|
actions = {
|
||||||
('GET', 1, None): lambda connection: vpnrouter_users.list_groups(connection),
|
('GET', 1, None): lambda connection: vpnrouter_users.list_groups(connection),
|
||||||
('POST', 1, None): lambda connection: vpnrouter_users.group_add(
|
('POST', 1, None): lambda connection: vpnrouter_users.group_add(
|
||||||
@@ -680,7 +740,8 @@ def metrics_history(view):
|
|||||||
|
|
||||||
|
|
||||||
def changes_admins(connection, method, parts, body):
|
def changes_admins(connection, method, parts, body):
|
||||||
"""Whether a /users or /groups request creates, renames to, changes or removes ADMIN_GROUP, or removes a member of it."""
|
"""Whether a /users, /service-accounts or /groups request creates, renames to, changes or removes ADMIN_GROUP, or
|
||||||
|
removes a member of it."""
|
||||||
admins = vpnrouter_users.ADMIN_GROUP
|
admins = vpnrouter_users.ADMIN_GROUP
|
||||||
if parts[0] == 'groups':
|
if parts[0] == 'groups':
|
||||||
if method == 'POST' and len(parts) == 1:
|
if method == 'POST' and len(parts) == 1:
|
||||||
@@ -688,7 +749,7 @@ def changes_admins(connection, method, parts, body):
|
|||||||
if method != 'GET' and len(parts) > 1 and parts[1] == admins:
|
if method != 'GET' and len(parts) > 1 and parts[1] == admins:
|
||||||
return True
|
return True
|
||||||
return method == 'PUT' and len(parts) == 2 and body.get('name') == admins
|
return method == 'PUT' and len(parts) == 2 and body.get('name') == admins
|
||||||
if parts[0] == 'users' and method == 'DELETE' and len(parts) == 2:
|
if parts[0] in ('users', 'service-accounts') and method == 'DELETE' and len(parts) == 2:
|
||||||
return admins in vpnrouter_users.groups_of(connection, parts[1])
|
return admins in vpnrouter_users.groups_of(connection, parts[1])
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -802,7 +863,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
# A body that is not read must not be taken for the next request.
|
# A body that is not read must not be taken for the next request.
|
||||||
if self.command not in ('GET', 'HEAD'):
|
if self.command not in ('GET', 'HEAD'):
|
||||||
self.close_connection = True
|
self.close_connection = True
|
||||||
directory = next((name for name in ('users', 'groups', 'account', 'site-to-site')
|
directory = next((name for name in ('users', 'service-accounts', 'groups', 'account', 'site-to-site')
|
||||||
if path == f'/{name}' or path.startswith(f'/{name}/')), None)
|
if path == f'/{name}' or path.startswith(f'/{name}/')), None)
|
||||||
protected = path in ('/config', '/metrics/history', '/postgresql') or directory is not None
|
protected = path in ('/config', '/metrics/history', '/postgresql') or directory is not None
|
||||||
signing = path in ('/auth', '/auth/refresh', '/auth/logout')
|
signing = path in ('/auth', '/auth/refresh', '/auth/logout')
|
||||||
@@ -843,6 +904,9 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
elif directory == 'site-to-site':
|
elif directory == 'site-to-site':
|
||||||
code, document = site_to_site(self.command, path, payload)
|
code, document = site_to_site(self.command, path, payload)
|
||||||
body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json'
|
body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json'
|
||||||
|
elif directory == 'service-accounts':
|
||||||
|
code, document = service_accounts(self.command, path, payload)
|
||||||
|
body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json'
|
||||||
elif directory is not None:
|
elif directory is not None:
|
||||||
code, document = (users if directory == 'users' else groups)(state, self.command, path, payload)
|
code, document = (users if directory == 'users' else groups)(state, self.command, path, payload)
|
||||||
body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json'
|
body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json'
|
||||||
|
|||||||
Reference in new issue
Block a user