Skip to content
Draft
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
18 changes: 18 additions & 0 deletions kanidm-formula/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Salt states for Kanidm

This formula provides states for the configuration of [Kanidm](https://kanidm.com/).

## Available states

`kanidm.client`

Installs and configures the [client tools](https://kanidm.github.io/kanidm/stable/client_tools.html).

`kanidm.unixd`

Installs and configures the [UNIX daemon](https://kanidm.github.io/kanidm/stable/integrations/pam_and_nsswitch.html)
and configures nsswitch/PAM to make use of it.

`kanidm.server`

Installs and configures the server (TODO).
131 changes: 131 additions & 0 deletions kanidm-formula/_modules/kanidm_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
from asyncio import run
from salt.exceptions import CommandNotFoundError
from salt.utils.path import which
from logging import getLogger

log = getLogger(__name__)
CMD = 'kanidm'

HAVE_KANIDM_CLI = None
HAVE_KANIDM_PY = None
client = None

if which(CMD) is None:
HAVE_KANIDM_CLI = False
else:
HAVE_KANIDM_CLI = True

def _py_client():
# TODO: error handling and factor out path
with open('/etc/kanidm/salt') as fh:
uri, token = fh.readlines()
#__salt__['pillar.get']('kanidm:server:config:origin', __salt__['pillar.get']('kanidm:client:config:uri'))

return KanidmClient(uri=uri.strip(), token=token.strip())

#log.error('The kanidm_client module cannot locate a Kanidm server URI. API functionality will not be available.')
#HAVE_KANIDM_PY = False
#return None

try:
from kanidm import KanidmClient
HAVE_KANIDM_PY = True
client = _py_client()
except ImportError:
HAVE_KANIDM_PY = False

def __virtual__():
if not HAVE_KANIDM_CLI and not HAVE_KANIDM_PY:
return (False, f'The kanidm_client module cannot be loaded: neither the "{CMD}" binary nor pykanidm are available.')

return True

def _guard_cli():
if not HAVE_KANIDM_CLI:
raise CommandNotFoundError(f'Execution not possible: "{CMD}" is not available.')

def _guard_py():
if not HAVE_KANIDM_PY:
raise CommandNotFoundError(f'Execution not possible: pykanidm is not available.')

def _run_kanidm(sub_cmd, expect_out=True, expect_single=True, user=None, env={}):
_guard_cli()

base_cmd = f'{CMD} -o json'

if user is not None:
base_cmd = f'{base_cmd} -D {user}'

cmd = f'{base_cmd} {sub_cmd}'
log.debug(f'Executing: {cmd}')

return __utils__['kanidm_parse.parse_cli_result'](__salt__['cmd.run_all'](cmd, env=env), expect_out, expect_single)

def local_login(user, password):
"""
This is used internally for bootstrapping through local_* functions. Do not use it on the command line!
"""
return _run_kanidm('login', expect_out=False, user=user, env={'KANIDM_PASSWORD': password})

def local_service_account_create(account_id, display_name, managed_by, groups=[]):
data = _run_kanidm(f'service-account create {account_id} "{display_name}" {managed_by}', expect_out=False)
if data is True:
for group in groups:
_run_kanidm(f'group add-members {group} {account_id}')

return True

return False

def local_service_account_api_token(account_id, label, expiry=None, rw=False):
cmd = f'service-account api-token generate {account_id}'
if expiry is not None:
cmd = f'{cmd} {expiry}'
if rw is True:
cmd = f'{cmd} -w'

return _run_kanidm(cmd).get('result')

def service_account_get(name):
_guard_py()

try:
return run(client.service_account_get(name)).dict()
except ValueError:
return None

def service_account_list():
_guard_py()

# TODO: https://github.com/kanidm/kanidm/issues/4044
return None

def person_account_create(name, displayname):
_guard_py()

r = run(client.person_account_create(name, displayname))
log.debug(f'person_account_create(): got {r.dict()}')
return r.status_code == 200

def person_account_update(name, newname=None, displayname=None, legalname=None, mail=None):
_guard_py()

r = run(client.person_account_update(name, newname, displayname, legalname, mail))
log.debug(f'person_account_update(): got {r.dict()}')
return r.status_code == 200

def person_account_get(name):
_guard_py()

try:
return run(_person_account_get(name))
except ValueError:
return None

async def _person_account_list():
return await client.person_account_list()

def person_account_list():
_guard_py()

return __utils__['kanidm_parse.parse_list_result'](run(_person_account_list()))
26 changes: 26 additions & 0 deletions kanidm-formula/_modules/kanidm_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import salt.utils
from logging import getLogger

log = getLogger(__name__)
CMD = 'kanidmd'

def __virtual__():
if salt.utils.path.which(CMD) is None:
return (False, f'The kanidm_server module cannot be loaded: "{CMD}" is not available.')

return True

def _run_kanidmd(sub_cmd, expect_out=True, expect_single=True):
cmd = f'{CMD} -o json {sub_cmd}'
log.debug(f'Executing: {cmd}')

return __utils__['kanidm_parse.parse_cli_result'](__salt__['cmd.run_all'](cmd), expect_out, expect_single)

def recover_account(name):
data = _run_kanidmd(f'recover-account {name}')

# TODO: kanidm should return with > 0 and print something useful
if data == 'error':
return False

return data.get('password')
92 changes: 92 additions & 0 deletions kanidm-formula/_states/kanidm_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
BUILTIN_ACCOUNTS = []

def person_account_managed(name, domain, account_data):
ret = {'name': name, 'changes': {}, 'result': None, 'comment': ''}

have_accounts_list = __salt__['kanidm_client.person_account_list']()
# TODO: not efficient to do this on every call
have_accounts = {}
for p in have_accounts_list:
name = p.pop('name')
if name:
have_accounts[name] = p

if name in have_accounts:
fields = ['displayname', 'legalname', 'mail']
update_fields = {
field: None
for field in fields
}
for field in fields:
if field in account_data:
if account_data[field] != have_accounts[name].get(field):
if name not in ret['changes']:
ret['changes'][name] = {}
if field not in ret['changes'][name]:
ret['changes'][name][field] = {}

ret['changes'][name][field]['new'] = account_data[field]
ret['changes'][name][field]['old'] = have_accounts[name][field]
update_fields[field] = account_data[field]

elif field in have_accounts[name]:
if name not in ret['changes']:
ret['changes'][name] = {}
ret['changes'][name][field] = {
'new': None,
'old': have_accounts[name][field],
}

if name in ret['changes']:
if __opts__['test']:
ret['comment'] = 'Account would be updated.'
ret['result'] = None
else:
ret['result'] = __salt__['kanidm_client.person_account_update'](name, displayname=update_fields['displayname'], legalname=update_fields['legalname'], mail=update_fields['mail'])
if ret['result'] is True:
ret['comment'] = 'Account updated.'
else:
ret['comment'] = 'Account update failed.'

else:
ret['comment'] = 'Account is up to date.'
ret['result'] = True

elif __opts__['test']:
ret['comment'] = 'Account would be created.'
ret['result'] = None

else:
ret['result'] = __salt__['kanidm_client.person_account_create'](name, displayname=account_data.get('displayname'))
if ret['result'] is True:
ret['comment'] = 'Account created.'
else:
ret['comment'] = 'Account creation failed.'

return ret


#def person_accounts_managed(domain, persons_pillar):
# want_persons = __salt__['pillar.get'](persons_pillar)
# have_persons_list = __salt__['kanidm_client.person_account_list')
# reduced_have_persons = []
# have_names = []
#
# have_persons = {}
# for p in have_persons_list:
# name = p.pop('name')
# if name:
# have_persons[name] = p
#
# add_persons = []
# update_persons = []
# delete_persons = []
#
# for name, more in want_persons:
# if name in have_persons:
# update_persons.append(name)
# if name not in have_persons:
# add_persons.append(name)
# for name, more in have_persons:
# if name not in want_persons and name not in BUILTIN_ACCOUNTS:
# delete_persons.append(name)
56 changes: 56 additions & 0 deletions kanidm-formula/_utils/kanidm_parse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from json.decoder import JSONDecodeError
from salt.exceptions import CommandExecutionError
from salt.utils.json import loads
from logging import getLogger

log = getLogger(__name__)

def parse_cli_result(out, expect_out=True, expect_single=True):
log.debug(f'Result: {out}')

# for commands such as "login" which do not produce json at all
if expect_out is False:
if out['retcode'] == 0:
return True
return False

if out['retcode'] != 0:
raise CommandExecutionError(f'Execution of "kanidmd" failed: {out}')

if not out['stdout']:
return True

res = None

try:
res = loads(out['stdout'])
except JSONDecodeError:
# sometimes the result is on stdout, sometimes on stderr
try:
res = loads(out['stderr'])
except JSONDecodeError:
# https://github.com/kanidm/kanidm/issues/4040
data = []
for line in out['stdout'].splitlines() + out['stderr'].splitlines():
if not ( line[0] == '{' or ( line[0] == '"' and line[-1] == '"' ) ):
continue
data.append(loads(line))

ld = len(data)
if ld == 1 and expect_single:
res = data[0]
elif ld > 1 and not expect_single:
res = data

if res is not None:
return res

raise CommandExecutionError(f'Execution of "kanidmd" did not yield expected JSON data: {out}')

def parse_list_result(data):
res = []

for obj in data:
res.append(obj.dict())

return res
14 changes: 14 additions & 0 deletions kanidm-formula/kanidm/client.sls
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
kanidm_client_packages:
pkg.installed:
- name: kanidm-clients

kanidm_client_config_header:
file.prepend:
- name: /etc/kanidm/config
- text: {{ pillar.get('managed_by_salt_formula', '# Managed by the Kanidm formula') | yaml_encode }}

kanidm_client_config:
file.serialize:
- name: /etc/kanidm/config
- dataset_pillar: kanidm:client:config
- serializer: toml
17 changes: 17 additions & 0 deletions kanidm-formula/kanidm/data.sls
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!py
# vim: ft=python.salt

def run():
states = {}
pillar = __salt__['pillar.get']('kanidm:data', {})

for name, data in pillar.get('person_accounts', {}).items():
states[f'kanidm-data-person-account-{name}'] = {
'kanidm_data.person_account_managed': [
{'name': name},
{'domain': pillar.get('domain', '')}, # TODO
{'account_data': data},
],
}

return states
13 changes: 13 additions & 0 deletions kanidm-formula/kanidm/server/defaults.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "2",
"bindaddress": "[::]:443",
"ldapbindaddress": "[::]:636",
"db_path": "/var/lib/private/kanidm/kanidm.db",
"tls_chain": "/etc/kanidm/chain.pem",
"tls_key": "/var/lib/private/kanidm/key.pem",
"log_level": "info",
"online_backup": {
"path": "/var/lib/private/kanidm/backups/",
"schedule": "00 22 * * *"
}
}
Loading