mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-04-17 05:48:38 +00:00
Merge pull request #251598 from oddlama/feat-kanidm-provision
nixos/kanidm: add basic provisioning
This commit is contained in:
commit
c49d0387e0
@ -338,6 +338,8 @@
|
||||
|
||||
- `nixosTests` now provide a working IPv6 setup for VLAN 1 by default.
|
||||
|
||||
- Kanidm can now be provisioned using the new [`services.kanidm.provision`] option, but requires using a patched version available via `pkgs.kanidm.withSecretProvisioning`.
|
||||
|
||||
- To facilitate dependency injection, the `imgui` package now builds a static archive using vcpkg' CMake rules.
|
||||
The derivation now installs "impl" headers selectively instead of by a wildcard.
|
||||
Use `imgui.src` if you just want to access the unpacked sources.
|
||||
|
@ -62,6 +62,94 @@ let
|
||||
#UMask = "0066";
|
||||
};
|
||||
|
||||
mkPresentOption = what:
|
||||
lib.mkOption {
|
||||
description = "Whether to ensure that this ${what} is present or absent.";
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
};
|
||||
|
||||
filterPresent = lib.filterAttrs (_: v: v.present);
|
||||
|
||||
provisionStateJson = pkgs.writeText "provision-state.json" (builtins.toJSON {
|
||||
inherit (cfg.provision) groups persons systems;
|
||||
});
|
||||
|
||||
# Only recover the admin account if a password should explicitly be provisioned
|
||||
# for the account. Otherwise it is not needed for provisioning.
|
||||
maybeRecoverAdmin = lib.optionalString (cfg.provision.adminPasswordFile != null) ''
|
||||
KANIDM_ADMIN_PASSWORD=$(< ${cfg.provision.adminPasswordFile})
|
||||
# We always reset the admin account password if a desired password was specified.
|
||||
if ! KANIDM_RECOVER_ACCOUNT_PASSWORD=$KANIDM_ADMIN_PASSWORD ${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} admin --from-environment >/dev/null; then
|
||||
echo "Failed to recover admin account" >&2
|
||||
exit 1
|
||||
fi
|
||||
'';
|
||||
|
||||
# Recover the idm_admin account. If a password should explicitly be provisioned
|
||||
# for the account we set it, otherwise we generate a new one because it is required
|
||||
# for provisioning.
|
||||
recoverIdmAdmin = if cfg.provision.idmAdminPasswordFile != null
|
||||
then ''
|
||||
KANIDM_IDM_ADMIN_PASSWORD=$(< ${cfg.provision.idmAdminPasswordFile})
|
||||
# We always reset the idm_admin account password if a desired password was specified.
|
||||
if ! KANIDM_RECOVER_ACCOUNT_PASSWORD=$KANIDM_IDM_ADMIN_PASSWORD ${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin --from-environment >/dev/null; then
|
||||
echo "Failed to recover idm_admin account" >&2
|
||||
exit 1
|
||||
fi
|
||||
''
|
||||
else ''
|
||||
# Recover idm_admin account
|
||||
if ! recover_out=$(${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin -o json); then
|
||||
echo "$recover_out" >&2
|
||||
echo "kanidm provision: Failed to recover admin account" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! KANIDM_IDM_ADMIN_PASSWORD=$(grep '{"password' <<< "$recover_out" | ${lib.getExe pkgs.jq} -r .password); then
|
||||
echo "$recover_out" >&2
|
||||
echo "kanidm provision: Failed to parse password for idm_admin account" >&2
|
||||
exit 1
|
||||
fi
|
||||
'';
|
||||
|
||||
postStartScript = pkgs.writeShellScript "post-start" ''
|
||||
set -euo pipefail
|
||||
|
||||
# Wait for the kanidm server to come online
|
||||
count=0
|
||||
while ! ${lib.getExe pkgs.curl} -L --silent --max-time 1 --connect-timeout 1 --fail \
|
||||
${lib.optionalString cfg.provision.acceptInvalidCerts "--insecure"} \
|
||||
${cfg.provision.instanceUrl} >/dev/null
|
||||
do
|
||||
sleep 1
|
||||
if [[ "$count" -eq 30 ]]; then
|
||||
echo "Tried for at least 30 seconds, giving up..."
|
||||
exit 1
|
||||
fi
|
||||
count=$((count++))
|
||||
done
|
||||
|
||||
${recoverIdmAdmin}
|
||||
${maybeRecoverAdmin}
|
||||
|
||||
KANIDM_PROVISION_IDM_ADMIN_TOKEN=$KANIDM_IDM_ADMIN_PASSWORD \
|
||||
${lib.getExe pkgs.kanidm-provision} \
|
||||
${lib.optionalString (!cfg.provision.autoRemove) "--no-auto-remove"} \
|
||||
${lib.optionalString cfg.provision.acceptInvalidCerts "--accept-invalid-certs"} \
|
||||
--url "${cfg.provision.instanceUrl}" \
|
||||
--state ${provisionStateJson}
|
||||
'';
|
||||
|
||||
serverPort =
|
||||
# ipv6:
|
||||
if lib.hasInfix "]:" cfg.serverSettings.bindaddress
|
||||
then lib.last (lib.splitString "]:" cfg.serverSettings.bindaddress)
|
||||
else
|
||||
# ipv4:
|
||||
if lib.hasInfix "." cfg.serverSettings.bindaddress
|
||||
then lib.last (lib.splitString ":" cfg.serverSettings.bindaddress)
|
||||
# default is 8443
|
||||
else "8443";
|
||||
in
|
||||
{
|
||||
options.services.kanidm = {
|
||||
@ -207,10 +295,267 @@ in
|
||||
for possible values.
|
||||
'';
|
||||
};
|
||||
|
||||
provision = {
|
||||
enable = lib.mkEnableOption "provisioning of groups, users and oauth2 resource servers";
|
||||
|
||||
instanceUrl = lib.mkOption {
|
||||
description = "The instance url to which the provisioning tool should connect.";
|
||||
default = "https://localhost:${serverPort}";
|
||||
defaultText = ''"https://localhost:<port from serverSettings.bindaddress>"'';
|
||||
type = lib.types.str;
|
||||
};
|
||||
|
||||
acceptInvalidCerts = lib.mkOption {
|
||||
description = ''
|
||||
Whether to allow invalid certificates when provisioning the target instance.
|
||||
By default this is only allowed when the instanceUrl is localhost. This is
|
||||
dangerous when used with an external URL.
|
||||
'';
|
||||
type = lib.types.bool;
|
||||
default = lib.hasPrefix "https://localhost:" cfg.provision.instanceUrl;
|
||||
defaultText = ''lib.hasPrefix "https://localhost:" cfg.provision.instanceUrl'';
|
||||
};
|
||||
|
||||
adminPasswordFile = lib.mkOption {
|
||||
description = "Path to a file containing the admin password for kanidm. Do NOT use a file from the nix store here!";
|
||||
example = "/run/secrets/kanidm-admin-password";
|
||||
default = null;
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
};
|
||||
|
||||
idmAdminPasswordFile = lib.mkOption {
|
||||
description = ''
|
||||
Path to a file containing the idm admin password for kanidm. Do NOT use a file from the nix store here!
|
||||
If this is not given but provisioning is enabled, the idm_admin password will be reset on each restart.
|
||||
'';
|
||||
example = "/run/secrets/kanidm-idm-admin-password";
|
||||
default = null;
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
};
|
||||
|
||||
autoRemove = lib.mkOption {
|
||||
description = ''
|
||||
Determines whether deleting an entity in this provisioning config should automatically
|
||||
cause them to be removed from kanidm, too. This works because the provisioning tool tracks
|
||||
all entities it has ever created. If this is set to false, you need to explicitly specify
|
||||
`present = false` to delete an entity.
|
||||
'';
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
};
|
||||
|
||||
groups = lib.mkOption {
|
||||
description = "Provisioning of kanidm groups";
|
||||
default = {};
|
||||
type = lib.types.attrsOf (lib.types.submodule (groupSubmod: {
|
||||
options = {
|
||||
present = mkPresentOption "group";
|
||||
|
||||
members = lib.mkOption {
|
||||
description = "List of kanidm entities (persons, groups, ...) which are part of this group.";
|
||||
type = lib.types.listOf lib.types.str;
|
||||
apply = lib.unique;
|
||||
default = [];
|
||||
};
|
||||
};
|
||||
config.members = lib.concatLists (lib.flip lib.mapAttrsToList cfg.provision.persons (person: personCfg:
|
||||
lib.optional (personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups) person
|
||||
));
|
||||
}));
|
||||
};
|
||||
|
||||
persons = lib.mkOption {
|
||||
description = "Provisioning of kanidm persons";
|
||||
default = {};
|
||||
type = lib.types.attrsOf (lib.types.submodule {
|
||||
options = {
|
||||
present = mkPresentOption "person";
|
||||
|
||||
displayName = lib.mkOption {
|
||||
description = "Display name";
|
||||
type = lib.types.str;
|
||||
example = "My User";
|
||||
};
|
||||
|
||||
legalName = lib.mkOption {
|
||||
description = "Full legal name";
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
example = "Jane Doe";
|
||||
default = null;
|
||||
};
|
||||
|
||||
mailAddresses = lib.mkOption {
|
||||
description = "Mail addresses. First given address is considered the primary address.";
|
||||
type = lib.types.listOf lib.types.str;
|
||||
example = ["jane.doe@example.com"];
|
||||
default = [];
|
||||
};
|
||||
|
||||
groups = lib.mkOption {
|
||||
description = "List of groups this person should belong to.";
|
||||
type = lib.types.listOf lib.types.str;
|
||||
apply = lib.unique;
|
||||
default = [];
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
systems.oauth2 = lib.mkOption {
|
||||
description = "Provisioning of oauth2 resource servers";
|
||||
default = {};
|
||||
type = lib.types.attrsOf (lib.types.submodule {
|
||||
options = {
|
||||
present = mkPresentOption "oauth2 resource server";
|
||||
|
||||
public = lib.mkOption {
|
||||
description = "Whether this is a public client (enforces PKCE, doesn't use a basic secret)";
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
};
|
||||
|
||||
displayName = lib.mkOption {
|
||||
description = "Display name";
|
||||
type = lib.types.str;
|
||||
example = "Some Service";
|
||||
};
|
||||
|
||||
originUrl = lib.mkOption {
|
||||
description = "The origin URL of the service. OAuth2 redirects will only be allowed to sites under this origin. Must end with a slash.";
|
||||
type = lib.types.strMatching ".*://.*/$";
|
||||
example = "https://someservice.example.com/";
|
||||
};
|
||||
|
||||
originLanding = lib.mkOption {
|
||||
description = "When redirecting from the Kanidm Apps Listing page, some linked applications may need to land on a specific page to trigger oauth2/oidc interactions.";
|
||||
type = lib.types.str;
|
||||
example = "https://someservice.example.com/home";
|
||||
};
|
||||
|
||||
basicSecretFile = lib.mkOption {
|
||||
description = ''
|
||||
The basic secret to use for this service. If null, the random secret generated
|
||||
by kanidm will not be touched. Do NOT use a path from the nix store here!
|
||||
'';
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
example = "/run/secrets/some-oauth2-basic-secret";
|
||||
default = null;
|
||||
};
|
||||
|
||||
enableLocalhostRedirects = lib.mkOption {
|
||||
description = "Allow localhost redirects. Only for public clients.";
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
};
|
||||
|
||||
enableLegacyCrypto = lib.mkOption {
|
||||
description = "Enable legacy crypto on this client. Allows JWT signing algorthms like RS256.";
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
};
|
||||
|
||||
allowInsecureClientDisablePkce = lib.mkOption {
|
||||
description = ''
|
||||
Disable PKCE on this oauth2 resource server to work around insecure clients
|
||||
that may not support it. You should request the client to enable PKCE!
|
||||
Only for non-public clients.
|
||||
'';
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
};
|
||||
|
||||
preferShortUsername = lib.mkOption {
|
||||
description = "Use 'name' instead of 'spn' in the preferred_username claim";
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
};
|
||||
|
||||
scopeMaps = lib.mkOption {
|
||||
description = ''
|
||||
Maps kanidm groups to returned oauth scopes.
|
||||
See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information.
|
||||
'';
|
||||
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
||||
default = {};
|
||||
};
|
||||
|
||||
supplementaryScopeMaps = lib.mkOption {
|
||||
description = ''
|
||||
Maps kanidm groups to additionally returned oauth scopes.
|
||||
See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information.
|
||||
'';
|
||||
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
||||
default = {};
|
||||
};
|
||||
|
||||
removeOrphanedClaimMaps = lib.mkOption {
|
||||
description = "Whether claim maps not specified here but present in kanidm should be removed from kanidm.";
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
};
|
||||
|
||||
claimMaps = lib.mkOption {
|
||||
description = ''
|
||||
Adds additional claims (and values) based on which kanidm groups an authenticating party belongs to.
|
||||
See [Claim Maps](https://kanidm.github.io/kanidm/master/integrations/oauth2.html#custom-claim-maps) for more information.
|
||||
'';
|
||||
default = {};
|
||||
type = lib.types.attrsOf (lib.types.submodule {
|
||||
options = {
|
||||
joinType = lib.mkOption {
|
||||
description = ''
|
||||
Determines how multiple values are joined to create the claim value.
|
||||
See [Claim Maps](https://kanidm.github.io/kanidm/master/integrations/oauth2.html#custom-claim-maps) for more information.
|
||||
'';
|
||||
type = lib.types.enum ["array" "csv" "ssv"];
|
||||
default = "array";
|
||||
};
|
||||
|
||||
valuesByGroup = lib.mkOption {
|
||||
description = "Maps kanidm groups to values for the claim.";
|
||||
default = {};
|
||||
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) {
|
||||
assertions =
|
||||
assertions = let
|
||||
entityList = type: attrs: lib.flip lib.mapAttrsToList (filterPresent attrs) (name: _: { inherit type name; });
|
||||
entities =
|
||||
entityList "group" cfg.provision.groups
|
||||
++ entityList "person" cfg.provision.persons
|
||||
++ entityList "oauth2" cfg.provision.systems.oauth2;
|
||||
|
||||
# Accumulate entities by name. Track corresponding entity types for later duplicate check.
|
||||
entitiesByName = lib.foldl' (acc: { type, name }:
|
||||
acc // {
|
||||
${name} = (acc.${name} or []) ++ [type];
|
||||
}
|
||||
) {} entities;
|
||||
|
||||
assertGroupsKnown = opt: groups: let
|
||||
knownGroups = lib.attrNames (filterPresent cfg.provision.groups);
|
||||
unknownGroups = lib.subtractLists knownGroups groups;
|
||||
in {
|
||||
assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == [];
|
||||
message = "${opt} refers to unknown groups: ${toString unknownGroups}";
|
||||
};
|
||||
|
||||
assertEntitiesKnown = opt: entities: let
|
||||
unknownEntities = lib.subtractLists (lib.attrNames entitiesByName) entities;
|
||||
in {
|
||||
assertion = (cfg.enableServer && cfg.provision.enable) -> unknownEntities == [];
|
||||
message = "${opt} refers to unknown entities: ${toString unknownEntities}";
|
||||
};
|
||||
in
|
||||
[
|
||||
{
|
||||
assertion = !cfg.enableServer || ((cfg.serverSettings.tls_chain or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_chain);
|
||||
@ -251,7 +596,69 @@ in
|
||||
the instance it follows.
|
||||
'';
|
||||
}
|
||||
];
|
||||
{
|
||||
assertion = cfg.provision.enable -> cfg.enableServer;
|
||||
message = "<option>services.kanidm.provision</option> requires <option>services.kanidm.enableServer</option> to be true";
|
||||
}
|
||||
# If any secret is provisioned, the kanidm package must have some required patches applied to it
|
||||
{
|
||||
assertion = (cfg.provision.enable &&
|
||||
(cfg.provision.adminPasswordFile != null
|
||||
|| cfg.provision.idmAdminPasswordFile != null
|
||||
|| lib.any (x: x.basicSecretFile != null) (lib.attrValues (filterPresent cfg.provision.systems.oauth2))
|
||||
)) -> cfg.package.enableSecretProvisioning;
|
||||
message = ''
|
||||
Specifying an admin account password or oauth2 basicSecretFile requires kanidm to be built with the secret provisioning patches.
|
||||
You may want to set `services.kanidm.package = pkgs.kanidm.withSecretProvisioning;`.
|
||||
'';
|
||||
}
|
||||
# Entity names must be globally unique:
|
||||
(let
|
||||
# Filter all names that occurred in more than one entity type.
|
||||
duplicateNames = lib.filterAttrs (_: v: builtins.length v > 1) entitiesByName;
|
||||
in {
|
||||
assertion = cfg.provision.enable -> duplicateNames == {};
|
||||
message = ''
|
||||
services.kanidm.provision requires all entity names (group, person, oauth2, ...) to be unique!
|
||||
${lib.concatLines (lib.mapAttrsToList (name: xs: " - '${name}' used as: ${toString xs}") duplicateNames)}'';
|
||||
})
|
||||
]
|
||||
++ lib.flip lib.mapAttrsToList (filterPresent cfg.provision.persons) (person: personCfg:
|
||||
assertGroupsKnown "services.kanidm.provision.persons.${person}.groups" personCfg.groups
|
||||
)
|
||||
++ lib.flip lib.mapAttrsToList (filterPresent cfg.provision.groups) (group: groupCfg:
|
||||
assertEntitiesKnown "services.kanidm.provision.groups.${group}.members" groupCfg.members
|
||||
)
|
||||
++ lib.concatLists (lib.flip lib.mapAttrsToList (filterPresent cfg.provision.systems.oauth2) (
|
||||
oauth2: oauth2Cfg:
|
||||
[
|
||||
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.scopeMaps" (lib.attrNames oauth2Cfg.scopeMaps))
|
||||
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" (lib.attrNames oauth2Cfg.supplementaryScopeMaps))
|
||||
]
|
||||
++ lib.concatLists (lib.flip lib.mapAttrsToList oauth2Cfg.claimMaps (claim: claimCfg: [
|
||||
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" (lib.attrNames claimCfg.valuesByGroup))
|
||||
# At least one group must map to a value in each claim map
|
||||
{
|
||||
assertion = (cfg.provision.enable && cfg.enableServer) -> lib.any (xs: xs != []) (lib.attrValues claimCfg.valuesByGroup);
|
||||
message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group";
|
||||
}
|
||||
# Public clients cannot define a basic secret
|
||||
{
|
||||
assertion = (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> oauth2Cfg.basicSecretFile == null;
|
||||
message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot specify a basic secret";
|
||||
}
|
||||
# Public clients cannot disable PKCE
|
||||
{
|
||||
assertion = (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> !oauth2Cfg.allowInsecureClientDisablePkce;
|
||||
message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot disable PKCE";
|
||||
}
|
||||
# Non-public clients cannot enable localhost redirects
|
||||
{
|
||||
assertion = (cfg.provision.enable && cfg.enableServer && !oauth2Cfg.public) -> !oauth2Cfg.enableLocalhostRedirects;
|
||||
message = "services.kanidm.provision.systems.oauth2.${oauth2} is a non-public client and thus cannot enable localhost redirects";
|
||||
}
|
||||
]))
|
||||
));
|
||||
|
||||
environment.systemPackages = lib.mkIf cfg.enableClient [ cfg.package ];
|
||||
|
||||
@ -277,6 +684,7 @@ in
|
||||
StateDirectoryMode = "0700";
|
||||
RuntimeDirectory = "kanidmd";
|
||||
ExecStart = "${cfg.package}/bin/kanidmd server -c ${serverConfigFile}";
|
||||
ExecStartPost = lib.mkIf cfg.provision.enable postStartScript;
|
||||
User = "kanidm";
|
||||
Group = "kanidm";
|
||||
|
||||
@ -419,6 +827,6 @@ in
|
||||
];
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ erictapen Flakebi ];
|
||||
meta.maintainers = with lib.maintainers; [ erictapen Flakebi oddlama ];
|
||||
meta.buildDocsInSandbox = false;
|
||||
}
|
||||
|
@ -484,6 +484,7 @@ in {
|
||||
k3s = handleTest ./k3s {};
|
||||
kafka = handleTest ./kafka.nix {};
|
||||
kanidm = handleTest ./kanidm.nix {};
|
||||
kanidm-provisioning = handleTest ./kanidm-provisioning.nix {};
|
||||
karma = handleTest ./karma.nix {};
|
||||
kavita = handleTest ./kavita.nix {};
|
||||
kbd-setfont-decompress = handleTest ./kbd-setfont-decompress.nix {};
|
||||
|
505
nixos/tests/kanidm-provisioning.nix
Normal file
505
nixos/tests/kanidm-provisioning.nix
Normal file
@ -0,0 +1,505 @@
|
||||
import ./make-test-python.nix (
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
certs = import ./common/acme/server/snakeoil-certs.nix;
|
||||
serverDomain = certs.domain;
|
||||
|
||||
provisionAdminPassword = "very-strong-password-for-admin";
|
||||
provisionIdmAdminPassword = "very-strong-password-for-idm-admin";
|
||||
provisionIdmAdminPassword2 = "very-strong-alternative-password-for-idm-admin";
|
||||
in
|
||||
{
|
||||
name = "kanidm-provisioning";
|
||||
meta.maintainers = with pkgs.lib.maintainers; [ oddlama ];
|
||||
|
||||
nodes.provision =
|
||||
{ pkgs, lib, ... }:
|
||||
{
|
||||
services.kanidm = {
|
||||
package = pkgs.kanidm.withSecretProvisioning;
|
||||
enableServer = true;
|
||||
serverSettings = {
|
||||
origin = "https://${serverDomain}";
|
||||
domain = serverDomain;
|
||||
bindaddress = "[::]:443";
|
||||
ldapbindaddress = "[::1]:636";
|
||||
tls_chain = certs."${serverDomain}".cert;
|
||||
tls_key = certs."${serverDomain}".key;
|
||||
};
|
||||
# So we can check whether provisioning did what we wanted
|
||||
enableClient = true;
|
||||
clientSettings = {
|
||||
uri = "https://${serverDomain}";
|
||||
verify_ca = true;
|
||||
verify_hostnames = true;
|
||||
};
|
||||
};
|
||||
|
||||
specialisation.credentialProvision.configuration =
|
||||
{ ... }:
|
||||
{
|
||||
services.kanidm.provision = lib.mkForce {
|
||||
enable = true;
|
||||
adminPasswordFile = pkgs.writeText "admin-pw" provisionAdminPassword;
|
||||
idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword;
|
||||
};
|
||||
};
|
||||
|
||||
specialisation.changedCredential.configuration =
|
||||
{ ... }:
|
||||
{
|
||||
services.kanidm.provision = lib.mkForce {
|
||||
enable = true;
|
||||
idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword2;
|
||||
};
|
||||
};
|
||||
|
||||
specialisation.addEntities.configuration =
|
||||
{ ... }:
|
||||
{
|
||||
services.kanidm.provision = lib.mkForce {
|
||||
enable = true;
|
||||
# Test whether credential recovery works without specific idmAdmin password
|
||||
#idmAdminPasswordFile =
|
||||
|
||||
groups.supergroup1 = {
|
||||
members = [ "testgroup1" ];
|
||||
};
|
||||
|
||||
groups.testgroup1 = { };
|
||||
|
||||
persons.testuser1 = {
|
||||
displayName = "Test User";
|
||||
legalName = "Jane Doe";
|
||||
mailAddresses = [ "jane.doe@example.com" ];
|
||||
groups = [
|
||||
"testgroup1"
|
||||
"service1-access"
|
||||
];
|
||||
};
|
||||
|
||||
persons.testuser2 = {
|
||||
displayName = "Powerful Test User";
|
||||
legalName = "Ryouiki Tenkai";
|
||||
groups = [ "service1-admin" ];
|
||||
};
|
||||
|
||||
groups.service1-access = { };
|
||||
groups.service1-admin = { };
|
||||
systems.oauth2.service1 = {
|
||||
displayName = "Service One";
|
||||
originUrl = "https://one.example.com/";
|
||||
originLanding = "https://one.example.com/landing";
|
||||
basicSecretFile = pkgs.writeText "bs-service1" "very-strong-secret-for-service1";
|
||||
scopeMaps.service1-access = [
|
||||
"openid"
|
||||
"email"
|
||||
"profile"
|
||||
];
|
||||
supplementaryScopeMaps.service1-admin = [ "admin" ];
|
||||
claimMaps.groups = {
|
||||
valuesByGroup.service1-admin = [ "admin" ];
|
||||
};
|
||||
};
|
||||
|
||||
systems.oauth2.service2 = {
|
||||
displayName = "Service Two";
|
||||
originUrl = "https://two.example.com/";
|
||||
originLanding = "https://landing2.example.com/";
|
||||
# Test not setting secret
|
||||
# basicSecretFile =
|
||||
allowInsecureClientDisablePkce = true;
|
||||
preferShortUsername = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
specialisation.changeAttributes.configuration =
|
||||
{ ... }:
|
||||
{
|
||||
services.kanidm.provision = lib.mkForce {
|
||||
enable = true;
|
||||
# Changing admin credentials at any time should not be a problem:
|
||||
idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword;
|
||||
|
||||
groups.supergroup1 = {
|
||||
#members = ["testgroup1"];
|
||||
};
|
||||
|
||||
groups.testgroup1 = { };
|
||||
|
||||
persons.testuser1 = {
|
||||
displayName = "Test User (changed)";
|
||||
legalName = "Jane Doe (changed)";
|
||||
mailAddresses = [
|
||||
"jane.doe@example.com"
|
||||
"second.doe@example.com"
|
||||
];
|
||||
groups = [
|
||||
#"testgroup1"
|
||||
"service1-access"
|
||||
];
|
||||
};
|
||||
|
||||
persons.testuser2 = {
|
||||
displayName = "Powerful Test User (changed)";
|
||||
legalName = "Ryouiki Tenkai (changed)";
|
||||
groups = [ "service1-admin" ];
|
||||
};
|
||||
|
||||
groups.service1-access = { };
|
||||
groups.service1-admin = { };
|
||||
systems.oauth2.service1 = {
|
||||
displayName = "Service One (changed)";
|
||||
originUrl = "https://changed-one.example.com/";
|
||||
originLanding = "https://changed-one.example.com/landing-changed";
|
||||
basicSecretFile = pkgs.writeText "bs-service1" "changed-very-strong-secret-for-service1";
|
||||
scopeMaps.service1-access = [
|
||||
"openid"
|
||||
"email"
|
||||
#"profile"
|
||||
];
|
||||
supplementaryScopeMaps.service1-admin = [ "adminchanged" ];
|
||||
claimMaps.groups = {
|
||||
valuesByGroup.service1-admin = [ "adminchanged" ];
|
||||
};
|
||||
};
|
||||
|
||||
systems.oauth2.service2 = {
|
||||
displayName = "Service Two (changed)";
|
||||
originUrl = "https://changed-two.example.com/";
|
||||
originLanding = "https://changed-landing2.example.com/";
|
||||
# Test not setting secret
|
||||
# basicSecretFile =
|
||||
allowInsecureClientDisablePkce = false;
|
||||
preferShortUsername = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
specialisation.removeAttributes.configuration =
|
||||
{ ... }:
|
||||
{
|
||||
services.kanidm.provision = lib.mkForce {
|
||||
enable = true;
|
||||
idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword;
|
||||
|
||||
groups.supergroup1 = { };
|
||||
|
||||
persons.testuser1 = {
|
||||
displayName = "Test User (changed)";
|
||||
};
|
||||
|
||||
persons.testuser2 = {
|
||||
displayName = "Powerful Test User (changed)";
|
||||
groups = [ "service1-admin" ];
|
||||
};
|
||||
|
||||
groups.service1-access = { };
|
||||
groups.service1-admin = { };
|
||||
systems.oauth2.service1 = {
|
||||
displayName = "Service One (changed)";
|
||||
originUrl = "https://changed-one.example.com/";
|
||||
originLanding = "https://changed-one.example.com/landing-changed";
|
||||
basicSecretFile = pkgs.writeText "bs-service1" "changed-very-strong-secret-for-service1";
|
||||
# Removing maps requires setting them to the empty list
|
||||
scopeMaps.service1-access = [ ];
|
||||
supplementaryScopeMaps.service1-admin = [ ];
|
||||
};
|
||||
|
||||
systems.oauth2.service2 = {
|
||||
displayName = "Service Two (changed)";
|
||||
originUrl = "https://changed-two.example.com/";
|
||||
originLanding = "https://changed-landing2.example.com/";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
specialisation.removeEntities.configuration =
|
||||
{ ... }:
|
||||
{
|
||||
services.kanidm.provision = lib.mkForce {
|
||||
enable = true;
|
||||
idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword;
|
||||
};
|
||||
};
|
||||
|
||||
security.pki.certificateFiles = [ certs.ca.cert ];
|
||||
|
||||
networking.hosts."::1" = [ serverDomain ];
|
||||
networking.firewall.allowedTCPPorts = [ 443 ];
|
||||
|
||||
users.users.kanidm.shell = pkgs.bashInteractive;
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
kanidm
|
||||
openldap
|
||||
ripgrep
|
||||
jq
|
||||
];
|
||||
};
|
||||
|
||||
testScript =
|
||||
{ nodes, ... }:
|
||||
let
|
||||
# We need access to the config file in the test script.
|
||||
filteredConfig = pkgs.lib.converge (pkgs.lib.filterAttrsRecursive (
|
||||
_: v: v != null
|
||||
)) nodes.provision.services.kanidm.serverSettings;
|
||||
serverConfigFile = (pkgs.formats.toml { }).generate "server.toml" filteredConfig;
|
||||
|
||||
specialisations = "${nodes.provision.system.build.toplevel}/specialisation";
|
||||
in
|
||||
''
|
||||
import re
|
||||
|
||||
def assert_contains(haystack, needle):
|
||||
if needle not in haystack:
|
||||
print("The haystack that will cause the following exception is:")
|
||||
print("---")
|
||||
print(haystack)
|
||||
print("---")
|
||||
raise Exception(f"Expected string '{needle}' was not found")
|
||||
|
||||
def assert_matches(haystack, expr):
|
||||
if not re.search(expr, haystack):
|
||||
print("The haystack that will cause the following exception is:")
|
||||
print("---")
|
||||
print(haystack)
|
||||
print("---")
|
||||
raise Exception(f"Expected regex '{expr}' did not match")
|
||||
|
||||
def assert_lacks(haystack, needle):
|
||||
if needle in haystack:
|
||||
print("The haystack that will cause the following exception is:")
|
||||
print("---")
|
||||
print(haystack, end="")
|
||||
print("---")
|
||||
raise Exception(f"Unexpected string '{needle}' was found")
|
||||
|
||||
provision.start()
|
||||
|
||||
def provision_login(pw):
|
||||
provision.wait_for_unit("kanidm.service")
|
||||
provision.wait_until_succeeds("curl -Lsf https://${serverDomain} | grep Kanidm")
|
||||
if pw is None:
|
||||
pw = provision.succeed("su - kanidm -c 'kanidmd recover-account -c ${serverConfigFile} idm_admin 2>&1 | rg -o \'[A-Za-z0-9]{48}\' '").strip().removeprefix("'").removesuffix("'")
|
||||
out = provision.succeed(f"KANIDM_PASSWORD={pw} kanidm login -D idm_admin")
|
||||
assert_contains(out, "Login Success for idm_admin")
|
||||
|
||||
with subtest("Test Provisioning - setup"):
|
||||
provision_login(None)
|
||||
provision.succeed("kanidm logout -D idm_admin")
|
||||
|
||||
with subtest("Test Provisioning - credentialProvision"):
|
||||
provision.succeed('${specialisations}/credentialProvision/bin/switch-to-configuration test')
|
||||
provision_login("${provisionIdmAdminPassword}")
|
||||
|
||||
# Test provisioned admin pw
|
||||
out = provision.succeed("KANIDM_PASSWORD=${provisionAdminPassword} kanidm login -D admin")
|
||||
assert_contains(out, "Login Success for admin")
|
||||
provision.succeed("kanidm logout -D admin")
|
||||
provision.succeed("kanidm logout -D idm_admin")
|
||||
|
||||
with subtest("Test Provisioning - changedCredential"):
|
||||
provision.succeed('${specialisations}/changedCredential/bin/switch-to-configuration test')
|
||||
provision_login("${provisionIdmAdminPassword2}")
|
||||
provision.succeed("kanidm logout -D idm_admin")
|
||||
|
||||
with subtest("Test Provisioning - addEntities"):
|
||||
provision.succeed('${specialisations}/addEntities/bin/switch-to-configuration test')
|
||||
# Unspecified idm admin password
|
||||
provision_login(None)
|
||||
|
||||
out = provision.succeed("kanidm group get testgroup1")
|
||||
assert_contains(out, "name: testgroup1")
|
||||
|
||||
out = provision.succeed("kanidm group get supergroup1")
|
||||
assert_contains(out, "name: supergroup1")
|
||||
assert_contains(out, "member: testgroup1")
|
||||
|
||||
out = provision.succeed("kanidm person get testuser1")
|
||||
assert_contains(out, "name: testuser1")
|
||||
assert_contains(out, "displayname: Test User")
|
||||
assert_contains(out, "legalname: Jane Doe")
|
||||
assert_contains(out, "mail: jane.doe@example.com")
|
||||
assert_contains(out, "memberof: testgroup1")
|
||||
assert_contains(out, "memberof: service1-access")
|
||||
|
||||
out = provision.succeed("kanidm person get testuser2")
|
||||
assert_contains(out, "name: testuser2")
|
||||
assert_contains(out, "displayname: Powerful Test User")
|
||||
assert_contains(out, "legalname: Ryouiki Tenkai")
|
||||
assert_contains(out, "memberof: service1-admin")
|
||||
assert_lacks(out, "mail:")
|
||||
|
||||
out = provision.succeed("kanidm group get service1-access")
|
||||
assert_contains(out, "name: service1-access")
|
||||
|
||||
out = provision.succeed("kanidm group get service1-admin")
|
||||
assert_contains(out, "name: service1-admin")
|
||||
|
||||
out = provision.succeed("kanidm system oauth2 get service1")
|
||||
assert_contains(out, "name: service1")
|
||||
assert_contains(out, "displayname: Service One")
|
||||
assert_contains(out, "oauth2_rs_origin: https://one.example.com/")
|
||||
assert_contains(out, "oauth2_rs_origin_landing: https://one.example.com/landing")
|
||||
assert_matches(out, 'oauth2_rs_scope_map: service1-access.*{"email", "openid", "profile"}')
|
||||
assert_matches(out, 'oauth2_rs_sup_scope_map: service1-admin.*{"admin"}')
|
||||
assert_matches(out, 'oauth2_rs_claim_map: groups:.*"admin"')
|
||||
|
||||
out = provision.succeed("kanidm system oauth2 show-basic-secret service1")
|
||||
assert_contains(out, "very-strong-secret-for-service1")
|
||||
|
||||
out = provision.succeed("kanidm system oauth2 get service2")
|
||||
assert_contains(out, "name: service2")
|
||||
assert_contains(out, "displayname: Service Two")
|
||||
assert_contains(out, "oauth2_rs_origin: https://two.example.com/")
|
||||
assert_contains(out, "oauth2_rs_origin_landing: https://landing2.example.com/")
|
||||
assert_contains(out, "oauth2_allow_insecure_client_disable_pkce: true")
|
||||
assert_contains(out, "oauth2_prefer_short_username: true")
|
||||
|
||||
provision.succeed("kanidm logout -D idm_admin")
|
||||
|
||||
with subtest("Test Provisioning - changeAttributes"):
|
||||
provision.succeed('${specialisations}/changeAttributes/bin/switch-to-configuration test')
|
||||
provision_login("${provisionIdmAdminPassword}")
|
||||
|
||||
out = provision.succeed("kanidm group get testgroup1")
|
||||
assert_contains(out, "name: testgroup1")
|
||||
|
||||
out = provision.succeed("kanidm group get supergroup1")
|
||||
assert_contains(out, "name: supergroup1")
|
||||
assert_lacks(out, "member: testgroup1")
|
||||
|
||||
out = provision.succeed("kanidm person get testuser1")
|
||||
assert_contains(out, "name: testuser1")
|
||||
assert_contains(out, "displayname: Test User (changed)")
|
||||
assert_contains(out, "legalname: Jane Doe (changed)")
|
||||
assert_contains(out, "mail: jane.doe@example.com")
|
||||
assert_contains(out, "mail: second.doe@example.com")
|
||||
assert_lacks(out, "memberof: testgroup1")
|
||||
assert_contains(out, "memberof: service1-access")
|
||||
|
||||
out = provision.succeed("kanidm person get testuser2")
|
||||
assert_contains(out, "name: testuser2")
|
||||
assert_contains(out, "displayname: Powerful Test User (changed)")
|
||||
assert_contains(out, "legalname: Ryouiki Tenkai (changed)")
|
||||
assert_contains(out, "memberof: service1-admin")
|
||||
assert_lacks(out, "mail:")
|
||||
|
||||
out = provision.succeed("kanidm group get service1-access")
|
||||
assert_contains(out, "name: service1-access")
|
||||
|
||||
out = provision.succeed("kanidm group get service1-admin")
|
||||
assert_contains(out, "name: service1-admin")
|
||||
|
||||
out = provision.succeed("kanidm system oauth2 get service1")
|
||||
assert_contains(out, "name: service1")
|
||||
assert_contains(out, "displayname: Service One (changed)")
|
||||
assert_contains(out, "oauth2_rs_origin: https://changed-one.example.com/")
|
||||
assert_contains(out, "oauth2_rs_origin_landing: https://changed-one.example.com/landing")
|
||||
assert_matches(out, 'oauth2_rs_scope_map: service1-access.*{"email", "openid"}')
|
||||
assert_matches(out, 'oauth2_rs_sup_scope_map: service1-admin.*{"adminchanged"}')
|
||||
assert_matches(out, 'oauth2_rs_claim_map: groups:.*"adminchanged"')
|
||||
|
||||
out = provision.succeed("kanidm system oauth2 show-basic-secret service1")
|
||||
assert_contains(out, "changed-very-strong-secret-for-service1")
|
||||
|
||||
out = provision.succeed("kanidm system oauth2 get service2")
|
||||
assert_contains(out, "name: service2")
|
||||
assert_contains(out, "displayname: Service Two (changed)")
|
||||
assert_contains(out, "oauth2_rs_origin: https://changed-two.example.com/")
|
||||
assert_contains(out, "oauth2_rs_origin_landing: https://changed-landing2.example.com/")
|
||||
assert_lacks(out, "oauth2_allow_insecure_client_disable_pkce: true")
|
||||
assert_lacks(out, "oauth2_prefer_short_username: true")
|
||||
|
||||
provision.succeed("kanidm logout -D idm_admin")
|
||||
|
||||
with subtest("Test Provisioning - removeAttributes"):
|
||||
provision.succeed('${specialisations}/removeAttributes/bin/switch-to-configuration test')
|
||||
provision_login("${provisionIdmAdminPassword}")
|
||||
|
||||
out = provision.succeed("kanidm group get testgroup1")
|
||||
assert_lacks(out, "name: testgroup1")
|
||||
|
||||
out = provision.succeed("kanidm group get supergroup1")
|
||||
assert_contains(out, "name: supergroup1")
|
||||
assert_lacks(out, "member: testgroup1")
|
||||
|
||||
out = provision.succeed("kanidm person get testuser1")
|
||||
assert_contains(out, "name: testuser1")
|
||||
assert_contains(out, "displayname: Test User (changed)")
|
||||
assert_lacks(out, "legalname: Jane Doe (changed)")
|
||||
assert_lacks(out, "mail: jane.doe@example.com")
|
||||
assert_lacks(out, "mail: second.doe@example.com")
|
||||
assert_lacks(out, "memberof: testgroup1")
|
||||
assert_lacks(out, "memberof: service1-access")
|
||||
|
||||
out = provision.succeed("kanidm person get testuser2")
|
||||
assert_contains(out, "name: testuser2")
|
||||
assert_contains(out, "displayname: Powerful Test User (changed)")
|
||||
assert_lacks(out, "legalname: Ryouiki Tenkai (changed)")
|
||||
assert_contains(out, "memberof: service1-admin")
|
||||
assert_lacks(out, "mail:")
|
||||
|
||||
out = provision.succeed("kanidm group get service1-access")
|
||||
assert_contains(out, "name: service1-access")
|
||||
|
||||
out = provision.succeed("kanidm group get service1-admin")
|
||||
assert_contains(out, "name: service1-admin")
|
||||
|
||||
out = provision.succeed("kanidm system oauth2 get service1")
|
||||
assert_contains(out, "name: service1")
|
||||
assert_contains(out, "displayname: Service One (changed)")
|
||||
assert_contains(out, "oauth2_rs_origin: https://changed-one.example.com/")
|
||||
assert_contains(out, "oauth2_rs_origin_landing: https://changed-one.example.com/landing")
|
||||
assert_lacks(out, "oauth2_rs_scope_map")
|
||||
assert_lacks(out, "oauth2_rs_sup_scope_map")
|
||||
assert_lacks(out, "oauth2_rs_claim_map")
|
||||
|
||||
out = provision.succeed("kanidm system oauth2 show-basic-secret service1")
|
||||
assert_contains(out, "changed-very-strong-secret-for-service1")
|
||||
|
||||
out = provision.succeed("kanidm system oauth2 get service2")
|
||||
assert_contains(out, "name: service2")
|
||||
assert_contains(out, "displayname: Service Two (changed)")
|
||||
assert_contains(out, "oauth2_rs_origin: https://changed-two.example.com/")
|
||||
assert_contains(out, "oauth2_rs_origin_landing: https://changed-landing2.example.com/")
|
||||
assert_lacks(out, "oauth2_allow_insecure_client_disable_pkce: true")
|
||||
assert_lacks(out, "oauth2_prefer_short_username: true")
|
||||
|
||||
provision.succeed("kanidm logout -D idm_admin")
|
||||
|
||||
with subtest("Test Provisioning - removeEntities"):
|
||||
provision.succeed('${specialisations}/removeEntities/bin/switch-to-configuration test')
|
||||
provision_login("${provisionIdmAdminPassword}")
|
||||
|
||||
out = provision.succeed("kanidm group get testgroup1")
|
||||
assert_lacks(out, "name: testgroup1")
|
||||
|
||||
out = provision.succeed("kanidm group get supergroup1")
|
||||
assert_lacks(out, "name: supergroup1")
|
||||
|
||||
out = provision.succeed("kanidm person get testuser1")
|
||||
assert_lacks(out, "name: testuser1")
|
||||
|
||||
out = provision.succeed("kanidm person get testuser2")
|
||||
assert_lacks(out, "name: testuser2")
|
||||
|
||||
out = provision.succeed("kanidm group get service1-access")
|
||||
assert_lacks(out, "name: service1-access")
|
||||
|
||||
out = provision.succeed("kanidm group get service1-admin")
|
||||
assert_lacks(out, "name: service1-admin")
|
||||
|
||||
out = provision.succeed("kanidm system oauth2 get service1")
|
||||
assert_lacks(out, "name: service1")
|
||||
|
||||
out = provision.succeed("kanidm system oauth2 get service2")
|
||||
assert_lacks(out, "name: service2")
|
||||
|
||||
provision.succeed("kanidm logout -D idm_admin")
|
||||
'';
|
||||
}
|
||||
)
|
@ -9,9 +9,9 @@ import ./make-test-python.nix ({ pkgs, ... }:
|
||||
in
|
||||
{
|
||||
name = "kanidm";
|
||||
meta.maintainers = with pkgs.lib.maintainers; [ erictapen Flakebi ];
|
||||
meta.maintainers = with pkgs.lib.maintainers; [ erictapen Flakebi oddlama ];
|
||||
|
||||
nodes.server = { config, pkgs, lib, ... }: {
|
||||
nodes.server = { pkgs, ... }: {
|
||||
services.kanidm = {
|
||||
enableServer = true;
|
||||
serverSettings = {
|
||||
@ -34,7 +34,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
|
||||
environment.systemPackages = with pkgs; [ kanidm openldap ripgrep ];
|
||||
};
|
||||
|
||||
nodes.client = { pkgs, nodes, ... }: {
|
||||
nodes.client = { nodes, ... }: {
|
||||
services.kanidm = {
|
||||
enableClient = true;
|
||||
clientSettings = {
|
||||
@ -62,10 +62,10 @@ import ./make-test-python.nix ({ pkgs, ... }:
|
||||
(pkgs.lib.filterAttrsRecursive (_: v: v != null))
|
||||
nodes.server.services.kanidm.serverSettings;
|
||||
serverConfigFile = (pkgs.formats.toml { }).generate "server.toml" filteredConfig;
|
||||
|
||||
in
|
||||
''
|
||||
start_all()
|
||||
server.start()
|
||||
client.start()
|
||||
server.wait_for_unit("kanidm.service")
|
||||
client.systemctl("start network-online.target")
|
||||
client.wait_for_unit("network-online.target")
|
||||
@ -122,5 +122,8 @@ import ./make-test-python.nix ({ pkgs, ... }:
|
||||
client.wait_until_succeeds("systemctl is-active user@$(id -u testuser).service")
|
||||
client.send_chars("touch done\n")
|
||||
client.wait_for_file("/home/testuser@${serverDomain}/done")
|
||||
|
||||
server.shutdown()
|
||||
client.shutdown()
|
||||
'';
|
||||
})
|
||||
|
29
pkgs/by-name/ka/kanidm-provision/package.nix
Normal file
29
pkgs/by-name/ka/kanidm-provision/package.nix
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
lib,
|
||||
rustPlatform,
|
||||
fetchFromGitHub,
|
||||
}:
|
||||
rustPlatform.buildRustPackage rec {
|
||||
pname = "kanidm-provision";
|
||||
version = "1.1.1";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "oddlama";
|
||||
repo = "kanidm-provision";
|
||||
rev = "v${version}";
|
||||
hash = "sha256-tX24cszmWu7kB5Eoa3OrPqU1bayD62OpAV12U0ayoEo=";
|
||||
};
|
||||
|
||||
cargoHash = "sha256-Ok8A47z5Z3QW4teql/4RyDlox/nrhkdA6IN/qJm13bM=";
|
||||
|
||||
meta = with lib; {
|
||||
description = "A small utility to help with kanidm provisioning";
|
||||
homepage = "https://github.com/oddlama/kanidm-provision";
|
||||
license = with licenses; [
|
||||
asl20
|
||||
mit
|
||||
];
|
||||
maintainers = with maintainers; [ oddlama ];
|
||||
mainProgram = "kanidm-provision";
|
||||
};
|
||||
}
|
@ -13,6 +13,14 @@
|
||||
, pam
|
||||
, bashInteractive
|
||||
, rust-jemalloc-sys
|
||||
, kanidm
|
||||
# If this is enabled, kanidm will be built with two patches allowing both
|
||||
# oauth2 basic secrets and admin credentials to be provisioned.
|
||||
# This is NOT officially supported (and will likely never be),
|
||||
# see https://github.com/kanidm/kanidm/issues/1747.
|
||||
# Please report any provisioning-related errors to
|
||||
# https://github.com/oddlama/kanidm-provision/issues/ instead.
|
||||
, enableSecretProvisioning ? false
|
||||
}:
|
||||
|
||||
let
|
||||
@ -33,6 +41,11 @@ rustPlatform.buildRustPackage rec {
|
||||
|
||||
KANIDM_BUILD_PROFILE = "release_nixos_${arch}";
|
||||
|
||||
patches = lib.optionals enableSecretProvisioning [
|
||||
./patches/oauth2-basic-secret-modify.patch
|
||||
./patches/recover-account.patch
|
||||
];
|
||||
|
||||
postPatch =
|
||||
let
|
||||
format = (formats.toml { }).generate "${KANIDM_BUILD_PROFILE}.toml";
|
||||
@ -94,10 +107,12 @@ rustPlatform.buildRustPackage rec {
|
||||
|
||||
passthru = {
|
||||
tests = {
|
||||
inherit (nixosTests) kanidm;
|
||||
inherit (nixosTests) kanidm kanidm-provisioning;
|
||||
};
|
||||
|
||||
updateScript = nix-update-script { };
|
||||
inherit enableSecretProvisioning;
|
||||
withSecretProvisioning = kanidm.override { enableSecretProvisioning = true; };
|
||||
};
|
||||
|
||||
meta = with lib; {
|
||||
|
303
pkgs/by-name/ka/kanidm/patches/oauth2-basic-secret-modify.patch
Normal file
303
pkgs/by-name/ka/kanidm/patches/oauth2-basic-secret-modify.patch
Normal file
@ -0,0 +1,303 @@
|
||||
From 44dfbc2b9dccce86c7d7e7b54db4c989344b8c56 Mon Sep 17 00:00:00 2001
|
||||
From: oddlama <oddlama@oddlama.org>
|
||||
Date: Mon, 12 Aug 2024 23:17:25 +0200
|
||||
Subject: [PATCH 1/2] oauth2 basic secret modify
|
||||
|
||||
---
|
||||
server/core/src/actors/v1_write.rs | 42 ++++++++++++++++++++++++++++++
|
||||
server/core/src/https/v1.rs | 6 ++++-
|
||||
server/core/src/https/v1_oauth2.rs | 29 +++++++++++++++++++++
|
||||
server/lib/src/constants/acp.rs | 6 +++++
|
||||
4 files changed, 82 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/server/core/src/actors/v1_write.rs b/server/core/src/actors/v1_write.rs
|
||||
index e00a969fb..1cacc67b8 100644
|
||||
--- a/server/core/src/actors/v1_write.rs
|
||||
+++ b/server/core/src/actors/v1_write.rs
|
||||
@@ -315,20 +315,62 @@ impl QueryServerWriteV1 {
|
||||
};
|
||||
|
||||
trace!(?del, "Begin delete event");
|
||||
|
||||
idms_prox_write
|
||||
.qs_write
|
||||
.delete(&del)
|
||||
.and_then(|_| idms_prox_write.commit().map(|_| ()))
|
||||
}
|
||||
|
||||
+ #[instrument(
|
||||
+ level = "info",
|
||||
+ skip_all,
|
||||
+ fields(uuid = ?eventid)
|
||||
+ )]
|
||||
+ pub async fn handle_oauth2_basic_secret_write(
|
||||
+ &self,
|
||||
+ client_auth_info: ClientAuthInfo,
|
||||
+ filter: Filter<FilterInvalid>,
|
||||
+ new_secret: String,
|
||||
+ eventid: Uuid,
|
||||
+ ) -> Result<(), OperationError> {
|
||||
+ // Given a protoEntry, turn this into a modification set.
|
||||
+ let ct = duration_from_epoch_now();
|
||||
+ let mut idms_prox_write = self.idms.proxy_write(ct).await;
|
||||
+ let ident = idms_prox_write
|
||||
+ .validate_client_auth_info_to_ident(client_auth_info, ct)
|
||||
+ .map_err(|e| {
|
||||
+ admin_error!(err = ?e, "Invalid identity");
|
||||
+ e
|
||||
+ })?;
|
||||
+
|
||||
+ let modlist = ModifyList::new_purge_and_set(
|
||||
+ Attribute::OAuth2RsBasicSecret,
|
||||
+ Value::SecretValue(new_secret),
|
||||
+ );
|
||||
+
|
||||
+ let mdf =
|
||||
+ ModifyEvent::from_internal_parts(ident, &modlist, &filter, &idms_prox_write.qs_write)
|
||||
+ .map_err(|e| {
|
||||
+ admin_error!(err = ?e, "Failed to begin modify during handle_oauth2_basic_secret_write");
|
||||
+ e
|
||||
+ })?;
|
||||
+
|
||||
+ trace!(?mdf, "Begin modify event");
|
||||
+
|
||||
+ idms_prox_write
|
||||
+ .qs_write
|
||||
+ .modify(&mdf)
|
||||
+ .and_then(|_| idms_prox_write.commit())
|
||||
+ }
|
||||
+
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_reviverecycled(
|
||||
&self,
|
||||
client_auth_info: ClientAuthInfo,
|
||||
filter: Filter<FilterInvalid>,
|
||||
eventid: Uuid,
|
||||
diff --git a/server/core/src/https/v1.rs b/server/core/src/https/v1.rs
|
||||
index 8aba83bb2..f1f815026 100644
|
||||
--- a/server/core/src/https/v1.rs
|
||||
+++ b/server/core/src/https/v1.rs
|
||||
@@ -1,17 +1,17 @@
|
||||
//! The V1 API things!
|
||||
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::{HeaderMap, HeaderValue};
|
||||
use axum::middleware::from_fn;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
-use axum::routing::{delete, get, post, put};
|
||||
+use axum::routing::{delete, get, post, put, patch};
|
||||
use axum::{Extension, Json, Router};
|
||||
use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
|
||||
use compact_jwt::{Jwk, Jws, JwsSigner};
|
||||
use kanidm_proto::constants::uri::V1_AUTH_VALID;
|
||||
use std::net::IpAddr;
|
||||
use uuid::Uuid;
|
||||
|
||||
use kanidm_proto::internal::{
|
||||
ApiToken, AppLink, CUIntentToken, CURequest, CUSessionToken, CUStatus, CreateRequest,
|
||||
CredentialStatus, DeleteRequest, IdentifyUserRequest, IdentifyUserResponse, ModifyRequest,
|
||||
@@ -3119,20 +3119,24 @@ pub(crate) fn route_setup(state: ServerState) -> Router<ServerState> {
|
||||
)
|
||||
.route(
|
||||
"/v1/oauth2/:rs_name/_image",
|
||||
post(super::v1_oauth2::oauth2_id_image_post)
|
||||
.delete(super::v1_oauth2::oauth2_id_image_delete),
|
||||
)
|
||||
.route(
|
||||
"/v1/oauth2/:rs_name/_basic_secret",
|
||||
get(super::v1_oauth2::oauth2_id_get_basic_secret),
|
||||
)
|
||||
+ .route(
|
||||
+ "/v1/oauth2/:rs_name/_basic_secret",
|
||||
+ patch(super::v1_oauth2::oauth2_id_patch_basic_secret),
|
||||
+ )
|
||||
.route(
|
||||
"/v1/oauth2/:rs_name/_scopemap/:group",
|
||||
post(super::v1_oauth2::oauth2_id_scopemap_post)
|
||||
.delete(super::v1_oauth2::oauth2_id_scopemap_delete),
|
||||
)
|
||||
.route(
|
||||
"/v1/oauth2/:rs_name/_sup_scopemap/:group",
|
||||
post(super::v1_oauth2::oauth2_id_sup_scopemap_post)
|
||||
.delete(super::v1_oauth2::oauth2_id_sup_scopemap_delete),
|
||||
)
|
||||
diff --git a/server/core/src/https/v1_oauth2.rs b/server/core/src/https/v1_oauth2.rs
|
||||
index 5e481afab..a771aed04 100644
|
||||
--- a/server/core/src/https/v1_oauth2.rs
|
||||
+++ b/server/core/src/https/v1_oauth2.rs
|
||||
@@ -144,20 +144,49 @@ pub(crate) async fn oauth2_id_get_basic_secret(
|
||||
) -> Result<Json<Option<String>>, WebError> {
|
||||
let filter = oauth2_id(&rs_name);
|
||||
state
|
||||
.qe_r_ref
|
||||
.handle_oauth2_basic_secret_read(client_auth_info, filter, kopid.eventid)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
+#[utoipa::path(
|
||||
+ patch,
|
||||
+ path = "/v1/oauth2/{rs_name}/_basic_secret",
|
||||
+ request_body=ProtoEntry,
|
||||
+ responses(
|
||||
+ DefaultApiResponse,
|
||||
+ ),
|
||||
+ security(("token_jwt" = [])),
|
||||
+ tag = "v1/oauth2",
|
||||
+ operation_id = "oauth2_id_patch_basic_secret"
|
||||
+)]
|
||||
+/// Overwrite the basic secret for a given OAuth2 Resource Server.
|
||||
+#[instrument(level = "info", skip(state, new_secret))]
|
||||
+pub(crate) async fn oauth2_id_patch_basic_secret(
|
||||
+ State(state): State<ServerState>,
|
||||
+ Extension(kopid): Extension<KOpId>,
|
||||
+ VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||
+ Path(rs_name): Path<String>,
|
||||
+ Json(new_secret): Json<String>,
|
||||
+) -> Result<Json<()>, WebError> {
|
||||
+ let filter = oauth2_id(&rs_name);
|
||||
+ state
|
||||
+ .qe_w_ref
|
||||
+ .handle_oauth2_basic_secret_write(client_auth_info, filter, new_secret, kopid.eventid)
|
||||
+ .await
|
||||
+ .map(Json::from)
|
||||
+ .map_err(WebError::from)
|
||||
+}
|
||||
+
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/v1/oauth2/{rs_name}",
|
||||
request_body=ProtoEntry,
|
||||
responses(
|
||||
DefaultApiResponse,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/oauth2",
|
||||
operation_id = "oauth2_id_patch"
|
||||
diff --git a/server/lib/src/constants/acp.rs b/server/lib/src/constants/acp.rs
|
||||
index f3409649d..42e407b7d 100644
|
||||
--- a/server/lib/src/constants/acp.rs
|
||||
+++ b/server/lib/src/constants/acp.rs
|
||||
@@ -645,34 +645,36 @@ lazy_static! {
|
||||
Attribute::Image,
|
||||
],
|
||||
modify_present_attrs: vec![
|
||||
Attribute::Description,
|
||||
Attribute::DisplayName,
|
||||
Attribute::OAuth2RsName,
|
||||
Attribute::OAuth2RsOrigin,
|
||||
Attribute::OAuth2RsOriginLanding,
|
||||
Attribute::OAuth2RsSupScopeMap,
|
||||
Attribute::OAuth2RsScopeMap,
|
||||
+ Attribute::OAuth2RsBasicSecret,
|
||||
Attribute::OAuth2AllowInsecureClientDisablePkce,
|
||||
Attribute::OAuth2JwtLegacyCryptoEnable,
|
||||
Attribute::OAuth2PreferShortUsername,
|
||||
Attribute::Image,
|
||||
],
|
||||
create_attrs: vec![
|
||||
Attribute::Class,
|
||||
Attribute::Description,
|
||||
Attribute::DisplayName,
|
||||
Attribute::OAuth2RsName,
|
||||
Attribute::OAuth2RsOrigin,
|
||||
Attribute::OAuth2RsOriginLanding,
|
||||
Attribute::OAuth2RsSupScopeMap,
|
||||
Attribute::OAuth2RsScopeMap,
|
||||
+ Attribute::OAuth2RsBasicSecret,
|
||||
Attribute::OAuth2AllowInsecureClientDisablePkce,
|
||||
Attribute::OAuth2JwtLegacyCryptoEnable,
|
||||
Attribute::OAuth2PreferShortUsername,
|
||||
Attribute::Image,
|
||||
],
|
||||
create_classes: vec![
|
||||
EntryClass::Object,
|
||||
EntryClass::OAuth2ResourceServer,
|
||||
EntryClass::OAuth2ResourceServerBasic,
|
||||
EntryClass::OAuth2ResourceServerPublic,
|
||||
@@ -739,36 +741,38 @@ lazy_static! {
|
||||
Attribute::Image,
|
||||
],
|
||||
modify_present_attrs: vec![
|
||||
Attribute::Description,
|
||||
Attribute::DisplayName,
|
||||
Attribute::OAuth2RsName,
|
||||
Attribute::OAuth2RsOrigin,
|
||||
Attribute::OAuth2RsOriginLanding,
|
||||
Attribute::OAuth2RsSupScopeMap,
|
||||
Attribute::OAuth2RsScopeMap,
|
||||
+ Attribute::OAuth2RsBasicSecret,
|
||||
Attribute::OAuth2AllowInsecureClientDisablePkce,
|
||||
Attribute::OAuth2JwtLegacyCryptoEnable,
|
||||
Attribute::OAuth2PreferShortUsername,
|
||||
Attribute::OAuth2AllowLocalhostRedirect,
|
||||
Attribute::OAuth2RsClaimMap,
|
||||
Attribute::Image,
|
||||
],
|
||||
create_attrs: vec![
|
||||
Attribute::Class,
|
||||
Attribute::Description,
|
||||
Attribute::DisplayName,
|
||||
Attribute::OAuth2RsName,
|
||||
Attribute::OAuth2RsOrigin,
|
||||
Attribute::OAuth2RsOriginLanding,
|
||||
Attribute::OAuth2RsSupScopeMap,
|
||||
Attribute::OAuth2RsScopeMap,
|
||||
+ Attribute::OAuth2RsBasicSecret,
|
||||
Attribute::OAuth2AllowInsecureClientDisablePkce,
|
||||
Attribute::OAuth2JwtLegacyCryptoEnable,
|
||||
Attribute::OAuth2PreferShortUsername,
|
||||
Attribute::OAuth2AllowLocalhostRedirect,
|
||||
Attribute::OAuth2RsClaimMap,
|
||||
Attribute::Image,
|
||||
],
|
||||
create_classes: vec![
|
||||
EntryClass::Object,
|
||||
EntryClass::OAuth2ResourceServer,
|
||||
@@ -840,36 +844,38 @@ lazy_static! {
|
||||
Attribute::Image,
|
||||
],
|
||||
modify_present_attrs: vec![
|
||||
Attribute::Description,
|
||||
Attribute::DisplayName,
|
||||
Attribute::Name,
|
||||
Attribute::OAuth2RsOrigin,
|
||||
Attribute::OAuth2RsOriginLanding,
|
||||
Attribute::OAuth2RsSupScopeMap,
|
||||
Attribute::OAuth2RsScopeMap,
|
||||
+ Attribute::OAuth2RsBasicSecret,
|
||||
Attribute::OAuth2AllowInsecureClientDisablePkce,
|
||||
Attribute::OAuth2JwtLegacyCryptoEnable,
|
||||
Attribute::OAuth2PreferShortUsername,
|
||||
Attribute::OAuth2AllowLocalhostRedirect,
|
||||
Attribute::OAuth2RsClaimMap,
|
||||
Attribute::Image,
|
||||
],
|
||||
create_attrs: vec![
|
||||
Attribute::Class,
|
||||
Attribute::Description,
|
||||
Attribute::Name,
|
||||
Attribute::OAuth2RsName,
|
||||
Attribute::OAuth2RsOrigin,
|
||||
Attribute::OAuth2RsOriginLanding,
|
||||
Attribute::OAuth2RsSupScopeMap,
|
||||
Attribute::OAuth2RsScopeMap,
|
||||
+ Attribute::OAuth2RsBasicSecret,
|
||||
Attribute::OAuth2AllowInsecureClientDisablePkce,
|
||||
Attribute::OAuth2JwtLegacyCryptoEnable,
|
||||
Attribute::OAuth2PreferShortUsername,
|
||||
Attribute::OAuth2AllowLocalhostRedirect,
|
||||
Attribute::OAuth2RsClaimMap,
|
||||
Attribute::Image,
|
||||
],
|
||||
create_classes: vec![
|
||||
EntryClass::Object,
|
||||
EntryClass::Account,
|
||||
--
|
||||
2.45.2
|
||||
|
173
pkgs/by-name/ka/kanidm/patches/recover-account.patch
Normal file
173
pkgs/by-name/ka/kanidm/patches/recover-account.patch
Normal file
@ -0,0 +1,173 @@
|
||||
From cc8269489b56755714f07eee4671f8aa2659c014 Mon Sep 17 00:00:00 2001
|
||||
From: oddlama <oddlama@oddlama.org>
|
||||
Date: Mon, 12 Aug 2024 23:17:42 +0200
|
||||
Subject: [PATCH 2/2] recover account
|
||||
|
||||
---
|
||||
server/core/src/actors/internal.rs | 3 ++-
|
||||
server/core/src/admin.rs | 6 +++---
|
||||
server/daemon/src/main.rs | 14 +++++++++++++-
|
||||
server/daemon/src/opt.rs | 4 ++++
|
||||
4 files changed, 22 insertions(+), 5 deletions(-)
|
||||
|
||||
diff --git a/server/core/src/actors/internal.rs b/server/core/src/actors/internal.rs
|
||||
index 40c18777f..40d553b40 100644
|
||||
--- a/server/core/src/actors/internal.rs
|
||||
+++ b/server/core/src/actors/internal.rs
|
||||
@@ -153,25 +153,26 @@ impl QueryServerWriteV1 {
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip(self, eventid),
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub(crate) async fn handle_admin_recover_account(
|
||||
&self,
|
||||
name: String,
|
||||
+ password: Option<String>,
|
||||
eventid: Uuid,
|
||||
) -> Result<String, OperationError> {
|
||||
let ct = duration_from_epoch_now();
|
||||
let mut idms_prox_write = self.idms.proxy_write(ct).await;
|
||||
- let pw = idms_prox_write.recover_account(name.as_str(), None)?;
|
||||
+ let pw = idms_prox_write.recover_account(name.as_str(), password.as_deref())?;
|
||||
|
||||
idms_prox_write.commit().map(|()| pw)
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub(crate) async fn handle_domain_raise(&self, eventid: Uuid) -> Result<u32, OperationError> {
|
||||
diff --git a/server/core/src/admin.rs b/server/core/src/admin.rs
|
||||
index 90ccb1927..85e31ddef 100644
|
||||
--- a/server/core/src/admin.rs
|
||||
+++ b/server/core/src/admin.rs
|
||||
@@ -17,21 +17,21 @@ use tokio_util::codec::{Decoder, Encoder, Framed};
|
||||
use tracing::{span, Instrument, Level};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub use kanidm_proto::internal::{
|
||||
DomainInfo as ProtoDomainInfo, DomainUpgradeCheckReport as ProtoDomainUpgradeCheckReport,
|
||||
DomainUpgradeCheckStatus as ProtoDomainUpgradeCheckStatus,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum AdminTaskRequest {
|
||||
- RecoverAccount { name: String },
|
||||
+ RecoverAccount { name: String, password: Option<String> },
|
||||
ShowReplicationCertificate,
|
||||
RenewReplicationCertificate,
|
||||
RefreshReplicationConsumer,
|
||||
DomainShow,
|
||||
DomainUpgradeCheck,
|
||||
DomainRaise,
|
||||
DomainRemigrate { level: Option<u32> },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@@ -302,22 +302,22 @@ async fn handle_client(
|
||||
let mut reqs = Framed::new(sock, ServerCodec);
|
||||
|
||||
trace!("Waiting for requests ...");
|
||||
while let Some(Ok(req)) = reqs.next().await {
|
||||
// Setup the logging span
|
||||
let eventid = Uuid::new_v4();
|
||||
let nspan = span!(Level::INFO, "handle_admin_client_request", uuid = ?eventid);
|
||||
|
||||
let resp = async {
|
||||
match req {
|
||||
- AdminTaskRequest::RecoverAccount { name } => {
|
||||
- match server_rw.handle_admin_recover_account(name, eventid).await {
|
||||
+ AdminTaskRequest::RecoverAccount { name, password } => {
|
||||
+ match server_rw.handle_admin_recover_account(name, password, eventid).await {
|
||||
Ok(password) => AdminTaskResponse::RecoverAccount { password },
|
||||
Err(e) => {
|
||||
error!(err = ?e, "error during recover-account");
|
||||
AdminTaskResponse::Error
|
||||
}
|
||||
}
|
||||
}
|
||||
AdminTaskRequest::ShowReplicationCertificate => match repl_ctrl_tx.as_mut() {
|
||||
Some(ctrl_tx) => show_replication_certificate(ctrl_tx).await,
|
||||
None => {
|
||||
diff --git a/server/daemon/src/main.rs b/server/daemon/src/main.rs
|
||||
index 577995615..a967928c9 100644
|
||||
--- a/server/daemon/src/main.rs
|
||||
+++ b/server/daemon/src/main.rs
|
||||
@@ -894,27 +894,39 @@ async fn kanidm_main(
|
||||
} else {
|
||||
let output_mode: ConsoleOutputMode = commonopts.output_mode.to_owned().into();
|
||||
submit_admin_req(
|
||||
config.adminbindpath.as_str(),
|
||||
AdminTaskRequest::RefreshReplicationConsumer,
|
||||
output_mode,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
- KanidmdOpt::RecoverAccount { name, commonopts } => {
|
||||
+ KanidmdOpt::RecoverAccount { name, from_environment, commonopts } => {
|
||||
info!("Running account recovery ...");
|
||||
let output_mode: ConsoleOutputMode = commonopts.output_mode.to_owned().into();
|
||||
+ let password = if *from_environment {
|
||||
+ match std::env::var("KANIDM_RECOVER_ACCOUNT_PASSWORD") {
|
||||
+ Ok(val) => Some(val),
|
||||
+ _ => {
|
||||
+ error!("Environment variable KANIDM_RECOVER_ACCOUNT_PASSWORD not set");
|
||||
+ return ExitCode::FAILURE;
|
||||
+ }
|
||||
+ }
|
||||
+ } else {
|
||||
+ None
|
||||
+ };
|
||||
submit_admin_req(
|
||||
config.adminbindpath.as_str(),
|
||||
AdminTaskRequest::RecoverAccount {
|
||||
name: name.to_owned(),
|
||||
+ password,
|
||||
},
|
||||
output_mode,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
KanidmdOpt::Database {
|
||||
commands: DbCommands::Reindex(_copt),
|
||||
} => {
|
||||
info!("Running in reindex mode ...");
|
||||
reindex_server_core(&config).await;
|
||||
diff --git a/server/daemon/src/opt.rs b/server/daemon/src/opt.rs
|
||||
index f1b45a5b3..9c013e32e 100644
|
||||
--- a/server/daemon/src/opt.rs
|
||||
+++ b/server/daemon/src/opt.rs
|
||||
@@ -229,20 +229,24 @@ enum KanidmdOpt {
|
||||
/// Create a self-signed ca and tls certificate in the locations listed from the
|
||||
/// configuration. These certificates should *not* be used in production, they
|
||||
/// are for testing and evaluation only!
|
||||
CertGenerate(CommonOpt),
|
||||
#[clap(name = "recover-account")]
|
||||
/// Recover an account's password
|
||||
RecoverAccount {
|
||||
#[clap(value_parser)]
|
||||
/// The account name to recover credentials for.
|
||||
name: String,
|
||||
+ /// Use the password given in the environment variable
|
||||
+ /// `KANIDM_RECOVER_ACCOUNT_PASSWORD` instead of generating one.
|
||||
+ #[clap(long = "from-environment")]
|
||||
+ from_environment: bool,
|
||||
#[clap(flatten)]
|
||||
commonopts: CommonOpt,
|
||||
},
|
||||
/// Display this server's replication certificate
|
||||
ShowReplicationCertificate {
|
||||
#[clap(flatten)]
|
||||
commonopts: CommonOpt,
|
||||
},
|
||||
/// Renew this server's replication certificate
|
||||
RenewReplicationCertificate {
|
||||
--
|
||||
2.45.2
|
||||
|
Loading…
Reference in New Issue
Block a user