diff --git a/test/integration/api_test/issues_test.rb b/test/integration/api_test/issues_test.rb index b8f676a43..e65c46567 100644 --- a/test/integration/api_test/issues_test.rb +++ b/test/integration/api_test/issues_test.rb @@ -1067,4 +1067,207 @@ class Redmine::ApiTest::IssuesTest < Redmine::ApiTest::Base issue = Issue.find(1) assert_include attachment, issue.attachments end + + # ========================================================================== + # API Authorization Tests for Private/Public Projects + # + # These tests verify that Redmine's authorization system correctly handles: + # 1. Private projects - only members and admins can access + # 2. Public projects - Non member/Anonymous roles control access + # + # Default permissions in a fresh Redmine installation: + # Non member: :view_issues, :add_issues, :add_issue_notes (NO :edit_issues) + # Anonymous: :view_issues only + # ========================================================================== + + # --- Private Project Tests --- + # Private projects should deny access to non-members regardless of role permissions + + test "POST /issues.json should not allow non-member to create issue on private project" do + project = Project.find(2) # onlinestore - private project + user = User.find(3) # dlopper - member of project 1, NOT project 2 + + assert_not project.is_public? + assert_nil Member.find_by(project_id: project.id, user_id: user.id) + + assert_no_difference('Issue.count') do + post( + '/issues.json', + :params => {:issue => {:project_id => project.id, :tracker_id => 1, :subject => 'Unauthorized issue'}}, + :headers => {'X-Redmine-API-Key' => user.api_key}) + end + + assert_includes [401, 403, 422], response.status + end + + test "PUT /issues/:id.json should not allow non-member to update issue on private project" do + issue = Issue.find(4) # belongs to Project 2 (private) + user = User.find(3) # dlopper - NOT a member of Project 2 + + assert_not issue.project.is_public? + assert_nil Member.find_by(project_id: issue.project_id, user_id: user.id) + + original_subject = issue.subject + + put( + "/issues/#{issue.id}.json", + :params => {:issue => {:subject => 'Hacked subject'}}, + :headers => {'X-Redmine-API-Key' => user.api_key}) + + assert_includes [401, 403, 404], response.status + + issue.reload + assert_equal original_subject, issue.subject + end + + test "POST /issues.json should allow admin to create issue on private project even as non-member" do + project = Project.find(2) + user = User.find(1) # admin + + assert user.admin? + assert_not project.is_public? + assert_nil Member.find_by(project_id: project.id, user_id: user.id) + + assert_difference('Issue.count', 1) do + post( + '/issues.json', + :params => {:issue => {:project_id => project.id, :tracker_id => 1, :subject => 'Admin created issue'}}, + :headers => credentials('admin')) + end + + assert_response :created + end + + test "PUT /issues/:id.json should allow admin to update issue on private project even as non-member" do + issue = Issue.find(4) + user = User.find(1) # admin + + assert user.admin? + assert_not issue.project.is_public? + assert_nil Member.find_by(project_id: issue.project_id, user_id: user.id) + + put( + "/issues/#{issue.id}.json", + :params => {:issue => {:subject => 'Admin updated subject'}}, + :headers => credentials('admin')) + + assert_response :no_content + + issue.reload + assert_equal 'Admin updated subject', issue.subject + end + + # --- Public Project Tests --- + # Public projects respect Non member/Anonymous role permissions + + test "POST /issues.json should allow non-member to create issue on public project when Non member role has add_issues permission" do + project = Project.find(1) # ecookbook - public project + user = User.find(4) # rhill - NOT a member of any project + + assert project.is_public? + assert_nil Member.find_by(project_id: project.id, user_id: user.id) + assert Role.non_member.has_permission?(:add_issues) + + assert_difference('Issue.count', 1) do + post( + '/issues.json', + :params => {:issue => {:project_id => project.id, :tracker_id => 1, :subject => 'Non-member created issue'}}, + :headers => {'X-Redmine-API-Key' => user.api_key}) + end + + assert_response :created + end + + test "PUT /issues/:id.json should not allow non-member to update issue on public project when Non member role lacks edit_issues permission" do + # This tests the DEFAULT Redmine behavior where Non member has :add_issues but NOT :edit_issues + issue = Issue.find(1) + user = User.find(4) + + Role.non_member.remove_permission!(:edit_issues) + Role.non_member.remove_permission!(:add_issue_notes) + + assert issue.project.is_public? + assert_nil Member.find_by(project_id: issue.project_id, user_id: user.id) + assert_not Role.non_member.has_permission?(:edit_issues) + + original_subject = issue.subject + + put( + "/issues/#{issue.id}.json", + :params => {:issue => {:subject => 'Should fail'}}, + :headers => {'X-Redmine-API-Key' => user.api_key}) + + assert_includes [401, 403], response.status + + issue.reload + assert_equal original_subject, issue.subject + ensure + Role.non_member.add_permission!(:edit_issues) + Role.non_member.add_permission!(:add_issue_notes) + end + + test "POST /issues.json should not allow anonymous to create issue on public project by default" do + # Default Anonymous role lacks :add_issues permission + project = Project.find(1) + + assert project.is_public? + assert_not Role.anonymous.has_permission?(:add_issues) + + assert_no_difference('Issue.count') do + post( + '/issues.json', + :params => {:issue => {:project_id => project.id, :tracker_id => 1, :subject => 'Should fail'}}) + end + + assert_includes [401, 403, 422], response.status + end + + test "PUT /issues/:id.json should not allow anonymous to update issue on public project by default" do + # Default Anonymous role lacks :edit_issues permission + issue = Issue.find(1) + + Role.anonymous.remove_permission!(:add_issue_notes) + + assert issue.project.is_public? + assert_not Role.anonymous.has_permission?(:edit_issues) + + original_subject = issue.subject + + put( + "/issues/#{issue.id}.json", + :params => {:issue => {:subject => 'Should fail'}}) + + assert_includes [401, 403], response.status + + issue.reload + assert_equal original_subject, issue.subject + ensure + Role.anonymous.add_permission!(:add_issue_notes) + end + + test "PUT /issues/:id.json should not modify attributes when user only has add_issue_notes permission" do + # User can access update action (to add notes) but attribute changes should be ignored + issue = Issue.find(1) + user = User.find(4) + + Role.non_member.remove_permission!(:edit_issues) + + assert issue.project.is_public? + assert_not Role.non_member.has_permission?(:edit_issues) + assert Role.non_member.has_permission?(:add_issue_notes) + + original_subject = issue.subject + + put( + "/issues/#{issue.id}.json", + :params => {:issue => {:subject => 'Should be ignored', :notes => 'Adding a note'}}, + :headers => {'X-Redmine-API-Key' => user.api_key}) + + assert_response :no_content + + issue.reload + assert_equal original_subject, issue.subject + ensure + Role.non_member.add_permission!(:edit_issues) + end end