]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/frontends/auth.py
Change uses of os.path.expanduser and os.path.abspath. refs #2235
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / frontends / auth.py
1 import os
2
3 from zope.interface import implements
4 from twisted.web.client import getPage
5 from twisted.internet import defer
6 from twisted.cred import error, checkers, credentials
7 from twisted.conch import error as conch_error
8 from twisted.conch.ssh import keys
9
10 from allmydata.util import base32
11 from allmydata.util.fileutil import abspath_expanduser_unicode
12
13
14 class NeedRootcapLookupScheme(Exception):
15     """Accountname+Password-based access schemes require some kind of
16     mechanism to translate name+passwd pairs into a rootcap, either a file of
17     name/passwd/rootcap tuples, or a server to do the translation."""
18
19 class FTPAvatarID:
20     def __init__(self, username, rootcap):
21         self.username = username
22         self.rootcap = rootcap
23
24 class AccountFileChecker:
25     implements(checkers.ICredentialsChecker)
26     credentialInterfaces = (credentials.IUsernamePassword,
27                             credentials.IUsernameHashedPassword,
28                             credentials.ISSHPrivateKey)
29     def __init__(self, client, accountfile):
30         self.client = client
31         self.passwords = {}
32         self.pubkeys = {}
33         self.rootcaps = {}
34         for line in open(abspath_expanduser_unicode(accountfile), "r"):
35             line = line.strip()
36             if line.startswith("#") or not line:
37                 continue
38             name, passwd, rest = line.split(None, 2)
39             if passwd.startswith("ssh-"):
40                 bits = rest.split()
41                 keystring = " ".join([passwd] + bits[:-1])
42                 rootcap = bits[-1]
43                 self.pubkeys[name] = keystring
44             else:
45                 self.passwords[name] = passwd
46                 rootcap = rest
47             self.rootcaps[name] = rootcap
48
49     def _avatarId(self, username):
50         return FTPAvatarID(username, self.rootcaps[username])
51
52     def _cbPasswordMatch(self, matched, username):
53         if matched:
54             return self._avatarId(username)
55         raise error.UnauthorizedLogin
56
57     def requestAvatarId(self, creds):
58         if credentials.ISSHPrivateKey.providedBy(creds):
59             # Re-using twisted.conch.checkers.SSHPublicKeyChecker here, rather
60             # than re-implementing all of the ISSHPrivateKey checking logic,
61             # would be better.  That would require Twisted 14.1.0 or newer,
62             # though.
63             return self._checkKey(creds)
64         elif credentials.IUsernameHashedPassword.providedBy(creds):
65             return self._checkPassword(creds)
66         elif credentials.IUsernamePassword.providedBy(creds):
67             return self._checkPassword(creds)
68         else:
69             raise NotImplementedError()
70
71     def _checkPassword(self, creds):
72         """
73         Determine whether the password in the given credentials matches the
74         password in the account file.
75
76         Returns a Deferred that fires with the username if the password matches
77         or with an UnauthorizedLogin failure otherwise.
78         """
79         try:
80             correct = self.passwords[creds.username]
81         except KeyError:
82             return defer.fail(error.UnauthorizedLogin())
83
84         d = defer.maybeDeferred(creds.checkPassword, correct)
85         d.addCallback(self._cbPasswordMatch, str(creds.username))
86         return d
87
88     def _checkKey(self, creds):
89         """
90         Determine whether some key-based credentials correctly authenticates a
91         user.
92
93         Returns a Deferred that fires with the username if so or with an
94         UnauthorizedLogin failure otherwise.
95         """
96
97         # Is the public key indicated by the given credentials allowed to
98         # authenticate the username in those credentials?
99         if creds.blob == self.pubkeys.get(creds.username):
100             if creds.signature is None:
101                 return defer.fail(conch_error.ValidPublicKey())
102
103             # Is the signature in the given credentials the correct
104             # signature for the data in those credentials?
105             key = keys.Key.fromString(creds.blob)
106             if key.verify(creds.signature, creds.sigData):
107                 return defer.succeed(self._avatarId(creds.username))
108
109         return defer.fail(error.UnauthorizedLogin())
110
111 class AccountURLChecker:
112     implements(checkers.ICredentialsChecker)
113     credentialInterfaces = (credentials.IUsernamePassword,)
114
115     def __init__(self, client, auth_url):
116         self.client = client
117         self.auth_url = auth_url
118
119     def _cbPasswordMatch(self, rootcap, username):
120         return FTPAvatarID(username, rootcap)
121
122     def post_form(self, username, password):
123         sepbase = base32.b2a(os.urandom(4))
124         sep = "--" + sepbase
125         form = []
126         form.append(sep)
127         fields = {"action": "authenticate",
128                   "email": username,
129                   "passwd": password,
130                   }
131         for name, value in fields.iteritems():
132             form.append('Content-Disposition: form-data; name="%s"' % name)
133             form.append('')
134             assert isinstance(value, str)
135             form.append(value)
136             form.append(sep)
137         form[-1] += "--"
138         body = "\r\n".join(form) + "\r\n"
139         headers = {"content-type": "multipart/form-data; boundary=%s" % sepbase,
140                    }
141         return getPage(self.auth_url, method="POST",
142                        postdata=body, headers=headers,
143                        followRedirect=True, timeout=30)
144
145     def _parse_response(self, res):
146         rootcap = res.strip()
147         if rootcap == "0":
148             raise error.UnauthorizedLogin
149         return rootcap
150
151     def requestAvatarId(self, credentials):
152         # construct a POST to the login form. While this could theoretically
153         # be done with something like the stdlib 'email' package, I can't
154         # figure out how, so we just slam together a form manually.
155         d = self.post_form(credentials.username, credentials.password)
156         d.addCallback(self._parse_response)
157         d.addCallback(self._cbPasswordMatch, str(credentials.username))
158         return d
159