nixos/taskserver: Handle declarative conf via JSON

We now no longer have the stupid --service-helper option, which silences
messages about already existing organisations, users or groups.

Instead of that option, we now have a new subcommand called
"process-json", which accepts a JSON file directly from the specified
NixOS module options and creates/deletes the users accordingly.

Note that this still has a two issues left to solve in this area:

 * Deletion is not supported yet.
 * If a user is created imperatively, the next run of process-json will
   delete it once deletion is supported.

So we need to implement deletion and a way to mark organisations, users
and groups as "imperatively managed".

Signed-off-by: aszlig <aszlig@redmoonstudios.org>
This commit is contained in:
aszlig 2016-04-11 22:24:58 +02:00
parent cf0501600a
commit 6e10705754
No known key found for this signature in database
GPG Key ID: D0EBD0EC8C2DC961
2 changed files with 77 additions and 32 deletions

View File

@ -142,8 +142,6 @@ let
propagatedBuildInputs = [ pkgs.pythonPackages.click ]; propagatedBuildInputs = [ pkgs.pythonPackages.click ];
}; };
ctlcmd = "${nixos-taskserver}/bin/nixos-taskserver --service-helper";
withMeta = meta: defs: mkMerge [ defs { inherit meta; } ]; withMeta = meta: defs: mkMerge [ defs { inherit meta; } ];
in { in {
@ -432,20 +430,10 @@ in {
environment.TASKDDATA = cfg.dataDir; environment.TASKDDATA = cfg.dataDir;
preStart = '' preStart = let
${concatStrings (mapAttrsToList (orgName: attrs: '' jsonOrgs = builtins.toJSON cfg.organisations;
${ctlcmd} add-org ${mkShellStr orgName} jsonFile = pkgs.writeText "orgs.json" jsonOrgs;
in "${nixos-taskserver}/bin/nixos-taskserver process-json '${jsonFile}'";
${concatMapStrings (user: ''
echo Creating ${user} >&2
${ctlcmd} add-user ${mkShellStr orgName} ${mkShellStr user}
'') attrs.users}
${concatMapStrings (group: ''
${ctlcmd} add-group ${mkShellStr orgName} ${mkShellStr user}
'') attrs.groups}
'') cfg.organisations)}
'';
serviceConfig = { serviceConfig = {
ExecStart = "@${taskd} taskd server"; ExecStart = "@${taskd} taskd server";

View File

@ -1,4 +1,5 @@
import grp import grp
import json
import pwd import pwd
import os import os
import re import re
@ -210,6 +211,13 @@ class Organisation(object):
return newuser return newuser
return None return None
def del_user(self, name):
"""
Delete a user and revoke its keys.
"""
sys.stderr.write("Delete user {}.".format(name))
# TODO: deletion!
def add_group(self, name): def add_group(self, name):
""" """
Create a new group. Create a new group.
@ -223,6 +231,13 @@ class Organisation(object):
return newgroup return newgroup
return None return None
def del_group(self, name):
"""
Delete a group.
"""
sys.stderr.write("Delete group {}.".format(name))
# TODO: deletion!
def get_user(self, name): def get_user(self, name):
return self.users.get(name) return self.users.get(name)
@ -261,6 +276,14 @@ class Manager(object):
return neworg return neworg
return None return None
def del_org(self, name):
"""
Delete and revoke keys of an organisation with all its users and
groups.
"""
sys.stderr.write("Delete org {}.".format(name))
# TODO: deletion!
def get_org(self, name): def get_org(self, name):
return self.orgs.get(name) return self.orgs.get(name)
@ -285,13 +308,11 @@ ORGANISATION = OrganisationType()
@click.group() @click.group()
@click.option('--service-helper', is_flag=True) def cli():
@click.pass_context
def cli(ctx, service_helper):
""" """
Manage Taskserver users and certificates Manage Taskserver users and certificates
""" """
ctx.obj = {'is_service_helper': service_helper} pass
@cli.command("list-users") @cli.command("list-users")
@ -351,14 +372,11 @@ def export_user(organisation, user):
@cli.command("add-org") @cli.command("add-org")
@click.argument("name") @click.argument("name")
@click.pass_obj def add_org(name):
def add_org(obj, name):
""" """
Create an organisation with the specified name. Create an organisation with the specified name.
""" """
if os.path.exists(mkpath(name)): if os.path.exists(mkpath(name)):
if obj['is_service_helper']:
return
msg = "Organisation with name {} already exists." msg = "Organisation with name {} already exists."
sys.exit(msg.format(name)) sys.exit(msg.format(name))
@ -368,8 +386,7 @@ def add_org(obj, name):
@cli.command("add-user") @cli.command("add-user")
@click.argument("organisation", type=ORGANISATION) @click.argument("organisation", type=ORGANISATION)
@click.argument("user") @click.argument("user")
@click.pass_obj def add_user(organisation, user):
def add_user(obj, organisation, user):
""" """
Create a user for the given organisation along with a client certificate Create a user for the given organisation along with a client certificate
and print the key of the new user. and print the key of the new user.
@ -379,8 +396,6 @@ def add_user(obj, organisation, user):
""" """
userobj = organisation.add_user(user) userobj = organisation.add_user(user)
if userobj is None: if userobj is None:
if obj['is_service_helper']:
return
msg = "User {} already exists in organisation {}." msg = "User {} already exists in organisation {}."
sys.exit(msg.format(user, organisation)) sys.exit(msg.format(user, organisation))
@ -388,18 +403,60 @@ def add_user(obj, organisation, user):
@cli.command("add-group") @cli.command("add-group")
@click.argument("organisation", type=ORGANISATION) @click.argument("organisation", type=ORGANISATION)
@click.argument("group") @click.argument("group")
@click.pass_obj def add_group(organisation, group):
def add_group(obj, organisation, group):
""" """
Create a group for the given organisation. Create a group for the given organisation.
""" """
userobj = organisation.add_group(group) userobj = organisation.add_group(group)
if userobj is None: if userobj is None:
if obj['is_service_helper']:
return
msg = "Group {} already exists in organisation {}." msg = "Group {} already exists in organisation {}."
sys.exit(msg.format(group, organisation)) sys.exit(msg.format(group, organisation))
def add_or_delete(old, new, add_fun, del_fun):
"""
Given an 'old' and 'new' list, figure out the intersections and invoke
'add_fun' against every element that is not in the 'old' list and 'del_fun'
against every element that is not in the 'new' list.
Returns a tuple where the first element is the list of elements that were
added and the second element consisting of elements that were deleted.
"""
old_set = set(old)
new_set = set(new)
to_delete = old_set - new_set
to_add = new_set - old_set
for elem in to_delete:
del_fun(elem)
for elem in to_add:
add_fun(elem)
return to_add, to_delete
@cli.command("process-json")
@click.argument('json-file', type=click.File('rb'))
def process_json(json_file):
"""
Create and delete users, groups and organisations based on a JSON file.
The structure of this file is exactly the same as the
'services.taskserver.organisations' option of the NixOS module and is used
for declaratively adding and deleting users.
Hence this subcommand is not recommended outside of the scope of the NixOS
module.
"""
data = json.load(json_file)
mgr = Manager()
add_or_delete(mgr.orgs.keys(), data.keys(), mgr.add_org, mgr.del_org)
for org in mgr.orgs.values():
add_or_delete(org.users.keys(), data[org.name]['users'],
org.add_user, org.del_user)
add_or_delete(org.groups.keys(), data[org.name]['groups'],
org.add_group, org.del_group)
if __name__ == '__main__': if __name__ == '__main__':
cli() cli()