From 91dfcbbdd17b66c31a5a7fa5fd6ed42ef3d04e3c Mon Sep 17 00:00:00 2001 From: Nick Misasi Date: Thu, 13 Nov 2025 06:12:30 -0500 Subject: [PATCH] Integration permission management changes (#34421) * Support for permissions allowing end users to create and manage their own integrations if sysadmin deems necessary * Adjustments based on new understanding * remove extra functions now that we've consolidated * Fix webapp i18n * Update snapshots * Fix test * Fix some tests, refactor some more, and add a few extra * fix linter * Update snapshots * Fix test * Missed some cleanup * Fix e2e * Fi * Fix * Fixes from PR feedback * Update snapshots * Fix tests * Fix slash command list endpoint per PR feedback. Remove changes around OAuth Apps * Further reversions of oauth stuff * Update tests * Small changes to fix when customOnly=false * Remove extra perm from cypress * Fixes from Eva's feedback * Fix i18n * More fixing * More fixing --- e2e-tests/cypress/tests/support/api/role.js | 4 +- server/channels/api4/command.go | 78 +- server/channels/api4/command_test.go | 716 +++++++++++++++++- .../api4/outgoing_oauth_connection.go | 6 +- .../api4/outgoing_oauth_connection_test.go | 24 +- server/channels/api4/webhook.go | 66 +- server/channels/api4/webhook_test.go | 301 ++++++-- server/channels/app/app_test.go | 7 +- server/channels/app/authorization_test.go | 4 +- server/channels/app/command.go | 23 + server/channels/app/permissions_migrations.go | 141 ++-- server/channels/testlib/store.go | 1 + .../utils/policies-roles-mapping.json | 40 + server/i18n/en.json | 4 + server/public/model/migration.go | 1 + server/public/model/permission.go | 43 +- server/public/model/role.go | 7 +- .../permissions_tree.test.tsx.snap | 150 +++- .../permissions_tree/permissions_tree.tsx | 40 +- .../strings/groups.tsx | 40 + .../strings/permissions.tsx | 82 +- .../components/backstage_sidebar.tsx | 6 +- .../src/components/backstage/index.ts | 10 +- .../product_menu/product_menu_list/index.ts | 10 +- .../abstract_incoming_hook.test.tsx.snap | 265 +++++++ .../abstract_incoming_hook.test.tsx | 7 + .../abstract_incoming_webhook.tsx | 49 +- .../add_incoming_webhook.tsx | 7 + .../add_incoming_webhook/index.ts | 4 + .../edit_incoming_webhook.tsx | 6 + .../edit_incoming_webhook/index.ts | 4 + .../components/integrations/integrations.tsx | 6 +- webapp/channels/src/i18n/en.json | 36 +- .../src/constants/permissions.ts | 5 +- webapp/channels/src/utils/constants.tsx | 16 +- 35 files changed, 1937 insertions(+), 272 deletions(-) diff --git a/e2e-tests/cypress/tests/support/api/role.js b/e2e-tests/cypress/tests/support/api/role.js index 61ccbfed028..bc6dfff5d0d 100644 --- a/e2e-tests/cypress/tests/support/api/role.js +++ b/e2e-tests/cypress/tests/support/api/role.js @@ -17,7 +17,7 @@ export const defaultRolesPermissions = { playbook_member: 'playbook_public_view playbook_public_manage_members playbook_public_manage_properties playbook_private_view playbook_private_manage_members playbook_private_manage_properties run_create', run_admin: 'run_manage_properties run_manage_members', run_member: 'run_view', - system_admin: 'sysconsole_write_environment_elasticsearch playbook_public_manage_properties sysconsole_write_authentication_ldap run_view manage_jobs manage_roles playbook_public_create manage_public_channel_properties sysconsole_read_plugins delete_post purge_elasticsearch_indexes sysconsole_read_integrations_bot_accounts read_data_retention_job manage_private_channel_members create_elasticsearch_post_indexing_job manage_elasticsearch_post_indexing_job sysconsole_read_authentication_guest_access create_elasticsearch_post_aggregation_job manage_elasticsearch_post_aggregation_job join_public_teams sysconsole_read_site_public_links add_saml_idp_cert sysconsole_write_site_announcement_banner sysconsole_write_site_notices sysconsole_read_experimental_feature_flags sysconsole_read_site_users_and_teams manage_slash_commands sysconsole_read_authentication_ldap read_channel read_channel_content sysconsole_write_authentication_password list_users_without_team sysconsole_read_authentication_email add_saml_public_cert playbook_private_create promote_guest sysconsole_read_user_management_system_roles manage_public_channel_members create_data_retention_job manage_data_retention_job add_saml_private_cert sysconsole_write_user_management_users sysconsole_read_compliance_compliance_monitoring playbook_public_manage_members sysconsole_write_environment_database sysconsole_write_user_management_teams playbook_private_manage_roles read_public_channel sysconsole_write_plugins sysconsole_read_authentication_openid sysconsole_write_user_management_groups sysconsole_write_site_file_sharing_and_downloads playbook_private_manage_properties sysconsole_read_site_customization join_public_channels add_user_to_team restore_custom_group download_compliance_export_result sysconsole_write_user_management_system_roles sysconsole_write_environment_session_lengths create_custom_group manage_private_channel_properties create_post_public remove_ldap_private_cert sysconsole_write_site_public_links import_team sysconsole_read_environment_developer sysconsole_read_environment_database sysconsole_read_environment_web_server sysconsole_read_environment_mobile_security sysconsole_write_environment_mobile_security use_channel_mentions view_team remove_others_reactions sysconsole_read_environment_session_lengths sysconsole_write_integrations_bot_accounts playbook_public_view use_group_mentions sysconsole_write_environment_web_server add_ldap_private_cert read_public_channel_groups invite_guest sysconsole_read_environment_smtp create_post sysconsole_read_about_edition_and_license sysconsole_read_authentication_signup sysconsole_read_authentication_saml sysconsole_read_environment_file_storage sysconsole_write_experimental_feature_flags sysconsole_write_site_localization sysconsole_write_environment_rate_limiting sysconsole_read_environment_rate_limiting sysconsole_read_products_boards get_saml_cert_status sysconsole_read_environment_high_availability manage_secure_connections read_compliance_export_job sysconsole_write_compliance_custom_terms_of_service read_user_access_token edit_post sysconsole_write_environment_logging sysconsole_read_environment_push_notification_server sysconsole_write_site_customization read_other_users_teams read_elasticsearch_post_aggregation_job sysconsole_write_compliance_data_retention_policy sysconsole_read_user_management_permissions sysconsole_read_site_emoji sysconsole_read_compliance_data_retention_policy read_license_information sysconsole_read_experimental_features read_deleted_posts sysconsole_read_environment_logging sysconsole_read_reporting_site_statistics test_elasticsearch sysconsole_read_site_posts add_reaction sysconsole_write_authentication_signup manage_outgoing_webhooks create_post_ephemeral sysconsole_read_environment_image_proxy invite_user manage_others_outgoing_webhooks create_user_access_token sysconsole_write_environment_image_proxy sysconsole_write_products_boards read_elasticsearch_post_indexing_job sysconsole_write_environment_performance_monitoring sysconsole_write_authentication_guest_access sysconsole_read_compliance_custom_terms_of_service edit_others_posts sysconsole_write_billing get_saml_metadata_from_idp sysconsole_write_authentication_saml invalidate_caches view_members manage_others_bots run_create join_private_teams convert_private_channel_to_public read_audits assign_bot read_jobs remove_user_from_team revoke_user_access_token manage_team sysconsole_read_reporting_server_logs get_public_link manage_others_slash_commands manage_system delete_public_channel read_private_channel_groups sysconsole_read_authentication_mfa delete_emojis list_private_teams create_emojis sysconsole_read_billing sysconsole_write_site_emoji invalidate_email_invite sysconsole_write_environment_file_storage sysconsole_write_compliance_compliance_monitoring remove_saml_public_cert sysconsole_read_compliance_compliance_export sysconsole_read_site_localization manage_team_roles list_public_teams get_logs sysconsole_write_integrations_integration_management sysconsole_read_integrations_cors manage_oauth manage_outgoing_oauth_connections delete_others_emojis sysconsole_write_integrations_gif manage_incoming_webhooks sysconsole_write_authentication_email create_private_channel playbook_private_make_public manage_bots add_ldap_public_cert remove_ldap_public_cert sysconsole_write_site_notifications sysconsole_write_environment_developer playbook_private_manage_members sysconsole_read_user_management_teams edit_custom_group remove_reaction playbook_public_manage_roles sysconsole_write_reporting_server_logs read_others_bots sysconsole_write_site_posts sysconsole_read_site_notifications sysconsole_read_authentication_password playbook_private_view manage_system_wide_oauth get_analytics list_team_channels sysconsole_write_user_management_channels delete_private_channel manage_custom_group_members test_s3 create_ldap_sync_job manage_ldap_sync_job sysconsole_read_integrations_integration_management test_site_url recycle_database_connections sysconsole_read_site_announcement_banner test_email manage_shared_channels read_bots sysconsole_write_environment_smtp sysconsole_write_environment_push_notification_server sysconsole_write_user_management_permissions sysconsole_read_environment_elasticsearch sysconsole_write_reporting_site_statistics sysconsole_write_site_users_and_teams demote_to_guest create_team test_ldap remove_saml_idp_cert delete_others_posts edit_other_users sysconsole_write_reporting_team_statistics sysconsole_read_integrations_gif sysconsole_read_site_notices sysconsole_write_about_edition_and_license manage_others_incoming_webhooks run_manage_members create_bot sysconsole_write_authentication_mfa sysconsole_read_user_management_users assign_system_admin_role sysconsole_write_experimental_features edit_brand create_group_channel sysconsole_write_authentication_openid create_direct_channel manage_license_information reload_config manage_channel_roles sysconsole_read_user_management_groups create_compliance_export_job manage_compliance_export_job read_ldap_sync_job upload_file sysconsole_read_site_file_sharing_and_downloads delete_custom_group sysconsole_read_user_management_channels sysconsole_write_compliance_compliance_export remove_saml_private_cert sysconsole_read_environment_performance_monitoring create_public_channel sysconsole_write_integrations_cors sysconsole_write_environment_high_availability playbook_public_make_private run_manage_properties sysconsole_read_reporting_team_statistics convert_public_channel_to_private add_bookmark_public_channel edit_bookmark_public_channel delete_bookmark_public_channel order_bookmark_public_channel add_bookmark_private_channel edit_bookmark_private_channel delete_bookmark_private_channel order_bookmark_private_channel', + system_admin: 'sysconsole_write_environment_elasticsearch playbook_public_manage_properties sysconsole_write_authentication_ldap run_view manage_jobs manage_roles playbook_public_create manage_public_channel_properties sysconsole_read_plugins delete_post purge_elasticsearch_indexes sysconsole_read_integrations_bot_accounts read_data_retention_job manage_private_channel_members create_elasticsearch_post_indexing_job manage_elasticsearch_post_indexing_job sysconsole_read_authentication_guest_access create_elasticsearch_post_aggregation_job manage_elasticsearch_post_aggregation_job join_public_teams sysconsole_read_site_public_links add_saml_idp_cert sysconsole_write_site_announcement_banner sysconsole_write_site_notices sysconsole_read_experimental_feature_flags sysconsole_read_site_users_and_teams manage_own_slash_commands manage_others_slash_commands sysconsole_read_authentication_ldap read_channel read_channel_content sysconsole_write_authentication_password list_users_without_team sysconsole_read_authentication_email add_saml_public_cert playbook_private_create promote_guest sysconsole_read_user_management_system_roles manage_public_channel_members create_data_retention_job manage_data_retention_job add_saml_private_cert sysconsole_write_user_management_users sysconsole_read_compliance_compliance_monitoring playbook_public_manage_members sysconsole_write_environment_database sysconsole_write_user_management_teams playbook_private_manage_roles read_public_channel sysconsole_write_plugins sysconsole_read_authentication_openid sysconsole_write_user_management_groups sysconsole_write_site_file_sharing_and_downloads playbook_private_manage_properties sysconsole_read_site_customization join_public_channels add_user_to_team restore_custom_group download_compliance_export_result sysconsole_write_user_management_system_roles sysconsole_write_environment_session_lengths create_custom_group manage_private_channel_properties create_post_public remove_ldap_private_cert sysconsole_write_site_public_links import_team sysconsole_read_environment_developer sysconsole_read_environment_database sysconsole_read_environment_web_server sysconsole_read_environment_mobile_security sysconsole_write_environment_mobile_security use_channel_mentions view_team remove_others_reactions sysconsole_read_environment_session_lengths sysconsole_write_integrations_bot_accounts playbook_public_view use_group_mentions sysconsole_write_environment_web_server add_ldap_private_cert read_public_channel_groups invite_guest sysconsole_read_environment_smtp create_post sysconsole_read_about_edition_and_license sysconsole_read_authentication_signup sysconsole_read_authentication_saml sysconsole_read_environment_file_storage sysconsole_write_experimental_feature_flags sysconsole_write_site_localization sysconsole_write_environment_rate_limiting sysconsole_read_environment_rate_limiting sysconsole_read_products_boards get_saml_cert_status sysconsole_read_environment_high_availability manage_secure_connections read_compliance_export_job sysconsole_write_compliance_custom_terms_of_service read_user_access_token edit_post sysconsole_write_environment_logging sysconsole_read_environment_push_notification_server sysconsole_write_site_customization read_other_users_teams read_elasticsearch_post_aggregation_job sysconsole_write_compliance_data_retention_policy sysconsole_read_user_management_permissions sysconsole_read_site_emoji sysconsole_read_compliance_data_retention_policy read_license_information sysconsole_read_experimental_features read_deleted_posts sysconsole_read_environment_logging sysconsole_read_reporting_site_statistics test_elasticsearch sysconsole_read_site_posts add_reaction sysconsole_write_authentication_signup manage_own_outgoing_webhooks manage_others_outgoing_webhooks create_post_ephemeral sysconsole_read_environment_image_proxy invite_user create_user_access_token sysconsole_write_environment_image_proxy sysconsole_write_products_boards read_elasticsearch_post_indexing_job sysconsole_write_environment_performance_monitoring sysconsole_write_authentication_guest_access sysconsole_read_compliance_custom_terms_of_service edit_others_posts sysconsole_write_billing get_saml_metadata_from_idp sysconsole_write_authentication_saml invalidate_caches view_members manage_others_bots run_create join_private_teams convert_private_channel_to_public read_audits assign_bot read_jobs remove_user_from_team revoke_user_access_token manage_team sysconsole_read_reporting_server_logs get_public_link manage_system delete_public_channel read_private_channel_groups sysconsole_read_authentication_mfa delete_emojis list_private_teams create_emojis sysconsole_read_billing sysconsole_write_site_emoji invalidate_email_invite sysconsole_write_environment_file_storage sysconsole_write_compliance_compliance_monitoring remove_saml_public_cert sysconsole_read_compliance_compliance_export sysconsole_read_site_localization manage_team_roles list_public_teams get_logs sysconsole_write_integrations_integration_management sysconsole_read_integrations_cors manage_oauth manage_outgoing_oauth_connections delete_others_emojis sysconsole_write_integrations_gif manage_own_incoming_webhooks manage_others_incoming_webhooks bypass_incoming_webhook_channel_lock sysconsole_write_authentication_email create_private_channel playbook_private_make_public manage_bots add_ldap_public_cert remove_ldap_public_cert sysconsole_write_site_notifications sysconsole_write_environment_developer playbook_private_manage_members sysconsole_read_user_management_teams edit_custom_group remove_reaction playbook_public_manage_roles sysconsole_write_reporting_server_logs read_others_bots sysconsole_write_site_posts sysconsole_read_site_notifications sysconsole_read_authentication_password playbook_private_view manage_system_wide_oauth get_analytics list_team_channels sysconsole_write_user_management_channels delete_private_channel manage_custom_group_members test_s3 create_ldap_sync_job manage_ldap_sync_job sysconsole_read_integrations_integration_management test_site_url recycle_database_connections sysconsole_read_site_announcement_banner test_email manage_shared_channels read_bots sysconsole_write_environment_smtp sysconsole_write_environment_push_notification_server sysconsole_write_user_management_permissions sysconsole_read_environment_elasticsearch sysconsole_write_reporting_site_statistics sysconsole_write_site_users_and_teams demote_to_guest create_team test_ldap remove_saml_idp_cert delete_others_posts edit_other_users sysconsole_write_reporting_team_statistics sysconsole_read_integrations_gif sysconsole_read_site_notices sysconsole_write_about_edition_and_license run_manage_members create_bot sysconsole_write_authentication_mfa sysconsole_read_user_management_users assign_system_admin_role sysconsole_write_experimental_features edit_brand create_group_channel sysconsole_write_authentication_openid create_direct_channel manage_license_information reload_config manage_channel_roles sysconsole_read_user_management_groups create_compliance_export_job manage_compliance_export_job read_ldap_sync_job upload_file sysconsole_read_site_file_sharing_and_downloads delete_custom_group sysconsole_read_user_management_channels sysconsole_write_compliance_compliance_export remove_saml_private_cert sysconsole_read_environment_performance_monitoring create_public_channel sysconsole_write_integrations_cors sysconsole_write_environment_high_availability playbook_public_make_private run_manage_properties sysconsole_read_reporting_team_statistics convert_public_channel_to_private add_bookmark_public_channel edit_bookmark_public_channel delete_bookmark_public_channel order_bookmark_public_channel add_bookmark_private_channel edit_bookmark_private_channel delete_bookmark_private_channel order_bookmark_private_channel', system_custom_group_admin: 'create_custom_group edit_custom_group delete_custom_group restore_custom_group manage_custom_group_members', system_guest: 'create_group_channel create_direct_channel', system_manager: 'sysconsole_read_site_announcement_banner manage_private_channel_properties edit_brand read_private_channel_groups manage_private_channel_members manage_team_roles sysconsole_write_environment_session_lengths sysconsole_read_site_emoji sysconsole_write_environment_developer sysconsole_read_user_management_groups sysconsole_write_user_management_groups sysconsole_write_environment_rate_limiting delete_private_channel sysconsole_read_environment_performance_monitoring sysconsole_read_environment_rate_limiting sysconsole_write_user_management_teams sysconsole_write_integrations_integration_management sysconsole_write_site_public_links sysconsole_read_authentication_ldap sysconsole_write_integrations_cors reload_config sysconsole_write_user_management_channels sysconsole_read_environment_high_availability sysconsole_read_site_users_and_teams sysconsole_read_user_management_teams sysconsole_write_site_users_and_teams sysconsole_read_site_customization sysconsole_write_environment_high_availability sysconsole_read_integrations_bot_accounts sysconsole_read_authentication_guest_access sysconsole_read_site_public_links read_elasticsearch_post_indexing_job sysconsole_read_user_management_channels sysconsole_read_reporting_team_statistics invalidate_caches sysconsole_read_authentication_signup read_elasticsearch_post_aggregation_job sysconsole_write_environment_smtp manage_public_channel_members list_public_teams add_user_to_team sysconsole_read_environment_web_server sysconsole_read_site_localization get_logs sysconsole_write_site_posts sysconsole_write_integrations_bot_accounts sysconsole_write_user_management_permissions sysconsole_read_environment_elasticsearch sysconsole_read_environment_smtp list_private_teams read_public_channel_groups sysconsole_write_environment_file_storage sysconsole_write_integrations_gif manage_public_channel_properties sysconsole_write_environment_performance_monitoring sysconsole_write_site_notifications sysconsole_read_site_notifications sysconsole_read_environment_image_proxy sysconsole_write_site_announcement_banner sysconsole_write_site_emoji test_site_url sysconsole_read_integrations_gif sysconsole_write_environment_logging convert_public_channel_to_private get_analytics sysconsole_read_user_management_permissions sysconsole_write_environment_image_proxy test_elasticsearch recycle_database_connections sysconsole_write_site_localization sysconsole_read_reporting_server_logs create_elasticsearch_post_indexing_job manage_elasticsearch_post_indexing_job sysconsole_read_reporting_site_statistics test_ldap delete_public_channel sysconsole_write_environment_push_notification_server read_license_information sysconsole_write_products_boards sysconsole_read_about_edition_and_license convert_private_channel_to_public sysconsole_read_integrations_integration_management create_elasticsearch_post_aggregation_job manage_elasticsearch_post_aggregation_job purge_elasticsearch_indexes sysconsole_read_environment_database join_public_teams sysconsole_read_authentication_email sysconsole_read_environment_push_notification_server view_team read_channel sysconsole_read_authentication_password read_ldap_sync_job sysconsole_read_integrations_cors sysconsole_read_environment_logging manage_team sysconsole_read_authentication_openid read_public_channel sysconsole_write_environment_elasticsearch sysconsole_read_plugins manage_channel_roles remove_user_from_team test_email sysconsole_write_site_file_sharing_and_downloads test_s3 sysconsole_read_site_file_sharing_and_downloads sysconsole_read_site_notices sysconsole_read_environment_file_storage join_private_teams sysconsole_read_products_boards sysconsole_read_environment_session_lengths sysconsole_write_environment_database sysconsole_read_authentication_saml sysconsole_read_authentication_mfa sysconsole_write_site_notices sysconsole_write_environment_web_server sysconsole_read_site_posts sysconsole_read_environment_developer sysconsole_write_site_customization sysconsole_read_environment_mobile_security sysconsole_write_environment_mobile_security manage_outgoing_oauth_connections', @@ -27,7 +27,7 @@ export const defaultRolesPermissions = { system_user: 'delete_custom_group create_emojis edit_custom_group create_direct_channel view_members join_public_teams restore_custom_group create_custom_group manage_custom_group_members delete_emojis list_public_teams create_team create_group_channel', system_user_access_token: 'create_user_access_token read_user_access_token revoke_user_access_token', system_user_manager: 'sysconsole_read_authentication_password sysconsole_read_authentication_openid sysconsole_write_user_management_groups list_private_teams sysconsole_read_user_management_groups sysconsole_read_authentication_email manage_public_channel_properties delete_private_channel sysconsole_read_authentication_signup read_private_channel_groups sysconsole_read_user_management_teams test_ldap read_channel view_team manage_team sysconsole_write_user_management_teams manage_channel_roles sysconsole_read_authentication_saml sysconsole_read_authentication_guest_access convert_private_channel_to_public sysconsole_read_user_management_permissions join_public_teams sysconsole_write_user_management_channels read_public_channel_groups sysconsole_read_user_management_channels list_public_teams manage_team_roles join_private_teams manage_public_channel_members convert_public_channel_to_private remove_user_from_team sysconsole_read_authentication_ldap manage_private_channel_properties delete_public_channel manage_private_channel_members read_public_channel add_user_to_team sysconsole_read_authentication_mfa read_ldap_sync_job', - team_admin: 'manage_others_slash_commands manage_channel_roles manage_others_outgoing_webhooks manage_team_roles use_channel_mentions manage_incoming_webhooks manage_slash_commands manage_public_channel_members convert_private_channel_to_public manage_private_channel_members manage_team convert_public_channel_to_private use_group_mentions delete_post read_public_channel_groups delete_others_posts playbook_private_manage_roles add_reaction remove_reaction remove_user_from_team read_private_channel_groups manage_outgoing_webhooks create_post playbook_public_manage_roles import_team manage_others_incoming_webhooks add_bookmark_public_channel edit_bookmark_public_channel delete_bookmark_public_channel order_bookmark_public_channel add_bookmark_private_channel edit_bookmark_private_channel delete_bookmark_private_channel order_bookmark_private_channel', + team_admin: 'manage_own_slash_commands manage_others_slash_commands manage_channel_roles manage_own_outgoing_webhooks manage_others_outgoing_webhooks manage_team_roles use_channel_mentions manage_own_incoming_webhooks manage_others_incoming_webhooks bypass_incoming_webhook_channel_lock manage_public_channel_members convert_private_channel_to_public manage_private_channel_members manage_team convert_public_channel_to_private use_group_mentions delete_post read_public_channel_groups delete_others_posts playbook_private_manage_roles add_reaction remove_reaction remove_user_from_team read_private_channel_groups create_post playbook_public_manage_roles import_team add_bookmark_public_channel edit_bookmark_public_channel delete_bookmark_public_channel order_bookmark_public_channel add_bookmark_private_channel edit_bookmark_private_channel delete_bookmark_private_channel order_bookmark_private_channel', team_guest: 'view_team', team_post_all: 'create_post use_channel_mentions use_group_mentions', team_post_all_public: 'create_post_public use_channel_mentions use_group_mentions', diff --git a/server/channels/api4/command.go b/server/channels/api4/command.go index 736c0b92d62..81f18d2b666 100644 --- a/server/channels/api4/command.go +++ b/server/channels/api4/command.go @@ -40,12 +40,28 @@ func createCommand(c *Context, w http.ResponseWriter, r *http.Request) { defer c.LogAuditRec(auditRec) c.LogAudit("attempt") - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) { - c.SetPermissionError(model.PermissionManageSlashCommands) + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageOwnSlashCommands) { + c.SetPermissionError(model.PermissionManageOwnSlashCommands) return } - cmd.CreatorId = c.AppContext.Session().UserId + userId := c.AppContext.Session().UserId + if cmd.CreatorId != "" && cmd.CreatorId != userId { + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageOthersSlashCommands) { + c.LogAudit("fail - inappropriate permissions") + c.SetPermissionError(model.PermissionManageOthersSlashCommands) + return + } + + if _, err := c.App.GetUser(cmd.CreatorId); err != nil { + c.Err = err + return + } + + userId = cmd.CreatorId + } + + cmd.CreatorId = userId rcmd, err := c.App.CreateCommand(&cmd) if err != nil { @@ -94,7 +110,7 @@ func updateCommand(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), oldCmd.TeamId, model.PermissionManageSlashCommands) { + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), oldCmd.TeamId, model.PermissionManageOwnSlashCommands) { c.LogAudit("fail - inappropriate permissions") // here we return Not_found instead of a permissions error so we don't leak the existence of // a command to someone without permissions for the team it belongs to. @@ -148,9 +164,9 @@ func moveCommand(c *Context, w http.ResponseWriter, r *http.Request) { } model.AddEventParameterAuditableToAuditRec(auditRec, "team", newTeam) - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), newTeam.Id, model.PermissionManageSlashCommands) { + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), newTeam.Id, model.PermissionManageOwnSlashCommands) { c.LogAudit("fail - inappropriate permissions") - c.SetPermissionError(model.PermissionManageSlashCommands) + c.SetPermissionError(model.PermissionManageOwnSlashCommands) return } @@ -161,7 +177,7 @@ func moveCommand(c *Context, w http.ResponseWriter, r *http.Request) { } auditRec.AddEventPriorState(cmd) - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) { + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageOwnSlashCommands) { c.LogAudit("fail - inappropriate permissions") // here we return Not_found instead of a permissions error so we don't leak the existence of // a command to someone without permissions for the team it belongs to. @@ -169,6 +185,20 @@ func moveCommand(c *Context, w http.ResponseWriter, r *http.Request) { return } + if c.AppContext.Session().UserId != cmd.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageOthersSlashCommands) { + c.LogAudit("fail - inappropriate permissions") + c.SetPermissionError(model.PermissionManageOthersSlashCommands) + return + } + + // Verify that the command creator has permission to the new team + // This prevents moving a command to a team where its creator doesn't have access + if !c.App.HasPermissionToTeam(c.AppContext, cmd.CreatorId, newTeam.Id, model.PermissionManageOwnSlashCommands) { + c.LogAudit("fail - command creator does not have permission to new team") + c.Err = model.NewAppError("moveCommand", "api.command.move_command.creator_no_permission.app_error", nil, "creator_id="+cmd.CreatorId+" team_id="+newTeam.Id, http.StatusBadRequest) + return + } + if appErr = c.App.MoveCommand(newTeam, cmd); appErr != nil { c.Err = appErr return @@ -200,7 +230,7 @@ func deleteCommand(c *Context, w http.ResponseWriter, r *http.Request) { } auditRec.AddEventPriorState(cmd) - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) { + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageOwnSlashCommands) { c.LogAudit("fail - inappropriate permissions") // here we return Not_found instead of a permissions error so we don't leak the existence of // a command to someone without permissions for the team it belongs to. @@ -244,25 +274,38 @@ func listCommands(c *Context, w http.ResponseWriter, r *http.Request) { var commands []*model.Command var err *model.AppError if customOnly { - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageSlashCommands) { - c.SetPermissionError(model.PermissionManageSlashCommands) + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageOwnSlashCommands) { + c.SetPermissionError(model.PermissionManageOwnSlashCommands) return } - commands, err = c.App.ListTeamCommands(teamId) + + // Filter to only commands the user can manage + userIdFilter := c.AppContext.Session().UserId + if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageOthersSlashCommands) { + userIdFilter = "" // Empty means return all commands + } + + commands, err = c.App.ListTeamCommandsByUser(teamId, userIdFilter) if err != nil { c.Err = err return } } else { //User with no permission should see only system commands - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageSlashCommands) { + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageOwnSlashCommands) { commands, err = c.App.ListAutocompleteCommands(teamId, c.AppContext.T) if err != nil { c.Err = err return } } else { - commands, err = c.App.ListAllCommands(teamId, c.AppContext.T) + // Filter custom commands to only those the user can manage + userIdFilter := c.AppContext.Session().UserId + if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageOthersSlashCommands) { + userIdFilter = "" // Empty means return all commands + } + + commands, err = c.App.ListAllCommandsByUser(teamId, userIdFilter, c.AppContext.T) if err != nil { c.Err = err return @@ -296,11 +339,16 @@ func getCommand(c *Context, w http.ResponseWriter, r *http.Request) { c.SetCommandNotFoundError() return } - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) { + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageOwnSlashCommands) { // again, return not_found to ensure id existence does not leak. c.SetCommandNotFoundError() return } + + if c.AppContext.Session().UserId != cmd.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageOthersSlashCommands) { + c.SetCommandNotFoundError() + return + } if err := json.NewEncoder(w).Encode(cmd); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) } @@ -473,7 +521,7 @@ func regenCommandToken(c *Context, w http.ResponseWriter, r *http.Request) { auditRec.AddEventPriorState(cmd) model.AddEventParameterToAuditRec(auditRec, "command_id", c.Params.CommandId) - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) { + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageOwnSlashCommands) { c.LogAudit("fail - inappropriate permissions") // here we return Not_found instead of a permissions error so we don't leak the existence of // a command to someone without permissions for the team it belongs to. diff --git a/server/channels/api4/command_test.go b/server/channels/api4/command_test.go index 32722eb909f..7f332954d91 100644 --- a/server/channels/api4/command_test.go +++ b/server/channels/api4/command_test.go @@ -32,11 +32,10 @@ func TestCreateCommand(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true }) newCmd := &model.Command{ - CreatorId: th.BasicUser.Id, - TeamId: th.BasicTeam.Id, - URL: "http://nowhere.com", - Method: model.CommandMethodPost, - Trigger: "trigger", + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger", } _, resp, err := client.CreateCommand(context.Background(), newCmd) @@ -55,6 +54,7 @@ func TestCreateCommand(t *testing.T) { CheckErrorID(t, err, "api.command.duplicate_trigger.app_error") newCmd.Trigger = "Local" + newCmd.CreatorId = th.BasicUser.Id localCreatedCmd, resp, err := LocalClient.CreateCommand(context.Background(), newCmd) require.NoError(t, err) CheckCreatedStatus(t, resp) @@ -82,6 +82,87 @@ func TestCreateCommand(t *testing.T) { CheckErrorID(t, err, "api.command.disabled.app_error") } +func TestCreateCommandForOtherUser(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t).InitBasic(t) + + enableCommands := *th.App.Config().ServiceSettings.EnableCommands + defer func() { + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableCommands = &enableCommands }) + }() + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true }) + + // Give BasicUser permission to manage their own commands + th.AddPermissionToRole(t, model.PermissionManageOwnSlashCommands.Id, model.TeamUserRoleId) + defer th.RemovePermissionFromRole(t, model.PermissionManageOwnSlashCommands.Id, model.TeamUserRoleId) + + t.Run("UserWithOnlyManageOwnCannotCreateForOthers", func(t *testing.T) { + cmdForOther := &model.Command{ + CreatorId: th.BasicUser2.Id, + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_for_other_fail", + } + + _, resp, err := th.Client.CreateCommand(context.Background(), cmdForOther) + require.Error(t, err) + CheckForbiddenStatus(t, resp) + }) + + t.Run("UserWithManageOthersCanCreateForOthers", func(t *testing.T) { + // Give BasicUser permission to manage others' commands + th.AddPermissionToRole(t, model.PermissionManageOthersSlashCommands.Id, model.TeamUserRoleId) + defer th.RemovePermissionFromRole(t, model.PermissionManageOthersSlashCommands.Id, model.TeamUserRoleId) + + cmdForOther := &model.Command{ + CreatorId: th.BasicUser2.Id, + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_for_other_success", + } + + createdCmd, _, err := th.Client.CreateCommand(context.Background(), cmdForOther) + require.NoError(t, err) + require.Equal(t, th.BasicUser2.Id, createdCmd.CreatorId, "command should be owned by BasicUser2") + require.Equal(t, th.BasicTeam.Id, createdCmd.TeamId) + }) + + t.Run("UserWithManageOthersCannotCreateForNonExistentUser", func(t *testing.T) { + // Give BasicUser permission to manage others' commands + th.AddPermissionToRole(t, model.PermissionManageOthersSlashCommands.Id, model.TeamUserRoleId) + defer th.RemovePermissionFromRole(t, model.PermissionManageOthersSlashCommands.Id, model.TeamUserRoleId) + + cmdForInvalidUser := &model.Command{ + CreatorId: model.NewId(), // Non-existent user ID + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_invalid_user", + } + + _, resp, err := th.Client.CreateCommand(context.Background(), cmdForInvalidUser) + require.Error(t, err) + CheckNotFoundStatus(t, resp) + }) + + t.Run("SystemAdminCanCreateForOthers", func(t *testing.T) { + cmdForOther := &model.Command{ + CreatorId: th.BasicUser.Id, + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_admin_for_other", + } + + createdCmd, _, err := th.SystemAdminClient.CreateCommand(context.Background(), cmdForOther) + require.NoError(t, err) + require.Equal(t, th.BasicUser.Id, createdCmd.CreatorId, "command should be owned by BasicUser") + require.Equal(t, th.BasicTeam.Id, createdCmd.TeamId) + }) +} + func TestUpdateCommand(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic(t) @@ -160,6 +241,90 @@ func TestUpdateCommand(t *testing.T) { _, resp, err := th.SystemAdminClient.UpdateCommand(context.Background(), cmd2) require.Error(t, err) CheckUnauthorizedStatus(t, resp) + + // Permission tests + th.LoginBasic(t) + + // Give BasicUser permission to manage their own commands + th.AddPermissionToRole(t, model.PermissionManageOwnSlashCommands.Id, model.TeamUserRoleId) + defer th.RemovePermissionFromRole(t, model.PermissionManageOwnSlashCommands.Id, model.TeamUserRoleId) + + t.Run("UserCanUpdateTheirOwnCommand", func(t *testing.T) { + // Create a command owned by BasicUser + cmd := &model.Command{ + CreatorId: th.BasicUser.Id, + TeamId: team.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_own", + } + createdCmd, _ := th.App.CreateCommand(cmd) + + // Update the command + createdCmd.URL = "http://newurl.com" + updatedCmd, _, err := th.Client.UpdateCommand(context.Background(), createdCmd) + require.NoError(t, err) + require.Equal(t, "http://newurl.com", updatedCmd.URL) + }) + + t.Run("UserWithoutManageOthersCannotUpdateOthersCommand", func(t *testing.T) { + // Create a command owned by BasicUser2 + cmd := &model.Command{ + CreatorId: th.BasicUser2.Id, + TeamId: team.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_other", + } + createdCmd, _ := th.App.CreateCommand(cmd) + + // Try to update the command + createdCmd.URL = "http://newurl.com" + _, resp, err := th.Client.UpdateCommand(context.Background(), createdCmd) + require.Error(t, err) + CheckForbiddenStatus(t, resp) + }) + + t.Run("UserWithManageOthersCanUpdateOthersCommand", func(t *testing.T) { + // Give BasicUser permission to manage others' commands + th.AddPermissionToRole(t, model.PermissionManageOthersSlashCommands.Id, model.TeamUserRoleId) + defer th.RemovePermissionFromRole(t, model.PermissionManageOthersSlashCommands.Id, model.TeamUserRoleId) + + // Create a command owned by BasicUser2 + cmd := &model.Command{ + CreatorId: th.BasicUser2.Id, + TeamId: team.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_other2", + } + createdCmd, _ := th.App.CreateCommand(cmd) + + // Update the command + createdCmd.URL = "http://newurl.com" + updatedCmd, _, err := th.Client.UpdateCommand(context.Background(), createdCmd) + require.NoError(t, err) + require.Equal(t, "http://newurl.com", updatedCmd.URL) + }) + + t.Run("UserWithOnlyManageOwnCannotUpdateOthersCommand", func(t *testing.T) { + // BasicUser should only have ManageOwn permission (already set up in the test) + // Create a command owned by BasicUser2 + cmd := &model.Command{ + CreatorId: th.BasicUser2.Id, + TeamId: team.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_other3", + } + createdCmd, _ := th.App.CreateCommand(cmd) + + // Try to update the command + createdCmd.URL = "http://newurl.com" + _, resp, err := th.Client.UpdateCommand(context.Background(), createdCmd) + require.Error(t, err) + CheckForbiddenStatus(t, resp) + }) } func TestMoveCommand(t *testing.T) { @@ -219,6 +384,133 @@ func TestMoveCommand(t *testing.T) { resp, err = th.SystemAdminClient.MoveCommand(context.Background(), newTeam.Id, rcmd2.Id) require.Error(t, err) CheckUnauthorizedStatus(t, resp) + + // Set up for permission tests + th.LoginBasic(t) + th.LinkUserToTeam(t, th.BasicUser, newTeam) + th.LinkUserToTeam(t, th.BasicUser2, newTeam) + + // Give BasicUser permission to manage their own commands on both teams + th.AddPermissionToRole(t, model.PermissionManageOwnSlashCommands.Id, model.TeamUserRoleId) + defer th.RemovePermissionFromRole(t, model.PermissionManageOwnSlashCommands.Id, model.TeamUserRoleId) + + t.Run("UserWithoutManageOthersPermissionCannotMoveOthersCommand", func(t *testing.T) { + // Create a command owned by BasicUser2 + cmd := &model.Command{ + CreatorId: th.BasicUser2.Id, + TeamId: team.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger3", + } + rcmd, _ := th.App.CreateCommand(cmd) + + // BasicUser should not be able to move BasicUser2's command + resp, err := th.Client.MoveCommand(context.Background(), newTeam.Id, rcmd.Id) + require.Error(t, err) + CheckForbiddenStatus(t, resp) + + // Verify the command was not moved + movedCmd, _ := th.App.GetCommand(rcmd.Id) + require.Equal(t, team.Id, movedCmd.TeamId) + }) + + t.Run("UserWithManageOthersPermissionCanMoveOthersCommand", func(t *testing.T) { + // Create a command owned by BasicUser2 + cmd := &model.Command{ + CreatorId: th.BasicUser2.Id, + TeamId: team.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger4", + } + rcmd, _ := th.App.CreateCommand(cmd) + + // Give BasicUser the permission to manage others' commands + th.AddPermissionToRole(t, model.PermissionManageOthersSlashCommands.Id, model.TeamUserRoleId) + defer th.RemovePermissionFromRole(t, model.PermissionManageOthersSlashCommands.Id, model.TeamUserRoleId) + + // Now BasicUser should be able to move BasicUser2's command + _, err := th.Client.MoveCommand(context.Background(), newTeam.Id, rcmd.Id) + require.NoError(t, err) + + // Verify the command was moved + movedCmd, _ := th.App.GetCommand(rcmd.Id) + require.Equal(t, newTeam.Id, movedCmd.TeamId) + }) + + t.Run("CreatorCanMoveTheirOwnCommand", func(t *testing.T) { + // Create a command owned by BasicUser + cmd := &model.Command{ + CreatorId: th.BasicUser.Id, + TeamId: team.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger5", + } + rcmd, _ := th.App.CreateCommand(cmd) + + // BasicUser should be able to move their own command + _, err := th.Client.MoveCommand(context.Background(), newTeam.Id, rcmd.Id) + require.NoError(t, err) + + // Verify the command was moved + movedCmd, _ := th.App.GetCommand(rcmd.Id) + require.Equal(t, newTeam.Id, movedCmd.TeamId) + }) + + t.Run("UserWithOnlyManageOwnCannotMoveOthersCommand", func(t *testing.T) { + // BasicUser should only have ManageOwn permission (already set up in the test) + // Create a command owned by BasicUser2 + cmd := &model.Command{ + CreatorId: th.BasicUser2.Id, + TeamId: team.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger6", + } + rcmd, _ := th.App.CreateCommand(cmd) + + // BasicUser should not be able to move BasicUser2's command + resp, err := th.Client.MoveCommand(context.Background(), newTeam.Id, rcmd.Id) + require.Error(t, err) + CheckForbiddenStatus(t, resp) + + // Verify the command was not moved + notMovedCmd, _ := th.App.GetCommand(rcmd.Id) + require.Equal(t, team.Id, notMovedCmd.TeamId) + }) + + t.Run("CannotMoveCommandWhenCreatorHasNoPermissionToNewTeam", func(t *testing.T) { + // Create a third team that the command creator (BasicUser2) is NOT a member of + thirdTeam := th.CreateTeam(t) + th.LinkUserToTeam(t, th.BasicUser, thirdTeam) + + // Give BasicUser permission to manage others' commands + th.AddPermissionToRole(t, model.PermissionManageOthersSlashCommands.Id, model.TeamUserRoleId) + defer th.RemovePermissionFromRole(t, model.PermissionManageOthersSlashCommands.Id, model.TeamUserRoleId) + + // Create a command owned by BasicUser2 + // Note: BasicUser2 is NOT a member of thirdTeam (only member of team and newTeam) + cmd := &model.Command{ + CreatorId: th.BasicUser2.Id, + TeamId: team.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger7", + } + rcmd, _ := th.App.CreateCommand(cmd) + + // BasicUser attempts to move BasicUser2's command to thirdTeam + // This should fail because BasicUser2 doesn't have permission to thirdTeam + resp, err := th.Client.MoveCommand(context.Background(), thirdTeam.Id, rcmd.Id) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + + // Verify the command was not moved + notMovedCmd, _ := th.App.GetCommand(rcmd.Id) + require.Equal(t, team.Id, notMovedCmd.TeamId) + }) } func TestDeleteCommand(t *testing.T) { @@ -278,6 +570,94 @@ func TestDeleteCommand(t *testing.T) { resp, err = th.SystemAdminClient.DeleteCommand(context.Background(), rcmd2.Id) require.Error(t, err) CheckUnauthorizedStatus(t, resp) + + // Permission tests for ManageOwn vs ManageOthers + th.LoginBasic(t) + + // Give BasicUser permission to manage their own commands + th.AddPermissionToRole(t, model.PermissionManageOwnSlashCommands.Id, model.TeamUserRoleId) + defer th.RemovePermissionFromRole(t, model.PermissionManageOwnSlashCommands.Id, model.TeamUserRoleId) + + t.Run("UserWithManageOwnCanDeleteOnlyOwnCommand", func(t *testing.T) { + // Create a command owned by BasicUser + cmdOwn := &model.Command{ + CreatorId: th.BasicUser.Id, + TeamId: team.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_own_delete", + } + createdCmdOwn, _ := th.App.CreateCommand(cmdOwn) + + // Create a command owned by BasicUser2 + cmdOther := &model.Command{ + CreatorId: th.BasicUser2.Id, + TeamId: team.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_other_delete", + } + createdCmdOther, _ := th.App.CreateCommand(cmdOther) + + // Should be able to delete own command + _, err := th.Client.DeleteCommand(context.Background(), createdCmdOwn.Id) + require.NoError(t, err) + + // Verify the command was deleted + deletedCmd, _ := th.App.GetCommand(createdCmdOwn.Id) + require.Nil(t, deletedCmd) + + // Should not be able to delete other user's command + resp, err := th.Client.DeleteCommand(context.Background(), createdCmdOther.Id) + require.Error(t, err) + CheckForbiddenStatus(t, resp) + + // Verify the command was not deleted + notDeletedCmd, _ := th.App.GetCommand(createdCmdOther.Id) + require.NotNil(t, notDeletedCmd) + }) + + t.Run("UserWithManageOthersCanDeleteAnyCommand", func(t *testing.T) { + // Give BasicUser permission to manage others' commands + th.AddPermissionToRole(t, model.PermissionManageOthersSlashCommands.Id, model.TeamUserRoleId) + defer th.RemovePermissionFromRole(t, model.PermissionManageOthersSlashCommands.Id, model.TeamUserRoleId) + + // Create a command owned by BasicUser + cmdOwn := &model.Command{ + CreatorId: th.BasicUser.Id, + TeamId: team.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_own_delete2", + } + createdCmdOwn, _ := th.App.CreateCommand(cmdOwn) + + // Create a command owned by BasicUser2 + cmdOther := &model.Command{ + CreatorId: th.BasicUser2.Id, + TeamId: team.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_other_delete2", + } + createdCmdOther, _ := th.App.CreateCommand(cmdOther) + + // Should be able to delete own command + _, err := th.Client.DeleteCommand(context.Background(), createdCmdOwn.Id) + require.NoError(t, err) + + // Verify the command was deleted + deletedCmd, _ := th.App.GetCommand(createdCmdOwn.Id) + require.Nil(t, deletedCmd) + + // Should be able to delete other user's command + _, err = th.Client.DeleteCommand(context.Background(), createdCmdOther.Id) + require.NoError(t, err) + + // Verify the command was deleted + deletedCmd, _ = th.App.GetCommand(createdCmdOther.Id) + require.Nil(t, deletedCmd) + }) } func TestListCommands(t *testing.T) { @@ -292,11 +672,10 @@ func TestListCommands(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true }) newCmd := &model.Command{ - CreatorId: th.BasicUser.Id, - TeamId: th.BasicTeam.Id, - URL: "http://nowhere.com", - Method: model.CommandMethodPost, - Trigger: "custom_command", + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "custom_command", } _, _, rootErr := th.SystemAdminClient.CreateCommand(context.Background(), newCmd) require.NoError(t, rootErr) @@ -376,6 +755,134 @@ func TestListCommands(t *testing.T) { require.Error(t, err) CheckUnauthorizedStatus(t, resp) }) + + // Permission tests for ManageOwn vs ManageOthers + th.LoginBasic(t) + + // Give BasicUser permission to manage their own commands + th.AddPermissionToRole(t, model.PermissionManageOwnSlashCommands.Id, model.TeamUserRoleId) + defer th.RemovePermissionFromRole(t, model.PermissionManageOwnSlashCommands.Id, model.TeamUserRoleId) + + t.Run("UserWithManageOwnCanListOnlyOwnCustomCommands", func(t *testing.T) { + // Create a command owned by BasicUser + cmdOwn := &model.Command{ + CreatorId: th.BasicUser.Id, + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_own_list", + } + createdCmdOwn, _ := th.App.CreateCommand(cmdOwn) + + // Create a command owned by BasicUser2 + cmdOther := &model.Command{ + CreatorId: th.BasicUser2.Id, + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_other_list", + } + createdCmdOther, _ := th.App.CreateCommand(cmdOther) + + // List custom commands only + listCommands, _, err := th.Client.ListCommands(context.Background(), th.BasicTeam.Id, true) + require.NoError(t, err) + + foundOwn := false + foundOther := false + for _, command := range listCommands { + if command.Id == createdCmdOwn.Id { + foundOwn = true + } + if command.Id == createdCmdOther.Id { + foundOther = true + } + } + require.True(t, foundOwn, "Should list own command") + require.False(t, foundOther, "Should not list other user's command") + + // List all commands (system + custom) + listCommandsAll, _, err := th.Client.ListCommands(context.Background(), th.BasicTeam.Id, false) + require.NoError(t, err) + + foundOwn = false + foundOther = false + foundSystem := false + for _, command := range listCommandsAll { + if command.Id == createdCmdOwn.Id { + foundOwn = true + } + if command.Id == createdCmdOther.Id { + foundOther = true + } + if command.Trigger == "echo" { + foundSystem = true + } + } + require.True(t, foundOwn, "Should list own command") + require.False(t, foundOther, "Should not list other user's command") + require.True(t, foundSystem, "Should list system commands") + }) + + t.Run("UserWithManageOthersCanListAllCustomCommands", func(t *testing.T) { + // Give BasicUser permission to manage others' commands + th.AddPermissionToRole(t, model.PermissionManageOthersSlashCommands.Id, model.TeamUserRoleId) + defer th.RemovePermissionFromRole(t, model.PermissionManageOthersSlashCommands.Id, model.TeamUserRoleId) + + // Create a command owned by BasicUser + cmdOwn := &model.Command{ + CreatorId: th.BasicUser.Id, + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_own_list2", + } + createdCmdOwn, _ := th.App.CreateCommand(cmdOwn) + + // Create a command owned by BasicUser2 + cmdOther := &model.Command{ + CreatorId: th.BasicUser2.Id, + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_other_list2", + } + createdCmdOther, _ := th.App.CreateCommand(cmdOther) + + // List custom commands only + listCommands, _, err := th.Client.ListCommands(context.Background(), th.BasicTeam.Id, true) + require.NoError(t, err) + + foundOwn := false + foundOther := false + for _, command := range listCommands { + if command.Id == createdCmdOwn.Id { + foundOwn = true + } + if command.Id == createdCmdOther.Id { + foundOther = true + } + } + require.True(t, foundOwn, "Should list own command") + require.True(t, foundOther, "Should list other user's command") + + // List all commands (system + custom) + listCommandsAll, _, err := th.Client.ListCommands(context.Background(), th.BasicTeam.Id, false) + require.NoError(t, err) + + foundOwn = false + foundOther = false + for _, command := range listCommandsAll { + if command.Id == createdCmdOwn.Id { + foundOwn = true + } + if command.Id == createdCmdOther.Id { + foundOther = true + } + } + require.True(t, foundOwn, "Should list own command") + require.True(t, foundOther, "Should list other user's command") + }) } func TestListAutocompleteCommands(t *testing.T) { @@ -384,11 +891,10 @@ func TestListAutocompleteCommands(t *testing.T) { client := th.Client newCmd := &model.Command{ - CreatorId: th.BasicUser.Id, - TeamId: th.BasicTeam.Id, - URL: "http://nowhere.com", - Method: model.CommandMethodPost, - Trigger: "custom_command", + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "custom_command", } _, _, err := th.SystemAdminClient.CreateCommand(context.Background(), newCmd) @@ -458,11 +964,10 @@ func TestListCommandAutocompleteSuggestions(t *testing.T) { client := th.Client newCmd := &model.Command{ - CreatorId: th.BasicUser.Id, - TeamId: th.BasicTeam.Id, - URL: "http://nowhere.com", - Method: model.CommandMethodPost, - Trigger: "custom_command", + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "custom_command", } _, _, err := th.SystemAdminClient.CreateCommand(context.Background(), newCmd) @@ -560,11 +1065,10 @@ func TestGetCommand(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true }) newCmd := &model.Command{ - CreatorId: th.BasicUser.Id, - TeamId: th.BasicTeam.Id, - URL: "http://nowhere.com", - Method: model.CommandMethodPost, - Trigger: "roger", + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "roger", } newCmd, _, rootErr := th.SystemAdminClient.CreateCommand(context.Background(), newCmd) require.NoError(t, rootErr) @@ -612,6 +1116,81 @@ func TestGetCommand(t *testing.T) { require.Error(t, err) CheckUnauthorizedStatus(t, resp) }) + + // Permission tests for ManageOwn vs ManageOthers + th.LoginBasic(t) + + // Give BasicUser permission to manage their own commands + th.AddPermissionToRole(t, model.PermissionManageOwnSlashCommands.Id, model.TeamUserRoleId) + defer th.RemovePermissionFromRole(t, model.PermissionManageOwnSlashCommands.Id, model.TeamUserRoleId) + + t.Run("UserWithManageOwnCanGetOnlyOwnCommand", func(t *testing.T) { + // Create a command owned by BasicUser + cmdOwn := &model.Command{ + CreatorId: th.BasicUser.Id, + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_own_get", + } + createdCmdOwn, _ := th.App.CreateCommand(cmdOwn) + + // Create a command owned by BasicUser2 + cmdOther := &model.Command{ + CreatorId: th.BasicUser2.Id, + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_other_get", + } + createdCmdOther, _ := th.App.CreateCommand(cmdOther) + + // Should be able to get own command + cmd, _, err := th.Client.GetCommandById(context.Background(), createdCmdOwn.Id) + require.NoError(t, err) + require.Equal(t, createdCmdOwn.Id, cmd.Id) + + // Should not be able to get other user's command + _, resp, err := th.Client.GetCommandById(context.Background(), createdCmdOther.Id) + require.Error(t, err) + CheckNotFoundStatus(t, resp) + }) + + t.Run("UserWithManageOthersCanGetAnyCommand", func(t *testing.T) { + // Give BasicUser permission to manage others' commands + th.AddPermissionToRole(t, model.PermissionManageOthersSlashCommands.Id, model.TeamUserRoleId) + defer th.RemovePermissionFromRole(t, model.PermissionManageOthersSlashCommands.Id, model.TeamUserRoleId) + + // Create a command owned by BasicUser + cmdOwn := &model.Command{ + CreatorId: th.BasicUser.Id, + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_own_get2", + } + createdCmdOwn, _ := th.App.CreateCommand(cmdOwn) + + // Create a command owned by BasicUser2 + cmdOther := &model.Command{ + CreatorId: th.BasicUser2.Id, + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_other_get2", + } + createdCmdOther, _ := th.App.CreateCommand(cmdOther) + + // Should be able to get own command + cmd, _, err := th.Client.GetCommandById(context.Background(), createdCmdOwn.Id) + require.NoError(t, err) + require.Equal(t, createdCmdOwn.Id, cmd.Id) + + // Should be able to get other user's command + cmd, _, err = th.Client.GetCommandById(context.Background(), createdCmdOther.Id) + require.NoError(t, err) + require.Equal(t, createdCmdOther.Id, cmd.Id) + }) } func TestRegenToken(t *testing.T) { @@ -626,11 +1205,10 @@ func TestRegenToken(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true }) newCmd := &model.Command{ - CreatorId: th.BasicUser.Id, - TeamId: th.BasicTeam.Id, - URL: "http://nowhere.com", - Method: model.CommandMethodPost, - Trigger: "trigger", + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger", } createdCmd, resp, err := th.SystemAdminClient.CreateCommand(context.Background(), newCmd) @@ -645,6 +1223,84 @@ func TestRegenToken(t *testing.T) { require.Error(t, err) CheckNotFoundStatus(t, resp) require.Empty(t, token, "should not return the token") + + // Permission tests for ManageOwn vs ManageOthers + th.LoginBasic(t) + + // Give BasicUser permission to manage their own commands + th.AddPermissionToRole(t, model.PermissionManageOwnSlashCommands.Id, model.TeamUserRoleId) + defer th.RemovePermissionFromRole(t, model.PermissionManageOwnSlashCommands.Id, model.TeamUserRoleId) + + t.Run("UserWithManageOwnCanRegenOnlyOwnCommandToken", func(t *testing.T) { + // Create a command owned by BasicUser + cmdOwn := &model.Command{ + CreatorId: th.BasicUser.Id, + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_own_regen", + } + createdCmdOwn, _ := th.App.CreateCommand(cmdOwn) + oldToken := createdCmdOwn.Token + + // Create a command owned by BasicUser2 + cmdOther := &model.Command{ + CreatorId: th.BasicUser2.Id, + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_other_regen", + } + createdCmdOther, _ := th.App.CreateCommand(cmdOther) + + // Should be able to regenerate own command token + newToken, _, err := th.Client.RegenCommandToken(context.Background(), createdCmdOwn.Id) + require.NoError(t, err) + require.NotEqual(t, oldToken, newToken) + + // Should not be able to regenerate other user's command token + _, resp, err := th.Client.RegenCommandToken(context.Background(), createdCmdOther.Id) + require.Error(t, err) + CheckForbiddenStatus(t, resp) + }) + + t.Run("UserWithManageOthersCanRegenAnyCommandToken", func(t *testing.T) { + // Give BasicUser permission to manage others' commands + th.AddPermissionToRole(t, model.PermissionManageOthersSlashCommands.Id, model.TeamUserRoleId) + defer th.RemovePermissionFromRole(t, model.PermissionManageOthersSlashCommands.Id, model.TeamUserRoleId) + + // Create a command owned by BasicUser + cmdOwn := &model.Command{ + CreatorId: th.BasicUser.Id, + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_own_regen2", + } + createdCmdOwn, _ := th.App.CreateCommand(cmdOwn) + oldTokenOwn := createdCmdOwn.Token + + // Create a command owned by BasicUser2 + cmdOther := &model.Command{ + CreatorId: th.BasicUser2.Id, + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.CommandMethodPost, + Trigger: "trigger_other_regen2", + } + createdCmdOther, _ := th.App.CreateCommand(cmdOther) + oldTokenOther := createdCmdOther.Token + + // Should be able to regenerate own command token + newToken, _, err := th.Client.RegenCommandToken(context.Background(), createdCmdOwn.Id) + require.NoError(t, err) + require.NotEqual(t, oldTokenOwn, newToken) + + // Should be able to regenerate other user's command token + newToken, _, err = th.Client.RegenCommandToken(context.Background(), createdCmdOther.Id) + require.NoError(t, err) + require.NotEqual(t, oldTokenOther, newToken) + }) } func TestExecuteInvalidCommand(t *testing.T) { diff --git a/server/channels/api4/outgoing_oauth_connection.go b/server/channels/api4/outgoing_oauth_connection.go index 46828e4db8b..70461639c0a 100644 --- a/server/channels/api4/outgoing_oauth_connection.go +++ b/server/channels/api4/outgoing_oauth_connection.go @@ -36,12 +36,12 @@ func (api *API) InitOutgoingOAuthConnection() { // other users can use them in their outgoing webhooks and slash commands if they have permissions to manage those. func checkOutgoingOAuthConnectionReadPermissions(c *Context, teamId string) bool { if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOutgoingOAuthConnections) || - c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageOutgoingWebhooks) || - c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageSlashCommands) { + c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageOwnOutgoingWebhooks) || + c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageOwnSlashCommands) { return true } - c.SetPermissionError(model.PermissionManageOutgoingWebhooks, model.PermissionManageSlashCommands) + c.SetPermissionError(model.PermissionManageOwnOutgoingWebhooks, model.PermissionManageOwnSlashCommands) return false } diff --git a/server/channels/api4/outgoing_oauth_connection_test.go b/server/channels/api4/outgoing_oauth_connection_test.go index 316e874d900..a5ef5289a04 100644 --- a/server/channels/api4/outgoing_oauth_connection_test.go +++ b/server/channels/api4/outgoing_oauth_connection_test.go @@ -98,7 +98,7 @@ func TestCheckOutgoingOAuthConnectionReadPermissions(t *testing.T) { c.App = th.App c.Logger = th.App.Srv().Log() - th.AddPermissionToRole(t, model.PermissionManageSlashCommands.Id, model.TeamAdminRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnSlashCommands.Id, model.TeamAdminRoleId) canRead := checkOutgoingOAuthConnectionReadPermissions(c, th.BasicTeam.Id) require.True(t, canRead) @@ -117,7 +117,7 @@ func TestCheckOutgoingOAuthConnectionReadPermissions(t *testing.T) { c.App = th.App c.Logger = th.App.Srv().Log() - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamAdminRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamAdminRoleId) canRead := checkOutgoingOAuthConnectionReadPermissions(c, th.BasicTeam.Id) require.True(t, canRead) @@ -177,8 +177,8 @@ func TestClientOutgoingOAuthConnectionGet(t *testing.T) { defer func() { th.RestoreDefaultRolePermissions(t, defaultRolePermissions) }() - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamAdminRoleId) - th.AddPermissionToRole(t, model.PermissionManageSlashCommands.Id, model.TeamAdminRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamAdminRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnSlashCommands.Id, model.TeamAdminRoleId) outgoingOauthIface := &mocks.OutgoingOAuthConnectionInterface{} outgoingOauthImpl := th.App.Srv().OutgoingOAuthConnection @@ -207,8 +207,8 @@ func TestClientOutgoingOAuthConnectionGet(t *testing.T) { defer func() { th.RestoreDefaultRolePermissions(t, defaultRolePermissions) }() - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamAdminRoleId) - th.AddPermissionToRole(t, model.PermissionManageSlashCommands.Id, model.TeamAdminRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamAdminRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnSlashCommands.Id, model.TeamAdminRoleId) outgoingOauthIface := &mocks.OutgoingOAuthConnectionInterface{} outgoingOauthImpl := th.App.Srv().OutgoingOAuthConnection @@ -318,8 +318,8 @@ func TestClientListOutgoingOAuthConnection(t *testing.T) { defer func() { th.RestoreDefaultRolePermissions(t, defaultRolePermissions) }() - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.SystemUserRoleId) - th.AddPermissionToRole(t, model.PermissionManageSlashCommands.Id, model.SystemUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.SystemUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnSlashCommands.Id, model.SystemUserRoleId) outgoingOauthIface := &mocks.OutgoingOAuthConnectionInterface{} th.App.Srv().OutgoingOAuthConnection = outgoingOauthIface @@ -356,8 +356,8 @@ func TestClientListOutgoingOAuthConnection(t *testing.T) { defer func() { th.RestoreDefaultRolePermissions(t, defaultRolePermissions) }() - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.SystemUserRoleId) - th.AddPermissionToRole(t, model.PermissionManageSlashCommands.Id, model.SystemUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.SystemUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnSlashCommands.Id, model.SystemUserRoleId) conn := newOutgoingOAuthConnection() conn.Audiences = []string{"http://knowhere.com"} @@ -402,8 +402,8 @@ func TestClientListOutgoingOAuthConnection(t *testing.T) { defer func() { th.RestoreDefaultRolePermissions(t, defaultRolePermissions) }() - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.SystemUserRoleId) - th.AddPermissionToRole(t, model.PermissionManageSlashCommands.Id, model.SystemUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.SystemUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnSlashCommands.Id, model.SystemUserRoleId) conn := newOutgoingOAuthConnection() conn.CreatorId = model.NewId() diff --git a/server/channels/api4/webhook.go b/server/channels/api4/webhook.go index a5c7a9e0953..3b420efe454 100644 --- a/server/channels/api4/webhook.go +++ b/server/channels/api4/webhook.go @@ -45,8 +45,8 @@ func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { model.AddEventParameterAuditableToAuditRec(auditRec, "channel", channel) c.LogAudit("attempt") - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionManageIncomingWebhooks) { - c.SetPermissionError(model.PermissionManageIncomingWebhooks) + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionManageOwnIncomingWebhooks) { + c.SetPermissionError(model.PermissionManageOwnIncomingWebhooks) return } @@ -72,6 +72,11 @@ func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { userId = hook.UserId } + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionBypassIncomingWebhookChannelLock) { + hook.ChannelLocked = true + hook.ChannelId = channel.Id + } + incomingHook, err := c.App.CreateIncomingWebhookForChannel(userId, channel, &hook) if err != nil { c.Err = err @@ -143,8 +148,8 @@ func updateIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionManageIncomingWebhooks) { - c.SetPermissionError(model.PermissionManageIncomingWebhooks) + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionManageOwnIncomingWebhooks) { + c.SetPermissionError(model.PermissionManageOwnIncomingWebhooks) return } @@ -160,6 +165,11 @@ func updateIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { return } + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionBypassIncomingWebhookChannelLock) { + updatedHook.ChannelLocked = true + updatedHook.ChannelId = channel.Id + } + incomingHook, err := c.App.UpdateIncomingWebhook(oldHook, &updatedHook) if err != nil { c.Err = err @@ -188,8 +198,8 @@ func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) { ) if teamID != "" { - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageIncomingWebhooks) { - c.SetPermissionError(model.PermissionManageIncomingWebhooks) + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageOwnIncomingWebhooks) { + c.SetPermissionError(model.PermissionManageOwnIncomingWebhooks) return } @@ -200,8 +210,8 @@ func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) { hooks, appErr = c.App.GetIncomingWebhooksForTeamPageByUser(teamID, userID, c.Params.Page, c.Params.PerPage) } else { - if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageIncomingWebhooks) { - c.SetPermissionError(model.PermissionManageIncomingWebhooks) + if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOwnIncomingWebhooks) { + c.SetPermissionError(model.PermissionManageOwnIncomingWebhooks) return } @@ -275,10 +285,10 @@ func getIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageIncomingWebhooks) || + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOwnIncomingWebhooks) || (channel.Type != model.ChannelTypeOpen && !c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)) { c.LogAudit("fail - bad permissions") - c.SetPermissionError(model.PermissionManageIncomingWebhooks) + c.SetPermissionError(model.PermissionManageOwnIncomingWebhooks) return } @@ -329,10 +339,10 @@ func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { auditRec.AddMeta("channel_name", channel.Name) auditRec.AddMeta("team_id", hook.TeamId) - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageIncomingWebhooks) || + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOwnIncomingWebhooks) || (channel.Type != model.ChannelTypeOpen && !c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)) { c.LogAudit("fail - bad permissions") - c.SetPermissionError(model.PermissionManageIncomingWebhooks) + c.SetPermissionError(model.PermissionManageOwnIncomingWebhooks) return } @@ -391,8 +401,8 @@ func updateOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), updatedHook.TeamId, model.PermissionManageOutgoingWebhooks) { - c.SetPermissionError(model.PermissionManageOutgoingWebhooks) + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), updatedHook.TeamId, model.PermissionManageOwnOutgoingWebhooks) { + c.SetPermissionError(model.PermissionManageOwnOutgoingWebhooks) return } @@ -430,8 +440,8 @@ func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { defer c.LogAuditRec(auditRec) c.LogAudit("attempt") - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOutgoingWebhooks) { - c.SetPermissionError(model.PermissionManageOutgoingWebhooks) + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOwnOutgoingWebhooks) { + c.SetPermissionError(model.PermissionManageOwnOutgoingWebhooks) return } @@ -481,8 +491,8 @@ func getOutgoingHooks(c *Context, w http.ResponseWriter, r *http.Request) { ) if channelID != "" { - if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelID, model.PermissionManageOutgoingWebhooks) { - c.SetPermissionError(model.PermissionManageOutgoingWebhooks) + if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelID, model.PermissionManageOwnOutgoingWebhooks) { + c.SetPermissionError(model.PermissionManageOwnOutgoingWebhooks) return } @@ -493,8 +503,8 @@ func getOutgoingHooks(c *Context, w http.ResponseWriter, r *http.Request) { hooks, appErr = c.App.GetOutgoingWebhooksForChannelPageByUser(channelID, userID, c.Params.Page, c.Params.PerPage) } else if teamID != "" { - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageOutgoingWebhooks) { - c.SetPermissionError(model.PermissionManageOutgoingWebhooks) + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageOwnOutgoingWebhooks) { + c.SetPermissionError(model.PermissionManageOwnOutgoingWebhooks) return } @@ -505,8 +515,8 @@ func getOutgoingHooks(c *Context, w http.ResponseWriter, r *http.Request) { hooks, appErr = c.App.GetOutgoingWebhooksForTeamPageByUser(teamID, userID, c.Params.Page, c.Params.PerPage) } else { - if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOutgoingWebhooks) { - c.SetPermissionError(model.PermissionManageOutgoingWebhooks) + if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOwnOutgoingWebhooks) { + c.SetPermissionError(model.PermissionManageOwnOutgoingWebhooks) return } @@ -555,8 +565,8 @@ func getOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { auditRec.AddMeta("team_id", hook.TeamId) c.LogAudit("attempt") - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOutgoingWebhooks) { - c.SetPermissionError(model.PermissionManageOutgoingWebhooks) + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOwnOutgoingWebhooks) { + c.SetPermissionError(model.PermissionManageOwnOutgoingWebhooks) return } @@ -594,8 +604,8 @@ func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request) auditRec.AddMeta("team_id", hook.TeamId) c.LogAudit("attempt") - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOutgoingWebhooks) { - c.SetPermissionError(model.PermissionManageOutgoingWebhooks) + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOwnOutgoingWebhooks) { + c.SetPermissionError(model.PermissionManageOwnOutgoingWebhooks) return } @@ -642,8 +652,8 @@ func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { auditRec.AddMeta("team_id", hook.TeamId) c.LogAudit("attempt") - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOutgoingWebhooks) { - c.SetPermissionError(model.PermissionManageOutgoingWebhooks) + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOwnOutgoingWebhooks) { + c.SetPermissionError(model.PermissionManageOwnOutgoingWebhooks) return } diff --git a/server/channels/api4/webhook_test.go b/server/channels/api4/webhook_test.go index d2fa7774a0a..c19e826aec9 100644 --- a/server/channels/api4/webhook_test.go +++ b/server/channels/api4/webhook_test.go @@ -13,6 +13,22 @@ import ( "github.com/mattermost/mattermost/server/public/model" ) +func addIncomingWebhookPermissions(t *testing.T, th *TestHelper, roleID string) { + th.AddPermissionToRole(t, model.PermissionManageOwnIncomingWebhooks.Id, roleID) + th.AddPermissionToRole(t, model.PermissionBypassIncomingWebhookChannelLock.Id, roleID) +} + +func addIncomingWebhookPermissionsWithOthers(t *testing.T, th *TestHelper, roleID string) { + addIncomingWebhookPermissions(t, th, roleID) + th.AddPermissionToRole(t, model.PermissionManageOthersIncomingWebhooks.Id, roleID) +} + +func removeIncomingWebhookPermissions(t *testing.T, th *TestHelper, roleID string) { + th.RemovePermissionFromRole(t, model.PermissionManageOwnIncomingWebhooks.Id, roleID) + th.RemovePermissionFromRole(t, model.PermissionManageOthersIncomingWebhooks.Id, roleID) + th.RemovePermissionFromRole(t, model.PermissionBypassIncomingWebhookChannelLock.Id, roleID) +} + func TestCreateIncomingWebhook(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic(t) @@ -28,8 +44,8 @@ func TestCreateIncomingWebhook(t *testing.T) { defer func() { th.RestoreDefaultRolePermissions(t, defaultRolePermissions) }() - th.AddPermissionToRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamAdminRoleId) - th.RemovePermissionFromRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamUserRoleId) + addIncomingWebhookPermissionsWithOthers(t, th, model.TeamAdminRoleId) + removeIncomingWebhookPermissions(t, th, model.TeamUserRoleId) hook := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id} @@ -55,11 +71,30 @@ func TestCreateIncomingWebhook(t *testing.T) { require.Error(t, err) CheckForbiddenStatus(t, resp) - th.AddPermissionToRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamUserRoleId) + addIncomingWebhookPermissionsWithOthers(t, th, model.TeamUserRoleId) _, _, err = client.CreateIncomingWebhook(context.Background(), hook) require.NoError(t, err) + t.Run("channel lock enforced without bypass", func(t *testing.T) { + removeIncomingWebhookPermissions(t, th, model.TeamUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnIncomingWebhooks.Id, model.TeamUserRoleId) + + unlockedHook := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id, ChannelLocked: false} + created, _, err2 := client.CreateIncomingWebhook(context.Background(), unlockedHook) + require.NoError(t, err2) + require.True(t, created.ChannelLocked) + + addIncomingWebhookPermissions(t, th, model.TeamUserRoleId) + }) + + t.Run("channel lock optional with bypass", func(t *testing.T) { + hookWithBypass := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id, ChannelLocked: false} + created, _, err2 := client.CreateIncomingWebhook(context.Background(), hookWithBypass) + require.NoError(t, err2) + require.False(t, created.ChannelLocked) + }) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnablePostUsernameOverride = false }) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnablePostIconOverride = false }) @@ -101,6 +136,19 @@ func TestCreateIncomingWebhook(t *testing.T) { CheckBadRequestStatus(t, response) }) + t.Run("Cannot create for another user with only manage own permission", func(t *testing.T) { + // Setup user with only "manage own" permission + removeIncomingWebhookPermissions(t, th, model.TeamUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnIncomingWebhooks.Id, model.TeamUserRoleId) + + testHook := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id, UserId: th.BasicUser2.Id} + _, response, err2 := client.CreateIncomingWebhook(context.Background(), testHook) + require.Error(t, err2) + CheckForbiddenStatus(t, response) + + addIncomingWebhookPermissions(t, th, model.TeamUserRoleId) + }) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = false }) _, resp, err = client.CreateIncomingWebhook(context.Background(), hook) require.Error(t, err) @@ -117,9 +165,9 @@ func TestCreateIncomingWebhook_BypassTeamPermissions(t *testing.T) { defaultRolePermissions := th.SaveDefaultRolePermissions(t) defer th.RestoreDefaultRolePermissions(t, defaultRolePermissions) - th.RemovePermissionFromRole(t, model.PermissionManageIncomingWebhooks.Id, model.SystemUserRoleId) - th.AddPermissionToRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamAdminRoleId) - th.AddPermissionToRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamUserRoleId) + removeIncomingWebhookPermissions(t, th, model.SystemUserRoleId) + addIncomingWebhookPermissionsWithOthers(t, th, model.TeamAdminRoleId) + addIncomingWebhookPermissions(t, th, model.TeamUserRoleId) hook := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id} @@ -155,8 +203,8 @@ func TestGetIncomingWebhooks(t *testing.T) { defer func() { th.RestoreDefaultRolePermissions(t, defaultRolePermissions) }() - th.AddPermissionToRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamAdminRoleId) - th.RemovePermissionFromRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamUserRoleId) + addIncomingWebhookPermissionsWithOthers(t, th, model.TeamAdminRoleId) + removeIncomingWebhookPermissions(t, th, model.TeamUserRoleId) hook := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id} rhook, _, err := th.SystemAdminClient.CreateIncomingWebhook(context.Background(), hook) @@ -202,7 +250,7 @@ func TestGetIncomingWebhooks(t *testing.T) { require.Error(t, err) CheckForbiddenStatus(t, resp) - th.AddPermissionToRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamUserRoleId) + addIncomingWebhookPermissions(t, th, model.TeamUserRoleId) _, _, err = client.GetIncomingWebhooksForTeam(context.Background(), th.BasicTeam.Id, 0, 1000, "") require.NoError(t, err) @@ -215,6 +263,24 @@ func TestGetIncomingWebhooks(t *testing.T) { require.Error(t, err) CheckForbiddenStatus(t, resp) + t.Run("User with only manage own cannot see others webhooks", func(t *testing.T) { + // Create webhook as admin + adminHook := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id} + adminCreatedHook, _, err2 := th.SystemAdminClient.CreateIncomingWebhook(context.Background(), adminHook) + require.NoError(t, err2) + + // Remove all permissions from team_user and give only "manage own" + removeIncomingWebhookPermissions(t, th, model.TeamUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnIncomingWebhooks.Id, model.TeamUserRoleId) + + // Should NOT be able to see admin's webhook + _, resp2, err2 := client.GetIncomingWebhook(context.Background(), adminCreatedHook.Id, "") + require.Error(t, err2) + CheckForbiddenStatus(t, resp2) + + addIncomingWebhookPermissions(t, th, model.TeamUserRoleId) + }) + _, err = client.Logout(context.Background()) require.NoError(t, err) _, resp, err = client.GetIncomingWebhooks(context.Background(), 0, 1000, "") @@ -234,8 +300,8 @@ func TestGetIncomingWebhooksListByUser(t *testing.T) { defer func() { th.RestoreDefaultRolePermissions(t, defaultRolePermissions) }() - th.AddPermissionToRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamAdminRoleId) - th.AddPermissionToRole(t, model.PermissionManageIncomingWebhooks.Id, model.SystemUserRoleId) + addIncomingWebhookPermissionsWithOthers(t, th, model.TeamAdminRoleId) + addIncomingWebhookPermissions(t, th, model.SystemUserRoleId) // Basic user webhook bHook := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id, TeamId: th.BasicTeam.Id, UserId: th.BasicUser.Id} @@ -276,8 +342,8 @@ func TestGetIncomingWebhooksByTeam(t *testing.T) { defer func() { th.RestoreDefaultRolePermissions(t, defaultRolePermissions) }() - th.AddPermissionToRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamAdminRoleId) - th.AddPermissionToRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamUserRoleId) + addIncomingWebhookPermissionsWithOthers(t, th, model.TeamAdminRoleId) + addIncomingWebhookPermissions(t, th, model.TeamUserRoleId) // Basic user webhook bHook := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id, TeamId: th.BasicTeam.Id, UserId: th.BasicUser.Id} @@ -319,8 +385,8 @@ func TestGetIncomingWebhooksWithCount(t *testing.T) { defer func() { th.RestoreDefaultRolePermissions(t, defaultRolePermissions) }() - th.AddPermissionToRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamAdminRoleId) - th.AddPermissionToRole(t, model.PermissionManageIncomingWebhooks.Id, model.SystemUserRoleId) + addIncomingWebhookPermissionsWithOthers(t, th, model.TeamAdminRoleId) + addIncomingWebhookPermissionsWithOthers(t, th, model.SystemUserRoleId) // Basic user webhook bHook := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id, TeamId: th.BasicTeam.Id, UserId: th.BasicUser.Id} @@ -446,6 +512,25 @@ func TestDeleteIncomingWebhook(t *testing.T) { require.Error(t, err) CheckForbiddenStatus(t, resp) }) + + t.Run("Cannot delete others webhook with only manage own permission", func(t *testing.T) { + // Create webhook as admin + adminHook := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id} + adminCreatedHook, _, err2 := th.SystemAdminClient.CreateIncomingWebhook(context.Background(), adminHook) + require.NoError(t, err2) + + // Give basic user only "manage own" permission + defaultPerms := th.SaveDefaultRolePermissions(t) + defer th.RestoreDefaultRolePermissions(t, defaultPerms) + th.RemovePermissionFromRole(t, model.PermissionManageOwnIncomingWebhooks.Id, model.TeamUserRoleId) + th.RemovePermissionFromRole(t, model.PermissionManageOthersIncomingWebhooks.Id, model.TeamUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnIncomingWebhooks.Id, model.TeamUserRoleId) + + th.LoginBasic(t) + resp, err2 := th.Client.DeleteIncomingWebhook(context.Background(), adminCreatedHook.Id) + require.Error(t, err2) + CheckForbiddenStatus(t, resp) + }) } func TestCreateOutgoingWebhook(t *testing.T) { @@ -459,8 +544,8 @@ func TestCreateOutgoingWebhook(t *testing.T) { defer func() { th.RestoreDefaultRolePermissions(t, defaultRolePermissions) }() - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamAdminRoleId) - th.RemovePermissionFromRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamAdminRoleId) + th.RemovePermissionFromRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamUserRoleId) hook := &model.OutgoingWebhook{ChannelId: th.BasicChannel.Id, TeamId: th.BasicChannel.TeamId, CallbackURLs: []string{"http://nowhere.com"}, Username: "some-user-name", IconURL: "http://some-icon-url/"} @@ -486,7 +571,7 @@ func TestCreateOutgoingWebhook(t *testing.T) { require.Error(t, err) CheckForbiddenStatus(t, resp) - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamUserRoleId) _, _, err = client.CreateOutgoingWebhook(context.Background(), hook) require.NoError(t, err) @@ -526,6 +611,25 @@ func TestCreateOutgoingWebhook(t *testing.T) { CheckBadRequestStatus(t, response) }) + t.Run("Cannot create for another user with only manage own permission", func(t *testing.T) { + // Remove all permissions and give only "manage own" + defaultPerms := th.SaveDefaultRolePermissions(t) + defer th.RestoreDefaultRolePermissions(t, defaultPerms) + th.RemovePermissionFromRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamUserRoleId) + + testHook := &model.OutgoingWebhook{ + ChannelId: th.BasicChannel.Id, + TeamId: th.BasicTeam.Id, + CallbackURLs: []string{"http://nowhere.com"}, + TriggerWords: []string{"test2"}, + CreatorId: th.BasicUser2.Id, + } + _, resp2, err2 := client.CreateOutgoingWebhook(context.Background(), testHook) + require.Error(t, err2) + CheckForbiddenStatus(t, resp2) + }) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOutgoingWebhooks = false }) _, resp, err = client.CreateOutgoingWebhook(context.Background(), hook) require.Error(t, err) @@ -541,8 +645,8 @@ func TestGetOutgoingWebhooks(t *testing.T) { defer func() { th.RestoreDefaultRolePermissions(t, defaultRolePermissions) }() - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamAdminRoleId) - th.RemovePermissionFromRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamAdminRoleId) + th.RemovePermissionFromRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamUserRoleId) hook := &model.OutgoingWebhook{ChannelId: th.BasicChannel.Id, TeamId: th.BasicChannel.TeamId, CallbackURLs: []string{"http://nowhere.com"}} rhook, _, err2 := th.SystemAdminClient.CreateOutgoingWebhook(context.Background(), hook) @@ -604,7 +708,7 @@ func TestGetOutgoingWebhooks(t *testing.T) { require.Error(t, err2) CheckForbiddenStatus(t, resp) - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamUserRoleId) _, _, err2 = th.Client.GetOutgoingWebhooksForTeam(context.Background(), th.BasicTeam.Id, 0, 1000, "") require.NoError(t, err2) @@ -624,6 +728,29 @@ func TestGetOutgoingWebhooks(t *testing.T) { require.Error(t, err2) CheckForbiddenStatus(t, resp) + t.Run("User with only manage own cannot see others outgoing webhooks", func(t *testing.T) { + // Create webhook as admin + adminHook := &model.OutgoingWebhook{ + ChannelId: th.BasicChannel.Id, + TeamId: th.BasicTeam.Id, + CallbackURLs: []string{"http://nowhere.com"}, + TriggerWords: []string{"admin"}, + } + adminCreatedHook, _, err3 := th.SystemAdminClient.CreateOutgoingWebhook(context.Background(), adminHook) + require.NoError(t, err3) + + // Remove all permissions and give only "manage own" + defaultPerms := th.SaveDefaultRolePermissions(t) + defer th.RestoreDefaultRolePermissions(t, defaultPerms) + th.RemovePermissionFromRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamUserRoleId) + + // Should NOT be able to get admin's webhook + _, resp2, err3 := th.Client.GetOutgoingWebhook(context.Background(), adminCreatedHook.Id) + require.Error(t, err3) + CheckForbiddenStatus(t, resp2) + }) + _, err := th.Client.Logout(context.Background()) require.NoError(t, err) _, resp, err2 = th.Client.GetOutgoingWebhooks(context.Background(), 0, 1000, "") @@ -641,8 +768,8 @@ func TestGetOutgoingWebhooksByTeam(t *testing.T) { defer func() { th.RestoreDefaultRolePermissions(t, defaultRolePermissions) }() - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamAdminRoleId) - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamAdminRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamUserRoleId) // Basic user webhook bHook := &model.OutgoingWebhook{ChannelId: th.BasicChannel.Id, TeamId: th.BasicChannel.TeamId, CallbackURLs: []string{"http://nowhere.com"}} @@ -682,8 +809,8 @@ func TestGetOutgoingWebhooksByChannel(t *testing.T) { defer func() { th.RestoreDefaultRolePermissions(t, defaultRolePermissions) }() - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamAdminRoleId) - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamAdminRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamUserRoleId) // Basic user webhook bHook := &model.OutgoingWebhook{ChannelId: th.BasicChannel.Id, TeamId: th.BasicChannel.TeamId, CallbackURLs: []string{"http://nowhere.com"}} @@ -724,8 +851,8 @@ func TestGetOutgoingWebhooksListByUser(t *testing.T) { defer func() { th.RestoreDefaultRolePermissions(t, defaultRolePermissions) }() - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamAdminRoleId) - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.SystemUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamAdminRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.SystemUserRoleId) // Basic user webhook bHook := &model.OutgoingWebhook{ChannelId: th.BasicChannel.Id, TeamId: th.BasicChannel.TeamId, CallbackURLs: []string{"http://nowhere.com"}} @@ -800,8 +927,8 @@ func TestUpdateIncomingHook(t *testing.T) { defer func() { th.RestoreDefaultRolePermissions(t, defaultRolePermissions) }() - th.AddPermissionToRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamAdminRoleId) - th.RemovePermissionFromRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamUserRoleId) + addIncomingWebhookPermissionsWithOthers(t, th, model.TeamAdminRoleId) + removeIncomingWebhookPermissions(t, th, model.TeamUserRoleId) hook1 := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id} @@ -908,11 +1035,33 @@ func TestUpdateIncomingHook(t *testing.T) { CheckForbiddenStatus(t, resp) }) - th.RemovePermissionFromRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamUserRoleId) - th.AddPermissionToRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamAdminRoleId) + removeIncomingWebhookPermissions(t, th, model.TeamUserRoleId) + addIncomingWebhookPermissionsWithOthers(t, th, model.TeamAdminRoleId) + + t.Run("update cannot clear channel lock without bypass", func(t *testing.T) { + removeIncomingWebhookPermissions(t, th, model.TeamUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnIncomingWebhooks.Id, model.TeamUserRoleId) + hookWithoutBypass := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id} + hookWithoutBypass, _, err := th.Client.CreateIncomingWebhook(context.Background(), hookWithoutBypass) + require.NoError(t, err) + require.True(t, hookWithoutBypass.ChannelLocked) + + hookWithoutBypass.ChannelLocked = false + removeIncomingWebhookPermissions(t, th, model.TeamUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnIncomingWebhooks.Id, model.TeamUserRoleId) + + updated, _, err := th.Client.UpdateIncomingWebhook(context.Background(), hookWithoutBypass) + require.NoError(t, err) + require.True(t, updated.ChannelLocked) + + removeIncomingWebhookPermissions(t, th, model.TeamUserRoleId) + addIncomingWebhookPermissions(t, th, model.TeamUserRoleId) + }) + + addIncomingWebhookPermissions(t, th, model.TeamUserRoleId) t.Run("OnlyAdminIntegrationsDisabled", func(t *testing.T) { - th.AddPermissionToRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamUserRoleId) + addIncomingWebhookPermissions(t, th, model.TeamUserRoleId) t.Run("UpdateHookOfSameUser", func(t *testing.T) { sameUserHook := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id} @@ -932,8 +1081,8 @@ func TestUpdateIncomingHook(t *testing.T) { }) }) - th.RemovePermissionFromRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamUserRoleId) - th.AddPermissionToRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamAdminRoleId) + removeIncomingWebhookPermissions(t, th, model.TeamUserRoleId) + addIncomingWebhookPermissionsWithOthers(t, th, model.TeamAdminRoleId) _, err := th.Client.Logout(context.Background()) require.NoError(t, err) @@ -990,6 +1139,22 @@ func TestUpdateIncomingHook(t *testing.T) { require.Error(t, err) CheckForbiddenStatus(t, resp) }) + + t.Run("Cannot update others webhook with only manage own permission", func(t *testing.T) { + // Create webhook as admin + adminHook2 := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id} + adminCreatedHook2, _, err2 := th.SystemAdminClient.CreateIncomingWebhook(context.Background(), adminHook2) + require.NoError(t, err2) + + // Remove all permissions and give only "manage own" + removeIncomingWebhookPermissions(t, th, model.TeamUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnIncomingWebhooks.Id, model.TeamUserRoleId) + + adminCreatedHook2.DisplayName = "Hacked" + _, resp, err2 := th.Client.UpdateIncomingWebhook(context.Background(), adminCreatedHook2) + require.Error(t, err2) + CheckForbiddenStatus(t, resp) + }) } func TestUpdateIncomingWebhook_BypassTeamPermissions(t *testing.T) { @@ -1002,9 +1167,9 @@ func TestUpdateIncomingWebhook_BypassTeamPermissions(t *testing.T) { defaultRolePermissions := th.SaveDefaultRolePermissions(t) defer th.RestoreDefaultRolePermissions(t, defaultRolePermissions) - th.RemovePermissionFromRole(t, model.PermissionManageIncomingWebhooks.Id, model.SystemUserRoleId) - th.AddPermissionToRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamAdminRoleId) - th.AddPermissionToRole(t, model.PermissionManageIncomingWebhooks.Id, model.TeamUserRoleId) + removeIncomingWebhookPermissions(t, th, model.SystemUserRoleId) + addIncomingWebhookPermissionsWithOthers(t, th, model.TeamAdminRoleId) + addIncomingWebhookPermissions(t, th, model.TeamUserRoleId) hook := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id} @@ -1071,8 +1236,8 @@ func TestUpdateOutgoingHook(t *testing.T) { defer func() { th.RestoreDefaultRolePermissions(t, defaultRolePermissions) }() - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamAdminRoleId) - th.RemovePermissionFromRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamAdminRoleId) + th.RemovePermissionFromRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamUserRoleId) createdHook := &model.OutgoingWebhook{ ChannelId: th.BasicChannel.Id, TeamId: th.BasicChannel.TeamId, @@ -1173,7 +1338,7 @@ func TestUpdateOutgoingHook(t *testing.T) { CheckForbiddenStatus(t, resp) }) - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamUserRoleId) hook2 := &model.OutgoingWebhook{ ChannelId: th.BasicChannel.Id, TeamId: th.BasicChannel.TeamId, CallbackURLs: []string{"http://nowhere.com"}, TriggerWords: []string{"rats2"}, @@ -1186,8 +1351,8 @@ func TestUpdateOutgoingHook(t *testing.T) { require.Error(t, err) CheckForbiddenStatus(t, resp) - th.RemovePermissionFromRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamUserRoleId) - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamAdminRoleId) + th.RemovePermissionFromRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamAdminRoleId) _, err = th.Client.Logout(context.Background()) require.NoError(t, err) @@ -1282,6 +1447,29 @@ func TestUpdateOutgoingHook(t *testing.T) { require.Error(t, err) CheckForbiddenStatus(t, resp) }) + + t.Run("Cannot update others webhook with only manage own permission", func(t *testing.T) { + // Create webhook as admin + adminHook := &model.OutgoingWebhook{ + ChannelId: th.BasicChannel.Id, + TeamId: th.BasicTeam.Id, + CallbackURLs: []string{"http://nowhere.com"}, + TriggerWords: []string{"admin2"}, + } + adminCreatedHook, _, err2 := th.SystemAdminClient.CreateOutgoingWebhook(context.Background(), adminHook) + require.NoError(t, err2) + + // Remove all permissions and give only "manage own" + defaultPerms := th.SaveDefaultRolePermissions(t) + defer th.RestoreDefaultRolePermissions(t, defaultPerms) + th.RemovePermissionFromRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamUserRoleId) + + adminCreatedHook.DisplayName = "Hacked" + _, resp, err2 := th.Client.UpdateOutgoingWebhook(context.Background(), adminCreatedHook) + require.Error(t, err2) + CheckForbiddenStatus(t, resp) + }) } func TestUpdateOutgoingWebhook_BypassTeamPermissions(t *testing.T) { @@ -1292,9 +1480,9 @@ func TestUpdateOutgoingWebhook_BypassTeamPermissions(t *testing.T) { defaultRolePermissions := th.SaveDefaultRolePermissions(t) defer th.RestoreDefaultRolePermissions(t, defaultRolePermissions) - th.RemovePermissionFromRole(t, model.PermissionManageOutgoingWebhooks.Id, model.SystemUserRoleId) - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamAdminRoleId) - th.AddPermissionToRole(t, model.PermissionManageOutgoingWebhooks.Id, model.TeamUserRoleId) + th.RemovePermissionFromRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.SystemUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamAdminRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamUserRoleId) hook := &model.OutgoingWebhook{ ChannelId: th.BasicChannel.Id, TeamId: th.BasicChannel.TeamId, @@ -1370,4 +1558,27 @@ func TestDeleteOutgoingHook(t *testing.T) { require.Error(t, err) CheckForbiddenStatus(t, resp) }) + + t.Run("Cannot delete others webhook with only manage own permission", func(t *testing.T) { + // Create webhook as admin + adminHook := &model.OutgoingWebhook{ + ChannelId: th.BasicChannel.Id, + TeamId: th.BasicChannel.TeamId, + CallbackURLs: []string{"http://nowhere.com"}, + TriggerWords: []string{"admin3"}, + } + adminCreatedHook, _, err2 := th.SystemAdminClient.CreateOutgoingWebhook(context.Background(), adminHook) + require.NoError(t, err2) + + // Give basic user only "manage own" permission + defaultPerms := th.SaveDefaultRolePermissions(t) + defer th.RestoreDefaultRolePermissions(t, defaultPerms) + th.RemovePermissionFromRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamUserRoleId) + th.AddPermissionToRole(t, model.PermissionManageOwnOutgoingWebhooks.Id, model.TeamUserRoleId) + + th.LoginBasic(t) + resp, err2 := th.Client.DeleteOutgoingWebhook(context.Background(), adminCreatedHook.Id) + require.Error(t, err2) + CheckForbiddenStatus(t, resp) + }) } diff --git a/server/channels/app/app_test.go b/server/channels/app/app_test.go index ae112648abd..6b8efe7b229 100644 --- a/server/channels/app/app_test.go +++ b/server/channels/app/app_test.go @@ -174,12 +174,13 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { model.PermissionImportTeam.Id, model.PermissionManageTeamRoles.Id, model.PermissionManageChannelRoles.Id, + model.PermissionManageOwnIncomingWebhooks.Id, model.PermissionManageOthersIncomingWebhooks.Id, + model.PermissionManageOwnOutgoingWebhooks.Id, model.PermissionManageOthersOutgoingWebhooks.Id, - model.PermissionManageSlashCommands.Id, + model.PermissionManageOwnSlashCommands.Id, model.PermissionManageOthersSlashCommands.Id, - model.PermissionManageIncomingWebhooks.Id, - model.PermissionManageOutgoingWebhooks.Id, + model.PermissionBypassIncomingWebhookChannelLock.Id, model.PermissionConvertPublicChannelToPrivate.Id, model.PermissionConvertPrivateChannelToPublic.Id, model.PermissionDeletePost.Id, diff --git a/server/channels/app/authorization_test.go b/server/channels/app/authorization_test.go index 5a831858505..c4109c10b6c 100644 --- a/server/channels/app/authorization_test.go +++ b/server/channels/app/authorization_test.go @@ -130,8 +130,8 @@ func TestCheckIfRolesGrantPermission(t *testing.T) { {[]string{model.ChannelUserRoleId}, model.PermissionManageSystem.Id, false}, {[]string{model.SystemAdminRoleId, model.ChannelUserRoleId}, model.PermissionManageSystem.Id, true}, {[]string{model.ChannelUserRoleId, model.SystemAdminRoleId}, model.PermissionManageSystem.Id, true}, - {[]string{model.TeamUserRoleId, model.TeamAdminRoleId}, model.PermissionManageSlashCommands.Id, true}, - {[]string{model.TeamAdminRoleId, model.TeamUserRoleId}, model.PermissionManageSlashCommands.Id, true}, + {[]string{model.TeamUserRoleId, model.TeamAdminRoleId}, model.PermissionManageOwnSlashCommands.Id, true}, + {[]string{model.TeamAdminRoleId, model.TeamUserRoleId}, model.PermissionManageOwnSlashCommands.Id, true}, {[]string{model.ChannelGuestRoleId}, model.PermissionReadChannelContent.Id, true}, } diff --git a/server/channels/app/command.go b/server/channels/app/command.go index 6c24530b6ce..ab0f4e025a1 100644 --- a/server/channels/app/command.go +++ b/server/channels/app/command.go @@ -128,6 +128,10 @@ func (a *App) ListAutocompleteCommands(teamID string, T i18n.TranslateFunc) ([]* } func (a *App) ListTeamCommands(teamID string) ([]*model.Command, *model.AppError) { + return a.ListTeamCommandsByUser(teamID, "") +} + +func (a *App) ListTeamCommandsByUser(teamID string, userID string) ([]*model.Command, *model.AppError) { if !*a.Config().ServiceSettings.EnableCommands { return nil, model.NewAppError("ListTeamCommands", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) } @@ -137,10 +141,25 @@ func (a *App) ListTeamCommands(teamID string) ([]*model.Command, *model.AppError return nil, model.NewAppError("ListTeamCommands", "app.command.listteamcommands.internal_error", nil, "", http.StatusInternalServerError).Wrap(err) } + // Filter by user if userID is specified + if userID != "" { + filteredCmds := make([]*model.Command, 0) + for _, cmd := range teamCmds { + if cmd.CreatorId == userID { + filteredCmds = append(filteredCmds, cmd) + } + } + return filteredCmds, nil + } + return teamCmds, nil } func (a *App) ListAllCommands(teamID string, T i18n.TranslateFunc) ([]*model.Command, *model.AppError) { + return a.ListAllCommandsByUser(teamID, "", T) +} + +func (a *App) ListAllCommandsByUser(teamID string, userID string, T i18n.TranslateFunc) ([]*model.Command, *model.AppError) { commands := make([]*model.Command, 0, 32) seen := make(map[string]bool) for _, value := range commandProviders { @@ -168,6 +187,10 @@ func (a *App) ListAllCommands(teamID string, T i18n.TranslateFunc) ([]*model.Com } for _, cmd := range teamCmds { if !seen[cmd.Trigger] { + // Filter by user if userID is specified (before sanitizing) + if userID != "" && cmd.CreatorId != userID { + continue + } cmd.Sanitize() seen[cmd.Trigger] = true commands = append(commands, cmd) diff --git a/server/channels/app/permissions_migrations.go b/server/channels/app/permissions_migrations.go index 623a38f66d3..965f6c0de04 100644 --- a/server/channels/app/permissions_migrations.go +++ b/server/channels/app/permissions_migrations.go @@ -21,58 +21,64 @@ type permissionTransformation struct { type permissionsMap []permissionTransformation const ( - PermissionManageSystem = "manage_system" - PermissionManageTeam = "manage_team" - PermissionManageEmojis = "manage_emojis" - PermissionManageOthersEmojis = "manage_others_emojis" - PermissionCreateEmojis = "create_emojis" - PermissionDeleteEmojis = "delete_emojis" - PermissionDeleteOthersEmojis = "delete_others_emojis" - PermissionManageWebhooks = "manage_webhooks" - PermissionManageOthersWebhooks = "manage_others_webhooks" - PermissionManageIncomingWebhooks = "manage_incoming_webhooks" - PermissionManageOthersIncomingWebhooks = "manage_others_incoming_webhooks" - PermissionManageOutgoingWebhooks = "manage_outgoing_webhooks" - PermissionManageOthersOutgoingWebhooks = "manage_others_outgoing_webhooks" - PermissionListPublicTeams = "list_public_teams" - PermissionListPrivateTeams = "list_private_teams" - PermissionJoinPublicTeams = "join_public_teams" - PermissionJoinPrivateTeams = "join_private_teams" - PermissionPermanentDeleteUser = "permanent_delete_user" - PermissionCreateBot = "create_bot" - PermissionReadBots = "read_bots" - PermissionReadOthersBots = "read_others_bots" - PermissionManageBots = "manage_bots" - PermissionManageOthersBots = "manage_others_bots" - PermissionDeletePublicChannel = "delete_public_channel" - PermissionDeletePrivateChannel = "delete_private_channel" - PermissionManagePublicChannelProperties = "manage_public_channel_properties" - PermissionManagePrivateChannelProperties = "manage_private_channel_properties" - PermissionConvertPublicChannelToPrivate = "convert_public_channel_to_private" - PermissionConvertPrivateChannelToPublic = "convert_private_channel_to_public" - PermissionViewMembers = "view_members" - PermissionInviteUser = "invite_user" - PermissionInviteGuest = "invite_guest" - PermissionPromoteGuest = "promote_guest" - PermissionDemoteToGuest = "demote_to_guest" - PermissionUseChannelMentions = "use_channel_mentions" - PermissionCreatePost = "create_post" - PermissionCreatePost_PUBLIC = "create_post_public" - PermissionUseGroupMentions = "use_group_mentions" - PermissionAddReaction = "add_reaction" - PermissionRemoveReaction = "remove_reaction" - PermissionManagePublicChannelMembers = "manage_public_channel_members" - PermissionManagePrivateChannelMembers = "manage_private_channel_members" - PermissionReadJobs = "read_jobs" - PermissionManageJobs = "manage_jobs" - PermissionReadOtherUsersTeams = "read_other_users_teams" - PermissionEditOtherUsers = "edit_other_users" - PermissionReadPublicChannelGroups = "read_public_channel_groups" - PermissionReadPrivateChannelGroups = "read_private_channel_groups" - PermissionEditBrand = "edit_brand" - PermissionManageSharedChannels = "manage_shared_channels" - PermissionManageSecureConnections = "manage_secure_connections" - PermissionManageRemoteClusters = "manage_remote_clusters" // deprecated; use `manage_secure_connections` + PermissionManageSystem = "manage_system" + PermissionManageTeam = "manage_team" + PermissionManageEmojis = "manage_emojis" + PermissionManageOthersEmojis = "manage_others_emojis" + PermissionCreateEmojis = "create_emojis" + PermissionDeleteEmojis = "delete_emojis" + PermissionDeleteOthersEmojis = "delete_others_emojis" + PermissionManageWebhooks = "manage_webhooks" + PermissionManageOthersWebhooks = "manage_others_webhooks" + PermissionManageIncomingWebhooks = "manage_incoming_webhooks" + PermissionManageOwnIncomingWebhooks = "manage_own_incoming_webhooks" + PermissionManageOthersIncomingWebhooks = "manage_others_incoming_webhooks" + PermissionManageOutgoingWebhooks = "manage_outgoing_webhooks" + PermissionManageOwnOutgoingWebhooks = "manage_own_outgoing_webhooks" + PermissionManageOthersOutgoingWebhooks = "manage_others_outgoing_webhooks" + PermissionBypassIncomingWebhookChannelLock = "bypass_incoming_webhook_channel_lock" + PermissionListPublicTeams = "list_public_teams" + PermissionListPrivateTeams = "list_private_teams" + PermissionJoinPublicTeams = "join_public_teams" + PermissionJoinPrivateTeams = "join_private_teams" + PermissionPermanentDeleteUser = "permanent_delete_user" + PermissionCreateBot = "create_bot" + PermissionReadBots = "read_bots" + PermissionReadOthersBots = "read_others_bots" + PermissionManageBots = "manage_bots" + PermissionManageOthersBots = "manage_others_bots" + PermissionManageSlashCommands = "manage_slash_commands" + PermissionManageOwnSlashCommands = "manage_own_slash_commands" + PermissionDeletePublicChannel = "delete_public_channel" + PermissionDeletePrivateChannel = "delete_private_channel" + PermissionManagePublicChannelProperties = "manage_public_channel_properties" + PermissionManagePrivateChannelProperties = "manage_private_channel_properties" + PermissionConvertPublicChannelToPrivate = "convert_public_channel_to_private" + PermissionConvertPrivateChannelToPublic = "convert_private_channel_to_public" + PermissionViewMembers = "view_members" + PermissionInviteUser = "invite_user" + PermissionInviteGuest = "invite_guest" + PermissionPromoteGuest = "promote_guest" + PermissionDemoteToGuest = "demote_to_guest" + PermissionUseChannelMentions = "use_channel_mentions" + PermissionCreatePost = "create_post" + PermissionCreatePost_PUBLIC = "create_post_public" + PermissionUseGroupMentions = "use_group_mentions" + PermissionAddReaction = "add_reaction" + PermissionRemoveReaction = "remove_reaction" + PermissionManagePublicChannelMembers = "manage_public_channel_members" + PermissionManagePrivateChannelMembers = "manage_private_channel_members" + PermissionReadJobs = "read_jobs" + PermissionManageJobs = "manage_jobs" + PermissionReadOtherUsersTeams = "read_other_users_teams" + PermissionEditOtherUsers = "edit_other_users" + PermissionReadPublicChannelGroups = "read_public_channel_groups" + PermissionReadPrivateChannelGroups = "read_private_channel_groups" + PermissionEditBrand = "edit_brand" + PermissionManageSharedChannels = "manage_shared_channels" + PermissionManageSecureConnections = "manage_secure_connections" + PermissionManageOAuth = "manage_oauth" + PermissionManageRemoteClusters = "manage_remote_clusters" // deprecated; use `manage_secure_connections` ) // Deprecated: This function should only be used if a case arises where team and/or channel scheme roles do not need to be migrated. @@ -275,6 +281,36 @@ func (a *App) getWebhooksPermissionsSplitMigration() (permissionsMap, error) { }, nil } +func (a *App) getIntegrationsOwnPermissionsMigration() (permissionsMap, error) { + return permissionsMap{ + permissionTransformation{ + On: permissionExists(PermissionManageIncomingWebhooks), + Add: []string{PermissionManageOwnIncomingWebhooks, PermissionBypassIncomingWebhookChannelLock}, + Remove: []string{PermissionManageIncomingWebhooks}, + }, + permissionTransformation{ + On: permissionExists(PermissionManageOutgoingWebhooks), + Add: []string{PermissionManageOwnOutgoingWebhooks}, + Remove: []string{PermissionManageOutgoingWebhooks}, + }, + permissionTransformation{ + On: permissionExists(PermissionManageSlashCommands), + Add: []string{PermissionManageOwnSlashCommands}, + Remove: []string{PermissionManageSlashCommands}, + }, + // Ensure system admin has the new "manage others" permissions + permissionTransformation{ + On: isExactRole(model.SystemAdminRoleId), + Add: []string{model.PermissionManageOthersIncomingWebhooks.Id, model.PermissionManageOthersOutgoingWebhooks.Id, model.PermissionManageOthersSlashCommands.Id}, + }, + // Ensure team admin (including scheme team admins) have the new "manage others" permissions + permissionTransformation{ + On: isRole(model.TeamAdminRoleId), + Add: []string{model.PermissionManageOthersIncomingWebhooks.Id, model.PermissionManageOthersOutgoingWebhooks.Id, model.PermissionManageOthersSlashCommands.Id}, + }, + }, nil +} + func (a *App) getListJoinPublicPrivateTeamsPermissionsMigration() (permissionsMap, error) { return permissionsMap{ permissionTransformation{ @@ -1225,6 +1261,7 @@ func (s *Server) doPermissionsMigrations() error { }{ {Key: model.MigrationKeyEmojiPermissionsSplit, Migration: a.getEmojisPermissionsSplitMigration}, {Key: model.MigrationKeyWebhookPermissionsSplit, Migration: a.getWebhooksPermissionsSplitMigration}, + {Key: model.MigrationKeyIntegrationsOwnPermissions, Migration: a.getIntegrationsOwnPermissionsMigration}, {Key: model.MigrationKeyListJoinPublicPrivateTeams, Migration: a.getListJoinPublicPrivateTeamsPermissionsMigration}, {Key: model.MigrationKeyRemovePermanentDeleteUser, Migration: a.removePermanentDeleteUserMigration}, {Key: model.MigrationKeyAddBotPermissions, Migration: a.getAddBotPermissionsMigration}, diff --git a/server/channels/testlib/store.go b/server/channels/testlib/store.go index 424b693c9fc..b0cda5a20f0 100644 --- a/server/channels/testlib/store.go +++ b/server/channels/testlib/store.go @@ -42,6 +42,7 @@ func GetMockStoreForSetupFunctions() *mocks.Store { systemStore.On("GetByName", "content_flagging_setup_done").Return(&model.System{Name: "content_flagging_setup_done", Value: "true"}, nil) systemStore.On("GetByName", model.MigrationKeyEmojiPermissionsSplit).Return(&model.System{Name: model.MigrationKeyEmojiPermissionsSplit, Value: "true"}, nil) systemStore.On("GetByName", model.MigrationKeyWebhookPermissionsSplit).Return(&model.System{Name: model.MigrationKeyWebhookPermissionsSplit, Value: "true"}, nil) + systemStore.On("GetByName", model.MigrationKeyIntegrationsOwnPermissions).Return(&model.System{Name: model.MigrationKeyIntegrationsOwnPermissions, Value: "true"}, nil) systemStore.On("GetByName", model.MigrationKeyListJoinPublicPrivateTeams).Return(&model.System{Name: model.MigrationKeyListJoinPublicPrivateTeams, Value: "true"}, nil) systemStore.On("GetByName", model.MigrationKeyRemovePermanentDeleteUser).Return(&model.System{Name: model.MigrationKeyRemovePermanentDeleteUser, Value: "true"}, nil) systemStore.On("GetByName", model.MigrationKeyAddBotPermissions).Return(&model.System{Name: model.MigrationKeyAddBotPermissions, Value: "true"}, nil) diff --git a/server/channels/utils/policies-roles-mapping.json b/server/channels/utils/policies-roles-mapping.json index 24929152c4f..9db97753f05 100644 --- a/server/channels/utils/policies-roles-mapping.json +++ b/server/channels/utils/policies-roles-mapping.json @@ -500,16 +500,36 @@ "permission": "manage_incoming_webhooks", "shouldHave": false }, + { + "roleName": "team_user", + "permission": "manage_own_incoming_webhooks", + "shouldHave": false + }, { "roleName": "team_user", "permission": "manage_outgoing_webhooks", "shouldHave": false }, + { + "roleName": "team_user", + "permission": "manage_own_outgoing_webhooks", + "shouldHave": false + }, { "roleName": "team_user", "permission": "manage_slash_commands", "shouldHave": false }, + { + "roleName": "team_user", + "permission": "manage_own_slash_commands", + "shouldHave": false + }, + { + "roleName": "team_user", + "permission": "bypass_incoming_webhook_channel_lock", + "shouldHave": false + }, { "roleName": "system_user", "permission": "manage_oauth", @@ -522,16 +542,36 @@ "permission": "manage_incoming_webhooks", "shouldHave": true }, + { + "roleName": "team_user", + "permission": "manage_own_incoming_webhooks", + "shouldHave": true + }, { "roleName": "team_user", "permission": "manage_outgoing_webhooks", "shouldHave": true }, + { + "roleName": "team_user", + "permission": "manage_own_outgoing_webhooks", + "shouldHave": true + }, { "roleName": "team_user", "permission": "manage_slash_commands", "shouldHave": true }, + { + "roleName": "team_user", + "permission": "manage_own_slash_commands", + "shouldHave": true + }, + { + "roleName": "team_user", + "permission": "bypass_incoming_webhook_channel_lock", + "shouldHave": true + }, { "roleName": "system_user", "permission": "manage_oauth", diff --git a/server/i18n/en.json b/server/i18n/en.json index c51e06cdaee..119dfc89717 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -731,6 +731,10 @@ "id": "api.command.invite_people.sent", "translation": "Email invite(s) sent" }, + { + "id": "api.command.move_command.creator_no_permission.app_error", + "translation": "No permission to move command" + }, { "id": "api.command.team_mismatch.app_error", "translation": "Unable to update commands across teams." diff --git a/server/public/model/migration.go b/server/public/model/migration.go index fc437fef370..a77785d62bd 100644 --- a/server/public/model/migration.go +++ b/server/public/model/migration.go @@ -9,6 +9,7 @@ const ( MigrationKeyEmojiPermissionsSplit = "emoji_permissions_split" MigrationKeyWebhookPermissionsSplit = "webhook_permissions_split" + MigrationKeyIntegrationsOwnPermissions = "integrations_own_permissions" MigrationKeyListJoinPublicPrivateTeams = "list_join_public_private_teams" MigrationKeyRemovePermanentDeleteUser = "remove_permanent_delete_user" MigrationKeyAddBotPermissions = "add_bot_permissions" diff --git a/server/public/model/permission.go b/server/public/model/permission.go index 815c08c3fd5..2393c0c2f89 100644 --- a/server/public/model/permission.go +++ b/server/public/model/permission.go @@ -69,11 +69,15 @@ var PermissionGetPublicLink *Permission var PermissionManageWebhooks *Permission var PermissionManageOthersWebhooks *Permission var PermissionManageIncomingWebhooks *Permission +var PermissionManageOwnIncomingWebhooks *Permission var PermissionManageOutgoingWebhooks *Permission +var PermissionManageOwnOutgoingWebhooks *Permission var PermissionManageOthersIncomingWebhooks *Permission var PermissionManageOthersOutgoingWebhooks *Permission +var PermissionManageOwnSlashCommands *Permission var PermissionManageOAuth *Permission var PermissionManageSystemWideOAuth *Permission +var PermissionBypassIncomingWebhookChannelLock *Permission var PermissionManageEmojis *Permission var PermissionManageOthersEmojis *Permission var PermissionCreateEmojis *Permission @@ -431,12 +435,19 @@ func initializePermissions() { "authentication.permissions.team_use_slash_commands.description", PermissionScopeChannel, } + // DEPRECATED - use PermissionManageOwnSlashCommands instead PermissionManageSlashCommands = &Permission{ "manage_slash_commands", "authentication.permissions.manage_slash_commands.name", "authentication.permissions.manage_slash_commands.description", PermissionScopeTeam, } + PermissionManageOwnSlashCommands = &Permission{ + "manage_own_slash_commands", + "authentication.permissions.manage_own_slash_commands.name", + "authentication.permissions.manage_own_slash_commands.description", + PermissionScopeTeam, + } PermissionManageOthersSlashCommands = &Permission{ "manage_others_slash_commands", "authentication.permissions.manage_others_slash_commands.name", @@ -668,18 +679,32 @@ func initializePermissions() { "authentication.permissions.manage_others_webhooks.description", PermissionScopeTeam, } + // DEPRECATED - use PermissionManageOwnIncomingWebhooks instead PermissionManageIncomingWebhooks = &Permission{ "manage_incoming_webhooks", "authentication.permissions.manage_incoming_webhooks.name", "authentication.permissions.manage_incoming_webhooks.description", PermissionScopeTeam, } + PermissionManageOwnIncomingWebhooks = &Permission{ + "manage_own_incoming_webhooks", + "authentication.permissions.manage_own_incoming_webhooks.name", + "authentication.permissions.manage_own_incoming_webhooks.description", + PermissionScopeTeam, + } + // DEPRECATED - use PermissionManageOwnOutgoingWebhooks instead PermissionManageOutgoingWebhooks = &Permission{ "manage_outgoing_webhooks", "authentication.permissions.manage_outgoing_webhooks.name", "authentication.permissions.manage_outgoing_webhooks.description", PermissionScopeTeam, } + PermissionManageOwnOutgoingWebhooks = &Permission{ + "manage_own_outgoing_webhooks", + "authentication.permissions.manage_own_outgoing_webhooks.name", + "authentication.permissions.manage_own_outgoing_webhooks.description", + PermissionScopeTeam, + } PermissionManageOthersIncomingWebhooks = &Permission{ "manage_others_incoming_webhooks", "authentication.permissions.manage_others_incoming_webhooks.name", @@ -692,6 +717,12 @@ func initializePermissions() { "authentication.permissions.manage_others_outgoing_webhooks.description", PermissionScopeTeam, } + PermissionBypassIncomingWebhookChannelLock = &Permission{ + "bypass_incoming_webhook_channel_lock", + "authentication.permissions.bypass_incoming_webhook_channel_lock.name", + "authentication.permissions.bypass_incoming_webhook_channel_lock.description", + PermissionScopeTeam, + } PermissionManageOAuth = &Permission{ "manage_oauth", "authentication.permissions.manage_oauth.name", @@ -2414,7 +2445,6 @@ func initializePermissions() { PermissionEditOtherUsers, PermissionReadOtherUsersTeams, PermissionGetPublicLink, - PermissionManageOAuth, PermissionManageSystemWideOAuth, PermissionCreateTeam, PermissionListUsersWithoutTeam, @@ -2484,7 +2514,7 @@ func initializePermissions() { TeamScopedPermissions := []*Permission{ PermissionInviteUser, PermissionAddUserToTeam, - PermissionManageSlashCommands, + PermissionManageOwnSlashCommands, PermissionManageOthersSlashCommands, PermissionCreatePublicChannel, PermissionCreatePrivateChannel, @@ -2492,10 +2522,11 @@ func initializePermissions() { PermissionListTeamChannels, PermissionJoinPublicChannels, PermissionReadPublicChannel, - PermissionManageIncomingWebhooks, - PermissionManageOutgoingWebhooks, + PermissionManageOwnIncomingWebhooks, + PermissionManageOwnOutgoingWebhooks, PermissionManageOthersIncomingWebhooks, PermissionManageOthersOutgoingWebhooks, + PermissionBypassIncomingWebhookChannelLock, PermissionCreateEmojis, PermissionDeleteEmojis, PermissionDeleteOthersEmojis, @@ -2562,6 +2593,10 @@ func initializePermissions() { PermissionPermanentDeleteUser, PermissionManageWebhooks, PermissionManageOthersWebhooks, + PermissionManageIncomingWebhooks, + PermissionManageOutgoingWebhooks, + PermissionManageSlashCommands, + PermissionManageOAuth, PermissionManageEmojis, PermissionManageOthersEmojis, PermissionSysconsoleReadAuthentication, diff --git a/server/public/model/role.go b/server/public/model/role.go index f9436192eec..bbd17f3b0ae 100644 --- a/server/public/model/role.go +++ b/server/public/model/role.go @@ -979,12 +979,13 @@ func MakeDefaultRoles() map[string]*Role { PermissionImportTeam.Id, PermissionManageTeamRoles.Id, PermissionManageChannelRoles.Id, + PermissionManageOwnIncomingWebhooks.Id, PermissionManageOthersIncomingWebhooks.Id, + PermissionManageOwnOutgoingWebhooks.Id, PermissionManageOthersOutgoingWebhooks.Id, - PermissionManageSlashCommands.Id, + PermissionManageOwnSlashCommands.Id, PermissionManageOthersSlashCommands.Id, - PermissionManageIncomingWebhooks.Id, - PermissionManageOutgoingWebhooks.Id, + PermissionBypassIncomingWebhookChannelLock.Id, PermissionConvertPublicChannelToPrivate.Id, PermissionConvertPrivateChannelToPublic.Id, PermissionDeletePost.Id, diff --git a/webapp/channels/src/components/admin_console/permission_schemes_settings/permissions_tree/__snapshots__/permissions_tree.test.tsx.snap b/webapp/channels/src/components/admin_console/permission_schemes_settings/permissions_tree/__snapshots__/permissions_tree.test.tsx.snap index 4afd9f2326e..dfd509ad3f4 100644 --- a/webapp/channels/src/components/admin_console/permission_schemes_settings/permissions_tree/__snapshots__/permissions_tree.test.tsx.snap +++ b/webapp/channels/src/components/admin_console/permission_schemes_settings/permissions_tree/__snapshots__/permissions_tree.test.tsx.snap @@ -351,10 +351,29 @@ exports[`components/admin_console/permission_schemes_settings/permission_tree sh Object { "id": "integrations", "permissions": Array [ - "manage_incoming_webhooks", - "manage_outgoing_webhooks", + Object { + "id": "manage_incoming_webhooks_group", + "permissions": Array [ + "manage_own_incoming_webhooks", + "manage_others_incoming_webhooks", + "bypass_incoming_webhook_channel_lock", + ], + }, + Object { + "id": "manage_outgoing_webhooks_group", + "permissions": Array [ + "manage_own_outgoing_webhooks", + "manage_others_outgoing_webhooks", + ], + }, "manage_oauth", - "manage_slash_commands", + Object { + "id": "manage_slash_commands_group", + "permissions": Array [ + "manage_own_slash_commands", + "manage_others_slash_commands", + ], + }, "create_emojis", "delete_emojis", "delete_others_emojis", @@ -542,10 +561,29 @@ exports[`components/admin_console/permission_schemes_settings/permission_tree sh Object { "id": "integrations", "permissions": Array [ - "manage_incoming_webhooks", - "manage_outgoing_webhooks", + Object { + "id": "manage_incoming_webhooks_group", + "permissions": Array [ + "manage_own_incoming_webhooks", + "manage_others_incoming_webhooks", + "bypass_incoming_webhook_channel_lock", + ], + }, + Object { + "id": "manage_outgoing_webhooks_group", + "permissions": Array [ + "manage_own_outgoing_webhooks", + "manage_others_outgoing_webhooks", + ], + }, "manage_oauth", - "manage_slash_commands", + Object { + "id": "manage_slash_commands_group", + "permissions": Array [ + "manage_own_slash_commands", + "manage_others_slash_commands", + ], + }, "create_emojis", "delete_emojis", "delete_others_emojis", @@ -744,10 +782,29 @@ exports[`components/admin_console/permission_schemes_settings/permission_tree sh Object { "id": "integrations", "permissions": Array [ - "manage_incoming_webhooks", - "manage_outgoing_webhooks", + Object { + "id": "manage_incoming_webhooks_group", + "permissions": Array [ + "manage_own_incoming_webhooks", + "manage_others_incoming_webhooks", + "bypass_incoming_webhook_channel_lock", + ], + }, + Object { + "id": "manage_outgoing_webhooks_group", + "permissions": Array [ + "manage_own_outgoing_webhooks", + "manage_others_outgoing_webhooks", + ], + }, "manage_oauth", - "manage_slash_commands", + Object { + "id": "manage_slash_commands_group", + "permissions": Array [ + "manage_own_slash_commands", + "manage_others_slash_commands", + ], + }, "create_emojis", "delete_emojis", "delete_others_emojis", @@ -946,10 +1003,29 @@ exports[`components/admin_console/permission_schemes_settings/permission_tree sh Object { "id": "integrations", "permissions": Array [ - "manage_incoming_webhooks", - "manage_outgoing_webhooks", + Object { + "id": "manage_incoming_webhooks_group", + "permissions": Array [ + "manage_own_incoming_webhooks", + "manage_others_incoming_webhooks", + "bypass_incoming_webhook_channel_lock", + ], + }, + Object { + "id": "manage_outgoing_webhooks_group", + "permissions": Array [ + "manage_own_outgoing_webhooks", + "manage_others_outgoing_webhooks", + ], + }, "manage_oauth", - "manage_slash_commands", + Object { + "id": "manage_slash_commands_group", + "permissions": Array [ + "manage_own_slash_commands", + "manage_others_slash_commands", + ], + }, "create_emojis", "delete_emojis", "delete_others_emojis", @@ -1148,10 +1224,29 @@ exports[`components/admin_console/permission_schemes_settings/permission_tree sh Object { "id": "integrations", "permissions": Array [ - "manage_incoming_webhooks", - "manage_outgoing_webhooks", + Object { + "id": "manage_incoming_webhooks_group", + "permissions": Array [ + "manage_own_incoming_webhooks", + "manage_others_incoming_webhooks", + "bypass_incoming_webhook_channel_lock", + ], + }, + Object { + "id": "manage_outgoing_webhooks_group", + "permissions": Array [ + "manage_own_outgoing_webhooks", + "manage_others_outgoing_webhooks", + ], + }, "manage_oauth", - "manage_slash_commands", + Object { + "id": "manage_slash_commands_group", + "permissions": Array [ + "manage_own_slash_commands", + "manage_others_slash_commands", + ], + }, "create_emojis", "delete_emojis", "delete_others_emojis", @@ -1357,10 +1452,29 @@ exports[`components/admin_console/permission_schemes_settings/permission_tree sh Object { "id": "integrations", "permissions": Array [ - "manage_incoming_webhooks", - "manage_outgoing_webhooks", + Object { + "id": "manage_incoming_webhooks_group", + "permissions": Array [ + "manage_own_incoming_webhooks", + "manage_others_incoming_webhooks", + "bypass_incoming_webhook_channel_lock", + ], + }, + Object { + "id": "manage_outgoing_webhooks_group", + "permissions": Array [ + "manage_own_outgoing_webhooks", + "manage_others_outgoing_webhooks", + ], + }, "manage_oauth", - "manage_slash_commands", + Object { + "id": "manage_slash_commands_group", + "permissions": Array [ + "manage_own_slash_commands", + "manage_others_slash_commands", + ], + }, "create_emojis", "delete_emojis", "delete_others_emojis", diff --git a/webapp/channels/src/components/admin_console/permission_schemes_settings/permissions_tree/permissions_tree.tsx b/webapp/channels/src/components/admin_console/permission_schemes_settings/permissions_tree/permissions_tree.tsx index 42cfcbb983b..9aa3d363f60 100644 --- a/webapp/channels/src/components/admin_console/permission_schemes_settings/permissions_tree/permissions_tree.tsx +++ b/webapp/channels/src/components/admin_console/permission_schemes_settings/permissions_tree/permissions_tree.tsx @@ -213,11 +213,30 @@ export default class PermissionsTree extends React.PureComponent { const sharedChannelsGroup = this.groups[9]; const customGroupsGroup = this.groups[10]; - if (config.EnableIncomingWebhooks === 'true' && !integrationsGroup.permissions.includes(Permissions.MANAGE_INCOMING_WEBHOOKS)) { - integrationsGroup.permissions.push(Permissions.MANAGE_INCOMING_WEBHOOKS); + if (config.EnableIncomingWebhooks === 'true') { + const incomingWebhookGroup = { + id: 'manage_incoming_webhooks_group', + permissions: [ + Permissions.MANAGE_OWN_INCOMING_WEBHOOKS, + Permissions.MANAGE_OTHERS_INCOMING_WEBHOOKS, + Permissions.BYPASS_INCOMING_WEBHOOK_CHANNEL_LOCK, + ], + }; + if (!integrationsGroup.permissions.some((p: any) => p.id === 'manage_incoming_webhooks_group')) { + integrationsGroup.permissions.push(incomingWebhookGroup); + } } - if (config.EnableOutgoingWebhooks === 'true' && !integrationsGroup.permissions.includes(Permissions.MANAGE_OUTGOING_WEBHOOKS)) { - integrationsGroup.permissions.push(Permissions.MANAGE_OUTGOING_WEBHOOKS); + if (config.EnableOutgoingWebhooks === 'true') { + const outgoingWebhookGroup = { + id: 'manage_outgoing_webhooks_group', + permissions: [ + Permissions.MANAGE_OWN_OUTGOING_WEBHOOKS, + Permissions.MANAGE_OTHERS_OUTGOING_WEBHOOKS, + ], + }; + if (!integrationsGroup.permissions.some((p: any) => p.id === 'manage_outgoing_webhooks_group')) { + integrationsGroup.permissions.push(outgoingWebhookGroup); + } } if (config.EnableOAuthServiceProvider === 'true' && !integrationsGroup.permissions.includes(Permissions.MANAGE_OAUTH)) { integrationsGroup.permissions.push(Permissions.MANAGE_OAUTH); @@ -225,8 +244,17 @@ export default class PermissionsTree extends React.PureComponent { if (config.EnableOutgoingOAuthConnections === 'true' && !integrationsGroup.permissions.includes(Permissions.MANAGE_OUTGOING_OAUTH_CONNECTIONS)) { integrationsGroup.permissions.push(Permissions.MANAGE_OUTGOING_OAUTH_CONNECTIONS); } - if (config.EnableCommands === 'true' && !integrationsGroup.permissions.includes(Permissions.MANAGE_SLASH_COMMANDS)) { - integrationsGroup.permissions.push(Permissions.MANAGE_SLASH_COMMANDS); + if (config.EnableCommands === 'true') { + const slashCommandGroup = { + id: 'manage_slash_commands_group', + permissions: [ + Permissions.MANAGE_OWN_SLASH_COMMANDS, + Permissions.MANAGE_OTHERS_SLASH_COMMANDS, + ], + }; + if (!integrationsGroup.permissions.some((p: any) => p.id === 'manage_slash_commands_group')) { + integrationsGroup.permissions.push(slashCommandGroup); + } } if (config.EnableCustomEmoji === 'true' && !integrationsGroup.permissions.includes(Permissions.CREATE_EMOJIS)) { integrationsGroup.permissions.push(Permissions.CREATE_EMOJIS); diff --git a/webapp/channels/src/components/admin_console/permission_schemes_settings/strings/groups.tsx b/webapp/channels/src/components/admin_console/permission_schemes_settings/strings/groups.tsx index f3291975c6f..48d91cf8dd4 100644 --- a/webapp/channels/src/components/admin_console/permission_schemes_settings/strings/groups.tsx +++ b/webapp/channels/src/components/admin_console/permission_schemes_settings/strings/groups.tsx @@ -105,6 +105,46 @@ export const groupRolesStrings: Record defaultMessage: 'Edit own and others\' posts.', }, }), + manage_incoming_webhooks_group: defineMessages({ + name: { + id: 'admin.permissions.group.manage_incoming_webhooks.name', + defaultMessage: 'Manage Incoming Webhooks', + }, + description: { + id: 'admin.permissions.group.manage_incoming_webhooks.description', + defaultMessage: 'Manage own and others\' incoming webhooks.', + }, + }), + manage_outgoing_webhooks_group: defineMessages({ + name: { + id: 'admin.permissions.group.manage_outgoing_webhooks.name', + defaultMessage: 'Manage Outgoing Webhooks', + }, + description: { + id: 'admin.permissions.group.manage_outgoing_webhooks.description', + defaultMessage: 'Manage own and others\' outgoing webhooks.', + }, + }), + manage_slash_commands_group: defineMessages({ + name: { + id: 'admin.permissions.group.manage_slash_commands.name', + defaultMessage: 'Manage Slash Commands', + }, + description: { + id: 'admin.permissions.group.manage_slash_commands.description', + defaultMessage: 'Manage own and others\' slash commands.', + }, + }), + manage_oauth_group: defineMessages({ + name: { + id: 'admin.permissions.group.manage_oauth.name', + defaultMessage: 'Manage OAuth Applications', + }, + description: { + id: 'admin.permissions.group.manage_oauth.description', + defaultMessage: 'Manage own and others\' OAuth 2.0 applications.', + }, + }), teams_team_scope: defineMessages({ name: { id: 'admin.permissions.group.teams_team_scope.name', 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 b433c007414..4c7ac290b1b 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 @@ -288,11 +288,31 @@ export const permissionRolesStrings: Record { if (this.props.enableIncomingWebhooks) { incomingWebhooks = ( { if (this.props.enableOutgoingWebhooks) { outgoingWebhooks = ( { if (this.props.enableCommands) { commands = ( `; +exports[`components/integrations/AbstractIncomingWebhook should match snapshot when channelLocked is true 1`] = ` +
+ + + + + + +
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ + + + + + + +
+
+
+
+`; + exports[`components/integrations/AbstractIncomingWebhook should match snapshot, displays client error when no initial hook 1`] = `
{ enablePostUsernameOverride, enablePostIconOverride, action, + canBypassChannelLock: true, }; test('should match snapshot', () => { @@ -175,4 +176,10 @@ describe('components/integrations/AbstractIncomingWebhook', () => { expect(wrapper.state('iconURL')).toBe(newIconURL); }); + + test('should match snapshot when channelLocked is true', () => { + const props = {...requiredProps, channelLocked: true}; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); }); diff --git a/webapp/channels/src/components/integrations/abstract_incoming_webhook.tsx b/webapp/channels/src/components/integrations/abstract_incoming_webhook.tsx index 9abbb4baa80..e0f60aebba5 100644 --- a/webapp/channels/src/components/integrations/abstract_incoming_webhook.tsx +++ b/webapp/channels/src/components/integrations/abstract_incoming_webhook.tsx @@ -69,6 +69,11 @@ interface Props { */ enablePostIconOverride: boolean; + /** + * Whether the user can bypass the channel lock requirement. + */ + canBypassChannelLock?: boolean; + /** * The async function to run when the action button is pressed */ @@ -280,31 +285,33 @@ export default class AbstractIncomingWebhook extends PureComponent
-
- -
- -
+ { this.props.canBypassChannelLock && +
+ +
+ +
+ +
-
+ } { this.props.enablePostUsernameOverride &&