whiteship 2014-01-14
Refactoring: added IsAllowed annotation
@f964efdb043a04735a73ef4fe450a593d6f971c9
 
app/actions/IsAllowedAction.java (added)
+++ app/actions/IsAllowedAction.java
@@ -0,0 +1,80 @@
+/**
+ * Yobi, Project Hosting SW
+ *
+ * Copyright 2013 NAVER Corp.
+ * http://yobi.io
+ *
+ * @Author Keesun Baik
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package actions;
+
+import actions.support.PathParser;
+import controllers.UserApp;
+import controllers.annotation.IsAllowed;
+import models.Project;
+import models.enumeration.Operation;
+import models.enumeration.ResourceType;
+import models.resource.Resource;
+import models.resource.ResourceConvertible;
+import play.mvc.Action;
+import play.mvc.Http;
+import play.mvc.Result;
+import utils.AccessControl;
+import utils.AccessLogger;
+import utils.ErrorViews;
+
+/**
+ * 1. 프로젝트가 존재하는지 확인한다.
+ * 2. 특정 타입의 리소스가 존재하는지 확인한다.
+ * 3. 특정 타입의 리소스에 권한이 있는지 확인한다.
+ *
+ * 자세한 설명은 {@link controllers.annotation.IsAllowed} 애노테이션 참고.
+ *
+ * @author Keesun Baik
+ * @see {@link controllers.annotation.IsAllowed}
+ */
+public class IsAllowedAction extends Action<IsAllowed> {
+
+    @Override
+    public Result call(Http.Context context) throws Throwable {
+        PathParser parser = new PathParser(context);
+        String ownerLoginId = parser.getOwnerLoginId();
+        String projectName = parser.getProjectName();
+
+        Project project = Project.findByOwnerAndProjectName(ownerLoginId, projectName);
+
+        if (project == null) {
+            return AccessLogger.log(context.request(),
+                    notFound(ErrorViews.NotFound.render("error.notfound.project")), null);
+        }
+
+        ResourceType resourceType = this.configuration.resourceType();
+        ResourceConvertible resourceObject = Resource.getResourceObject(parser, project, resourceType);
+        Operation operation = this.configuration.value();
+
+        if(resourceObject == null) {
+            return AccessLogger.log(context.request(),
+                    notFound(ErrorViews.NotFound.render("error.notfound", project, resourceType.resource())) , null);
+        }
+
+        if(!AccessControl.isAllowed(UserApp.currentUser(), resourceObject.asResource(), operation)) {
+            return AccessLogger.log(context.request(),
+                    forbidden(ErrorViews.Forbidden.render("error.forbidden", project)), null);
+        }
+
+        return this.delegate.call(context);
+    }
+
+}
app/actions/IsCreatableAction.java
--- app/actions/IsCreatableAction.java
+++ app/actions/IsCreatableAction.java
@@ -33,30 +33,24 @@
 import controllers.annotation.IsCreatable;
 
 /**
- * 해당 프로젝트가 존재하는지 체크하고 사용자가 resource type별 생성권한이 있는지 체크한다.
+ * 1. 프로젝트가 존재하는지 확인한다.
+ * 2. 프로젝트에 특정 타입의 리소스를 생성할 수 있는지 확인한다.
  *
- * @author Wansoon Park, Keesun Beak
- *
+ * @author Wansoon Park, Keesun Baik
+ * @see {@link controllers.annotation.IsCreatable}
  */
 public class IsCreatableAction extends Action<IsCreatable> {
 
     @Override
     public Result call(Context context) throws Throwable {
-        String path = context._requestHeader().path();
-
-        play.Configuration config = play.Configuration.root();
-        String contextPath = config.getString("application.context");
-
-        PathParser parser = new PathParser(contextPath, path);
-
+        PathParser parser = new PathParser(context);
         String ownerLoginId = parser.getOwnerLoginId();
         String projectName = parser.getProjectName();
 
         Project project = Project.findByOwnerAndProjectName(ownerLoginId, projectName);
 
         if (project == null) {
-            return AccessLogger.log(context.request()
-                    , notFound(ErrorViews.NotFound.render("No project matches given parameters '" + ownerLoginId + "' and project_name '" + projectName + "'"))
+            return AccessLogger.log(context.request(), notFound(ErrorViews.NotFound.render("error.notfound.project"))
                     , null);
         }
 
app/actions/IsOnlyGitAvailableAction.java (Renamed from app/actions/ProjectCheckAction.java)
--- app/actions/ProjectCheckAction.java
+++ app/actions/IsOnlyGitAvailableAction.java
@@ -4,7 +4,7 @@
  * Copyright 2013 NAVER Corp.
  * http://yobi.io
  *
- * @Author Wansoon Park, Keesun Baek
+ * @Author Keesun Baik
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -20,27 +20,25 @@
  */
 package actions;
 
+import actions.support.PathParser;
+import controllers.annotation.IsOnlyGitAvailable;
 import models.Project;
 import play.mvc.Action;
-import play.mvc.Http.Context;
+import play.mvc.Http;
 import play.mvc.Result;
-import utils.AccessControl;
 import utils.AccessLogger;
 import utils.ErrorViews;
-import actions.support.PathParser;
-import controllers.UserApp;
-import controllers.annotation.ProjectAccess;
 
 /**
- * 해당 프로젝트가 존재하는지 체크하고 사용자가 resource에 대한 operation 권한이 있는지 확인한다.
+ * 1. 프로젝트가 존재하는지 확인한다.
+ * 2. 프로젝트가 Git 프로젝트인지 확인한다.
  *
- * @author Wansoon Park, Keesun Beak
- *
+ * @author Keesun Baik
  */
-public class ProjectCheckAction extends Action<ProjectAccess> {
+public class IsOnlyGitAvailableAction extends Action<IsOnlyGitAvailable> {
 
     @Override
-    public Result call(Context context) throws Throwable {
+    public Result call(Http.Context context) throws Throwable {
         PathParser parser = new PathParser(context);
         String ownerLoginId = parser.getOwnerLoginId();
         String projectName = parser.getProjectName();
@@ -52,17 +50,11 @@
                     , null);
         }
 
-        if (!AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), this.configuration.value())) {
-            return AccessLogger.log(context.request()
-                    , forbidden(ErrorViews.Forbidden.render("error.forbidden", project))
-                    , null);
-        }
-
-        boolean isGitOnly = this.configuration.isGitOnly();
-        if(isGitOnly && !project.isGit()) {
-            return AccessLogger.log(context.request(), badRequest(ErrorViews.BadRequest.render()), null);
+        if(!project.isGit()) {
+            return AccessLogger.log(context.request(), badRequest(ErrorViews.BadRequest.render("error.badrequest.only.available.for.git")), null);
         }
 
         return this.delegate.call(context);
     }
+
 }
 
app/actions/PullRequestCheckAction.java (deleted)
--- app/actions/PullRequestCheckAction.java
@@ -1,74 +0,0 @@
-/**
- * Yobi, Project Hosting SW
- *
- * Copyright 2013 NAVER Corp.
- * http://yobi.io
- *
- * @Author Wansoon Park, Keesun Baek
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package actions;
-
-import models.Project;
-import models.PullRequest;
-import play.mvc.Action;
-import play.mvc.Http.Context;
-import play.mvc.Result;
-import utils.AccessControl;
-import utils.AccessLogger;
-import utils.ErrorViews;
-import actions.support.PathParser;
-import controllers.UserApp;
-import controllers.annotation.PullRequestAccess;
-
-/**
- * 해당 pullrequest가 존재하는지 체크하고 사용자가 pullrequest resource에 대한 operation 권한이 있는지 확인한다.
- *
- * @author Wansoon Park, Keesun Beak
- *
- */
-public class PullRequestCheckAction extends Action<PullRequestAccess> {
-
-    @Override
-    public Result call(Context context) throws Throwable {
-        String path = context._requestHeader().path();
-
-        play.Configuration config = play.Configuration.root();
-        String contextPath = config.getString("application.context");
-
-        PathParser parser = new PathParser(contextPath, path);
-
-        String ownerLoginId = parser.getOwnerLoginId();
-        String projectName = parser.getProjectName();
-        int pullRequestNumber = parser.getPullRequestNumber();
-
-        Project project = Project.findByOwnerAndProjectName(ownerLoginId, projectName);
-        PullRequest pullRequest = PullRequest.findOne(project, pullRequestNumber);
-
-        if (pullRequest == null) {
-            return AccessLogger.log(context.request()
-                    , notFound(ErrorViews.NotFound.render("No pullrequest matches given parameter '" + pullRequestNumber + "'", project))
-                    , null);
-        }
-
-        if (!AccessControl.isAllowed(UserApp.currentUser(), pullRequest.asResource(), this.configuration.value())) {
-            return AccessLogger.log(context.request()
-                    , forbidden(ErrorViews.Forbidden.render("error.forbidden", project))
-                    , null);
-        }
-
-        return this.delegate.call(context);
-    }
-
-}
app/actions/support/PathParser.java
--- app/actions/support/PathParser.java
+++ app/actions/support/PathParser.java
@@ -25,20 +25,21 @@
 
 /**
  * 프로젝트 관련 URL을 파싱한다.
- * /{user.loginId}/{project.name}/** 패턴에 해당하는 URL에서 프로젝트 owner와 name 정보를 축출한다.
+ * /{user.loginId}/{project.name}/** 패턴에 해당하는 URL에서 필요한 정보를 축출한다.
+ *
+ * @author Wansoon Park, Keesun Baik
  */
 public class PathParser {
-    private static final String DELIMETER = "/";
-    private String[] paths;
+    private static final String DELIM = "/";
+    private String[] pathSegments;
 
     public PathParser(String path) {
-        this.paths = StringUtils.split(path, DELIMETER);
+        this.pathSegments = StringUtils.split(path, DELIM);
     }
 
     public PathParser(String contextPath, String path) {
-        String delimRemovedPath = StringUtils.removeEnd(contextPath, DELIMETER);
-        String contextRemovedPath = StringUtils.removeStart(path, delimRemovedPath);
-        this.paths = StringUtils.split(contextRemovedPath, DELIMETER);
+        String contextRemovedPath = StringUtils.removeStart(path, contextPath);
+        this.pathSegments = StringUtils.split(contextRemovedPath, DELIM);
     }
 
     public PathParser(Http.Context context) {
@@ -46,17 +47,19 @@
     }
 
     public String getOwnerLoginId() {
-        return this.paths[0];
+        return this.pathSegments[0];
     }
 
     public String getProjectName() {
-        return this.paths[1];
+        return this.pathSegments[1];
     }
 
-    public int getPullRequestNumber() {
-        return Integer.parseInt(this.paths[3]);
+    public String getPathSegment(int index) {
+        return pathSegments[index];
     }
+
     public String toString() {
-        return DELIMETER + StringUtils.join(this.paths, DELIMETER);
+        return DELIM + StringUtils.join(this.pathSegments, DELIM);
     }
+
 }
app/controllers/AbstractPostingApp.java
--- app/controllers/AbstractPostingApp.java
+++ app/controllers/AbstractPostingApp.java
@@ -288,21 +288,4 @@
         return redirect(redirectTo);
     }
 
-    /**
-     * 새 게시물 또는 이슈 생성 권한이 있는지 확인하고 {@code content}를 보여준다.
-     *
-     * when: 게시물이나 이슈 생성할 때 사용한다.
-     *
-     * @param project
-     * @param resourceType
-     * @param content
-     * @return
-     */
-    public static Result newPostingForm(Project project, ResourceType resourceType, Content content) {
-        if (!AccessControl.isProjectResourceCreatable(UserApp.currentUser(), project, resourceType)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
-
-        return ok(content);
-    }
 }
app/controllers/BoardApp.java
--- app/controllers/BoardApp.java
+++ app/controllers/BoardApp.java
@@ -4,6 +4,8 @@
 import actions.NullProjectCheckAction;
 import com.avaje.ebean.ExpressionList;
 import com.avaje.ebean.Page;
+import controllers.annotation.IsAllowed;
+import controllers.annotation.IsCreatable;
 import models.*;
 import models.enumeration.Operation;
 import models.enumeration.ResourceType;
@@ -57,13 +59,9 @@
      * @param pageNum 페이지 번호
      * @return
      */
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(value = Operation.READ, resourceType = ResourceType.PROJECT)
     public static Result posts(String userName, String projectName, int pageNum) {
         Project project = ProjectApp.getProject(userName, projectName);
-
-        if (!AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.READ)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
 
         Form<SearchCondition> postParamForm = new Form<>(SearchCondition.class);
         SearchCondition searchCondition = postParamForm.bindFromRequest().get();
@@ -90,14 +88,14 @@
      * @param projectName 프로젝트 이름
      * @return
      */
-    @With(NullProjectCheckAction.class)
+    @IsCreatable(ResourceType.BOARD_POST)
     public static Result newPostForm(String userName, String projectName) {
         Project project = ProjectApp.getProject(userName, projectName);
 
-        boolean isAllowedToNotice = ProjectUser.isAllowedToNotice(UserApp.currentUser(), project);
+        boolean isAllowedToNotice =
+                AccessControl.isProjectResourceCreatable(UserApp.currentUser(), project, ResourceType.BOARD_NOTICE);
 
-        return newPostingForm(project, ResourceType.BOARD_POST,
-                create.render("post.new", new Form<>(Posting.class), project, isAllowedToNotice));
+        return ok(create.render("post.new", new Form<>(Posting.class), project, isAllowedToNotice));
     }
 
     /**
@@ -112,17 +110,14 @@
      * @return
      */
     @Transactional
-    @With(NullProjectCheckAction.class)
+    @IsCreatable(ResourceType.BOARD_POST)
     public static Result newPost(String userName, String projectName) {
         Form<Posting> postForm = new Form<>(Posting.class).bindFromRequest();
         Project project = ProjectApp.getProject(userName, projectName);
 
-        if (!AccessControl.isProjectResourceCreatable(UserApp.currentUser(), project, ResourceType.BOARD_POST)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
-
         if (postForm.hasErrors()) {
-            boolean isAllowedToNotice = ProjectUser.isAllowedToNotice(UserApp.currentUser(), project);
+            boolean isAllowedToNotice =
+                    AccessControl.isProjectResourceCreatable(UserApp.currentUser(), project, ResourceType.BOARD_NOTICE);
             return badRequest(create.render("error.validation", postForm, project, isAllowedToNotice));
         }
 
@@ -162,18 +157,10 @@
      * @param number 게시물number
      * @return
      */
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(value = Operation.READ, resourceType = ResourceType.BOARD_POST)
     public static Result post(String userName, String projectName, Long number) {
         Project project = Project.findByOwnerAndProjectName(userName, projectName);
-
         Posting post = Posting.findByNumber(project, number);
-        if (post == null) {
-            return notFound(ErrorViews.NotFound.render("error.notfound", project, "post"));
-        }
-
-        if (!AccessControl.isAllowed(UserApp.currentUser(), post.asResource(), Operation.READ)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
 
         if(request().getHeader("Accept").contains("application/json")) {
             ObjectNode json = Json.newObject();
@@ -200,18 +187,10 @@
      * @param number 게시물number
      * @return
      */
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(value = Operation.UPDATE, resourceType = ResourceType.BOARD_POST)
     public static Result editPostForm(String owner, String projectName, Long number) {
         Project project = Project.findByOwnerAndProjectName(owner, projectName);
-
         Posting posting = Posting.findByNumber(project, number);
-        if (posting == null) {
-            return notFound(ErrorViews.NotFound.render("error.notfound", project, "post"));
-        }
-
-        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);
@@ -239,7 +218,8 @@
         Project project = ProjectApp.getProject(userName, projectName);
 
         if (postForm.hasErrors()) {
-            boolean isAllowedToNotice = ProjectUser.isAllowedToNotice(UserApp.currentUser(), project);
+            boolean isAllowedToNotice =
+                    AccessControl.isProjectResourceCreatable(UserApp.currentUser(), project, ResourceType.BOARD_NOTICE);
             return badRequest(edit.render("error.validation", postForm, Posting.findByNumber(project, number), number, project, isAllowedToNotice));
         }
 
@@ -270,7 +250,7 @@
      * @see controllers.AbstractPostingApp#delete(play.db.ebean.Model, models.resource.Resource, play.mvc.Call)
      */
     @Transactional
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(value = Operation.DELETE, resourceType = ResourceType.BOARD_POST)
     public static Result deletePost(String owner, String projectName, Long number) {
         Project project = Project.findByOwnerAndProjectName(owner, projectName);
         Posting posting = Posting.findByNumber(project, number);
@@ -295,24 +275,17 @@
      * @see controllers.AbstractPostingApp#newComment(models.Comment, play.data.Form, play.mvc.Call, Runnable)
      */
     @Transactional
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(value = Operation.READ, resourceType = ResourceType.BOARD_POST)
+    @IsCreatable(ResourceType.NONISSUE_COMMENT)
     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);
-        if (posting == null) {
-            return notFound(ErrorViews.NotFound.render("error.notfound"));
-        }
         Call redirectTo = routes.BoardApp.post(project.owner, project.name, number);
         Form<PostingComment> commentForm = new Form<>(PostingComment.class)
                 .bindFromRequest();
 
         if (commentForm.hasErrors()) {
             return badRequest(views.html.error.badrequest.render("error.validation", project));
-        }
-
-        if (!AccessControl.isProjectResourceCreatable(
-                    UserApp.currentUser(), project, ResourceType.NONISSUE_COMMENT)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
         }
 
         final PostingComment comment = commentForm.get();
@@ -344,8 +317,7 @@
     public static Result deleteComment(String userName, String projectName, Long number, Long commentId) {
         Comment comment = PostingComment.find.byId(commentId);
         Project project = comment.asResource().getProject();
-        Call redirectTo =
-                routes.BoardApp.post(project.owner, project.name, number);
+        Call redirectTo = routes.BoardApp.post(project.owner, project.name, number);
 
         return delete(comment, comment.asResource(), redirectTo);
     }
app/controllers/BranchApp.java
--- app/controllers/BranchApp.java
+++ app/controllers/BranchApp.java
@@ -20,7 +20,8 @@
  */
 package controllers;
 
-import controllers.annotation.ProjectAccess;
+import controllers.annotation.IsAllowed;
+import controllers.annotation.IsOnlyGitAvailable;
 import models.Project;
 import models.enumeration.Operation;
 import org.apache.commons.collections.CollectionUtils;
@@ -39,6 +40,7 @@
 /**
  * @author Keesun Baik
  */
+@IsOnlyGitAvailable
 public class BranchApp extends Controller {
 
     /**
@@ -50,7 +52,7 @@
      * @throws IOException
      * @throws GitAPIException
      */
-    @ProjectAccess(value = Operation.READ, isGitOnly = true)
+    @IsAllowed(Operation.READ)
     public static Result branches(String loginId, String projectName) throws IOException, GitAPIException {
         Project project = Project.findByOwnerAndProjectName(loginId, projectName);
         GitRepository gitRepository = new GitRepository(project);
@@ -80,7 +82,7 @@
      * @throws IOException
      * @throws GitAPIException
      */
-    @ProjectAccess(value = Operation.DELETE, isGitOnly = true)
+    @IsAllowed(Operation.DELETE)
     public static Result deleteBranch(String loginId, String projectName, String branchName) throws GitAPIException {
         Project project = Project.findByOwnerAndProjectName(loginId, projectName);
         Repository repository = GitRepository.buildGitRepository(project);
@@ -96,7 +98,7 @@
      * @param branchName
      * @return
      */
-    @ProjectAccess(value = Operation.UPDATE, isGitOnly = true)
+    @IsAllowed(Operation.UPDATE)
     public static Result setAsDefault(String loginId, String projectName, String branchName) throws IOException, GitAPIException {
         Project project = Project.findByOwnerAndProjectName(loginId, projectName);
         GitRepository gitRepository = new GitRepository(project);
app/controllers/CodeApp.java
--- app/controllers/CodeApp.java
+++ app/controllers/CodeApp.java
@@ -1,6 +1,7 @@
 package controllers;
 
 import actions.NullProjectCheckAction;
+import controllers.annotation.IsAllowed;
 import models.Project;
 import models.enumeration.Operation;
 import org.apache.tika.Tika;
@@ -13,7 +14,6 @@
 import play.mvc.With;
 import playRepository.PlayRepository;
 import playRepository.RepositoryService;
-import utils.AccessControl;
 import utils.ErrorViews;
 import views.html.code.nohead;
 import views.html.code.nohead_svn;
@@ -35,14 +35,10 @@
      * @param userName 프로젝트 소유자 이름
      * @param projectName 프로젝트 이름
      */
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(Operation.READ)
     public static Result codeBrowser(String userName, String projectName)
             throws IOException, UnsupportedOperationException, ServletException {
         Project project = ProjectApp.getProject(userName, projectName);
-
-        if (!AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.READ)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
 
         if (!RepositoryService.VCS_GIT.equals(project.vcs) && !RepositoryService.VCS_SUBVERSION.equals(project.vcs)) {
             return status(Http.Status.NOT_IMPLEMENTED, project.vcs + " is not supported!");
app/controllers/CodeHistoryApp.java
--- app/controllers/CodeHistoryApp.java
+++ app/controllers/CodeHistoryApp.java
@@ -1,6 +1,8 @@
 package controllers;
 
 import actions.NullProjectCheckAction;
+import controllers.annotation.IsAllowed;
+import controllers.annotation.IsCreatable;
 import models.Attachment;
 import models.CommitComment;
 import models.NotificationEvent;
@@ -20,7 +22,6 @@
 import playRepository.FileDiff;
 import playRepository.PlayRepository;
 import playRepository.RepositoryService;
-import utils.AccessControl;
 import utils.ErrorViews;
 import utils.HttpUtil;
 import utils.PullRequestCommit;
@@ -28,9 +29,7 @@
 import views.html.code.history;
 import views.html.code.nohead;
 import views.html.code.svnDiff;
-import views.html.error.forbidden;
 import views.html.error.notfound;
-import views.html.error.notfound_default;
 
 import javax.servlet.ServletException;
 import java.io.IOException;
@@ -87,17 +86,12 @@
      * @throws GitAPIException
      * @throws SVNException
      */
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(Operation.READ)
     public static Result history(String ownerName, String projectName, String branch, String path) throws IOException,
             UnsupportedOperationException, ServletException, GitAPIException,
             SVNException {
         Project project = Project.findByOwnerAndProjectName(ownerName, projectName);
-
         PlayRepository repository = RepositoryService.getRepository(project);
-
-        if (!AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.READ)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
 
         String pageStr = HttpUtil.getFirstValueFromQuery(request().queryString(), "page");
         int page = 0;
@@ -136,16 +130,11 @@
      * @throws GitAPIException
      * @throws SVNException
      */
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(Operation.READ)
     public static Result show(String ownerName, String projectName, String commitId)
             throws IOException, UnsupportedOperationException, ServletException, GitAPIException,
             SVNException {
         Project project = Project.findByOwnerAndProjectName(ownerName, projectName);
-
-        if (!AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.READ)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
-
         PlayRepository repository = RepositoryService.getRepository(project);
         Commit commit = repository.getCommit(commitId);
 
@@ -179,7 +168,7 @@
         }
     }
 
-    @With(NullProjectCheckAction.class)
+    @IsCreatable(ResourceType.COMMIT_COMMENT)
     public static Result newComment(String ownerName, String projectName, String commitId)
             throws IOException, ServletException, SVNException {
         Form<CommitComment> codeCommentForm = new Form<>(CommitComment.class)
@@ -193,11 +182,6 @@
 
         if (RepositoryService.getRepository(project).getCommit(commitId) == null) {
             return notFound(notfound.render("error.notfound", project, request().path()));
-        }
-
-        if (!AccessControl.isProjectResourceCreatable(UserApp.currentUser(), project,
-                ResourceType.COMMIT_COMMENT)) {
-            return forbidden(forbidden.render("error.forbidden", project));
         }
 
         CommitComment codeComment = codeCommentForm.get();
@@ -227,19 +211,10 @@
     }
 
     @With(NullProjectCheckAction.class)
+    @IsAllowed(value = Operation.DELETE, resourceType = ResourceType.COMMIT_COMMENT)
     public static Result deleteComment(String ownerName, String projectName, String commitId,
                                        Long id) {
         CommitComment codeComment = CommitComment.find.byId(id);
-
-        if (codeComment == null) {
-            return notFound(notfound_default.render(request().path()));
-        }
-
-        if (!AccessControl.isAllowed(UserApp.currentUser(), codeComment.asResource(),
-                Operation.DELETE)) {
-            return forbidden(forbidden.render("error.forbidden", codeComment.project));
-        }
-
         codeComment.delete();
 
         Call toView = routes.CodeHistoryApp.show(ownerName, projectName, commitId);
app/controllers/EnrollProjectApp.java
--- app/controllers/EnrollProjectApp.java
+++ app/controllers/EnrollProjectApp.java
@@ -1,5 +1,7 @@
 package controllers;
 
+import actions.AnonymousCheckAction;
+import actions.NullProjectCheckAction;
 import models.NotificationEvent;
 import models.Project;
 import models.ProjectUser;
@@ -8,10 +10,12 @@
 import play.db.ebean.Transactional;
 import play.mvc.Controller;
 import play.mvc.Result;
+import play.mvc.With;
 
 /**
  * 프로젝트에 멤버로 등록해달라는 요청을 처리하는 컨트롤러
  */
+@With(AnonymousCheckAction.class)
 public class EnrollProjectApp extends Controller {
 
     /**
@@ -23,11 +27,9 @@
      * @return
      */
     @Transactional
+    @With(NullProjectCheckAction.class)
     public static Result enroll(String loginId, String projectName) {
         Project project = Project.findByOwnerAndProjectName(loginId, projectName);
-        if(project == null) {
-            return badRequest();
-        }
 
         User user = UserApp.currentUser();
         if (!ProjectUser.isGuest(project, user)) {
@@ -51,11 +53,9 @@
      * @return
      */
     @Transactional
+    @With(NullProjectCheckAction.class)
     public static Result cancelEnroll(String loginId, String proejctName) {
         Project project = Project.findByOwnerAndProjectName(loginId, proejctName);
-        if(project == null) {
-            return badRequest();
-        }
 
         User user = UserApp.currentUser();
         if (!ProjectUser.isGuest(project, user)) {
app/controllers/IssueApp.java
--- app/controllers/IssueApp.java
+++ app/controllers/IssueApp.java
@@ -3,18 +3,16 @@
 import actions.NullProjectCheckAction;
 import com.avaje.ebean.ExpressionList;
 import com.avaje.ebean.Page;
-
-import controllers.annotation.ProjectAccess;
+import controllers.annotation.IsAllowed;
+import controllers.annotation.IsCreatable;
 import jxl.write.WriteException;
 import models.*;
 import models.enumeration.Operation;
 import models.enumeration.ResourceType;
 import models.enumeration.State;
-
 import org.apache.commons.lang.StringUtils;
 import org.apache.tika.Tika;
 import org.codehaus.jackson.node.ObjectNode;
-
 import play.data.Form;
 import play.db.ebean.Transactional;
 import play.i18n.Messages;
@@ -53,7 +51,7 @@
      * @throws WriteException
      * @throws IOException
      */
-    @ProjectAccess(Operation.READ)
+    @IsAllowed(Operation.READ)
     public static Result issues(String ownerName, String projectName, String state, String format, int pageNum) throws WriteException, IOException {
         Project project = ProjectApp.getProject(ownerName, projectName);
 
@@ -230,7 +228,7 @@
                 result.put("body", Messages.get("error.notfound.issue"));
                 return ok(result);
             } else {
-                return notFound(ErrorViews.NotFound.render("error.notfound", project, "issue"));
+                return notFound(ErrorViews.NotFound.render("error.notfound", project, ResourceType.ISSUE_POST.resource()));
             }
         }
 
@@ -278,18 +276,10 @@
      * @param number 이슈 번호
      * @return
      */
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(resourceType = ResourceType.ISSUE_POST, value = Operation.READ)
     public static Result timeline(String ownerName, String projectName, Long number) {
         Project project = ProjectApp.getProject(ownerName, projectName);
-
         Issue issueInfo = Issue.findByNumber(project, number);
-        if (issueInfo == null) {
-            return notFound(ErrorViews.NotFound.render("error.notfound", project, "issue"));
-        }
-
-        if (!AccessControl.isAllowed(UserApp.currentUser(), issueInfo.asResource(), Operation.READ)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
 
         for (IssueLabel label: issueInfo.labels) {
             label.refresh();
@@ -306,14 +296,11 @@
      * @param ownerName 프로젝트 소유자 이름
      * @param projectName 프로젝트 이름
      * @return
-     * @see {@link AbstractPostingApp#newPostingForm(Project, ResourceType, play.mvc.Content)}
      */
-    @With(NullProjectCheckAction.class)
+    @IsCreatable(ResourceType.ISSUE_POST)
     public static Result newIssueForm(String ownerName, String projectName) {
         Project project = ProjectApp.getProject(ownerName, projectName);
-
-        return newPostingForm(project, ResourceType.ISSUE_POST,
-                create.render("title.newIssue", new Form<>(Issue.class), project));
+        return ok(create.render("title.newIssue", new Form<>(Issue.class), project));
     }
 
     /**
@@ -446,14 +433,10 @@
      * @throws IOException
      */
     @Transactional
-    @With(NullProjectCheckAction.class)
+    @IsCreatable(ResourceType.ISSUE_POST)
     public static Result newIssue(String ownerName, String projectName) {
         Form<Issue> issueForm = new Form<>(Issue.class).bindFromRequest();
         Project project = ProjectApp.getProject(ownerName, projectName);
-
-        if (!AccessControl.isProjectResourceCreatable(UserApp.currentUser(), project, ResourceType.ISSUE_POST)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
 
         if (issueForm.hasErrors()) {
             return badRequest(create.render("error.validation", issueForm, project));
@@ -498,16 +481,10 @@
      * @param number 이슈 번호
      * @return
      */
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(resourceType = ResourceType.ISSUE_POST, value = Operation.UPDATE)
     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));
@@ -528,16 +505,13 @@
      * @throws IOException
      */
     @Transactional
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(value = Operation.UPDATE, resourceType = ResourceType.ISSUE_POST)
     public static Result nextState(String ownerName, String projectName, Long number) {
         Project project = ProjectApp.getProject(ownerName, projectName);
 
         final Issue issue = Issue.findByNumber(project, number);
 
         Call redirectTo = routes.IssueApp.issue(project.owner, project.name, number);
-        if (!AccessControl.isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", issue.project));
-        }
         issue.toNextState();
         NotificationEvent notiEvent = NotificationEvent.afterStateChanged(issue.previousState(), issue, redirectTo.url());
         IssueEvent.addFromNotificationEvent(notiEvent, issue, UserApp.currentUser().loginId);
@@ -635,7 +609,7 @@
      * @ see {@link AbstractPostingApp#delete(play.db.ebean.Model, models.resource.Resource, Call)}
      */
     @Transactional
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(value = Operation.DELETE, resourceType = ResourceType.ISSUE_POST)
     public static Result deleteIssue(String ownerName, String projectName, Long number) {
         Project project = ProjectApp.getProject(ownerName, projectName);
         Issue issue = Issue.findByNumber(project, number);
@@ -660,29 +634,17 @@
      * @see {@link AbstractPostingApp#newComment(models.Comment, play.data.Form}
      */
     @Transactional
-    @With(NullProjectCheckAction.class)
+    @IsCreatable(ResourceType.ISSUE_COMMENT)
     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);
-        if (issue == null) {
-            return notFound(ErrorViews.NotFound.render("error.notfound"));
-        }
         Call redirectTo = routes.IssueApp.issue(project.owner, project.name, number);
-        Form<IssueComment> commentForm = new Form<>(IssueComment.class)
-                .bindFromRequest();
+        Form<IssueComment> commentForm = new Form<>(IssueComment.class).bindFromRequest();
 
         if (commentForm.hasErrors()) {
             return badRequest(ErrorViews.BadRequest.render("error.validation", project));
         }
-
-        if (!AccessControl.isProjectResourceCreatable(
-                    UserApp.currentUser(), project, ResourceType.ISSUE_COMMENT)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
-
         final IssueComment comment = commentForm.get();
-
         return newComment(comment, commentForm, redirectTo, new Runnable() {
             @Override
             public void run() {
app/controllers/IssueLabelApp.java
--- app/controllers/IssueLabelApp.java
+++ app/controllers/IssueLabelApp.java
@@ -1,6 +1,7 @@
 package controllers;
 
-import actions.NullProjectCheckAction;
+import controllers.annotation.IsAllowed;
+import controllers.annotation.IsCreatable;
 import models.IssueLabel;
 import models.Project;
 import models.enumeration.Operation;
@@ -11,8 +12,6 @@
 import play.mvc.Controller;
 import play.mvc.Http;
 import play.mvc.Result;
-import play.mvc.With;
-import utils.AccessControl;
 import utils.ErrorViews;
 
 import java.util.ArrayList;
@@ -43,17 +42,13 @@
      * @param projectName 프로젝트의 이름
      * @return 이슈라벨들을 달라는 요청에 대한 응답
      */
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(Operation.READ)
     public static Result labels(String ownerName, String projectName) {
         if (!request().accepts("application/json")) {
             return status(Http.Status.NOT_ACCEPTABLE);
         }
 
         Project project = ProjectApp.getProject(ownerName, projectName);
-
-        if (!AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.READ)) {
-            return forbidden("You have no permission to access the project '" + project + "'.");
-        }
 
         List<Map<String, String>> labels = new ArrayList<>();
         for (IssueLabel label : IssueLabel.findByProject(project)) {
@@ -93,16 +88,10 @@
      * @return 이슈라벨을 추가해달라는 요청에 대한 응답
      */
     @Transactional
-    @With(NullProjectCheckAction.class)
+    @IsCreatable(ResourceType.ISSUE_LABEL)
     public static Result newLabel(String ownerName, String projectName) {
         Form<IssueLabel> labelForm = new Form<>(IssueLabel.class).bindFromRequest();
-
         Project project = ProjectApp.getProject(ownerName, projectName);
-
-        if (!AccessControl.isProjectResourceCreatable(UserApp.currentUser(), project, ResourceType.ISSUE_LABEL)) {
-            return forbidden(ErrorViews.Forbidden.render("You have no permission to add an issue label to the project '" +
-                    project + "'.", project));
-        }
 
         IssueLabel label = labelForm.get();
         label.project = project;
@@ -147,7 +136,7 @@
      * @return 이슈라벨을 삭제해달라는 요청에 대한 응답
      */
     @Transactional
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(value = Operation.DELETE, resourceType = ResourceType.ISSUE_LABEL)
     public static Result delete(String ownerName, String projectName, Long id) {
         // _method must be 'delete'
         DynamicForm bindedForm = form().bindFromRequest();
@@ -157,17 +146,7 @@
         }
 
         IssueLabel label = IssueLabel.finder.byId(id);
-
-        if (label == null) {
-            return notFound(ErrorViews.NotFound.render("The label #" + id + " is not found."));
-        }
-
-        if (!AccessControl.isAllowed(UserApp.currentUser(), label.asResource(), Operation.DELETE)) {
-            return forbidden(ErrorViews.Forbidden.render("You have no permission to delete the label #" + label.id + ".", label.project));
-        }
-
         label.delete();
-
         return ok();
     }
 }
app/controllers/MilestoneApp.java
--- app/controllers/MilestoneApp.java
+++ app/controllers/MilestoneApp.java
@@ -1,20 +1,28 @@
 package controllers;
 
-import actions.NullProjectCheckAction;
-import models.*;
-import models.enumeration.*;
-import play.data.*;
+import controllers.annotation.IsAllowed;
+import controllers.annotation.IsCreatable;
+import models.Attachment;
+import models.Milestone;
+import models.Project;
+import models.enumeration.Direction;
+import models.enumeration.Operation;
+import models.enumeration.ResourceType;
+import models.enumeration.State;
+import play.data.Form;
 import play.db.ebean.Transactional;
-import play.mvc.*;
+import play.mvc.Controller;
+import play.mvc.Result;
 import utils.AccessControl;
 import utils.Constants;
 import utils.ErrorViews;
 import utils.HttpUtil;
-import views.html.milestone.*;
+import views.html.milestone.create;
+import views.html.milestone.edit;
+import views.html.milestone.list;
+import views.html.milestone.view;
 
-import java.util.*;
-
-import org.apache.commons.lang3.StringUtils;
+import java.util.List;
 
 import static play.data.Form.form;
 
@@ -46,13 +54,9 @@
      * @param projectName
      * @return
      */
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(Operation.READ)
     public static Result milestones(String userName, String projectName) {
         Project project = ProjectApp.getProject(userName, projectName);
-        if (!AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.READ)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
-
         MilestoneCondition mCondition = form(MilestoneCondition.class).bindFromRequest().get();
 
         List<Milestone> milestones = Milestone.findMilestones(project.id,
@@ -75,14 +79,9 @@
      * @param projectName
      * @return
      */
-    @With(NullProjectCheckAction.class)
+    @IsCreatable(ResourceType.MILESTONE)
     public static Result newMilestoneForm(String userName, String projectName) {
         Project project = ProjectApp.getProject(userName, projectName);
-
-        if(!AccessControl.isProjectResourceCreatable(UserApp.currentUser(), project, ResourceType.MILESTONE)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
-
         return ok(create.render("title.newMilestone", new Form<>(Milestone.class), project));
     }
 
@@ -103,14 +102,10 @@
      * @see {@link #validate(models.Project, play.data.Form)}
      */
     @Transactional
-    @With(NullProjectCheckAction.class)
+    @IsCreatable(ResourceType.MILESTONE)
     public static Result newMilestone(String userName, String projectName) {
         Form<Milestone> milestoneForm = new Form<>(Milestone.class).bindFromRequest();
         Project project = ProjectApp.getProject(userName, projectName);
-
-        if(!AccessControl.isProjectResourceCreatable(UserApp.currentUser(), project, ResourceType.MILESTONE)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
 
         validate(project, milestoneForm);
         if (milestoneForm.hasErrors()) {
@@ -158,14 +153,10 @@
      * @param milestoneId
      * @return
      */
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(value = Operation.UPDATE, resourceType = ResourceType.MILESTONE)
     public static Result editMilestoneForm(String userName, String projectName, Long milestoneId) {
         Project project = ProjectApp.getProject(userName, projectName);
         Milestone milestone = Milestone.findById(milestoneId);
-
-        if(!AccessControl.isAllowed(UserApp.currentUser(), milestone.asResource(), Operation.UPDATE)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
 
         Form<Milestone> editForm = new Form<>(Milestone.class).fill(milestone);
         return ok(edit.render("title.editMilestone", editForm, milestoneId, project));
@@ -186,15 +177,11 @@
      * @return
      */
     @Transactional
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(value = Operation.UPDATE, resourceType = ResourceType.MILESTONE)
     public static Result editMilestone(String userName, String projectName, Long milestoneId) {
         Project project = ProjectApp.getProject(userName, projectName);
         Form<Milestone> milestoneForm = new Form<>(Milestone.class).bindFromRequest();
         Milestone original = Milestone.findById(milestoneId);
-
-        if(!AccessControl.isAllowed(UserApp.currentUser(), original.asResource(), Operation.UPDATE)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
 
         if(!original.title.equals(milestoneForm.field("title").value())) {
             validate(project, milestoneForm);
@@ -232,13 +219,11 @@
      * @return
      */
     @Transactional
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(value = Operation.DELETE, resourceType = ResourceType.MILESTONE)
     public static Result deleteMilestone(String userName, String projectName, Long id) {
         Project project = ProjectApp.getProject(userName, projectName);
         Milestone milestone = Milestone.findById(id);
-        if(!AccessControl.isAllowed(UserApp.currentUser(), milestone.asResource(), Operation.DELETE)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
+
         if(!project.id.equals(milestone.project.id)) {
             return internalServerError();
         }
@@ -263,16 +248,10 @@
      * @return
      */
     @Transactional
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(value = Operation.UPDATE, resourceType = ResourceType.MILESTONE)
     public static Result open(String userName, String projectName, Long id) {
-        Project project = ProjectApp.getProject(userName, projectName);
         Milestone milestone = Milestone.findById(id);
-        if(!AccessControl.isAllowed(UserApp.currentUser(), milestone.asResource(), Operation.UPDATE)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
-
         milestone.open();
-
         return redirect(routes.MilestoneApp.milestone(userName, projectName, id));
     }
 
@@ -286,16 +265,10 @@
      * @return
      */
     @Transactional
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(value = Operation.UPDATE, resourceType = ResourceType.MILESTONE)
     public static Result close(String userName, String projectName, Long id) {
-        Project project = ProjectApp.getProject(userName, projectName);
         Milestone milestone = Milestone.findById(id);
-        if(!AccessControl.isAllowed(UserApp.currentUser(), milestone.asResource(), Operation.UPDATE)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
-
         milestone.close();
-
         return redirect(routes.MilestoneApp.milestone(userName, projectName, id));
     }
 
@@ -310,17 +283,10 @@
      * @param id
      * @return
      */
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(value = Operation.READ, resourceType = ResourceType.MILESTONE)
     public static Result milestone(String userName, String projectName, Long id) {
         Project project = ProjectApp.getProject(userName, projectName);
         Milestone milestone = Milestone.findById(id);
-        if(milestone == null) {
-            return notFound(ErrorViews.NotFound.render("error.notfound"));
-        }
-
-        if (!AccessControl.isAllowed(UserApp.currentUser(), milestone.asResource(), Operation.READ)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
 
         String paramState = request().getQueryString("state");
         State state = State.getValue(paramState);
app/controllers/ProjectApp.java
--- app/controllers/ProjectApp.java
+++ app/controllers/ProjectApp.java
@@ -1,16 +1,15 @@
 package controllers;
 
 import actions.AnonymousCheckAction;
-
 import actions.NullProjectCheckAction;
+import com.avaje.ebean.ExpressionList;
 import com.avaje.ebean.Junction;
 import com.avaje.ebean.Page;
-import com.avaje.ebean.ExpressionList;
-
-import controllers.annotation.ProjectAccess;
+import controllers.annotation.IsAllowed;
 import models.*;
-import models.enumeration.*;
-
+import models.enumeration.Operation;
+import models.enumeration.ResourceType;
+import models.enumeration.RoleType;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.codehaus.jackson.JsonNode;
@@ -18,28 +17,28 @@
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.NoHeadException;
 import org.tmatesoft.svn.core.SVNException;
-
 import play.data.Form;
 import play.data.validation.ValidationError;
 import play.db.ebean.Transactional;
 import play.libs.Json;
-import play.mvc.*;
+import play.mvc.Controller;
+import play.mvc.Http;
 import play.mvc.Http.MultipartFormData;
 import play.mvc.Http.MultipartFormData.FilePart;
+import play.mvc.Result;
+import play.mvc.With;
 import playRepository.Commit;
 import playRepository.PlayRepository;
 import playRepository.RepositoryService;
 import scala.reflect.io.FileOperationException;
-import utils.AccessControl;
-import utils.Constants;
-import utils.HttpUtil;
-import utils.ErrorViews;
-import utils.LabelSearchUtil;
+import utils.*;
 import validation.ExConstraints.RestrictedValidator;
-import views.html.project.*;
+import views.html.project.create;
+import views.html.project.delete;
+import views.html.project.overview;
+import views.html.project.setting;
 
 import javax.servlet.ServletException;
-
 import java.io.IOException;
 import java.security.NoSuchAlgorithmException;
 import java.util.*;
@@ -91,7 +90,7 @@
     }
 
     @With(AnonymousCheckAction.class)
-    @ProjectAccess(Operation.UPDATE)
+    @IsAllowed(Operation.UPDATE)
     public static Result projectOverviewUpdate(String ownerId, String projectName){
         JsonNode json = request().body().asJson();
         Project targetProject = Project.findByOwnerAndProjectName(ownerId, projectName);
@@ -120,14 +119,10 @@
      * @throws SVNException the svn exception
      * @throws GitAPIException the git api exception
      */
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(Operation.READ)
     public static Result project(String loginId, String projectName) throws IOException, ServletException, SVNException, GitAPIException {
         Project project = Project.findByOwnerAndProjectName(loginId, projectName);
         project.fixInvalidForkData();
-
-        if (!AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.READ)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
 
         PlayRepository repository = RepositoryService.getRepository(project);
 
@@ -174,14 +169,9 @@
      * @param projectName
      * @return 프로젝트 정보
      */
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(Operation.UPDATE)
     public static Result settingForm(String loginId, String projectName) throws Exception {
         Project project = Project.findByOwnerAndProjectName(loginId, projectName);
-
-        if (!AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.UPDATE)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
-
         Form<Project> projectForm = form(Project.class).fill(project);
         PlayRepository repository = RepositoryService.getRepository(project);
         return ok(setting.render("title.projectSetting", projectForm, project, repository.getBranches()));
@@ -340,14 +330,9 @@
      * @param projectName the project name
      * @return 프로젝트폼, 프로젝트 정보
      */
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(Operation.DELETE)
     public static Result deleteForm(String loginId, String projectName) {
         Project project = Project.findByOwnerAndProjectName(loginId, projectName);
-
-        if (!AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.UPDATE)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
-
         Form<Project> projectForm = form(Project.class).fill(project);
         return ok(delete.render("title.projectDelete", projectForm, project));
     }
@@ -364,25 +349,19 @@
      * @throws Exception the exception
      */
     @Transactional
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(Operation.DELETE)
     public static Result deleteProject(String loginId, String projectName) throws Exception {
         Project project = Project.findByOwnerAndProjectName(loginId, projectName);
+        project.delete();
+        RepositoryService.deleteRepository(project);
 
-        if (AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.DELETE)) {
-            project.delete();
-            RepositoryService.deleteRepository(project);
-
-            // XHR 호출에 의한 경우라면 204 No Content 와 Location 헤더로 응답한다
-            if(HttpUtil.isRequestedWithXHR(request())){
-                response().setHeader("Location", routes.Application.index().toString());
-                return status(204);
-            }
-
-            return redirect(routes.Application.index());
-        } else {
-            flash(Constants.WARNING, "project.member.isManager");
-            return redirect(routes.ProjectApp.settingForm(loginId, projectName));
+        // XHR 호출에 의한 경우라면 204 No Content 와 Location 헤더로 응답한다
+        if(HttpUtil.isRequestedWithXHR(request())){
+            response().setHeader("Location", routes.Application.index().toString());
+            return status(204);
         }
+
+        return redirect(routes.Application.index());
     }
 
     /**
@@ -398,16 +377,10 @@
      * @return 프로젝트, 멤버목록, Role 목록
      */
     @Transactional
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(Operation.UPDATE)
     public static Result members(String loginId, String projectName) {
         Project project = Project.findByOwnerAndProjectName(loginId, projectName);
-
-        if (!AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.UPDATE)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
-
         project.cleanEnrolledUsers();
-
         return ok(views.html.project.members.render("title.projectMembers",
                 ProjectUser.findMemberListByProject(project.id), project,
                 Role.getActiveRoles()));
@@ -427,7 +400,7 @@
      * @param resourceType
      * @return
      */
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(Operation.READ)
     public static Result mentionList(String loginId, String projectName, Long number, String resourceType) {
         String prefer = HttpUtil.getPreferType(request(), HTML, JSON);
         if (prefer == null) {
@@ -466,15 +439,11 @@
      * @throws GitAPIException
      * @throws SVNException
      */
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(Operation.READ)
     public static Result mentionListAtCommitDiff(String ownerLoginId, String projectName, String commitId, Long pullRequestId)
             throws IOException, UnsupportedOperationException, ServletException,
             SVNException {
         Project project = Project.findByOwnerAndProjectName(ownerLoginId, projectName);
-
-        if (!AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.READ)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
 
         PullRequest pullRequest;
         Project fromProject = project;
@@ -516,15 +485,11 @@
      * @throws GitAPIException
      * @throws SVNException
      */
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(Operation.READ)
     public static Result mentionListAtPullRequest(String ownerLoginId, String projectName, String commitId, Long pullRequestId)
             throws IOException, UnsupportedOperationException, ServletException,
             SVNException {
         Project project = Project.findByOwnerAndProjectName(ownerLoginId, projectName);
-
-        if (!AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.READ)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
 
         PullRequest pullRequest = PullRequest.findById(pullRequestId);
         List<User> userList = new ArrayList<>();
@@ -723,20 +688,14 @@
      * @return
      */
     @Transactional
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(Operation.UPDATE)
     public static Result editMember(String loginId, String projectName, Long userId) {
         Project project = Project.findByOwnerAndProjectName(loginId, projectName);
-
-        if (AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.UPDATE)) {
-            if (project.isOwner(User.find.byId(userId))) {
-                return forbidden(ErrorViews.Forbidden.render("project.member.ownerMustBeAManager", project));
-            }
-            ProjectUser.assignRole(userId, project.id, form(Role.class)
-                    .bindFromRequest().get().id);
-            return status(Http.Status.NO_CONTENT);
-        } else {
-            return forbidden(ErrorViews.Forbidden.render("project.member.isManager", project));
+        if (project.isOwner(User.find.byId(userId))) {
+            return forbidden(ErrorViews.Forbidden.render("project.member.ownerMustBeAManager", project));
         }
+        ProjectUser.assignRole(userId, project.id, form(Role.class).bindFromRequest().get().id);
+        return status(Http.Status.NO_CONTENT);
     }
 
     /**
@@ -863,13 +822,9 @@
      * @param projectName the project name
      * @return 프로젝트 태그 JSON 데이터
      */
-    @With(NullProjectCheckAction.class)
+    @IsAllowed(Operation.READ)
     public static Result labels(String owner, String projectName) {
         Project project = Project.findByOwnerAndProjectName(owner, projectName);
-
-        if (!AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.READ)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
 
         if (!request().accepts("application/json")) {
             return status(Http.Status.NOT_ACCEPTABLE);
@@ -1005,7 +960,7 @@
      */
     @Transactional
     @With(AnonymousCheckAction.class)
-    @ProjectAccess(Operation.DELETE)
+    @IsAllowed(Operation.DELETE)
     public static Result deletePushedBranch(String ownerName, String projectName, Long id) {
         PushedBranch pushedBranch = PushedBranch.find.byId(id);
         if (pushedBranch != null) {
app/controllers/PullRequestApp.java
--- app/controllers/PullRequestApp.java
+++ app/controllers/PullRequestApp.java
@@ -24,9 +24,7 @@
 import actors.PullRequestMergingActor;
 import akka.actor.Props;
 import com.avaje.ebean.Page;
-import controllers.annotation.IsCreatable;
-import controllers.annotation.ProjectAccess;
-import controllers.annotation.PullRequestAccess;
+import controllers.annotation.*;
 import models.*;
 import models.enumeration.Operation;
 import models.enumeration.ResourceType;
@@ -62,6 +60,7 @@
 /**
  * 프로젝트 복사(포크)와 코드 주고받기(풀리퀘) 기능을 다루는 컨트롤러
  */
+@IsOnlyGitAvailable
 public class PullRequestApp extends Controller {
 
     /**
@@ -349,15 +348,9 @@
      * @param projectName
      * @return
      */
-    @ProjectAccess(Operation.READ)
+    @IsAllowed(Operation.READ)
     public static Result pullRequests(String userName, String projectName, int pageNum) {
-
         Project project = Project.findByOwnerAndProjectName(userName, projectName);
-
-        if(!project.vcs.equals("GIT")) {
-            return badRequest(ErrorViews.BadRequest.render("Now, only git project is allowed this request.", project));
-        }
-
         Page<PullRequest> page = PullRequest.findPagingList(State.OPEN, project, pageNum - 1);
         return ok(list.render(project, page, "opened"));
     }
@@ -369,7 +362,7 @@
      * @param projectName
      * @return
      */
-    @ProjectAccess(Operation.READ)
+    @IsAllowed(Operation.READ)
     public static Result closedPullRequests(String userName, String projectName, int pageNum) {
         Project project = Project.findByOwnerAndProjectName(userName, projectName);
         Page<PullRequest> page = PullRequest.findClosedPagingList(project, pageNum - 1);
@@ -383,7 +376,7 @@
      * @param projectName
      * @return
      */
-    @ProjectAccess(Operation.READ)
+    @IsAllowed(Operation.READ)
     public static Result sentPullRequests(String userName, String projectName, int pageNum) {
         Project project = Project.findByOwnerAndProjectName(userName, projectName);
         Page<PullRequest> page = PullRequest.findSentPullRequests(project, pageNum - 1);
@@ -398,8 +391,7 @@
      * @param pullRequestNumber
      * @return
      */
-    @ProjectAccess(Operation.READ)
-    @PullRequestAccess(Operation.READ)
+    @IsAllowed(value = Operation.READ, resourceType = ResourceType.PULL_REQUEST)
     public static Result pullRequest(String userName, String projectName, long pullRequestNumber) {
         Project project = Project.findByOwnerAndProjectName(userName, projectName);
         PullRequest pullRequest = PullRequest.findOne(project, pullRequestNumber);
@@ -432,8 +424,7 @@
      * @param pullRequestNumber
      * @return
      */
-    @ProjectAccess(Operation.READ)
-    @PullRequestAccess(Operation.READ)
+    @IsAllowed(value = Operation.READ, resourceType = ResourceType.PULL_REQUEST)
     public static Result pullRequestState(String userName, String projectName, long pullRequestNumber) {
         Project project = Project.findByOwnerAndProjectName(userName, projectName);
         PullRequest pullRequest = PullRequest.findOne(project, pullRequestNumber);
@@ -470,8 +461,7 @@
      * @param pullRequestNumber
      * @return
      */
-    @ProjectAccess(Operation.READ)
-    @PullRequestAccess(Operation.READ)
+    @IsAllowed(value = Operation.READ, resourceType = ResourceType.PULL_REQUEST)
     public static Result pullRequestCommits(String userName, String projectName, long pullRequestNumber) {
         Project project = Project.findByOwnerAndProjectName(userName, projectName);
         PullRequest pullRequest = PullRequest.findOne(project, pullRequestNumber);
@@ -487,8 +477,7 @@
      * @param pullRequestNumber
      * @return
      */
-    @ProjectAccess(Operation.READ)
-    @PullRequestAccess(Operation.READ)
+    @IsAllowed(value = Operation.READ, resourceType = ResourceType.PULL_REQUEST)
     public static Result pullRequestChanges(String userName, String projectName, long pullRequestNumber) {
         Project project = Project.findByOwnerAndProjectName(userName, projectName);
         PullRequest pullRequest = PullRequest.findOne(project, pullRequestNumber);
@@ -516,8 +505,7 @@
      */
     @Transactional
     @With(AnonymousCheckAction.class)
-    @ProjectAccess(Operation.READ)
-    @PullRequestAccess(Operation.ACCEPT)
+    @IsAllowed(value = Operation.ACCEPT, resourceType = ResourceType.PULL_REQUEST)
     public static Result accept(final String userName, final String projectName,
             final long pullRequestNumber) {
         Project project = Project.findByOwnerAndProjectName(userName, projectName);
@@ -562,8 +550,7 @@
      */
     @Transactional
     @With(AnonymousCheckAction.class)
-    @ProjectAccess(Operation.READ)
-    @PullRequestAccess(Operation.CLOSE)
+    @IsAllowed(value = Operation.CLOSE, resourceType = ResourceType.PULL_REQUEST)
     public static Result close(String userName, String projectName, Long pullRequestNumber) {
         Project project = Project.findByOwnerAndProjectName(userName, projectName);
         PullRequest pullRequest = PullRequest.findOne(project, pullRequestNumber);
@@ -589,8 +576,7 @@
      */
     @Transactional
     @With(AnonymousCheckAction.class)
-    @ProjectAccess(Operation.READ)
-    @PullRequestAccess(Operation.CLOSE)
+    @IsAllowed(value = Operation.REOPEN, resourceType = ResourceType.PULL_REQUEST)
     public static Result open(String userName, String projectName, Long pullRequestNumber) {
         Project project = Project.findByOwnerAndProjectName(userName, projectName);
         PullRequest pullRequest = PullRequest.findOne(project, pullRequestNumber);
@@ -623,8 +609,7 @@
      * @throws ServletException
      */
     @With(AnonymousCheckAction.class)
-    @ProjectAccess(Operation.READ)
-    @PullRequestAccess(Operation.UPDATE)
+    @IsAllowed(value = Operation.UPDATE, resourceType = ResourceType.PULL_REQUEST)
     public static Result editPullRequestForm(String userName, String projectName, Long pullRequestNumber) throws IOException, GitAPIException {
         Project toProject = Project.findByOwnerAndProjectName(userName, projectName);
         PullRequest pullRequest = PullRequest.findOne(toProject, pullRequestNumber);
@@ -652,8 +637,7 @@
      */
     @Transactional
     @With(AnonymousCheckAction.class)
-    @ProjectAccess(Operation.READ)
-    @PullRequestAccess(Operation.UPDATE)
+    @IsAllowed(value = Operation.UPDATE, resourceType = ResourceType.PULL_REQUEST)
     public static Result editPullRequest(String userName, String projectName, Long pullRequestNumber) {
         Project toProject = Project.findByOwnerAndProjectName(userName, projectName);
         PullRequest pullRequest = PullRequest.findOne(toProject, pullRequestNumber);
@@ -693,8 +677,7 @@
      */
     @Transactional
     @With(AnonymousCheckAction.class)
-    @ProjectAccess(Operation.READ)
-    @PullRequestAccess(Operation.UPDATE)
+    @IsAllowed(value = Operation.UPDATE, resourceType = ResourceType.PULL_REQUEST)
     public static Result deleteFromBranch(String userName, String projectName, Long pullRequestNumber) {
         Project toProject = Project.findByOwnerAndProjectName(userName, projectName);
         PullRequest pullRequest = PullRequest.findOne(toProject, pullRequestNumber);
@@ -714,8 +697,7 @@
      */
     @Transactional
     @With(AnonymousCheckAction.class)
-    @ProjectAccess(Operation.READ)
-    @PullRequestAccess(Operation.UPDATE)
+    @IsAllowed(value = Operation.UPDATE, resourceType = ResourceType.PULL_REQUEST)
     public static Result restoreFromBranch(String userName, String projectName, Long pullRequestNumber) {
         Project toProject = Project.findByOwnerAndProjectName(userName, projectName);
         PullRequest pullRequest = PullRequest.findOne(toProject, pullRequestNumber);
@@ -739,7 +721,7 @@
      * @throws SVNException
      */
     @Transactional
-    @ProjectAccess(Operation.READ)
+    @IsAllowed(Operation.READ)
     public static Result commitView(String userName, String projectName, Long pullRequestNumber, String commitId) throws GitAPIException, SVNException, IOException, ServletException {
         Project toProject = Project.findByOwnerAndProjectName(userName, projectName);
         PullRequest pullRequest = PullRequest.findOne(toProject, pullRequestNumber);
app/controllers/ReviewApp.java
--- app/controllers/ReviewApp.java
+++ app/controllers/ReviewApp.java
@@ -21,14 +21,14 @@
 package controllers;
 
 import actions.AnonymousCheckAction;
-import controllers.annotation.ProjectAccess;
-import controllers.annotation.PullRequestAccess;
+import controllers.annotation.IsAllowed;
 import models.NotificationEvent;
 import models.Project;
 import models.PullRequest;
 import models.PullRequestEvent;
 import models.enumeration.EventType;
 import models.enumeration.Operation;
+import models.enumeration.ResourceType;
 import play.api.mvc.Call;
 import play.db.ebean.Transactional;
 import play.mvc.Controller;
@@ -38,12 +38,11 @@
 /**
  * @author Keesun Baik
  */
+@With(AnonymousCheckAction.class)
 public class ReviewApp extends Controller {
 
     @Transactional
-    @With(AnonymousCheckAction.class)
-    @ProjectAccess(Operation.READ)
-    @PullRequestAccess(Operation.ACCEPT)
+    @IsAllowed(value = Operation.ACCEPT, resourceType = ResourceType.PULL_REQUEST)
     public static Result review(String userName, String projectName, Long pullRequestNumber) {
         Project project = Project.findByOwnerAndProjectName(userName, projectName);
         PullRequest pullRequest = PullRequest.findOne(project, pullRequestNumber);
@@ -57,9 +56,7 @@
     }
 
     @Transactional
-    @With(AnonymousCheckAction.class)
-    @ProjectAccess(Operation.READ)
-    @PullRequestAccess(Operation.ACCEPT)
+    @IsAllowed(value = Operation.ACCEPT, resourceType = ResourceType.PULL_REQUEST)
     public static Result unreview(String userName, String projectName, Long pullRequestNumber) {
         Project project = Project.findByOwnerAndProjectName(userName, projectName);
         PullRequest pullRequest = PullRequest.findOne(project, pullRequestNumber);
app/controllers/StatisticsApp.java
--- app/controllers/StatisticsApp.java
+++ app/controllers/StatisticsApp.java
@@ -1,17 +1,18 @@
 package controllers;
 
+import actions.NullProjectCheckAction;
 import models.Project;
 import play.mvc.Controller;
 import play.mvc.Result;
-import utils.ErrorViews;
+import play.mvc.With;
 import views.html.project.statistics;
 
 public class StatisticsApp extends Controller {
+
+    @With(NullProjectCheckAction.class)
     public static Result statistics(String userName, String projectName) {
         Project project = Project.findByOwnerAndProjectName(userName, projectName);
-        if (project == null) {
-            return notFound(ErrorViews.NotFound.render("error.notfound"));
-        }
         return ok(statistics.render("statistics", project));
     }
+
 }
app/controllers/WatchProjectApp.java
--- app/controllers/WatchProjectApp.java
+++ app/controllers/WatchProjectApp.java
@@ -1,5 +1,7 @@
 package controllers;
 
+import actions.AnonymousCheckAction;
+import controllers.annotation.IsAllowed;
 import models.Project;
 import models.User;
 import models.UserProjectNotification;
@@ -9,42 +11,31 @@
 import play.mvc.Controller;
 import play.mvc.Result;
 import play.mvc.With;
-import utils.AccessControl;
-import utils.ErrorViews;
 import utils.WatchService;
-import actions.AnonymousCheckAction;
-import controllers.annotation.ProjectAccess;
 
+@With(AnonymousCheckAction.class)
 public class WatchProjectApp extends Controller {
 
-    @ProjectAccess(Operation.READ)
-    @With(AnonymousCheckAction.class)
+    @IsAllowed(Operation.READ)
     public static Result watch(String userName, String projectName) {
         Project project = Project.findByOwnerAndProjectName(userName, projectName);
         WatchService.watch(project.asResource());
         return ok();
     }
 
-    @ProjectAccess(Operation.READ)
-    @With(AnonymousCheckAction.class)
+    @IsAllowed(Operation.READ)
     public static Result unwatch(String userName, String projectName) {
         Project project = Project.findByOwnerAndProjectName(userName, projectName);
         WatchService.unwatch(project.asResource());
         return ok();
     }
 
-    @With(AnonymousCheckAction.class)
+    @IsAllowed(Operation.READ)
     public static Result toggle(Long projectId, String notificationType) {
         EventType notiType = EventType.valueOf(notificationType);
         Project project = Project.find.byId(projectId);
         User user = UserApp.currentUser();
 
-        if(project == null) {
-            return notFound(ErrorViews.NotFound.render("No project matches given projectId '" + projectId + "'"));
-        }
-        if(!AccessControl.isAllowed(user, project.asResource(), Operation.READ)) {
-            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
-        }
         if(!WatchService.isWatching(user, project.asResource())) {
             return badRequest(Messages.get("error.notfound.watch"));
         }
 
app/controllers/annotation/IsAllowed.java (added)
+++ app/controllers/annotation/IsAllowed.java
@@ -0,0 +1,53 @@
+/**
+ * Yobi, Project Hosting SW
+ *
+ * Copyright 2013 NAVER Corp.
+ * http://yobi.io
+ *
+ * @Author Keesun Baik
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package controllers.annotation;
+
+import actions.IsAllowedAction;
+import models.enumeration.Operation;
+import models.enumeration.ResourceType;
+import play.mvc.With;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * /{user.loginId}/{project.name}/** 에 해당하는 URL에 적용할 수 있는 리소스 존재 확인 및 권한 확인용 애노테이션.
+ * - URL 패턴에 대응하는 프로젝트가 있는 확인하다. 없으면 404 Not Found.
+ * - {@code resourceType}에 해당하는 리소스가 있는지 확인한다. 없으면 404 Not Found.
+ * - 현재 사용자가 {@code resourceType}에 해당하는 리소스에 {@code value}에 해당하는 권한이 있는지 확인한다. 권한이 없으면 403 Forbidden.
+ *
+ * 이 애노테이션으로 다룰 수 있는 리소스 타입은
+ * {@link models.resource.Resource#getResourceObject(actions.support.PathParser, models.Project, models.enumeration.ResourceType)}
+ * 참조하고 필요하면 추가 해야 한다.
+ *
+ * @author Keesun Baik
+ * @see {@link actions.IsAllowedAction}
+ * @see {@link models.resource.Resource#getResourceObject(actions.support.PathParser, models.Project, models.enumeration.ResourceType)}
+ */
+@With(IsAllowedAction.class)
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface IsAllowed {
+    Operation value();
+    ResourceType resourceType() default ResourceType.PROJECT;
+}
app/controllers/annotation/IsCreatable.java
--- app/controllers/annotation/IsCreatable.java
+++ app/controllers/annotation/IsCreatable.java
@@ -5,7 +5,7 @@
  * http://yobi.io
  *
  * @Author Wansoon Park, Keesun Baek
- * 
+ *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
@@ -30,9 +30,16 @@
 import actions.IsCreatableAction;
 
 /**
- * {@code project}의 {@code resourceType} 생성권한을 체크한다.
- * @author Wansoon Park, Keesun Beak
+ * /{user.loginId}/{project.name}/** 에 해당하는 URL에 적용할 수 있는 리소스 생성 권한 확인하는 애노테이션.
+ * - URL 패턴에 대응하는 프로젝트가 있는 확인하다. 없으면 404 Not Found.
+ * - 현재 사용자가 {@code resourceType}에 해당하는 리소스를 생성할 수 있는지 확인한다. 권한이 없으면 403 Forbidden.
  *
+ * 이 애노테이션은 {@link utils.AccessControl#isProjectResourceCreatable(models.User, models.Project, models.enumeration.ResourceType)}
+ * 을 사용한다.
+ *
+ * @author Wansoon Park, Keesun Baik
+ * @see {@link actions.IsCreatableAction}
+ * @see {@link utils.AccessControl#isProjectResourceCreatable(models.User, models.Project, models.enumeration.ResourceType)}
  */
 @With(IsCreatableAction.class)
 @Target({ElementType.TYPE, ElementType.METHOD})
app/controllers/annotation/IsOnlyGitAvailable.java (Renamed from app/controllers/annotation/PullRequestAccess.java)
--- app/controllers/annotation/PullRequestAccess.java
+++ app/controllers/annotation/IsOnlyGitAvailable.java
@@ -4,8 +4,8 @@
  * Copyright 2013 NAVER Corp.
  * http://yobi.io
  *
- * @Author Wansoon Park, Keesun Baek
- * 
+ * @Author Keesun Baik
+ *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
@@ -20,23 +20,25 @@
  */
 package controllers.annotation;
 
+import actions.IsOnlyGitAvailableAction;
+import play.mvc.With;
+
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
-import models.enumeration.Operation;
-import play.mvc.With;
-import actions.PullRequestCheckAction;
-
 /**
- * {@code PullRequest}의 {@code Operation} 권한을 체크한다.
- * @author Wansoon Park, Keesun Beak
+ * /{user.loginId}/{project.name}/** 에 해당하는 URL에 적용할 수 있는 Git 프로젝트 확인용 애노테이션.
+ * - URL 패턴에 대응하는 프로젝트가 있는 확인하다. 없으면 404 Not Found.
+ * - 접근하려는 프로젝트가 Git 프로젝트인지 확인한다. Git 프로젝트가 아니면 400 BadRequest.
  *
+ * @author Keesun Baik
+ * @see {@link actions.IsOnlyGitAvailableAction}
  */
-@With(PullRequestCheckAction.class)
+@With(IsOnlyGitAvailableAction.class)
 @Target({ElementType.TYPE, ElementType.METHOD})
 @Retention(RetentionPolicy.RUNTIME)
-public @interface PullRequestAccess {
-    Operation value();
+public @interface IsOnlyGitAvailable {
+
 }
 
app/controllers/annotation/ProjectAccess.java (deleted)
--- app/controllers/annotation/ProjectAccess.java
@@ -1,47 +0,0 @@
-/**
- * Yobi, Project Hosting SW
- *
- * Copyright 2013 NAVER Corp.
- * http://yobi.io
- *
- * @Author Wansoon Park, Keesun Baek
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package controllers.annotation;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-import models.enumeration.Operation;
-import play.mvc.With;
-import actions.ProjectCheckAction;
-
-/**
- * {@code Project}의 {@code Operation} 권한을 체크한다.
- *
- * 이 애노테이션은 /user/project/** 패턴에 해당하는 요청만 처리할 수 있다.
- * 그 이외 요청을 처리하는 핸들러에 사용하면 에러가 발생하고 요청이 처리되지 않는다.
- *
- * @author Wansoon Park, Keesun Beak
- *
- */
-@With(ProjectCheckAction.class)
-@Target({ElementType.TYPE, ElementType.METHOD})
-@Retention(RetentionPolicy.RUNTIME)
-public @interface ProjectAccess {
-    Operation value();
-    boolean isGitOnly() default false;
-}
app/models/Posting.java
--- app/models/Posting.java
+++ app/models/Posting.java
@@ -117,7 +117,7 @@
      * @param number
      * @return
      */
-    public static Posting findByNumber(Project project, Long number) {
+    public static Posting findByNumber(Project project, long number) {
         return AbstractPosting.findByNumber(finder, project, number);
     }
 
app/models/resource/Resource.java
--- app/models/resource/Resource.java
+++ app/models/resource/Resource.java
@@ -1,5 +1,7 @@
 package models.resource;
 
+import actions.support.PathParser;
+import controllers.routes;
 import models.*;
 import models.enumeration.ResourceType;
 import play.db.ebean.Model;
@@ -139,4 +141,37 @@
     public Resource getContainer() { return null; }
     public Long getAuthorId() { return null; }
     public void delete() { throw new UnsupportedOperationException(); }
+
+    /**
+     * {@code parser}와 {@code project} 정보에서 {@code resourceType}에 해당하는 리소스를 찾는다.
+     * - 해당하는 리소스가 있을 때는 {@link models.resource.ResourceConvertible} 타입 객체를 리턴한다.
+     * - 해당하는 리소스가 없을 때는 리턴한 레퍼런스가 null일 수도 있다.
+     *
+     * @param parser
+     * @param resourceType
+     * @param project
+     * @return
+     * @see {@link actions.IsAllowedAction}
+     */
+    public static ResourceConvertible getResourceObject(PathParser parser, Project project, ResourceType resourceType) {
+        switch (resourceType) {
+            case PROJECT:
+                return project;
+            case MILESTONE:
+                return Milestone.findById(Long.parseLong(parser.getPathSegment(3)));
+            case BOARD_POST:
+                return Posting.findByNumber(project, Long.parseLong(parser.getPathSegment(3)));
+            case ISSUE_POST:
+                return Issue.findByNumber(project, Long.parseLong(parser.getPathSegment(3)));
+            case ISSUE_LABEL:
+                return IssueLabel.finder.byId(Long.parseLong(parser.getPathSegment(4)));
+            case PULL_REQUEST:
+                return PullRequest.findOne(project, Long.parseLong(parser.getPathSegment(3)));
+            case COMMIT_COMMENT:
+                return CommitComment.find.byId(Long.parseLong(parser.getPathSegment(5)));
+            default:
+                throw new IllegalAccessError(getInvalidResourceTypeMessage(resourceType));
+        }
+    }
+
 }
app/views/error/notfound.scala.html
--- app/views/error/notfound.scala.html
+++ app/views/error/notfound.scala.html
@@ -2,16 +2,18 @@
 
 @getMenuType(target:String) = @{
     target match {
-        case "issues" => utils.MenuType.ISSUE
-        case "post" => utils.MenuType.BOARD
+        case "issue_post" => utils.MenuType.ISSUE
+        case "board_post" => utils.MenuType.BOARD
+        case "milestone" => utils.MenuType.MILESTONE
         case _ => utils.MenuType.PROJECT_HOME
     }
 }
 
 @getReturnURL(target:String) = @{
     target match {
-        case "issues" => routes.IssueApp.issues(project.owner, project.name, "all")
-        case "post" => routes.BoardApp.posts(project.owner, project.name)
+        case "issue_post" => routes.IssueApp.issues(project.owner, project.name, "all")
+        case "board_post" => routes.BoardApp.posts(project.owner, project.name)
+        case "milestone" => routes.MilestoneApp.milestones(project.owner, project.name)
         case _ => "javascript:history.back();"
     }
 }
conf/messages
--- conf/messages
+++ conf/messages
@@ -142,14 +142,17 @@
 error.auth.unauthorized.comment = You need authorization to comment
 error.auth.unauthorized.waringMessage = You don't have authorization or aren't logged in
 error.badrequest = The request cannot be fulfilled due to bad syntax
-error.forbidden = You need access Control!
+error.badrequest.only.available.for.git = This request is only available for a git project.
+error.forbidden = You are not authorized
 error.internalServerError = Server error occurred and the service is not available
 error.notfound = Page not found
-error.notfound.issue = Issue not found
-error.notfound.post = Post not found
+error.notfound.code_comment = Commit-comment not found
+error.notfound.issue_post = Issue not found
+error.notfound.board_post = Post not found
 error.notfound.watch = Watch not found
 error.notfound.commit = Commit not found
 error.notfound.project = Project not found
+error.notfound.milestone = Milestone not found
 error.required = Mandatory field is empty
 error.tooLargeText.title = Request text entity is too large
 error.tooLargeText.limit = Your request has an text entity which exceeds the limit "{0}" bytes.
conf/messages.ja
--- conf/messages.ja
+++ conf/messages.ja
@@ -145,8 +145,8 @@
 error.forbidden = 権限がありません
 error.internalServerError = サーバエラーでサービスを利用できません
 error.notfound = 存在しないページです
-error.notfound.issue = 存在しないイシューです
-error.notfound.post = 存在しないスレッドです
+error.notfound.issue_post = 存在しないイシューです
+error.notfound.board_post = 存在しないスレッドです
 error.notfound.watch = 存在しなおメッセージです
 error.notfound.commit = Commit not found
 error.required = 必須項目です
conf/messages.ko
--- conf/messages.ko
+++ conf/messages.ko
@@ -142,14 +142,17 @@
 error.auth.unauthorized.comment = 로그인 후 댓글 입력이 가능합니다.
 error.auth.unauthorized.waringMessage = 권한이 없거나 로그인을 하지 않았습니다.
 error.badrequest = 잘못된 요청입니다
+error.badrequest.only.available.for.git = GIT 프로젝트에서만 지원하는 요청입니다.
 error.forbidden = 권한이 없습니다
 error.internalServerError = 서버 오류가 발생하여 서비스를 이용할 수 없습니다
 error.notfound = 페이지를 찾을 수 없습니다
-error.notfound.issue = 존재하지 않는 이슈입니다
-error.notfound.post = 존재하지 않는 글입니다
+error.notfound.code_comment = 존재하지 않는 커밋 댓글입니다.
+error.notfound.issue_post = 존재하지 않는 이슈입니다
+error.notfound.board_post = 존재하지 않는 글입니다
 error.notfound.watch = 존재하지 않는 알림 정보입니다.
 error.notfound.commit = 존재하지 않는 커밋입니다.
 error.notfound.project = 존재하지 않는 프로젝트입니다.
+error.notfound.milestone = 존재하지 않는 마일스톤입니다.
 error.required = 필수 입력란입니다.
 error.tooLargeText.title = 너무 큰 텍스트 데이터를 보냈습니다.
 error.tooLargeText.limit = 텍스트 데이터는 최대 "{0}" 바이트까지만 보낼 수 있습니다.
test/actions/support/PathParserTest.java
--- test/actions/support/PathParserTest.java
+++ test/actions/support/PathParserTest.java
@@ -5,7 +5,7 @@
  * http://yobi.io
  *
  * @Author Wansoon Park
- * 
+ *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
@@ -20,14 +20,8 @@
  */
 package actions.support;