From 60c3b01c9cb4c310f69ccf63547886ef2df4af9f Mon Sep 17 00:00:00 2001 From: Johannes Ernst Date: Sun, 29 Sep 2024 18:26:17 -0700 Subject: [PATCH] WordPress working except for the SSRF problem (#368) * 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 --- .../nodedrivers/fallback/fediverse.py | 28 +++-- src/feditest/nodedrivers/mastodon/__init__.py | 119 +++++++++++------- src/feditest/nodedrivers/mastodon/ubos.py | 12 +- .../nodedrivers/wordpress/__init__.py | 65 +++++----- src/feditest/nodedrivers/wordpress/ubos.py | 26 ++-- src/feditest/ubos/__init__.py | 2 +- ...40_ubos_mastodon_accounts_from_testplan.py | 6 +- 7 files changed, 159 insertions(+), 99 deletions(-) diff --git a/src/feditest/nodedrivers/fallback/fediverse.py b/src/feditest/nodedrivers/fallback/fediverse.py index e6def58..f64b3d9 100644 --- a/src/feditest/nodedrivers/fallback/fediverse.py +++ b/src/feditest/nodedrivers/fallback/fediverse.py @@ -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 @@ -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 @@ -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( diff --git a/src/feditest/nodedrivers/mastodon/__init__.py b/src/feditest/nodedrivers/mastodon/__init__.py index 42499f7..f7d2742 100644 --- a/src/feditest/nodedrivers/mastodon/__init__.py +++ b/src/feditest/nodedrivers/mastodon/__init__.py @@ -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 @@ -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: @@ -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 @@ -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 @@ -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 @@ -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 @@ -238,20 +265,24 @@ 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 }' @@ -259,7 +290,7 @@ def webfinger_uri(self): @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): @@ -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 @@ -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( diff --git a/src/feditest/nodedrivers/mastodon/ubos.py b/src/feditest/nodedrivers/mastodon/ubos.py index f9e4d75..d3a8251 100644 --- a/src/feditest/nodedrivers/mastodon/ubos.py +++ b/src/feditest/nodedrivers/mastodon/ubos.py @@ -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 diff --git a/src/feditest/nodedrivers/wordpress/__init__.py b/src/feditest/nodedrivers/wordpress/__init__.py index 1e0f602..8012fbe 100644 --- a/src/feditest/nodedrivers/wordpress/__init__.py +++ b/src/feditest/nodedrivers/wordpress/__init__.py @@ -23,6 +23,7 @@ HOSTNAME_PAR ) from feditest.protocols.fediverse import FediverseNode +from feditest.reporting import is_trace_active, trace from feditest.testplan import TestPlanConstellationNode, TestPlanNodeAccountField, TestPlanNodeNonExistingAccountField, TestPlanNodeParameter from feditest.utils import boolean_parse_validate, hostname_validate @@ -83,24 +84,23 @@ class WordPressAccount(AccountOnNodeWithMastodonAPI): """ Compare with MastodonOAuthTokenAccount. """ - def __init__(self, role: str | None, userid: str, oauth_token: str | None): + def __init__(self, role: str | None, userid: str, oauth_token: str | None, internal_userid: int | None = None): """ The oauth_token may be None. In which case we dynamically obtain one. """ - super().__init__(role, userid) - self.oauth_token = oauth_token + super().__init__(role, userid, internal_userid) + self._oauth_token = oauth_token self._mastodon_user_client: Mastodon | None = None # Allocated as needed @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 }: ') - - oauth_token = OAUTH_TOKEN_ACCOUNT_FIELD.get_validate_from_or_raise(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_or_raise(account_info_in_testplan, context_msg) return WordPressAccount(role, userid, oauth_token) @@ -109,29 +109,33 @@ def actor_uri(self): return f'https://{ self.node.hostname }/author/{ self.userid }/' - 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) - self._ensure_oauth_token(node, oauth_app.client_id) + node = cast(NodeWithMastodonAPI, self._node) + oauth_app = cast(MastodonOAuthApp, node._mastodon_oauth_app) + self._ensure_oauth_token(oauth_app.client_id) + trace(f'Logging into Mastodon at "{ oauth_app.api_base_url }" with userid "{ self.userid }" with OAuth token "{ self._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 + session=oauth_app.session, + version_check_mode='none', # mastodon.py cannot parse this version string, e.g. "WordPress/6.5.3, EMA/0.9.4" instead of Mastodon's "4.1.12" + debug_requests = is_trace_active() ) self._mastodon_user_client = client return self._mastodon_user_client - def _ensure_oauth_token(self, node: NodeWithMastodonAPI, oauth_client_id: str) -> None: + def _ensure_oauth_token(self, oauth_client_id: str) -> None: """ Helper to dynamically provision an OAuth token if we don't have one yet. """ - if self.oauth_token: + if self._oauth_token: return - real_node = cast(WordPressPlusActivityPubPluginNode, node) - self.oauth_token = real_node._provision_oauth_token_for(self.userid, oauth_client_id) + real_node = cast(WordPressPlusActivityPubPluginNode, self._node) + self._oauth_token = real_node._provision_oauth_token_for(self, oauth_client_id) class WordPressNonExistingAccount(NonExistingAccount): @@ -141,13 +145,12 @@ def __init__(self, role: str | None, userid: str): @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 WordPressNonExistingAccount(role, userid) @@ -173,10 +176,10 @@ def _actor_uri_to_userid(self, actor_uri: str) -> str: raise ValueError( f'Cannot find actor at this node: { actor_uri }' ) - def _provision_oauth_token_for(self, userid: str, oauth_client_id: str): - ret = self.prompt_user(f'Enter the OAuth token for the Mastodon API for user "{ userid }"' - + f' on constellation role "{ self.rolename }" (user field "{ OAUTH_TOKEN_ACCOUNT_FIELD }"): ', - parse_validate=_oauth_token_validate) + def _provision_oauth_token_for(self, account: WordPressAccount, oauth_client_id: str) -> str: + ret = cast(str, self.prompt_user(f'Enter the OAuth token for the Mastodon API for user "{ account.userid }"' + + f' on constellation role "{ self.rolename }", OAuth client id "{ oauth_client_id }" (user field "{ OAUTH_TOKEN_ACCOUNT_FIELD }"): ', + parse_validate=_oauth_token_validate)) return ret @@ -216,13 +219,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(WordPressAccount.create_from_account_info_in_testplan(account_info, self)) + for index, account_info in enumerate(test_plan_node.accounts): + accounts.append(WordPressAccount.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(WordPressNonExistingAccount.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(WordPressNonExistingAccount.create_from_non_existing_account_info_in_testplan( + non_existing_account_info, + f'Constellation role "{ rolename }", NodeDriver "{ self }, Non-existing account { index }: ')) return ( NodeWithMastodonApiConfiguration( diff --git a/src/feditest/nodedrivers/wordpress/ubos.py b/src/feditest/nodedrivers/wordpress/ubos.py index dcd44d1..12d0003 100644 --- a/src/feditest/nodedrivers/wordpress/ubos.py +++ b/src/feditest/nodedrivers/wordpress/ubos.py @@ -1,6 +1,7 @@ """ """ +import os from typing import cast from feditest.nodedrivers.mastodon.ubos import MastodonUbosNodeConfiguration @@ -41,7 +42,7 @@ def set_node(self, node: Node) -> None: if not self._accounts_allocated_to_role and not self._accounts_not_allocated_to_role: config = cast(UbosNodeConfiguration, node.config) - admin_account = WordPressAccount(None, config.admin_userid, None) + admin_account = WordPressAccount(None, config.admin_userid, None, 1) # We know this is account with internal identifier 1 admin_account.set_node(node) self._accounts_not_allocated_to_role.append(admin_account) @@ -75,16 +76,18 @@ def remove_cert_from_trust_store(self, root_cert: str) -> None: # Python 3.12 @override - def _provision_oauth_token_for(self, userid: str, oauth_client_id: str): + def _provision_oauth_token_for(self, account: WordPressAccount, oauth_client_id: str) -> str : # Code from here: https://wordpress.org/support/topic/programmatically-obtaining-oauth-token-for-testing/ # $desired_token = '123'; # $user_id = 1; # $oauth = new Enable_Mastodon_Apps\Mastodon_OAuth(); # $oauth->get_token_storage()->setAccessToken( $desired_token, $app->get_client_id(), $user_id, time() + HOUR_IN_SECONDS, $app->get_scopes() ); + trace(f'Provisioning OAuth token on {self} for user with name="{ account.userid }".') config = cast(UbosNodeConfiguration, self.config) node_driver = cast(WordPressPlusActivityPubPluginUbosNodeDriver, self.node_driver) + token = os.urandom(16).hex() php_script = f""" get_token_storage()->setAccessToken( "11223344", "{ oauth_client_id }", "{ userid }", time() + HOUR_IN_SECONDS, 'read write follow push' ); +$oauth->get_token_storage()->setAccessToken( "{ token }", "{ oauth_client_id }", { account.internal_userid }, time() + HOUR_IN_SECONDS, 'read write follow push' ); """ dir = f'/ubos/http/sites/{ config.siteid }' cmd = f'cd { dir } && sudo sudo -u http php' # from user ubosdev -> root -> http + trace( f'PHP script is "{ php_script }"') if node_driver._exec_shell(cmd, config.rshcmd, stdin_content=php_script).returncode: - raise Exception(self, f"Failed to create OAuth token for user { userid }, cmd: { cmd }") + raise Exception(self, f'Failed to create OAuth token for user with id="{ account.userid }", cmd: { cmd }"') + return token class WordPressPlusActivityPubPluginUbosNodeDriver(UbosNodeDriver): @@ -121,13 +126,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(WordPressAccount.create_from_account_info_in_testplan(account_info, self)) + for index, account_info in enumerate(test_plan_node.accounts): + accounts.append(WordPressAccount.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(WordPressNonExistingAccount.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(WordPressNonExistingAccount.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 @@ -140,6 +149,7 @@ def create_configuration_account_manager(self, rolename: str, test_plan_node: Te "accessoryids" : [ "wordpress-plugin-activitypub", "wordpress-plugin-enable-mastodon-apps", + "wordpress-plugin-friends", "wordpress-plugin-webfinger" ], "context" : "" diff --git a/src/feditest/ubos/__init__.py b/src/feditest/ubos/__init__.py index f2c3380..b67f658 100644 --- a/src/feditest/ubos/__init__.py +++ b/src/feditest/ubos/__init__.py @@ -173,7 +173,7 @@ def _generate_appconfigid(): @staticmethod def _generate_credential(): - chars = string.ascii_letters + string.digits + string.punctuation + chars = string.ascii_letters + string.digits + "_-%" ret = ''.join(random.choice(chars) for i in range(16)) return ret diff --git a/tests.unit/unit/test_40_ubos_mastodon_accounts_from_testplan.py b/tests.unit/unit/test_40_ubos_mastodon_accounts_from_testplan.py index eb2892e..d9b3d3d 100644 --- a/tests.unit/unit/test_40_ubos_mastodon_accounts_from_testplan.py +++ b/tests.unit/unit/test_40_ubos_mastodon_accounts_from_testplan.py @@ -85,15 +85,15 @@ def test_parse(the_test_plan: TestPlan) -> None: assert acc1.role == 'role1' assert acc1.userid == 'foo' assert isinstance(acc1, MastodonUserPasswordAccount) - assert acc1.email == 'foo@bar.com' - assert acc1.password == 'verysecret' + assert acc1._email == 'foo@bar.com' + assert acc1._password == 'verysecret' acc2 = cast(MastodonAccount | None, account_manager.get_account_by_role('role2')) assert acc2 assert acc2.role == 'role2' assert acc2.userid == 'bar' assert isinstance(acc2, MastodonOAuthTokenAccount) - assert acc2.oauth_token == 'tokentokentoken' + assert acc2._oauth_token == 'tokentokentoken' non_acc1 = cast(MastodonNonExistingAccount | None, account_manager.get_non_existing_account_by_role('nonrole1')) assert non_acc1