Exemplo n.º 1
0
 def log_err(self, stuff, why):
     try:
         # DRPC still logs errors in the old format. (Exception, String Explaining Error)
         # log.failure() can autodetect the exception in flight so we will just log the reason
         log.failure(why)
     except Exception:
         pass
def rotate_skeys(duo_client):
    """
    Generate new DRPC skeys (signing and encryption) via ECDHE with the Duo cloud service.

    Args:
        duo_client (CloudSSODuoClient): a duo client with the current credentials

    Returns:
        (signing_skey, encryption_skey) tuple with the new keys as bytes, or reraises API errors

    """
    proxy_public_key, proxy_private_key = drpc_crypto.generate_ephemeral_keys()
    try:
        ser_proxy_public_key = drpc_crypto.serialize_ephemeral_key(
            proxy_public_key)
        rotate_result = yield duo_client.proxy_rotate_skeys(
            ser_proxy_public_key)
        duo_public_key = drpc_crypto.deserialize_ephemeral_key(
            rotate_result["duo_public_key"])
    except duo_async.DuoAPIError as e:
        log.failure("Rotate call failed")
        raise e

    new_signing_skey_bytes, new_encryption_skey_bytes = drpc_crypto.derive_shared_keys(
        duo_public_key, proxy_private_key)

    defer.returnValue((new_signing_skey_bytes, new_encryption_skey_bytes))
Exemplo n.º 3
0
    def get_creds_for_ldap_idp(self, rikey, auth_type):
        try:
            creds = self.credential_mapping[rikey]
            if auth_type == const.AD_AUTH_TYPE_SSPI:
                creds = ldap_base.ServiceAccountCredential(username=None,
                                                           password=None)

            return creds
        except KeyError as e:
            log.failure(ERR_LDAP_SSO_MISSING_RIKEY)
            raise drpc.CallError(ERR_LDAP_SSO_MISSING_RIKEY, {
                "error": str(e),
            })
Exemplo n.º 4
0
 def datagramReceived(self, datagram, addr):
     """addr is a tuple of (host, port). If calling from a test case, you
     probably want to call handle_datagram_received instead so exceptions
     aren't lost."""
     host, port = addr
     try:
         yield self.handle_datagram_received(datagram, host, port)
     except packet.PacketError as err:
         log.msg("dropping packet from %s:%s - %s" % (host, port, err))
     except Exception:
         log.failure(
             "unhandled error processing request from {host}:{port}",
             host=host,
             port=port,
         )
Exemplo n.º 5
0
    def do_get_proxy_counter(self):
        log.msg("Reporting proxy counter")
        try:
            counter = secret_storage.access_proxy_counter()
            if self.debug:
                log.msg(
                    "Found counter value {counter}".format(counter=counter))
        except Exception as e:
            log.failure(ERR_COUNTER_FAILURE)
            raise drpc.CallError(ERR_COUNTER_FAILURE, {
                "during": "get_proxy_counter",
                "error": str(e)
            })

        return {
            "counter": counter,
        }
Exemplo n.º 6
0
    def rotate_and_rejoin(self):
        new_signing_skey, new_encryption_skey = yield drpc_keys_rotation.rotate_skeys(
            self.duo_client)
        self.update_secrets(new_signing_skey, new_encryption_skey)
        # Rebuild the client with the new credentials
        self.duo_client = self.make_duo_client(
            duo_creds=self.duo_creds,
            host=self.host,
            client_type=duo_async.CloudSSODuoClient,
        )

        try:
            server_protocol = yield self.duo_client.proxy_join(
                server_module=self, drpc_path=self.drpc_path)
            defer.returnValue(server_protocol)
        except Exception as e:
            log.failure("Failed to rejoin after rotation")
            raise e
Exemplo n.º 7
0
        def handle_msg(value, controls, d):
            try:
                if isinstance(value, pureldap.LDAPSearchResultDone):
                    e = ldaperrors.get(value.resultCode, value.errorMessage)
                    if isinstance(e, (ldaperrors.Success,
                                      ldaperrors.LDAPSizeLimitExceeded)):
                        cookie = get_cookie(controls)
                        d.callback((None, cookie))
                    else:
                        d.callback((e, None))
                elif isinstance(value, pureldap.LDAPSearchResultEntry):
                    # Always send DN. Overwrite DN from attribute set, if any.
                    obj = {
                        "distinguishedname": [escape_bytes(value.objectName)]
                    }

                    for k, vs in value.attributes:
                        # Smash attribute name case.
                        k = k.decode().lower()

                        # Server may not honor attributes (e.g.
                        # SearchByTreeWalkingMixin).
                        if attributes and k.encode() not in attributes:
                            continue

                        # Covert value to list and encode for JSON.
                        vs = [escape_bytes(v) for v in vs]

                        obj[k] = vs

                    # Refuse to return certain attributes even if all
                    # attributes were requested.
                    obj.pop("userpassword", None)

                    res.append(obj)
            except Exception:
                log.failure("Unexpected error handling message")
            finally:
                return isinstance(value, (
                    pureldap.LDAPBindResponse,
                    pureldap.LDAPSearchResultDone,
                ))
Exemplo n.º 8
0
    def _get_client(
        self,
        host,
        port,
        transport_type,
        ssl_verify_depth,
        ssl_verify_hostname,
        ssl_ca_certs,
        timeout,
        debug,
        is_logging_insecure,
    ):
        if ssl_ca_certs:
            ssl_ca_certs = load_ca_bundle(ssl_ca_certs)
            if not ssl_ca_certs:
                # Didn't parse out any PEM certificates.
                raise drpc_exceptions.CallBadArgError(["ssl_ca_certs"])
        else:
            # Ensure ssl_ca_certs is a list.
            ssl_ca_certs = []

        is_ssl = transport_type != const.AD_TRANSPORT_CLEAR
        if is_ssl:
            if ssl_verify_hostname and not ssl_ca_certs:
                log.msg("Missing required configuration item: "
                        "'SSL verify hostname' requires that "
                        "'SSL CA certs' also be specified "
                        "(and non-empty).")
                raise drpc_exceptions.CallError(ERR_LDAP_CONFIGURATION_FAILED)

        try:
            factory = self.ldap_client_factory(
                timeout=timeout,
                transport_type=transport_type,
                ssl_verify_depth=ssl_verify_depth,
                ssl_verify_hostname=ssl_verify_hostname,
                ssl_ca_certs=ssl_ca_certs,
                debug=debug,
                is_logging_insecure=is_logging_insecure,
            )
        except Exception as e:
            log.failure(ERR_LDAP_CONFIGURATION_FAILED)
            raise drpc_exceptions.CallError(ERR_LDAP_CONFIGURATION_FAILED, {
                "error": str(e),
            })

        try:
            factory.connect_ldap(host, port)
            client = yield factory.deferred
        except ldap.client.ADClientError as e:
            if isinstance(e.underlying_exception, DNSLookupError):
                log.failure(ERR_LDAP_HOSTNAME_RESOLUTION_FAILED)
                raise drpc_exceptions.CallError(
                    ERR_LDAP_HOSTNAME_RESOLUTION_FAILED, {
                        "error": str(e),
                    })
            else:
                log.failure(ERR_LDAP_CONNECTION_FAILED)
                raise drpc_exceptions.CallError(ERR_LDAP_CONNECTION_FAILED, {
                    "error": str(e),
                })
        except Exception as e:
            log.failure(ERR_LDAP_CONNECTION_FAILED)
            raise drpc_exceptions.CallError(ERR_LDAP_CONNECTION_FAILED, {
                "error": str(e),
            })

        def timeout_cb():
            log.msg("LDAP operation timed out")
            client.transport.abortConnection()

        timeout_dc = reactor.callLater(timeout, timeout_cb)

        defer.returnValue((client, timeout_dc))
Exemplo n.º 9
0
    def do_ldap_search(
        self,
        host: str,
        port: int,
        base_dn: str,
        filter_text: Optional[str] = None,
        attributes: Optional[Set[str]] = None,
        ntlm_domain: Optional[str] = None,
        ntlm_workstation: Optional[str] = None,
        auth_type: str = const.AD_AUTH_TYPE_NTLM_V2,
        transport_type: str = const.AD_TRANSPORT_STARTTLS,
        ssl_verify_depth: int = const.DEFAULT_SSL_VERIFY_DEPTH,
        ssl_verify_hostname: bool = True,
        ssl_ca_certs: Optional[str] = None,
        pagination: Optional[str] = PAGINATION_TYPE_CRITICAL,
        page_size: int = DEFAULT_PAGE_SIZE,
        max_result_size: Optional[int] = None,
        timeout: int = 60,
        call_id: Optional[int] = None,
    ):
        """
        * host: LDAP IP address we will perform actions against.
        * port: LDAP server port.
        * filter_text: Filter user search.
        * attributes: Retrieve only the listed attributes. If None,
          retrieve all attributes.
        * ntlm_{domain, workstation}: Windows authentication mechanism.
          Allows for bypassing primary bind if already within the domain.
        * auth_type: Bind method to use, e.g. NTLM, Plain, SSPI, etc.
        * transport_type: Method of transportation to use, e.g. Clear, TLS,
          LDAPS, etc.
        * timeout: If either establishing the connection or binding
          and searching take longer than this number of seconds
          the response will be an error.
        * page_size: As in RFC 2696.
        * max_result_size: If paging, stop requesting results after
          when at least this number of results have been received.
        """

        if attributes is None:
            attributes = set()

        try:
            attributes = set(k.lower() for k in attributes)
            if "userpassword" in attributes:
                attributes.remove("userpassword")
        except Exception:
            log.failure("Error parsing provided attributes dict")
            raise drpc.CallBadArgError(["attributes"])

        page_size = parse_positive_int(page_size, "page_size")
        if max_result_size is not None:
            max_result_size = parse_positive_int(max_result_size,
                                                 "max_result_size")

        if not (filter_text is None
                or isinstance(filter_text, six.string_types)):
            raise drpc.CallBadArgError(["filter_text"])

        bind_dn = self.service_account_username
        bind_pw = self.service_account_password

        log.msg(
            "Performing LDAP search: "
            "call_id={call_id} host={host} port={port} base_dn={base_dn} "
            "auth_type={auth_type} transport_type={transport_type} "
            "ssl_verify_depth={ssl_verify_depth} ssl_verify_hostname={ssl_verify_hostname} "
            "ssl_ca_certs={ssl_ca_certs} attributes={attributes}",
            call_id=call_id,
            host=host,
            port=port,
            base_dn=base_dn,
            auth_type=auth_type,
            transport_type=transport_type,
            ssl_verify_depth=ssl_verify_depth,
            ssl_verify_hostname=ssl_verify_hostname,
            attributes=attributes,
            ssl_ca_certs=ssl_ca_certs is not None,
        )

        self._verify_ldap_config_args(bind_dn, bind_pw, auth_type,
                                      transport_type)

        cl, timeout_dc = yield self._get_client(
            host,
            port,
            transport_type,
            ssl_verify_depth,
            ssl_verify_hostname,
            ssl_ca_certs,
            timeout,
            self.debug,
            self.is_logging_insecure,
        )

        try:
            try:
                yield cl.perform_bind(
                    auth_type=auth_type,
                    dn=bind_dn,
                    username=bind_dn,
                    password=bind_pw,
                    domain=ntlm_domain,
                    workstation=ntlm_workstation,
                    permit_implicit=True,
                )
            except Exception:
                if timeout_dc.active():
                    log.failure(ldap_base.ERR_LDAP_BIND_FAILED)
                    raise drpc.CallError(ldap_base.ERR_LDAP_BIND_FAILED)
                else:
                    log.failure(ldap_base.ERR_LDAP_TIMEOUT)
                    raise drpc.CallError(ldap_base.ERR_LDAP_TIMEOUT, {
                        "during": "bind",
                    })

            try:
                filter_bytes = None
                if filter_text is not None:
                    filter_bytes = filter_text.encode("utf-8")

                result = yield self._paged_search(
                    client=cl,
                    base_dn=base_dn,
                    filter_text=filter_bytes,
                    attributes=[a.encode("utf-8") for a in attributes],
                    pagination=pagination,
                    page_size=page_size,
                    max_result_size=max_result_size,
                )

                # any object that has a 'member' attribute is a potential
                # group object. if that attribute is an empty list then the
                # group is either empty or contains more values than the
                # server's configured maximum attribute length (usually 1500)
                # results.
                # try to check for members using range before giving up.
                try_ranged = [obj for obj in result if obj.get("member") == []]
                if try_ranged:
                    yield self.try_ranged_search(cl, base_dn, try_ranged)
            except ldap.client.ConnectionNotProperlyBoundError:
                log.failure(ldap_base.ERR_LDAP_SEARCH_FAILED_BAD_BIND)
                raise drpc.CallError(ldap_base.ERR_LDAP_SEARCH_FAILED_BAD_BIND)
            except Exception:
                if timeout_dc.active():
                    log.failure(ldap_base.ERR_LDAP_SEARCH_FAILED)
                    raise drpc.CallError(ldap_base.ERR_LDAP_SEARCH_FAILED)
                else:
                    log.failure(ldap_base.ERR_LDAP_TIMEOUT)
                    raise drpc.CallError(ldap_base.ERR_LDAP_TIMEOUT, {
                        "during": "search",
                    })
        finally:
            if timeout_dc.active():
                timeout_dc.cancel()
            try:
                cl.transport.abortConnection()
            except Exception:
                pass

        defer.returnValue({
            "results": result,
        })
Exemplo n.º 10
0
    def try_ranged_search(self, client, base_dn, objs):
        """ So, you are an empty group object or a group with too many members
            to return in one shot.
            Let's try to grab those members a few at a time until we determine
            that the group is empty or we've built up the full list of members.
        """
        for obj in objs:
            member_range = None
            previous_result_empty = False
            # loop until we've exhausted all range segments
            while True:
                # can't help you without a dn. sorry.
                if not obj.get("distinguishedname"):
                    break
                previous_range = member_range
                member_range = next_range(previous_range,
                                          previous_result_empty)

                # create an ldap filter object to search with.
                # pureldap will escape the distinguishedname, making it safe.
                # Since we're reading the distinguishedname from search results,
                # we'll need to unescape it before it gets re-escaped.
                attr_obj = pureldap.LDAPAttributeDescription(
                    "distinguishedname")
                value_obj = pureldap.LDAPAssertionValue(
                    distinguishedname.unescape(obj["distinguishedname"][0]))
                filter_object = pureldap.LDAPFilter_equalityMatch(
                    attr_obj, value_obj)

                attributes = [
                    "member;range={0}-{1}".format(member_range[0],
                                                  member_range[1])
                ]
                try:
                    ranged_res = yield client.perform_search(
                        base_dn, filter_object, attributes=attributes)
                except LDAPOperationsError as e:
                    # searched for a range that exceeds the total number of
                    # elements in the attribute.
                    # if this is the first instance of this error, we may
                    # just need to try again with a '*' as the upper bound
                    # to get the last few members.
                    # if this is the second time (we are using the '*'), then
                    # we already have all of the members and can break.
                    if ldap.client.ERR_AD_CANT_RETRIEVE_ATTS in e.message:
                        if previous_result_empty:
                            break
                        previous_result_empty = True
                        continue
                    # something bad happened... give up
                    else:
                        log.failure("Unexpected error")
                        break

                # no results; we're done here
                if len(ranged_res) == 0:
                    break
                else:
                    # update the result object with the newly discovered
                    # members
                    for k, vs in ranged_res[0].items():
                        k = k.decode().lower()
                        vs = [escape_bytes(v) for v in vs]
                        if k.startswith("member;"):
                            obj["member"].extend(vs)
Exemplo n.º 11
0
    def get_challenge_response(self, request: _ProxyRequest, state: Dict[str,
                                                                         Any]):
        """
        Gets a challenge response for the given request

        Returns:
            packet.Packet
        """
        # Do not have access to factor - request.password is users cookie
        success = False
        self.log_request(request, "Challenge Response: %r" % request.password)

        if state and "primary_res" in state:
            radius_attrs = state["primary_res"].radius_attrs
        else:
            radius_attrs = {}

        auth_cookie = urllib.parse.unquote(self._get_authcookie(request))
        try:
            finish_res = yield self.client.proxy_finish(auth_cookie)
        except duo_async.DuoAPIError as e:
            log.failure("Duo proxy_finish call failed")
            response_packet = self.response_for_api_error(
                request,
                state["primary_res"],
                e,
                state["primary_res"].radius_attrs,
            )
            defer.returnValue(response_packet)

        self.log_request(request,
                         "Authcookie validation result: %r" % finish_res)
        if finish_res["valid_cookie"] and (finish_res["user"]
                                           == request.username):
            success = True

        if success:
            log.auth_standard(
                msg="Valid login from iframe",
                username=request.username,
                auth_stage=log.AUTH_SECONDARY,
                status=log.AUTH_ALLOW,
                server_section=self.server_section_name,
                server_section_ikey=self.server_section_ikey,
                client_ip=request.client_ip,
            )
            defer.returnValue(
                self.create_accept_packet(
                    request,
                    radius_attrs=radius_attrs,
                ))
        else:
            log.auth_standard(
                msg="Invalid login from iframe",
                username=request.username,
                auth_stage=log.AUTH_SECONDARY,
                status=log.AUTH_REJECT,
                server_section=self.server_section_name,
                server_section_ikey=self.server_section_ikey,
                client_ip=request.client_ip,
            )
            defer.returnValue(self.create_reject_packet(request))
Exemplo n.º 12
0
    def get_initial_response(self, request: _ProxyRequest):
        """
        Gets a response for the first request over a connection from an appliance

        Returns:
            packet.Packet
        """
        # make sure username, password were provided
        if request.username is None:
            msg = "No username provided"
            self.log_request(request, msg)
            log.auth_standard(
                msg=msg,
                username=request.username,
                auth_stage="Unknown",
                status=log.AUTH_ERROR,
                server_section=self.server_section_name,
                server_section_ikey=self.server_section_ikey,
                client_ip=request.client_ip,
            )
            defer.returnValue(self.create_reject_packet(request, msg))

        self.log_request(request,
                         "login attempt for username %r" % request.username)

        if request.password is None:
            self.log_request(
                request,
                "Only the PAP with a Shared Secret format is"
                " supported. Is the system communicating with"
                " the Authentication Proxy using CHAP or"
                " MSCHAPv2 instead?",
            )
            msg = "No password provided"
            self.log_request(request, msg)
            log.auth_standard(
                msg=msg,
                username=request.username,
                auth_stage=log.AUTH_PRIMARY,
                status=log.AUTH_ERROR,
                server_section=self.server_section_name,
                server_section_ikey=self.server_section_ikey,
                client_ip=request.client_ip,
            )
            defer.returnValue(self.create_reject_packet(request, msg))

        # perform primary authentication
        primary_res = yield self.primary_auth(request, request.password)
        if not primary_res.success:
            defer.returnValue(
                self.create_reject_packet(request, primary_res.msg))

        if request.username in self.exempt_usernames:
            msg = "User exempted from 2FA"
            log.auth_standard(
                msg=msg,
                username=request.username,
                auth_stage=log.AUTH_SECONDARY,
                status=log.AUTH_ALLOW,
                server_section=self.server_section_name,
                server_section_ikey=self.server_section_ikey,
                client_ip=request.client_ip,
            )
            defer.returnValue(
                self.create_accept_packet(
                    request, msg, radius_attrs=primary_res.radius_attrs))

        # get a txid from duo service
        try:
            init_res = yield self.client.proxy_init(request.username)
        except duo_async.DuoAPIError as e:
            log.failure("Duo proxy_init call failed")
            response_packet = self.response_for_api_error(
                request,
                primary_res,
                e,
                primary_res.radius_attrs,
            )
            defer.returnValue(response_packet)

        # Note: MUST NOT YIELD between calling _create_challenge_id
        # and calling create_challenge()
        challenge_id = self._create_challenge_id()

        # build script injection with the txid
        params = {
            "script_uri": self.script_uri,
            "proxy_txid": init_res["proxy_txid"],
            "api_host": self.client.host,
            "state": challenge_id,
        }
        challenge_msg = self.script_inject % params
        if len(challenge_msg) > 253:
            raise ValueError(
                "response string is %d chars long, but cannot exceed 253 "
                "chars. If you specified a custom iframe_script_uri, you "
                "may need to shorten it by at least %d chars" %
                (len(challenge_msg), len(challenge_msg) - 253))

        self.log_request(request, "Sending authentication challenge packet")
        state = {
            "primary_res": primary_res,
        }
        challenge_packet = self.create_challenge(request,
                                                 challenge_msg,
                                                 state=state,
                                                 challenge_id=challenge_id)
        defer.returnValue(challenge_packet)
Exemplo n.º 13
0
    def do_ldap_health_check(
        self,
        rikey,
        host,
        port,
        base_dns,
        filter_text=None,
        attributes=None,
        ntlm_domain=None,
        ntlm_workstation=None,
        auth_type=const.AD_AUTH_TYPE_NTLM_V2,
        transport_type=const.AD_TRANSPORT_STARTTLS,
        ssl_verify_depth=const.DEFAULT_SSL_VERIFY_DEPTH,
        ssl_verify_hostname=True,
        ssl_ca_certs=None,
        timeout=60,
        call_id=None,
    ):
        """
        Performs a health check against the specified host by binding as the
        service user and executing a dummy search against each provided base DN
        to ensure the searches are executed without error.
        """

        # Do this outside of the try/except. If something goes wrong with these
        # operations, then we should raise that error and not treat it as an
        # unhealthy result.
        service_account_credentials = self.get_creds_for_ldap_idp(
            rikey, auth_type)

        self._verify_ldap_config_args(
            service_account_credentials.username,
            service_account_credentials.password,
            auth_type,
            transport_type,
        )

        log.msg((
            "Performing health check: "
            "call_id={call_id} host={host} port={port} rikey={rikey} "
            "attributes={attributes} base_dns={base_dns} ntlm_domain={ntlm_domain} "
            "ntlm_workstation={ntlm_workstation} auth_type={auth_type} transport_type={transport_type} "
            "ssl_verify_depth={ssl_verify_depth} ssl_verify_hostname={ssl_verify_hostname} "
            "ssl_ca_certs={ssl_ca_certs}").format(
                call_id=call_id,
                host=host,
                port=port,
                rikey=rikey,
                attributes=attributes,
                base_dns=base_dns,
                ntlm_domain=ntlm_domain,
                ntlm_workstation=ntlm_workstation,
                auth_type=auth_type,
                transport_type=transport_type,
                ssl_verify_depth=ssl_verify_depth,
                ssl_verify_hostname=ssl_verify_hostname,
                ssl_ca_certs=ssl_ca_certs is not None,
            ))

        client, timeout_dc = yield self._get_client(
            host,
            port,
            transport_type,
            ssl_verify_depth,
            ssl_verify_hostname,
            ssl_ca_certs,
            timeout,
            self.debug,
            self.is_logging_insecure,
        )

        try:
            try:
                # First make sure we can bind as the service user
                yield client.perform_bind(
                    auth_type=auth_type,
                    dn=service_account_credentials.username,
                    username=service_account_credentials.username,
                    password=service_account_credentials.password,
                    domain=ntlm_domain,
                    workstation=ntlm_workstation,
                    permit_implicit=True,
                )
            except OpenSSL.SSL.Error as e:
                error_message = util.retrieve_error_string_from_openssl_error(
                    e)
                if OPENSSL_ERROR_INVALID_CERT in error_message:
                    drpc_error = ERR_TLS_CERT
                elif OPENSSL_ERROR_INVALID_PROTOCOL in error_message:
                    drpc_error = ERR_TLS_INVALID_PROTOCOL
                else:
                    drpc_error = ERR_TLS_GENERIC
                log.failure(drpc_error)
                raise drpc.CallError(drpc_error, {
                    "error": str(e),
                })
            except ldaperrors.LDAPInvalidCredentials as e:
                raise drpc.CallError(ERR_LDAP_BIND_INVALID_CREDS, {
                    "error": str(e),
                })
            except Exception as e:
                # T76043: add better/more specific error handling so we can tell
                # Gary why the search failed
                if timeout_dc.active():
                    log.failure(ldap_base.ERR_LDAP_BIND_FAILED)
                    raise drpc.CallError(ldap_base.ERR_LDAP_BIND_FAILED, {
                        "error": str(e),
                    })
                else:
                    log.failure(ldap_base.ERR_LDAP_TIMEOUT)
                    raise drpc.CallError(ldap_base.ERR_LDAP_TIMEOUT, {
                        "during": "bind",
                        "error": str(e),
                    })

            # Then execute a search over each provided base DN to make sure that:
            #   1) The service user has search permissions
            #   2) Each of the provided base DN's exists
            filter_obj = (ldapfilter.parseFilter(filter_text.encode("utf-8"))
                          if filter_text else None)
            try:
                for base_dn in base_dns:
                    yield client.perform_search(
                        dn=base_dn,
                        filter_object=filter_obj,
                        attributes=attributes,
                        scope=pureldap.LDAP_SCOPE_baseObject,
                        sizeLimit=1,
                    )
            except (ldaperrors.LDAPNoSuchObject, ldaperrors.LDAPReferral) as e:
                # https://ldap.com/ldap-result-code-reference-core-ldapv3-result-codes/#rc-noSuchObject
                # Object at the requested BaseDN doesn't exist. Since we are searching for just anything at all
                # this likely means the Base DN is invalid.
                # https://ldap.com/ldap-result-code-reference-core-ldapv3-result-codes/#rc-referral
                # We dont support referrals so it's basically an unknown/bad DN
                log.failure(ERR_LDAP_INVALID_BASE_DN)
                raise drpc.CallError(ERR_LDAP_INVALID_BASE_DN, {
                    "error": str(e),
                    "base_dn": str(base_dn)
                })
            except ldap.client.ConnectionNotProperlyBoundError as e:
                log.failure(ldap_base.ERR_LDAP_SEARCH_FAILED_BAD_BIND)
                raise drpc.CallError(ldap_base.ERR_LDAP_SEARCH_FAILED_BAD_BIND,
                                     {
                                         "error": str(e),
                                     })
            except Exception as e:
                # T76043: add better/more specific error handling so we can tell
                # Gary why the search failed
                if timeout_dc.active():
                    log.failure(ldap_base.ERR_LDAP_SEARCH_FAILED)
                    raise drpc.CallError(ldap_base.ERR_LDAP_SEARCH_FAILED, {
                        "error": str(e),
                    })
                else:
                    log.failure(ldap_base.ERR_LDAP_TIMEOUT)
                    raise drpc.CallError(
                        ldap_base.ERR_LDAP_TIMEOUT,
                        {
                            "during": "search",
                            "error": str(e),
                        },
                    )
        finally:
            if timeout_dc.active():
                timeout_dc.cancel()
            try:
                client.transport.abortConnection()
            except Exception:
                log.failure(
                    "Error cleaning up connection to host {}".format(host))

        # If nothing went wrong by this point, then everything's healthy as
        # far as we can tell.
        result = {
            "healthy": True,
        }

        log.msg(result)
        defer.returnValue(result)
Exemplo n.º 14
0
    def fetch_ldap_attributes_from_server(
        self,
        host,
        port,
        rikey,
        desired_attributes,
        user_dn,
        ntlm_domain=None,
        ntlm_workstation=None,
        auth_type=const.AD_AUTH_TYPE_NTLM_V2,
        transport_type=const.AD_TRANSPORT_STARTTLS,
        ssl_verify_depth=const.DEFAULT_SSL_VERIFY_DEPTH,
        ssl_verify_hostname=True,
        ssl_ca_certs=None,
        timeout=60,
        call_id=None,
    ):
        creds = self.get_creds_for_ldap_idp(rikey, auth_type)
        bind_dn = creds.username
        bind_pw = creds.password

        log.msg((
            "Performing user attributes fetch: "
            "call_id={call_id} host={host} port={port} rikey={rikey} "
            "desired_attributes={desired_attributes} user_dn={user_dn} ntlm_domain={ntlm_domain} "
            "ntlm_workstation={ntlm_workstation} auth_type={auth_type} transport_type={transport_type} "
            "ssl_verify_depth={ssl_verify_depth} ssl_verify_hostname={ssl_verify_hostname} "
            "ssl_ca_certs={ssl_ca_certs}").format(
                call_id=call_id,
                host=host,
                port=port,
                rikey=rikey,
                desired_attributes=desired_attributes,
                user_dn=user_dn,
                ntlm_domain=ntlm_domain,
                ntlm_workstation=ntlm_workstation,
                auth_type=auth_type,
                transport_type=transport_type,
                ssl_verify_depth=ssl_verify_depth,
                ssl_verify_hostname=ssl_verify_hostname,
                ssl_ca_certs=ssl_ca_certs is not None,
            ))

        self._verify_ldap_config_args(bind_dn, bind_pw, auth_type,
                                      transport_type)

        client, timeout_dc = yield self._get_client(
            host,
            port,
            transport_type,
            ssl_verify_depth,
            ssl_verify_hostname,
            ssl_ca_certs,
            timeout,
            self.debug,
            self.is_logging_insecure,
        )

        # Try to do everything network-related
        try:
            # Bind as service user, for searching
            try:
                yield client.perform_bind(
                    auth_type=auth_type,
                    dn=bind_dn,
                    username=bind_dn,
                    password=bind_pw,
                    domain=ntlm_domain,
                    workstation=ntlm_workstation,
                    permit_implicit=True,
                )
            except Exception as e:
                if timeout_dc.active():
                    log.failure(ldap_base.ERR_LDAP_BIND_FAILED)
                    raise drpc.CallError(ldap_base.ERR_LDAP_BIND_FAILED,
                                         {"error": six.text_type(e)})
                else:
                    log.failure(ldap_base.ERR_LDAP_TIMEOUT)
                    raise drpc.CallError(
                        ldap_base.ERR_LDAP_TIMEOUT,
                        {
                            "during": "bind",
                            "error": six.text_type(e)
                        },
                    )

            # Search for the user
            try:
                result = yield client.perform_search(
                    user_dn,
                    None,
                    attributes=desired_attributes,
                    scope=pureldap.LDAP_SCOPE_baseObject,
                )

                if len(result) != 1:
                    log.error(AUTH_INVALID_USER)
                    raise Exception(AUTH_INVALID_USER)

                result = result[0]
            except distinguishedname.InvalidRelativeDistinguishedName as irdn:
                log.failure(ldap_base.ERR_LDAP_BAD_AD_CONFIGURATION)
                raise drpc.CallError(
                    ldap_base.ERR_LDAP_BAD_AD_CONFIGURATION,
                    {"error": six.text_type(irdn)},
                )
            except Exception as e:
                log.failure(ldap_base.ERR_LDAP_SEARCH_FAILED)
                raise drpc.CallError(ldap_base.ERR_LDAP_SEARCH_FAILED,
                                     {"error": six.text_type(e)})
        finally:
            # Clean up networking
            if timeout_dc.active():
                timeout_dc.cancel()

            try:
                client.transport.abortConnection()
            except Exception:
                pass

        # At this point, result is a single LDAPEntry, or something has gone wrong and we ideally raised somewhere above
        encoded_dict = transform_result(result, desired_attributes)

        logged_atts = transform_result(
            result,
            desired_attributes,
            value_transform=self.to_unicode_ignore_errors)
        log.msg("For user dn {dn}, found attributes {atts}".format(
            dn=user_dn, atts=logged_atts))

        defer.returnValue(encoded_dict)
Exemplo n.º 15
0
    def authenticate_against_server(
        self,
        host: str,
        port: str,
        username: str,
        password: str,
        service_account_username: str,
        service_account_password: str,
        base_dns: Optional[List[str]] = None,
        ntlm_domain: Optional[str] = None,
        ntlm_workstation: Optional[str] = None,
        auth_type: str = const.AD_AUTH_TYPE_NTLM_V2,
        transport_type: str = const.AD_TRANSPORT_STARTTLS,
        ssl_verify_depth: int = const.DEFAULT_SSL_VERIFY_DEPTH,
        ssl_verify_hostname: bool = True,
        ssl_ca_certs: Optional[str] = None,
        timeout: int = 60,
        username_attributes: Optional[List[str]] = None,
        call_id: Optional[int] = None,
    ):
        """
        * username_attribute: attribute within AD that we will compare the
          username against.

        See do_ldap_search for further argument documentation.

        Returns: tuple(bool, str).
            - True if the auth was successful,
            - The full dn of the user who authed
        """

        bind_dn = service_account_username
        bind_pw = service_account_password
        # At this point we *should* have a username attribute passed to us. But if we don't have one
        # we will default to the most popular attribute, samaccountname. This will only work for AD.
        username_attributes = (username_attributes
                               if username_attributes else ["samaccountname"])
        base_dns = base_dns if base_dns else [""]

        log.msg((
            "Performing LDAP authentication: "
            "call_id={call_id} host={host} port={port} base_dns={base_dns} "
            "auth_type={auth_type} transport_type={transport_type} "
            "ssl_verify_depth={ssl_verify_depth} ssl_verify_hostname={ssl_verify_hostname} "
            "ssl_ca_certs={ssl_ca_certs} username={username} username_attributes={username_attributes}"
        ).format(
            call_id=call_id,
            host=host,
            port=port,
            base_dns=base_dns,
            auth_type=auth_type,
            transport_type=transport_type,
            ssl_verify_depth=ssl_verify_depth,
            ssl_verify_hostname=ssl_verify_hostname,
            username=username,
            username_attributes=username_attributes,
            ssl_ca_certs=ssl_ca_certs is not None,
        ))

        # The factory has a username attribute field on it but we will not be using it
        cl, timeout_dc = yield self._get_client(
            host,
            port,
            transport_type,
            ssl_verify_depth,
            ssl_verify_hostname,
            ssl_ca_certs,
            timeout,
            self.debug,
            self.is_logging_insecure,
        )

        try:
            # Primary bind. This authenticates us to AD and allows us to
            # make search queries.
            try:
                yield cl.perform_bind(
                    auth_type=auth_type,
                    dn=bind_dn,
                    username=bind_dn,
                    password=bind_pw,
                    domain=ntlm_domain,
                    workstation=ntlm_workstation,
                    permit_implicit=True,
                )
            except Exception as e:
                if timeout_dc.active():
                    log.failure(ldap_base.ERR_LDAP_BIND_FAILED)
                    raise drpc.CallError(ldap_base.ERR_LDAP_BIND_FAILED, {
                        "error": str(e),
                    })
                else:
                    log.failure(ldap_base.ERR_LDAP_TIMEOUT)
                    raise drpc.CallError(ldap_base.ERR_LDAP_TIMEOUT, {
                        "during": "bind",
                        "error": str(e),
                    })

            username_match = dict.fromkeys(username_attributes, username)
            filterObject = yield cl.user_filter_object(
                username_matches=username_match)

            # Search for the user. With AD, the user's cn is their full
            # name, not username. So, we need to search for the user by comparing
            # the username to a selection of specific attributes. For example:
            # sAMAccountName or userprincipalname

            search_hits: List[BytesLDAPEntry] = []
            try:
                # Always fetch msDS-PrincipalName since we need the user's
                # sAMAccountName and domain. The username entered by the user
                # may not be in a valid format for SSPI or NTLM depending on
                # the username attributes configured, but the sAMAccountName
                # that's part of msDS-PrincipalName will always work.
                attributes_to_fetch = {
                    str(attr).lower()
                    for attr in username_attributes
                }
                attributes_to_fetch.add("msds-principalname")
                # Also fetch some helpful attributes for debugging if something goes wrong
                attributes_to_fetch.add("objectclass")
                attributes_to_fetch.add("objectcategory")

                # To check multiple base dns we need to perform a search for each one. If the
                # combination of all of these searches returns more than 1 unique user we will
                # fail the auth
                for base_dn in base_dns:
                    result = yield cl.perform_search(
                        base_dn,
                        filterObject,
                        attributes=tuple(attributes_to_fetch))
                    search_hits.extend(result)
            except distinguishedname.InvalidRelativeDistinguishedName as e:
                log.failure(ldap_base.ERR_LDAP_BAD_AD_CONFIGURATION)
                raise drpc.CallError(ldap_base.ERR_LDAP_BAD_AD_CONFIGURATION, {
                    "error": str(e),
                })
            except Exception as e:
                log.failure(
                    "{msg} for {username}",
                    msg=ldap_base.ERR_LDAP_SEARCH_FAILED,
                    username=username,
                )
                raise drpc.CallError(ldap_base.ERR_LDAP_SEARCH_FAILED, {
                    "error": str(e),
                })

            if len(search_hits) == 0:
                err_msg = AUTH_INVALID_USER
                err = drpc.CallError(ldap_base.ERR_LDAP_SEARCH_FAILED, {
                    "error": err_msg,
                })
                log.error(
                    "{msg} {username}. Error: {error}",
                    msg=err_msg,
                    username=username,
                    error=err,
                )
                raise err

            if len(search_hits) > 1:
                err_msg = AUTH_TOO_MANY_USERS
                err = drpc.CallError(ldap_base.ERR_LDAP_SEARCH_FAILED, {
                    "error": err_msg,
                })
                log.error(
                    "{msg} while searching for {username}: {users}. Error: {error}",
                    msg=err_msg,
                    username=username,
                    users=[user.dn.getText() for user in search_hits],
                    error=err,
                )
                raise err

            user_result = search_hits[0]
            user_full_dn = user_result.dn.getText()
            # Log out the search result
            matched_attributes = determine_matched_attributes(
                username, username_attributes, user_result)
            log.msg("Found {username} with attributes {atts}".format(
                username=username, atts=matched_attributes))

            # Initialize these as None since Plain binds don't these values
            bind_username = None
            user_domain = None
            # If the auth type is not plain, determine the user's domain and
            # sAMAccountName by pulling them from msDS-PrincipalName. We need
            # both in order to do NTLM and SSPI authentications.
            if auth_type != AD_AUTH_TYPE_PLAIN:
                msds_principalname_attribute_set = user_result.get(
                    "msDS-PrincipalName")
                if not msds_principalname_attribute_set:
                    # msDS-PrincipalName must be provided so we can determine
                    # the domain of the user authenticating. Abort the
                    # authentication if the attribute doesn't exist for the user.
                    err = drpc.CallError(
                        ERR_LDAP_MISSING_REQUIRED_ATTRIBUTE,
                        {
                            "error":
                            MSDS_PRINCIPAL_NAME_MISSING.format(username)
                        },
                    )
                    log.error(
                        "Error: {error}, User Result: {user_result}",
                        error=err,
                        user_result=user_result,
                    )
                    raise err

                msds_principalname = list(
                    user_result.get("msDS-PrincipalName"))[0].decode("utf8")
                if not msds_principalname or "\\" not in msds_principalname:
                    # This means that msDS-PrincipalName was empty, a form
                    # we can't work with (e.g. SID), or didn't have any
                    # domain information on it. We can't guarantee we'll
                    # log in the correct user without knowing their domain,
                    # so abort the authentication.
                    err = drpc.CallError(
                        ERR_LDAP_INVALID_ATTRIBUTE_VALUE,
                        {
                            "error":
                            INVALID_MSDS_PRINCIPAL_NAME.format(username)
                        },
                    )
                    log.error(
                        "Error: {error}, User Result: {user_result}",
                        error=err,
                        user_result=user_result,
                    )
                    raise err

                user_domain, bind_username = msds_principalname.split("\\", 1)

            # Secondary bind. Assuming the user we queried exists, attempt
            # to bind as them (this is essentially performing an authentication).
            try:
                yield cl.perform_bind(
                    auth_type=auth_type,
                    dn=user_full_dn,
                    username=bind_username,
                    password=password,
                    domain=user_domain,
                    workstation=ntlm_workstation,
                    permit_implicit=False,
                )
            except Exception as e:
                log.msg(
                    "Authentication failed for user {username} ({dn})".format(
                        username=username, dn=user_full_dn))
                log.msg(e)
                defer.returnValue((False, None))
        finally:
            if timeout_dc.active():
                timeout_dc.cancel()
            try:
                cl.transport.abortConnection()
            except Exception:
                pass

        log.msg("Authentication succeeded for user {username} ({dn})".format(
            username=username, dn=user_full_dn))
        defer.returnValue((True, user_full_dn))