1 | """ |
---|
2 | Authentication for frontends. |
---|
3 | """ |
---|
4 | |
---|
5 | from zope.interface import implementer |
---|
6 | from twisted.internet import defer |
---|
7 | from twisted.cred import checkers, credentials |
---|
8 | from twisted.conch.ssh import keys |
---|
9 | from twisted.conch.checkers import SSHPublicKeyChecker, InMemorySSHKeyDB |
---|
10 | |
---|
11 | from allmydata.util.dictutil import BytesKeyDict |
---|
12 | from allmydata.util.fileutil import abspath_expanduser_unicode |
---|
13 | |
---|
14 | |
---|
15 | class NeedRootcapLookupScheme(Exception): |
---|
16 | """Accountname+Password-based access schemes require some kind of |
---|
17 | mechanism to translate name+passwd pairs into a rootcap, either a file of |
---|
18 | name/passwd/rootcap tuples, or a server to do the translation.""" |
---|
19 | |
---|
20 | class FTPAvatarID(object): |
---|
21 | def __init__(self, username, rootcap): |
---|
22 | self.username = username |
---|
23 | self.rootcap = rootcap |
---|
24 | |
---|
25 | @implementer(checkers.ICredentialsChecker) |
---|
26 | class AccountFileChecker(object): |
---|
27 | credentialInterfaces = (credentials.ISSHPrivateKey,) |
---|
28 | |
---|
29 | def __init__(self, client, accountfile): |
---|
30 | self.client = client |
---|
31 | path = abspath_expanduser_unicode(accountfile) |
---|
32 | with open_account_file(path) as f: |
---|
33 | self.rootcaps, pubkeys = load_account_file(f) |
---|
34 | self._pubkeychecker = SSHPublicKeyChecker(InMemorySSHKeyDB(pubkeys)) |
---|
35 | |
---|
36 | def _avatarId(self, username): |
---|
37 | return FTPAvatarID(username, self.rootcaps[username]) |
---|
38 | |
---|
39 | def requestAvatarId(self, creds): |
---|
40 | if credentials.ISSHPrivateKey.providedBy(creds): |
---|
41 | d = defer.maybeDeferred(self._pubkeychecker.requestAvatarId, creds) |
---|
42 | d.addCallback(self._avatarId) |
---|
43 | return d |
---|
44 | raise NotImplementedError() |
---|
45 | |
---|
46 | def open_account_file(path): |
---|
47 | """ |
---|
48 | Open and return the accounts file at the given path. |
---|
49 | """ |
---|
50 | return open(path, "rt", encoding="utf-8") |
---|
51 | |
---|
52 | def load_account_file(lines): |
---|
53 | """ |
---|
54 | Load credentials from an account file. |
---|
55 | |
---|
56 | :param lines: An iterable of account lines to load. |
---|
57 | |
---|
58 | :return: See ``create_account_maps``. |
---|
59 | """ |
---|
60 | return create_account_maps( |
---|
61 | parse_accounts( |
---|
62 | content_lines( |
---|
63 | lines, |
---|
64 | ), |
---|
65 | ), |
---|
66 | ) |
---|
67 | |
---|
68 | def content_lines(lines): |
---|
69 | """ |
---|
70 | Drop empty and commented-out lines (``#``-prefixed) from an iterator of |
---|
71 | lines. |
---|
72 | |
---|
73 | :param lines: An iterator of lines to process. |
---|
74 | |
---|
75 | :return: An iterator of lines including only those from ``lines`` that |
---|
76 | include content intended to be loaded. |
---|
77 | """ |
---|
78 | for line in lines: |
---|
79 | line = line.strip() |
---|
80 | if line and not line.startswith("#"): |
---|
81 | yield line |
---|
82 | |
---|
83 | def parse_accounts(lines): |
---|
84 | """ |
---|
85 | Parse account lines into their components (name, key, rootcap). |
---|
86 | """ |
---|
87 | for line in lines: |
---|
88 | name, passwd, rest = line.split(None, 2) |
---|
89 | if not passwd.startswith("ssh-"): |
---|
90 | raise ValueError( |
---|
91 | "Password-based authentication is not supported; " |
---|
92 | "configure key-based authentication instead." |
---|
93 | ) |
---|
94 | |
---|
95 | bits = rest.split() |
---|
96 | keystring = " ".join([passwd] + bits[:-1]) |
---|
97 | key = keys.Key.fromString(keystring) |
---|
98 | rootcap = bits[-1] |
---|
99 | yield (name, key, rootcap) |
---|
100 | |
---|
101 | def create_account_maps(accounts): |
---|
102 | """ |
---|
103 | Build mappings from account names to keys and rootcaps. |
---|
104 | |
---|
105 | :param accounts: An iterator if (name, key, rootcap) tuples. |
---|
106 | |
---|
107 | :return: A tuple of two dicts. The first maps account names to rootcaps. |
---|
108 | The second maps account names to public keys. |
---|
109 | """ |
---|
110 | rootcaps = BytesKeyDict() |
---|
111 | pubkeys = BytesKeyDict() |
---|
112 | for (name, key, rootcap) in accounts: |
---|
113 | name_bytes = name.encode("utf-8") |
---|
114 | rootcaps[name_bytes] = rootcap.encode("utf-8") |
---|
115 | pubkeys[name_bytes] = [key] |
---|
116 | return rootcaps, pubkeys |
---|