Skip to content

Commit

Permalink
(auth) connection problem
Browse files Browse the repository at this point in the history
- convert datas from ldap.search_s returned datas as str when it is possible
- ProvisioningError class must inherit from BaseException
- change all the twisted objects in glpi/auth.py

(cherry picked from commit 5fbc9fa)
  • Loading branch information
botheis authored and neoclust committed Mar 14, 2024
1 parent e330623 commit 8a89dc7
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 99 deletions.
52 changes: 50 additions & 2 deletions agent/mmc/plugins/base/externalldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,48 @@ def connect(self):
raise Exception("Can't find an external LDAP server to connect to")
return l

def convert(self, data):
"""Convert recursively the incoming datas from bytes to string when it's possible.
@param self : instance of the object
@type self : ExternalLdapAuthenticator instance
@param data: the data we want to convert
@param type: mixed
"""
# If data is type bytes, try to convert it or let it as bytes if any problem occurs
if isinstance(data, bytes):
try:
return data.decode("utf-8")
except:
return data

# if data is a list
if isinstance(data, list):
root = []
# try convert all the elements of the list
for element in data:
root.append(self.convert(element))

# if data is a tuple, convert all elements as list and cast it as tuple
elif isinstance(data, tuple):
root = []
# 1 - convert all element from the tuple and save them in root
for element in data:
root.append(self.convert(element))
# 2 - cast root (which is a list) as tuple
root = tuple(root)

# if data is a dict
elif isinstance(data, dict):
root = {}
# convert all associated values
for key in data:
root[key] = self.convert(data[key])
else:
# in others cases (i.e. str, int, None, bool ...) leave as it
root = data
return root

def searchUser(self, l, login):
"""
Search the user dn into the LDAP
Expand All @@ -125,9 +167,11 @@ def searchUser(self, l, login):
ldap.SCOPE_SUBTREE,
f"(&({self.config.attr}={login})({self.config.filter}))",
)
users = self.convert(users)
for user in users:
self.logger.debug(f"Found user dn: {user[0]}")
self.logger.debug(str(user))

return (
users[0]
if users and users[0][1][self.config.attr][0] == login
Expand Down Expand Up @@ -201,8 +245,12 @@ def doProvisioning(self, authtoken):
if l.existUser(uid):
self.logger.debug(f"User {uid} already exists, so this user won't be added")
else:
givenName = userentry[self.config.ldap_givenName][0].decode("utf-8")
sn = userentry[self.config.ldap_sn][0].decode("utf-8")
givenName = userentry[self.config.ldap_givenName][0]
if isinstance(givenName, bytes):
givenName = givenName.decode("utf-8")
sn = userentry[self.config.ldap_sn][0]
if isinstance(sn, bytes):
sn = sn.decode("utf-8")
l.addUser(uid, authtoken.getPassword(), givenName, sn)
if self.config.profileAttr and self.config.profilesAcl:
# Set or update the user right
Expand Down
2 changes: 1 addition & 1 deletion agent/mmc/plugins/base/provisioning.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def validate(self):
return False


class ProvisioningError:
class ProvisioningError(BaseException):
"""
Raised by the ProvisioningManager if the provisioning process failed
"""
Expand Down
180 changes: 84 additions & 96 deletions services/mmc/plugins/glpi/auth.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
# -*- coding: utf-8; -*-
# SPDX-FileCopyrightText: 2004-2007 Linbox / Free&ALter Soft, http://linbox.com
# SPDX-FileCopyrightText:2007-2014 Mandriva, http://www.mandriva.com
# SPDX-FileCopyrightText: 2007-2008 Mandriva, http://www.mandriva.com/
# SPDX-FileCopyrightText: 2016-2023 Siveo <support@siveo.net>
# SPDX-License-Identifier: GPL-3.0-or-later

import urllib.parse
import re
import io

from twisted.internet import reactor, defer
from twisted.web import client
from twisted.internet import interfaces, protocol
from zope.interface import implementer

try:
from twisted.web.client import _parse
import logging

parseAvailable = True
except ImportError:
try:
from twisted.web.client import _URI as URI
except ImportError:
from twisted.web.client import URI
parseAvailable = False
logger = logging.getLogger()

from mmc.plugins.base.auth import AuthenticatorConfig, AuthenticatorI
from mmc.support.mmctools import getConfigFile
Expand All @@ -45,45 +40,73 @@ class GlpiAuthenticator(AuthenticatorI):
This is useful to create and to provision her/his account in GLPI database.
"""

login_namename = b"medulladefault"
login_passwordname = b"password"
glpi_csrf_token = b"your_csrf_token_value"

def __init__(self, conffile=None, name="glpi"):
if not conffile:
conffile = getConfigFile(name)
AuthenticatorI.__init__(self, conffile, name, GlpiAuthenticatorConfig)

def _cbIndexPage(self, value):
def _cbIndexPage(self, response):
self.logger.debug("GlpiAuthenticator: on index page")
phpsessid = value.response_headers["set-cookie"][0].split("=")
params = {
"method": "POST",
"cookies": {phpsessid[0]: phpsessid[1]},
"headers": {
"Content-Type": "application/x-www-form-urlencoded",
"Referer": urllib.parse.urljoin(
self.config.baseurl, self.config.loginpage
),
},
"postdata": urllib.parse.urlencode(
{
value.login_namename: self.user,
value.login_passwordname: self.password,
"_glpi_csrf_token": value.glpi_csrf_token,
}
),
}
return params
set_cookie_header = response.headers.getRawHeaders(b"set-cookie")
if set_cookie_header:
phpsessid = set_cookie_header[0].split(b"=")
params = {
"method": b"POST",
"cookies": {phpsessid[0]: phpsessid[1]},
"headers": {
b"Content-Type": b"application/x-www-form-urlencoded",
b"Referer": urllib.parse.urljoin(
self.config.baseurl, self.config.loginpage
),
},
"postdata": urllib.parse.urlencode(
{
self.login_namename: self.user,
self.login_passwordname: self.password,
b"_glpi_csrf_token": self.glpi_csrf_token,
}
).encode("utf-8"),
}
return params
else:
# Gérer le cas où l'en-tête "set-cookie" est manquant
# Peut-être qu'il y a une autre logique à appliquer ici, selon vos besoins
raise ValueError("Missing 'set-cookie' header in response")

def _cbLoginPost(self, params):
self.logger.debug("GlpiAuthenticator: posting on login page")
d = agent.request(
urllib.parse.urljoin(self.config.baseurl, self.config.loginpost),
None,
**params
d = self.agent.request(
b"POST",
urllib.parse.urljoin(self.config.baseurl.encode('utf-8'), self.config.loginpost.encode('utf-8')),
headers=client.Headers({b"User-Agent": [b"Twisted Client"]}),
bodyProducer = client.FileBodyProducer(io.BytesIO(params["postdata"]))
)
d.addCallback(self._cbCheckOutput)
return d

def _cbCheckOutput(self, value):
return re.search(self.config.match, value) is not None
def _cbCheckOutput(self, response):
deferred = defer.Deferred()
response.deliverBody(GlpiAuthenticator._ResponseReader(deferred))
return deferred

class _ResponseReader(protocol.Protocol):
def __init__(self, deferred):
self.deferred = deferred
self.data = b""

def dataReceived(self, chunk):
self.data += chunk

def connectionLost(self, reason):
self.deferred.callback(self.data)

def _cbReadResponseBody(self, body):
content = body.read()
return re.search(self.config.match, content.decode("utf-8")) is not None

def authenticate(self, user, password):
"""
Expand All @@ -94,75 +117,40 @@ def authenticate(self, user, password):
"GlpiAuthenticator: do not authenticate user %s (doauth = False)" % user
)
return defer.succeed(True)

self.user = user
self.password = password
d = getPageWithHeader(
urllib.parse.urljoin(self.config.baseurl, self.config.loginpage)
).addCallback(self._cbIndexPage)
self.agent = client.Agent(reactor)
d = self.agent.request(
b"GET",
urllib.parse.urljoin(self.config.baseurl, self.config.loginpage).encode('utf-8'),
headers=client.Headers({b"User-Agent": [b"Twisted Client"]})
)
d.addCallback(self._cbIndexPage)
d.addCallback(self._cbLoginPost)
return d

def validate(self):
return True


class HTTPClientFactoryWithHeader(client.Agent):
"""
HTTPClientFactory don't allow to get the HTTP header.
So we subclass the page() method and modify it to get the HTTP payload
header
"""

# the GLPI anti-csrf token (GLPI 0.83.3+)
glpi_csrf_token = ""

def page(self, page):
# grabbing the GLPI anti-csrf token (GLPI 0.83.3+) by
# looking for such patterns :
# <input type='hidden' name='_glpi_csrf_token' value='82d37af7f30d76f2238d49c28167654f'>
m = re.search(
'input type="hidden" name="_glpi_csrf_token" value="([0-9a-z]{32})">', page
)
if m is not None:
self.glpi_csrf_token = m.group(1)

m = re.search(
'input type="password" name="([0-9a-z]{19})" id="login_password"', page
)
if m is not None:
self.login_passwordname = m.group(1)
@implementer(interfaces.IPushProducer)
class StringProducer:
def __init__(self, body):
self.body = body
self.paused = False

m = re.search('input type="text" name="([0-9a-z]{19})" id="login_name"', page)
if m is not None:
self.login_namename = m.group(1)
def pauseProducing(self):
self.paused = True

if self.waiting:
self.waiting = 0
self.deferred.callback(self)
def resumeProducing(self):
if self.paused:
self.paused = False
self.consumer.write(self.body)

def stopProducing(self):
pass

def getPageWithHeader(url, contextFactory=None, *args, **kwargs):
"""
Same as twisted.web.client.getPage, but we keep the HTTP header in the
result thanks to the HTTPClientFactoryWithHeader class
"""
if parseAvailable:
scheme, host, port, path = _parse(url)
else:
uri = URI.fromBytes(url)
scheme = uri.scheme
host = uri.host
port = uri.port

factory = HTTPClientFactoryWithHeader(url, *args, **kwargs)
d = factory.deferred

if scheme == "https":
from twisted.internet import ssl

if contextFactory is None:
contextFactory = ssl.ClientContextFactory()
reactor.connectSSL(host, port, factory, contextFactory)
else:
reactor.connectTCP(host, port, factory)
return d
def startProducing(self, consumer):
self.consumer = consumer
self.consumer.write(self.body)

0 comments on commit 8a89dc7

Please sign in to comment.