]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-4399 Added support for issue filter ownership change (admins only)
authorJean-Baptiste Vilain <jean-baptiste.vilain@sonarsource.com>
Tue, 25 Jun 2013 13:43:44 +0000 (15:43 +0200)
committerJean-Baptiste Vilain <jean-baptiste.vilain@sonarsource.com>
Tue, 25 Jun 2013 13:43:44 +0000 (15:43 +0200)
16 files changed:
plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties
sonar-application/src/main/assembly/conf/sonar.properties
sonar-core/src/main/resources/org/sonar/core/issue/db/IssueFilterMapper.xml
sonar-core/src/test/java/org/sonar/core/issue/db/IssueFilterDaoTest.java
sonar-core/src/test/resources/org/sonar/core/issue/db/IssueFilterDaoTest/should_update-result.xml
sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java
sonar-server/src/main/java/org/sonar/server/issue/IssueFilterService.java
sonar-server/src/main/webapp/WEB-INF/app/controllers/issues_controller.rb
sonar-server/src/main/webapp/WEB-INF/app/helpers/issues_helper.rb
sonar-server/src/main/webapp/WEB-INF/app/views/issues/_edit_form.html.erb
sonar-server/src/main/webapp/WEB-INF/app/views/issues/_shared_form.html.erb
sonar-server/src/main/webapp/WEB-INF/app/views/measures/_copy_form.html.erb
sonar-server/src/main/webapp/WEB-INF/app/views/measures/_edit_form.html.erb
sonar-server/src/main/webapp/WEB-INF/app/views/measures/_save_as_form.html.erb
sonar-server/src/main/webapp/WEB-INF/app/views/measures/_shared_form.html.erb [new file with mode: 0644]
sonar-server/src/test/java/org/sonar/server/issue/IssueFilterServiceTest.java

index 53f55b0016244331e68cb3c3f5dcfa4e8c664646..b796753716506fc8a48a6d3ffb3b51d3c68acbef 100644 (file)
@@ -538,6 +538,7 @@ issue_filter.copy_filter=Copy Filter
 issue_filter.form.name=Name
 issue_filter.form.description=Description
 issue_filter.form.share=Shared with all users
+issue_filter.form.owner=Owner
 issue_filter.favourite_filters=Favourite Filters
 issue_filter.manage.my_filters=My Filters
 issue_filter.no_filters=No filters
index d4fc1ff02e8120d0ba47f6791c17bdc691406e39..024dc35d67fb0b56e312f669ae28f8fc6c1a2e15 100644 (file)
@@ -42,12 +42,12 @@ sonar.jdbc.password:                       sonar
 
 #----- Embedded database H2
 # Note : it does not accept connections from remote hosts, so the
-# sonar server and the maven plugin must be executed on the same host.
+# SonarQube server and the maven plugin must be executed on the same host.
 
 # Comment the following line to deactivate the default embedded database.
 sonar.jdbc.url:                            jdbc:h2:tcp://localhost:9092/sonar
 
-# directory containing H2 database files. By default it's the /data directory in the sonar installation.
+# directory containing H2 database files. By default it's the /data directory in the SonarQube installation.
 #sonar.embeddedDatabase.dataDir:
 # H2 embedded database server listening port, defaults to 9092
 #sonar.embeddedDatabase.port:               9092
index 3e908084468cf8e7f92f11304339416da1af5d83..21a2c87cfee88a48beea9f82b3534c259369535b 100644 (file)
@@ -85,6 +85,7 @@
     shared=#{shared},
     description=#{description},
     data=#{data},
+    user_login=#{userLogin},
     updated_at=current_timestamp
     where id=#{id}
   </update>
index 5a2a714ec95e02af210553a1febf1d50a6172298..fd3eb8e7e93d5f5c2d20b9f870322d9d406f832e 100644 (file)
@@ -130,6 +130,7 @@ public class IssueFilterDaoTest extends AbstractDaoTestCase {
     filterDto.setShared(false);
     filterDto.setDescription("All closed issues");
     filterDto.setData("statuses=CLOSED");
+    filterDto.setUserLogin("bernard");
 
     dao.update(filterDto);
 
index daf43474984c3f4ef76de283b8f3211902a13778..134411beaf4978713e646930c172f2c76d53e5a2 100644 (file)
@@ -13,7 +13,7 @@
   <issue_filters
       id="2"
       name="Closed issues"
-      user_login="michael"
+      user_login="bernard"
       shared="[false]"
       description="All closed issues"
       data="statuses=CLOSED"
index 858c061f1cc401a4a2c50ac9736d3e2bb4bd7eb5..0491ce1f029b5975befd50ddd91682de5be17e3e 100644 (file)
@@ -525,6 +525,7 @@ public class InternalRubyIssueService implements ServerComponent {
     String name = params.get("name");
     String description = params.get("description");
     String data = params.get("data");
+    String user = params.get("user");
     Boolean sharedParam = RubyUtils.toBoolean(params.get("shared"));
     boolean shared = sharedParam != null ? sharedParam : false;
 
@@ -538,6 +539,7 @@ public class InternalRubyIssueService implements ServerComponent {
       DefaultIssueFilter defaultIssueFilter = DefaultIssueFilter.create(name)
         .setDescription(description)
         .setShared(shared)
+        .setUser(user)
         .setData(data);
       if (isUpdate) {
         defaultIssueFilter.setId(Long.valueOf(id));
index 7eaae9e62baacf5ec17ad8b0a18c5e52b53cdb58..eddbe7c0245d4a5ea0508879d15ab750bf34a16a 100644 (file)
@@ -95,6 +95,9 @@ public class IssueFilterService implements ServerComponent {
     String user = getNotNullLogin(userSession);
     IssueFilterDto issueFilterDto = findIssueFilterDto(issueFilter.id(), user);
     verifyCurrentUserCanModifyFilter(issueFilterDto.toIssueFilter(), user);
+    if(issueFilterDto.getUserLogin() != issueFilter.user()) {
+      verifyCurrentUserCanChangeFilterOwnership(user);
+    }
     validateFilter(issueFilter, user);
 
     issueFilterDao.update(IssueFilterDto.toIssueFilter(issueFilter));
@@ -190,6 +193,13 @@ public class IssueFilterService implements ServerComponent {
     }
   }
 
+  private void verifyCurrentUserCanChangeFilterOwnership(String user) {
+    if(!isAdmin(user)) {
+      // TODO throw unauthorized
+      throw new IllegalStateException("User is not authorized to change the owner of this filter");
+    }
+  }
+
   private void validateFilter(DefaultIssueFilter issueFilter, String user) {
     if (issueFilterDao.selectByNameAndUser(issueFilter.name(), user, issueFilter.id()) != null) {
       throw new IllegalArgumentException("Name already exists");
index a40844c658301fe8e27171ad4940494368ebf297..4ff635695fb3b2037b07e52fce9f4fe442bb5185 100644 (file)
@@ -123,7 +123,8 @@ class IssuesController < ApplicationController
     verify_post_request
 
     existing_filter = find_filter(params[:id].to_i)
-    options = {'id' => params[:id].to_s, 'name' => params[:name], 'description' => params[:description], 'data' => existing_filter.data, 'shared' => params[:shared]=='true' }
+    options = {'id' => params[:id].to_s, 'name' => params[:name], 'description' => params[:description],
+               'data' => existing_filter.data, 'shared' => params[:shared]=='true', 'user' => params[:user]}
     filter_result = Internal.issues.updateIssueFilter(options)
     if filter_result.ok
       @filter = filter_result.get()
index 62abbc88488799090b4b3cf67f10c8c93f6ef269..911d658f87dd089f3430ae8342d940266aea0088 100644 (file)
@@ -44,4 +44,8 @@ module IssuesHelper
     "<a href='#' class='issue-filter-star #{style}' filter-id='#{filter.id.to_s}' title='#{title}'></a>"
   end
 
+  def can_be_reassigned_by(user, filter)
+    user.has_role?(:admin) && filter.shared
+  end
+
 end
index 42ea290e3aef96217d947e725f2a23444c8458be..dbd158d01cee03283c09562749ce20a49211ea59 100644 (file)
@@ -4,7 +4,9 @@
     <div class="modal-head">
       <h2><%= message('issue_filter.edit_filter') -%></h2>
     </div>
-    <%= render :partial => 'shared_form' %>
+
+    <%= render :partial => 'shared_form', :locals => {:display_owner => true} %>
+
     <div class="modal-foot">
       <input type="submit" value="<%= message('save') -%>" id="save-submit"/>
       <a href="#" onclick="return closeModalWindow()" id="save-cancel"><%= message('cancel') -%></a>
index af3d073e3c0bd78025fece6575532832f2dd242f..f9574560297e75791860053f26480212c9c2c3f6 100644 (file)
@@ -1,3 +1,6 @@
+<% if !local_assigns.has_key? :display_owner
+     display_owner = false
+   end %>
 <div class="modal-body">
   <%= render :partial => 'display_errors' %>
   <div class="modal-field">
@@ -8,6 +11,13 @@
     <label for="description"><%= message('issue_filter.form.description') -%></label>
     <input id="description" name="description" type="text" size="50" maxlength="4000" value="<%= params[:description] || (h(@filter.description) if @filter)  -%>"/>
   </div>
+  <% if display_owner && can_be_reassigned_by(current_user, @filter) %>
+    <% filter_owner = Api.users.findByLogin(@filter.user) %>
+    <div class="modal-field">
+      <label for="user"><%= h message('issue_filter.form.owner') -%></label>
+      <%= user_select_tag('user', :html_id => 'select-filter-owner', :selected_user => filter_owner) -%>
+    </div>
+  <% end %>
   <div class="modal-field">
     <label for="shared"><%= message('issue_filter.form.share') -%></label>
     <input id="shared" name="shared" type="checkbox" value="true" <%= 'checked' if (params[:shared] || (@filter && @filter.shared)) -%>/>
index dab5991e4e7ee8a0c433aef5d1df1703a97ca0bd..4aac628b14c5f92c175dc16800215946932f90e4 100644 (file)
@@ -4,23 +4,9 @@
     <div class="modal-head">
       <h2>Copy Filter</h2>
     </div>
-    <div class="modal-body">
-      <% @filter.errors.each do |attr, msg| %>
-        <p class="error"><%= h msg -%></p>
-      <% end %>
-      <div class="modal-field">
-        <label for="name"><%= h message('name') -%> <em class="mandatory">*</em></label>
-        <input id="name" name="name" type="text" size="50" maxlength="100" value="" autofocus="autofocus"/>
-      </div>
-      <div class="modal-field">
-        <label for="description"><%= h message('description') -%></label>
-        <input id="description" name="description" type="text" size="50" maxlength="4000" value="<%= h @filter.description -%>" />
-      </div>
-      <div class="modal-field">
-        <label for="shared"><%= h message('measure_filter.shared_with_all_users') -%></label>
-        <input id="shared" name="shared" type="checkbox" value="true" <%= 'checked' if @filter.shared -%>/>
-      </div>
-    </div>
+
+    <%= render :partial => 'shared_form' %>
+
     <div class="modal-foot">
       <input type="submit" value="<%= h message('copy') -%>" id="copy-submit"/>
       <a href="#" onclick="return closeModalWindow()" id="copy-cancel"><%= h message('cancel') -%></a>
index 5b9f6b439844f2be0db78f98966eb0b2137b3622..9a26c12a220116b041d2ffad3a4bfb23addea3bb 100644 (file)
@@ -4,29 +4,9 @@
     <div class="modal-head">
       <h2>Edit Filter</h2>
     </div>
-    <div class="modal-body">
-      <% @filter.errors.each do |attr, msg| %>
-        <p class="error"><%= h "#{attr} #{msg}" -%></p>
-      <% end %>
-      <div class="modal-field">
-        <label for="name"><%= h message('name') -%> <em class="mandatory">*</em></label>
-        <input id="name" name="name" type="text" size="50" maxlength="100" value="<%= h @filter.name -%>" autofocus="autofocus"/>
-      </div>
-      <div class="modal-field">
-        <label for="description"><%= h message('description') -%></label>
-        <input id="description" name="description" type="text" size="50" maxlength="4000" value="<%= h @filter.description -%>"/>
-      </div>
-      <% if @filter.can_be_reassigned_by(current_user) %>
-        <div class="modal-field">
-          <label for="owner"><%= h message('owner') -%></label>
-          <%= user_select_tag('owner', :html_id => 'select-filter-owner', :selected_user => @filter.user) -%>
-        </div>
-      <% end %>
-      <div class="modal-field">
-        <label for="shared"><%= h message('measure_filter.shared_with_all_users') -%></label>
-        <input id="shared" name="shared" type="checkbox" value="true" <%= 'checked' if @filter.shared -%>/>
-      </div>
-    </div>
+
+    <%= render :partial => 'shared_form', :locals => {:display_owner => true} %>
+
     <div class="modal-foot">
       <input type="submit" value="<%= h message('save') -%>" id="save-submit"/>
       <a href="#" onclick="return closeModalWindow()" id="save-cancel"><%= h message('cancel') -%></a>
index 19a6be0ef974011edb7d2bee96b541155e3bb824..6bfdc3ae1c14c1e670f09a0e5a2f4681b948e2b4 100644 (file)
@@ -6,23 +6,8 @@
       <h2>Save Filter</h2>
     </div>
 
-    <div class="modal-body">
-      <% @filter.errors.each do |attr, msg| %>
-        <p class="error"><%= h msg -%></p>
-      <% end %>
-      <div class="modal-field">
-        <label for="name">Name <em class="mandatory">*</em></label>
-        <input id="name" name="name" type="text" size="50" maxlength="100" value="<%= h @filter.name -%>" autofocus="autofocus"/>
-      </div>
-      <div class="modal-field">
-        <label for="description">Description</label>
-        <input id="description" name="description" type="text" size="50" maxlength="4000" value="<%= h @filter.description -%>"/>
-      </div>
-      <div class="modal-field">
-        <label for="shared">Shared with all users</label>
-        <input id="shared" name="shared" type="checkbox" value="true" <%= 'checked' if @filter.shared -%>/>
-      </div>
-    </div>
+    <%= render :partial => 'shared_form' %>
+
     <div class="modal-foot">
       <input type="submit" value="<%= h message('save') -%>" id="save-as-submit"/>
       <a href="#" onclick="return closeModalWindow()" id="save-as-cancel"><%= h message('cancel') -%></a>
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/measures/_shared_form.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/measures/_shared_form.html.erb
new file mode 100644 (file)
index 0000000..0bd2807
--- /dev/null
@@ -0,0 +1,26 @@
+<% if !local_assigns.has_key? :display_owner
+  display_owner = false
+   end %>
+<div class="modal-body">
+  <% @filter.errors.full_messages.each do |msg| %>
+    <p class="error"><%= h msg -%></p>
+  <% end %>
+  <div class="modal-field">
+    <label for="name"><%= h message('name') -%> <em class="mandatory">*</em></label>
+    <input id="name" name="name" type="text" size="50" maxlength="100" value="<%= h @filter.name -%>" autofocus="autofocus"/>
+  </div>
+  <div class="modal-field">
+    <label for="description"><%= h message('description') -%></label>
+    <input id="description" name="description" type="text" size="50" maxlength="4000" value="<%= h @filter.description -%>"/>
+  </div>
+  <% if display_owner && @filter.can_be_reassigned_by(current_user) %>
+    <div class="modal-field">
+      <label for="owner"><%= h message('owner') -%></label>
+      <%= user_select_tag('owner', :html_id => 'select-filter-owner', :selected_user => @filter.user) -%>
+    </div>
+  <% end %>
+  <div class="modal-field">
+    <label for="shared"><%= h message('measure_filter.shared_with_all_users') -%></label>
+    <input id="shared" name="shared" type="checkbox" value="true" <%= 'checked' if @filter.shared -%>/>
+  </div>
+</div>
\ No newline at end of file
index cd6c87a504c364a5354d6d8239b4222a19cb1e58..2eaee024cdb41f5497cdf22211b8d1014c7fede3 100644 (file)
@@ -20,6 +20,8 @@
 
 package org.sonar.server.issue;
 
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
@@ -222,7 +224,7 @@ public class IssueFilterServiceTest {
   public void should_update() {
     when(issueFilterDao.selectById(1L)).thenReturn(new IssueFilterDto().setId(1L).setName("My Old Filter").setUserLogin("john"));
 
-    DefaultIssueFilter result = service.update(new DefaultIssueFilter().setId(1L).setName("My New Filter"), userSession);
+    DefaultIssueFilter result = service.update(new DefaultIssueFilter().setId(1L).setName("My New Filter").setUser("john"), userSession);
     assertThat(result.name()).isEqualTo("My New Filter");
 
     verify(issueFilterDao).update(any(IssueFilterDto.class));
@@ -270,10 +272,10 @@ public class IssueFilterServiceTest {
   @Test
   public void should_not_update_if_name_already_used() {
     when(issueFilterDao.selectById(1L)).thenReturn(new IssueFilterDto().setId(1L).setName("My Old Filter").setUserLogin("john"));
-    when(issueFilterDao.selectByNameAndUser(eq("My Issue"), eq("john"), eq(1L))).thenReturn(new IssueFilterDto());
+    when(issueFilterDao.selectByNameAndUser("My Issue", "john", 1L)).thenReturn(new IssueFilterDto());
 
     try {
-      service.update(new DefaultIssueFilter().setId(1L).setName("My Issue"), userSession);
+      service.update(new DefaultIssueFilter().setId(1L).setUser("john").setName("My Issue"), userSession);
       fail();
     } catch (Exception e) {
       assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Name already exists");
@@ -294,6 +296,40 @@ public class IssueFilterServiceTest {
 
     verify(issueFilterDao).update(any(IssueFilterDto.class));
   }
+  
+  @Test
+  public void should_change_shared_filter_ownership_when_admin() throws Exception {
+    String currentUser = "dave.loper";
+    IssueFilterDto sharedFilter = new IssueFilterDto().setId(1L).setName("My filter").setUserLogin("former.owner").setShared(true);
+    IssueFilterDto expectedDto = new IssueFilterDto().setName("My filter").setUserLogin("new.owner").setShared(true);
+
+    when(authorizationDao.selectGlobalPermissions(currentUser)).thenReturn(newArrayList(UserRole.ADMIN));
+    when(issueFilterDao.selectById(1L)).thenReturn(sharedFilter);
+
+    DefaultIssueFilter issueFilter = new DefaultIssueFilter().setId(1L).setName("My filter").setShared(true).setUser("new.owner");
+    service.update(issueFilter, new UserSession(1, currentUser, null));
+
+    verify(issueFilterDao).update(argThat(Matches.filter(expectedDto)));
+  }
+
+  @Test
+  public void should_not_change_own_filter_ownership() throws Exception {
+    String currentUser = "dave.loper";
+    IssueFilterDto sharedFilter = new IssueFilterDto().setId(1L).setName("My filter").setUserLogin(currentUser).setShared(true);
+
+    when(authorizationDao.selectGlobalPermissions(currentUser)).thenReturn(newArrayList(UserRole.USER));
+    when(issueFilterDao.selectById(1L)).thenReturn(sharedFilter);
+
+    try {
+      DefaultIssueFilter issueFilter = new DefaultIssueFilter().setId(1L).setName("My filter").setShared(true).setUser("new.owner");
+      service.update(issueFilter, new UserSession(1, currentUser, null));
+      fail();
+    } catch (Exception e) {
+      assertThat(e).isInstanceOf(IllegalStateException.class).hasMessage("User is not authorized to change the owner of this filter");
+    }
+
+    verify(issueFilterDao, never()).update(any(IssueFilterDto.class));
+  }
 
   @Test
   public void should_delete() {
@@ -490,4 +526,34 @@ public class IssueFilterServiceTest {
     verify(issueFilterFavouriteDao, never()).delete(anyLong());
   }
 
+  private static class Matches extends BaseMatcher<IssueFilterDto> {
+
+    private final IssueFilterDto referenceFilter;
+
+    private Matches(IssueFilterDto reference) {
+      referenceFilter = reference;
+    }
+
+    static Matches filter(IssueFilterDto filterDto) {
+      return new Matches(filterDto);
+    }
+
+    @Override
+    public boolean matches(Object o) {
+      if(o != null && o instanceof IssueFilterDto) {
+        IssueFilterDto otherFilter = (IssueFilterDto) o;
+        return referenceFilter.isShared() == otherFilter.isShared()
+          && referenceFilter.getUserLogin() == otherFilter.getUserLogin()
+          && referenceFilter.getDescription() == otherFilter.getDescription()
+          && referenceFilter.getName() == otherFilter.getName()
+          && referenceFilter.getData() == otherFilter.getData();
+      }
+      return false;
+    }
+
+    @Override
+    public void describeTo(Description description) {
+    }
+  }
+
 }