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.
Release / release (push) Successful in 32s
Release / release (push) Successful in 32s
This commit is contained in:
1 parent
bf651da5a6
commit
d1970b77b7
6 files changed
+73
-26
No files matched your search
@@ -558,7 +558,7 @@ purge. A view shows only what its level keeps, so a shorter retention empties th
|
|||||||
view. A month view needs the hourly rows and a year view the monthly rows. `/metrics` has only the
|
view. A month view needs the hourly rows and a year view the monthly rows. `/metrics` has only the
|
||||||
last sample; history is not scraped.
|
last sample; history is not scraped.
|
||||||
|
|
||||||
## ADR-23: /metrics takes HTTP Basic authentication of a service account that setup owns
|
## ADR-23: /metrics takes HTTP Basic authentication of an internal service account that setup creates
|
||||||
|
|
||||||
Status: Accepted
|
Status: Accepted
|
||||||
|
|
||||||
@@ -568,11 +568,14 @@ 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`.
|
||||||
`vpn-router-setup` owns it: while metrics are enabled it adds the account `metrics.service_account`
|
Administrators add service accounts with `vpn-router-user add --service` and `/users`, for
|
||||||
names, `metrics` by default, with a random password it writes to `/etc/vpn-router/metrics.secret`
|
services of their own that check passwords against the directory, and manage them like other
|
||||||
(root only), and sets a new random password, ending a lockout, when the file is missing or no
|
accounts. The metrics service's is the internal one, created by `vpn-router-setup`: while metrics
|
||||||
longer holds the password. `vpn-router-user` and `/users` cannot add one. `/auth` refuses it
|
are enabled it adds the account `metrics.service_account` names, `metrics` by default, with a
|
||||||
with the message of a wrong password. `/metrics` accepts HTTP Basic only for that account, and
|
random password it writes to `/etc/vpn-router/metrics.secret` (root only), and sets a new random
|
||||||
|
password, ending a lockout, when the file is missing or no longer holds the password. The answers
|
||||||
|
of `/users` mark it as `internal`, and `/users` refuses to set its password, which setup would
|
||||||
|
replace. `/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.
|
||||||
|
|||||||
@@ -427,9 +427,11 @@ it is added:
|
|||||||
name, the password and an optional e-mail address, for notifications: no names and no
|
name, the password and an optional e-mail address, for notifications: no names and no
|
||||||
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`, which
|
- a service account, a Manager-only account whose `vpnRouterScope` is `service`, for a service
|
||||||
`vpn-router-setup` adds and keeps for the metrics service (see Metrics); it cannot sign in to
|
that checks passwords against the directory; it cannot sign in to `/auth`. `add --service` and
|
||||||
`/auth`, and `vpn-router-user` and `/users` cannot add one.
|
`POST /users` with `"service": true` add one. The internal one is the account
|
||||||
|
`metrics.service_account` names while metrics are enabled: `vpn-router-setup` adds it and keeps
|
||||||
|
its password (see Metrics), so `/users` refuses to set its password.
|
||||||
|
|
||||||
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
|
||||||
@@ -441,6 +443,7 @@ after a command its options.
|
|||||||
```sh
|
```sh
|
||||||
echo 's3cr3t-pass' | vpn-router-user add alice --first-name Alice --last-name Smith --email alice@example.com
|
echo 's3cr3t-pass' | vpn-router-user add alice --first-name Alice --last-name Smith --email alice@example.com
|
||||||
echo 'm4nager-pass' | vpn-router-user add ops
|
echo 'm4nager-pass' | vpn-router-user add ops
|
||||||
|
echo 's3rvice-pass' | vpn-router-user add backup --service
|
||||||
vpn-router-user update alice --email a.smith@example.com
|
vpn-router-user update alice --email a.smith@example.com
|
||||||
vpn-router-user list
|
vpn-router-user list
|
||||||
vpn-router-user profile alice > alice.ovpn
|
vpn-router-user profile alice > alice.ovpn
|
||||||
@@ -460,6 +463,7 @@ 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 |
|
||||||
| `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 |
|
||||||
| `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 |
|
||||||
@@ -474,8 +478,8 @@ A user name is 1 to 32 lowercase letters, digits and hyphens, starting with a le
|
|||||||
password has 8 to 128 characters; a first or last name has 1 to 64 printable characters; an
|
password has 8 to 128 characters; a first or last name has 1 to 64 printable characters; an
|
||||||
e-mail address has one `@`, no spaces and a dot in its domain; a TOTP secret is 32 base32
|
e-mail address has one `@`, no spaces and a dot in its domain; a TOTP secret is 32 base32
|
||||||
characters, as `otp-new` makes it, and a code 6 digits. Once enrolled, a user logs in to OpenVPN
|
characters, as `otp-new` makes it, and a code 6 digits. Once enrolled, a user logs in to OpenVPN
|
||||||
and `/auth` with the password followed by a current code. In the answers `manager_only` tells the
|
and `/auth` with the password followed by a current code. In the answers `manager_only`, `service`
|
||||||
kind, and a Manager-only account has its names as `null`; `update` changes only its e-mail
|
and `internal` tell the kind, and a Manager-only account has its names as `null`; `update` changes only its e-mail
|
||||||
address, and `--email ''` removes it. Changing a user's password, disabling, unlocking or removing the user also revokes its
|
address, and `--email ''` removes it. Changing a user's password, disabling, unlocking or removing the user also revokes its
|
||||||
refresh tokens of `/auth`. The exit status is 0 on success, 2 for an invalid request, 3 when the
|
refresh tokens of `/auth`. The exit status is 0 on success, 2 for an invalid request, 3 when the
|
||||||
user does not exist, 4 when the user exists and 1 for any other failure.
|
user does not exist, 4 when the user exists and 1 for any other failure.
|
||||||
@@ -712,10 +716,10 @@ 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", "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 | 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; `{"name", "password", "service": true, "email"?}` for a service 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": ...}` | the user |
|
| `PUT /users/<name>/password` | `{"password": ...}`; refused with 400 for the internal service account | 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 |
|
||||||
|
|||||||
@@ -1,3 +1,17 @@
|
|||||||
|
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.
|
||||||
|
|
||||||
|
* POST /users takes "service": true and adds a service account, which
|
||||||
|
cannot sign in to /auth; vpn-router-user add takes --service.
|
||||||
|
* The users of /users and vpn-router-user have internal, true for the
|
||||||
|
account metrics.service_account names while metrics are enabled;
|
||||||
|
vpn-router-user list shows its kind as internal.
|
||||||
|
* PUT /users/<name>/password refuses the internal service account,
|
||||||
|
whose password vpn-router-setup keeps.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Mon, 26 Oct 2026 17:00:00 +0100
|
||||||
|
|
||||||
vpn-router (0.18.0-2) unstable; urgency=medium
|
vpn-router (0.18.0-2) unstable; urgency=medium
|
||||||
|
|
||||||
update: release.py tags a Debian revision other than 1 as x.y.z-n.
|
update: release.py tags a Debian revision other than 1 as x.y.z-n.
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ 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
|
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
|
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
|
account is a Manager-only account with the scope SERVICE_SCOPE in vpnRouterScope, which cannot
|
||||||
vpn-router-setup adds and keeps for the metrics service, as metrics.service_account names it; it
|
sign in to the web service; an administrator adds one for a service that checks passwords
|
||||||
cannot sign in to the web service. Users exist whether or not a P2S system is enabled.
|
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).
|
||||||
|
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
|
||||||
optional description and at least one member; a group cannot be empty, so the last member
|
optional description and at least one member; a group cannot be empty, so the last member
|
||||||
@@ -192,14 +194,27 @@ def service(attributes):
|
|||||||
return SERVICE_SCOPE.encode() in (value.lower() for value in attributes.get('vpnRouterScope', []))
|
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."""
|
||||||
|
try:
|
||||||
|
metrics = vpnrouter.load_config()['metrics']
|
||||||
|
except SystemExit:
|
||||||
|
return None
|
||||||
|
return metrics['service_account'] if metrics['enabled'] else None
|
||||||
|
|
||||||
|
|
||||||
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 the entry does not have is None.
|
||||||
|
|
||||||
|
internal is whether it is the internal service account (metrics_account).
|
||||||
|
"""
|
||||||
name = attributes['uid'][0].decode()
|
name = attributes['uid'][0].decode()
|
||||||
lock = attributes.get('pwdAccountLockedTime', [None])[0]
|
lock = attributes.get('pwdAccountLockedTime', [None])[0]
|
||||||
user = {
|
user = {
|
||||||
'name': name,
|
'name': name,
|
||||||
'manager_only': manager_only(attributes),
|
'manager_only': manager_only(attributes),
|
||||||
'service': service(attributes),
|
'service': service(attributes),
|
||||||
|
'internal': service(attributes) and name == metrics_account(),
|
||||||
'enabled': lock != LOCKED,
|
'enabled': lock != LOCKED,
|
||||||
'locked': lock not in (None, LOCKED) and locked_since(lock) > time.time() - vpnrouter_ldap.LOCKOUT_SECONDS,
|
'locked': lock not in (None, LOCKED) and locked_since(lock) > time.time() - vpnrouter_ldap.LOCKOUT_SECONDS,
|
||||||
'certificate': certificate(name)[0].is_file(),
|
'certificate': certificate(name)[0].is_file(),
|
||||||
@@ -225,8 +240,8 @@ 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, which only vpn-router-setup uses, a
|
with at most the e-mail address when it gives neither name, and, with service, a service account, with at most the
|
||||||
service account, with at most the e-mail address too."""
|
e-mail address too."""
|
||||||
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
|
||||||
|
|||||||
@@ -441,9 +441,11 @@ 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
|
without the names a Manager-only account, and with "service": true a
|
||||||
|
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": ...}
|
PUT /users/<name>/password set the password: {"password": ...}; refused for the internal service
|
||||||
|
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
|
||||||
@@ -461,7 +463,8 @@ def users(state, method, path, body):
|
|||||||
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: vpnrouter_users.list_users(connection),
|
||||||
('POST', 1, None): lambda connection: vpnrouter_users.add(connection, body.get('name'), body.get('password'), body),
|
('POST', 1, None): lambda connection: vpnrouter_users.add(connection, body.get('name'), body.get('password'), body,
|
||||||
|
service=body.get('service') is True),
|
||||||
('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),
|
||||||
@@ -478,6 +481,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:
|
||||||
document = actions[route](connection)
|
document = actions[route](connection)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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] 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] 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>
|
||||||
@@ -40,7 +41,8 @@ from vpnrouter_users import Failure
|
|||||||
HELP = {
|
HELP = {
|
||||||
'list': 'list the users',
|
'list': 'list the users',
|
||||||
'add': 'add a VPN user with the three person options and issue the certificate, or without the names a '
|
'add': 'add a VPN user with the three person options and issue the certificate, or without the names a '
|
||||||
'Manager-only account, with an optional e-mail address; the password is read from standard input',
|
'Manager-only account, or with --service a service account, each with an optional e-mail address; the '
|
||||||
|
'password is read from standard input',
|
||||||
'update': "change the first name, the last name or the e-mail address; a Manager-only account has only the "
|
'update': "change the first name, the last name or the e-mail address; a Manager-only account has only the "
|
||||||
"e-mail address, which '' removes",
|
"e-mail address, which '' removes",
|
||||||
'passwd': 'set the password, read from standard input',
|
'passwd': 'set the password, read from standard input',
|
||||||
@@ -76,6 +78,8 @@ def parse(argv):
|
|||||||
if command in ('add', 'update'):
|
if command in ('add', 'update'):
|
||||||
for key, _ in vpnrouter_users.PERSON:
|
for key, _ in vpnrouter_users.PERSON:
|
||||||
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':
|
||||||
|
sub.add_argument('--service', action='store_true', help='add a service account, which cannot sign in to the web service')
|
||||||
if command == 'otp-enrol':
|
if command == 'otp-enrol':
|
||||||
sub.add_argument('code', help='a code of the secret from the authenticator')
|
sub.add_argument('code', help='a code of the secret from the authenticator')
|
||||||
return parser.parse_args(argv)
|
return parser.parse_args(argv)
|
||||||
@@ -93,7 +97,7 @@ def run(connection, options, secret):
|
|||||||
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}
|
||||||
if command == 'add':
|
if command == 'add':
|
||||||
return vpnrouter_users.add(connection, options.name, secret, values)
|
return vpnrouter_users.add(connection, options.name, secret, values, service=options.service)
|
||||||
if command == 'update':
|
if command == 'update':
|
||||||
return vpnrouter_users.update(connection, options.name, values)
|
return vpnrouter_users.update(connection, options.name, values)
|
||||||
if command == 'passwd':
|
if command == 'passwd':
|
||||||
@@ -115,7 +119,7 @@ def text_of(answer):
|
|||||||
if not users:
|
if not users:
|
||||||
return 'no users'
|
return 'no users'
|
||||||
head = ('USER', 'KIND', 'STATE', 'CERTIFICATE', 'OTP', 'FIRST NAME', 'LAST NAME', 'E-MAIL')
|
head = ('USER', 'KIND', 'STATE', 'CERTIFICATE', 'OTP', 'FIRST NAME', 'LAST NAME', 'E-MAIL')
|
||||||
rows = [(user['name'], 'service' if user['service'] else 'manager' if user['manager_only'] else 'vpn',
|
rows = [(user['name'], 'internal' if user['internal'] else 'service' if user['service'] else 'manager' if user['manager_only'] else 'vpn',
|
||||||
'disabled' if not user['enabled'] else 'locked' if user['locked'] else 'enabled',
|
'disabled' if not user['enabled'] else 'locked' if user['locked'] else 'enabled',
|
||||||
'issued' if user['certificate'] else 'missing',
|
'issued' if user['certificate'] else 'missing',
|
||||||
'enrolled' if user['otp'] else '-', user['first_name'] or '-', user['last_name'] or '-', user['email'] or '-')
|
'enrolled' if user['otp'] else '-', user['first_name'] or '-', user['last_name'] or '-', user['email'] or '-')
|
||||||
|
|||||||
Reference in new issue
Block a user