Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • ai3/tools/webdav-auth
  • svp-bot/webdav-auth
2 results
Select Git revision
Show changes
Commits on Source (4)
include: "https://git.autistici.org/ai3/build-deb/raw/master/ci-common.yml"
include:
- "https://git.autistici.org/pipelines/debian/raw/master/common.yml"
- "https://git.autistici.org/pipelines/images/test/python/raw/master/ci.yml"
run_tests:
stage: test
image: "registry.git.autistici.org/ai3/docker/test/python:master"
script:
- apt-get update
- env DEBIAN_FRONTEND=noninteractive apt-get install -qy --no-install-recommends python3-six libldap2-dev libsasl2-dev default-jre-headless
- tox
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: cover.xml
junit: junit.xml
variables:
PY_TEST_PACKAGES: "python3-six libldap2-dev libsasl2-dev default-jre-headless"
webdav-auth
===
The authentication server bridges the [users-dav](https://git.autistici.org/ai3/docker/apache2-users-dav)
container (which runs the [webdav-server](https://git.autistici.org/ai3/tools/webdav-server) component)
and the local [auth-server](https://git.autistici.org/id/auth).
The authentication server bridges the
[users-dav](https://git.autistici.org/ai3/docker/apache2-users-dav)
container (which runs the
[webdav-server](https://git.autistici.org/ai3/tools/webdav-server)
component) and the local
[auth-server](https://git.autistici.org/id/auth).
It runs as a system-level daemon.
There are two issues with the way DAV authentication works in A/I:
* *authentication*: are username and password correct?
* *authorization*: can the user access this particular DAV path?
This is quite complicated by the fact that the DAV server process runs
in an isolated and untrusted container[^1], and we did not want to
just grant arbitrary LDAP access to anything in there. So we split the
authentication off to a process that runs *outside* the container, and
devised a simple restricted API just for the DAV server to talk to it
(safely, over a UNIX socket).
Here's how the process works in detail:
* when a DAV request comes in, Apache spawns a DAV server running with
the appropriate user ID;
* when the DAV server starts, it fetches the list of DAV accounts
associated with the user ID it is running as, and configures HTTP
handlers for those paths only;
* on every DAV request, an authentication check is performed on the
provided username/password, verifying also that the username appears
in the list of DAV accounts associated with the current user ID.
Authorization is thusly provided by binding user IDs to specific DAV
accounts. The overall process is a bit convoluted, but it's convenient
to rely on Apache (specifically fcgid + suexec) to manage the setuid
processes.
The authentication server does not trust the DAV server, which is why
the user ID is not a request parameter but it is instead obtained by
looking at the *peer* of the UNIX socket connection.
[^1]: This used to be the case when we ran DAV and the users' PHP
processes in the same environment, now that they are completely
separate containers we could revisit this design decision.
......@@ -70,7 +70,6 @@ class DavAuthServer(socketserver.ThreadingUnixStreamServer):
@contextlib.contextmanager
def ldapconn(self, bind_dn=None, bind_pw=None):
logging.info('LDAP connection to %s', self.ldap_params['uri'])
c = ldap.initialize(self.ldap_params['uri'])
try:
if bind_dn:
......@@ -87,19 +86,24 @@ class DavAuthHandler(socketserver.StreamRequestHandler):
def handle(self):
pid, uid, gid = getpeercred(self.request)
logging.info('request from uid=%d gid=%d pid=%d', uid, gid, pid)
request_debug_str = ''
try:
request = json.loads(TextIOWrapper(self.rfile, 'utf-8').readline())
if request['type'] == 'auth':
request_debug_str = 'auth(dn=%s)' % request['dn']
response = self.authenticate(uid, request['dn'], request['password'])
elif request['type'] == 'get_accounts':
request_debug_str = 'get_accounts()'
response = self.get_accounts(uid)
else:
raise ValueError('Unknown request type')
response_data = json.dumps(response)
self.wfile.write(response_data.encode('utf-8'))
except Exception as e:
logging.exception('request error from uid %d: %s', uid, e)
response_data = f'unhandled exception: {e}'
logging.info('request from uid=%d gid=%d pid=%d: %s -> %s',
uid, gid, pid,
request_debug_str, response_data)
def authenticate(self, uid, dn, password):
# An empty password causes a "server is unwilling to perform"
......@@ -146,7 +150,6 @@ class DavAuthHandler(socketserver.StreamRequestHandler):
accounts[realm] = {'dn': r[0],
'ftpname': name,
'home': r[1]['homeDirectory'][0].decode('utf-8')}
logging.info('account list for uid=%d: %s', uid, accounts)
return accounts
......
......@@ -9,6 +9,7 @@ EnvironmentFile=-/etc/default/ai-webdav-auth-server
ExecStart=/usr/bin/ai-webdav-auth-server $ARGS
Restart=always
RuntimeDirectory=authdav
LimitNOFILE=65535
# Hardening
NoNewPrivileges=yes
......