Skip to content

Commit

Permalink
WordPress working except for the SSRF problem (#368)
Browse files Browse the repository at this point in the history
* Disable mastodon.py's version check for WordPress -- it cannot parse the version string
* Random OAuth token for WordPress
* Fix Mastodon userid regex
* In NodeWithMastodonAPI, distinguish between userid (the handle) and internal_userid (an integer); the latter is needed in Mastodon API calls
* In NodeWithMastodonAPI, use @Property and _property
* Don't allow all punctuation chars for auto-generated passwords
* Activate mastodon.py debug_requests when trace logging is active
* Add WordPress friends plugin by default on UBOS
* Better error messages when Node parameter fields or user attributes are missing or invalid
* Better logging and exceptions
* Lint improvement

Closes #284
Closes #285

---------

Co-authored-by: Johannes Ernst <git@j12t.org>
  • Loading branch information
jernst and Johannes Ernst authored Sep 30, 2024
1 parent cc671db commit 60c3b01
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 99 deletions.
28 changes: 16 additions & 12 deletions src/feditest/nodedrivers/fallback/fediverse.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,13 @@ def __init__(self, role: str | None, uri: str, actor_uri: str | None):


@staticmethod
def create_from_account_info_in_testplan(account_info_in_testplan: dict[str, str | None], node_driver: NodeDriver):
def create_from_account_info_in_testplan(account_info_in_testplan: dict[str, str | None], context_msg: str = ''):
"""
Parses the information provided in an "account" dict of TestPlanConstellationNode
"""
uri = URI_ACCOUNT_FIELD.get_validate_from_or_raise(account_info_in_testplan, f'NodeDriver { node_driver }: ')
actor_uri = ACTOR_URI_ACCOUNT_FIELD.get_validate_from_or_raise(account_info_in_testplan, f'NodeDriver { node_driver }: ')
role = ROLE_ACCOUNT_FIELD.get_validate_from(account_info_in_testplan, f'NodeDriver { node_driver }: ')
uri = URI_ACCOUNT_FIELD.get_validate_from_or_raise(account_info_in_testplan, context_msg)
actor_uri = ACTOR_URI_ACCOUNT_FIELD.get_validate_from_or_raise(account_info_in_testplan, context_msg)
role = ROLE_ACCOUNT_FIELD.get_validate_from(account_info_in_testplan, context_msg)

# If actor_uri was not given, we cannot perform a WebFinger query here: the Node may not exist yet

Expand Down Expand Up @@ -122,13 +122,13 @@ def __init__(self, role: str | None, uri: str, actor_uri: str | None):


@staticmethod
def create_from_non_existing_account_info_in_testplan(non_existing_account_info_in_testplan: dict[str, str | None], node_driver: NodeDriver):
def create_from_non_existing_account_info_in_testplan(non_existing_account_info_in_testplan: dict[str, str | None], context_msg: str = ''):
"""
Parses the information provided in an "non_existing_account" dict of TestPlanConstellationNode
"""
uri = URI_NON_EXISTING_ACCOUNT_FIELD.get_validate_from_or_raise(non_existing_account_info_in_testplan, f'NodeDriver { node_driver }: ')
actor_uri = ACTOR_URI_NON_EXISTING_ACCOUNT_FIELD.get_validate_from(non_existing_account_info_in_testplan, f'NodeDriver { node_driver }: ')
role = ROLE_NON_EXISTING_ACCOUNT_FIELD.get_validate_from(non_existing_account_info_in_testplan, f'NodeDriver { node_driver }: ')
uri = URI_NON_EXISTING_ACCOUNT_FIELD.get_validate_from_or_raise(non_existing_account_info_in_testplan, context_msg)
actor_uri = ACTOR_URI_NON_EXISTING_ACCOUNT_FIELD.get_validate_from(non_existing_account_info_in_testplan, context_msg)
role = ROLE_NON_EXISTING_ACCOUNT_FIELD.get_validate_from(non_existing_account_info_in_testplan, context_msg)

# We cannot perform a WebFinger query: account does not exist

Expand Down Expand Up @@ -314,13 +314,17 @@ def create_configuration_account_manager(self, rolename: str, test_plan_node: Te

accounts : list[Account] = []
if test_plan_node.accounts:
for account_info in test_plan_node.accounts:
accounts.append(FallbackFediverseAccount.create_from_account_info_in_testplan(account_info, self))
for index, account_info in enumerate(test_plan_node.accounts):
accounts.append(FallbackFediverseAccount.create_from_account_info_in_testplan(
account_info,
f'Constellation role "{ rolename }", NodeDriver "{ self }, Account { index }: '))

non_existing_accounts : list[NonExistingAccount] = []
if test_plan_node.non_existing_accounts:
for non_existing_account_info in test_plan_node.non_existing_accounts:
non_existing_accounts.append(FallbackFediverseNonExistingAccount.create_from_non_existing_account_info_in_testplan(non_existing_account_info, self))
for index, non_existing_account_info in enumerate(test_plan_node.non_existing_accounts):
non_existing_accounts.append(FallbackFediverseNonExistingAccount.create_from_non_existing_account_info_in_testplan(
non_existing_account_info,
f'Constellation role "{ rolename }", NodeDriver "{ self }, Non-existing account { index }: '))

return (
NodeConfiguration(
Expand Down
119 changes: 77 additions & 42 deletions src/feditest/nodedrivers/mastodon/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
)
from feditest.protocols.activitypub import AnyObject
from feditest.protocols.fediverse import FediverseNode
from feditest.reporting import trace
from feditest.testplan import TestPlanConstellationNode, TestPlanNodeAccountField, TestPlanNodeNonExistingAccountField, TestPlanNodeParameter
from feditest.reporting import is_trace_active, trace
from feditest.testplan import InvalidAccountSpecificationException, TestPlanConstellationNode, TestPlanNodeAccountField, TestPlanNodeNonExistingAccountField, TestPlanNodeParameter
from feditest.utils import boolean_parse_validate, email_validate, find_first_in_array, hostname_validate


Expand Down Expand Up @@ -73,7 +73,7 @@ def _userid_validate(candidate: str) -> str | None:
FIXME this is a wild guess and can be better.
"""
candidate = candidate.strip()
return candidate if re.match(r'[a-zA-Z0-9_]', candidate) else None
return candidate if re.match(r'[a-zA-Z0-9_]+', candidate) else None


def _password_validate(candidate: str) -> str | None:
Expand Down Expand Up @@ -141,9 +141,28 @@ def create(api_base_url: str, session: requests.Session) -> 'MastodonOAuthApp':


class AccountOnNodeWithMastodonAPI(Account): # this is intended to be abstract
def __init__(self, role: str | None, userid: str):
def __init__(self, role: str | None, userid: str, internal_userid: int | None = None):
"""
userid: the string representing the user, e.g. "joe"
internal_userid: the id of the user object in the API, e.g. 1
"""
super().__init__(role)
self.userid = userid
self._userid = userid
self._internal_userid = internal_userid


@property
def userid(self):
return self._userid


@property
def internal_userid(self) -> int:
if not self._internal_userid:
mastodon_client = self.mastodon_user_client()
actor = mastodon_client.account_verify_credentials()
self._internal_userid = cast(int, actor.id)
return self._internal_userid


@property
Expand All @@ -152,28 +171,34 @@ def webfinger_uri(self):


@abstractmethod
def mastodon_user_client(self, node: 'NodeWithMastodonAPI') -> Mastodon:
def mastodon_user_client(self) -> Mastodon:
...


class MastodonAccount(AccountOnNodeWithMastodonAPI): # this is intended to be abstract
@staticmethod
def create_from_account_info_in_testplan(account_info_in_testplan: dict[str, str | None], node_driver: NodeDriver):
def create_from_account_info_in_testplan(account_info_in_testplan: dict[str, str | None], context_msg: str = ''):
"""
Parses the information provided in an "account" dict of TestPlanConstellationNode
"""
userid = USERID_ACCOUNT_FIELD.get_validate_from_or_raise(account_info_in_testplan, f'NodeDriver { node_driver }: ')
role = ROLE_ACCOUNT_FIELD.get_validate_from(account_info_in_testplan, f'NodeDriver { node_driver }: ')
userid = USERID_ACCOUNT_FIELD.get_validate_from_or_raise(account_info_in_testplan, context_msg)
role = ROLE_ACCOUNT_FIELD.get_validate_from(account_info_in_testplan, context_msg)
oauth_token = OAUTH_TOKEN_ACCOUNT_FIELD.get_validate_from(account_info_in_testplan, context_msg)

oauth_token = OAUTH_TOKEN_ACCOUNT_FIELD.get_validate_from(account_info_in_testplan, f'NodeDriver { node_driver }: ')
if oauth_token:
# FIXME: Raise error if email or password are given
if EMAIL_ACCOUNT_FIELD.name in account_info_in_testplan:
raise InvalidAccountSpecificationException(
account_info_in_testplan,
f'Specify { OAUTH_TOKEN_ACCOUNT_FIELD.name } or { EMAIL_ACCOUNT_FIELD.name }, not both.')
if PASSWORD_ACCOUNT_FIELD.name in account_info_in_testplan:
raise InvalidAccountSpecificationException(
account_info_in_testplan,
f'Specify { OAUTH_TOKEN_ACCOUNT_FIELD.name } or { PASSWORD_ACCOUNT_FIELD.name }, not both.')
return MastodonOAuthTokenAccount(role, userid, oauth_token)

else:
email = EMAIL_ACCOUNT_FIELD.get_validate_from_or_raise(account_info_in_testplan, f'NodeDriver { node_driver }: ')
password = PASSWORD_ACCOUNT_FIELD.get_validate_from_or_raise(account_info_in_testplan, f'NodeDriver { node_driver }: ')
return MastodonUserPasswordAccount(role, userid, password, email)
email = EMAIL_ACCOUNT_FIELD.get_validate_from_or_raise(account_info_in_testplan, context_msg)
password = PASSWORD_ACCOUNT_FIELD.get_validate_from_or_raise(account_info_in_testplan, context_msg)
return MastodonUserPasswordAccount(role, userid, password, email)


@property
Expand All @@ -182,26 +207,27 @@ def actor_uri(self):


class MastodonUserPasswordAccount(MastodonAccount):
def __init__(self, role: str | None, userid: str, password: str, email: str):
super().__init__(role, userid)
self.password = password
self.email = email
def __init__(self, role: str | None, username: str, password: str, email: str):
super().__init__(role, username)
self._password = password
self._email = email
self._mastodon_user_client: Mastodon | None = None # Allocated as needed


# Python 3.12 @override
def mastodon_user_client(self, node: 'NodeWithMastodonAPI') -> Mastodon:
def mastodon_user_client(self) -> Mastodon:
if self._mastodon_user_client is None:
oauth_app = cast(MastodonOAuthApp,node._mastodon_oauth_app)
trace(f'Logging into Mastodon at "{ oauth_app.api_base_url }" as "{ self.email }" with password.')
node = cast(NodeWithMastodonAPI, self._node)
oauth_app = cast(MastodonOAuthApp, node._mastodon_oauth_app)
trace(f'Logging into Mastodon at "{ oauth_app.api_base_url }" as "{ self._email }" with password.')
client = Mastodon(
client_id = oauth_app.client_id,
client_secret = oauth_app.client_secret,
api_base_url = oauth_app.api_base_url,
session = oauth_app.session
# , debug_requests = True
session = oauth_app.session,
debug_requests = is_trace_active()
)
client.log_in(username = self.email, password = self.password) # returns the token
client.log_in(username = self._email, password = self._password) # returns the token

self._mastodon_user_client = client

Expand All @@ -214,22 +240,23 @@ class MastodonOAuthTokenAccount(MastodonAccount):
"""
def __init__(self, role: str | None, userid: str, oauth_token: str):
super().__init__(role, userid)
self.oauth_token = oauth_token
self._oauth_token = oauth_token
self._mastodon_user_client: Mastodon | None = None # Allocated as needed


# Python 3.12 @override
def mastodon_user_client(self, node: 'NodeWithMastodonAPI') -> Mastodon:
def mastodon_user_client(self) -> Mastodon:
if self._mastodon_user_client is None:
oauth_app = cast(MastodonOAuthApp,node._mastodon_oauth_app)
node = cast(NodeWithMastodonAPI, self._node)
oauth_app = cast(MastodonOAuthApp, node._mastodon_oauth_app)
trace(f'Logging into Mastodon at "{ oauth_app.api_base_url }" with userid "{ self.userid }" with OAuth token.')
client = Mastodon(
client_id = oauth_app.client_id,
client_secret=oauth_app.client_secret,
access_token=self.oauth_token,
access_token=self._oauth_token,
api_base_url=oauth_app.api_base_url,
session=oauth_app.session
# , debug_requests = True
session=oauth_app.session,
debug_requests = is_trace_active()
)
self._mastodon_user_client = client
return self._mastodon_user_client
Expand All @@ -238,28 +265,32 @@ def mastodon_user_client(self, node: 'NodeWithMastodonAPI') -> Mastodon:
class MastodonNonExistingAccount(NonExistingAccount):
def __init__(self, role: str | None, userid: str):
super().__init__(role)
self.userid = userid
self._userid = userid


@staticmethod
def create_from_non_existing_account_info_in_testplan(non_existing_account_info_in_testplan: dict[str, str | None], node_driver: NodeDriver):
def create_from_non_existing_account_info_in_testplan(non_existing_account_info_in_testplan: dict[str, str | None], context_msg: str = ''):
"""
Parses the information provided in an "non_existing_account" dict of TestPlanConstellationNode
"""
userid = USERID_NON_EXISTING_ACCOUNT_FIELD.get_validate_from_or_raise(non_existing_account_info_in_testplan, f'NodeDriver { node_driver }: ')
role = ROLE_ACCOUNT_FIELD.get_validate_from(non_existing_account_info_in_testplan, f'NodeDriver { node_driver }: ')

userid = USERID_NON_EXISTING_ACCOUNT_FIELD.get_validate_from_or_raise(non_existing_account_info_in_testplan, context_msg)
role = ROLE_ACCOUNT_FIELD.get_validate_from(non_existing_account_info_in_testplan, context_msg)
return MastodonNonExistingAccount(role, userid)


@property
def userid(self):
return self._userid


@property
def webfinger_uri(self):
return f'acct:{ self.userid }@{ self.node.hostname }'


@property
def actor_uri(self):
return f'https://{ self.node.hostname }/users/{ self.userid }'
return f'https://{ self.node.hostname }/users/{ self._userid }'


class NodeWithMastodonApiConfiguration(NodeConfiguration):
Expand Down Expand Up @@ -619,7 +650,7 @@ def _get_mastodon_client_by_actor_uri(self, actor_uri: str) -> Mastodon:
if account is None:
raise Exception(f'On Node { self }, failed to find account with userid "{ userid }".')

ret = cast(MastodonAccount, account).mastodon_user_client(self)
ret = cast(MastodonAccount, account).mastodon_user_client()
return ret


Expand Down Expand Up @@ -738,13 +769,17 @@ def create_configuration_account_manager(self, rolename: str, test_plan_node: Te

accounts : list[Account] = []
if test_plan_node.accounts:
for account_info in test_plan_node.accounts:
accounts.append(MastodonAccount.create_from_account_info_in_testplan(account_info, self))
for index, account_info in enumerate(test_plan_node.accounts):
accounts.append(MastodonAccount.create_from_account_info_in_testplan(
account_info,
f'Constellation role "{ rolename }", NodeDriver "{ self }, Account { index }: '))

non_existing_accounts : list[NonExistingAccount] = []
if test_plan_node.non_existing_accounts:
for non_existing_account_info in test_plan_node.non_existing_accounts:
non_existing_accounts.append(MastodonNonExistingAccount.create_from_non_existing_account_info_in_testplan(non_existing_account_info, self))
for index, non_existing_account_info in enumerate(test_plan_node.non_existing_accounts):
non_existing_accounts.append(MastodonNonExistingAccount.create_from_non_existing_account_info_in_testplan(
non_existing_account_info,
f'Constellation role "{ rolename }", NodeDriver "{ self }, Non-existing account { index }: '))

return (
NodeWithMastodonApiConfiguration(
Expand Down
12 changes: 8 additions & 4 deletions src/feditest/nodedrivers/mastodon/ubos.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,13 +258,17 @@ def test_plan_node_non_existing_account_fields() -> list[TestPlanNodeNonExisting
def create_configuration_account_manager(self, rolename: str, test_plan_node: TestPlanConstellationNode) -> tuple[NodeConfiguration, AccountManager | None]:
accounts : list[Account] = []
if test_plan_node.accounts:
for account_info in test_plan_node.accounts:
accounts.append(MastodonAccount.create_from_account_info_in_testplan(account_info, self))
for index, account_info in enumerate(test_plan_node.accounts):
accounts.append(MastodonAccount.create_from_account_info_in_testplan(
account_info,
f'Constellation role "{ rolename }", NodeDriver "{ self }, Account { index }: '))

non_existing_accounts : list[NonExistingAccount] = []
if test_plan_node.non_existing_accounts:
for non_existing_account_info in test_plan_node.non_existing_accounts:
non_existing_accounts.append(MastodonNonExistingAccount.create_from_non_existing_account_info_in_testplan(non_existing_account_info, self))
for index, non_existing_account_info in enumerate(test_plan_node.non_existing_accounts):
non_existing_accounts.append(MastodonNonExistingAccount.create_from_non_existing_account_info_in_testplan(
non_existing_account_info,
f'Constellation role "{ rolename }", NodeDriver "{ self }, Non-existing account { index }: '))

# Once has the Node has been instantiated (we can't do that here yet): if the user did not specify at least one Account, we add the admin account

Expand Down
Loading

0 comments on commit 60c3b01

Please sign in to comment.