Skip to content

Commit

Permalink
feat: support LDAP rename
Browse files Browse the repository at this point in the history
Changes:

- add new env variable USER_DOMAIN
- ns8-join: add dynamic baseDN for OpenLDAP
- ui: add new field for entering OpenLDAP user domain

NethServer/dev#7103

Co-authored-by: Giacomo Sanchietti <[email protected]>
Co-authored-by: Davide Principi <[email protected]>
  • Loading branch information
3 people committed Nov 25, 2024
1 parent c6fbba4 commit 865021d
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 21 deletions.
12 changes: 10 additions & 2 deletions api/connection/read
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,25 @@
import sys
import subprocess
import simplejson

import os

def get_config():
# ns8 config
bash_command = "/sbin/e-smith/config getjson ns8"
process = subprocess.Popen(bash_command.split(), stdout=subprocess.PIPE)
output, error = process.communicate()
ns8_config = simplejson.loads(output)
props = {"ns8": ns8_config, "slapd": {"props":{"status": "disabled"}}}

return {"ns8": ns8_config}
# slapd config
if os.path.isfile('/etc/e-smith/db/configuration/defaults/slapd/type'):
bash_command = "/sbin/e-smith/config getjson slapd"
process = subprocess.Popen(bash_command.split(), stdout=subprocess.PIPE)
output, error = process.communicate()
slapd_config = simplejson.loads(output)
props["slapd"] = slapd_config

return props

try:
config = get_config()
Expand Down
6 changes: 4 additions & 2 deletions api/connection/update
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ host=$(echo $data | jq -r '.Host')
user=$(echo $data | jq -r '.User')
password=$(echo $data | jq -r '.Password')
tls_verify=$(echo $data | jq -r '.TLSVerify')
# user domain used to rename the directory.nh to another baseDN
ldap_user_domain=$(echo $data | jq -r '.LdapUserDomain')

if [[ "$action" == "login" ]]; then
# execute ns8-join
Expand All @@ -38,9 +40,9 @@ if [[ "$action" == "login" ]]; then
trap 'rm -f $tmp_output' EXIT
echo "=========== Join cluster" $(date -R) >>/var/log/ns8-migration.log
if [ "$tls_verify" = "disabled" ]; then
/usr/sbin/ns8-join --no-tlsverify "$host" "$user" "$password" &>"${tmp_output}"
/usr/sbin/ns8-join --no-tlsverify "$host" "$user" "$password" "$ldap_user_domain" &>"${tmp_output}"
else
/usr/sbin/ns8-join "$host" "$user" "$password" &>"${tmp_output}"
/usr/sbin/ns8-join "$host" "$user" "$password" "$ldap_user_domain" &>"${tmp_output}"
fi

if [ "$?" -gt 0 ]; then
Expand Down
26 changes: 26 additions & 0 deletions api/connection/validate
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,25 @@

import sys
import simplejson
import re


def invalid_attribute(parameter, error):
return {"parameter": parameter, "error": error, "value": ""}

def is_valid_fqdn(domain):
# Regex breakdown:
# - ^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+ # Domain labels
# - [a-zA-Z0-9]{2,63}$ # TLD with 2-63 characters
fqdn_pattern = r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9]{2,63}$'

# Check overall domain length
if not domain or len(domain) > 255:
return False

# Validate using regex
return re.match(fqdn_pattern, domain) is not None


input_json = simplejson.load(sys.stdin)
invalid_attributes = []
Expand All @@ -36,11 +50,13 @@ host_p = 'Host'
user_p = 'User'
password_p = 'Password'
tls_verify_p = 'TLSVerify'
ldap_user_domain_p = 'LdapUserDomain'

host = ''
user = ''
password = ''
tls_verify = ''
ldap_user_domain = ''

# action

Expand Down Expand Up @@ -76,6 +92,16 @@ else:
if tls_verify not in ['enabled', 'disabled']:
invalid_attributes.append(invalid_attribute(tls_verify_p, "invalid"))

# ldap user domain
if (ldap_user_domain_p not in input_json) or (not input_json[ldap_user_domain_p]):
invalid_attributes.append(invalid_attribute(ldap_user_domain_p, "empty"))
else:
ldap_user_domain = input_json[ldap_user_domain_p]

# check if the domain is a valid domain
if not is_valid_fqdn(ldap_user_domain):
invalid_attributes.append(invalid_attribute(ldap_user_domain_p, "invalid"))

# output
success = len(invalid_attributes) == 0

Expand Down
9 changes: 7 additions & 2 deletions root/usr/sbin/ns8-join
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ parser = argparse.ArgumentParser()
parser.add_argument('host')
parser.add_argument('username', default="admin")
parser.add_argument('password', default="Nethesis,1234")
# user domain used to rename the directory.nh to another baseDN
parser.add_argument('user_domain', default="")
parser.add_argument('--no-tlsverify', dest='tlsverify', action='store_false', default=True)

args = parser.parse_args()
Expand Down Expand Up @@ -248,8 +250,11 @@ if account_provider_config['isAD'] == '1':
subprocess.run(['/usr/sbin/ns8-leave'])
sys.exit(1)
elif account_provider_config['isLdap'] == '1' and '127.0.0.1' in account_provider_config['LdapURI']:
# Configure OpenLDAP as account provider of an external user domain:
account_provider_domain = "directory.nh"
# Configure OpenLDAP as account provider of an external user domain: (retrieve the baseDN from the UI, directory.nh is obsoleted)
if not args.user_domain:
print("ns8-join: user_domain is required for OpenLDAP account provider", file=sys.stderr)
sys.exit(1)
account_provider_domain = args.user_domain.lower()
account_provider_external = ""
add_external_domain_request = {
"domain": account_provider_domain,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@

set -e

fqdn=${USER_DOMAIN:?}
ldap_suffix=dc=$(echo "$fqdn" | sed 's/\./,dc=/g')
# Extract the hostname (part before the first dot)
host=${USER_DOMAIN%%.*}
# Extract the domain (part after the first dot)
domain=${USER_DOMAIN#*.}

ldapservice_password=$(< /var/lib/nethserver/secrets/ldapservice)
(
umask 077
Expand All @@ -32,8 +39,8 @@ ldapservice_password=$(< /var/lib/nethserver/secrets/ldapservice)
cat - >import.env <<EOF
LDAP_SVCUSER=ldapservice
LDAP_SVCPASS=${ldapservice_password}
LDAP_DOMAIN=directory.nh
LDAP_SUFFIX=dc=directory,dc=nh
LDAP_DOMAIN=${fqdn}
LDAP_SUFFIX=${ldap_suffix}
EOF

# Generate .ldif data dump
Expand All @@ -43,6 +50,10 @@ EOF
# NS8 apps require a displayName attribute is set. Copy gecos attribute
# value to displayName, assuming it is not already present.
sed -i '/^gecos: / { h ; s/^gecos:/displayName:/ ; G }' dump-mdb0.ldif
# replace dc=directory,dc=nh by ldap_suffix
sed -i "s/dc=directory,dc=nh/${ldap_suffix}/g" dump-mdb0.ldif
# replace dc: directory by dc: host
sed -i "s/^dc: directory/dc: $host/" dump-mdb0.ldif

# Send import.env
rsync -i import.env "${RSYNC_ENDPOINT:?}"/data/state/import.env
Expand All @@ -58,7 +69,7 @@ if [[ "${MIGRATE_ACTION}" != "finish" ]]; then
fi

# Remove temporary external user domain
ns8-action --attach cluster remove-external-domain "$(printf '{"domain":"%s"}' "directory.nh")" || :
ns8-action --attach cluster remove-external-domain "$(printf '{"domain":"%s"}' $fqdn)" || :

# Commit DC migration
rsync -v "${RSYNC_ENDPOINT}"/terminate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Host=$(/sbin/e-smith/config get DomainName)
# Search for Samba or LDAP domain
domain=$(/sbin/e-smith/config getprop sssd Realm | tr '[:upper:]' '[:lower:]')
if [ -z "$domain" ]; then
domain="directory.nh"
domain=${USER_DOMAIN:?}
fi

# we find admin users from jabberadmins group
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ ns8-action --attach wait "${IMPORT_TASK_ID}"
# Search for Samba or LDAP domain
domain=$(/sbin/e-smith/config getprop sssd Realm | tr '[:upper:]' '[:lower:]')
if [ -z "$domain" ]; then
domain="directory.nh"
domain=${USER_DOMAIN:?}
fi

if [ -z "${host}" ]; then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ ns8-action --attach wait "${IMPORT_TASK_ID}"
# Search for Samba or LDAP domain
domain=$(/sbin/e-smith/config getprop sssd Realm | tr '[:upper:]' '[:lower:]')
if [ -z "$domain" ]; then
domain="directory.nh"
domain=${USER_DOMAIN:?}
fi

# we find admin users
Expand Down
8 changes: 6 additions & 2 deletions ui/public/i18n/language.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@
"no_skip": "Enable migration",
"error_on_skip": "There was an error when changing the skip flag.",
"app_migrated_with_ad": "This app is migrated together with Local Active Directory app",
"enable_forge_sogo": "To successfully migrate SOGo, ensure NethForge repository is enabled under NS8 Settings"
"enable_forge_sogo": "To successfully migrate SOGo, ensure NethForge repository is enabled under NS8 Settings",
"ldap_user_domain_description": "This machine uses a local OpenLDAP account provider. You must choose a new name for the domain, something like ldap.domain.tld. The name must be unique inside the NethServer 8 cluster",
"ldap_user_domain": "LDAP user domain"
},
"validation": {
"leader_node_empty": "Leader node is required",
Expand All @@ -109,7 +111,9 @@
"virtual_host_empty": "Virtual host is required",
"virtualhost_cannot_be_the_same": "Virtual host cannot be the same",
"ad_ip_address_empty": "Active Directory IP address is required",
"admin_password_not_allowed":"The `|` character is not allowed"
"admin_password_not_allowed":"The `|` character is not allowed",
"ldap_user_domain_empty": "LDAP user domain is required",
"ldap_user_domain_invalid": "Invalid LDAP user domain"
},
"docs": {},
"are_you_sure": "Are you sure",
Expand Down
55 changes: 48 additions & 7 deletions ui/src/views/Dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,35 @@
/>
</div>
</div>
<!-- ldap_userdomain -->
<div v-if="isLdapEnabled && accountProviderConfig.location == 'local' && accountProviderConfig.type == 'ldap'">
<div
class="page-description"
>
{{
$t("dashboard.ldap_user_domain_description")
}}
</div>
</div>
<div v-if="isLdapEnabled && accountProviderConfig.location == 'local' && accountProviderConfig.type == 'ldap'">
<div :class="['form-group', { 'has-error': error.ldapUserDomain }]">
<label class="col-sm-2 control-label" for="ldap-userdomain">{{
$t("dashboard.ldap_user_domain")
}}</label>
<div class="col-sm-5">
<input
type="text"
v-model="config.ldapUserDomain"
id="ldap-userdomain"
ref="ldapUserDomain"
class="form-control"
/>
<span v-if="error.ldapUserDomain" class="help-block">{{
$t("validation.ldap_user_domain_" + error.ldapUserDomain)
}}</span>
</div>
</div>
</div>
<!-- connect button -->
<div class="form-group">
<label class="col-sm-2 control-label">
Expand Down Expand Up @@ -1088,8 +1117,10 @@ export default {
tlsVerify: false,
leaderNode: "",
adminUsername: "",
adminPassword: ""
adminPassword: "",
ldapUserDomain: "",
},
isLdapEnabled: false,
installedApps: [],
apps: [],
currentApp: null,
Expand Down Expand Up @@ -1143,7 +1174,8 @@ export default {
ctiVirtualHost: "",
sogoVirtualHost: "",
webtopVirtualHost: "",
userDomains: ""
userDomains: "",
ldapUserDomain: "",
}
};
},
Expand Down Expand Up @@ -1201,7 +1233,9 @@ export default {
}
},
mounted() {
this.connectionRead();
// get connection info we need to retrieve the account provider info first
// to check if it's local or remote and ldap or ad
this.getAccountProviderInfo();
},
methods: {
togglePassword() {
Expand Down Expand Up @@ -1442,6 +1476,7 @@ export default {
},
connectionRead() {
const context = this;
context.config.ldapUserDomain = "";
context.loading.connectionRead = true;
nethserver.exec(
["nethserver-ns8-migration/connection/read"],
Expand All @@ -1463,15 +1498,16 @@ export default {
},
connectionReadSuccess(output) {
const ns8Config = output.configuration.ns8.props;
const slapd = output.configuration.slapd.props;
this.config.isConnected = ns8Config.Host != "";
this.config.leaderNode = ns8Config.Host;
this.config.adminUsername = ns8Config.User;
this.config.adminPassword = ns8Config.Password;
this.config.tlsVerify = ns8Config.TLSVerify == "enabled";
this.loading.connectionRead = false;
this.isLdapEnabled = slapd.status === "enabled";
if (this.config.isConnected) {
this.getAccountProviderInfo();
this.listApplications();
} else {
this.$nextTick(() => {
this.$refs.leaderNode.focus();
Expand All @@ -1482,6 +1518,7 @@ export default {
this.error.leaderNode = "";
this.error.adminUsername = "";
this.error.adminPassword = "";
this.error.ldapUserDomain = "";
this.error.leaderNode = "";
this.loading.connectionUpdate = true;
this.error.connectionUpdate = "";
Expand All @@ -1492,7 +1529,8 @@ export default {
Host: this.config.leaderNode,
User: this.config.adminUsername,
Password: this.config.adminPassword,
TLSVerify: this.config.tlsVerify ? "enabled" : "disabled"
TLSVerify: this.config.tlsVerify ? "enabled" : "disabled",
LdapUserDomain: this.isLdapEnabled ? this.config.ldapUserDomain : "ad.provider.is.used" // dummy value to validate, not used with ad
};
const context = this;
Expand Down Expand Up @@ -1530,6 +1568,9 @@ export default {
} else if (param === "Password") {
this.error.adminPassword = attr.error;
this.$refs.adminPassword.focus();
} else if (param === "LdapUserDomain") {
this.error.ldapUserDomain = attr.error;
this.$refs.ldapUserDomain.focus();
}
}
},
Expand Down Expand Up @@ -1856,7 +1897,7 @@ export default {
accountProviderConfig.location = location;
context.accountProviderConfig = accountProviderConfig;
context.loading.accountProviderInfo = false;
context.listApplications();
context.connectionRead();
},
function (error) {
const errorMessage = context.$i18n.t(
Expand Down

0 comments on commit 865021d

Please sign in to comment.