Yi EungJun 2014-03-19
Give all permissions to authors for some resources
Authors have all permissions for these resources:
* issues
* postings
* comments
@5f8294724bce989ae472c744393a577594e5e318
app/controllers/BoardApp.java
--- app/controllers/BoardApp.java
+++ app/controllers/BoardApp.java
@@ -3,6 +3,7 @@
 
 import actions.AnonymousCheckAction;
 import actions.DefaultProjectCheckAction;
+import actions.NullProjectCheckAction;
 
 import com.avaje.ebean.ExpressionList;
 import com.avaje.ebean.Page;
@@ -194,11 +195,14 @@
      * @param number 게시물number
      * @return
      */
-    @With(AnonymousCheckAction.class)
-    @IsAllowed(value = Operation.UPDATE, resourceType = ResourceType.BOARD_POST)
+    @With(NullProjectCheckAction.class)
     public static Result editPostForm(String owner, String projectName, Long number) {
         Project project = Project.findByOwnerAndProjectName(owner, projectName);
         Posting posting = Posting.findByNumber(project, number);
+
+        if (!AccessControl.isAllowed(UserApp.currentUser(), posting.asResource(), Operation.UPDATE)) {
+            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
+        }
 
         Form<Posting> editForm = new Form<>(Posting.class).fill(posting);
         boolean isAllowedToNotice = ProjectUser.isAllowedToNotice(UserApp.currentUser(), project);
@@ -220,7 +224,7 @@
      * @see AbstractPostingApp#editPosting(models.AbstractPosting, models.AbstractPosting, play.data.Form
      */
     @Transactional
-    @With(DefaultProjectCheckAction.class)
+    @With(NullProjectCheckAction.class)
     public static Result editPost(String userName, String projectName, Long number) {
         Form<Posting> postForm = new Form<>(Posting.class).bindFromRequest();
         Project project = ProjectApp.getProject(userName, projectName);
@@ -233,6 +237,7 @@
 
         final Posting post = postForm.get();
         final Posting original = Posting.findByNumber(project, number);
+
         Call redirectTo = routes.BoardApp.post(project.owner, project.name, number);
         Runnable updatePostingBeforeUpdate = new Runnable() {
             @Override
@@ -284,7 +289,7 @@
      */
     @Transactional
     @IsAllowed(value = Operation.READ, resourceType = ResourceType.BOARD_POST)
-    @IsCreatable(ResourceType.NONISSUE_COMMENT)
+    @With(NullProjectCheckAction.class)
     public static Result newComment(String owner, String projectName, Long number) throws IOException {
         Project project = Project.findByOwnerAndProjectName(owner, projectName);
         final Posting posting = Posting.findByNumber(project, number);
@@ -294,6 +299,11 @@
 
         if (commentForm.hasErrors()) {
             return badRequest(views.html.error.badrequest.render("error.validation", project));
+        }
+
+        if (!AccessControl.isResourceCreatable(
+                    UserApp.currentUser(), posting.asResource(), ResourceType.NONISSUE_COMMENT)) {
+            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
         }
 
         final PostingComment comment = commentForm.get();
@@ -321,7 +331,7 @@
      * @see controllers.AbstractPostingApp#delete(play.db.ebean.Model, models.resource.Resource, play.mvc.Call)
      */
     @Transactional
-    @With(DefaultProjectCheckAction.class)
+    @With(NullProjectCheckAction.class)
     public static Result deleteComment(String userName, String projectName, Long number, Long commentId) {
         Comment comment = PostingComment.find.byId(commentId);
         Project project = comment.asResource().getProject();
app/controllers/CodeHistoryApp.java
--- app/controllers/CodeHistoryApp.java
+++ app/controllers/CodeHistoryApp.java
@@ -1,6 +1,7 @@
 package controllers;
 
 import actions.DefaultProjectCheckAction;
+import actions.NullProjectCheckAction;
 import controllers.annotation.IsAllowed;
 import controllers.annotation.IsCreatable;
 import models.Attachment;
@@ -22,6 +23,7 @@
 import playRepository.FileDiff;
 import playRepository.PlayRepository;
 import playRepository.RepositoryService;
+import utils.AccessControl;
 import utils.ErrorViews;
 import utils.HttpUtil;
 import utils.PullRequestCommit;
@@ -176,7 +178,7 @@
         }
     }
 
-    @IsCreatable(ResourceType.COMMIT_COMMENT)
+    @With(NullProjectCheckAction.class)
     public static Result newComment(String ownerName, String projectName, String commitId)
             throws IOException, ServletException, SVNException {
         Form<CommitComment> codeCommentForm = new Form<>(CommitComment.class)
@@ -188,10 +190,17 @@
             return badRequest(ErrorViews.BadRequest.render("error.validation", project));
         }
 
-        if (RepositoryService.getRepository(project).getCommit(commitId) == null) {
+        Commit commit = RepositoryService.getRepository(project).getCommit(commitId);
+
+        if (commit == null) {
             return notFound(notfound.render("error.notfound", project, request().path()));
         }
 
+        if (!AccessControl.isResourceCreatable(
+                    UserApp.currentUser(), commit.asResource(project), ResourceType.COMMIT_COMMENT)) {
+            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
+        }
+
         CommitComment codeComment = codeCommentForm.get();
         codeComment.project = project;
         codeComment.commitId = commitId;
app/controllers/IssueApp.java
--- app/controllers/IssueApp.java
+++ app/controllers/IssueApp.java
@@ -1,6 +1,7 @@
 package controllers;
 
 import actions.DefaultProjectCheckAction;
+import actions.NullProjectCheckAction;
 import actions.AnonymousCheckAction;
 import com.avaje.ebean.ExpressionList;
 import com.avaje.ebean.Page;
@@ -278,7 +279,7 @@
      * @param number 이슈 번호
      * @return
      */
-    @With(DefaultProjectCheckAction.class)
+    @With(NullProjectCheckAction.class)
     public static Result issue(String ownerName, String projectName, Long number) {
         Project project = ProjectApp.getProject(ownerName, projectName);
 
@@ -380,7 +381,7 @@
      * @throws IOException
      */
     @Transactional
-    @With(DefaultProjectCheckAction.class)
+    @With(NullProjectCheckAction.class)
     public static Result massUpdate(String ownerName, String projectName) {
         Form<IssueMassUpdate> issueMassUpdateForm
                 = new Form<>(IssueMassUpdate.class).bindFromRequest();
@@ -560,11 +561,15 @@
      * @param number 이슈 번호
      * @return
      */
-    @With(AnonymousCheckAction.class)
-    @IsAllowed(resourceType = ResourceType.ISSUE_POST, value = Operation.UPDATE)
+    @With(NullProjectCheckAction.class)
     public static Result editIssueForm(String ownerName, String projectName, Long number) {
         Project project = ProjectApp.getProject(ownerName, projectName);
         Issue issue = Issue.findByNumber(project, number);
+
+        if (!AccessControl.isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE)) {
+            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
+        }
+
         Form<Issue> editForm = new Form<>(Issue.class).fill(issue);
 
         return ok(edit.render("title.editIssue", editForm, issue, project));
@@ -639,7 +644,7 @@
      * @throws IOException
      * @see {@link AbstractPostingApp#editPosting}
      */
-    @With(DefaultProjectCheckAction.class)
+    @With(NullProjectCheckAction.class)
     public static Result editIssue(String ownerName, String projectName, Long number) {
         Form<Issue> issueForm = new Form<>(Issue.class).bindFromRequest();
 
@@ -704,7 +709,7 @@
      * @ see {@link AbstractPostingApp#delete(play.db.ebean.Model, models.resource.Resource, Call)}
      */
     @Transactional
-    @IsAllowed(value = Operation.DELETE, resourceType = ResourceType.ISSUE_POST)
+    @With(NullProjectCheckAction.class)
     public static Result deleteIssue(String ownerName, String projectName, Long number) {
         Project project = ProjectApp.getProject(ownerName, projectName);
         Issue issue = Issue.findByNumber(project, number);
@@ -732,12 +737,17 @@
      * @see {@link AbstractPostingApp#newComment(models.Comment, play.data.Form}
      */
     @Transactional
-    @IsCreatable(ResourceType.ISSUE_COMMENT)
+    @With(NullProjectCheckAction.class)
     public static Result newComment(String ownerName, String projectName, Long number) throws IOException {
         Project project = Project.findByOwnerAndProjectName(ownerName, projectName);
         final Issue issue = Issue.findByNumber(project, number);
         Call redirectTo = routes.IssueApp.issue(project.owner, project.name, number);
         Form<IssueComment> commentForm = new Form<>(IssueComment.class).bindFromRequest();
+
+        if (!AccessControl.isResourceCreatable(
+                    UserApp.currentUser(), issue.asResource(), ResourceType.ISSUE_COMMENT)) {
+            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
+        }
 
         if (commentForm.hasErrors()) {
             return badRequest(ErrorViews.BadRequest.render("error.validation", project));
@@ -837,7 +847,7 @@
      * @see {@link AbstractPostingApp#delete(play.db.ebean.Model, models.resource.Resource, Call)}
      */
     @Transactional
-    @With(DefaultProjectCheckAction.class)
+    @With(NullProjectCheckAction.class)
     public static Result deleteComment(String ownerName, String projectName, Long issueNumber,
             Long commentId) {
         Comment comment = IssueComment.find.byId(commentId);
app/controllers/PullRequestCommentApp.java
--- app/controllers/PullRequestCommentApp.java
+++ app/controllers/PullRequestCommentApp.java
@@ -1,5 +1,6 @@
 package controllers;
 
+import actions.NullProjectCheckAction;
 import models.Attachment;
 import models.NotificationEvent;
 import models.PullRequest;
@@ -10,6 +11,7 @@
 import play.db.ebean.Transactional;
 import play.mvc.Controller;
 import play.mvc.Result;
+import play.mvc.With;
 import utils.AccessControl;
 import utils.Constants;
 import utils.ErrorViews;
@@ -26,7 +28,7 @@
 public class PullRequestCommentApp extends Controller {
 
     @Transactional
-    @IsCreatable(ResourceType.PULL_REQUEST_COMMENT)
+    @With(NullProjectCheckAction.class)
     public static Result newComment(String ownerName, String projectName, Long pullRequestId) throws IOException {
         PullRequest pullRequest = PullRequest.findById(pullRequestId);
 
@@ -34,6 +36,11 @@
             return notFound();
         }
 
+        if (!AccessControl.isResourceCreatable(
+                    UserApp.currentUser(), pullRequest.asResource(), ResourceType.PULL_REQUEST_COMMENT)) {
+            return forbidden(ErrorViews.Forbidden.render("error.forbidden", pullRequest.toProject));
+        }
+
         String referer = request().getHeader("Referer");
 
         Form<PullRequestComment> commentForm = new Form<>(PullRequestComment.class).bindFromRequest();
app/models/Issue.java
--- app/models/Issue.java
+++ app/models/Issue.java
@@ -293,8 +293,8 @@
             }
 
             @Override
-            public Long getAuthorId() {
-                return authorId;
+            public Resource getContainer() {
+                return Issue.this.asResource();
             }
         };
     }
app/models/resource/Resource.java
--- app/models/resource/Resource.java
+++ app/models/resource/Resource.java
@@ -140,6 +140,7 @@
     abstract public ResourceType getType();
     public Resource getContainer() { return null; }
     public Long getAuthorId() { return null; }
+    public boolean isAuthoredBy(User user) { return getAuthorId() != null && getAuthorId().equals(user.id); }
     public void delete() { throw new UnsupportedOperationException(); }
 
     /**
app/playRepository/Commit.java
--- app/playRepository/Commit.java
+++ app/playRepository/Commit.java
@@ -86,6 +86,17 @@
             public ResourceType getType() {
                 return ResourceType.COMMIT;
             }
+
+            @Override
+            public Long getAuthorId() {
+                User author = getAuthor();
+
+                if (author != null) {
+                    return getAuthor().id;
+                } else {
+                    return null;
+                }
+            }
         };
     }
 }
app/utils/AccessControl.java
--- app/utils/AccessControl.java
+++ app/utils/AccessControl.java
@@ -1,6 +1,5 @@
 package utils;
 
-import controllers.UserApp;
 import models.Project;
 import models.ProjectUser;
 import models.User;
@@ -30,6 +29,9 @@
      *
      * 자신이 프로젝트 멤버일 경우에는 프로젝트에 속하는 모든 리소스에 대한 생성권한을 갖고
      * 로그인 유저일 경우에는 이슈와 게시물에 한해서만 생성할 수 있다.
+     *
+     * 주의: 어떤 리소스의 저자이기 때문에 그 리소스에 속한 리소스를 생성할 수 있는지에 대한
+     * 여부는 검사하지 않는다.
      *
      * @param user
      * @param project
@@ -69,6 +71,21 @@
             }
 
             return false;
+        }
+    }
+
+    public static boolean isResourceCreatable(User user, Resource container, ResourceType resourceType) {
+        if (isAllowedIfAuthor(user, container)) {
+            return true;
+        }
+
+        Project project = (container.getType() == ResourceType.PROJECT) ?
+            Project.find.byId(Long.valueOf(container.getId())) : container.getProject();
+
+        if (project == null) {
+            return isGlobalResourceCreatable(user);
+        } else {
+            return isProjectResourceCreatable(user, project, resourceType);
         }
     }
 
@@ -127,7 +144,7 @@
         case PROJECT:
             return ProjectUser.isManager(user.id, Long.valueOf(resource.getId()));
         case PULL_REQUEST_COMMENT:
-            return user.isSiteManager() || isEditableAsAuthor(user, resource);
+            return user.isSiteManager();
         default:
             // undefined
             return false;
@@ -147,26 +164,31 @@
      * @return
      */
     private static boolean isProjectResourceAllowed(User user, Project project, Resource resource, Operation operation) {
-        if (user.isSiteManager() || ProjectUser.isManager(user.id, project.id)) {
+        if (user.isSiteManager()
+                || ProjectUser.isManager(user.id, project.id)
+                || isAllowedIfAuthor(user, resource)) {
             return true;
         }
 
-        // If the resource is an attachment, the permission depends on its container.
-        if (resource.getType() == ResourceType.ATTACHMENT) {
-            switch(operation) {
-                case READ:
-                    return isAllowed(user, resource.getContainer(), Operation.READ);
-                case UPDATE:
-                case DELETE:
-                    return isAllowed(user, resource.getContainer(), Operation.UPDATE);
-            }
+        // Some resource's permission depends on their container.
+        switch(resource.getType()) {
+            case ISSUE_STATE:
+            case ISSUE_ASSIGNEE:
+            case ISSUE_MILESTONE:
+            case ATTACHMENT:
+                switch(operation) {
+                    case READ:
+                        return isAllowed(user, resource.getContainer(), Operation.READ);
+                    case UPDATE:
+                    case DELETE:
+                        return isAllowed(user, resource.getContainer(), Operation.UPDATE);
+                }
         }
 
         // Access Control for members, nonmembers and anonymous.
         // - Anyone can read public project's resource.
         // - Members can update anything and delete anything except code repository.
         // - Nonmember can update or delete a resource if only
-        //     * the user is the author of the resource,
         //     * the resource is not a code repository,
         //     * and the project to which the resource belongs is public.
         // See docs/technical/access-control.md for more information.
@@ -176,28 +198,25 @@
         case UPDATE:
             if (ProjectUser.isMember(user.id, project.id)) {
                 return true;
-            }
-
-            if (resource.getType() == ResourceType.CODE) {
-                // Nonmember cannot update the repository.
-                return false;
             } else {
-                return project.isPublic && isEditableAsAuthor(user, resource);
+                return false;
             }
         case DELETE:
             if (resource.getType() == ResourceType.CODE) {
                 return false;
             } else {
-                return ProjectUser.isMember(user.id, project.id) ||
-                        (project.isPublic && isEditableAsAuthor(user, resource));
+                return ProjectUser.isMember(user.id, project.id);
             }
         case ACCEPT:
-            return ProjectUser.isMember(user.id, project.id);
         case CLOSE:
         case REOPEN:
-            return ProjectUser.isMember(user.id, project.id) || isEditableAsAuthor(user, resource);
+            return ProjectUser.isMember(user.id, project.id);
         case WATCH:
-            return project.isPublic ? !user.isAnonymous() : ProjectUser.isMember(user.id, project.id);
+            if (project.isPublic) {
+                return !user.isAnonymous();
+            } else {
+                return ProjectUser.isMember(user.id, project.id);
+            }
         default:
             // undefined
             return false;
@@ -235,28 +254,29 @@
     }
 
     /**
-     * {@code user}가 {@code project}의 {@code resource}에 대해 저자로서의
-     * 수정 권한을 갖는지의 여부를 반환한다.
+     * {@code user}가 {@code resource}에 대해 저자로서의 읽기, 수정,
+     * 삭제 권한을 갖는지의 여부를 반환한다.
      *
-     * 현재는 이슈 및 게시물과 그것들의 댓글에 대해서만 동작한다.
+     * 다음의 두 조건이 모두 참인 경우에만 참을 반환한다.
+     * - {@code resource}가 저자에게 읽기, 수정, 삭제 권한을
+     *   부여하는 리소스인가
+     * - {@code user}가 저자인가
      *
      * @param user
      * @param resource
-     * @return {@code user}가 {@code project}의 {@code resource}에
-     *         대해 저자로서의 수정 권한을 갖는지의 여부를 반환한다.
+     * @return {@code user}가 {@code resource}에 대해 저자로서의
+    *          읽기, 수정, 삭제 권한을 갖는지의 여부
      */
-    private static boolean isEditableAsAuthor(User user, Resource resource) {
+    private static boolean isAllowedIfAuthor(User user, Resource resource) {
         switch (resource.getType()) {
         case ISSUE_POST:
-        case ISSUE_STATE:
-        case ISSUE_ASSIGNEE:
         case ISSUE_COMMENT:
         case NONISSUE_COMMENT:
         case BOARD_POST:
         case COMMIT_COMMENT:
         case PULL_REQUEST:
         case PULL_REQUEST_COMMENT:
-            return resource.getAuthorId().equals(user.id);
+            return resource.isAuthoredBy(user);
         default:
             return false;
         }
app/views/board/view.scala.html
--- app/views/board/view.scala.html
+++ app/views/board/view.scala.html
@@ -102,7 +102,7 @@
     	    }
     		</ul>
 
-            @common.commentForm(project, ResourceType.NONISSUE_COMMENT, routes.BoardApp.newComment(project.owner, project.name, post.getNumber).toString())
+            @common.commentForm(post.asResource(), ResourceType.NONISSUE_COMMENT, routes.BoardApp.newComment(project.owner, project.name, post.getNumber).toString())
     	</div>
 
     	@help.keymap("boardDetail", project)
app/views/code/diff.scala.html
--- app/views/code/diff.scala.html
+++ app/views/code/diff.scala.html
@@ -114,7 +114,7 @@
                 }
                 }
 
-                @common.commentForm(project, ResourceType.COMMIT_COMMENT, routes.CodeHistoryApp.newComment(project.owner, project.name, commit.getId).toString())
+                @common.commentForm(commit.asResource(project), ResourceType.COMMIT_COMMENT, routes.CodeHistoryApp.newComment(project.owner, project.name, commit.getId).toString())
             </div>
             @** // Comment **@
         </div>
app/views/code/svnDiff.scala.html
--- app/views/code/svnDiff.scala.html
+++ app/views/code/svnDiff.scala.html
@@ -146,7 +146,7 @@
                 </ul>
                 }
 
-                @common.commentForm(project, ResourceType.COMMIT_COMMENT, routes.CodeHistoryApp.newComment(project.owner, project.name, commit.getId).toString())
+                @common.commentForm(commit.asResource(project), ResourceType.COMMIT_COMMENT, routes.CodeHistoryApp.newComment(project.owner, project.name, commit.getId).toString())
             </div>
             @** // Comment **@
         </div>
app/views/common/commentForm.scala.html
--- app/views/common/commentForm.scala.html
+++ app/views/common/commentForm.scala.html
@@ -1,9 +1,9 @@
-@(project:Project, resourceType:ResourceType, action:String)
+@(container: models.resource.Resource, resourceType:ResourceType, action:String)
 
 @import models.enumeration.ResourceType
 @import utils.AccessControl._
 
-@if(isProjectResourceCreatable(User.findByLoginId(session.get("loginId")), project, resourceType)){
+@if(isResourceCreatable(User.findByLoginId(session.get("loginId")), container, resourceType)){
 
     <form id="comment-form" action="@action" method="post" enctype="multipart/form-data">
         <div class="write-comment-box">
app/views/git/diff.scala.html
--- app/views/git/diff.scala.html
+++ app/views/git/diff.scala.html
@@ -121,7 +121,7 @@
                 }
                 }
 
-                @common.commentForm(pull.fromProject, ResourceType.COMMIT_COMMENT, routes.CodeHistoryApp.newComment(pull.fromProject.owner, pull.fromProject.name, commit.getId).toString())
+                @common.commentForm(pull.asResource(), ResourceType.COMMIT_COMMENT, routes.CodeHistoryApp.newComment(pull.fromProject.owner, pull.fromProject.name, commit.getId).toString())
             </div>
             @** // Comment **@
         </div>
app/views/git/view.scala.html
--- app/views/git/view.scala.html
+++ app/views/git/view.scala.html
@@ -93,7 +93,7 @@
                 @renderCommentsOnPullRequest(pull, Html(""), pull.getTimelineComments.toList)
                 </ul>
                 }
-                @common.commentForm(project, ResourceType.PULL_REQUEST_COMMENT, routes.PullRequestCommentApp.newComment(project.owner, project.name, pull.id).toString())
+                @common.commentForm(pull.asResource(), ResourceType.PULL_REQUEST_COMMENT, routes.PullRequestCommentApp.newComment(project.owner, project.name, pull.id).toString())
             </div>
         </div>
     </div>
app/views/git/viewChanges.scala.html
--- app/views/git/viewChanges.scala.html
+++ app/views/git/viewChanges.scala.html
@@ -29,7 +29,7 @@
             </div>
         </div>
         <div style="display:none">
-        @common.commentForm(project, ResourceType.PULL_REQUEST_COMMENT, routes.PullRequestCommentApp.newComment(project.owner, project.name, pull.id).toString())
+        @common.commentForm(pull.asResource(), ResourceType.PULL_REQUEST_COMMENT, routes.PullRequestCommentApp.newComment(project.owner, project.name, pull.id).toString())
         </div>
 
         <div id="minimap" class="minimap-outer">
app/views/issue/view.scala.html
--- app/views/issue/view.scala.html
+++ app/views/issue/view.scala.html
@@ -194,8 +194,9 @@
                         @**<!-- voters -->**@
                         <dl>
                             <dt>@Messages("issue.vote")</dt>
+                            <dd>
                             <dd style="padding:5px 10px;">
-                                @if(isProjectResourceCreatable(User.findByLoginId(session.get("loginId")), project, ResourceType.ISSUE_COMMENT)) {
+                                @if(isResourceCreatable(User.findByLoginId(session.get("loginId")), issue.asResource(), ResourceType.ISSUE_COMMENT)) {
                                     @if(issue.isVotedBy(UserApp.currentUser())) {
                                         <a href="@routes.VoteApp.unvote(project.owner, project.name, issue.getNumber)" data-request-method="post" class="ybtn ybtn-success"  data-toggle="tooltip" data-placement="right" data-original-title="@Messages("issue.unvote.description")">+@issue.voters.size</a>
                                     } else {
@@ -255,7 +256,7 @@
                 @partial_comments(project, issue)
                 </div>
             </div>
-            @common.commentForm(project, ResourceType.ISSUE_COMMENT, routes.IssueApp.newComment(project.owner, project.name, issue.getNumber).toString())
+            @common.commentForm(issue.asResource(), ResourceType.ISSUE_COMMENT, routes.IssueApp.newComment(project.owner, project.name, issue.getNumber).toString())
         </div>
         @** // Comment **@
 
docs/technical/access-control.md
--- docs/technical/access-control.md
+++ docs/technical/access-control.md
@@ -1,24 +1,32 @@
+공통
+====
+
+* 사이트 관리자: 모든 권한
+* 저자: 자신이 만든 이슈, 게시물, 댓글에 대한 모든 권한
+    * 다만 자신이 만든 이슈나 게시물에 다른 사람이 댓글을 단 경우, 그 댓글에 대한 수정/삭제 권한은 갖지 않는다.
+
 공개 프로젝트
 =============
+
+* 프로젝트 관리자: 자신의 관리하는 프로젝트에 대한 모든 권한
 
 코드
 ----
 
-* 사이트 관리자, 프로젝트 관리자: 모든 권한
 * 멤버: 저장소 삭제(이 기능은 아직 없다)를 제외한 모든 권한
 * 그 외의 모든 사용자: 모든 읽기 권한
 
 이슈, 게시판
 ------------
 
-* 사이트 관리자, 프로젝트 관리자, 멤버: 모든 권한
-* 로그인 사용자: 모든 읽기 권한, 게시물/댓글 등록, 자신이 등록한 게시물 수정/삭제, 자신이 등록한 댓글 삭제
+* 멤버: 모든 권한
+* 로그인 사용자: 모든 읽기 권한, 게시물/댓글 등록
 * 비로그인 사용자: 모든 읽기 권한
 
 마일스톤
 --------
 
-* 사이트 관리자, 프로젝트 관리자, 멤버: 모든 권한
+* 멤버: 모든 권한
 * 그 외의 모든 사용자: 모든 읽기 권한
 
 프로젝트 관리
test/controllers/IssueAppTest.java
--- test/controllers/IssueAppTest.java
+++ test/controllers/IssueAppTest.java
@@ -29,6 +29,7 @@
 
     private String projectOwner = "yobi";
     private String projectName = "projectYobi";
+    private Project project;
 
     @BeforeClass
     public static void beforeClass() {
@@ -42,7 +43,9 @@
         app = support.Helpers.makeTestApplication();
         Helpers.start(app);
 
-        Project project = Project.findByOwnerAndProjectName(projectOwner, projectName);
+        project = Project.findByOwnerAndProjectName(projectOwner, projectName);
+        project.setIsPublic(false);
+
         admin = User.findByLoginId("admin");
         manager = User.findByLoginId("yobi");
         member = User.findByLoginId("laziel");
@@ -59,10 +62,10 @@
 
         assertThat(this.admin.isSiteManager()).describedAs("admin is Site Admin.").isTrue();
         assertThat(ProjectUser.isManager(manager.id, project.id)).describedAs("manager is a manager").isTrue();
-        assertThat(ProjectUser.isManager(member.id, project.id)).describedAs("member is not a manager").isFalse();
+        assertThat(ProjectUser.isManager(member.id, project.id)).describedAs("member is a manager").isFalse();
         assertThat(ProjectUser.isMember(member.id, project.id)).describedAs("member is a member").isTrue();
-        assertThat(ProjectUser.isMember(author.id, project.id)).describedAs("author is not a member").isFalse();
-        assertThat(project.isPublic).isTrue();
+        assertThat(ProjectUser.isMember(author.id, project.id)).describedAs("author is a member").isFalse();
+        assertThat(project.isPublic).describedAs("project is public").isFalse();
     }
 
     @After
@@ -127,6 +130,10 @@
 
     @Test
     public void editByNonmember() {
+        // Given
+        project.setIsPublic(true);
+        project.update();
+
         // When
         Result result = editBy(nonmember);
 
@@ -174,6 +181,10 @@
 
     @Test
     public void deleteByNonmember() {
+        // Given
+        project.setIsPublic(true);
+        project.update();
+
         // When
         Result result = deleteBy(nonmember);
 
@@ -221,6 +232,10 @@
 
     @Test
     public void postByAnonymous() {
+        // Given
+        project.setIsPublic(true);
+        project.update();
+
         // When
         Result result = postBy(anonymous);
 
@@ -230,6 +245,10 @@
 
     @Test
     public void postByNonmember() {
+        // Given
+        project.setIsPublic(true);
+        project.update();
+
         // When
         Result result = postBy(nonmember);
 
@@ -266,6 +285,10 @@
 
     @Test
     public void commentByAnonymous() {
+        // Given
+        project.setIsPublic(true);
+        project.update();
+
         // When
         Result result = commentBy(anonymous);
 
@@ -275,6 +298,10 @@
 
     @Test
     public void commentByNonmember() {
+        // Given
+        project.setIsPublic(true);
+        project.update();
+
         // When
         Result result = commentBy(nonmember);
 
@@ -320,6 +347,8 @@
     public void watch() {
         // Given
         Resource resource = issue.asResource();
+        project.setIsPublic(true);
+        project.update();
 
         // When
         Result result = callAction(
@@ -336,7 +365,46 @@
     }
 
     @Test
+    public void watchByAuthor() {
+        // Given
+        Resource resource = issue.asResource();
+
+        // When
+        Result result = callAction(
+                controllers.routes.ref.WatchApp.watch(resource.asParameter()),
+                fakeRequest()
+                        .withSession(UserApp.SESSION_USERID, author.id.toString())
+        );
+
+        // Then
+        issue.refresh();
+        assertThat(status(result)).isEqualTo(OK);
+    }
+
+    @Test
     public void unwatch() {
+        // Given
+        Resource resource = issue.asResource();
+        project.setIsPublic(true);
+        project.update();
+
+
+        // When
+        Result result = callAction(
+                controllers.routes.ref.WatchApp.unwatch(resource.asParameter()),
+                fakeRequest()
+                        .withSession(UserApp.SESSION_USERID, nonmember.id.toString())
+        );
+
+        // Then
+        issue.refresh();
+        assertThat(status(result)).isEqualTo(OK);
+        assertThat(issue.getWatchers().contains(nonmember))
+            .describedAs("A user becomes a unwatcher if the user explictly choose not to watch the issue.").isFalse();
+    }
+
+    @Test
+    public void unwatchByAuthor() {
         // Given
         Resource resource = issue.asResource();
 
@@ -350,7 +418,5 @@
         // Then
         issue.refresh();
         assertThat(status(result)).isEqualTo(OK);
-        assertThat(issue.getWatchers().contains(author))
-            .describedAs("A user becomes a unwatcher if the user explictly choose not to watch the issue.").isFalse();
     }
 }
Add a comment
List