-
Notifications
You must be signed in to change notification settings - Fork 96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improved authenticator system #218
Improved authenticator system #218
Conversation
To prevent breakage, auth_callback is kept. But if `authenticator` is set, then auth_callback will be ignored. The internals of smtp_AUTH is also overhauled with the new system.
I'm keeping this as Draft until I have the time to properly test the changes on my test systems. |
Codecov Report
@@ Coverage Diff @@
## master #218 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 5 5
Lines 1100 1143 +43
Branches 197 202 +5
=========================================
+ Hits 1100 1143 +43
Continue to review full report at Codecov.
|
Sorry, had been a very intense sprint in my job for the past 2.5 weeks. I can now pick up on things. Testing progress:
All local testing passes, so I'm undrafting this. |
... because the same intermittent error as the one explained in the comments started to rear its head again.
# Conflicts: # aiosmtpd/docs/NEWS.rst # aiosmtpd/docs/smtp.rst # aiosmtpd/smtp.py # aiosmtpd/tests/test_smtp.py
I originally use the term "stable", which was NOT used when I first created this file. Then Ned Deily put in the definition for "stable" -- which, for the record, I am actually disagreeing with -- so I had to coin a new, different term. I chose "frozen" to reflect the fact that the binaries won't ever be officially updated again.
# In addition, pypy3 implementation in Windows is ... complicated. We should stay | ||
# away from all those complications; users choosing to use pypy3 on Windows to run | ||
# aiosmtpd should be considered advanced, know what they're doing, and ready for | ||
# all the consequences |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A looong explanation on why we use 3.7 on Windows ... and a self-made term "frozen" because Ned thinks "stable" == "still receiving bugfixes".
(I personally think "stable" == "no possible (official) behavior change", but whatevs.)
aiosmtpd/docs/smtp.rst
Outdated
.. class:: SMTP(handler, *, data_size_limit=33554432, enable_SMTPUTF8=False, decode_data=False, \ | ||
hostname=None, ident=None, tls_context=None, require_starttls=False, timeout=300, \ | ||
auth_required=False, auth_require_tls=True, auth_exclude_mechanism=None, \ | ||
auth_callback=None, authenticator=None, \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Split overly-long signature line.
aiosmtpd/docs/smtp.rst
Outdated
@@ -302,7 +332,4 @@ advertised, and the ``STARTTLS`` command will not be accepted. | |||
``False``. | |||
|
|||
.. _StreamReaderProtocol: https://docs.python.org/3/library/asyncio-stream.html#streamreaderprotocol | |||
.. _`RFC 3207`: http://www.faqs.org/rfcs/rfc3207.html | |||
.. _`RFC 2821`: https://www.ietf.org/rfc/rfc2821.txt |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The :rfc:
directive takes care of formatting and linking.
aiosmtpd/docs/smtp.rst
Outdated
.. _`asyncio transport`: https://docs.python.org/3/library/asyncio-protocol.html#asyncio-transport | ||
.. _ssl: https://docs.python.org/3/library/ssl.html |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The :mod:
directive takes care of formatting and linking.
elif not arg: | ||
await self.push('501 Not enough value') | ||
else: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code is pushed to the right and honestly looks really ugly. Especially as it has gained complexity due to having to cater to the new authenticator
system while maintaining backward compatibility with the old auth_callback
system.
So I changed all the naked await
s up there with return await
async def auth_LOGIN(self, _, args: List[str]) -> AuthResult: | ||
if len(args) > 1: | ||
try: | ||
login = b64decode(args[1].encode(), validate=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This piece of code handles the -- actually RFC non-compliant -- AUTH LOGIN <b64_username>
procedure.
Well, actually there is no RFC for AUTH LOGIN
... only a draft RFC that never gets adopted/promoted into full RFC status. So in this case, we can be more lenient with implementation of AUTH LOGIN
.
(Also, Python>=3.5 smtplib.SMTP
has a seriously buggy implementation of SASL LOGIN; see bpo-27820)
log.debug(f"auth_{mechanism} returned {auth_result}") | ||
|
||
# New system using `authenticator` and AuthResult | ||
if isinstance(auth_result, AuthResult): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test handles backward- and forward-compatibility for auth_*
methods.
|
||
# Mark this as expectedFailure because smtplib.SMTP implementation in Python>=3.5 | ||
# is buggy. See bpo-27820 | ||
@unittest.expectedFailure |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Until bpo-27820 is fixed, keep this expectedFailure
line. When the bug is fixed, we'll ... figure something out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've left comments on possibly interesting points.
Because it seems that some SMTP client implementations expect to see "Username:" and "Password:" for the AUTH LOGIN challenges, while in actuality the nearest-to-authoritative documentation for AUTH LOGIN [1] _explicitly_ specifies that the contents of the challenges MUST be ignored by SMTP clients. So, these two knobs are added to cater for such noncompliant clients. [1] https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt
# Conflicts: # aiosmtpd/docs/NEWS.rst # aiosmtpd/smtp.py # aiosmtpd/tests/test_smtp.py
For some reasons, PyCharm's code inspection insists that "self.parser" is not an instance of "FakeParser" and thus does not have the "message" attribute. Renaming "self.parser" to "self.fakeparser" totally fixes this issue.
As mentioned in Issue aio-libs#219
To prevent confusion with the new "authenticator" system
"enable_SMTPUTF8" is already in self.server_kwargs
And while we're at it, also make it into a list and do direct comparison, and using pytest's assert instead of assertEqual
After all, we only want to ensure it's successful, we no longer have to concern ourselves with login data. Also partially converted to pytest's assert system.
I have no idea why I tested that login_data must be None; we're testing for authentication success, and that would have had resulted in a not-None value. Anyways, the test is fixed and detailed into several tests that ensure authentication is successful and some fields are properly set.
AuthResult is now attrs-based and _requires_ keyword-only initializers. Also, the branch where auth is successful, it should have recorded the login's username & password.
This helps with troubleshooting. The piecemeal assertEquals failed when verifying the numeric code, while important information is "trapped" inside the message which did not get compared. With direct assert comparing the tuples, we see _both_ the wrong code _and_ the helpful message.
For characters <= \x7F, encode() and encode("ascii") will be identical anyways. But if the challenge str contains ... 'exotic' characters (for example, emojis), then encode("ascii") will fail while encode() will always succeeds. Whether client will be able to understand the challenge or not, that depends on the client, not our concern.
Also add another reference to another place in RFC4954 that explicitly requires trailing space after 334
aiosmtpd/docs/handlers.rst
Outdated
@@ -183,7 +183,7 @@ those methods of the handler instance will override the built-in methods. | |||
|
|||
:boldital:`server` is the instance of the ``SMTP`` class invoking the AUTH hook. | |||
This allows the AUTH hook implementation to invoke facilities such as the | |||
``push()`` and ``_auth_interact()`` methods. | |||
``push()`` and ``challenge_auth()`` methods. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed the function so it doesn't start with "_"
Had to be creative here because "auth_*" prefix has its own meaning...
@@ -1,5 +1,6 @@ | |||
import re | |||
import ssl | |||
import attr |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using attrs
to enforce some behaviors.
aiosmtpd/testing/helpers.py
Outdated
testcase.assertEqual( | ||
(235, b"2.7.0 Authentication successful"), | ||
response | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Compare tuple against tuple directly so helpful error messages won't get hidden.
Also for assert_auth_invalid
and assert_auth_required
below.
@@ -48,7 +57,7 @@ def tearDownModule(): | |||
ModuleResources.close() | |||
|
|||
|
|||
def authenticator(mechanism, login, password): | |||
def auth_callback(mechanism, login, password): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename to prevent confusion with the brand new authenticator
system.
for actual, expected in zip(lines, expecteds): | ||
self.assertEqual(actual, expected) | ||
] | ||
assert lines == expecteds |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, do direct compare list against list. The line-by-line comparison sometimes resulted in confusing failures. By comparing whole list against whole list, I can get a better grasp of what's going on.
Also, transition to pytest's assert
-based testing.
assert auth_peeker.login == b"" | ||
assert auth_peeker.password == b"" | ||
response = client.mail("alice@example.com") | ||
assert response == (250, b"OK") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace the pointless "let's check that login_data doesn't change" test with a test that actually tests that 'protected' commands are accepted.
Also, migrated some tests to pytest's assert
methodology.
@@ -1154,9 +1344,8 @@ def test_help_authenticated(self): | |||
b64encode(b'\0goodlogin\0goodpasswd').decode() | |||
) | |||
assert_auth_success(self, code, response) | |||
code, response = client.docmd('HELP') | |||
self.assertEqual(code, 250) | |||
self.assertEqual(response, SUPPORTED_COMMANDS_NOTLS) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, change from piece-by-piece comparison to direct tuple-vs-tuple comparison, in order to not hide possibly helpful error messages.
Also, migrate to pytest's assert
syntax.
# Conflicts: # aiosmtpd/smtp.py # aiosmtpd/testing/helpers.py # aiosmtpd/tests/test_smtp.py
# Conflicts: # .github/workflows/unit-testing-and-coverage.yml # aiosmtpd/docs/NEWS.rst # aiosmtpd/tests/test_smtp.py
Huh, what's up with MacOS runner in GA? It's now intermittently failing. First time it failed with Pyhon 3.8, afterwards it failed with Python 3.6. Luckily, with GA I can just ask it to "re-run all jobs" and 3rd time was the charm. But this makes me wonder if I should drop the MacOS runner in the GA yml ... |
As usual, I'm marinating this for 2x24 before squamerging. |
# Conflicts: # aiosmtpd/docs/smtp.rst
What do these changes do?
In previous implementation of
auth_callback
, the authenticator callback function is only able to see three information: Mechanism of authentication, login/username, and password. The function cannot determine additional information such as the peer IP address, state of TLS, etc.The proposed changes in this PR adds, as an alternative to
auth_callback
, a new parameter namedauthenticator
.The function passed to
authenticator
will not only see the three information as seen by auth_callback, it will also see server, session, and envelope (similar to how handler hooks are called -- although in AUTH case, envelope is likely empty and useless).In addition, the return value of
authenticator
function is more informative instead of the overloaded, non-intuitive_TriStateType
.In addition, this PR also now supports
AUTH LOGIN <b64encoded-username>
because Python'ssmtplib.SMTP
class has a buggy implementation ofAUTH LOGIN
(see bpo-27820).Are there changes in behavior for the user?
Should be None.
authenticator
does not replaceauth_callback
; current users relying onauth_callback
can continue doing so, while future users can use the much more powerfulauthenticator
system.Related issue number
Closes #219
Checklist
(ALL)
(ALL)
qa,py36-{nocov,cov}
(ALL)
+pypy3-{nocov,cov,diffcov}
(ALL)
+pypy3-{nocov,cov,diffcov}
(ALL)
+pypy3-{nocov,cov,diffcov}
(ALL)
+pypy3-nocov
NEWS.rst
file