feature: service accounts have a display name and a description, which the router sets for its internal ones.
Release / release (push) Successful in 20s
Release / release (push) Successful in 20s
This commit is contained in:
1 parent
68ebd541b2
commit
25c151841b
8 files changed
+139
-52
No files matched your search
@@ -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
|
||||
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
|
||||
system signs in with to use a router service, and which cannot sign in to the web service. The
|
||||
internal service accounts are those the router creates and manages for its own services
|
||||
(internal_accounts); vpn-router-setup keeps the metrics service's.
|
||||
system signs in with to use a router service, and which cannot sign in to the web service; it has
|
||||
a display name and a description (labels), in displayName and description of vpnRouterAccount. The
|
||||
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.
|
||||
|
||||
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
|
||||
#: The keys of the person attributes and their LDAP attributes.
|
||||
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.
|
||||
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.
|
||||
SERVICE_SCOPE = 'service'
|
||||
#: The object classes of a VPN user and of a Manager-only account.
|
||||
@@ -147,6 +151,26 @@ def person(values, required):
|
||||
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):
|
||||
return f'uid={name},{vpnrouter_ldap.USERS}'
|
||||
|
||||
@@ -195,17 +219,23 @@ def service(attributes):
|
||||
|
||||
|
||||
def internal_accounts():
|
||||
"""The names of the internal service accounts, which the router creates and manages for its own services: the one
|
||||
metrics.service_account names while metrics are enabled."""
|
||||
"""The internal service accounts, which the router creates and manages for its own services, by name, each with
|
||||
the labels the router gives it ({display_name, description}): the one metrics.service_account names while metrics
|
||||
are enabled."""
|
||||
try:
|
||||
metrics = vpnrouter.load_config()['metrics']
|
||||
except SystemExit:
|
||||
return set()
|
||||
return {metrics['service_account']} if metrics['enabled'] else set()
|
||||
return {}
|
||||
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):
|
||||
"""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).
|
||||
"""
|
||||
@@ -221,7 +251,7 @@ def summary(attributes):
|
||||
'certificate': certificate(name)[0].is_file(),
|
||||
'otp': 'oathTOTPToken' in attributes,
|
||||
}
|
||||
for key, attribute in PERSON:
|
||||
for key, attribute in PERSON + LABELS:
|
||||
values = attributes.get(attribute)
|
||||
user[key] = values[0].decode() if values else None
|
||||
return user
|
||||
@@ -242,12 +272,14 @@ def list_users(connection):
|
||||
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
|
||||
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)
|
||||
details = person(values, required=False)
|
||||
vpn_user = 'givenName' in details or 'sn' in details
|
||||
if service and vpn_user:
|
||||
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):
|
||||
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)
|
||||
@@ -277,9 +309,12 @@ def add(connection, name, password, values, service=False):
|
||||
def update(connection, name, values):
|
||||
"""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)
|
||||
if service(current):
|
||||
return update_service(connection, name, values)
|
||||
if manager_only(current):
|
||||
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)
|
||||
@@ -303,6 +338,21 @@ def update(connection, name, values):
|
||||
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):
|
||||
check_password(password)
|
||||
entry(connection, name)
|
||||
|
||||
Reference in new issue
Block a user