summaryrefslogtreecommitdiff
path: root/mail/src
diff options
context:
space:
mode:
authorKali Kaneko <kali@leap.se>2015-11-26 21:27:23 -0400
committerKali Kaneko <kali@leap.se>2015-12-15 02:30:19 -0400
commitda4317a4df7184cf581c95b43460456d3225205f (patch)
tree49af4b8eca53942918ddef40c5c8d1fa1c42862b /mail/src
parent195f92a62cb95c0681269868d9a831b2cff523e2 (diff)
[feat] credentials handling: use twisted.cred
Diffstat (limited to 'mail/src')
-rw-r--r--mail/src/leap/mail/imap/account.py5
-rw-r--r--mail/src/leap/mail/imap/server.py47
-rw-r--r--mail/src/leap/mail/imap/service/imap-server.tac9
-rw-r--r--mail/src/leap/mail/imap/service/imap.py200
-rw-r--r--mail/src/leap/mail/imap/tests/utils.py5
-rw-r--r--mail/src/leap/mail/outgoing/service.py2
-rw-r--r--mail/src/leap/mail/smtp/gateway.py3
7 files changed, 150 insertions, 121 deletions
diff --git a/mail/src/leap/mail/imap/account.py b/mail/src/leap/mail/imap/account.py
index cc56fff..2f9ed1d 100644
--- a/mail/src/leap/mail/imap/account.py
+++ b/mail/src/leap/mail/imap/account.py
@@ -49,6 +49,7 @@ if PROFILE_CMD:
# Soledad IMAP Account
#######################################
+
class IMAPAccount(object):
"""
An implementation of an imap4 Account
@@ -68,8 +69,8 @@ class IMAPAccount(object):
You can either pass a deferred to this constructor, or use
`callWhenReady` method.
- :param user_id: The name of the account (user id, in the form
- user@provider).
+ :param user_id: The identifier of the user this account belongs to
+ (user id, in the form user@provider).
:type user_id: str
:param store: a Soledad instance.
diff --git a/mail/src/leap/mail/imap/server.py b/mail/src/leap/mail/imap/server.py
index 0e5d011..2682db7 100644
--- a/mail/src/leap/mail/imap/server.py
+++ b/mail/src/leap/mail/imap/server.py
@@ -20,16 +20,10 @@ LEAP IMAP4 Server Implementation.
import StringIO
from copy import copy
-from twisted import cred
-from twisted.internet import reactor
from twisted.internet.defer import maybeDeferred
from twisted.mail import imap4
from twisted.python import log
-from leap.common.check import leap_assert, leap_assert_type
-from leap.common.events import emit_async, catalog
-from leap.soledad.client import Soledad
-
# imports for LITERAL+ patch
from twisted.internet import defer, interfaces
from twisted.mail.imap4 import IllegalClientResponse
@@ -72,25 +66,6 @@ class LEAPIMAPServer(imap4.IMAP4Server):
"""
An IMAP4 Server with a LEAP Storage Backend.
"""
- def __init__(self, *args, **kwargs):
- # pop extraneous arguments
- soledad = kwargs.pop('soledad', None)
- uuid = kwargs.pop('uuid', None)
- userid = kwargs.pop('userid', None)
-
- leap_assert(soledad, "need a soledad instance")
- leap_assert_type(soledad, Soledad)
- leap_assert(uuid, "need a user in the initialization")
-
- self._userid = userid
-
- # initialize imap server!
- imap4.IMAP4Server.__init__(self, *args, **kwargs)
-
- # we should initialize the account here,
- # but we move it to the factory so we can
- # populate the test account properly (and only once
- # per session)
#############################################################
#
@@ -181,10 +156,6 @@ class LEAPIMAPServer(imap4.IMAP4Server):
:param line: the line from the server, without the line delimiter.
:type line: str
"""
- if self.theAccount.session_ended is True and self.state != "unauth":
- log.msg("Closing the session. State: unauth")
- self.state = "unauth"
-
if "login" in line.lower():
# avoid to log the pass, even though we are using a dummy auth
# by now.
@@ -208,24 +179,6 @@ class LEAPIMAPServer(imap4.IMAP4Server):
self.mbox = None
self.state = 'unauth'
- def authenticateLogin(self, username, password):
- """
- Lookup the account with the given parameters, and deny
- the improper combinations.
-
- :param username: the username that is attempting authentication.
- :type username: str
- :param password: the password to authenticate with.
- :type password: str
- """
- # XXX this should use portal:
- # return portal.login(cred.credentials.UsernamePassword(user, pass)
- if username != self._userid:
- # bad username, reject.
- raise cred.error.UnauthorizedLogin()
- # any dummy password is allowed so far. use realm instead!
- emit_async(catalog.IMAP_CLIENT_LOGIN, username, "1")
- return imap4.IAccount, self.theAccount, lambda: None
def do_FETCH(self, tag, messages, query, uid=0):
"""
diff --git a/mail/src/leap/mail/imap/service/imap-server.tac b/mail/src/leap/mail/imap/service/imap-server.tac
index 2045757..c4d602d 100644
--- a/mail/src/leap/mail/imap/service/imap-server.tac
+++ b/mail/src/leap/mail/imap/service/imap-server.tac
@@ -1,3 +1,4 @@
+#!/usr/bin/env python
# -*- coding: utf-8 -*-
# imap-server.tac
# Copyright (C) 2013,2014 LEAP
@@ -27,6 +28,9 @@ userid = 'user@provider'
uuid = 'deadbeefdeadabad'
passwd = 'supersecret' # optional, will get prompted if not found.
"""
+
+# TODO -- this .tac file should be deprecated in favor of bitmask.core.bitmaskd
+
import ConfigParser
import getpass
import os
@@ -112,7 +116,7 @@ tempdir = "/tmp/"
print "[~] user:", userid
soledad = initialize_soledad(uuid, userid, passwd, secrets,
- localdb, gnupg_home, tempdir)
+ localdb, gnupg_home, tempdir, userid=userid)
km_args = (userid, "https://localhost", soledad)
km_kwargs = {
"token": "",
@@ -131,7 +135,8 @@ keymanager = KeyManager(*km_args, **km_kwargs)
def getIMAPService():
- factory = imap.LeapIMAPFactory(uuid, userid, soledad)
+ soledad_sessions = {userid: soledad}
+ factory = imap.LeapIMAPFactory(soledad_sessions)
return internet.TCPServer(port, factory, interface="localhost")
diff --git a/mail/src/leap/mail/imap/service/imap.py b/mail/src/leap/mail/imap/service/imap.py
index a50611b..24fa865 100644
--- a/mail/src/leap/mail/imap/service/imap.py
+++ b/mail/src/leap/mail/imap/service/imap.py
@@ -15,21 +15,26 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
-IMAP service initialization
+IMAP Service Initialization.
"""
import logging
import os
from collections import defaultdict
+from twisted.cred.portal import Portal, IRealm
+from twisted.cred.credentials import IUsernamePassword
+from twisted.cred.checkers import ICredentialsChecker
+from twisted.cred.error import UnauthorizedLogin
+from twisted.mail.imap4 import IAccount
+from twisted.internet import defer
from twisted.internet import reactor
from twisted.internet.error import CannotListenError
from twisted.internet.protocol import ServerFactory
-from twisted.mail import imap4
from twisted.python import log
+from zope.interface import implementer
from leap.common.events import emit_async, catalog
-from leap.common.check import leap_check
from leap.mail.imap.account import IMAPAccount
from leap.mail.imap.server import LEAPIMAPServer
@@ -41,57 +46,145 @@ DO_MANHOLE = os.environ.get("LEAP_MAIL_MANHOLE", None)
if DO_MANHOLE:
from leap.mail.imap.service import manhole
-DO_PROFILE = os.environ.get("LEAP_PROFILE", None)
-if DO_PROFILE:
- import cProfile
- log.msg("Starting PROFILING...")
-
- PROFILE_DAT = "/tmp/leap_mail_profile.pstats"
- pr = cProfile.Profile()
- pr.enable()
-
# The default port in which imap service will run
+
IMAP_PORT = 1984
+#
+# Credentials Handling
+#
+
+
+@implementer(IRealm)
+class LocalSoledadIMAPRealm(object):
+
+ _encoding = 'utf-8'
+
+ def __init__(self, soledad_sessions):
+ """
+ :param soledad_sessions: a dict-like object, containing instances
+ of a Store (soledad instances), indexed by
+ userid.
+ """
+ self._soledad_sessions = soledad_sessions
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if isinstance(avatarId, str):
+ avatarId = avatarId.decode(self._encoding)
+
+ def gotSoledad(soledad):
+ for iface in interfaces:
+ if iface is IAccount:
+ avatar = IMAPAccount(avatarId, soledad)
+ return (IAccount, avatar,
+ getattr(avatar, 'logout', lambda: None))
+ raise NotImplementedError(self, interfaces)
+
+ return self.lookupSoledadInstance(avatarId).addCallback(gotSoledad)
+
+ def lookupSoledadInstance(self, userid):
+ soledad = self._soledad_sessions[userid]
+ # XXX this should return the instance after whenReady callback
+ return defer.succeed(soledad)
+
+
+@implementer(ICredentialsChecker)
+class LocalSoledadTokenChecker(object):
-class IMAPAuthRealm(object):
"""
- Dummy authentication realm. Do not use in production!
+ A Credentials Checker for a LocalSoledad store.
+
+ It checks that:
+
+ 1) The Local SoledadStorage has been correctly unlocked for the given
+ user. This currently means that the right passphrase has been passed
+ to the Local SoledadStorage.
+
+ 2) The password passed in the credentials matches whatever token has
+ been stored in the local encrypted SoledadStorage, associated to the
+ Protocol that is requesting the authentication.
"""
- theAccount = None
- def requestAvatar(self, avatarId, mind, *interfaces):
- return imap4.IAccount, self.theAccount, lambda: None
+ credentialInterfaces = (IUsernamePassword,)
+ service = None
+
+ def __init__(self, soledad_sessions):
+ """
+ :param soledad_sessions: a dict-like object, containing instances
+ of a Store (soledad instances), indexed by
+ userid.
+ """
+ self._soledad_sessions = soledad_sessions
+
+ def requestAvatarId(self, credentials):
+ if self.service is None:
+ raise NotImplementedError(
+ "this checker has not defined its service name")
+ username, password = credentials.username, credentials.password
+ d = self.checkSoledadToken(username, password, self.service)
+ d.addErrback(lambda f: defer.fail(UnauthorizedLogin()))
+ return d
+
+ def checkSoledadToken(self, username, password, service):
+ soledad = self._soledad_sessions.get(username)
+ if not soledad:
+ return defer.fail(Exception("No soledad"))
+
+ def match_token(token):
+ if token is None:
+ raise RuntimeError('no token')
+ if token == password:
+ return username
+ else:
+ raise RuntimeError('bad token')
+
+ d = soledad.get_or_create_service_token(service)
+ d.addCallback(match_token)
+ return d
+
+
+class IMAPTokenChecker(LocalSoledadTokenChecker):
+ """A credentials checker that will lookup a token for the IMAP service."""
+ service = 'imap'
+
+
+class LocalSoledadIMAPServer(LEAPIMAPServer):
+
+ """
+ An IMAP Server that authenticates against a LocalSoledad store.
+ """
+
+ def __init__(self, soledad_sessions, *args, **kw):
+
+ LEAPIMAPServer.__init__(self, *args, **kw)
+
+ realm = LocalSoledadIMAPRealm(soledad_sessions)
+ portal = Portal(realm)
+ checker = IMAPTokenChecker(soledad_sessions)
+ self.checker = checker
+ self.portal = portal
+ portal.registerChecker(checker)
class LeapIMAPFactory(ServerFactory):
+
"""
Factory for a IMAP4 server with soledad remote sync and gpg-decryption
capabilities.
"""
- protocol = LEAPIMAPServer
- def __init__(self, uuid, userid, soledad):
+ protocol = LocalSoledadIMAPServer
+
+ def __init__(self, soledad_sessions):
"""
Initializes the server factory.
- :param uuid: user uuid
- :type uuid: str
-
- :param userid: user id (user@provider.org)
- :type userid: str
-
- :param soledad: soledad instance
- :type soledad: Soledad
+ :param soledad_sessions: a dict-like object, containing instances
+ of a Store (soledad instances), indexed by
+ userid.
"""
- self._uuid = uuid
- self._userid = userid
- self._soledad = soledad
-
- theAccount = IMAPAccount(uuid, soledad)
- self.theAccount = theAccount
+ self._soledad_sessions = soledad_sessions
self._connections = defaultdict()
- # XXX how to pass the store along?
def buildProtocol(self, addr):
"""
@@ -103,13 +196,7 @@ class LeapIMAPFactory(ServerFactory):
# TODO should reject anything from addr != localhost,
# just in case.
log.msg("Building protocol for connection %s" % addr)
- imapProtocol = self.protocol(
- uuid=self._uuid,
- userid=self._userid,
- soledad=self._soledad)
- imapProtocol.theAccount = self.theAccount
- imapProtocol.factory = self
-
+ imapProtocol = self.protocol(self._soledad_sessions)
self._connections[addr] = imapProtocol
return imapProtocol
@@ -123,39 +210,21 @@ class LeapIMAPFactory(ServerFactory):
"""
Stops imap service (fetcher, factory and port).
"""
- # mark account as unusable, so any imap command will fail
- # with unauth state.
- self.theAccount.end_session()
-
- # TODO should wait for all the pending deferreds,
- # the twisted way!
- if DO_PROFILE:
- log.msg("Stopping PROFILING")
- pr.disable()
- pr.dump_stats(PROFILE_DAT)
-
return ServerFactory.doStop(self)
-def run_service(store, **kwargs):
+def run_service(soledad_sessions, port=IMAP_PORT):
"""
Main entry point to run the service from the client.
- :param store: a soledad instance
+ :param soledad_sessions: a dict-like object, containing instances
+ of a Store (soledad instances), indexed by userid.
:returns: the port as returned by the reactor when starts listening, and
the factory for the protocol.
+ :rtype: tuple
"""
- leap_check(store, "store cannot be None")
- # XXX this can also be a ProxiedObject, FIXME
- # leap_assert_type(store, Soledad)
-
- port = kwargs.get('port', IMAP_PORT)
- userid = kwargs.get('userid', None)
- leap_check(userid is not None, "need an user id")
-
- uuid = store.uuid
- factory = LeapIMAPFactory(uuid, userid, store)
+ factory = LeapIMAPFactory(soledad_sessions)
try:
interface = "localhost"
@@ -164,6 +233,7 @@ def run_service(store, **kwargs):
if os.environ.get("LEAP_DOCKERIZED"):
interface = ''
+ # TODO use Endpoints !!!
tport = reactor.listenTCP(port, factory,
interface=interface)
except CannotListenError:
@@ -178,9 +248,9 @@ def run_service(store, **kwargs):
# TODO get pass from env var.too.
manhole_factory = manhole.getManholeFactory(
{'f': factory,
- 'a': factory.theAccount,
'gm': factory.theAccount.getMailbox},
"boss", "leap")
+ # TODO use Endpoints !!!
reactor.listenTCP(manhole.MANHOLE_PORT, manhole_factory,
interface="127.0.0.1")
logger.debug("IMAP4 Server is RUNNING in port %s" % (port,))
diff --git a/mail/src/leap/mail/imap/tests/utils.py b/mail/src/leap/mail/imap/tests/utils.py
index a34538b..b1f8563 100644
--- a/mail/src/leap/mail/imap/tests/utils.py
+++ b/mail/src/leap/mail/imap/tests/utils.py
@@ -77,14 +77,11 @@ class IMAP4HelperMixin(SoledadTestMixin):
soledad_adaptor.cleanup_deferred_locks()
- UUID = 'deadbeef',
USERID = TEST_USER
def setup_server(account):
self.server = LEAPIMAPServer(
- uuid=UUID, userid=USERID,
- contextFactory=self.serverCTX,
- soledad=self._soledad)
+ contextFactory=self.serverCTX)
self.server.theAccount = account
d_server_ready = defer.Deferred()
diff --git a/mail/src/leap/mail/outgoing/service.py b/mail/src/leap/mail/outgoing/service.py
index 3bd0ea2..7943c12 100644
--- a/mail/src/leap/mail/outgoing/service.py
+++ b/mail/src/leap/mail/outgoing/service.py
@@ -71,7 +71,7 @@ class OutgoingMail:
def __init__(self, from_address, keymanager, cert, key, host, port):
"""
- Initialize the mail service.
+ Initialize the outgoing mail service.
:param from_address: The sender address.
:type from_address: str
diff --git a/mail/src/leap/mail/smtp/gateway.py b/mail/src/leap/mail/smtp/gateway.py
index 3657250..3c86d7e 100644
--- a/mail/src/leap/mail/smtp/gateway.py
+++ b/mail/src/leap/mail/smtp/gateway.py
@@ -49,6 +49,9 @@ from email import generator
generator.Generator = RFC3156CompliantGenerator
+# TODO -- implement Queue using twisted.mail.mail.MailService
+
+
#
# Helper utilities
#