Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions autoinstall-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,33 @@
},
"password": {
"type": "string"
},
"groups": {
"oneOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "object",
"properties": {
"override": {
"type": "array",
"items": {
"type": "string"
}
},
"append": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
]
}
},
"required": [
Expand Down
1 change: 1 addition & 0 deletions doc/.custom_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ ubuntu
udev
unencrypted
unformatted
useradd
validator
VLAN
webhook
Expand Down
42 changes: 41 additions & 1 deletion doc/reference/autoinstall-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,7 @@ identity

Configure the initial user for the system. This is the only configuration key that must be present (unless the :ref:`user-data section <ai-user-data>` is present, in which case it is optional).

A mapping that can contain keys, all of which take string values:
A mapping that can contain keys, all of which but groups take string values:

realname
^^^^^^^^
Expand All @@ -844,6 +844,24 @@ The encrypted password string must conform to what the ``passwd`` command requir

Several tools can generate the encrypted password, such as ``mkpasswd`` from the ``whois`` package, or ``openssl passwd``.

groups
^^^^^^

* **type:** mapping or list of strings (see below)
* **default:** hard-coded list of groups (e.g., `sudo`, `admin`)

Configures which groups the newly created user should belong to. The hard-coded groups (i.e., `sudo`, `admin`) can either be included or excluded.

The mapping contains the `override` and `append` keys, which are mutually exclusive. The groups directive also accepts a list of strings, providing syntactic sugar for `{override: ...}`.

* `override`

Specifies the list of groups that the user should belong to, ignoring groups hard-coded in Subiquity.

* `append`

Specifies the list of groups that the user should belong to, in addition to the defaults.

Example:

.. _ai-identity-example:
Expand All @@ -857,6 +875,28 @@ Example:
password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1'
hostname: ubuntu

autoinstall:
identity:
username: ubuntu
password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1'
hostname: ubuntu
# Specifies the list of groups the user should belong to.
groups: [adm, sudo, lpadmin]
# Alternative syntax:
# groups:
# override: [adm, sudo, lpadmin]

autoinstall:
identity:
username: ubuntu
password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1'
hostname: ubuntu
# Ensure the user is part of supplementary groups.
groups:
append: [adm, sudo, lpadmin]

Note that in any case, one more group will be created for the user, with the same name as their username (see `useradd(8)`). This behavior cannot currently be disabled.

.. _ai-active-directory:

active-directory
Expand Down
109 changes: 82 additions & 27 deletions subiquity/models/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,43 +14,98 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import logging
from typing import Any

import attr

from subiquity.common.types import IdentityData
from subiquity.server.autoinstall import AutoinstallError

log = logging.getLogger("subiquity.models.identity")


@attr.s
class User(object):
realname = attr.ib()
username = attr.ib()
password = attr.ib()
class DefaultGroups:
"""Special value for unresolved default groups"""


@attr.s(auto_attribs=True)
class User:
realname: str
username: str
password: str

groups: set[str | type[DefaultGroups]]

def resolved_groups(self, *, default: set[str]) -> set[str]:
groups = self.groups.copy()
if DefaultGroups in groups:
groups.remove(DefaultGroups)
groups.update(default)
return groups

@classmethod
def from_identity_data(cls, data: IdentityData) -> "User":
return cls(
username=data.username,
password=data.crypted_password,
realname=data.realname if data.realname else data.username,
groups={DefaultGroups},
)

class IdentityModel(object):
@classmethod
def from_autoinstall(cls, data: dict[str, Any]) -> "User":
if "groups" not in data:
groups = {DefaultGroups}
elif isinstance(data["groups"], list):
groups = set(data["groups"])
elif isinstance(data["groups"], dict):
if "override" in data["groups"] and "append" in data["groups"]:
raise AutoinstallError(
"cannot combine `groups: append` and `groups: override`"
)
if "override" in data["groups"]:
groups = set(data["groups"]["override"])
elif "append" in data["groups"]:
groups = {DefaultGroups}
groups.update(set(data["groups"]["append"]))
else:
raise ValueError
else:
raise ValueError
return cls(
username=data["username"],
realname=data.get("realname", ""),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: if we don't pass a realname in the identity flow then it will take the value of the user name. Here we set it to empty. Is the behavior difference intentional?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see this is the original behavior, but maybe more clear now they are closer together. I'll leave the question up so we can consider it but not blocking imo.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this realistically needs to be fixed in a follow on PR! Thanks for pointing it out!

password=data["password"],
groups=groups,
)

def to_autoinstall(self) -> dict[str, Any]:
d = {
"realname": self.realname,
"username": self.username,
"password": self.password,
}

if self.groups == {DefaultGroups}:
# This is the default
pass
elif DefaultGroups in self.groups:
d["groups"] = {"append": sorted(self.groups - {DefaultGroups})}
else:
d["groups"] = {"override": sorted(self.groups)}
return d


class IdentityModel:
"""Model representing user identity"""

def __init__(self):
self._user = None
self._hostname = None

def add_user(self, identity_data):
self._hostname = identity_data.hostname
d = {}
d["realname"] = identity_data.realname
d["username"] = identity_data.username
d["password"] = identity_data.crypted_password
if not d["realname"]:
d["realname"] = identity_data.username
self._user = User(**d)

@property
def hostname(self):
return self._hostname

@property
def user(self):
return self._user
def __init__(self) -> None:
self.user: User | None = None
self.hostname: str | None = None

def add_user(self, data: IdentityData) -> None:
self.hostname = data.hostname
self.user = User.from_identity_data(data)

def __repr__(self):
return "<LocalUser: {} {}>".format(self.user, self.hostname)
Loading
Loading