From f112b9a7159b25c630a52b05dc27dfc2f9b5d694 Mon Sep 17 00:00:00 2001 From: Felipe Martin <812088+fmartingr@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:41:14 +0200 Subject: [PATCH] Remove deprecated built-in Slack import API and CLI (#37999) The webapp Slack import path was deprecated in v6.0 in favor of mmetl and Mattermost bulk import; remove the leftover API, importer package, CLI command, and import_team permission. Co-authored-by: Cursor --- api/v4/source/teams.yaml | 56 - .../comply/embedded-json-audit-log-schema.mdx | 8 - .../manage/command-line-tools.mdx | 18 - ...ced-permissions-backend-infrastructure.mdx | 8 - .../support/api/default_roles_permissions.js | 4 +- server/.golangci.yml | 1 - server/channels/api4/team.go | 99 +- server/channels/api4/team_test.go | 93 -- server/channels/app/app_test.go | 1 - server/channels/app/permissions_migrations.go | 10 + server/channels/app/slack.go | 64 - server/channels/testlib/store.go | 1 + server/cmd/mattermost/commands/import.go | 75 -- server/i18n/en.json | 124 -- .../services/slackimport/converters.go | 150 --- .../services/slackimport/main_test.go | 31 - .../platform/services/slackimport/parsers.go | 50 - .../services/slackimport/slackimport.go | 873 -------------- .../services/slackimport/slackimport_test.go | 1042 ----------------- server/public/model/audit_events.go | 2 - server/public/model/client4.go | 48 - server/public/model/migration.go | 1 + server/public/model/permission.go | 8 - server/public/model/role.go | 1 - server/tests/slack-import-test-channels.json | 165 --- .../slack-import-test-direct-messages.json | 34 - ...ack-import-test-group-direct-messages.json | 69 -- server/tests/slack-import-test-posts.json | 288 ----- .../slack-import-test-private-channels.json | 25 - server/tests/slack-import-test-users.json | 363 ------ .../strings/permissions.tsx | 10 - webapp/channels/src/i18n/en.json | 2 - .../src/constants/permissions.ts | 1 - webapp/channels/src/utils/constants.tsx | 2 - 34 files changed, 16 insertions(+), 3711 deletions(-) delete mode 100644 server/platform/services/slackimport/converters.go delete mode 100644 server/platform/services/slackimport/main_test.go delete mode 100644 server/platform/services/slackimport/parsers.go delete mode 100644 server/platform/services/slackimport/slackimport.go delete mode 100644 server/platform/services/slackimport/slackimport_test.go delete mode 100644 server/tests/slack-import-test-channels.json delete mode 100644 server/tests/slack-import-test-direct-messages.json delete mode 100644 server/tests/slack-import-test-group-direct-messages.json delete mode 100644 server/tests/slack-import-test-posts.json delete mode 100644 server/tests/slack-import-test-private-channels.json delete mode 100644 server/tests/slack-import-test-users.json diff --git a/api/v4/source/teams.yaml b/api/v4/source/teams.yaml index cf5ab8266cd4..293650accab0 100644 --- a/api/v4/source/teams.yaml +++ b/api/v4/source/teams.yaml @@ -1572,62 +1572,6 @@ $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" - "/api/v4/teams/{team_id}/import": - post: - tags: - - teams - summary: Import a Team from other application - description: > - Import a team into a existing team. Import users, channels, posts, - hooks. - - ##### Permissions - - Must have `permission_import_team` permission. - operationId: ImportTeam - parameters: - - name: team_id - in: path - description: Team GUID - required: true - schema: - type: string - requestBody: - content: - multipart/form-data: - schema: - type: object - properties: - file: - description: A file to be uploaded in zip format. - type: string - format: binary - filesize: - description: The size of the zip file to be imported. - type: integer - importFrom: - description: String that defines from which application the team was - exported to be imported into Mattermost. - type: string - required: - - file - - filesize - - importFrom - responses: - "200": - description: JSON object containing a base64 encoded text file of the import logs - in its `results` property. - content: - application/json: - schema: - type: object - properties: - results: - type: string - "400": - $ref: "#/components/responses/BadRequest" - "403": - $ref: "#/components/responses/Forbidden" "/api/v4/teams/invite/{invite_id}": get: tags: diff --git a/docs/main/administration-guide/comply/embedded-json-audit-log-schema.mdx b/docs/main/administration-guide/comply/embedded-json-audit-log-schema.mdx index a62ddd703da0..612529120c88 100644 --- a/docs/main/administration-guide/comply/embedded-json-audit-log-schema.mdx +++ b/docs/main/administration-guide/comply/embedded-json-audit-log-schema.mdx @@ -439,10 +439,6 @@ The following tables list the comprehensive audit event types (`event_name` valu Deleting teams -importTeam -Importing team data - - invalidateAllEmailInvites Invalidating all email invitations @@ -1484,10 +1480,6 @@ From Mattermost v11.5.0, audit log entries for posts and content access events i scheduleExport Scheduling export operations - -slackImport -Slack data import operations - diff --git a/docs/main/administration-guide/manage/command-line-tools.mdx b/docs/main/administration-guide/manage/command-line-tools.mdx index 9511d00bd05d..1dc0b5142730 100644 --- a/docs/main/administration-guide/manage/command-line-tools.mdx +++ b/docs/main/administration-guide/manage/command-line-tools.mdx @@ -320,29 +320,11 @@ Import data into Mattermost. Child Command - [mattermost import bulk](#mattermost-import-bulk) - Import a Mattermost Bulk Import File. Deprecated in favor of [mmctl import commands](/administration-guide/manage/mmctl-command-line-tool#mmctl-import). -- [mattermost import slack](#mattermost-import-slack) - Import a team from Slack. ### mattermost import bulk From Mattermost v6.0, this command has been deprecated in favor of [mmctl import commands](/administration-guide/manage/mmctl-command-line-tool#mmctl-import) as the supported way to import data into Mattermost. -### mattermost import slack - -See the [mmctl import commands](/administration-guide/manage/mmctl-command-line-tool#mmctl-import) documentation as the preferred way to import Slack data into Mattermost. - -Description -Import a team from a Slack export zip file. - -Format -. code-block:: sh - -mattermost import slack {team} {file} - -Example -``` sh -bin/mattermost import slack myteam slack_export.zip -``` - ------------------------------------------------------------------------------------------------------------------------ ## mattermost jobserver diff --git a/docs/main/administration-guide/onboard/advanced-permissions-backend-infrastructure.mdx b/docs/main/administration-guide/onboard/advanced-permissions-backend-infrastructure.mdx index b6a9f77684c1..81881a5e09bf 100644 --- a/docs/main/administration-guide/onboard/advanced-permissions-backend-infrastructure.mdx +++ b/docs/main/administration-guide/onboard/advanced-permissions-backend-infrastructure.mdx @@ -387,12 +387,6 @@ Permissions in Mattermost are a property of the server code base and are not cre -import_team -system -Import teams in Team Settings. - - - view_team team Read the Team object. @@ -802,7 +796,6 @@ The following built-in roles with default permissions are available: - sysconsole_read_user_management_permissions - manage_public_channel_members - sysconsole_write_about -- import_team - sysconsole_write_user_management_system_roles - sysconsole_read_reporting - upload_file @@ -1021,7 +1014,6 @@ The following built-in roles with default permissions are available: - manage_private_channel_members - convert_public_channel_to_private - add_reaction -- import_team - add_bookmark_public_channel - edit_bookmark_public_channel - delete_bookmark_public_channel diff --git a/e2e-tests/cypress/tests/support/api/default_roles_permissions.js b/e2e-tests/cypress/tests/support/api/default_roles_permissions.js index 33aeaa48c574..89b5acdb8b05 100644 --- a/e2e-tests/cypress/tests/support/api/default_roles_permissions.js +++ b/e2e-tests/cypress/tests/support/api/default_roles_permissions.js @@ -12,7 +12,7 @@ export const defaultRolesPermissions = { playbook_member: 'playbook_private_manage_members playbook_private_manage_properties playbook_private_view playbook_public_manage_members playbook_public_manage_properties playbook_public_view run_create', run_admin: 'run_manage_members run_manage_properties', run_member: 'run_view', - system_admin: 'add_bookmark_private_channel add_bookmark_public_channel add_ldap_private_cert add_ldap_public_cert add_reaction add_saml_idp_cert add_saml_private_cert add_saml_public_cert add_user_to_team assign_bot assign_system_admin_role bypass_incoming_webhook_channel_lock convert_private_channel_to_public convert_public_channel_to_private create_bot create_compliance_export_job create_custom_group create_data_retention_job create_direct_channel create_elasticsearch_post_aggregation_job create_elasticsearch_post_indexing_job create_emojis create_group_channel create_ldap_sync_job create_post create_post_ephemeral create_post_public create_private_channel create_public_channel create_team create_user_access_token delete_bookmark_private_channel delete_bookmark_public_channel delete_custom_group delete_emojis delete_others_emojis delete_others_posts delete_post delete_private_channel delete_public_channel demote_to_guest download_compliance_export_result edit_bookmark_private_channel edit_bookmark_public_channel edit_brand edit_custom_group edit_file_attachment edit_other_users edit_others_posts edit_post get_analytics get_logs get_public_link get_saml_cert_status get_saml_metadata_from_idp import_team invalidate_caches invalidate_email_invite invite_guest invite_user join_private_teams join_public_channels join_public_teams list_private_teams list_public_teams list_team_channels list_users_without_team manage_bots manage_channel_access_rules manage_channel_join_requests manage_channel_roles manage_compliance_export_job manage_custom_group_members manage_data_retention_job manage_elasticsearch_post_aggregation_job manage_elasticsearch_post_indexing_job manage_jobs manage_ldap_sync_job manage_license_information manage_oauth manage_others_agent manage_others_bots manage_others_incoming_webhooks manage_others_outgoing_webhooks manage_others_slash_commands manage_outgoing_oauth_connections manage_own_agent manage_own_incoming_webhooks manage_own_outgoing_webhooks manage_own_slash_commands manage_private_channel_auto_translation manage_private_channel_banner manage_private_channel_discoverability manage_private_channel_members manage_private_channel_properties manage_public_channel_auto_translation manage_public_channel_banner manage_public_channel_members manage_public_channel_properties manage_roles manage_secure_connections manage_shared_channels manage_system manage_system_wide_oauth manage_team manage_team_access_rules manage_team_roles order_bookmark_private_channel order_bookmark_public_channel playbook_private_create playbook_private_make_public playbook_private_manage_members playbook_private_manage_properties playbook_private_manage_roles playbook_private_view playbook_public_create playbook_public_make_private playbook_public_manage_members playbook_public_manage_properties playbook_public_manage_roles playbook_public_view promote_guest purge_elasticsearch_indexes read_audits read_bots read_channel read_channel_content read_compliance_export_job read_data_retention_job read_deleted_posts read_elasticsearch_post_aggregation_job read_elasticsearch_post_indexing_job read_jobs read_ldap_sync_job read_license_information read_other_users_teams read_others_bots read_private_channel_groups read_public_channel read_public_channel_groups read_user_access_token recycle_database_connections reload_config remove_ldap_private_cert remove_ldap_public_cert remove_others_reactions remove_reaction remove_saml_idp_cert remove_saml_private_cert remove_saml_public_cert remove_user_from_team restore_custom_group revoke_user_access_token run_create run_manage_members run_manage_properties run_view sysconsole_read_about_edition_and_license sysconsole_read_ai_recaps sysconsole_read_authentication_email sysconsole_read_authentication_guest_access sysconsole_read_authentication_ldap sysconsole_read_authentication_mfa sysconsole_read_authentication_openid sysconsole_read_authentication_password sysconsole_read_authentication_saml sysconsole_read_authentication_signup sysconsole_read_billing sysconsole_read_compliance_compliance_export sysconsole_read_compliance_compliance_monitoring sysconsole_read_compliance_custom_terms_of_service sysconsole_read_compliance_data_retention_policy sysconsole_read_environment_database sysconsole_read_environment_developer sysconsole_read_environment_elasticsearch sysconsole_read_environment_file_storage sysconsole_read_environment_high_availability sysconsole_read_environment_image_proxy sysconsole_read_environment_logging sysconsole_read_environment_mobile_security sysconsole_read_environment_performance_monitoring sysconsole_read_environment_push_notification_server sysconsole_read_environment_rate_limiting sysconsole_read_environment_session_lengths sysconsole_read_environment_smtp sysconsole_read_environment_web_server sysconsole_read_experimental_feature_flags sysconsole_read_experimental_features sysconsole_read_integrations_bot_accounts sysconsole_read_integrations_cors sysconsole_read_integrations_gif sysconsole_read_integrations_integration_management sysconsole_read_plugins sysconsole_read_products_boards sysconsole_read_reporting_server_logs sysconsole_read_reporting_site_statistics sysconsole_read_reporting_team_statistics sysconsole_read_site_announcement_banner sysconsole_read_site_customization sysconsole_read_site_emoji sysconsole_read_site_file_sharing_and_downloads sysconsole_read_site_ip_filters sysconsole_read_site_localization sysconsole_read_site_notices sysconsole_read_site_notifications sysconsole_read_site_posts sysconsole_read_site_public_links sysconsole_read_site_users_and_teams sysconsole_read_user_management_channels sysconsole_read_user_management_groups sysconsole_read_user_management_permissions sysconsole_read_user_management_system_roles sysconsole_read_user_management_teams sysconsole_read_user_management_users sysconsole_write_about_edition_and_license sysconsole_write_ai_recaps sysconsole_write_authentication_email sysconsole_write_authentication_guest_access sysconsole_write_authentication_ldap sysconsole_write_authentication_mfa sysconsole_write_authentication_openid sysconsole_write_authentication_password sysconsole_write_authentication_saml sysconsole_write_authentication_signup sysconsole_write_billing sysconsole_write_compliance_compliance_export sysconsole_write_compliance_compliance_monitoring sysconsole_write_compliance_custom_terms_of_service sysconsole_write_compliance_data_retention_policy sysconsole_write_environment_database sysconsole_write_environment_developer sysconsole_write_environment_elasticsearch sysconsole_write_environment_file_storage sysconsole_write_environment_high_availability sysconsole_write_environment_image_proxy sysconsole_write_environment_logging sysconsole_write_environment_mobile_security sysconsole_write_environment_performance_monitoring sysconsole_write_environment_push_notification_server sysconsole_write_environment_rate_limiting sysconsole_write_environment_session_lengths sysconsole_write_environment_smtp sysconsole_write_environment_web_server sysconsole_write_experimental_feature_flags sysconsole_write_experimental_features sysconsole_write_integrations_bot_accounts sysconsole_write_integrations_cors sysconsole_write_integrations_gif sysconsole_write_integrations_integration_management sysconsole_write_plugins sysconsole_write_products_boards sysconsole_write_reporting_server_logs sysconsole_write_reporting_site_statistics sysconsole_write_reporting_team_statistics sysconsole_write_site_announcement_banner sysconsole_write_site_customization sysconsole_write_site_emoji sysconsole_write_site_file_sharing_and_downloads sysconsole_write_site_ip_filters sysconsole_write_site_localization sysconsole_write_site_notices sysconsole_write_site_notifications sysconsole_write_site_posts sysconsole_write_site_public_links sysconsole_write_site_users_and_teams sysconsole_write_user_management_channels sysconsole_write_user_management_groups sysconsole_write_user_management_permissions sysconsole_write_user_management_system_roles sysconsole_write_user_management_teams sysconsole_write_user_management_users test_elasticsearch test_email test_ldap test_s3 test_site_url upload_file use_channel_mentions use_group_mentions use_slash_commands view_members view_team', + system_admin: 'add_bookmark_private_channel add_bookmark_public_channel add_ldap_private_cert add_ldap_public_cert add_reaction add_saml_idp_cert add_saml_private_cert add_saml_public_cert add_user_to_team assign_bot assign_system_admin_role bypass_incoming_webhook_channel_lock convert_private_channel_to_public convert_public_channel_to_private create_bot create_compliance_export_job create_custom_group create_data_retention_job create_direct_channel create_elasticsearch_post_aggregation_job create_elasticsearch_post_indexing_job create_emojis create_group_channel create_ldap_sync_job create_post create_post_ephemeral create_post_public create_private_channel create_public_channel create_team create_user_access_token delete_bookmark_private_channel delete_bookmark_public_channel delete_custom_group delete_emojis delete_others_emojis delete_others_posts delete_post delete_private_channel delete_public_channel demote_to_guest download_compliance_export_result edit_bookmark_private_channel edit_bookmark_public_channel edit_brand edit_custom_group edit_file_attachment edit_other_users edit_others_posts edit_post get_analytics get_logs get_public_link get_saml_cert_status get_saml_metadata_from_idp invalidate_caches invalidate_email_invite invite_guest invite_user join_private_teams join_public_channels join_public_teams list_private_teams list_public_teams list_team_channels list_users_without_team manage_bots manage_channel_access_rules manage_channel_join_requests manage_channel_roles manage_compliance_export_job manage_custom_group_members manage_data_retention_job manage_elasticsearch_post_aggregation_job manage_elasticsearch_post_indexing_job manage_jobs manage_ldap_sync_job manage_license_information manage_oauth manage_others_agent manage_others_bots manage_others_incoming_webhooks manage_others_outgoing_webhooks manage_others_slash_commands manage_outgoing_oauth_connections manage_own_agent manage_own_incoming_webhooks manage_own_outgoing_webhooks manage_own_slash_commands manage_private_channel_auto_translation manage_private_channel_banner manage_private_channel_discoverability manage_private_channel_members manage_private_channel_properties manage_public_channel_auto_translation manage_public_channel_banner manage_public_channel_members manage_public_channel_properties manage_roles manage_secure_connections manage_shared_channels manage_system manage_system_wide_oauth manage_team manage_team_access_rules manage_team_roles order_bookmark_private_channel order_bookmark_public_channel playbook_private_create playbook_private_make_public playbook_private_manage_members playbook_private_manage_properties playbook_private_manage_roles playbook_private_view playbook_public_create playbook_public_make_private playbook_public_manage_members playbook_public_manage_properties playbook_public_manage_roles playbook_public_view promote_guest purge_elasticsearch_indexes read_audits read_bots read_channel read_channel_content read_compliance_export_job read_data_retention_job read_deleted_posts read_elasticsearch_post_aggregation_job read_elasticsearch_post_indexing_job read_jobs read_ldap_sync_job read_license_information read_other_users_teams read_others_bots read_private_channel_groups read_public_channel read_public_channel_groups read_user_access_token recycle_database_connections reload_config remove_ldap_private_cert remove_ldap_public_cert remove_others_reactions remove_reaction remove_saml_idp_cert remove_saml_private_cert remove_saml_public_cert remove_user_from_team restore_custom_group revoke_user_access_token run_create run_manage_members run_manage_properties run_view sysconsole_read_about_edition_and_license sysconsole_read_ai_recaps sysconsole_read_authentication_email sysconsole_read_authentication_guest_access sysconsole_read_authentication_ldap sysconsole_read_authentication_mfa sysconsole_read_authentication_openid sysconsole_read_authentication_password sysconsole_read_authentication_saml sysconsole_read_authentication_signup sysconsole_read_billing sysconsole_read_compliance_compliance_export sysconsole_read_compliance_compliance_monitoring sysconsole_read_compliance_custom_terms_of_service sysconsole_read_compliance_data_retention_policy sysconsole_read_environment_database sysconsole_read_environment_developer sysconsole_read_environment_elasticsearch sysconsole_read_environment_file_storage sysconsole_read_environment_high_availability sysconsole_read_environment_image_proxy sysconsole_read_environment_logging sysconsole_read_environment_mobile_security sysconsole_read_environment_performance_monitoring sysconsole_read_environment_push_notification_server sysconsole_read_environment_rate_limiting sysconsole_read_environment_session_lengths sysconsole_read_environment_smtp sysconsole_read_environment_web_server sysconsole_read_experimental_feature_flags sysconsole_read_experimental_features sysconsole_read_integrations_bot_accounts sysconsole_read_integrations_cors sysconsole_read_integrations_gif sysconsole_read_integrations_integration_management sysconsole_read_plugins sysconsole_read_products_boards sysconsole_read_reporting_server_logs sysconsole_read_reporting_site_statistics sysconsole_read_reporting_team_statistics sysconsole_read_site_announcement_banner sysconsole_read_site_customization sysconsole_read_site_emoji sysconsole_read_site_file_sharing_and_downloads sysconsole_read_site_ip_filters sysconsole_read_site_localization sysconsole_read_site_notices sysconsole_read_site_notifications sysconsole_read_site_posts sysconsole_read_site_public_links sysconsole_read_site_users_and_teams sysconsole_read_user_management_channels sysconsole_read_user_management_groups sysconsole_read_user_management_permissions sysconsole_read_user_management_system_roles sysconsole_read_user_management_teams sysconsole_read_user_management_users sysconsole_write_about_edition_and_license sysconsole_write_ai_recaps sysconsole_write_authentication_email sysconsole_write_authentication_guest_access sysconsole_write_authentication_ldap sysconsole_write_authentication_mfa sysconsole_write_authentication_openid sysconsole_write_authentication_password sysconsole_write_authentication_saml sysconsole_write_authentication_signup sysconsole_write_billing sysconsole_write_compliance_compliance_export sysconsole_write_compliance_compliance_monitoring sysconsole_write_compliance_custom_terms_of_service sysconsole_write_compliance_data_retention_policy sysconsole_write_environment_database sysconsole_write_environment_developer sysconsole_write_environment_elasticsearch sysconsole_write_environment_file_storage sysconsole_write_environment_high_availability sysconsole_write_environment_image_proxy sysconsole_write_environment_logging sysconsole_write_environment_mobile_security sysconsole_write_environment_performance_monitoring sysconsole_write_environment_push_notification_server sysconsole_write_environment_rate_limiting sysconsole_write_environment_session_lengths sysconsole_write_environment_smtp sysconsole_write_environment_web_server sysconsole_write_experimental_feature_flags sysconsole_write_experimental_features sysconsole_write_integrations_bot_accounts sysconsole_write_integrations_cors sysconsole_write_integrations_gif sysconsole_write_integrations_integration_management sysconsole_write_plugins sysconsole_write_products_boards sysconsole_write_reporting_server_logs sysconsole_write_reporting_site_statistics sysconsole_write_reporting_team_statistics sysconsole_write_site_announcement_banner sysconsole_write_site_customization sysconsole_write_site_emoji sysconsole_write_site_file_sharing_and_downloads sysconsole_write_site_ip_filters sysconsole_write_site_localization sysconsole_write_site_notices sysconsole_write_site_notifications sysconsole_write_site_posts sysconsole_write_site_public_links sysconsole_write_site_users_and_teams sysconsole_write_user_management_channels sysconsole_write_user_management_groups sysconsole_write_user_management_permissions sysconsole_write_user_management_system_roles sysconsole_write_user_management_teams sysconsole_write_user_management_users test_elasticsearch test_email test_ldap test_s3 test_site_url upload_file use_channel_mentions use_group_mentions use_slash_commands view_members view_team', system_custom_group_admin: 'create_custom_group delete_custom_group edit_custom_group manage_custom_group_members restore_custom_group', system_guest: 'create_direct_channel create_group_channel', system_manager: 'add_user_to_team convert_private_channel_to_public convert_public_channel_to_private create_elasticsearch_post_aggregation_job create_elasticsearch_post_indexing_job delete_private_channel delete_public_channel edit_brand get_analytics get_logs invalidate_caches join_private_teams join_public_teams list_private_teams list_public_teams manage_channel_roles manage_elasticsearch_post_aggregation_job manage_elasticsearch_post_indexing_job manage_outgoing_oauth_connections manage_private_channel_auto_translation manage_private_channel_members manage_private_channel_properties manage_public_channel_auto_translation manage_public_channel_members manage_public_channel_properties manage_team manage_team_roles purge_elasticsearch_indexes read_channel read_elasticsearch_post_aggregation_job read_elasticsearch_post_indexing_job read_ldap_sync_job read_license_information read_private_channel_groups read_public_channel read_public_channel_groups recycle_database_connections reload_config remove_user_from_team sysconsole_read_about_edition_and_license sysconsole_read_authentication_email sysconsole_read_authentication_guest_access sysconsole_read_authentication_ldap sysconsole_read_authentication_mfa sysconsole_read_authentication_openid sysconsole_read_authentication_password sysconsole_read_authentication_saml sysconsole_read_authentication_signup sysconsole_read_environment_database sysconsole_read_environment_developer sysconsole_read_environment_elasticsearch sysconsole_read_environment_file_storage sysconsole_read_environment_high_availability sysconsole_read_environment_image_proxy sysconsole_read_environment_logging sysconsole_read_environment_performance_monitoring sysconsole_read_environment_push_notification_server sysconsole_read_environment_rate_limiting sysconsole_read_environment_session_lengths sysconsole_read_environment_smtp sysconsole_read_environment_web_server sysconsole_read_integrations_bot_accounts sysconsole_read_integrations_cors sysconsole_read_integrations_gif sysconsole_read_integrations_integration_management sysconsole_read_plugins sysconsole_read_products_boards sysconsole_read_reporting_server_logs sysconsole_read_reporting_site_statistics sysconsole_read_reporting_team_statistics sysconsole_read_site_announcement_banner sysconsole_read_site_customization sysconsole_read_site_emoji sysconsole_read_site_file_sharing_and_downloads sysconsole_read_site_localization sysconsole_read_site_notices sysconsole_read_site_notifications sysconsole_read_site_posts sysconsole_read_site_public_links sysconsole_read_site_users_and_teams sysconsole_read_user_management_channels sysconsole_read_user_management_groups sysconsole_read_user_management_permissions sysconsole_read_user_management_teams sysconsole_write_environment_database sysconsole_write_environment_developer sysconsole_write_environment_elasticsearch sysconsole_write_environment_file_storage sysconsole_write_environment_high_availability sysconsole_write_environment_image_proxy sysconsole_write_environment_logging sysconsole_write_environment_performance_monitoring sysconsole_write_environment_push_notification_server sysconsole_write_environment_rate_limiting sysconsole_write_environment_session_lengths sysconsole_write_environment_smtp sysconsole_write_environment_web_server sysconsole_write_integrations_bot_accounts sysconsole_write_integrations_cors sysconsole_write_integrations_gif sysconsole_write_integrations_integration_management sysconsole_write_products_boards sysconsole_write_site_announcement_banner sysconsole_write_site_customization sysconsole_write_site_emoji sysconsole_write_site_file_sharing_and_downloads sysconsole_write_site_localization sysconsole_write_site_notices sysconsole_write_site_notifications sysconsole_write_site_posts sysconsole_write_site_public_links sysconsole_write_site_users_and_teams sysconsole_write_user_management_channels sysconsole_write_user_management_groups sysconsole_write_user_management_permissions sysconsole_write_user_management_teams test_elasticsearch test_email test_ldap test_s3 test_site_url view_team', @@ -23,7 +23,7 @@ export const defaultRolesPermissions = { system_user: 'create_custom_group create_direct_channel create_emojis create_group_channel create_team delete_custom_group delete_emojis edit_custom_group join_public_teams list_public_teams manage_custom_group_members manage_own_agent restore_custom_group view_members', system_user_access_token: 'create_user_access_token read_user_access_token revoke_user_access_token', system_user_manager: 'add_user_to_team convert_private_channel_to_public convert_public_channel_to_private delete_private_channel delete_public_channel join_private_teams join_public_teams list_private_teams list_public_teams manage_channel_roles manage_private_channel_auto_translation manage_private_channel_members manage_private_channel_properties manage_public_channel_auto_translation manage_public_channel_members manage_public_channel_properties manage_team manage_team_roles read_channel read_ldap_sync_job read_private_channel_groups read_public_channel read_public_channel_groups remove_user_from_team sysconsole_read_authentication_email sysconsole_read_authentication_guest_access sysconsole_read_authentication_ldap sysconsole_read_authentication_mfa sysconsole_read_authentication_openid sysconsole_read_authentication_password sysconsole_read_authentication_saml sysconsole_read_authentication_signup sysconsole_read_user_management_channels sysconsole_read_user_management_groups sysconsole_read_user_management_permissions sysconsole_read_user_management_teams sysconsole_write_user_management_channels sysconsole_write_user_management_groups sysconsole_write_user_management_teams test_ldap view_team', - team_admin: 'add_bookmark_private_channel add_bookmark_public_channel add_reaction bypass_incoming_webhook_channel_lock convert_private_channel_to_public convert_public_channel_to_private create_post delete_bookmark_private_channel delete_bookmark_public_channel delete_others_posts delete_post edit_bookmark_private_channel edit_bookmark_public_channel import_team manage_channel_access_rules manage_channel_join_requests manage_channel_roles manage_others_incoming_webhooks manage_others_outgoing_webhooks manage_others_slash_commands manage_own_incoming_webhooks manage_own_outgoing_webhooks manage_own_slash_commands manage_private_channel_auto_translation manage_private_channel_banner manage_private_channel_discoverability manage_private_channel_members manage_public_channel_auto_translation manage_public_channel_banner manage_public_channel_members manage_team manage_team_access_rules manage_team_roles order_bookmark_private_channel order_bookmark_public_channel playbook_private_manage_roles playbook_public_manage_roles read_private_channel_groups read_public_channel_groups remove_reaction remove_user_from_team upload_file use_channel_mentions use_group_mentions', + team_admin: 'add_bookmark_private_channel add_bookmark_public_channel add_reaction bypass_incoming_webhook_channel_lock convert_private_channel_to_public convert_public_channel_to_private create_post delete_bookmark_private_channel delete_bookmark_public_channel delete_others_posts delete_post edit_bookmark_private_channel edit_bookmark_public_channel manage_channel_access_rules manage_channel_join_requests manage_channel_roles manage_others_incoming_webhooks manage_others_outgoing_webhooks manage_others_slash_commands manage_own_incoming_webhooks manage_own_outgoing_webhooks manage_own_slash_commands manage_private_channel_auto_translation manage_private_channel_banner manage_private_channel_discoverability manage_private_channel_members manage_public_channel_auto_translation manage_public_channel_banner manage_public_channel_members manage_team manage_team_access_rules manage_team_roles order_bookmark_private_channel order_bookmark_public_channel playbook_private_manage_roles playbook_public_manage_roles read_private_channel_groups read_public_channel_groups remove_reaction remove_user_from_team upload_file use_channel_mentions use_group_mentions', team_guest: 'view_team', team_post_all: 'create_post upload_file use_channel_mentions use_group_mentions', team_post_all_public: 'create_post_public use_channel_mentions use_group_mentions', diff --git a/server/.golangci.yml b/server/.golangci.yml index 0476dcc59ce1..3f5521be8cc1 100644 --- a/server/.golangci.yml +++ b/server/.golangci.yml @@ -228,7 +228,6 @@ linters: platform/services/sharedchannel/permalink_test.go|\ platform/services/sharedchannel/sync_recv.go|\ platform/services/sharedchannel/sync_send.go|\ - platform/services/slackimport/slackimport.go|\ platform/services/telemetry/telemetry.go|\ platform/services/telemetry/telemetry_test.go|\ platform/services/upgrader/upgrader_linux.go|\ diff --git a/server/channels/api4/team.go b/server/channels/api4/team.go index 31cb486da8e2..1ee579bd07d2 100644 --- a/server/channels/api4/team.go +++ b/server/channels/api4/team.go @@ -4,8 +4,6 @@ package api4 import ( - "bytes" - "encoding/base64" "encoding/json" "fmt" "io" @@ -19,9 +17,8 @@ import ( ) const ( - MaxAddMembersBatch = 256 - MaximumBulkImportSize = 10 * 1024 * 1024 - groupIDsParamPattern = "[^a-zA-Z0-9,]*" + MaxAddMembersBatch = 256 + groupIDsParamPattern = "[^a-zA-Z0-9,]*" ) var groupIDsQueryParamRegex *regexp.Regexp @@ -68,7 +65,6 @@ func (api *API) InitTeam() { api.BaseRoutes.TeamByName.Handle("/exists", api.APISessionRequired(teamExists)).Methods(http.MethodGet) api.BaseRoutes.TeamMember.Handle("/roles", api.APISessionRequired(updateTeamMemberRoles)).Methods(http.MethodPut) api.BaseRoutes.TeamMember.Handle("/schemeRoles", api.APISessionRequired(updateTeamMemberSchemeRoles)).Methods(http.MethodPut) - api.BaseRoutes.Team.Handle("/import", api.APISessionRequired(importTeam)).Methods(http.MethodPost) api.BaseRoutes.Team.Handle("/invite/email", api.APISessionRequired(inviteUsersToTeam)).Methods(http.MethodPost) api.BaseRoutes.Team.Handle("/invite-guests/email", api.APISessionRequired(inviteGuestsToChannels)).Methods(http.MethodPost) api.BaseRoutes.Teams.Handle("/invites/email", api.APISessionRequired(invalidateAllEmailInvites)).Methods(http.MethodDelete) @@ -1657,97 +1653,6 @@ func teamExists(c *Context, w http.ResponseWriter, r *http.Request) { } } -func importTeam(c *Context, w http.ResponseWriter, r *http.Request) { - if c.App.Channels().License().IsCloud() { - c.Err = model.NewAppError("importTeam", "api.restricted_system_admin", nil, "", http.StatusForbidden) - return - } - - c.RequireTeamId() - if c.Err != nil { - return - } - - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionImportTeam) { - c.SetPermissionError(model.PermissionImportTeam) - return - } - - if err := r.ParseMultipartForm(MaximumBulkImportSize); err != nil { - c.Err = model.NewAppError("importTeam", "api.team.import_team.parse.app_error", nil, "", http.StatusInternalServerError).Wrap(err) - return - } - - importFromArray, ok := r.MultipartForm.Value["importFrom"] - if !ok || len(importFromArray) < 1 { - c.Err = model.NewAppError("importTeam", "api.team.import_team.no_import_from.app_error", nil, "", http.StatusBadRequest) - return - } - importFrom := importFromArray[0] - - fileSizeStr, ok := r.MultipartForm.Value["filesize"] - if !ok || len(fileSizeStr) < 1 { - c.Err = model.NewAppError("importTeam", "api.team.import_team.unavailable.app_error", nil, "", http.StatusBadRequest) - return - } - - fileSize, err := strconv.ParseInt(fileSizeStr[0], 10, 64) - if err != nil { - c.Err = model.NewAppError("importTeam", "api.team.import_team.integer.app_error", nil, "", http.StatusBadRequest) - return - } - - fileInfoArray, ok := r.MultipartForm.File["file"] - if !ok { - c.Err = model.NewAppError("importTeam", "api.team.import_team.no_file.app_error", nil, "", http.StatusBadRequest) - return - } - - if len(fileInfoArray) <= 0 { - c.Err = model.NewAppError("importTeam", "api.team.import_team.array.app_error", nil, "", http.StatusBadRequest) - return - } - - auditRec := c.MakeAuditRecord(model.AuditEventImportTeam, model.AuditStatusFail) - defer c.LogAuditRec(auditRec) - model.AddEventParameterToAuditRec(auditRec, "team_id", c.Params.TeamId) - - fileInfo := fileInfoArray[0] - - fileData, err := fileInfo.Open() - if err != nil { - c.Err = model.NewAppError("importTeam", "api.team.import_team.open.app_error", nil, "", http.StatusBadRequest).Wrap(err) - return - } - defer fileData.Close() - model.AddEventParameterToAuditRec(auditRec, "filename", fileInfo.Filename) - model.AddEventParameterToAuditRec(auditRec, "filesize", fileSize) - model.AddEventParameterToAuditRec(auditRec, "from", importFrom) - - var log *bytes.Buffer - data := map[string]string{} - switch importFrom { - case "slack": - var err *model.AppError - if err, log = c.App.SlackImport(c.AppContext, fileData, fileSize, c.Params.TeamId); err != nil { - c.Err = err - c.Err.StatusCode = http.StatusBadRequest - } - data["results"] = base64.StdEncoding.EncodeToString(log.Bytes()) - default: - c.Err = model.NewAppError("importTeam", "api.team.import_team.unknown_import_from.app_error", nil, "", http.StatusBadRequest) - } - - if c.Err != nil { - w.WriteHeader(c.Err.StatusCode) - return - } - auditRec.Success() - if _, err := w.Write([]byte(model.MapToJSON(data))); err != nil { - c.Logger.Warn("Error while writing response", mlog.Err(err)) - } -} - func inviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request) { graceful := r.URL.Query().Get("graceful") != "" diff --git a/server/channels/api4/team_test.go b/server/channels/api4/team_test.go index 0a799c815ad4..735b4b86d31e 100644 --- a/server/channels/api4/team_test.go +++ b/server/channels/api4/team_test.go @@ -5,8 +5,6 @@ package api4 import ( "context" - "encoding/base64" - "encoding/binary" "encoding/json" "fmt" "net/http" @@ -4131,97 +4129,6 @@ func TestTeamExists(t *testing.T) { }) } -func TestImportTeam(t *testing.T) { - mainHelper.Parallel(t) - th := Setup(t).InitBasic(t) - - th.TestForAllClients(t, func(T *testing.T, c *model.Client4) { - data, err := testutils.ReadTestFile("Fake_Team_Import.zip") - - require.False(t, err != nil && len(data) == 0, "Error while reading the test file.") - _, resp, err := th.SystemAdminClient.ImportTeam(context.Background(), data, binary.Size(data), "XYZ", "Fake_Team_Import.zip", th.BasicTeam.Id) - require.Error(t, err) - CheckBadRequestStatus(t, resp) - - _, resp, err = th.SystemAdminClient.ImportTeam(context.Background(), data, binary.Size(data), "", "Fake_Team_Import.zip", th.BasicTeam.Id) - require.Error(t, err) - CheckBadRequestStatus(t, resp) - }, "Import from unknown and source") - - t.Run("ImportTeam", func(t *testing.T) { - var data []byte - var err error - data, err = testutils.ReadTestFile("Fake_Team_Import.zip") - - require.False(t, err != nil && len(data) == 0, "Error while reading the test file.") - - // Import the channels/users/posts - fileResp, _, err := th.SystemAdminClient.ImportTeam(context.Background(), data, binary.Size(data), "slack", "Fake_Team_Import.zip", th.BasicTeam.Id) - require.NoError(t, err) - - fileData, err := base64.StdEncoding.DecodeString(fileResp["results"]) - require.NoError(t, err, "failed to decode base64 results data") - - fileReturned := string(fileData) - require.Truef(t, strings.Contains(fileReturned, "darth.vader@stardeath.com"), "failed to report the user was imported, fileReturned: %s", fileReturned) - - // Checking the imported users - importedUser, _, err := th.SystemAdminClient.GetUserByUsername(context.Background(), "bot_test", "") - require.NoError(t, err) - require.Equal(t, importedUser.Username, "bot_test", "username should match with the imported user") - - importedUser, _, err = th.SystemAdminClient.GetUserByUsername(context.Background(), "lordvader", "") - require.NoError(t, err) - require.Equal(t, importedUser.Username, "lordvader", "username should match with the imported user") - - // Checking the imported Channels - importedChannel, _, err := th.SystemAdminClient.GetChannelByName(context.Background(), "testchannel", th.BasicTeam.Id, "") - require.NoError(t, err) - require.Equal(t, importedChannel.Name, "testchannel", "names did not match expected: testchannel") - - importedChannel, _, err = th.SystemAdminClient.GetChannelByName(context.Background(), "general", th.BasicTeam.Id, "") - require.NoError(t, err) - require.Equal(t, importedChannel.Name, "general", "names did not match expected: general") - - posts, _, err := th.SystemAdminClient.GetPostsForChannel(context.Background(), importedChannel.Id, 0, 60, "", false, false) - require.NoError(t, err) - require.Equal(t, posts.Posts[posts.Order[3]].Message, "This is a test post to test the import process", "missing posts in the import process") - }) - - t.Run("Cloud Forbidden", func(t *testing.T) { - var data []byte - var err error - data, err = testutils.ReadTestFile("Fake_Team_Import.zip") - - require.False(t, err != nil && len(data) == 0, "Error while reading the test file.") - th.App.Srv().SetLicense(model.NewTestLicense("cloud")) - - // Import the channels/users/posts - _, resp, err := th.SystemAdminClient.ImportTeam(context.Background(), data, binary.Size(data), "slack", "Fake_Team_Import.zip", th.BasicTeam.Id) - require.Error(t, err) - CheckForbiddenStatus(t, resp) - th.App.Srv().SetLicense(nil) - }) - - t.Run("MissingFile", func(t *testing.T) { - _, resp, err := th.SystemAdminClient.ImportTeam(context.Background(), nil, 4343, "slack", "Fake_Team_Import.zip", th.BasicTeam.Id) - require.Error(t, err) - CheckBadRequestStatus(t, resp) - }) - - t.Run("WrongPermission", func(t *testing.T) { - var data []byte - var err error - data, err = testutils.ReadTestFile("Fake_Team_Import.zip") - require.False(t, err != nil && len(data) == 0, "Error while reading the test file.") - - // Import the channels/users/posts - _, resp, err := th.Client.ImportTeam(context.Background(), data, binary.Size(data), "slack", "Fake_Team_Import.zip", th.BasicTeam.Id) - require.Error(t, err) - CheckForbiddenStatus(t, resp) - }) -} - func TestValidateUserPermissionsOnChannels(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic(t) diff --git a/server/channels/app/app_test.go b/server/channels/app/app_test.go index 03792e4acf92..a504abdbcb43 100644 --- a/server/channels/app/app_test.go +++ b/server/channels/app/app_test.go @@ -175,7 +175,6 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { "team_admin": { model.PermissionRemoveUserFromTeam.Id, model.PermissionManageTeam.Id, - model.PermissionImportTeam.Id, model.PermissionManageTeamRoles.Id, model.PermissionManageTeamAccessRules.Id, model.PermissionManageChannelRoles.Id, diff --git a/server/channels/app/permissions_migrations.go b/server/channels/app/permissions_migrations.go index 620129dccd9d..c8ef2f62f8f3 100644 --- a/server/channels/app/permissions_migrations.go +++ b/server/channels/app/permissions_migrations.go @@ -1187,6 +1187,15 @@ func (a *App) removeGetAnalyticsPermissionMigration() (permissionsMap, error) { return transformations, nil } +func (a *App) removeImportTeamPermissionMigration() (permissionsMap, error) { + return permissionsMap{ + permissionTransformation{ + On: permissionExists("import_team"), + Remove: []string{"import_team"}, + }, + }, nil +} + func (a *App) addSysConsoleMobileSecurityPermission() (permissionsMap, error) { transformations := []permissionTransformation{} @@ -1408,6 +1417,7 @@ func (s *Server) doPermissionsMigrations() error { {Key: model.MigrationKeyAddManageAgentPermissions, Migration: a.getAddManageAgentPermissionsMigration}, {Key: model.MigrationKeyAddEditFileAttachmentPermission, Migration: a.getAddEditFileAttachmentPermissionMigration}, {Key: model.MigrationKeyAddDiscoverableChannelPermissions, Migration: a.getAddDiscoverableChannelPermissionsMigration}, + {Key: model.MigrationRemoveImportTeamPermission, Migration: a.removeImportTeamPermissionMigration}, } roles, err := s.Store().Role().GetAll() diff --git a/server/channels/app/slack.go b/server/channels/app/slack.go index 0f93f4396ff1..aeeae285ede8 100644 --- a/server/channels/app/slack.go +++ b/server/channels/app/slack.go @@ -4,79 +4,15 @@ package app import ( - "bytes" "fmt" - "image" - "mime/multipart" "regexp" "strings" - "time" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" - "github.com/mattermost/mattermost/server/v8/platform/services/slackimport" ) -func (a *App) SlackImport(rctx request.CTX, fileData multipart.File, fileSize int64, teamID string) (*model.AppError, *bytes.Buffer) { - actions := slackimport.Actions{ - UpdateActive: func(user *model.User, active bool) (*model.User, *model.AppError) { - return a.UpdateActive(rctx, user, active) - }, - AddUserToChannel: a.AddUserToChannel, - JoinUserToTeam: func(team *model.Team, user *model.User, userRequestorId string) (*model.TeamMember, *model.AppError) { - return a.JoinUserToTeam(rctx, team, user, userRequestorId) - }, - CreateDirectChannel: a.createDirectChannel, - CreateGroupChannel: a.createGroupChannel, - CreateChannel: func(channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) { - return a.CreateChannel(rctx, channel, addMember) - }, - DoUploadFile: func(now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, *model.AppError) { - return a.DoUploadFile(rctx, now, rawTeamId, rawChannelId, rawUserId, rawFilename, data, true) - }, - GenerateThumbnailImage: a.generateThumbnailImage, - GeneratePreviewImage: a.generatePreviewImage, - InvalidateAllCaches: func() *model.AppError { return a.ch.srv.platform.InvalidateAllCaches() }, - MaxPostSize: func() int { return a.ch.srv.platform.MaxPostSize() }, - SendPasswordReset: func(email string) (bool, *model.AppError) { - sent, err := a.SendPasswordReset(rctx, email, a.GetSiteURL()) - if err != nil { - return false, err - } - - return sent, nil - }, - PrepareImage: func(fileData []byte) (image.Image, string, func(), error) { - img, imgType, release, err := prepareImage(rctx, a.ch.imgDecoder, bytes.NewReader(fileData)) - if err != nil { - return nil, "", nil, err - } - return img, imgType, release, err - }, - } - - // Determine if this is an Admin import: - // mattermost cmd imports (no session) are treated as admin imports since only server admins can run them - // Web imports (include mmctl calls) check the actual user's role - isAdminImport := false - - if rctx.Session() == nil { - // no session means it's being run directly on the server and only - // server admins can run CLI commands, so treat as admin import - isAdminImport = true - rctx.Logger().Info("Slack import initiated via CLI, treating as admin import") - } else if rctx.Session().UserId != "" { - // Web API + mmctl import - check if the user is a system admin - if user, err := a.GetUser(rctx.Session().UserId); err == nil { - isAdminImport = user.IsSystemAdmin() - } - } - - importer := slackimport.NewWithAdminFlag(a.Srv().Store(), actions, a.Config(), isAdminImport) - return importer.SlackImport(rctx, fileData, fileSize, teamID) -} - func (a *App) ProcessSlackText(rctx request.CTX, text string) string { text = expandAnnouncement(text) text = replaceUserIds(rctx, a.Srv().Store().User(), text) diff --git a/server/channels/testlib/store.go b/server/channels/testlib/store.go index 82a910d2d7ad..aa28de161c6a 100644 --- a/server/channels/testlib/store.go +++ b/server/channels/testlib/store.go @@ -105,6 +105,7 @@ func GetMockStoreForSetupFunctions() *mocks.Store { systemStore.On("GetByName", model.MigrationKeyAddManageAgentPermissions).Return(&model.System{Name: model.MigrationKeyAddManageAgentPermissions, Value: "true"}, nil) systemStore.On("GetByName", model.MigrationKeyAddEditFileAttachmentPermission).Return(&model.System{Name: model.MigrationKeyAddEditFileAttachmentPermission, Value: "true"}, nil) systemStore.On("GetByName", model.MigrationKeyAddDiscoverableChannelPermissions).Return(&model.System{Name: model.MigrationKeyAddDiscoverableChannelPermissions, Value: "true"}, nil) + systemStore.On("GetByName", model.MigrationRemoveImportTeamPermission).Return(&model.System{Name: model.MigrationRemoveImportTeamPermission, Value: "true"}, nil) systemStore.On("InsertIfExists", mock.AnythingOfType("*model.System")).Return(&model.System{}, nil).Once() systemStore.On("Save", mock.AnythingOfType("*model.System")).Return(nil) diff --git a/server/cmd/mattermost/commands/import.go b/server/cmd/mattermost/commands/import.go index 85448c62e817..99aee90eabf1 100644 --- a/server/cmd/mattermost/commands/import.go +++ b/server/cmd/mattermost/commands/import.go @@ -12,7 +12,6 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/request" - "github.com/mattermost/mattermost/server/v8/channels/app" ) var ImportCmd = &cobra.Command{ @@ -20,14 +19,6 @@ var ImportCmd = &cobra.Command{ Short: "Import data.", } -var SlackImportCmd = &cobra.Command{ - Use: "slack [team] [file]", - Short: "Import a team from Slack.", - Long: "Import a team from a Slack export zip file.", - Example: " import slack myteam slack_export.zip", - RunE: slackImportCmdF, -} - var BulkImportCmd = &cobra.Command{ Use: "bulk [file]", Short: "Import bulk data.", @@ -44,63 +35,10 @@ func init() { ImportCmd.AddCommand( BulkImportCmd, - SlackImportCmd, ) RootCmd.AddCommand(ImportCmd) } -func slackImportCmdF(command *cobra.Command, args []string) error { - a, err := InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Srv().Shutdown() - - rctx := request.EmptyContext(a.Log()) - - if len(args) != 2 { - return errors.New("Incorrect number of arguments.") - } - - team := getTeamFromTeamArg(a, args[0]) - if team == nil { - return errors.New("Unable to find team '" + args[0] + "'") - } - - fileReader, err := os.Open(args[1]) - if err != nil { - return err - } - defer fileReader.Close() - - fileInfo, err := fileReader.Stat() - if err != nil { - return err - } - - CommandPrettyPrintln("Running Slack Import. This may take a long time for large teams or teams with many messages.") - - importErr, log := a.SlackImport(rctx, fileReader, fileInfo.Size(), team.Id) - - if importErr != nil { - return err - } - - CommandPrettyPrintln("") - CommandPrintln(log.String()) - CommandPrettyPrintln("") - - CommandPrettyPrintln("Finished Slack Import.") - CommandPrettyPrintln("") - - auditRec := a.MakeAuditRecord(rctx, model.AuditEventSlackImport, model.AuditStatusSuccess) - auditRec.AddMeta("team", team) - auditRec.AddMeta("file", args[1]) - a.LogAuditRec(rctx, auditRec, nil) - - return nil -} - func bulkImportCmdF(command *cobra.Command, args []string) error { a, err := InitDBCommandContextCobra(command) if err != nil { @@ -174,16 +112,3 @@ func bulkImportCmdF(command *cobra.Command, args []string) error { return nil } - -func getTeamFromTeamArg(a *app.App, teamArg string) *model.Team { - var team *model.Team - team, err := a.Srv().Store().Team().GetByName(teamArg) - - if err != nil { - var t *model.Team - if t, err = a.Srv().Store().Team().Get(teamArg); err == nil { - team = t - } - } - return team -} diff --git a/server/i18n/en.json b/server/i18n/en.json index 9b6bfc1ab1c4..60e09e381242 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -3654,98 +3654,6 @@ "id": "api.shared_channel.uninvite_remote_to_channel_error", "translation": "Could not uninvite remote to channel" }, - { - "id": "api.slackimport.slack_add_bot_user.email", - "translation": "The Integration/Slack Bot user with email {{.Email}} has been imported.\r\n" - }, - { - "id": "api.slackimport.slack_add_bot_user.unable_import", - "translation": "Unable to import the Integration/Slack Bot user {{.Username}}.\r\n" - }, - { - "id": "api.slackimport.slack_add_channels.added", - "translation": "\r\nChannels added:\r\n" - }, - { - "id": "api.slackimport.slack_add_channels.failed_to_add_user", - "translation": "Unable to add Slack user {{.Username}} to channel.\r\n" - }, - { - "id": "api.slackimport.slack_add_channels.import_failed", - "translation": "Unable to import Slack channel {{.DisplayName}}.\r\n" - }, - { - "id": "api.slackimport.slack_add_channels.merge", - "translation": "The Slack channel {{.DisplayName}} already exists as an active Mattermost channel. Both channels have been merged.\r\n" - }, - { - "id": "api.slackimport.slack_add_users.created", - "translation": "\r\nUsers created:\r\n" - }, - { - "id": "api.slackimport.slack_add_users.email", - "translation": "Slack user with email {{.Email}} has been imported.\r\n" - }, - { - "id": "api.slackimport.slack_add_users.merge_existing", - "translation": "Slack user merged with an existing Mattermost user with matching email {{.Email}} and username {{.Username}}.\r\n" - }, - { - "id": "api.slackimport.slack_add_users.merge_existing_failed", - "translation": "Slack user merged with an existing Mattermost user with matching email {{.Email}} and username {{.Username}}, but was unable to add the user to their team.\r\n" - }, - { - "id": "api.slackimport.slack_add_users.merge_existing_skipped_non_admin", - "translation": "Slack user with email {{.Email}} matches existing Mattermost user {{.Username}}. Merging into an existing Mattermost account is only available for imports run by a system admin, so this user's posts and channel memberships will not be imported. Re-run the import as a system admin to merge them.\r\n" - }, - { - "id": "api.slackimport.slack_add_users.missing_email_address", - "translation": "User {{.Username}} does not have an email address in the Slack export. Used {{.Email}} as a placeholder. The user should update their email address once logged in to the system.\r\n" - }, - { - "id": "api.slackimport.slack_add_users.send_reset_email_failed", - "translation": "Unable to send password reset email to {{.Username}} at {{.Email}}.\r\n" - }, - { - "id": "api.slackimport.slack_add_users.unable_import", - "translation": "Unable to import Slack user: {{.Username}}.\r\n" - }, - { - "id": "api.slackimport.slack_import.log", - "translation": "Mattermost Slack Import Log\r\n" - }, - { - "id": "api.slackimport.slack_import.note1", - "translation": "- Some messages may not have been imported because they were not supported by this importer.\r\n" - }, - { - "id": "api.slackimport.slack_import.note2", - "translation": "- Slack bot messages are currently not supported.\r\n" - }, - { - "id": "api.slackimport.slack_import.note3", - "translation": "- Additional errors may be found in the server logs.\r\n" - }, - { - "id": "api.slackimport.slack_import.notes", - "translation": "\r\nNotes:\r\n" - }, - { - "id": "api.slackimport.slack_import.open.app_error", - "translation": "Unable to open the file: {{.Filename}}.\r\n" - }, - { - "id": "api.slackimport.slack_import.team_fail", - "translation": "Unable to get the team to import into.\r\n" - }, - { - "id": "api.slackimport.slack_import.zip.app_error", - "translation": "Unable to open the Slack export zip file.\r\n" - }, - { - "id": "api.slackimport.slack_import.zip.file_too_large", - "translation": "{{.Filename}} in zip archive too large to process for Slack import\r\n" - }, { "id": "api.status.user_not_found.app_error", "translation": "User not found." @@ -3850,38 +3758,6 @@ "id": "api.team.get_team_icon.read_file.app_error", "translation": "Unable to read the team icon file." }, - { - "id": "api.team.import_team.array.app_error", - "translation": "Empty array under 'file' in request." - }, - { - "id": "api.team.import_team.integer.app_error", - "translation": "Filesize not an integer." - }, - { - "id": "api.team.import_team.no_file.app_error", - "translation": "No file under 'file' in request." - }, - { - "id": "api.team.import_team.no_import_from.app_error", - "translation": "Malformed request: importFrom field is not present." - }, - { - "id": "api.team.import_team.open.app_error", - "translation": "Could not open file." - }, - { - "id": "api.team.import_team.parse.app_error", - "translation": "Could not parse multipart form." - }, - { - "id": "api.team.import_team.unavailable.app_error", - "translation": "Malformed request: filesize field is not present." - }, - { - "id": "api.team.import_team.unknown_import_from.app_error", - "translation": "Unknown import source." - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Error invalidating email invites." diff --git a/server/platform/services/slackimport/converters.go b/server/platform/services/slackimport/converters.go deleted file mode 100644 index d6e820ab67a7..000000000000 --- a/server/platform/services/slackimport/converters.go +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package slackimport - -import ( - "regexp" - "strconv" - "strings" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func slackConvertTimeStamp(ts string) int64 { - timeString := strings.SplitN(ts, ".", 2)[0] - - timeStamp, err := strconv.ParseInt(timeString, 10, 64) - if err != nil { - mlog.Warn("Slack Import: Bad timestamp detected.") - return 1 - } - return timeStamp * 1000 // Convert to milliseconds -} - -func slackConvertChannelName(channelName string, channelId string) string { - newName := strings.Trim(channelName, "_-") - if len(newName) == 1 { - return "slack-channel-" + newName - } - - if isValidChannelNameCharacters(newName) { - return newName - } - return strings.ToLower(channelId) -} - -func slackConvertUserMentions(users []slackUser, posts map[string][]slackPost) map[string][]slackPost { - var regexes = make(map[string]*regexp.Regexp, len(users)) - for _, user := range users { - r, err := regexp.Compile("<@" + user.Id + `(\|` + user.Username + ")?>") - if err != nil { - mlog.Warn("Slack Import: Unable to compile the @mention, matching regular expression for the Slack user.", mlog.String("user_name", user.Username), mlog.String("user_id", user.Id)) - continue - } - regexes["@"+user.Username] = r - } - - // Special cases. - regexes["@here"], _ = regexp.Compile(``) - regexes["@channel"], _ = regexp.Compile("") - regexes["@all"], _ = regexp.Compile("") - - for channelName, channelPosts := range posts { - for postIdx, post := range channelPosts { - for mention, r := range regexes { - post.Text = r.ReplaceAllString(post.Text, mention) - posts[channelName][postIdx] = post - } - } - } - - return posts -} - -func slackConvertChannelMentions(channels []slackChannel, posts map[string][]slackPost) map[string][]slackPost { - var regexes = make(map[string]*regexp.Regexp, len(channels)) - for _, channel := range channels { - r, err := regexp.Compile("<#" + channel.Id + `(\|` + channel.Name + ")?>") - if err != nil { - mlog.Warn("Slack Import: Unable to compile the !channel, matching regular expression for the Slack channel.", mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name)) - continue - } - regexes["~"+channel.Name] = r - } - - for channelName, channelPosts := range posts { - for postIdx, post := range channelPosts { - for channelReplace, r := range regexes { - post.Text = r.ReplaceAllString(post.Text, channelReplace) - posts[channelName][postIdx] = post - } - } - } - - return posts -} - -func slackConvertPostsMarkup(posts map[string][]slackPost) map[string][]slackPost { - regexReplaceAllString := []struct { - regex *regexp.Regexp - rpl string - }{ - // URL - { - regexp.MustCompile(`<([^|<>]+)\|([^|<>]+)>`), - "[$2]($1)", - }, - // bold - { - regexp.MustCompile(`(^|[\s.;,])\*(\S[^*\n]+)\*`), - "$1**$2**", - }, - // strikethrough - { - regexp.MustCompile(`(^|[\s.;,])\~(\S[^~\n]+)\~`), - "$1~~$2~~", - }, - // single paragraph blockquote - // Slack converts > character to > - { - regexp.MustCompile(`(?sm)^>`), - ">", - }, - } - - regexReplaceAllStringFunc := []struct { - regex *regexp.Regexp - fn func(string) string - }{ - // multiple paragraphs blockquotes - { - regexp.MustCompile(`(?sm)^>>>(.+)$`), - func(src string) string { - // remove >>> prefix, might have leading \n - prefixRegexp := regexp.MustCompile(`^([\n])?>>>(.*)`) - src = prefixRegexp.ReplaceAllString(src, "$1$2") - // append > to start of line - appendRegexp := regexp.MustCompile(`(?m)^`) - return appendRegexp.ReplaceAllString(src, ">$0") - }, - }, - } - - for channelName, channelPosts := range posts { - for postIdx, post := range channelPosts { - result := post.Text - - for _, rule := range regexReplaceAllString { - result = rule.regex.ReplaceAllString(result, rule.rpl) - } - - for _, rule := range regexReplaceAllStringFunc { - result = rule.regex.ReplaceAllStringFunc(result, rule.fn) - } - posts[channelName][postIdx].Text = result - } - } - - return posts -} diff --git a/server/platform/services/slackimport/main_test.go b/server/platform/services/slackimport/main_test.go deleted file mode 100644 index ad8ff61f405b..000000000000 --- a/server/platform/services/slackimport/main_test.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package slackimport - -import ( - "fmt" - "os" - "testing" -) - -func TestMain(m *testing.M) { - prevDir, err := os.Getwd() - if err != nil { - panic("Failed to get current working directory: " + err.Error()) - } - - err = os.Chdir("../..") - if err != nil { - panic(fmt.Sprintf("Failed to set current working directory to %s: %s", "../..", err.Error())) - } - - defer func() { - err := os.Chdir(prevDir) - if err != nil { - panic(fmt.Sprintf("Failed to restore current working directory to %s: %s", prevDir, err.Error())) - } - }() - - os.Exit(m.Run()) -} diff --git a/server/platform/services/slackimport/parsers.go b/server/platform/services/slackimport/parsers.go deleted file mode 100644 index eeab1219fdb8..000000000000 --- a/server/platform/services/slackimport/parsers.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package slackimport - -import ( - "encoding/json" - "io" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func slackParseChannels(data io.Reader, channelType model.ChannelType) ([]slackChannel, error) { - decoder := json.NewDecoder(data) - - var channels []slackChannel - if err := decoder.Decode(&channels); err != nil { - mlog.Warn("Slack Import: Error occurred when parsing some Slack channels. Import may work anyway.", mlog.Err(err)) - return channels, err - } - - for i := range channels { - channels[i].Type = channelType - } - - return channels, nil -} - -func slackParseUsers(data io.Reader) ([]slackUser, error) { - decoder := json.NewDecoder(data) - - var users []slackUser - err := decoder.Decode(&users) - // This actually returns errors that are ignored. - // In this case it is erroring because of a null that Slack - // introduced. So we just return the users here. - return users, err -} - -func slackParsePosts(data io.Reader) ([]slackPost, error) { - decoder := json.NewDecoder(data) - - var posts []slackPost - if err := decoder.Decode(&posts); err != nil { - mlog.Warn("Slack Import: Error occurred when parsing some Slack posts. Import may work anyway.", mlog.Err(err)) - return posts, err - } - return posts, nil -} diff --git a/server/platform/services/slackimport/slackimport.go b/server/platform/services/slackimport/slackimport.go deleted file mode 100644 index 1a5fad551e38..000000000000 --- a/server/platform/services/slackimport/slackimport.go +++ /dev/null @@ -1,873 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package slackimport - -import ( - "archive/zip" - "bytes" - "errors" - "image" - "io" - "mime/multipart" - "net/http" - "path/filepath" - "regexp" - "sort" - "strings" - "time" - "unicode/utf8" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/i18n" - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/public/shared/request" - "github.com/mattermost/mattermost/server/v8/channels/store" - "github.com/mattermost/mattermost/server/v8/channels/utils" -) - -type slackChannel struct { - Id string `json:"id"` - Name string `json:"name"` - Creator string `json:"creator"` - Members []string `json:"members"` - Purpose slackChannelSub `json:"purpose"` - Topic slackChannelSub `json:"topic"` - Type model.ChannelType -} - -type slackChannelSub struct { - Value string `json:"value"` -} - -type slackProfile struct { - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Email string `json:"email"` -} - -type slackUser struct { - Id string `json:"id"` - Username string `json:"name"` - Profile slackProfile `json:"profile"` -} - -type slackFile struct { - Id string `json:"id"` - Title string `json:"title"` -} - -type slackPost struct { - User string `json:"user"` - BotId string `json:"bot_id"` - BotUsername string `json:"username"` - Text string `json:"text"` - TimeStamp string `json:"ts"` - ThreadTS string `json:"thread_ts"` - Type string `json:"type"` - SubType string `json:"subtype"` - Comment *slackComment `json:"comment"` - Upload bool `json:"upload"` - File *slackFile `json:"file"` - Files []*slackFile `json:"files"` - Attachments []*model.MessageAttachment `json:"attachments"` -} - -var isValidChannelNameCharacters = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`).MatchString - -const slackImportMaxFileSize = 1024 * 1024 * 70 - -type slackComment struct { - User string `json:"user"` - Comment string `json:"comment"` -} - -// Actions provides the actions that needs to be used for import slack data -type Actions struct { - UpdateActive func(*model.User, bool) (*model.User, *model.AppError) - AddUserToChannel func(request.CTX, *model.User, *model.Channel, bool) (*model.ChannelMember, *model.AppError) - JoinUserToTeam func(*model.Team, *model.User, string) (*model.TeamMember, *model.AppError) - CreateDirectChannel func(request.CTX, string, string, ...model.ChannelOption) (*model.Channel, *model.AppError) - CreateGroupChannel func(request.CTX, []string, string, ...model.ChannelOption) (*model.Channel, *model.AppError) - CreateChannel func(*model.Channel, bool) (*model.Channel, *model.AppError) - DoUploadFile func(time.Time, string, string, string, string, []byte) (*model.FileInfo, *model.AppError) - GenerateThumbnailImage func(request.CTX, image.Image, string, string) - GeneratePreviewImage func(request.CTX, image.Image, string, string) - InvalidateAllCaches func() *model.AppError - MaxPostSize func() int - SendPasswordReset func(string) (bool, *model.AppError) - PrepareImage func(fileData []byte) (image.Image, string, func(), error) -} - -// SlackImporter is a service that allows to import slack dumps into mattermost -type SlackImporter struct { - store store.Store - actions Actions - config *model.Config - isAdminImport bool -} - -// New creates a new SlackImporter service instance. It receive a store, a set of actions and the current config. -// It is expected to be used right away and discarded after that -func New(store store.Store, actions Actions, config *model.Config) *SlackImporter { - return &SlackImporter{ - store: store, - actions: actions, - config: config, - } -} - -// NewWithAdminFlag creates a new SlackImporter service instance with information about whether this is an admin import. -// This allows for enhanced security controls based on the importing user's role. -func NewWithAdminFlag(store store.Store, actions Actions, config *model.Config, isAdminImport bool) *SlackImporter { - return &SlackImporter{ - store: store, - actions: actions, - config: config, - isAdminImport: isAdminImport, - } -} - -func (si *SlackImporter) SlackImport(rctx request.CTX, fileData multipart.File, fileSize int64, teamID string) (*model.AppError, *bytes.Buffer) { - // Create log file - log := bytes.NewBufferString(i18n.T("api.slackimport.slack_import.log")) - - zipreader, err := zip.NewReader(fileData, fileSize) - if err != nil || zipreader.File == nil { - log.WriteString(i18n.T("api.slackimport.slack_import.zip.app_error")) - return model.NewAppError("SlackImport", "api.slackimport.slack_import.zip.app_error", nil, "", http.StatusBadRequest).Wrap(err), log - } - - var channels []slackChannel - var publicChannels []slackChannel - var privateChannels []slackChannel - var groupChannels []slackChannel - var directChannels []slackChannel - - var users []slackUser - posts := make(map[string][]slackPost) - uploads := make(map[string]*zip.File) - for _, file := range zipreader.File { - fileReader, err := file.Open() - if err != nil { - log.WriteString(i18n.T("api.slackimport.slack_import.open.app_error", map[string]any{"Filename": file.Name})) - return model.NewAppError("SlackImport", "api.slackimport.slack_import.open.app_error", map[string]any{"Filename": file.Name}, "", http.StatusInternalServerError).Wrap(err), log - } - defer fileReader.Close() - - reader := utils.NewLimitedReaderWithError(fileReader, slackImportMaxFileSize) - if file.Name == "channels.json" { - publicChannels, err = slackParseChannels(reader, model.ChannelTypeOpen) - if errors.Is(err, utils.ErrSizeLimitExceeded) { - log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name})) - continue - } - channels = append(channels, publicChannels...) - } else if file.Name == "dms.json" { - directChannels, err = slackParseChannels(reader, model.ChannelTypeDirect) - if errors.Is(err, utils.ErrSizeLimitExceeded) { - log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name})) - continue - } - channels = append(channels, directChannels...) - } else if file.Name == "groups.json" { - privateChannels, err = slackParseChannels(reader, model.ChannelTypePrivate) - if errors.Is(err, utils.ErrSizeLimitExceeded) { - log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name})) - continue - } - channels = append(channels, privateChannels...) - } else if file.Name == "mpims.json" { - groupChannels, err = slackParseChannels(reader, model.ChannelTypeGroup) - if errors.Is(err, utils.ErrSizeLimitExceeded) { - log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name})) - continue - } - channels = append(channels, groupChannels...) - } else if file.Name == "users.json" { - users, err = slackParseUsers(reader) - if errors.Is(err, utils.ErrSizeLimitExceeded) { - log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name})) - continue - } - } else { - spl := strings.Split(file.Name, "/") - if len(spl) == 2 && strings.HasSuffix(spl[1], ".json") { - newposts, err := slackParsePosts(reader) - if errors.Is(err, utils.ErrSizeLimitExceeded) { - log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name})) - continue - } - channel := spl[0] - if _, ok := posts[channel]; !ok { - posts[channel] = newposts - } else { - posts[channel] = append(posts[channel], newposts...) - } - } else if len(spl) == 3 && spl[0] == "__uploads" { - uploads[spl[1]] = file - } - } - } - - posts = slackConvertUserMentions(users, posts) - posts = slackConvertChannelMentions(channels, posts) - posts = slackConvertPostsMarkup(posts) - - addedUsers := si.slackAddUsers(rctx, teamID, users, log) - botUser := si.slackAddBotUser(rctx, teamID, log) - - si.slackAddChannels(rctx, teamID, channels, posts, addedUsers, uploads, botUser, log) - - if botUser != nil { - si.deactivateSlackBotUser(rctx, botUser) - } - - if err := si.actions.InvalidateAllCaches(); err != nil { - return err, log - } - - log.WriteString(i18n.T("api.slackimport.slack_import.notes")) - log.WriteString("=======\r\n\r\n") - - log.WriteString(i18n.T("api.slackimport.slack_import.note1")) - log.WriteString(i18n.T("api.slackimport.slack_import.note2")) - log.WriteString(i18n.T("api.slackimport.slack_import.note3")) - - return nil, log -} - -func truncateRunes(s string, i int) string { - runes := []rune(s) - if len(runes) > i { - return string(runes[:i]) - } - return s -} - -func (si *SlackImporter) slackAddUsers(rctx request.CTX, teamId string, slackusers []slackUser, importerLog *bytes.Buffer) map[string]*model.User { - // Log header - importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.created")) - importerLog.WriteString("===============\r\n\r\n") - - addedUsers := make(map[string]*model.User) - - // Need the team - team, err := si.store.Team().Get(teamId) - if err != nil { - importerLog.WriteString(i18n.T("api.slackimport.slack_import.team_fail")) - return addedUsers - } - - for _, sUser := range slackusers { - firstName := sUser.Profile.FirstName - lastName := sUser.Profile.LastName - email := sUser.Profile.Email - if email == "" { - email = sUser.Username + "@example.com" - importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.missing_email_address", map[string]any{"Email": email, "Username": sUser.Username})) - rctx.Logger().Warn("Slack Import: User does not have an email address in the Slack export. Used username as a placeholder. The user should update their email address once logged in to the system.", mlog.String("user_email", email), mlog.String("user_name", sUser.Username)) - } - - // Check for email conflict; only admin imports may merge into an existing account. - if existingUser, err := si.store.User().GetByEmail(email); err == nil { - if si.isAdminImport { - addedUsers[sUser.Id] = existingUser - if _, err := si.actions.JoinUserToTeam(team, addedUsers[sUser.Id], ""); err != nil { - importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.merge_existing_failed", map[string]any{"Email": existingUser.Email, "Username": existingUser.Username})) - } else { - importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.merge_existing", map[string]any{"Email": existingUser.Email, "Username": existingUser.Username})) - } - continue - } - importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.merge_existing_skipped_non_admin", map[string]any{"Email": existingUser.Email, "Username": existingUser.Username})) - continue - } - - email = strings.ToLower(email) - newUser := model.User{ - Username: sUser.Username, - FirstName: firstName, - LastName: lastName, - Email: email, - Password: "", - } - - mUser := si.oldImportUser(rctx, team, &newUser) - if mUser == nil { - importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.unable_import", map[string]any{"Username": sUser.Username})) - continue - } - - sent, err := si.actions.SendPasswordReset(email) - if err != nil { - rctx.Logger().Warn("Slack Import: Cannot send password reset email to user. An admin should update their email address once logged in to the system.", mlog.String("user_email", email), mlog.String("user_name", sUser.Username)) - } - - if !sent { - importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.send_reset_email_failed", map[string]any{"Username": sUser.Username, "Email": newUser.Email})) - } - - addedUsers[sUser.Id] = mUser - importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.email", map[string]any{"Email": newUser.Email})) - } - - return addedUsers -} - -func (si *SlackImporter) slackAddBotUser(rctx request.CTX, teamId string, log *bytes.Buffer) *model.User { - team, err := si.store.Team().Get(teamId) - if err != nil { - log.WriteString(i18n.T("api.slackimport.slack_import.team_fail")) - return nil - } - - password := model.NewId() - username := "slackimportuser_" + model.NewId() - email := username + "@localhost" - - botUser := model.User{ - Username: username, - FirstName: "", - LastName: "", - Email: email, - Password: password, - } - - mUser := si.oldImportUser(rctx, team, &botUser) - if mUser == nil { - log.WriteString(i18n.T("api.slackimport.slack_add_bot_user.unable_import", map[string]any{"Username": username})) - return nil - } - - log.WriteString(i18n.T("api.slackimport.slack_add_bot_user.email", map[string]any{"Email": botUser.Email})) - return mUser -} - -func (si *SlackImporter) slackAddPosts(rctx request.CTX, teamId string, channel *model.Channel, posts []slackPost, users map[string]*model.User, uploads map[string]*zip.File, botUser *model.User) { - sort.Slice(posts, func(i, j int) bool { - return slackConvertTimeStamp(posts[i].TimeStamp) < slackConvertTimeStamp(posts[j].TimeStamp) - }) - threads := make(map[string]string) - for _, sPost := range posts { - switch { - case sPost.Type == "message" && (sPost.SubType == "" || sPost.SubType == "file_share"): - if sPost.User == "" { - rctx.Logger().Debug("Slack Import: Unable to import the message as the user field is missing.") - continue - } - if users[sPost.User] == nil { - rctx.Logger().Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User)) - continue - } - newPost := model.Post{ - UserId: users[sPost.User].Id, - ChannelId: channel.Id, - Message: sPost.Text, - CreateAt: slackConvertTimeStamp(sPost.TimeStamp), - } - if sPost.Upload { - if sPost.File != nil { - if fileInfo, ok := si.slackUploadFile(rctx, sPost.File, uploads, teamId, newPost.ChannelId, newPost.UserId, sPost.TimeStamp); ok { - newPost.FileIds = append(newPost.FileIds, fileInfo.Id) - } - } else if sPost.Files != nil { - for _, file := range sPost.Files { - if fileInfo, ok := si.slackUploadFile(rctx, file, uploads, teamId, newPost.ChannelId, newPost.UserId, sPost.TimeStamp); ok { - newPost.FileIds = append(newPost.FileIds, fileInfo.Id) - } - } - } - } - // If post in thread - if sPost.ThreadTS != "" && sPost.ThreadTS != sPost.TimeStamp { - newPost.RootId = threads[sPost.ThreadTS] - } - postId := si.oldImportPost(rctx, &newPost) - // If post is thread starter - if sPost.ThreadTS == sPost.TimeStamp { - threads[sPost.ThreadTS] = postId - } - case sPost.Type == "message" && sPost.SubType == "file_comment": - if sPost.Comment == nil { - rctx.Logger().Debug("Slack Import: Unable to import the message as it has no comments.") - continue - } - if sPost.Comment.User == "" { - rctx.Logger().Debug("Slack Import: Unable to import the message as the user field is missing.") - continue - } - if users[sPost.Comment.User] == nil { - rctx.Logger().Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User)) - continue - } - newPost := model.Post{ - UserId: users[sPost.Comment.User].Id, - ChannelId: channel.Id, - Message: sPost.Comment.Comment, - CreateAt: slackConvertTimeStamp(sPost.TimeStamp), - } - si.oldImportPost(rctx, &newPost) - case sPost.Type == "message" && sPost.SubType == "bot_message": - if botUser == nil { - rctx.Logger().Warn("Slack Import: Unable to import the bot message as the bot user does not exist.") - continue - } - if sPost.BotId == "" { - rctx.Logger().Warn("Slack Import: Unable to import bot message as the BotId field is missing.") - continue - } - - props := make(model.StringInterface) - props[model.PostPropsOverrideUsername] = sPost.BotUsername - if len(sPost.Attachments) > 0 { - props[model.PostPropsAttachments] = sPost.Attachments - } - - post := &model.Post{ - UserId: botUser.Id, - ChannelId: channel.Id, - CreateAt: slackConvertTimeStamp(sPost.TimeStamp), - Message: sPost.Text, - Type: model.PostTypeMessageAttachment, - } - - postId := si.oldImportIncomingWebhookPost(rctx, post, props) - // If post is thread starter - if sPost.ThreadTS == sPost.TimeStamp { - threads[sPost.ThreadTS] = postId - } - case sPost.Type == "message" && (sPost.SubType == "channel_join" || sPost.SubType == "channel_leave"): - if sPost.User == "" { - rctx.Logger().Debug("Slack Import: Unable to import the message as the user field is missing.") - continue - } - if users[sPost.User] == nil { - rctx.Logger().Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User)) - continue - } - - var postType string - if sPost.SubType == "channel_join" { - postType = model.PostTypeJoinChannel - } else { - postType = model.PostTypeLeaveChannel - } - - newPost := model.Post{ - UserId: users[sPost.User].Id, - ChannelId: channel.Id, - Message: sPost.Text, - CreateAt: slackConvertTimeStamp(sPost.TimeStamp), - Type: postType, - Props: model.StringInterface{ - "username": users[sPost.User].Username, - }, - } - si.oldImportPost(rctx, &newPost) - case sPost.Type == "message" && sPost.SubType == "me_message": - if sPost.User == "" { - rctx.Logger().Debug("Slack Import: Unable to import the message as the user field is missing.") - continue - } - if users[sPost.User] == nil { - rctx.Logger().Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User)) - continue - } - newPost := model.Post{ - UserId: users[sPost.User].Id, - ChannelId: channel.Id, - Message: "*" + sPost.Text + "*", - CreateAt: slackConvertTimeStamp(sPost.TimeStamp), - } - postId := si.oldImportPost(rctx, &newPost) - // If post is thread starter - if sPost.ThreadTS == sPost.TimeStamp { - threads[sPost.ThreadTS] = postId - } - case sPost.Type == "message" && sPost.SubType == "channel_topic": - if sPost.User == "" { - rctx.Logger().Debug("Slack Import: Unable to import the message as the user field is missing.") - continue - } - if users[sPost.User] == nil { - rctx.Logger().Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User)) - continue - } - newPost := model.Post{ - UserId: users[sPost.User].Id, - ChannelId: channel.Id, - Message: sPost.Text, - CreateAt: slackConvertTimeStamp(sPost.TimeStamp), - Type: model.PostTypeHeaderChange, - } - si.oldImportPost(rctx, &newPost) - case sPost.Type == "message" && sPost.SubType == "channel_purpose": - if sPost.User == "" { - rctx.Logger().Debug("Slack Import: Unable to import the message as the user field is missing.") - continue - } - if users[sPost.User] == nil { - rctx.Logger().Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User)) - continue - } - newPost := model.Post{ - UserId: users[sPost.User].Id, - ChannelId: channel.Id, - Message: sPost.Text, - CreateAt: slackConvertTimeStamp(sPost.TimeStamp), - Type: model.PostTypePurposeChange, - } - si.oldImportPost(rctx, &newPost) - case sPost.Type == "message" && sPost.SubType == "channel_name": - if sPost.User == "" { - rctx.Logger().Debug("Slack Import: Unable to import the message as the user field is missing.") - continue - } - if users[sPost.User] == nil { - rctx.Logger().Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User)) - continue - } - newPost := model.Post{ - UserId: users[sPost.User].Id, - ChannelId: channel.Id, - Message: sPost.Text, - CreateAt: slackConvertTimeStamp(sPost.TimeStamp), - Type: model.PostTypeDisplaynameChange, - } - si.oldImportPost(rctx, &newPost) - default: - rctx.Logger().Warn( - "Slack Import: Unable to import the message as its type is not supported", - mlog.String("post_type", sPost.Type), - mlog.String("post_subtype", sPost.SubType), - ) - } - } -} - -func (si *SlackImporter) slackUploadFile(rctx request.CTX, slackPostFile *slackFile, uploads map[string]*zip.File, teamId string, channelId string, userId string, slackTimestamp string) (*model.FileInfo, bool) { - if slackPostFile == nil { - rctx.Logger().Warn("Slack Import: Unable to attach the file to the post as the latter has no file section present in Slack export.") - return nil, false - } - file, ok := uploads[slackPostFile.Id] - if !ok { - rctx.Logger().Warn("Slack Import: Unable to import file as the file is missing from the Slack export zip file.", mlog.String("file_id", slackPostFile.Id)) - return nil, false - } - openFile, err := file.Open() - if err != nil { - rctx.Logger().Warn("Slack Import: Unable to open the file from the Slack export.", mlog.String("file_id", slackPostFile.Id), mlog.Err(err)) - return nil, false - } - defer openFile.Close() - - // since this is an attachment, we should treat it as a file and apply according limits - reader := utils.NewLimitedReaderWithError(openFile, *si.config.FileSettings.MaxFileSize) - timestamp := utils.TimeFromMillis(slackConvertTimeStamp(slackTimestamp)) - uploadedFile, err := si.oldImportFile(rctx, timestamp, reader, teamId, channelId, userId, filepath.Base(file.Name)) - if err != nil { - rctx.Logger().Warn("Slack Import: An error occurred when uploading file.", mlog.String("file_id", slackPostFile.Id), mlog.Err(err)) - return nil, false - } - - return uploadedFile, true -} - -func (si *SlackImporter) deactivateSlackBotUser(rctx request.CTX, user *model.User) { - if _, err := si.actions.UpdateActive(user, false); err != nil { - rctx.Logger().Warn("Slack Import: Unable to deactivate the user account used for the bot.") - } -} - -func (si *SlackImporter) addSlackUsersToChannel(rctx request.CTX, members []string, users map[string]*model.User, channel *model.Channel, log *bytes.Buffer) { - for _, member := range members { - user, ok := users[member] - if !ok { - log.WriteString(i18n.T("api.slackimport.slack_add_channels.failed_to_add_user", map[string]any{"Username": "?"})) - continue - } - if _, err := si.actions.AddUserToChannel(rctx, user, channel, false); err != nil { - log.WriteString(i18n.T("api.slackimport.slack_add_channels.failed_to_add_user", map[string]any{"Username": user.Username})) - } - } -} - -func slackSanitiseChannelProperties(rctx request.CTX, channel model.Channel) model.Channel { - if utf8.RuneCountInString(channel.DisplayName) > model.ChannelDisplayNameMaxRunes { - rctx.Logger().Warn("Slack Import: Channel display name exceeds the maximum length. It will be truncated when imported.", mlog.String("channel_display_name", channel.DisplayName)) - channel.DisplayName = truncateRunes(channel.DisplayName, model.ChannelDisplayNameMaxRunes) - } - - if len(channel.Name) > model.ChannelNameMaxLength { - rctx.Logger().Warn("Slack Import: Channel handle exceeds the maximum length. It will be truncated when imported.", mlog.String("channel_display_name", channel.DisplayName)) - channel.Name = channel.Name[0:model.ChannelNameMaxLength] - } - - if utf8.RuneCountInString(channel.Purpose) > model.ChannelPurposeMaxRunes { - rctx.Logger().Warn("Slack Import: Channel purpose exceeds the maximum length. It will be truncated when imported.", mlog.String("channel_display_name", channel.DisplayName)) - channel.Purpose = truncateRunes(channel.Purpose, model.ChannelPurposeMaxRunes) - } - - if utf8.RuneCountInString(channel.Header) > model.ChannelHeaderMaxRunes { - rctx.Logger().Warn("Slack Import: Channel header exceeds the maximum length. It will be truncated when imported.", mlog.String("channel_display_name", channel.DisplayName)) - channel.Header = truncateRunes(channel.Header, model.ChannelHeaderMaxRunes) - } - - return channel -} - -func (si *SlackImporter) slackAddChannels(rctx request.CTX, teamId string, slackchannels []slackChannel, posts map[string][]slackPost, users map[string]*model.User, uploads map[string]*zip.File, botUser *model.User, importerLog *bytes.Buffer) map[string]*model.Channel { - // Write Header - importerLog.WriteString(i18n.T("api.slackimport.slack_add_channels.added")) - importerLog.WriteString("=================\r\n\r\n") - - addedChannels := make(map[string]*model.Channel) - for _, sChannel := range slackchannels { - newChannel := model.Channel{ - TeamId: teamId, - Type: sChannel.Type, - DisplayName: sChannel.Name, - Name: slackConvertChannelName(sChannel.Name, sChannel.Id), - Purpose: sChannel.Purpose.Value, - Header: sChannel.Topic.Value, - } - - // Direct message channels in Slack don't have a name so we set the id as name or else the messages won't get imported. - if newChannel.Type == model.ChannelTypeDirect { - sChannel.Name = sChannel.Id - } - - newChannel = slackSanitiseChannelProperties(rctx, newChannel) - - var mChannel *model.Channel - var err error - if mChannel, err = si.store.Channel().GetByName(teamId, sChannel.Name, true); err == nil { - // The channel already exists as an active channel. Merge with the existing one. - importerLog.WriteString(i18n.T("api.slackimport.slack_add_channels.merge", map[string]any{"DisplayName": newChannel.DisplayName})) - } else if _, nErr := si.store.Channel().GetDeletedByName(teamId, sChannel.Name); nErr == nil { - // The channel already exists but has been deleted. Generate a random string for the handle instead. - newChannel.Name = model.NewId() - newChannel = slackSanitiseChannelProperties(rctx, newChannel) - } - - if mChannel == nil { - // Haven't found an existing channel to merge with. Try importing it as a new one. - mChannel = si.oldImportChannel(rctx, &newChannel, sChannel, users) - if mChannel == nil { - rctx.Logger().Warn("Slack Import: Unable to import Slack channel.", mlog.String("channel_display_name", newChannel.DisplayName)) - importerLog.WriteString(i18n.T("api.slackimport.slack_add_channels.import_failed", map[string]any{"DisplayName": newChannel.DisplayName})) - continue - } - } - - // Members for direct and group channels are added during the creation of the channel in the oldImportChannel function - if sChannel.Type == model.ChannelTypeOpen || sChannel.Type == model.ChannelTypePrivate { - si.addSlackUsersToChannel(rctx, sChannel.Members, users, mChannel, importerLog) - } - importerLog.WriteString(newChannel.DisplayName + "\r\n") - addedChannels[sChannel.Id] = mChannel - si.slackAddPosts(rctx, teamId, mChannel, posts[sChannel.Name], users, uploads, botUser) - } - - return addedChannels -} - -// -// -- Old SlackImport Functions -- -// Import functions are suitable for entering posts and users into the database without -// some of the usual checks. (IsValid is still run) -// - -func (si *SlackImporter) oldImportPost(rctx request.CTX, post *model.Post) string { - // Workaround for empty messages, which may be the case if they are webhook posts. - firstIteration := true - firstPostId := "" - if post.RootId != "" { - firstPostId = post.RootId - } - maxPostSize := si.actions.MaxPostSize() - for messageRuneCount := utf8.RuneCountInString(post.Message); messageRuneCount > 0 || firstIteration; messageRuneCount = utf8.RuneCountInString(post.Message) { - var remainder string - if messageRuneCount > maxPostSize { - remainder = string(([]rune(post.Message))[maxPostSize:]) - post.Message = truncateRunes(post.Message, maxPostSize) - } else { - remainder = "" - } - - post.Hashtags, _ = model.ParseHashtags(post.Message) - - post.RootId = firstPostId - - _, err := si.store.Post().Save(rctx, post) - if err != nil { - rctx.Logger().Debug("Error saving post.", mlog.String("user_id", post.UserId), mlog.String("message", post.Message)) - } - - if firstIteration { - if firstPostId == "" { - firstPostId = post.Id - } - for _, fileId := range post.FileIds { - if err := si.store.FileInfo().AttachToPost(rctx, fileId, post.Id, post.ChannelId, post.UserId); err != nil { - rctx.Logger().Error( - "Error attaching files to post.", - mlog.String("post_id", post.Id), - mlog.String("file_ids", strings.Join(post.FileIds, ",")), - mlog.String("user_id", post.UserId), - mlog.Err(err), - ) - } - } - post.FileIds = nil - } - - post.Id = "" - post.CreateAt++ - post.Message = remainder - firstIteration = false - } - return firstPostId -} - -func (si *SlackImporter) oldImportUser(rctx request.CTX, team *model.Team, user *model.User) *model.User { - user.MakeNonNil() - - user.Roles = model.SystemUserRoleId - - ruser, nErr := si.store.User().Save(rctx, user) - if nErr != nil { - rctx.Logger().Debug("Error saving user.", mlog.Err(nErr)) - return nil - } - - // Only system admins can automatically verify emails during import - if si.isAdminImport { - if _, err := si.store.User().VerifyEmail(ruser.Id, ruser.Email); err != nil { - rctx.Logger().Warn("Failed to set email verified for admin import.", mlog.Err(err)) - } - } else { - // Non-admin users: emails remain unverified - rctx.Logger().Debug("Email verification skipped for non-admin import.", - mlog.String("user_email", ruser.Email)) - } - - if _, err := si.actions.JoinUserToTeam(team, user, ""); err != nil { - rctx.Logger().Warn("Failed to join team when importing.", mlog.Err(err)) - } - - return ruser -} - -func (si *SlackImporter) oldImportChannel(rctx request.CTX, channel *model.Channel, sChannel slackChannel, users map[string]*model.User) *model.Channel { - switch { - case channel.Type == model.ChannelTypeDirect: - if len(sChannel.Members) < 2 { - return nil - } - u1 := users[sChannel.Members[0]] - u2 := users[sChannel.Members[1]] - if u1 == nil || u2 == nil { - rctx.Logger().Warn("Either or both of user ids not found in users.json. Ignoring.", mlog.String("id1", sChannel.Members[0]), mlog.String("id2", sChannel.Members[1])) - return nil - } - sc, err := si.actions.CreateDirectChannel(rctx, u1.Id, u2.Id) - if err != nil { - return nil - } - - return sc - // check if direct channel has less than 8 members and if not import as private channel instead - case channel.Type == model.ChannelTypeGroup && len(sChannel.Members) < 8: - members := make([]string, len(sChannel.Members)) - - for i := range sChannel.Members { - u := users[sChannel.Members[i]] - if u == nil { - rctx.Logger().Warn("User not found in users.json. Ignoring.", mlog.String("id", sChannel.Members[i])) - continue - } - members[i] = u.Id - } - - creator := users[sChannel.Creator] - if creator == nil { - return nil - } - sc, err := si.actions.CreateGroupChannel(rctx, members, "") - if err != nil { - return nil - } - - return sc - case channel.Type == model.ChannelTypeGroup: - channel.Type = model.ChannelTypePrivate - sc, err := si.actions.CreateChannel(channel, false) - if err != nil { - return nil - } - - return sc - } - - sc, err := si.store.Channel().Save(rctx, channel, *si.config.TeamSettings.MaxChannelsPerTeam) - if err != nil { - return nil - } - - return sc -} - -func (si *SlackImporter) oldImportFile(rctx request.CTX, timestamp time.Time, file io.Reader, teamId string, channelId string, userId string, fileName string) (*model.FileInfo, error) { - buf := bytes.NewBuffer(nil) - _, err := io.Copy(buf, file) - if err != nil { - return nil, err - } - - data := buf.Bytes() - - fileInfo, appErr := si.actions.DoUploadFile(timestamp, teamId, channelId, userId, fileName, data) - if appErr != nil { - return nil, appErr - } - - if fileInfo.IsImage() && !fileInfo.IsSvg() { - img, imgType, release, err := si.actions.PrepareImage(data) - if err != nil { - return nil, err - } - defer release() - si.actions.GenerateThumbnailImage(rctx, img, imgType, fileInfo.ThumbnailPath) - si.actions.GeneratePreviewImage(rctx, img, imgType, fileInfo.PreviewPath) - } - - return fileInfo, nil -} - -func (si *SlackImporter) oldImportIncomingWebhookPost(rctx request.CTX, post *model.Post, props model.StringInterface) string { - linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`) - post.Message = linkWithTextRegex.ReplaceAllString(post.Message, "[${2}](${1})") - - post.AddProp(model.PostPropsFromWebhook, "true") - - if _, ok := props[model.PostPropsOverrideUsername]; !ok { - post.AddProp(model.PostPropsOverrideUsername, model.DefaultWebhookUsername) - } - - if len(props) > 0 { - for key, val := range props { - if key == model.PostPropsAttachments { - if attachments, success := val.([]*model.MessageAttachment); success { - model.ParseMessageAttachment(post, attachments) - } - } else if key != model.PostPropsFromWebhook { - post.AddProp(key, val) - } - } - } - - return si.oldImportPost(rctx, post) -} diff --git a/server/platform/services/slackimport/slackimport_test.go b/server/platform/services/slackimport/slackimport_test.go deleted file mode 100644 index dbeeb33b9bc1..000000000000 --- a/server/platform/services/slackimport/slackimport_test.go +++ /dev/null @@ -1,1042 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package slackimport - -import ( - "archive/zip" - "bytes" - "fmt" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/request" - "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks" -) - -func TestSlackConvertTimeStamp(t *testing.T) { - assert.EqualValues(t, slackConvertTimeStamp("1469785419.000033"), 1469785419000) -} - -func TestSlackConvertChannelName(t *testing.T) { - for _, tc := range []struct { - nameInput string - idInput string - output string - }{ - {"test-channel", "C0G08DLQH", "test-channel"}, - {"_test_channel_", "C0G04DLQH", "test_channel"}, - {"__test", "C0G07DLQH", "test"}, - {"-t", "C0G06DLQH", "slack-channel-t"}, - {"a", "C0G05DLQH", "slack-channel-a"}, - {"случайный", "C0G05DLQD", "c0g05dlqd"}, - } { - assert.Equal(t, slackConvertChannelName(tc.nameInput, tc.idInput), tc.output, "nameInput = %v", tc.nameInput) - } -} - -func TestSlackConvertUserMentions(t *testing.T) { - users := []slackUser{ - {Id: "U00000A0A", Username: "firstuser"}, - {Id: "U00000B1B", Username: "seconduser"}, - } - - posts := map[string][]slackPost{ - "test-channel": { - { - Text: ": Hi guys.", - }, - { - Text: "Calling .", - }, - { - Text: "Yo .", - }, - { - Text: "Regular user test <@U00000B1B|seconduser> and <@U00000A0A>.", - }, - }, - } - - expectedPosts := map[string][]slackPost{ - "test-channel": { - { - Text: "@channel: Hi guys.", - }, - { - Text: "Calling @here.", - }, - { - Text: "Yo @all.", - }, - { - Text: "Regular user test @seconduser and @firstuser.", - }, - }, - } - - assert.Equal(t, expectedPosts, slackConvertUserMentions(users, posts)) -} - -func TestSlackConvertChannelMentions(t *testing.T) { - channels := []slackChannel{ - {Id: "C000AA00A", Name: "one"}, - {Id: "C000BB11B", Name: "two"}, - } - - posts := map[string][]slackPost{ - "test-channel": { - { - Text: "Go to <#C000AA00A>.", - }, - { - User: "U00000A0A", - Text: "Try <#C000BB11B|two> for this.", - }, - }, - } - - expectedPosts := map[string][]slackPost{ - "test-channel": { - { - Text: "Go to ~one.", - }, - { - User: "U00000A0A", - Text: "Try ~two for this.", - }, - }, - } - - assert.Equal(t, expectedPosts, slackConvertChannelMentions(channels, posts)) -} - -func openTestFile(t *testing.T, filename string) (*os.File, error) { - working, err := os.Getwd() - if err != nil { - return nil, err - } - t.Log("working directory:", working) - - path := filepath.Join("../tests", filename) - return os.Open(path) -} - -func TestSlackParseChannels(t *testing.T) { - file, err := openTestFile(t, "slack-import-test-channels.json") - require.NoError(t, err) - defer file.Close() - - channels, err := slackParseChannels(file, model.ChannelTypeOpen) - require.NoError(t, err) - assert.Equal(t, 6, len(channels)) -} - -func TestSlackParseDirectMessages(t *testing.T) { - file, err := openTestFile(t, "slack-import-test-direct-messages.json") - require.NoError(t, err) - defer file.Close() - - channels, err := slackParseChannels(file, model.ChannelTypeDirect) - require.NoError(t, err) - assert.Equal(t, 4, len(channels)) -} - -func TestSlackParsePrivateChannels(t *testing.T) { - file, err := openTestFile(t, "slack-import-test-private-channels.json") - require.NoError(t, err) - defer file.Close() - - channels, err := slackParseChannels(file, model.ChannelTypePrivate) - require.NoError(t, err) - assert.Equal(t, 1, len(channels)) -} - -func TestSlackParseGroupDirectMessages(t *testing.T) { - file, err := openTestFile(t, "slack-import-test-group-direct-messages.json") - require.NoError(t, err) - defer file.Close() - - channels, err := slackParseChannels(file, model.ChannelTypeGroup) - require.NoError(t, err) - assert.Equal(t, 3, len(channels)) -} - -func TestSlackParseUsers(t *testing.T) { - file, err := openTestFile(t, "slack-import-test-users.json") - require.NoError(t, err) - defer file.Close() - - users, err := slackParseUsers(file) - require.NoError(t, err) - assert.Equal(t, 11, len(users)) -} - -func TestSlackParsePosts(t *testing.T) { - file, err := openTestFile(t, "slack-import-test-posts.json") - require.NoError(t, err) - defer file.Close() - - posts, err := slackParsePosts(file) - require.NoError(t, err) - assert.Equal(t, 9, len(posts)) -} - -func TestSlackParseMultipleAttachments(t *testing.T) { - file, err := openTestFile(t, "slack-import-test-posts.json") - require.NoError(t, err) - defer file.Close() - - posts, err := slackParsePosts(file) - require.NoError(t, err) - assert.Equal(t, 2, len(posts[8].Files)) -} - -func TestSlackSanitiseChannelProperties(t *testing.T) { - rctx := request.TestContext(t) - - c1 := model.Channel{ - DisplayName: "display-name", - Name: "name", - Purpose: "The channel purpose", - Header: "The channel header", - } - - c1s := slackSanitiseChannelProperties(rctx, c1) - assert.Equal(t, c1, c1s) - - c2 := model.Channel{ - DisplayName: strings.Repeat("abcdefghij", 7), - Name: strings.Repeat("abcdefghij", 7), - Purpose: strings.Repeat("0123456789", 30), - Header: strings.Repeat("0123456789", 120), - } - - c2s := slackSanitiseChannelProperties(rctx, c2) - assert.Equal(t, model.Channel{ - DisplayName: strings.Repeat("abcdefghij", 6) + "abcd", - Name: strings.Repeat("abcdefghij", 6) + "abcd", - Purpose: strings.Repeat("0123456789", 25), - Header: strings.Repeat("0123456789", 102) + "0123", - }, c2s) -} - -func TestSlackConvertPostsMarkup(t *testing.T) { - input := make(map[string][]slackPost) - input["test"] = []slackPost{ - { - Text: "This message contains a link to .", - }, - { - Text: "This message contains a mailto link to in it.", - }, - { - Text: "This message contains a *bold* word.", - }, - { - Text: "This is not a * bold * word.", - }, - { - Text: `There is *no bold word -in this*.`, - }, - { - Text: "*This* is not a*bold* word.*This* is a bold word, *and* this; *and* this too.", - }, - { - Text: "This message contains a ~strikethrough~ word.", - }, - { - Text: "This is not a ~ strikethrough ~ word.", - }, - { - Text: `There is ~no strikethrough word -in this~.`, - }, - { - Text: "~This~ is not a~strikethrough~ word.~This~ is a strikethrough word, ~and~ this; ~and~ this too.", - }, - { - Text: `This message contains multiple paragraphs blockquotes ->>>first -second -third`, - }, - { - Text: `This message contains single paragraph blockquotes ->something ->another thing`, - }, - { - Text: "This message has no > block quote", - }, - } - - expectedOutput := make(map[string][]slackPost) - expectedOutput["test"] = []slackPost{ - { - Text: "This message contains a link to [Google](https://google.com).", - }, - { - Text: "This message contains a mailto link to [me@example.com](mailto:me@example.com) in it.", - }, - { - Text: "This message contains a **bold** word.", - }, - { - Text: "This is not a * bold * word.", - }, - { - Text: `There is *no bold word -in this*.`, - }, - { - Text: "**This** is not a*bold* word.**This** is a bold word, **and** this; **and** this too.", - }, - { - Text: "This message contains a ~~strikethrough~~ word.", - }, - { - Text: "This is not a ~ strikethrough ~ word.", - }, - { - Text: `There is ~no strikethrough word -in this~.`, - }, - { - Text: "~~This~~ is not a~strikethrough~ word.~~This~~ is a strikethrough word, ~~and~~ this; ~~and~~ this too.", - }, - { - Text: `This message contains multiple paragraphs blockquotes ->first ->second ->third`, - }, - { - Text: `This message contains single paragraph blockquotes ->something ->another thing`, - }, - { - Text: "This message has no > block quote", - }, - } - - assert.Equal(t, expectedOutput, slackConvertPostsMarkup(input)) -} - -func TestOldImportChannel(t *testing.T) { - u1 := &model.User{ - Id: model.NewId(), - Username: "test-user-1", - } - u2 := &model.User{ - Id: model.NewId(), - Username: "test-user-2", - } - store := &mocks.Store{} - config := &model.Config{} - config.SetDefaults() - rctx := request.TestContext(t) - - t.Run("No panic on direct channel", func(t *testing.T) { - // ch := th.CreateDmChannel(u1) - ch := &model.Channel{ - Type: model.ChannelTypeDirect, - Name: model.GetDMNameFromIds(u1.Id, u2.Id), - } - users := map[string]*model.User{ - u2.Id: u2, - } - sCh := slackChannel{ - Id: "someid", - Members: []string{u1.Id, "randomID"}, - Creator: "randomID2", - } - - actions := Actions{} - - importer := New(store, actions, config) - _ = importer.oldImportChannel(rctx, ch, sCh, users) - }) - - t.Run("No panic on direct channel with 1 member", func(t *testing.T) { - ch := &model.Channel{ - Type: model.ChannelTypeDirect, - Name: model.GetDMNameFromIds(u1.Id, u1.Id), - } - users := map[string]*model.User{ - u1.Id: u1, - } - sCh := slackChannel{ - Id: "someid", - Members: []string{u1.Id}, - Creator: "randomID2", - } - - actions := Actions{} - - importer := New(store, actions, config) - _ = importer.oldImportChannel(rctx, ch, sCh, users) - }) - - t.Run("No panic on group channel", func(t *testing.T) { - ch := &model.Channel{ - Type: model.ChannelTypeGroup, - Name: "test-channel", - } - users := map[string]*model.User{ - u1.Id: u1, - } - sCh := slackChannel{ - Id: "someid", - Members: []string{u1.Id}, - Creator: "randomID2", - } - actions := Actions{} - - importer := New(store, actions, config) - _ = importer.oldImportChannel(rctx, ch, sCh, users) - }) -} - -func TestSlackUploadFile(t *testing.T) { - store := &mocks.Store{} - config := &model.Config{} - config.SetDefaults() - defaultLimit := *config.FileSettings.MaxFileSize - - rctx := request.TestContext(t) - - sf := &slackFile{ - Id: "testfile", - Title: "test-file", - } - - buf := new(bytes.Buffer) - zipWriter := zip.NewWriter(buf) - writer, err := zipWriter.Create("testfile") - require.NoError(t, err) - - _, err = writer.Write([]byte(strings.Repeat("a", 100))) - require.NoError(t, err) - - err = zipWriter.Close() - require.NoError(t, err) - - zipReader, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len())) - require.NoError(t, err) - - uploads := map[string]*zip.File{ - "testfile": zipReader.File[0], - } - - t.Run("Should not fail when file is in limits", func(t *testing.T) { - importer := New(store, Actions{ - DoUploadFile: func(_ time.Time, _, _, _, _ string, _ []byte) (*model.FileInfo, *model.AppError) { - return &model.FileInfo{}, nil - }, - }, config) - _, ok := importer.slackUploadFile(rctx, sf, uploads, "team-id", "channel-id", "user-id", time.Now().String()) - require.True(t, ok) - }) - - t.Run("Should fail when file size exceeded", func(t *testing.T) { - defer func() { - config.FileSettings.MaxFileSize = new(defaultLimit) - }() - - config.FileSettings.MaxFileSize = new(int64(10)) - - importer := New(store, Actions{}, config) - _, ok := importer.slackUploadFile(rctx, sf, uploads, "team-id", "channel-id", "user-id", time.Now().String()) - require.False(t, ok) - }) -} - -func TestOldImportUserEmailVerificationIsNotAutomatic(t *testing.T) { - rctx := request.TestContext(t) - - store := &mocks.Store{} - userStore := &mocks.UserStore{} - store.On("User").Return(userStore) - - // Track if VerifyEmail is called (it should NOT be called) - verifyEmailCalled := false - userStore.On("VerifyEmail", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return("user-id", nil).Run(func(args mock.Arguments) { - verifyEmailCalled = true - }) - - savedUser := &model.User{ - Id: "test-user-id", - Username: "testuser", - Email: "testuser@restricted-domain.com", - EmailVerified: false, // Must remain false after import - Roles: model.SystemUserRoleId, - } - userStore.On("Save", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("*model.User")).Return(savedUser, nil) - - joinTeamCalled := false - actions := Actions{ - JoinUserToTeam: func(team *model.Team, user *model.User, userRequestorId string) (*model.TeamMember, *model.AppError) { - joinTeamCalled = true - return &model.TeamMember{}, nil - }, - } - - config := &model.Config{} - config.SetDefaults() - - importer := New(store, actions, config) - - team := &model.Team{ - Id: "test-team-id", - Name: "test-team", - } - - user := &model.User{ - Username: "testuser", - Email: "testuser@restricted-domain.com", - FirstName: "Test", - LastName: "User", - } - - result := importer.oldImportUser(rctx, team, user) - - require.NotNil(t, result, "User import should succeed") - assert.Equal(t, "test-user-id", result.Id, "Should return the saved user") - assert.False(t, verifyEmailCalled, "SECURITY: VerifyEmail should NOT be called - this prevents domain bypass vulnerability") - assert.True(t, joinTeamCalled, "User should still be joined to the team") - - // Verify the user was saved with unverified email (VerifyEmail should not have been called) - userStore.AssertCalled(t, "Save", mock.AnythingOfType("*request.Context"), mock.MatchedBy(func(u *model.User) bool { - return u.Email == "testuser@restricted-domain.com" && !u.EmailVerified - })) -} - -// TestSlackImportEnhancedSecurityAdminCanVerifyEmails tests that system admins can automatically verify emails -func TestSlackImportEnhancedSecurityAdminCanVerifyEmails(t *testing.T) { - rctx := request.TestContext(t) - - store := &mocks.Store{} - userStore := &mocks.UserStore{} - store.On("User").Return(userStore) - - // Track if VerifyEmail is called (it SHOULD be called for admin imports) - verifyEmailCalled := false - userStore.On("VerifyEmail", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return("user-id", nil).Run(func(args mock.Arguments) { - verifyEmailCalled = true - }) - - savedUser := &model.User{ - Id: "test-user-id", - Username: "testuser", - Email: "testuser@restricted-domain.com", - EmailVerified: false, // Will be verified by admin import - Roles: model.SystemUserRoleId, - } - userStore.On("Save", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("*model.User")).Return(savedUser, nil) - - joinTeamCalled := false - actions := Actions{ - JoinUserToTeam: func(team *model.Team, user *model.User, userRequestorId string) (*model.TeamMember, *model.AppError) { - joinTeamCalled = true - return &model.TeamMember{}, nil - }, - } - - config := &model.Config{} - config.SetDefaults() - - // Pass true to indicate this is an admin import - importer := NewWithAdminFlag(store, actions, config, true) - - team := &model.Team{ - Id: "test-team-id", - Name: "test-team", - } - - user := &model.User{ - Username: "testuser", - Email: "testuser@restricted-domain.com", - FirstName: "Test", - LastName: "User", - } - - result := importer.oldImportUser(rctx, team, user) - - require.NotNil(t, result, "User import should succeed") - assert.Equal(t, "test-user-id", result.Id, "Should return the saved user") - assert.True(t, verifyEmailCalled, "ADMIN IMPORT: VerifyEmail SHOULD be called for system admin imports") - assert.True(t, joinTeamCalled, "User should still be joined to the team") - - // Verify VerifyEmail was called with correct parameters - userStore.AssertCalled(t, "VerifyEmail", "test-user-id", "testuser@restricted-domain.com") -} - -// TestSlackImportEnhancedSecurityNonAdminCannotVerifyEmails tests that non-admin users cannot automatically verify emails -func TestSlackImportEnhancedSecurityNonAdminCannotVerifyEmails(t *testing.T) { - rctx := request.TestContext(t) - - store := &mocks.Store{} - userStore := &mocks.UserStore{} - store.On("User").Return(userStore) - - // Track if VerifyEmail is called (it should NOT be called for non-admin imports) - verifyEmailCalled := false - userStore.On("VerifyEmail", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return("user-id", nil).Run(func(args mock.Arguments) { - verifyEmailCalled = true - }) - - savedUser := &model.User{ - Id: "test-user-id", - Username: "testuser", - Email: "testuser@restricted-domain.com", - EmailVerified: false, // Should remain false for non-admin import - Roles: model.SystemUserRoleId, - } - userStore.On("Save", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("*model.User")).Return(savedUser, nil) - - joinTeamCalled := false - actions := Actions{ - JoinUserToTeam: func(team *model.Team, user *model.User, userRequestorId string) (*model.TeamMember, *model.AppError) { - joinTeamCalled = true - return &model.TeamMember{}, nil - }, - } - - config := &model.Config{} - config.SetDefaults() - - // Pass false to indicate this is NOT an admin import - importer := NewWithAdminFlag(store, actions, config, false) - - team := &model.Team{ - Id: "test-team-id", - Name: "test-team", - } - - user := &model.User{ - Username: "testuser", - Email: "testuser@restricted-domain.com", - FirstName: "Test", - LastName: "User", - } - - result := importer.oldImportUser(rctx, team, user) - - require.NotNil(t, result, "User import should succeed") - assert.Equal(t, "test-user-id", result.Id, "Should return the saved user") - assert.False(t, verifyEmailCalled, "NON-ADMIN IMPORT: VerifyEmail should NOT be called for non-admin imports") - assert.True(t, joinTeamCalled, "User should still be joined to the team") - - // Verify VerifyEmail was NOT called - userStore.AssertNotCalled(t, "VerifyEmail") -} - -// TestSlackImportEnhancedSecurityNoImportingUser tests behavior when no importing user is provided -func TestSlackImportEnhancedSecurityNoImportingUser(t *testing.T) { - rctx := request.TestContext(t) - - store := &mocks.Store{} - userStore := &mocks.UserStore{} - store.On("User").Return(userStore) - - // Track if VerifyEmail is called (it should NOT be called when no importing user) - verifyEmailCalled := false - userStore.On("VerifyEmail", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return("user-id", nil).Run(func(args mock.Arguments) { - verifyEmailCalled = true - }) - - savedUser := &model.User{ - Id: "test-user-id", - Username: "testuser", - Email: "testuser@restricted-domain.com", - EmailVerified: false, // Should remain false when no importing user - Roles: model.SystemUserRoleId, - } - userStore.On("Save", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("*model.User")).Return(savedUser, nil) - - joinTeamCalled := false - actions := Actions{ - JoinUserToTeam: func(team *model.Team, user *model.User, userRequestorId string) (*model.TeamMember, *model.AppError) { - joinTeamCalled = true - return &model.TeamMember{}, nil - }, - } - - config := &model.Config{} - config.SetDefaults() - - // Pass false to indicate no admin privileges (default secure behavior) - importer := NewWithAdminFlag(store, actions, config, false) - - team := &model.Team{ - Id: "test-team-id", - Name: "test-team", - } - - user := &model.User{ - Username: "testuser", - Email: "testuser@restricted-domain.com", - FirstName: "Test", - LastName: "User", - } - - result := importer.oldImportUser(rctx, team, user) - - require.NotNil(t, result, "User import should succeed") - assert.Equal(t, "test-user-id", result.Id, "Should return the saved user") - assert.False(t, verifyEmailCalled, "NO IMPORTING USER: VerifyEmail should NOT be called when no importing user is provided") - assert.True(t, joinTeamCalled, "User should still be joined to the team") - - // Verify VerifyEmail was NOT called - userStore.AssertNotCalled(t, "VerifyEmail") -} - -// TestSlackImportEnhancedSecurityBackwardsCompatibility tests that the old New() constructor still works -func TestSlackImportEnhancedSecurityBackwardsCompatibility(t *testing.T) { - rctx := request.TestContext(t) - - store := &mocks.Store{} - userStore := &mocks.UserStore{} - store.On("User").Return(userStore) - - // Track if VerifyEmail is called (it should NOT be called with old constructor) - verifyEmailCalled := false - userStore.On("VerifyEmail", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return("user-id", nil).Run(func(args mock.Arguments) { - verifyEmailCalled = true - }) - - savedUser := &model.User{ - Id: "test-user-id", - Username: "testuser", - Email: "testuser@restricted-domain.com", - EmailVerified: false, // Should remain false with old constructor - Roles: model.SystemUserRoleId, - } - userStore.On("Save", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("*model.User")).Return(savedUser, nil) - - joinTeamCalled := false - actions := Actions{ - JoinUserToTeam: func(team *model.Team, user *model.User, userRequestorId string) (*model.TeamMember, *model.AppError) { - joinTeamCalled = true - return &model.TeamMember{}, nil - }, - } - - config := &model.Config{} - config.SetDefaults() - - // Use the old constructor (backwards compatibility) - importer := New(store, actions, config) - - team := &model.Team{ - Id: "test-team-id", - Name: "test-team", - } - - user := &model.User{ - Username: "testuser", - Email: "testuser@restricted-domain.com", - FirstName: "Test", - LastName: "User", - } - - result := importer.oldImportUser(rctx, team, user) - - require.NotNil(t, result, "User import should succeed") - assert.Equal(t, "test-user-id", result.Id, "Should return the saved user") - assert.False(t, verifyEmailCalled, "BACKWARDS COMPATIBILITY: VerifyEmail should NOT be called with old constructor") - assert.True(t, joinTeamCalled, "User should still be joined to the team") - - // Verify VerifyEmail was NOT called - userStore.AssertNotCalled(t, "VerifyEmail") -} - -type slackAddUsersTestSetup struct { - store *mocks.Store - teamStore *mocks.TeamStore - userStore *mocks.UserStore - team *model.Team - savedUser *model.User -} - -func newSlackAddUsersTestSetup(t *testing.T) *slackAddUsersTestSetup { - t.Helper() - - s := &slackAddUsersTestSetup{} - s.store = &mocks.Store{} - s.teamStore = &mocks.TeamStore{} - s.userStore = &mocks.UserStore{} - s.store.On("Team").Return(s.teamStore) - s.store.On("User").Return(s.userStore) - - s.team = &model.Team{Id: "test-team-id", Name: "test-team"} - s.teamStore.On("Get", "test-team-id").Return(s.team, nil) - s.userStore.On("GetByEmail", mock.AnythingOfType("string")).Return(nil, fmt.Errorf("not found")) - - s.savedUser = &model.User{Id: "test-user-id", Username: "testuser", Email: "testuser@example.com"} - s.userStore.On("Save", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("*model.User")).Return(s.savedUser, nil) - - return s -} - -func (s *slackAddUsersTestSetup) newImporter(actions Actions) *SlackImporter { - config := &model.Config{} - config.SetDefaults() - return New(s.store, actions, config) -} - -func (s *slackAddUsersTestSetup) newImporterWithAdminFlag(actions Actions, isAdminImport bool) *SlackImporter { - config := &model.Config{} - config.SetDefaults() - return NewWithAdminFlag(s.store, actions, config, isAdminImport) -} - -func (s *slackAddUsersTestSetup) defaultActions() Actions { - return Actions{ - JoinUserToTeam: func(team *model.Team, user *model.User, userRequestorId string) (*model.TeamMember, *model.AppError) { - return &model.TeamMember{}, nil - }, - SendPasswordReset: func(email string) (bool, *model.AppError) { - return true, nil - }, - } -} - -func defaultSlackUsers() []slackUser { - return []slackUser{ - {Id: "U001", Username: "testuser", Profile: slackProfile{FirstName: "Test", LastName: "User", Email: "testuser@example.com"}}, - } -} - -func TestSlackAddUsersLogContainsProperUserCreationMessage(t *testing.T) { - rctx := request.TestContext(t) - s := newSlackAddUsersTestSetup(t) - - importer := s.newImporter(s.defaultActions()) - importerLog := new(bytes.Buffer) - importer.slackAddUsers(rctx, "test-team-id", defaultSlackUsers(), importerLog) - - logOutput := importerLog.String() - assert.Contains(t, logOutput, "api.slackimport.slack_add_users.email", "import log should contain the user creation message") - assert.NotContains(t, logOutput, "api.slackimport.slack_add_users.email_pwd", "import log must not use the old user creation message") -} - -func TestSlackAddUsersLogsSendResetEmailFailure(t *testing.T) { - rctx := request.TestContext(t) - s := newSlackAddUsersTestSetup(t) - - actions := s.defaultActions() - actions.SendPasswordReset = func(email string) (bool, *model.AppError) { - return false, nil - } - - importer := s.newImporter(actions) - importerLog := new(bytes.Buffer) - importer.slackAddUsers(rctx, "test-team-id", defaultSlackUsers(), importerLog) - - assert.Contains(t, importerLog.String(), "api.slackimport.slack_add_users.send_reset_email_failed") -} - -func TestSlackAddUsersGeneratesUserWithEmptyPassword(t *testing.T) { - rctx := request.TestContext(t) - s := newSlackAddUsersTestSetup(t) - - importer := s.newImporter(s.defaultActions()) - importerLog := new(bytes.Buffer) - importer.slackAddUsers(rctx, "test-team-id", defaultSlackUsers(), importerLog) - - s.userStore.AssertCalled(t, "Save", mock.AnythingOfType("*request.Context"), mock.MatchedBy(func(u *model.User) bool { - return u.Password == "" - })) -} - -func TestSlackAddUsersTriggersPasswordResetFlow(t *testing.T) { - rctx := request.TestContext(t) - s := newSlackAddUsersTestSetup(t) - - passwordResetCalled := false - actions := s.defaultActions() - actions.SendPasswordReset = func(email string) (bool, *model.AppError) { - passwordResetCalled = true - return true, nil - } - - importer := s.newImporter(actions) - importerLog := new(bytes.Buffer) - importer.slackAddUsers(rctx, "test-team-id", defaultSlackUsers(), importerLog) - - assert.True(t, passwordResetCalled, "SendPasswordReset should be called for each imported user") -} - -// TestSlackAddUsersNonAdminImportDoesNotMergeExistingUser tests that a non-admin import skips -// a Slack user whose email matches an existing Mattermost account that is not already on the -// team, rather than force-enrolling that account into the team. -func TestSlackAddUsersNonAdminImportDoesNotMergeExistingUser(t *testing.T) { - rctx := request.TestContext(t) - - store := &mocks.Store{} - teamStore := &mocks.TeamStore{} - userStore := &mocks.UserStore{} - store.On("Team").Return(teamStore) - store.On("User").Return(userStore) - - team := &model.Team{Id: "test-team-id", Name: "test-team"} - teamStore.On("Get", "test-team-id").Return(team, nil) - - existingUser := &model.User{Id: "existing-user-id", Username: "existinguser", Email: "shared@example.com"} - userStore.On("GetByEmail", "shared@example.com").Return(existingUser, nil) - - var joinedUserIDs []string - actions := Actions{ - JoinUserToTeam: func(team *model.Team, user *model.User, userRequestorId string) (*model.TeamMember, *model.AppError) { - joinedUserIDs = append(joinedUserIDs, user.Id) - return &model.TeamMember{}, nil - }, - SendPasswordReset: func(email string) (bool, *model.AppError) { - return true, nil - }, - } - - config := &model.Config{} - config.SetDefaults() - importer := NewWithAdminFlag(store, actions, config, false) - - slackUsers := []slackUser{ - {Id: "U001", Username: "slackuser", Profile: slackProfile{FirstName: "Slack", LastName: "User", Email: "shared@example.com"}}, - } - - importerLog := new(bytes.Buffer) - addedUsers := importer.slackAddUsers(rctx, "test-team-id", slackUsers, importerLog) - - assert.NotContains(t, addedUsers, "U001", "user should be skipped entirely on email conflict") - assert.Empty(t, joinedUserIDs, "no account should be joined to the team") - userStore.AssertNotCalled(t, "Save", mock.Anything, mock.Anything) - assert.Contains(t, importerLog.String(), "api.slackimport.slack_add_users.merge_existing_skipped_non_admin") - assert.NotContains(t, importerLog.String(), "api.slackimport.slack_add_users.email") - assert.NotContains(t, importerLog.String(), "api.slackimport.slack_add_users.unable_import") -} - -// TestSlackAddUsersNonAdminImportSkipsExistingTeamMember tests that a non-admin import skips -// a Slack user whose email matches an existing Mattermost account even when that account is -// already a team member, preventing post authorship forgery. -func TestSlackAddUsersNonAdminImportSkipsExistingTeamMember(t *testing.T) { - rctx := request.TestContext(t) - - store := &mocks.Store{} - teamStore := &mocks.TeamStore{} - userStore := &mocks.UserStore{} - store.On("Team").Return(teamStore) - store.On("User").Return(userStore) - - team := &model.Team{Id: "test-team-id", Name: "test-team"} - teamStore.On("Get", "test-team-id").Return(team, nil) - - existingUser := &model.User{Id: "existing-user-id", Username: "existinguser", Email: "shared@example.com"} - userStore.On("GetByEmail", "shared@example.com").Return(existingUser, nil) - - var joinedUserIDs []string - actions := Actions{ - JoinUserToTeam: func(team *model.Team, user *model.User, userRequestorId string) (*model.TeamMember, *model.AppError) { - joinedUserIDs = append(joinedUserIDs, user.Id) - return &model.TeamMember{}, nil - }, - SendPasswordReset: func(email string) (bool, *model.AppError) { - return true, nil - }, - } - - config := &model.Config{} - config.SetDefaults() - importer := NewWithAdminFlag(store, actions, config, false) - - slackUsers := []slackUser{ - {Id: "U001", Username: "slackuser", Profile: slackProfile{FirstName: "Slack", LastName: "User", Email: "shared@example.com"}}, - } - - importerLog := new(bytes.Buffer) - addedUsers := importer.slackAddUsers(rctx, "test-team-id", slackUsers, importerLog) - - assert.NotContains(t, addedUsers, "U001", "existing account should be skipped even when already on the team") - assert.Empty(t, joinedUserIDs, "no account should be joined to the team") - userStore.AssertNotCalled(t, "Save", mock.Anything, mock.Anything) - assert.Contains(t, importerLog.String(), "api.slackimport.slack_add_users.merge_existing_skipped_non_admin") -} - -// TestSlackAddUsersAdminImportMergesExistingUser tests that an admin import preserves the -// existing behavior of adopting an existing Mattermost account that shares an email with a -// Slack user. -func TestSlackAddUsersAdminImportMergesExistingUser(t *testing.T) { - rctx := request.TestContext(t) - - store := &mocks.Store{} - teamStore := &mocks.TeamStore{} - userStore := &mocks.UserStore{} - store.On("Team").Return(teamStore) - store.On("User").Return(userStore) - - team := &model.Team{Id: "test-team-id", Name: "test-team"} - teamStore.On("Get", "test-team-id").Return(team, nil) - - existingUser := &model.User{Id: "existing-user-id", Username: "existinguser", Email: "shared@example.com"} - userStore.On("GetByEmail", "shared@example.com").Return(existingUser, nil) - - var joinedUserIDs []string - actions := Actions{ - JoinUserToTeam: func(team *model.Team, user *model.User, userRequestorId string) (*model.TeamMember, *model.AppError) { - joinedUserIDs = append(joinedUserIDs, user.Id) - return &model.TeamMember{}, nil - }, - SendPasswordReset: func(email string) (bool, *model.AppError) { - return true, nil - }, - } - - config := &model.Config{} - config.SetDefaults() - importer := NewWithAdminFlag(store, actions, config, true) - - slackUsers := []slackUser{ - {Id: "U001", Username: "slackuser", Profile: slackProfile{FirstName: "Slack", LastName: "User", Email: "shared@example.com"}}, - } - - importerLog := new(bytes.Buffer) - addedUsers := importer.slackAddUsers(rctx, "test-team-id", slackUsers, importerLog) - - require.Contains(t, addedUsers, "U001") - assert.Equal(t, "existing-user-id", addedUsers["U001"].Id, "admin import should still adopt the existing account") - assert.Contains(t, joinedUserIDs, "existing-user-id") - - userStore.AssertNotCalled(t, "Save", mock.Anything, mock.Anything) - - expectedLog := "api.slackimport.slack_add_users.created" + - "===============\r\n\r\n" + - "api.slackimport.slack_add_users.merge_existing" - assert.Equal(t, expectedLog, importerLog.String()) -} - -// TestSlackAddUsersCreatesNewAccountWhenNoExistingMatch tests that when no existing account -// matches the Slack user's email, a new account is created and joined to the team, regardless -// of whether the import is an admin import. -func TestSlackAddUsersCreatesNewAccountWhenNoExistingMatch(t *testing.T) { - rctx := request.TestContext(t) - s := newSlackAddUsersTestSetup(t) - - importer := s.newImporterWithAdminFlag(s.defaultActions(), false) - importerLog := new(bytes.Buffer) - addedUsers := importer.slackAddUsers(rctx, "test-team-id", defaultSlackUsers(), importerLog) - - require.Contains(t, addedUsers, "U001") - assert.Equal(t, s.savedUser.Id, addedUsers["U001"].Id) - assert.Contains(t, importerLog.String(), "api.slackimport.slack_add_users.email") -} diff --git a/server/public/model/audit_events.go b/server/public/model/audit_events.go index d0c0bed1d4e6..2e9732eb4ec3 100644 --- a/server/public/model/audit_events.go +++ b/server/public/model/audit_events.go @@ -237,7 +237,6 @@ const ( const ( AuditEventBulkImport = "bulkImport" // bulk import data from a file AuditEventDeleteImport = "deleteImport" // delete import file - AuditEventSlackImport = "slackImport" // import data from Slack ) // Jobs @@ -433,7 +432,6 @@ const ( AuditEventAddUserToTeamFromInvite = "addUserToTeamFromInvite" // add user to team using invitation link AuditEventCreateTeam = "createTeam" // create team AuditEventDeleteTeam = "deleteTeam" // delete team - AuditEventImportTeam = "importTeam" // import team data from external source AuditEventInvalidateAllEmailInvites = "invalidateAllEmailInvites" // invalidate all pending email invitations AuditEventInviteGuestsToChannels = "inviteGuestsToChannels" // invite guest users to specific channels AuditEventInviteUsersToTeam = "inviteUsersToTeam" // invite users to team diff --git a/server/public/model/client4.go b/server/public/model/client4.go index f13f4827f9c2..f54b5dada2b7 100644 --- a/server/public/model/client4.go +++ b/server/public/model/client4.go @@ -269,10 +269,6 @@ func (c *Client4) teamStatsRoute(teamId string) clientRoute { return c.teamRoute(teamId).Join("stats") } -func (c *Client4) teamImportRoute(teamId string) clientRoute { - return c.teamRoute(teamId).Join("import") -} - func (c *Client4) channelsRoute() clientRoute { return newClientRoute("channels") } @@ -2629,50 +2625,6 @@ func (c *Client4) GetTeamUnread(ctx context.Context, teamId, userId string) (*Te return DecodeJSONFromResponse[*TeamUnread](r) } -// ImportTeam will import an exported team from other app into a existing team. -func (c *Client4) ImportTeam(ctx context.Context, data []byte, filesize int, importFrom, filename, teamId string) (map[string]string, *Response, error) { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - - part, err := writer.CreateFormFile("file", filename) - if err != nil { - return nil, nil, err - } - - if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil { - return nil, nil, err - } - - part, err = writer.CreateFormField("filesize") - if err != nil { - return nil, nil, err - } - - if _, err = io.Copy(part, strings.NewReader(strconv.Itoa(filesize))); err != nil { - return nil, nil, err - } - - part, err = writer.CreateFormField("importFrom") - if err != nil { - return nil, nil, err - } - - if _, err = io.Copy(part, strings.NewReader(importFrom)); err != nil { - return nil, nil, err - } - - if err = writer.Close(); err != nil { - return nil, nil, err - } - - r, err := c.doAPIRequestReaderRoute(ctx, http.MethodPost, c.teamImportRoute(teamId), writer.FormDataContentType(), body, nil) - if err != nil { - return nil, BuildResponse(r), err - } - defer closeBody(r) - return DecodeJSONFromResponse[map[string]string](r) -} - // InviteUsersToTeam invite users by email to the team. func (c *Client4) InviteUsersToTeam(ctx context.Context, teamId string, userEmails []string) (*Response, error) { r, err := c.doAPIPostJSON(ctx, c.teamRoute(teamId).Join("invite", "email"), userEmails) diff --git a/server/public/model/migration.go b/server/public/model/migration.go index 7ece8d257ba8..065062067e9b 100644 --- a/server/public/model/migration.go +++ b/server/public/model/migration.go @@ -66,4 +66,5 @@ const ( MigrationKeyAddManageAgentPermissions = "add_manage_agent_permissions" MigrationKeyAddEditFileAttachmentPermission = "add_edit_file_attachment_permission" MigrationKeyAddDiscoverableChannelPermissions = "add_discoverable_channel_permissions" + MigrationRemoveImportTeamPermission = "remove_import_team_permission" ) diff --git a/server/public/model/permission.go b/server/public/model/permission.go index 9a89b665473c..abb69c3b54a6 100644 --- a/server/public/model/permission.go +++ b/server/public/model/permission.go @@ -99,7 +99,6 @@ var PermissionDeleteOthersPosts *Permission var PermissionRemoveUserFromTeam *Permission var PermissionCreateTeam *Permission var PermissionManageTeam *Permission -var PermissionImportTeam *Permission var PermissionViewTeam *Permission var PermissionListUsersWithoutTeam *Permission var PermissionReadJobs *Permission @@ -1190,12 +1189,6 @@ func initializePermissions() { "authentication.permissions.manage_team.description", PermissionScopeTeam, } - PermissionImportTeam = &Permission{ - "import_team", - "authentication.permissions.import_team.name", - "authentication.permissions.import_team.description", - PermissionScopeTeam, - } PermissionViewTeam = &Permission{ "view_team", "authentication.permissions.view_team.name", @@ -2611,7 +2604,6 @@ func initializePermissions() { PermissionDeleteOthersEmojis, PermissionRemoveUserFromTeam, PermissionManageTeam, - PermissionImportTeam, PermissionViewTeam, PermissionViewMembers, PermissionInviteGuest, diff --git a/server/public/model/role.go b/server/public/model/role.go index d4a392849069..e0327e8fac31 100644 --- a/server/public/model/role.go +++ b/server/public/model/role.go @@ -1067,7 +1067,6 @@ func MakeDefaultRoles() map[string]*Role { Permissions: []string{ PermissionRemoveUserFromTeam.Id, PermissionManageTeam.Id, - PermissionImportTeam.Id, PermissionManageTeamRoles.Id, PermissionManageTeamAccessRules.Id, PermissionManageChannelRoles.Id, diff --git a/server/tests/slack-import-test-channels.json b/server/tests/slack-import-test-channels.json deleted file mode 100644 index c8a2b5ec375c..000000000000 --- a/server/tests/slack-import-test-channels.json +++ /dev/null @@ -1,165 +0,0 @@ -[ - { - "id": "C13CFUSDV", - "name": "artwork", - "created": "1461575251", - "creator": "U07Q4MHCP", - "is_archived": false, - "is_general": false, - "members": [ - "U07Q4MHCP", - "U13C5JZ7W", - "U13CLGTKK", - "U13CSKE2E" - ], - "topic": { - "value": "", - "creator": "", - "last_set": "0" - }, - "purpose": { - "value": "Discussing art", - "creator": "U07Q4MHCP", - "last_set": "1461575252" - } - }, - { - "id": "C13CN0Q0H", - "name": "eating-out", - "created": "1461575308", - "creator": "U07Q4MHCP", - "is_archived": false, - "is_general": false, - "members": [ - "U07Q4MHCP", - "U13C5JZ7W", - "U13CLGTKK", - "U13CSKE2E" - ], - "topic": { - "value": "", - "creator": "", - "last_set": "0" - }, - "purpose": { - "value": "Restaurants around town", - "creator": "U07Q4MHCP", - "last_set": "1461575309" - } - }, - { - "id": "C07Q4SBL3", - "name": "general", - "created": "1437124885", - "creator": "U07Q4MHCP", - "is_archived": false, - "is_general": true, - "members": [ - "U07Q4MHCP", - "U13C5JZ7W", - "U13CLGTKK", - "U13CSKE2E", - "U15F73RL4", - "U15F922BY", - "U15FHKEKX", - "U15FHR92R", - "U15FUR1QS", - "U1THQPEMV", - "U24CG47QU" - ], - "topic": { - "value": "Company-wide announcements and work-based matters", - "creator": "", - "last_set": "0" - }, - "purpose": { - "value": "This channel is for team-wide communication and announcements. All team members are in this channel.", - "creator": "", - "last_set": "0" - } - }, - { - "id": "C13B104PM", - "name": "movies", - "created": "1461575333", - "creator": "U07Q4MHCP", - "is_archived": false, - "is_general": false, - "members": [ - "U07Q4MHCP", - "U13C5JZ7W", - "U13CLGTKK", - "U13CSKE2E" - ], - "topic": { - "value": "", - "creator": "", - "last_set": "0" - }, - "purpose": { - "value": "New movies", - "creator": "U07Q4MHCP", - "last_set": "1461575334" - } - }, - { - "id": "C15FGPREU", - "name": "outings", - "created": "1462216386", - "creator": "U15FUR1QS", - "is_archived": false, - "is_general": false, - "members": [ - "U07Q4MHCP", - "U13C5JZ7W", - "U13CLGTKK", - "U13CSKE2E", - "U15F73RL4", - "U15F922BY", - "U15FHKEKX", - "U15FHR92R", - "U15FUR1QS" - ], - "topic": { - "value": "", - "creator": "", - "last_set": "0" - }, - "purpose": { - "value": "Let's discuss our next monthly outing", - "creator": "U15FUR1QS", - "last_set": "1462216387" - } - }, - { - "id": "C07Q4MBTL", - "name": "random", - "created": "1437124885", - "creator": "U07Q4MHCP", - "is_archived": false, - "is_general": false, - "members": [ - "U07Q4MHCP", - "U13C5JZ7W", - "U13CLGTKK", - "U13CSKE2E", - "U15F73RL4", - "U15F922BY", - "U15FHKEKX", - "U15FHR92R", - "U15FUR1QS", - "U1THQPEMV", - "U24CG47QU" - ], - "topic": { - "value": "Non-work banter and water cooler conversation", - "creator": "", - "last_set": "0" - }, - "purpose": { - "value": "A place for non-work-related flimflam, faffing, hodge-podge or jibber-jabber you'd prefer to keep out of more focused work-related channels.", - "creator": "", - "last_set": "0" - } - } -] \ No newline at end of file diff --git a/server/tests/slack-import-test-direct-messages.json b/server/tests/slack-import-test-direct-messages.json deleted file mode 100644 index c13fe1b3363a..000000000000 --- a/server/tests/slack-import-test-direct-messages.json +++ /dev/null @@ -1,34 +0,0 @@ -[ - { - "id": "D24TJMP61", - "created": "1472131245", - "members": [ - "U1VVY70HK", - "U1W4XLR62" - ] - }, - { - "id": "D24TJNXUG", - "created": "1472132335", - "members": [ - "U1XG9JUH1", - "U20APGU90" - ] - }, - { - "id": "D24TLE5V5", - "created": "1472131113", - "members": [ - "U1TQ3A3L7", - "U1URR19QV" - ] - }, - { - "id": "D24TM4MPT", - "created": "1472131021", - "members": [ - "U1V47GAM9", - "U1XVNP6DT" - ] - } -] \ No newline at end of file diff --git a/server/tests/slack-import-test-group-direct-messages.json b/server/tests/slack-import-test-group-direct-messages.json deleted file mode 100644 index 4e00c78c8a23..000000000000 --- a/server/tests/slack-import-test-group-direct-messages.json +++ /dev/null @@ -1,69 +0,0 @@ -[ - { - "id": "G1XSSNBTP", - "name": "mpdm-mcauly--sutton--barker--lankford-1", - "created": 1470239933, - "creator": "U1FFMLSKC", - "is_archived": false, - "members": [ - "U1934896U", - "U1TTSPAGP", - "U1FFMLSKC", - "U1U234D4P" - ], - "topic": { - "value": "Group messaging", - "creator": "U1FFMLSKC", - "last_set": "1470239933" - }, - "purpose": { - "value": "Group messaging with: @mcauly @sutton @barker @lankford", - "creator": "U1FFMLSKC", - "last_set": "1470239933" - } - }, - { - "id": "G22KV02G2", - "name": "mpdm-mcauly--wolfgram--weddington-1", - "created": 1471532476, - "creator": "U1K9SEY9K", - "is_archived": false, - "members": [ - "U1934896U", - "U1K9SEY9K", - "U1YTGR5CP" - ], - "topic": { - "value": "Group messaging", - "creator": "U1K9SEY9K", - "last_set": "1471532476" - }, - "purpose": { - "value": "Group messaging with: @mcauly @wolfgram @weddington", - "creator": "U1K9SEY9K", - "last_set": "1471532476" - } - }, - { - "id": "G2436812A", - "name": "mpdm-mcauly--lovell--buck-1", - "created": 1471964085, - "creator": "U1934896U", - "is_archived": false, - "members": [ - "U1934896U", - "U1YYZNDJP", - "U1JSZSPR7" - ], - "topic": { - "value": "Group messaging", - "creator": "U1934896U", - "last_set": "1471964085" - }, - "purpose": { - "value": "Group messaging with: @mcauly @lovell @buck", - "creator": "U1934896U", - "last_set": "1471964085" - } - } -] \ No newline at end of file diff --git a/server/tests/slack-import-test-posts.json b/server/tests/slack-import-test-posts.json deleted file mode 100644 index 07b8c65d5bac..000000000000 --- a/server/tests/slack-import-test-posts.json +++ /dev/null @@ -1,288 +0,0 @@ -[ - { - "type": "message", - "user": "U07Q4MHCP", - "text": "<@U24CG47QU>", - "ts": "1472932439.000002" - }, - { - "user": "U07Q4MHCP", - "text": "<@U07Q4MHCP|lindy> has left the channel", - "type": "message", - "subtype": "channel_leave", - "ts": "1469440454.000002" - }, - { - "user": "U15FUR1QS", - "purpose": "Let's discuss our next monthly outing", - "type": "message", - "subtype": "channel_purpose", - "text": "<@U15FUR1QS|igor> set the channel purpose: Let's discuss our next monthly outing", - "ts": "1462216387.000003" - }, - { - "type": "message", - "subtype": "file_share", - "text": "<@U07Q4MHCP|lindy> uploaded a file: ", - "file": { - "id": "F1WB5FH7W", - "created": 1469782519, - "timestamp": 1469782519, - "name": "Pdf.pdf", - "title": "Pdf.pdf", - "mimetype": "application\/pdf", - "filetype": "pdf", - "pretty_type": "PDF", - "user": "U07Q4MHCP", - "editable": false, - "size": 457717, - "mode": "hosted", - "is_external": false, - "external_type": "", - "is_public": true, - "public_url_shared": false, - "display_as_bot": false, - "username": "", - "url_private": "https:\/\/files.slack.com\/files-pri\/T07Q4MHC7-F1WB5FH7W\/pdf.pdf?t=xoxe-7820731415-83616398485-83620610359-b95751e3ea", - "url_private_download": "https:\/\/files.slack.com\/files-pri\/T07Q4MHC7-F1WB5FH7W\/download\/pdf.pdf?t=xoxe-7820731415-83616398485-83620610359-b95751e3ea", - "permalink": "https:\/\/slackexportteam.slack.com\/files\/lindy\/F1WB5FH7W\/pdf.pdf", - "permalink_public": "https:\/\/slack-files.com\/T07Q4MHC7-F1WB5FH7W-8560bd68a4", - "channels": [ - "C07Q4MBTL" - ], - "groups": [ - - ], - "ims": [ - - ], - "comments_count": 0 - }, - "user": "U07Q4MHCP", - "upload": true, - "display_as_bot": false, - "username": "<@U07Q4MHCP|lindy>", - "bot_id": null, - "ts": "1469782519.000002" - }, - { - "user": "U24CG47QU", - "text": "<@U24CG47QU|lindy.invite> has joined the channel", - "type": "message", - "subtype": "channel_join", - "ts": "1472028157.000002" - }, - { - "bot_id": "B13CH9S5V", - "attachments": [ - { - "fallback": ": M2 informatique PLS et EID2 on vous retrouve sur le du master, les invitations partiront lundi (ou demander en MP).", - "ts": 1474722287, - "author_name": "Master 2 PLS", - "author_link": "https:\/\/twitter.com\/m2pls\/status\/779667900600844288", - "author_icon": "https:\/\/pbs.twimg.com\/profile_images\/649829889890086912\/ULpVHs9S_normal.jpg", - "author_subname": "@m2pls", - "pretext": "", - "text": "M2 informatique PLS et EID2 on vous retrouve sur le du master, les invitations partiront lundi (ou demander en MP).", - "service_name": "twitter", - "service_url": "https:\/\/twitter.com\/", - "from_url": "https:\/\/twitter.com\/m2pls\/status\/779667900600844288", - "id": 1, - "footer": "Twitter", - "footer_icon": "https:\/\/a.slack-edge.com\/6e067\/img\/services\/twitter_pixel_snapped_32.png" - } - ], - "text": null, - "type": "message", - "subtype": "bot_message", - "ts": "1474722288.637240" - }, - { - "type": "message", - "user": "U07Q4MHCP", - "text": "", - "attachments": [ - { - "from_url": "https:\/\/slackexportteam.slack.com\/archives\/general\/p1469781868000005", - "fallback": "[July 29th, 2016 1:44 AM] lindy: permalink?", - "ts": "1469781868.000005", - "author_subname": "lindy", - "channel_id": "C07Q4SBL3", - "channel_name": "general", - "is_msg_unfurl": true, - "text": "permalink?", - "author_link": "https:\/\/slackexportteam.slack.com\/team\/lindy", - "author_icon": "https:\/\/secure.gravatar.com\/avatar\/822d051544b1f50f1e9f015cd2990edc.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2F0180%2Fimg%2Favatars%2Fava_0001-48.png", - "mrkdwn_in": [ - "text" - ], - "id": 1, - "footer": "Posted in #general" - } - ], - "ts": "1469781880.000006" - }, - { - "type": "message", - "subtype": "file_share", - "text": "<@U07Q4MHCP|lindy> uploaded a file: ", - "file": { - "id": "F1WBDJW93", - "created": 1469782076, - "timestamp": 1469782076, - "name": "fb-wow.gif", - "title": "fb-wow.gif", - "mimetype": "image\/gif", - "filetype": "gif", - "pretty_type": "GIF", - "user": "U07Q4MHCP", - "editable": false, - "size": 27235, - "mode": "hosted", - "is_external": false, - "external_type": "", - "is_public": true, - "public_url_shared": false, - "display_as_bot": false, - "username": "", - "url_private": "https:\/\/files.slack.com\/files-pri\/T07Q4MHC7-F1WBDJW93\/fb-wow.gif?t=xoxe-7820731415-83616398485-83620610359-b95751e3ea", - "url_private_download": "https:\/\/files.slack.com\/files-pri\/T07Q4MHC7-F1WBDJW93\/download\/fb-wow.gif?t=xoxe-7820731415-83616398485-83620610359-b95751e3ea", - "thumb_64": "https:\/\/files.slack.com\/files-tmb\/T07Q4MHC7-F1WBDJW93-7031b90b93\/fb-wow_64.png?t=xoxe-7820731415-83616398485-83620610359-b95751e3ea", - "thumb_80": "https:\/\/files.slack.com\/files-tmb\/T07Q4MHC7-F1WBDJW93-7031b90b93\/fb-wow_80.png?t=xoxe-7820731415-83616398485-83620610359-b95751e3ea", - "thumb_360": "https:\/\/files.slack.com\/files-tmb\/T07Q4MHC7-F1WBDJW93-7031b90b93\/fb-wow_360.png?t=xoxe-7820731415-83616398485-83620610359-b95751e3ea", - "thumb_360_w": 59, - "thumb_360_h": 59, - "thumb_160": "https:\/\/files.slack.com\/files-tmb\/T07Q4MHC7-F1WBDJW93-7031b90b93\/fb-wow_160.png?t=xoxe-7820731415-83616398485-83620610359-b95751e3ea", - "image_exif_rotation": 1, - "original_w": 59, - "original_h": 59, - "deanimate_gif": "https:\/\/files.slack.com\/files-tmb\/T07Q4MHC7-F1WBDJW93-7031b90b93\/fb-wow_deanimate_gif.png?t=xoxe-7820731415-83616398485-83620610359-b95751e3ea", - "permalink": "https:\/\/slackexportteam.slack.com\/files\/lindy\/F1WBDJW93\/fb-wow.gif", - "permalink_public": "https:\/\/slack-files.com\/T07Q4MHC7-F1WBDJW93-718081d960", - "channels": [ - "C07Q4SBL3" - ], - "groups": [ - - ], - "ims": [ - - ], - "comments_count": 0 - }, - "user": "U07Q4MHCP", - "upload": true, - "display_as_bot": false, - "username": "<@U07Q4MHCP|lindy>", - "bot_id": null, - "ts": "1469782077.000010" - }, - { - "type": "message", - "text": "multiple_attachments", - "files": [ - { - "id": "FL9GSN60L", - "created": 1562758810, - "timestamp": 1562758810, - "name": "Screenshot from 2019-07-09 17-08-24.png", - "title": "Screenshot from 2019-07-09 17-08-24.png", - "mimetype": "image\/png", - "filetype": "png", - "pretty_type": "PNG", - "user": "UKW2XKH7E", - "editable": false, - "size": 79513, - "mode": "hosted", - "is_external": false, - "external_type": "", - "is_public": true, - "public_url_shared": false, - "display_as_bot": false, - "username": "", - "url_private": "https:\/\/files.slack.com\/files-pri\/TKK4L637U-FL9GSN60L\/screenshot_from_2019-07-09_17-08-24.png?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "url_private_download": "https:\/\/files.slack.com\/files-pri\/TKK4L637U-FL9GSN60L\/download\/screenshot_from_2019-07-09_17-08-24.png?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "thumb_64": "https:\/\/files.slack.com\/files-tmb\/TKK4L637U-FL9GSN60L-e70b98a77f\/screenshot_from_2019-07-09_17-08-24_64.png?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "thumb_80": "https:\/\/files.slack.com\/files-tmb\/TKK4L637U-FL9GSN60L-e70b98a77f\/screenshot_from_2019-07-09_17-08-24_80.png?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "thumb_360": "https:\/\/files.slack.com\/files-tmb\/TKK4L637U-FL9GSN60L-e70b98a77f\/screenshot_from_2019-07-09_17-08-24_360.png?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "thumb_360_w": 197, - "thumb_360_h": 360, - "thumb_480": "https:\/\/files.slack.com\/files-tmb\/TKK4L637U-FL9GSN60L-e70b98a77f\/screenshot_from_2019-07-09_17-08-24_480.png?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "thumb_480_w": 263, - "thumb_480_h": 480, - "thumb_160": "https:\/\/files.slack.com\/files-tmb\/TKK4L637U-FL9GSN60L-e70b98a77f\/screenshot_from_2019-07-09_17-08-24_160.png?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "thumb_720": "https:\/\/files.slack.com\/files-tmb\/TKK4L637U-FL9GSN60L-e70b98a77f\/screenshot_from_2019-07-09_17-08-24_720.png?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "thumb_720_w": 395, - "thumb_720_h": 720, - "thumb_800": "https:\/\/files.slack.com\/files-tmb\/TKK4L637U-FL9GSN60L-e70b98a77f\/screenshot_from_2019-07-09_17-08-24_800.png?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "thumb_800_w": 439, - "thumb_800_h": 800, - "thumb_960": "https:\/\/files.slack.com\/files-tmb\/TKK4L637U-FL9GSN60L-e70b98a77f\/screenshot_from_2019-07-09_17-08-24_960.png?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "thumb_960_w": 526, - "thumb_960_h": 960, - "thumb_1024": "https:\/\/files.slack.com\/files-tmb\/TKK4L637U-FL9GSN60L-e70b98a77f\/screenshot_from_2019-07-09_17-08-24_1024.png?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "thumb_1024_w": 561, - "thumb_1024_h": 1024, - "image_exif_rotation": 1, - "original_w": 632, - "original_h": 1153, - "permalink": "https:\/\/svenstestteam.slack.com\/files\/UKW2XKH7E\/FL9GSN60L\/screenshot_from_2019-07-09_17-08-24.png", - "permalink_public": "https:\/\/slack-files.com\/TKK4L637U-FL9GSN60L-19fdfd655a", - "has_rich_preview": false - }, - { - "id": "FL3LBGRT3", - "created": 1562758810, - "timestamp": 1562758810, - "name": "vscode.jpg", - "title": "vscode.jpg", - "mimetype": "image\/jpeg", - "filetype": "jpg", - "pretty_type": "JPEG", - "user": "UKW2XKH7E", - "editable": false, - "size": 549439, - "mode": "hosted", - "is_external": false, - "external_type": "", - "is_public": true, - "public_url_shared": false, - "display_as_bot": false, - "username": "", - "url_private": "https:\/\/files.slack.com\/files-pri\/TKK4L637U-FL3LBGRT3\/vscode.jpg?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "url_private_download": "https:\/\/files.slack.com\/files-pri\/TKK4L637U-FL3LBGRT3\/download\/vscode.jpg?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "thumb_64": "https:\/\/files.slack.com\/files-tmb\/TKK4L637U-FL3LBGRT3-ece28edc37\/vscode_64.jpg?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "thumb_80": "https:\/\/files.slack.com\/files-tmb\/TKK4L637U-FL3LBGRT3-ece28edc37\/vscode_80.jpg?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "thumb_360": "https:\/\/files.slack.com\/files-tmb\/TKK4L637U-FL3LBGRT3-ece28edc37\/vscode_360.jpg?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "thumb_360_w": 360, - "thumb_360_h": 203, - "thumb_480": "https:\/\/files.slack.com\/files-tmb\/TKK4L637U-FL3LBGRT3-ece28edc37\/vscode_480.jpg?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "thumb_480_w": 480, - "thumb_480_h": 270, - "thumb_160": "https:\/\/files.slack.com\/files-tmb\/TKK4L637U-FL3LBGRT3-ece28edc37\/vscode_160.jpg?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "thumb_720": "https:\/\/files.slack.com\/files-tmb\/TKK4L637U-FL3LBGRT3-ece28edc37\/vscode_720.jpg?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "thumb_720_w": 720, - "thumb_720_h": 405, - "thumb_800": "https:\/\/files.slack.com\/files-tmb\/TKK4L637U-FL3LBGRT3-ece28edc37\/vscode_800.jpg?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "thumb_800_w": 800, - "thumb_800_h": 450, - "thumb_960": "https:\/\/files.slack.com\/files-tmb\/TKK4L637U-FL3LBGRT3-ece28edc37\/vscode_960.jpg?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "thumb_960_w": 960, - "thumb_960_h": 540, - "thumb_1024": "https:\/\/files.slack.com\/files-tmb\/TKK4L637U-FL3LBGRT3-ece28edc37\/vscode_1024.jpg?t=xoxe-665156207266-683692183393-689571362948-acb8000145d9929cba3d5c38249a6673", - "thumb_1024_w": 1024, - "thumb_1024_h": 576, - "image_exif_rotation": 1, - "original_w": 1920, - "original_h": 1080, - "permalink": "https:\/\/svenstestteam.slack.com\/files\/UKW2XKH7E\/FL3LBGRT3\/vscode.jpg", - "permalink_public": "https:\/\/slack-files.com\/TKK4L637U-FL3LBGRT3-ffd5da7911", - "has_rich_preview": false - } - ], - "upload": true, - "user": "UKW2XKH7E", - "ts": "1562758814.000100", - "client_msg_id": "bf2a1192-6b57-4dd3-90f9-8e833fcad758" - } -] diff --git a/server/tests/slack-import-test-private-channels.json b/server/tests/slack-import-test-private-channels.json deleted file mode 100644 index 1e5adf991d4d..000000000000 --- a/server/tests/slack-import-test-private-channels.json +++ /dev/null @@ -1,25 +0,0 @@ -[ - { - "id": "G1T7B1T9N", - "name": "csi-team-chat", - "created": 1468959839, - "creator": "U17G57PB6", - "is_archived": false, - "members": [ - "U15CML9C1", - "U1934896U", - "U17G57PB6", - "U1T7A2J56" - ], - "topic": { - "value": "\"A constantly evolving landscape of hills\"", - "creator": "U1934896U", - "last_set": "1469017133" - }, - "purpose": { - "value": "CSI team chat", - "creator": "U17G57PB6", - "last_set": "1468972624" - } - } -] \ No newline at end of file diff --git a/server/tests/slack-import-test-users.json b/server/tests/slack-import-test-users.json deleted file mode 100644 index 40b53a45b578..000000000000 --- a/server/tests/slack-import-test-users.json +++ /dev/null @@ -1,363 +0,0 @@ -[ - { - "id": "U13CLGTKK", - "team_id": "T07Q4MHC7", - "name": "alice", - "deleted": false, - "status": null, - "color": "4bbe2e", - "real_name": "Alice", - "tz": "Africa\/Cairo", - "tz_label": "Eastern European Time", - "tz_offset": 7200, - "profile": { - "first_name": "Alice", - "last_name": "", - "avatar_hash": "g2280143ab5d", - "real_name": "Alice", - "real_name_normalized": "Alice", - "email": "lindy+1@example.com", - "image_24": "https:\/\/secure.gravatar.com\/avatar\/2280143ab5dd760d0a951b74bd06185a.jpg?s=24&d=https%3A%2F%2Fa.slack-edge.com%2F0180%2Fimg%2Favatars%2Fava_0021-24.png", - "image_32": "https:\/\/secure.gravatar.com\/avatar\/2280143ab5dd760d0a951b74bd06185a.jpg?s=32&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0021-32.png", - "image_48": "https:\/\/secure.gravatar.com\/avatar\/2280143ab5dd760d0a951b74bd06185a.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2F3654%2Fimg%2Favatars%2Fava_0021-48.png", - "image_72": "https:\/\/secure.gravatar.com\/avatar\/2280143ab5dd760d0a951b74bd06185a.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0021-72.png", - "image_192": "https:\/\/secure.gravatar.com\/avatar\/2280143ab5dd760d0a951b74bd06185a.jpg?s=192&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0021-192.png", - "image_512": "https:\/\/secure.gravatar.com\/avatar\/2280143ab5dd760d0a951b74bd06185a.jpg?s=512&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0021-512.png", - "fields": null - }, - "is_admin": true, - "is_owner": false, - "is_primary_owner": false, - "is_restricted": false, - "is_ultra_restricted": false, - "is_bot": false - }, - { - "id": "U15F73RL4", - "team_id": "T07Q4MHC7", - "name": "brett", - "deleted": false, - "status": null, - "color": "e96699", - "real_name": "Brett", - "tz": "Africa\/Cairo", - "tz_label": "Eastern European Time", - "tz_offset": 7200, - "profile": { - "first_name": "Brett", - "last_name": "", - "avatar_hash": "g16de52e9c3d", - "real_name": "Brett", - "real_name_normalized": "Brett", - "email": "lindy+13@example.com", - "image_24": "https:\/\/secure.gravatar.com\/avatar\/16de52e9c3dd5917b72a327f9b36a7db.jpg?s=24&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0001-24.png", - "image_32": "https:\/\/secure.gravatar.com\/avatar\/16de52e9c3dd5917b72a327f9b36a7db.jpg?s=32&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0001-32.png", - "image_48": "https:\/\/secure.gravatar.com\/avatar\/16de52e9c3dd5917b72a327f9b36a7db.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2F0180%2Fimg%2Favatars%2Fava_0001-48.png", - "image_72": "https:\/\/secure.gravatar.com\/avatar\/16de52e9c3dd5917b72a327f9b36a7db.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F3654%2Fimg%2Favatars%2Fava_0001-72.png", - "image_192": "https:\/\/secure.gravatar.com\/avatar\/16de52e9c3dd5917b72a327f9b36a7db.jpg?s=192&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0001-192.png", - "image_512": "https:\/\/secure.gravatar.com\/avatar\/16de52e9c3dd5917b72a327f9b36a7db.jpg?s=512&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0001-512.png", - "fields": null - }, - "is_admin": false, - "is_owner": false, - "is_primary_owner": false, - "is_restricted": false, - "is_ultra_restricted": false, - "is_bot": false - }, - { - "id": "U15FUR1QS", - "team_id": "T07Q4MHC7", - "name": "igor", - "deleted": false, - "status": null, - "color": "5b89d5", - "real_name": "Igor", - "tz": "Africa\/Cairo", - "tz_label": "Eastern European Time", - "tz_offset": 7200, - "profile": { - "first_name": "Igor", - "last_name": "", - "avatar_hash": "gd2d8b6603c7", - "real_name": "Igor", - "real_name_normalized": "Igor", - "email": "lindy+12@example.com", - "image_24": "https:\/\/secure.gravatar.com\/avatar\/d2d8b6603c7e7eb443ccc103f50852c5.jpg?s=24&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0019-24.png", - "image_32": "https:\/\/secure.gravatar.com\/avatar\/d2d8b6603c7e7eb443ccc103f50852c5.jpg?s=32&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0019-32.png", - "image_48": "https:\/\/secure.gravatar.com\/avatar\/d2d8b6603c7e7eb443ccc103f50852c5.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0019-48.png", - "image_72": "https:\/\/secure.gravatar.com\/avatar\/d2d8b6603c7e7eb443ccc103f50852c5.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0019-72.png", - "image_192": "https:\/\/secure.gravatar.com\/avatar\/d2d8b6603c7e7eb443ccc103f50852c5.jpg?s=192&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0019-192.png", - "image_512": "https:\/\/secure.gravatar.com\/avatar\/d2d8b6603c7e7eb443ccc103f50852c5.jpg?s=512&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0019-512.png", - "fields": null - }, - "is_admin": false, - "is_owner": false, - "is_primary_owner": false, - "is_restricted": false, - "is_ultra_restricted": false, - "is_bot": false - }, - { - "id": "U15FHR92R", - "team_id": "T07Q4MHC7", - "name": "karen", - "deleted": false, - "status": null, - "color": "684b6c", - "real_name": "Karen", - "tz": "Africa\/Cairo", - "tz_label": "Eastern European Time", - "tz_offset": 7200, - "profile": { - "first_name": "Karen", - "last_name": "", - "avatar_hash": "g6fd11d3e252", - "real_name": "Karen", - "real_name_normalized": "Karen", - "email": "lindy+10@example.com", - "image_24": "https:\/\/secure.gravatar.com\/avatar\/6fd11d3e252e571d7371c7a5bbeb54ff.jpg?s=24&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0007-24.png", - "image_32": "https:\/\/secure.gravatar.com\/avatar\/6fd11d3e252e571d7371c7a5bbeb54ff.jpg?s=32&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0007-32.png", - "image_48": "https:\/\/secure.gravatar.com\/avatar\/6fd11d3e252e571d7371c7a5bbeb54ff.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0007-48.png", - "image_72": "https:\/\/secure.gravatar.com\/avatar\/6fd11d3e252e571d7371c7a5bbeb54ff.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0007-72.png", - "image_192": "https:\/\/secure.gravatar.com\/avatar\/6fd11d3e252e571d7371c7a5bbeb54ff.jpg?s=192&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0007-192.png", - "image_512": "https:\/\/secure.gravatar.com\/avatar\/6fd11d3e252e571d7371c7a5bbeb54ff.jpg?s=512&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0007-512.png", - "fields": null - }, - "is_admin": false, - "is_owner": false, - "is_primary_owner": false, - "is_restricted": false, - "is_ultra_restricted": false, - "is_bot": false - }, - { - "id": "U1THQPEMV", - "team_id": "T07Q4MHC7", - "name": "lindsay", - "deleted": false, - "status": null, - "color": "2b6836", - "real_name": "Lindsay B", - "tz": "America\/New_York", - "tz_label": "Eastern Daylight Time", - "tz_offset": -14400, - "profile": { - "first_name": "Lindsay", - "last_name": "B", - "avatar_hash": "g59bda23e5db", - "real_name": "Lindsay B", - "real_name_normalized": "Lindsay B", - "email": "lindsay@example.com", - "image_24": "https:\/\/secure.gravatar.com\/avatar\/59bda23e5dbe5930c3941c7bb90c3e2a.jpg?s=24&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0000-24.png", - "image_32": "https:\/\/secure.gravatar.com\/avatar\/59bda23e5dbe5930c3941c7bb90c3e2a.jpg?s=32&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0000-32.png", - "image_48": "https:\/\/secure.gravatar.com\/avatar\/59bda23e5dbe5930c3941c7bb90c3e2a.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0000-48.png", - "image_72": "https:\/\/secure.gravatar.com\/avatar\/59bda23e5dbe5930c3941c7bb90c3e2a.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0000-72.png", - "image_192": "https:\/\/secure.gravatar.com\/avatar\/59bda23e5dbe5930c3941c7bb90c3e2a.jpg?s=192&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0000-192.png", - "image_512": "https:\/\/secure.gravatar.com\/avatar\/59bda23e5dbe5930c3941c7bb90c3e2a.jpg?s=512&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0000-512.png", - "fields": null - }, - "is_admin": false, - "is_owner": false, - "is_primary_owner": false, - "is_restricted": false, - "is_ultra_restricted": false, - "is_bot": false - }, - { - "id": "U07Q4MHCP", - "team_id": "T07Q4MHC7", - "name": "lindy", - "deleted": false, - "status": null, - "color": "9f69e7", - "real_name": "", - "tz": "Africa\/Cairo", - "tz_label": "Eastern European Time", - "tz_offset": 7200, - "profile": { - "avatar_hash": "g822d051544b", - "real_name": "", - "real_name_normalized": "", - "email": "lindy@example.com", - "image_24": "https:\/\/secure.gravatar.com\/avatar\/822d051544b1f50f1e9f015cd2990edc.jpg?s=24&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0001-24.png", - "image_32": "https:\/\/secure.gravatar.com\/avatar\/822d051544b1f50f1e9f015cd2990edc.jpg?s=32&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0001-32.png", - "image_48": "https:\/\/secure.gravatar.com\/avatar\/822d051544b1f50f1e9f015cd2990edc.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2F0180%2Fimg%2Favatars%2Fava_0001-48.png", - "image_72": "https:\/\/secure.gravatar.com\/avatar\/822d051544b1f50f1e9f015cd2990edc.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F3654%2Fimg%2Favatars%2Fava_0001-72.png", - "image_192": "https:\/\/secure.gravatar.com\/avatar\/822d051544b1f50f1e9f015cd2990edc.jpg?s=192&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0001-192.png", - "image_512": "https:\/\/secure.gravatar.com\/avatar\/822d051544b1f50f1e9f015cd2990edc.jpg?s=512&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0001-512.png", - "fields": null - }, - "is_admin": true, - "is_owner": true, - "is_primary_owner": true, - "is_restricted": false, - "is_ultra_restricted": false, - "is_bot": false - }, - { - "id": "U24CG47QU", - "team_id": "T07Q4MHC7", - "name": "lindy.invite", - "deleted": false, - "status": null, - "color": "99a949", - "real_name": "lindy invite", - "tz": "Africa\/Cairo", - "tz_label": "Eastern European Time", - "tz_offset": 7200, - "profile": { - "first_name": "lindy", - "last_name": "invite", - "avatar_hash": "g1c46f51413d", - "real_name": "lindy invite", - "real_name_normalized": "lindy invite", - "email": "lindy+invite@example.com", - "image_24": "https:\/\/secure.gravatar.com\/avatar\/1c46f51413d2af8048dfaadcf43f5a03.jpg?s=24&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0015-24.png", - "image_32": "https:\/\/secure.gravatar.com\/avatar\/1c46f51413d2af8048dfaadcf43f5a03.jpg?s=32&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0015-32.png", - "image_48": "https:\/\/secure.gravatar.com\/avatar\/1c46f51413d2af8048dfaadcf43f5a03.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0015-48.png", - "image_72": "https:\/\/secure.gravatar.com\/avatar\/1c46f51413d2af8048dfaadcf43f5a03.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0015-72.png", - "image_192": "https:\/\/secure.gravatar.com\/avatar\/1c46f51413d2af8048dfaadcf43f5a03.jpg?s=192&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0015-192.png", - "image_512": "https:\/\/secure.gravatar.com\/avatar\/1c46f51413d2af8048dfaadcf43f5a03.jpg?s=512&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0015-512.png", - "fields": null - }, - "is_admin": false, - "is_owner": false, - "is_primary_owner": false, - "is_restricted": false, - "is_ultra_restricted": false, - "is_bot": false - }, - { - "id": "U15FHKEKX", - "team_id": "T07Q4MHC7", - "name": "peter", - "deleted": false, - "status": null, - "color": "e0a729", - "real_name": "Peter", - "tz": "Africa\/Cairo", - "tz_label": "Eastern European Time", - "tz_offset": 7200, - "profile": { - "first_name": "Peter", - "last_name": "", - "avatar_hash": "g5f5c8dacc18", - "real_name": "Peter", - "real_name_normalized": "Peter", - "email": "lindy+14@example.com", - "image_24": "https:\/\/secure.gravatar.com\/avatar\/5f5c8dacc18f616a0dcdec904fdd5cea.jpg?s=24&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0014-24.png", - "image_32": "https:\/\/secure.gravatar.com\/avatar\/5f5c8dacc18f616a0dcdec904fdd5cea.jpg?s=32&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0014-32.png", - "image_48": "https:\/\/secure.gravatar.com\/avatar\/5f5c8dacc18f616a0dcdec904fdd5cea.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0014-48.png", - "image_72": "https:\/\/secure.gravatar.com\/avatar\/5f5c8dacc18f616a0dcdec904fdd5cea.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0014-72.png", - "image_192": "https:\/\/secure.gravatar.com\/avatar\/5f5c8dacc18f616a0dcdec904fdd5cea.jpg?s=192&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0014-192.png", - "image_512": "https:\/\/secure.gravatar.com\/avatar\/5f5c8dacc18f616a0dcdec904fdd5cea.jpg?s=512&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0014-512.png", - "fields": null - }, - "is_admin": false, - "is_owner": false, - "is_primary_owner": false, - "is_restricted": false, - "is_ultra_restricted": false, - "is_bot": false - }, - { - "id": "U15F922BY", - "team_id": "T07Q4MHC7", - "name": "sam", - "deleted": false, - "status": null, - "color": "674b1b", - "real_name": "Sam", - "tz": "Africa\/Cairo", - "tz_label": "Eastern European Time", - "tz_offset": 7200, - "profile": { - "first_name": "Sam", - "last_name": "", - "avatar_hash": "gef41b9b8176", - "real_name": "Sam", - "real_name_normalized": "Sam", - "email": "lindy+11@example.com", - "image_24": "https:\/\/secure.gravatar.com\/avatar\/ef41b9b8176fba6eaffdfe04415df8be.jpg?s=24&d=https%3A%2F%2Fa.slack-edge.com%2F0180%2Fimg%2Favatars%2Fava_0017-24.png", - "image_32": "https:\/\/secure.gravatar.com\/avatar\/ef41b9b8176fba6eaffdfe04415df8be.jpg?s=32&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0017-32.png", - "image_48": "https:\/\/secure.gravatar.com\/avatar\/ef41b9b8176fba6eaffdfe04415df8be.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0017-48.png", - "image_72": "https:\/\/secure.gravatar.com\/avatar\/ef41b9b8176fba6eaffdfe04415df8be.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0017-72.png", - "image_192": "https:\/\/secure.gravatar.com\/avatar\/ef41b9b8176fba6eaffdfe04415df8be.jpg?s=192&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0017-192.png", - "image_512": "https:\/\/secure.gravatar.com\/avatar\/ef41b9b8176fba6eaffdfe04415df8be.jpg?s=512&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0017-512.png", - "fields": null - }, - "is_admin": false, - "is_owner": false, - "is_primary_owner": false, - "is_restricted": false, - "is_ultra_restricted": false, - "is_bot": false - }, - { - "id": "U13CSKE2E", - "team_id": "T07Q4MHC7", - "name": "sandy", - "deleted": false, - "status": null, - "color": "3c989f", - "real_name": "Sandy", - "tz": "Africa\/Cairo", - "tz_label": "Eastern European Time", - "tz_offset": 7200, - "profile": { - "first_name": "Sandy", - "last_name": "", - "avatar_hash": "g7bb3eebada3", - "real_name": "Sandy", - "real_name_normalized": "Sandy", - "email": "lindy+3@example.com", - "image_24": "https:\/\/secure.gravatar.com\/avatar\/7bb3eebada3d8ee1251980df86904ec9.jpg?s=24&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0013-24.png", - "image_32": "https:\/\/secure.gravatar.com\/avatar\/7bb3eebada3d8ee1251980df86904ec9.jpg?s=32&d=https%3A%2F%2Fa.slack-edge.com%2F0180%2Fimg%2Favatars%2Fava_0013-32.png", - "image_48": "https:\/\/secure.gravatar.com\/avatar\/7bb3eebada3d8ee1251980df86904ec9.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0013-48.png", - "image_72": "https:\/\/secure.gravatar.com\/avatar\/7bb3eebada3d8ee1251980df86904ec9.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0013-72.png", - "image_192": "https:\/\/secure.gravatar.com\/avatar\/7bb3eebada3d8ee1251980df86904ec9.jpg?s=192&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0013-192.png", - "image_512": "https:\/\/secure.gravatar.com\/avatar\/7bb3eebada3d8ee1251980df86904ec9.jpg?s=512&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0013-512.png", - "fields": null - }, - "is_admin": false, - "is_owner": false, - "is_primary_owner": false, - "is_restricted": false, - "is_ultra_restricted": false, - "is_bot": false - }, - { - "id": "U13C5JZ7W", - "team_id": "T07Q4MHC7", - "name": "ted", - "deleted": false, - "status": null, - "color": "e7392d", - "real_name": "Ted", - "tz": "Africa\/Cairo", - "tz_label": "Eastern European Time", - "tz_offset": 7200, - "profile": { - "first_name": "Ted", - "last_name": "", - "avatar_hash": "g5b31933276f", - "real_name": "Ted", - "real_name_normalized": "Ted", - "email": "lindy+2@example.com", - "image_24": "https:\/\/secure.gravatar.com\/avatar\/5b31933276f699aa8ff55a8fe4a2e7cb.jpg?s=24&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0022-24.png", - "image_32": "https:\/\/secure.gravatar.com\/avatar\/5b31933276f699aa8ff55a8fe4a2e7cb.jpg?s=32&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0022-32.png", - "image_48": "https:\/\/secure.gravatar.com\/avatar\/5b31933276f699aa8ff55a8fe4a2e7cb.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0022-48.png", - "image_72": "https:\/\/secure.gravatar.com\/avatar\/5b31933276f699aa8ff55a8fe4a2e7cb.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0022-72.png", - "image_192": "https:\/\/secure.gravatar.com\/avatar\/5b31933276f699aa8ff55a8fe4a2e7cb.jpg?s=192&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0022-192.png", - "image_512": "https:\/\/secure.gravatar.com\/avatar\/5b31933276f699aa8ff55a8fe4a2e7cb.jpg?s=512&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0022-512.png", - "fields": null - }, - "is_admin": true, - "is_owner": false, - "is_primary_owner": false, - "is_restricted": false, - "is_ultra_restricted": false, - "is_bot": false - } -] \ No newline at end of file diff --git a/webapp/channels/src/components/admin_console/permission_schemes_settings/strings/permissions.tsx b/webapp/channels/src/components/admin_console/permission_schemes_settings/strings/permissions.tsx index 0fea18e18927..cd3f9e807699 100644 --- a/webapp/channels/src/components/admin_console/permission_schemes_settings/strings/permissions.tsx +++ b/webapp/channels/src/components/admin_console/permission_schemes_settings/strings/permissions.tsx @@ -185,16 +185,6 @@ export const permissionRolesStrings: Record