Merge pull request #1038 from tahoe-lafs/3652.remove-account.url

3652 Remove STFPd account.url support.
This commit is contained in:
Jason R. Coombs 2021-04-23 10:21:53 -04:00 committed by GitHub
commit 6142168977
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 10 additions and 97 deletions

View File

@ -7,11 +7,10 @@ Tahoe-LAFS SFTP Frontend
1. `SFTP Background`_ 1. `SFTP Background`_
2. `Tahoe-LAFS Support`_ 2. `Tahoe-LAFS Support`_
3. `Creating an Account File`_ 3. `Creating an Account File`_
4. `Running An Account Server (accounts.url)`_ 4. `Configuring SFTP Access`_
5. `Configuring SFTP Access`_ 5. `Dependencies`_
6. `Dependencies`_ 6. `Immutable and Mutable Files`_
7. `Immutable and Mutable Files`_ 7. `Known Issues`_
8. `Known Issues`_
SFTP Background SFTP Background
@ -78,33 +77,6 @@ start with "ssh-".
Now add an ``accounts.file`` directive to your ``tahoe.cfg`` file, as described in Now add an ``accounts.file`` directive to your ``tahoe.cfg`` file, as described in
the next sections. the next sections.
Running An Account Server (accounts.url)
========================================
The accounts.url directive allows access requests to be controlled by an
HTTP-based login service, useful for centralized deployments. This was used
by AllMyData to provide web-based file access, where the service used a
simple PHP script and database lookups to map an account email address and
password to a Tahoe-LAFS directory cap. The service will receive a
multipart/form-data POST, just like one created with a <form> and <input>
fields, with three parameters:
• action: "authenticate" (this is a static string)
• email: USERNAME (Tahoe-LAFS has no notion of email addresses, but the
authentication service uses them as account names, so the interface
presents this argument as "email" rather than "username").
• passwd: PASSWORD
It should return a single string that either contains a Tahoe-LAFS directory
cap (URI:DIR2:...), or "0" to indicate a login failure.
Tahoe-LAFS recommends the service be secure, preferably localhost-only. This
makes it harder for attackers to brute force the password or use DNS
poisoning to cause the Tahoe-LAFS gateway to talk with the wrong server,
thereby revealing the usernames and passwords.
Public key authentication is not supported when an account server is used.
Configuring SFTP Access Configuring SFTP Access
======================= =======================

View File

@ -0,0 +1 @@
Removed support for the Account Server frontend authentication type.

View File

@ -116,7 +116,6 @@ _client_config = configutil.ValidConfiguration(
), ),
"sftpd": ( "sftpd": (
"accounts.file", "accounts.file",
"accounts.url",
"enabled", "enabled",
"host_privkey_file", "host_privkey_file",
"host_pubkey_file", "host_pubkey_file",
@ -1042,13 +1041,12 @@ class _Client(node.Node, pollmixin.PollMixin):
accountfile = self.config.get_config("sftpd", "accounts.file", None) accountfile = self.config.get_config("sftpd", "accounts.file", None)
if accountfile: if accountfile:
accountfile = self.config.get_config_path(accountfile) accountfile = self.config.get_config_path(accountfile)
accounturl = self.config.get_config("sftpd", "accounts.url", None)
sftp_portstr = self.config.get_config("sftpd", "port", "tcp:8022") sftp_portstr = self.config.get_config("sftpd", "port", "tcp:8022")
pubkey_file = self.config.get_config("sftpd", "host_pubkey_file") pubkey_file = self.config.get_config("sftpd", "host_pubkey_file")
privkey_file = self.config.get_config("sftpd", "host_privkey_file") privkey_file = self.config.get_config("sftpd", "host_privkey_file")
from allmydata.frontends import sftpd from allmydata.frontends import sftpd
s = sftpd.SFTPServer(self, accountfile, accounturl, s = sftpd.SFTPServer(self, accountfile,
sftp_portstr, pubkey_file, privkey_file) sftp_portstr, pubkey_file, privkey_file)
s.setServiceParent(self) s.setServiceParent(self)

View File

@ -1,14 +1,10 @@
import os
from zope.interface import implementer from zope.interface import implementer
from twisted.web.client import getPage
from twisted.internet import defer from twisted.internet import defer
from twisted.cred import error, checkers, credentials from twisted.cred import error, checkers, credentials
from twisted.conch.ssh import keys from twisted.conch.ssh import keys
from twisted.conch.checkers import SSHPublicKeyChecker, InMemorySSHKeyDB from twisted.conch.checkers import SSHPublicKeyChecker, InMemorySSHKeyDB
from allmydata.util.dictutil import BytesKeyDict from allmydata.util.dictutil import BytesKeyDict
from allmydata.util import base32
from allmydata.util.fileutil import abspath_expanduser_unicode from allmydata.util.fileutil import abspath_expanduser_unicode
@ -86,54 +82,3 @@ class AccountFileChecker(object):
d = defer.maybeDeferred(creds.checkPassword, correct) d = defer.maybeDeferred(creds.checkPassword, correct)
d.addCallback(self._cbPasswordMatch, str(creds.username)) d.addCallback(self._cbPasswordMatch, str(creds.username))
return d return d
@implementer(checkers.ICredentialsChecker)
class AccountURLChecker(object):
credentialInterfaces = (credentials.IUsernamePassword,)
def __init__(self, client, auth_url):
self.client = client
self.auth_url = auth_url
def _cbPasswordMatch(self, rootcap, username):
return FTPAvatarID(username, rootcap)
def post_form(self, username, password):
sepbase = base32.b2a(os.urandom(4))
sep = "--" + sepbase
form = []
form.append(sep)
fields = {"action": "authenticate",
"email": username,
"passwd": password,
}
for name, value in fields.iteritems():
form.append('Content-Disposition: form-data; name="%s"' % name)
form.append('')
assert isinstance(value, str)
form.append(value)
form.append(sep)
form[-1] += "--"
body = "\r\n".join(form) + "\r\n"
headers = {"content-type": "multipart/form-data; boundary=%s" % sepbase,
}
return getPage(self.auth_url, method="POST",
postdata=body, headers=headers,
followRedirect=True, timeout=30)
def _parse_response(self, res):
rootcap = res.strip()
if rootcap == "0":
raise error.UnauthorizedLogin
return rootcap
def requestAvatarId(self, credentials):
# construct a POST to the login form. While this could theoretically
# be done with something like the stdlib 'email' package, I can't
# figure out how, so we just slam together a form manually.
d = self.post_form(credentials.username, credentials.password)
d.addCallback(self._parse_response)
d.addCallback(self._cbPasswordMatch, str(credentials.username))
return d

View File

@ -1983,7 +1983,7 @@ class ShellSession(PrefixingLogMixin):
components.registerAdapter(ShellSession, SFTPUserHandler, ISession) components.registerAdapter(ShellSession, SFTPUserHandler, ISession)
from allmydata.frontends.auth import AccountURLChecker, AccountFileChecker, NeedRootcapLookupScheme from allmydata.frontends.auth import AccountFileChecker, NeedRootcapLookupScheme
@implementer(portal.IRealm) @implementer(portal.IRealm)
class Dispatcher(object): class Dispatcher(object):
@ -2000,7 +2000,7 @@ class Dispatcher(object):
class SFTPServer(service.MultiService): class SFTPServer(service.MultiService):
name = "frontend:sftp" name = "frontend:sftp"
def __init__(self, client, accountfile, accounturl, def __init__(self, client, accountfile,
sftp_portstr, pubkey_file, privkey_file): sftp_portstr, pubkey_file, privkey_file):
precondition(isinstance(accountfile, (str, type(None))), accountfile) precondition(isinstance(accountfile, (str, type(None))), accountfile)
precondition(isinstance(pubkey_file, str), pubkey_file) precondition(isinstance(pubkey_file, str), pubkey_file)
@ -2013,12 +2013,9 @@ class SFTPServer(service.MultiService):
if accountfile: if accountfile:
c = AccountFileChecker(self, accountfile) c = AccountFileChecker(self, accountfile)
p.registerChecker(c) p.registerChecker(c)
if accounturl: if not accountfile:
c = AccountURLChecker(self, accounturl)
p.registerChecker(c)
if not accountfile and not accounturl:
# we could leave this anonymous, with just the /uri/CAP form # we could leave this anonymous, with just the /uri/CAP form
raise NeedRootcapLookupScheme("must provide an account file or URL") raise NeedRootcapLookupScheme("must provide an account file")
pubkey = keys.Key.fromFile(pubkey_file.encode(get_filesystem_encoding())) pubkey = keys.Key.fromFile(pubkey_file.encode(get_filesystem_encoding()))
privkey = keys.Key.fromFile(privkey_file.encode(get_filesystem_encoding())) privkey = keys.Key.fromFile(privkey_file.encode(get_filesystem_encoding()))