Skip to content

Commit

Permalink
squashme: move tests, address comments
Browse files Browse the repository at this point in the history
  • Loading branch information
half-duplex committed Nov 12, 2023
1 parent 746e614 commit 69812a6
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 227 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ lint-style:
flake8

lint-type:
mypy --check-untyped-defs sopel
mypy sopel

.PHONY: test test_norecord test_novcr vcr_rerecord
test:
Expand Down
9 changes: 9 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ sopel-plugins = "sopel.cli.plugins:main"
[project.entry-points.pytest11]
pytest-sopel = "sopel.tests.pytest_plugin"

[tool.mypy]
check_untyped_defs = true
plugins = "sqlalchemy.ext.mypy.plugin"
show_error_codes = true

[[tool.mypy.overrides]]
module = 'scramp.*'
ignore_missing_imports = true

[tool.pytest.ini_options]
# NOTE: sopel/ is included here to include dynamically-generated tests
testpaths = ["test", "sopel"]
Expand Down
4 changes: 0 additions & 4 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,3 @@ exclude =
contrib/*,
conftest.py
no-accept-encodings = True

[mypy]
plugins = sqlalchemy.ext.mypy.plugin
show_error_codes = True
26 changes: 14 additions & 12 deletions sopel/coretasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,8 @@ def _handle_sasl_capability(
# Manage CAP REQ :sasl
auth_method = bot.settings.core.auth_method
server_auth_method = bot.settings.core.server_auth_method
auth_target = bot.settings.core.auth_target
is_required = 'sasl' in (auth_method, server_auth_method)

LOGGER.critical("FOO. BAR!")
if not is_required:
# not required: we are fine, available or not
return plugin.CapabilityNegotiation.DONE
Expand All @@ -112,13 +110,6 @@ def _handle_sasl_capability(
'cannot authenticate with SASL.',
)
return plugin.CapabilityNegotiation.ERROR
elif auth_target not in ["PLAIN", "EXTERNAL", "SCRAM-SHA-256"]:
LOGGER.error(
'Configured SASL method %r is not supported by Sopel.',
auth_target,
)
return plugin.CapabilityNegotiation.ERROR

# Check SASL configuration (password is required)
password, mech = _get_sasl_pass_and_mech(bot)
if not password:
Expand All @@ -130,9 +121,18 @@ def _handle_sasl_capability(
cap_info = bot.capabilities.get_capability_info('sasl')
cap_params = cap_info.params

available_mechs = cap_params.split(',') if cap_params else []
server_mechs = cap_params.split(',') if cap_params else []

if available_mechs and mech not in available_mechs:
sopel_mechs = ["PLAIN", "EXTERNAL", "SCRAM-SHA-256"]
if mech not in sopel_mechs:
raise config.ConfigurationError(
'SASL mechanism "{mech}" is not supported by Sopel; '
'available mechanisms are: {available}.'.format(
mech=mech,
available=', '.join(sopel_mechs),
)
)
if server_mechs and mech not in server_mechs:
# Raise an error if configured to use an unsupported SASL mechanism,
# but only if the server actually advertised supported mechanisms,
# i.e. this network supports SASL 3.2
Expand All @@ -141,11 +141,13 @@ def _handle_sasl_capability(
# by the sasl_mechs() function

# See https://github.com/sopel-irc/sopel/issues/1780 for background

common_mechs = set(sopel_mechs) & set(server_mechs)
raise config.ConfigurationError(
'SASL mechanism "{mech}" is not advertised by this server; '
'available mechanisms are: {available}.'.format(
mech=mech,
available=', '.join(available_mechs),
available=', '.join(common_mechs),
)
)

Expand Down
207 changes: 207 additions & 0 deletions test/coretasks/test_coretasks_sasl.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
"""Test behavior of SASL by ``sopel.coretasks``"""
from __future__ import annotations

from base64 import b64decode, b64encode
from logging import ERROR
from typing import TYPE_CHECKING

import pytest
from scramp import ScramMechanism

from sopel import coretasks
from sopel.tests import rawlist

if TYPE_CHECKING:
from sopel.bot import Sopel
from sopel.config import Config
from sopel.tests.factories import BotFactory, ConfigFactory

Expand Down Expand Up @@ -65,6 +69,11 @@ def tmpconfig(configfactory: ConfigFactory) -> Config:
return configfactory('conf.ini', TMP_CONFIG_SASL_DEFAULT)


@pytest.fixture
def mockbot(tmpconfig, botfactory):
return botfactory.preloaded(tmpconfig)


def test_sasl_plain_token_generation() -> None:
"""Make sure SASL PLAIN tokens match the expected format."""
assert (
Expand Down Expand Up @@ -344,3 +353,201 @@ def test_sasl_nak(botfactory: BotFactory, tmpconfig) -> None:
'CAP END',
'QUIT :Error negotiating capabilities.',
)


def test_sasl_bad_method(mockbot: Sopel, caplog: pytest.LogCaptureFixture):
"""Verify the bot behaves when configured with an unsupported SASL method."""
mockbot.settings.core.auth_method = "sasl"
mockbot.settings.core.auth_target = "SCRAM-MD4"
mockbot.on_message("CAP * LS :sasl")
mockbot.on_message("CAP TestBot ACK :sasl")
assert mockbot.backend.message_sent == rawlist(
"CAP REQ :sasl",
"CAP END",
)
with caplog.at_level(ERROR):
mockbot.on_message("AUTHENTICATE +")
assert '"SCRAM-MD4" is not supported' in caplog.text


def test_sasl_plain_auth(mockbot: Sopel):
"""Verify the bot performs SASL PLAIN auth correctly."""
mockbot.settings.core.auth_method = "sasl"
mockbot.settings.core.auth_target = "PLAIN"
mockbot.on_message("CAP * LS :sasl")
mockbot.on_message("CAP TestBot ACK :sasl")
assert mockbot.backend.message_sent == rawlist(
"CAP REQ :sasl",
"AUTHENTICATE PLAIN",
)
mockbot.on_message("AUTHENTICATE +")
assert (
len(mockbot.backend.message_sent) == 3
and mockbot.backend.message_sent[-1]
== rawlist("AUTHENTICATE VGVzdEJvdABUZXN0Qm90AHNlY3JldA==")[0]
)
mockbot.on_message(
"900 TestBot test!test@test TestBot :You are now logged in as TestBot"
)
mockbot.on_message("903 TestBot :SASL authentication succeeded")
assert (
len(mockbot.backend.message_sent) == 4
and mockbot.backend.message_sent[-1] == rawlist("CAP END")[0]
)


def test_sasl_scram_sha_256_auth(mockbot: Sopel):
"""Verify the bot performs SASL SCRAM-SHA-256 auth correctly."""
mech = ScramMechanism()
salt, stored_key, server_key, iter_count = mech.make_auth_info(
"secret", iteration_count=5000
)
scram_server = mech.make_server(
lambda x: (salt, stored_key, server_key, iter_count)
)

mockbot.settings.core.auth_method = "sasl"
mockbot.settings.core.auth_target = "SCRAM-SHA-256"
mockbot.on_message("CAP * LS :sasl")
mockbot.on_message("CAP TestBot ACK :sasl")
assert mockbot.backend.message_sent == rawlist(
"CAP REQ :sasl",
"AUTHENTICATE SCRAM-SHA-256",
)
mockbot.on_message("AUTHENTICATE +")

scram_server.set_client_first(
b64decode(mockbot.backend.message_sent[-1].split(b" ")[-1]).decode("utf-8")
)
mockbot.on_message(
"AUTHENTICATE "
+ b64encode(scram_server.get_server_first().encode("utf-8")).decode("utf-8")
)
scram_server.set_client_final(
b64decode(mockbot.backend.message_sent[-1].split(b" ")[-1]).decode("utf-8")
)
mockbot.on_message(
"AUTHENTICATE "
+ b64encode(scram_server.get_server_final().encode("utf-8")).decode("utf-8")
)
assert (
len(mockbot.backend.message_sent) == 5
and mockbot.backend.message_sent[-1] == rawlist("AUTHENTICATE +")[0]
)

mockbot.on_message(
"900 TestBot test!test@test TestBot :You are now logged in as TestBot"
)
mockbot.on_message("903 TestBot :SASL authentication succeeded")
assert (
len(mockbot.backend.message_sent) == 6
and mockbot.backend.message_sent[-1] == rawlist("CAP END")[0]
)


def test_sasl_scram_sha_256_nonsense_server_first(mockbot: Sopel):
"""Verify the bot handles a nonsense SCRAM-SHA-256 server_first correctly."""
mech = ScramMechanism()
salt, stored_key, server_key, iter_count = mech.make_auth_info(
"secret", iteration_count=5000
)
scram_server = mech.make_server(
lambda x: (salt, stored_key, server_key, iter_count)
)

mockbot.settings.core.auth_method = "sasl"
mockbot.settings.core.auth_target = "SCRAM-SHA-256"
mockbot.on_message("CAP * LS :sasl")
mockbot.on_message("CAP TestBot ACK :sasl")
mockbot.on_message("AUTHENTICATE +")

scram_server.set_client_first(
b64decode(mockbot.backend.message_sent[-1].split(b" ")[-1]).decode("utf-8")
)
mockbot.on_message("AUTHENTICATE " + b64encode(b"junk").decode("utf-8"))
assert (
len(mockbot.backend.message_sent) == 4
and mockbot.backend.message_sent[-1] == rawlist("AUTHENTICATE *")[0]
)


def test_sasl_scram_sha_256_nonsense_server_final(mockbot: Sopel):
"""Verify the bot handles a nonsense SCRAM-SHA-256 server_final correctly."""
mech = ScramMechanism()
salt, stored_key, server_key, iter_count = mech.make_auth_info(
"secret", iteration_count=5000
)
scram_server = mech.make_server(
lambda x: (salt, stored_key, server_key, iter_count)
)

mockbot.settings.core.auth_method = "sasl"
mockbot.settings.core.auth_target = "SCRAM-SHA-256"
mockbot.on_message("CAP * LS :sasl")
mockbot.on_message("CAP TestBot ACK :sasl")
mockbot.on_message("AUTHENTICATE +")

scram_server.set_client_first(
b64decode(mockbot.backend.message_sent[-1].split(b" ")[-1]).decode("utf-8")
)
mockbot.on_message(
"AUTHENTICATE "
+ b64encode(scram_server.get_server_first().encode("utf-8")).decode("utf-8")
)
scram_server.set_client_final(
b64decode(mockbot.backend.message_sent[-1].split(b" ")[-1]).decode("utf-8")
)
mockbot.on_message("AUTHENTICATE " + b64encode(b"junk").decode("utf-8"))
assert (
len(mockbot.backend.message_sent) == 5
and mockbot.backend.message_sent[-1] == rawlist("AUTHENTICATE *")[0]
)


def test_sasl_scram_sha_256_error_server_first(mockbot: Sopel):
"""Verify the bot handles an error SCRAM-SHA-256 server_first correctly."""

mockbot.settings.core.auth_method = "sasl"
mockbot.settings.core.auth_target = "SCRAM-SHA-256"
mockbot.on_message("CAP * LS :sasl")
mockbot.on_message("CAP TestBot ACK :sasl")
mockbot.on_message("AUTHENTICATE +")

mockbot.on_message("AUTHENTICATE " + b64encode(b"e=some-error").decode("utf-8"))
assert (
len(mockbot.backend.message_sent) == 4
and mockbot.backend.message_sent[-1] == rawlist("AUTHENTICATE *")[0]
)


def test_sasl_scram_sha_256_error_server_final(mockbot: Sopel):
"""Verify the bot handles an error SCRAM-SHA-256 server_final correctly."""
mech = ScramMechanism()
salt, stored_key, server_key, iter_count = mech.make_auth_info(
"secret", iteration_count=5000
)
scram_server = mech.make_server(
lambda x: (salt, stored_key, server_key, iter_count)
)

mockbot.settings.core.auth_method = "sasl"
mockbot.settings.core.auth_target = "SCRAM-SHA-256"
mockbot.on_message("CAP * LS :sasl")
mockbot.on_message("CAP TestBot ACK :sasl")
mockbot.on_message("AUTHENTICATE +")

scram_server.set_client_first(
b64decode(mockbot.backend.message_sent[-1].split(b" ")[-1]).decode("utf-8")
)
mockbot.on_message(
"AUTHENTICATE "
+ b64encode(scram_server.get_server_first().encode("utf-8")).decode("utf-8")
)
scram_server.set_client_final(
b64decode(mockbot.backend.message_sent[-1].split(b" ")[-1]).decode("utf-8")
)
mockbot.on_message("AUTHENTICATE " + b64encode(b"e=some-error").decode("utf-8"))
assert (
len(mockbot.backend.message_sent) == 5
and mockbot.backend.message_sent[-1] == rawlist("AUTHENTICATE *")[0]
)
Loading

0 comments on commit 69812a6

Please sign in to comment.