Skip to content

Commit

Permalink
Merge pull request #2235 from FroggyFlox/Issue2232_564_SSSD_AD_LDAP
Browse files Browse the repository at this point in the history
Implement SSSD-based enrollment into Active Directory or LDAP domains. Fixes #2232
  • Loading branch information
phillxnet authored Jan 2, 2021
2 parents 26be2da + 4a722d9 commit 1223360
Show file tree
Hide file tree
Showing 9 changed files with 541 additions and 287 deletions.
167 changes: 43 additions & 124 deletions src/rockstor/smart_manager/views/active_directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,27 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""

import re
import socket
from tempfile import mkstemp
import shutil
from rest_framework.response import Response
from storageadmin.util import handle_exception
from django.db import transaction
from base_service import BaseServiceDetailView
from smart_manager.models import Service
from system.directory_services import (
update_nss,
sssd_update_ad,
join_domain,
domain_workgroup,
leave_domain,
)
from system.osi import run_command
from system.samba import update_global_config
from system.services import systemctl

import logging

logger = logging.getLogger(__name__)
NET = "/usr/bin/net"
REALM = "/usr/sbin/realm"


class ActiveDirectoryServiceView(BaseServiceDetailView):
Expand All @@ -52,9 +56,9 @@ def _resolve_check(domain, request):
socket.gethostbyname(domain)
except Exception as e:
e_msg = (
"Domain/Realm(%s) could not be resolved. Check "
"Domain/Realm({}) could not be resolved. Check "
"your DNS configuration and try again. "
"Lower level error: %s" % (domain, e.__str__())
"Lower level error: {}".format(domain, e.__str__())
)
handle_exception(Exception(e_msg), request)

Expand All @@ -70,91 +74,21 @@ def _validate_config(config, request):
if e_msg is not None:
handle_exception(Exception(e_msg), request)

@staticmethod
def _join_domain(config, method="winbind"):
domain = config.get("domain")
admin = config.get("username")
cmd = [NET, "ads", "join", "-U", admin]
if method == "sssd":
cmd = ["realm", "join", "-U", admin, domain]
return run_command(cmd, input=("%s\n" % config.get("password")))

@staticmethod
def _domain_workgroup(domain=None, method="winbind"):
cmd = [NET, "ads", "workgroup"]
if method == "sssd":
cmd = ["adcli", "info", domain]
o, e, rc = run_command(cmd)
match_str = "Workgroup:"
if method == "sssd":
match_str = "domain-short = "
for l in o:
l = l.strip()
if re.match(match_str, l) is not None:
return l.split(match_str)[1].strip()
raise Exception(
"Failed to retrieve Workgroup. out: %s err: %s rc: %d" % (o, e, rc)
)

@staticmethod
def _update_sssd(domain):
# add enumerate = True in sssd so user/group lists will be
# visible on the web-ui.
el = "enumerate = True\n"
fh, npath = mkstemp()
sssd_config = "/etc/sssd/sssd.conf"
with open(sssd_config) as sfo, open(npath, "w") as tfo:
domain_section = False
for line in sfo.readlines():
if domain_section is True:
if len(line.strip()) == 0 or line[0] == "[":
# empty line or new section without empty line before
# it.
tfo.write(el)
domain_section = False
elif re.match("\[domain/%s]" % domain, line) is not None:
domain_section = True
tfo.write(line)
if domain_section is True:
# reached end of file, also coinciding with end of domain
# section
tfo.write(el)
shutil.move(npath, sssd_config)
systemctl("sssd", "restart")

@staticmethod
def _leave_domain(config, method="winbind"):
pstr = "%s\n" % config.get("password")
cmd = [NET, "ads", "leave", "-U", config.get("username")]
if method == "sssd":
cmd = ["realm", "leave", config.get("domain")]
return run_command(cmd)
try:
return run_command(cmd, input=pstr)
except:
status_cmd = [NET, "ads", "status", "-U", config.get("username")]
o, e, rc = run_command(status_cmd, input=pstr, throw=False)
if rc != 0:
return logger.debug(
"Status shows not joined. out: %s err: %s rc: %d" % (o, e, rc)
)
raise

def _config(self, service, request):
try:
return self._get_config(service)
except Exception as e:
e_msg = (
"Missing configuration. Please configure the "
"service and try again. Exception: %s" % e.__str__()
"service and try again. Exception: {}".format(e.__str__())
)
handle_exception(Exception(e_msg), request)

@transaction.atomic
def post(self, request, command):

with self._handle_exception(request):
method = "winbind"
method = "sssd"
service = Service.objects.get(name="active-directory")
if command == "config":
config = request.data.get("config")
Expand All @@ -164,58 +98,31 @@ def post(self, request, command):
self._resolve_check(config.get("domain"), request)

# 2. realm discover check?
# @todo: phase our realm and just use net?
domain = config.get("domain")
try:
cmd = ["realm", "discover", "--name-only", domain]
cmd = [REALM, "discover", "--name-only", domain]
o, e, rc = run_command(cmd)
except Exception as e:
e_msg = (
"Failed to discover the given(%s) AD domain. "
"Error: %s" % (domain, e.__str__())
"Failed to discover the given({}) AD domain. "
"Error: {}".format(domain, e.__str__())
)
handle_exception(Exception(e_msg), request)

default_range = "10000 - 999999"
idmap_range = config.get("idmap_range", "10000 - 999999")
idmap_range = idmap_range.strip()
if len(idmap_range) > 0:
rfields = idmap_range.split()
if len(rfields) != 3:
raise Exception(
"Invalid idmap range. valid format is "
"two integers separated by a -. eg: "
"10000 - 999999"
)
try:
rlow = int(rfields[0].strip())
rhigh = int(rfields[2].strip())
except Exception as e:
raise Exception(
"Invalid idmap range. Numbers in the "
"range must be valid integers. "
"Error: %s." % e.__str__()
)
if rlow >= rhigh:
raise Exception(
"Invalid idmap range. Numbers in the "
"range must go from low to high. eg: "
"10000 - 999999"
)
else:
config["idmap_range"] = default_range
# Would be required only if method == "winbind":
# validate_idmap_range(config)

self._save_config(service, config)

elif command == "start":
config = self._config(service, request)
smbo = Service.objects.get(name="smb")
smb_config = self._get_config(smbo)
domain = config.get("domain")
# 1. make sure ntpd is running, or else, don't start.
self._ntp_check(request)
# 2. Name resolution check?
self._resolve_check(config.get("domain"), request)
# 3. Get current Samba config
smbo = Service.objects.get(name="smb")
smb_config = self._get_config(smbo)

if method == "winbind":
cmd = [
Expand Down Expand Up @@ -253,32 +160,44 @@ def post(self, request, command):
"--enablelocauthorize",
]
run_command(cmd)
config["workgroup"] = self._domain_workgroup(domain, method=method)
config["workgroup"] = domain_workgroup(domain, method=method)
self._save_config(service, config)
update_global_config(smb_config, config)
self._join_domain(config, method=method)
if method == "sssd" and config.get("enumerate") is True:
self._update_sssd(domain)
join_domain(config, method=method)

# Customize SSSD config
if (
method == "sssd"
and (config.get("enumerate") or config.get("case_sensitive"))
is True
):
sssd_update_ad(domain, config)

# Update nsswitch.conf
update_nss(["passwd", "group"], "sss")

if method == "winbind":
systemctl("winbind", "enable")
systemctl("winbind", "start")
systemctl("smb", "restart")
systemctl("nmb", "restart")
# The winbind service is required only for id mapping while
# accessing samba shares hosted by Rockstor
systemctl("winbind", "enable")
systemctl("winbind", "start")

elif command == "stop":
config = self._config(service, request)
try:
self._leave_domain(config, method=method)
leave_domain(config, method=method)
smbo = Service.objects.get(name="smb")
smb_config = self._get_config(smbo)
update_global_config(smb_config)
systemctl("smb", "restart")
systemctl("nmb", "restart")
systemctl("winbind", "stop")
systemctl("winbind", "disable")
update_nss(["passwd", "group"], "sss", remove=True)
except Exception as e:
e_msg = "Failed to leave AD domain(%s). Error: %s" % (
config.get("domain"),
e.__str__(),
e_msg = "Failed to leave AD domain({}). Error: {}".format(
config.get("domain"), e.__str__(),
)
handle_exception(Exception(e_msg), request)

Expand Down
86 changes: 79 additions & 7 deletions src/rockstor/smart_manager/views/ldap_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,51 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""

import socket

from rest_framework.response import Response
from os.path import dirname
from storageadmin.util import handle_exception
from system.services import toggle_auth_service
from django.db import transaction
from base_service import BaseServiceDetailView
from smart_manager.models import Service

import logging

from system.directory_services import (
update_nss,
sssd_add_ldap,
sssd_remove_ldap,
validate_tls_cert,
)
from system.services import systemctl

logger = logging.getLogger(__name__)


class LdapServiceView(BaseServiceDetailView):
@staticmethod
def _resolve_check(server, request):
try:
socket.gethostbyname(server)
except Exception as e:
e_msg = (
"The LDAP server({}) could not be resolved. Check "
"your DNS configuration and try again. "
"Lower level error: {}".format(server, e.__str__())
)
handle_exception(Exception(e_msg), request)

def _config(self, service, request):
try:
return self._get_config(service)
except Exception as e:
e_msg = (
"Missing configuration. Please configure the "
"service and try again. Exception: {}".format(e.__str__())
)
handle_exception(Exception(e_msg), request)

@transaction.atomic
def post(self, request, command):
"""
Expand All @@ -39,20 +71,60 @@ def post(self, request, command):
if command == "config":
try:
config = request.data["config"]

# Name resolution check
self._resolve_check(config.get("server"), request)

self._save_config(service, config)
except Exception as e:
logger.exception(e)
e_msg = "Ldap could not be configured. Try again"
e_msg = "LDAP could not be configured. Try again"
handle_exception(Exception(e_msg), request)

elif command == "start":
# Get config from database
config = self._config(service, request)
server = config.get("server")
cert = config.get("cert")

# @todo: add NTP check as for active_directory?
# Name resolution check
self._resolve_check(server, request)

# TLS certificate check
validate_tls_cert(server, cert)

# Extract and format all info of interest
ldap_params = {
"server": server,
"basedn": config.get("basedn"),
"ldap_uri": "".join(["ldap://", server]),
"cacertpath": cert,
"cacert_dir": dirname(cert),
"enumerate": config.get("enumerate"),
}
# Update SSSD config
try:
sssd_add_ldap(ldap_params)
systemctl("sssd", "enable")
except Exception as e:
logger.exception(e)
e_msg = "Failed to start LDAP service due to system error."
handle_exception(Exception(e_msg), request)
# Update nsswitch.conf
update_nss(["passwd", "group"], "sss")

else:
elif command == "stop":
config = self._config(service, request)
try:
toggle_auth_service(
"ldap", command, config=self._get_config(service)
)
sssd_remove_ldap(config.get("server"))
systemctl("sssd", "disable")
systemctl("sssd", "stop")
except Exception as e:
logger.exception(e)
e_msg = "Failed to %s ldap service due to system error." % command
e_msg = "Failed to stop LDAP service due to system error."
handle_exception(Exception(e_msg), request)
# Remove domain from SSSD config
update_nss(["passwd", "group"], "sss", remove=True)

return Response()
Loading

0 comments on commit 1223360

Please sign in to comment.