OSDN Git Service

Adding authenticated public mode (internal).
authorJason Hollingsworth <jhworth.developer@gmail.com>
Wed, 6 Nov 2013 15:13:21 +0000 (09:13 -0600)
committerJason Hollingsworth <jhworth.developer@gmail.com>
Wed, 27 Nov 2013 04:22:07 +0000 (22:22 -0600)
Added visibility_level icons to project view (rather than just text).
Added public projects to search results.
Added ability to restrict visibility levels standard users can set.

50 files changed:
app/assets/stylesheets/common.scss
app/assets/stylesheets/gitlab_bootstrap/common.scss
app/assets/stylesheets/sections/admin.scss
app/assets/stylesheets/sections/projects.scss
app/contexts/projects/create_context.rb
app/contexts/projects/update_context.rb
app/contexts/search_context.rb
app/controllers/admin/projects_controller.rb
app/controllers/application_controller.rb
app/controllers/projects/application_controller.rb
app/controllers/projects_controller.rb
app/controllers/public/projects_controller.rb
app/controllers/search_controller.rb
app/helpers/icons_helper.rb
app/helpers/search_helper.rb
app/helpers/visibility_level_helper.rb [new file with mode: 0644]
app/models/ability.rb
app/models/project.rb
app/views/admin/projects/index.html.haml
app/views/admin/projects/show.html.haml
app/views/dashboard/projects.html.haml
app/views/groups/edit.html.haml
app/views/help/permissions.html.haml
app/views/help/public_access.html.haml
app/views/projects/_home_panel.html.haml [new file with mode: 0644]
app/views/projects/_visibility_level.html.haml [new file with mode: 0644]
app/views/projects/edit.html.haml
app/views/projects/empty.html.haml
app/views/projects/new.html.haml
app/views/projects/show.html.haml
app/views/public/projects/index.html.haml
config/gitlab.yml.example
config/initializers/1_settings.rb
db/migrate/20131112220935_add_visibility_level_to_projects.rb [new file with mode: 0644]
db/schema.rb
doc/api/projects.md
features/public/public_projects.feature
features/steps/public/projects_feature.rb
lib/api/entities.rb
lib/api/projects.rb
lib/gitlab/backend/grack_auth.rb
lib/gitlab/visibility_level.rb [new file with mode: 0644]
spec/contexts/projects_create_context_spec.rb
spec/contexts/projects_update_context_spec.rb [new file with mode: 0644]
spec/contexts/search_context_spec.rb
spec/features/security/project/internal_access_spec.rb [new file with mode: 0644]
spec/features/security/project/private_access_spec.rb
spec/features/security/project/public_access_spec.rb
spec/models/project_spec.rb
spec/requests/api/projects_spec.rb

index 935d24e..ce89729 100644 (file)
@@ -365,6 +365,10 @@ table {
   &.input-large {
     width: 210px;
   }
+
+  &.input-clamp {
+    max-width: 100%;
+  }
 }
 
 .user-result {
index b5fd1fc..26fe02e 100644 (file)
@@ -6,6 +6,7 @@
 .cblue { color: #29A }
 .cblack { color: #111 }
 .cdark { color: #444 }
+.camber { color: #ffc000 }
 .cwhite { color: #fff!important }
 .bgred { background: #F2DEDE!important }
 
index 82556e9..8ad9bc7 100644 (file)
   label { width: 110px; }
   .controls { margin-left: 130px; }
   .form-actions { padding-left: 130px; background: #fff }
+  .visibility-levels {
+    .controls {
+        margin-bottom: 9px;
+    }
+
+    i {
+      color: inherit;
+    }
+  }
 }
 
 .broadcast-messages {
index e9162b3..22bd6fb 100644 (file)
   border-bottom: 1px solid #DDD;
   padding-bottom: 25px;
   margin-bottom: 30px;
+  
+  &.empty-project {
+      border-bottom: 0px;
+      padding-bottom: 15px;
+      margin-bottom: 0px;
+  }
 
   .project-home-title {
     font-size: 18px;
@@ -45,7 +51,7 @@
     }
   }
 
-  .public-label {
+  .visibility-level-label {
     font-size: 14px;
     background: #f1f1f1;
     padding: 8px 10px;
     margin-left: 10px;
     color: #888;
     text-shadow: 0 1px 1px #FFF;
+
+    i {
+      color: inherit;
+    }
   }
 }
 
   }
 }
 
-.project-public-holder {
-  .help-inline {
-    padding-top: 7px;
+.project-visibility-level-holder {
+  .controls {
+    padding-bottom: 9px;
+  }
+
+  .controls {
+    input {
+      float: left;
+    }
+    .descr {
+      display: block;
+      margin-left: 1.5em;
+      &.restricted {
+        color: #888;
+      }
+    }
+    .info {
+      display: block;
+      margin-top: 5px;
+    }
+    strong {
+      display: inline-block;
+      width: 4em;
+    }
+  }
+  i {
+    color: inherit;
   }
 }
 
@@ -130,7 +164,8 @@ ul.nav.nav-projects-tabs {
   margin: 0px;
 }
 
-.my-projects {
+.my-projects,
+.public-projects {
   li {
     .project-info {
       margin-bottom: 10px;
index 904c60a..2acb9fb 100644 (file)
@@ -8,6 +8,11 @@ module Projects
       # get namespace id
       namespace_id = params.delete(:namespace_id)
 
+      # check that user is allowed to set specified visibility_level
+      unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
+        params.delete(:visibility_level)
+      end
+
       # Load default feature settings
       default_features = Gitlab.config.gitlab.default_projects_features
 
@@ -17,7 +22,7 @@ module Projects
         wall_enabled: default_features.wall,
         snippets_enabled: default_features.snippets,
         merge_requests_enabled: default_features.merge_requests,
-        public: default_features.public
+        visibility_level: default_features.visibility_level
       }.stringify_keys
 
       @project = Project.new(default_opts.merge(params))
index 9564dd9..27d7a95 100644 (file)
@@ -2,7 +2,11 @@ module Projects
   class UpdateContext < BaseContext
     def execute(role = :default)
       params[:project].delete(:namespace_id)
-      params[:project].delete(:public) unless can?(current_user, :change_public_mode, project)
+      # check that user is allowed to set specified visibility_level
+      unless can?(current_user, :change_visibility_level, project) && Gitlab::VisibilityLevel.allowed_for?(current_user, params[:project][:visibility_level])
+        params[:project].delete(:visibility_level)
+      end
+
       new_branch = params[:project].delete(:default_branch)
 
       if project.repository.exists? && new_branch != project.repository.root_ref
index 2f9438f..5985ab1 100644 (file)
@@ -1,8 +1,8 @@
 class SearchContext
-  attr_accessor :project_ids, :params
+  attr_accessor :project_ids, :current_user, :params
 
-  def initialize(project_ids, params)
-    @project_ids, @params = project_ids, params.dup
+  def initialize(project_ids, user, params)
+    @project_ids, @current_user, @params = project_ids, user, params.dup
   end
 
   def execute
@@ -10,7 +10,8 @@ class SearchContext
     query = Shellwords.shellescape(query) if query.present?
 
     return result unless query.present?
-    result[:projects] = Project.where("projects.id in (?) OR projects.public = true", project_ids).search(query).limit(20)
+    visibility_levels = @current_user ? [ Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC ] : [ Gitlab::VisibilityLevel::PUBLIC ]
+    result[:projects] = Project.where("projects.id in (?) OR projects.visibility_level in (?)", project_ids, visibility_levels).search(query).limit(20)
 
     # Search inside single project
     single_project_search(Project.where(id: project_ids), query)
index 4de0a65..0e8335f 100644 (file)
@@ -8,7 +8,7 @@ class Admin::ProjectsController < Admin::ApplicationController
     user = User.find_by_id(owner_id)
 
     @projects = user ? user.owned_projects : Project.scoped
-    @projects = @projects.where(public: true) if params[:public_only].present?
+    @projects = @projects.where("visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present?
     @projects = @projects.with_push if params[:with_push].present?
     @projects = @projects.abandoned if params[:abandoned].present?
     @projects = @projects.search(params[:name]) if params[:name].present?
index cfa3cac..68ea636 100644 (file)
@@ -102,7 +102,7 @@ class ApplicationController < ActionController::Base
   end
 
   def authorize_code_access!
-    return access_denied! unless can?(current_user, :download_code, project) or project.public?
+    return access_denied! unless can?(current_user, :download_code, project)
   end
 
   def authorize_push!
index 80aeb5c..7e45800 100644 (file)
@@ -10,7 +10,7 @@ class Projects::ApplicationController < ApplicationController
       id = params[:project_id] || params[:id]
       @project = Project.find_with_namespace(id)
 
-      return if @project && @project.public
+      return if @project && @project.public?
     end
 
     super
index 66d6edd..6000318 100644 (file)
@@ -55,7 +55,7 @@ class ProjectsController < ApplicationController
   end
 
   def show
-    return authenticate_user! unless @project.public || current_user
+    return authenticate_user! unless @project.public? || current_user
 
     limit = (params[:limit] || 20).to_i
     @events = @project.events.recent
index 87e903a..8d66250 100644 (file)
@@ -6,7 +6,7 @@ class Public::ProjectsController < ApplicationController
   layout 'public'
 
   def index
-    @projects = Project.public_only
+    @projects = Project.public_or_internal_only(current_user)
     @projects = @projects.search(params[:search]) if params[:search].present?
     @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]).per(20)
   end
index f5c3bb1..2a2748d 100644 (file)
@@ -14,7 +14,7 @@ class SearchController < ApplicationController
       project_ids.select! { |id| id == project_id.to_i}
     end
 
-    result = SearchContext.new(project_ids, params).execute
+    result = SearchContext.new(project_ids, current_user, params).execute
 
     @projects       = result[:projects]
     @merge_requests = result[:merge_requests]
index e4dfc23..1688cfc 100644 (file)
@@ -11,6 +11,10 @@ module IconsHelper
     content_tag :i, nil, class: 'icon-globe cblue'
   end
 
+  def internal_icon
+    content_tag :i, nil, class: 'icon-shield camber'
+  end
+
   def private_icon
     content_tag :i, nil, class: 'icon-lock cgreen'
   end
index 33c5e4f..8ff0bc6 100644 (file)
@@ -1,10 +1,10 @@
 module SearchHelper
   def search_autocomplete_source
     return unless current_user
-
     [
       groups_autocomplete,
       projects_autocomplete,
+      public_projects_autocomplete,
       default_autocomplete,
       project_autocomplete,
       help_autocomplete
@@ -75,4 +75,11 @@ module SearchHelper
       { label: "project: #{simple_sanitize(p.name_with_namespace)}", url: project_path(p) }
     end
   end
+
+  # Autocomplete results for the current user's projects
+  def public_projects_autocomplete
+    Project.public_or_internal_only(current_user).map do |p|
+      { label: "project: #{simple_sanitize(p.name_with_namespace)}", url: project_path(p) }
+    end
+  end
 end
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
new file mode 100644 (file)
index 0000000..5cec7f8
--- /dev/null
@@ -0,0 +1,55 @@
+module VisibilityLevelHelper
+  def visibility_level_color(level)
+    case level
+    when Gitlab::VisibilityLevel::PRIVATE
+      'cgreen'
+    when Gitlab::VisibilityLevel::INTERNAL
+      'camber'
+    when Gitlab::VisibilityLevel::PUBLIC
+      'cblue'
+    end
+  end
+
+  def visibility_level_description(level)
+    capture_haml do
+      haml_tag :span do
+        case level
+        when Gitlab::VisibilityLevel::PRIVATE
+          haml_concat "Project access must be granted explicitly for each user."
+        when Gitlab::VisibilityLevel::INTERNAL
+          haml_concat "The project can be cloned by"
+          haml_tag :em, "any logged in user."
+          haml_concat "It will also be listed on the #{link_to "public access directory", public_root_path} for logged in users."
+          haml_tag :em, "Any logged in user"
+          haml_concat "will have #{link_to "Guest", help_permissions_path} permissions on the repository."
+        when Gitlab::VisibilityLevel::PUBLIC
+          haml_concat "The project can be cloned"
+          haml_tag :em, "without any"
+          haml_concat "authentication."
+          haml_concat "It will also be listed on the #{link_to "public access directory", public_root_path}."
+          haml_tag :em, "Any logged in user"
+          haml_concat "will have #{link_to "Guest", help_permissions_path} permissions on the repository."
+        end
+      end
+    end
+  end
+
+  def visibility_level_icon(level)
+    case level
+    when Gitlab::VisibilityLevel::PRIVATE
+      private_icon
+    when Gitlab::VisibilityLevel::INTERNAL
+      internal_icon
+    when Gitlab::VisibilityLevel::PUBLIC
+      public_icon
+    end
+  end
+
+  def visibility_level_label(level)
+    Project.visibility_levels.key(level)
+  end
+  
+  def restricted_visibility_levels
+    current_user.is_admin? ? [] : gitlab_config.restricted_visibility_levels
+  end
+end
\ No newline at end of file
index 8547608..6df56ee 100644 (file)
@@ -29,7 +29,7 @@ class Ability
                   nil
                 end
 
-      if project && project.public
+      if project && project.public?
         [
           :read_project,
           :read_wiki,
@@ -71,7 +71,7 @@ class Ability
         rules << project_guest_rules
       end
 
-      if project.public?
+      if project.public? || project.internal?
         rules << public_project_rules
       end
 
@@ -89,7 +89,7 @@ class Ability
     def public_project_rules
       project_guest_rules + [
         :download_code,
-        :fork_project,
+        :fork_project
       ]
     end
 
@@ -145,7 +145,7 @@ class Ability
     def project_admin_rules
       project_master_rules + [
         :change_namespace,
-        :change_public_mode,
+        :change_visibility_level,
         :rename_project,
         :remove_project
       ]
index eab7c14..9ac078f 100644 (file)
 #  merge_requests_enabled :boolean          default(TRUE), not null
 #  wiki_enabled           :boolean          default(TRUE), not null
 #  namespace_id           :integer
-#  public                 :boolean          default(FALSE), not null
 #  issues_tracker         :string(255)      default("gitlab"), not null
 #  issues_tracker_id      :string(255)
 #  snippets_enabled       :boolean          default(TRUE), not null
 #  last_activity_at       :datetime
 #  imported               :boolean          default(FALSE), not null
 #  import_url             :string(255)
+#  visibility_level       :integer          default(0), not null
 #
 
 class Project < ActiveRecord::Base
   include Gitlab::ShellAdapter
+  include Gitlab::VisibilityLevel
   extend Enumerize
    
   ActsAsTaggableOn.strict_case_match = true
 
   attr_accessible :name, :path, :description, :issues_tracker, :label_list,
     :issues_enabled, :wall_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id,
-    :wiki_enabled, :public, :import_url, :last_activity_at, as: [:default, :admin]
+    :wiki_enabled, :visibility_level, :import_url, :last_activity_at, as: [:default, :admin]
 
   attr_accessible :namespace_id, :creator_id, as: :admin
 
@@ -108,7 +109,8 @@ class Project < ActiveRecord::Base
   scope :sorted_by_activity, -> { reorder("projects.last_activity_at DESC") }
   scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
   scope :joined, ->(user) { where("namespace_id != ?", user.namespace_id) }
-  scope :public_only, -> { where(public: true) }
+  scope :public_only, -> { where(visibility_level: PUBLIC) }
+  scope :public_or_internal_only, ->(user) { where("visibility_level IN (:levels)", levels: user ? [ INTERNAL, PUBLIC ] : [ PUBLIC ]) }
 
   enumerize :issues_tracker, in: (Gitlab.config.issues_tracker.keys).append(:gitlab), default: :gitlab
 
@@ -140,6 +142,10 @@ class Project < ActiveRecord::Base
         where(path: id, namespace_id: nil).last
       end
     end
+    
+    def visibility_levels
+      Gitlab::VisibilityLevel.options
+    end
   end
 
   def team
@@ -451,4 +457,8 @@ class Project < ActiveRecord::Base
   def default_branch
     @default_branch ||= repository.root_ref if repository.exists?
   end
+  
+  def visibility_level_field
+    visibility_level
+  end
 end
index bc29720..05236e3 100644 (file)
         .control-group
           = label_tag :owner_id, 'Owner:', class: 'control-label'
           .controls
-            = users_select_tag :owner_id, selected: params[:owner_id], class: 'input-large'
-        .control-group
-          = label_tag :public_only, 'Public Only', class: 'control-label'
-          .controls
-            = check_box_tag :public_only, 1, params[:public_only]
+            = users_select_tag :owner_id, selected: params[:owner_id], class: 'input-large input-clamp'
+        .control-group.visibility-levels
+          = label_tag :visibility_level, 'Visibility Levels', class: 'control-label'
+          - Project.visibility_levels.each do |label, level|
+            .controls
+              = check_box_tag 'visibility_levels[]', level, params[:visibility_levels].present? && params[:visibility_levels].include?(level.to_s)
+              %span.descr
+                = visibility_level_icon(level)
+                = label
         .control-group
           = label_tag :with_push, 'Not empty', class: 'control-label'
           .controls
       %ul.well-list
         - @projects.each do |project|
           %li
-            - if project.public
-              = public_icon
-            - else
-              = private_icon
+            = visibility_level_icon(project.visibility_level)
             = link_to project.name_with_namespace, [:admin, project]
             .pull-right
               = link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-small"
index c9c9c38..42c427a 100644 (file)
         %li
           %span.light access:
           %strong
-            - if @project.public
-              %span.cblue
-                %i.icon-share
-                Public
-            - else
-              %span.cgreen
-                %i.icon-lock
-                Private
+            %span{ class: visibility_level_color(@project.visibility_level) }
+              = visibility_level_icon(@project.visibility_level)
+              = visibility_level_label(@project.visibility_level)
+
     .ui-box
       .title
         Transfer project
@@ -88,9 +84,6 @@
             .controls
               = f.submit 'Transfer', class: 'btn btn-primary'
 
-
-
-
   .span6
     - if @group
       .ui-box
index 904ac2d..5ac9059 100644 (file)
           %h4.project-title
             = link_to project_path(project), class: dom_class(project) do
               = project.name_with_namespace
-            - if project.public
+            - unless project.private?
               %small.access-icon
-                = public_icon
-                Public
+                = visibility_level_icon(project.visibility_level)
+                = visibility_level_label(project.visibility_level)
 
             - if current_user.can_leave_project?(project)
               .pull-right
index 10b974e..5b5f8a2 100644 (file)
           %ul.well-list
             - @group.projects.each do |project|
               %li
-                - if project.public
-                  = public_icon
-                - else
-                  = private_icon
+                = visibility_level_icon(project.visibility_level)
                 = link_to project.name_with_namespace, project
                 .pull-right
                   = link_to 'Members', project_team_index_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-small"
index df35f41..15e3bf3 100644 (file)
         %td.permission-x &#10003;
         %td.permission-x &#10003;
       %tr
-        %td Switch public mode
+        %td Switch visibility level
         %td
         %td
         %td
index c67402e..3a8aedc 100644 (file)
@@ -2,14 +2,20 @@
   %h3.page-title Public Access
 
   %p
-    GitLab allows you to open selected projects to be accessed publicly.
-    These projects will be cloneable
+    GitLab allows you to open selected projects to be accessed publicly or internally.
+    Projects with either of these visibility levels will be listed in the #{link_to "public access directory", public_root_path}. Internal projects will only be available to authenticated users.
+  %p
+    = public_icon
+    Public projects will be cloneable
     %em without any
     authentication.
-    Also they will be listed on the #{link_to "public access directory", public_root_path}.
+  %p
+    = internal_icon
+    Internal projects will be cloneable by
+    %em any authenticated user.
 
   %ol
     %li Go to your project dashboard
     %li Click on the "Edit" tab
-    %li Select "Public clone access"
+    %li Change "Visibility Level"
 
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
new file mode 100644 (file)
index 0000000..19c150b
--- /dev/null
@@ -0,0 +1,31 @@
+- empty_repo = @project.empty_repo?
+.project-home-panel{:class => ("empty-project" if empty_repo)}
+  .row
+    .span5
+      %h4.project-home-title
+        = @project.name_with_namespace
+        %span.visibility-level-label
+          = visibility_level_icon(@project.visibility_level)
+          = visibility_level_label(@project.visibility_level)
+
+    .span7
+      - unless empty_repo
+        .project-home-dropdown
+          = render "dropdown"
+      .form-horizontal
+        = render "shared/clone_panel"
+
+  .project-home-extra.clearfix
+    .project-home-desc
+      - if @project.description.present?
+        = @project.description
+      - if can?(current_user, :admin_project, @project)
+        &ndash;
+        %strong= link_to 'Edit', edit_project_path
+
+    - unless empty_repo
+      .project-home-links
+        = link_to pluralize(@repository.round_commit_count, 'commit'), project_commits_path(@project, @ref || @repository.root_ref)
+        = link_to pluralize(@repository.branch_names.count, 'branch'), project_branches_path(@project)
+        = link_to pluralize(@repository.tag_names.count, 'tag'), project_tags_path(@project)
+        %span.light.prepend-left-20= repository_size
\ No newline at end of file
diff --git a/app/views/projects/_visibility_level.html.haml b/app/views/projects/_visibility_level.html.haml
new file mode 100644 (file)
index 0000000..1bce3ba
--- /dev/null
@@ -0,0 +1,23 @@
+.control-group.project-visibility-level-holder
+  = f.label :visibility_level, "Visibility Level"
+  - if can_change_visibility_level
+    - Gitlab::VisibilityLevel.values.each do |level|
+      - restricted = restricted_visibility_levels.include?(level)
+      .controls
+        = f.radio_button :visibility_level, level, checked: (visibility_level == level), disabled: restricted
+        %span.descr{:class => ("restricted" if restricted)}
+          = visibility_level_icon(level)
+          %strong
+            = visibility_level_label(level)
+          = visibility_level_description(level)
+    - unless restricted_visibility_levels.empty?
+      .controls
+        %span.info
+          Some visibility level settings have been restricted by the administrator.
+  - else
+    .controls
+      %span.info
+        = visibility_level_icon(visibility_level)
+        %strong
+          = visibility_level_label(visibility_level)
+        = visibility_level_description(visibility_level)
\ No newline at end of file
index 3afc607..d282fc6 100644 (file)
                 .controls= f.select(:default_branch, @repository.branch_names, {}, {class: 'chosen'})
 
 
-          - if can?(current_user, :change_public_mode, @project)
-            %fieldset.public-mode
-              %legend
-                Public mode:
-              .control-group
-                = f.label :public, class: 'control-label' do
-                  %span Public access
-                .controls
-                  = f.check_box :public
-                  %span.descr
-                    If checked, this project can be cloned
-                    %em without any
-                    authentication.
-                    It will also be listed on the #{link_to "public access directory", public_root_path}.
-                    %em Any
-                    user will have #{link_to "Guest", help_permissions_path} permissions on the repository.
+          = render "visibility_level", f: f, visibility_level: @project.visibility_level, can_change_visibility_level: can?(current_user, :change_visibility_level, @project)
 
           %fieldset.features
             %legend
index 04fc0c3..3ed2201 100644 (file)
@@ -1,7 +1,4 @@
-%h3.page-title
-  = @project.name_with_namespace
-  .form-horizontal.pull-right
-    = render "shared/clone_panel"
+= render "home_panel"
 
 - if @project.import? && !@project.imported
   .save-project-loader
index 466a63d..ee6c42b 100644 (file)
           %span.light (optional)
         .controls
           = f.text_area :description, placeholder: "Awesome project", class: "input-xlarge", rows: 3, maxlength: 250, tabindex: 3
-      .control-group.project-public-holder
-        = f.label :public do
-          %span Public project
-        .controls
-          = f.check_box :public, { checked: gitlab_config.default_projects_features.public }, true, false
-          %span.help-inline Make project visible to everyone
+      = render "visibility_level", f: f, visibility_level: gitlab_config.default_projects_features.visibility_level, can_change_visibility_level: true
 
       .form-actions
         = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
index 1f4b583..41035d9 100644 (file)
@@ -1,32 +1,4 @@
-.project-home-panel
-  .row
-    .span5
-      %h4.project-home-title
-        = @project.name_with_namespace
-        - if @project.public
-          %span.public-label Public
-        - else
-          %span.public-label Private
-
-    .span7
-      .project-home-dropdown
-        = render "dropdown"
-      .form-horizontal
-        = render "shared/clone_panel"
-
-  .project-home-extra.clearfix
-    .project-home-desc
-      - if @project.description.present?
-        = @project.description
-      - if can?(current_user, :admin_project, @project)
-        &ndash;
-        %strong= link_to 'Edit', edit_project_path
-
-    .project-home-links
-      = link_to pluralize(@repository.round_commit_count, 'commit'), project_commits_path(@project, @ref || @repository.root_ref)
-      = link_to pluralize(@repository.branch_names.count, 'branch'), project_branches_path(@project)
-      = link_to pluralize(@repository.tag_names.count, 'tag'), project_tags_path(@project)
-      %span.light.prepend-left-20= repository_size
+= render "home_panel"
 
 .row
   .span9
index 21aee64..b88169a 100644 (file)
         %h4
           = link_to project_path(project) do
             = project.name_with_namespace
+          - if project.internal?
+            %small.access-icon
+              = internal_icon
+              Internal
           .pull-right
             %pre.public-clone git clone #{project.http_url_to_repo}
 
index 759d743..6b527cf 100644 (file)
@@ -55,6 +55,10 @@ production: &base
     # default: false - Account passwords are not sent via the email if signup is enabled. 
     # signup_enabled: true
 
+    # Restrict setting visibility levels for non-admin users.
+    # The default is to allow all levels.
+    #restricted_visibility_levels: [ "public" ]
+
     ## Automatic issue closing
     # If a commit message matches this regular expression, all issues referenced from the matched text will be closed.
     # This happens when the commit is pushed or merged into the default branch of a project.
@@ -68,7 +72,7 @@ production: &base
       wiki: true
       wall: false
       snippets: false
-      public: false
+      visibility_level: "private"  # can be "private" | "internal" | "public"
 
   ## External issues trackers
   issues_tracker:
index 942b77f..06e0571 100644 (file)
@@ -30,6 +30,29 @@ class Settings < Settingslogic
         gitlab.relative_url_root
       ].join('')
     end
+    
+    # check that values in `current` (string or integer) is a contant in `modul`.
+    def verify_constant_array(modul, current, default)
+      values = default || []
+      if !current.nil?
+        values = []
+        current.each do |constant|
+          values.push(verify_constant(modul, constant, nil))
+        end
+        values.delete_if { |value| value.nil? }
+      end
+      values
+    end
+
+    # check that `current` (string or integer) is a contant in `modul`.
+    def verify_constant(modul, current, default)
+      constant = modul.constants.find{ |name| modul.const_get(name) == current }
+      value = constant.nil? ? default : modul.const_get(constant)
+      if current.is_a? String
+        value = modul.const_get(current.upcase) rescue default
+      end
+      value
+    end
   end
 end
 
@@ -68,6 +91,7 @@ rescue ArgumentError # no user configured
   '/home/' + Settings.gitlab['user']
 end
 Settings.gitlab['signup_enabled'] ||= false
+Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], [])
 Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil?
 Settings.gitlab['issue_closing_pattern'] = '([Cc]loses|[Ff]ixes) #(\d+)' if Settings.gitlab['issue_closing_pattern'].nil?
 Settings.gitlab['default_projects_features'] ||= {}
@@ -76,7 +100,7 @@ Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.g
 Settings.gitlab.default_projects_features['wiki']           = true if Settings.gitlab.default_projects_features['wiki'].nil?
 Settings.gitlab.default_projects_features['wall']           = false if Settings.gitlab.default_projects_features['wall'].nil?
 Settings.gitlab.default_projects_features['snippets']       = false if Settings.gitlab.default_projects_features['snippets'].nil?
-Settings.gitlab.default_projects_features['public']         = false if Settings.gitlab.default_projects_features['public'].nil?
+Settings.gitlab.default_projects_features['visibility_level']    = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
 
 #
 # Gravatar
diff --git a/db/migrate/20131112220935_add_visibility_level_to_projects.rb b/db/migrate/20131112220935_add_visibility_level_to_projects.rb
new file mode 100644 (file)
index 0000000..cf1e9f9
--- /dev/null
@@ -0,0 +1,13 @@
+class AddVisibilityLevelToProjects < ActiveRecord::Migration
+  def self.up
+    add_column :projects, :visibility_level, :integer, :default => 0, :null => false
+    Project.where(public: true).update_all(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+    remove_column :projects, :public
+  end
+
+  def self.down
+    add_column :projects, :public, :boolean, :default => false, :null => false
+    Project.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).update_all(public: true)
+    remove_column :projects, :visibility_level
+  end
+end
index a03e471..24d2fef 100644 (file)
@@ -11,7 +11,7 @@
 #
 # It's strongly recommended to check this file into your version control system.
 
-ActiveRecord::Schema.define(:version => 20131112114325) do
+ActiveRecord::Schema.define(:version => 20131112220935) do
 
   create_table "broadcast_messages", :force => true do |t|
     t.text     "message",    :null => false
@@ -185,13 +185,13 @@ ActiveRecord::Schema.define(:version => 20131112114325) do
     t.boolean  "merge_requests_enabled", :default => true,     :null => false
     t.boolean  "wiki_enabled",           :default => true,     :null => false
     t.integer  "namespace_id"
-    t.boolean  "public",                 :default => false,    :null => false
     t.string   "issues_tracker",         :default => "gitlab", :null => false
     t.string   "issues_tracker_id"
     t.boolean  "snippets_enabled",       :default => true,     :null => false
     t.datetime "last_activity_at"
     t.boolean  "imported",               :default => false,    :null => false
     t.string   "import_url"
+    t.integer  "visibility_level",       :default => 0,        :null => false
   end
 
   add_index "projects", ["creator_id"], :name => "index_projects_on_owner_id"
index 9b5d62a..5ec4c4a 100644 (file)
@@ -15,6 +15,7 @@ GET /projects
     "description": null,
     "default_branch": "master",
     "public": false,
+    "visibility_level": 0,
     "ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git",
     "http_url_to_repo": "http://example.com/diaspora/diaspora-client.git",
     "web_url": "http://example.com/diaspora/diaspora-client",
@@ -49,6 +50,7 @@ GET /projects
     "description": null,
     "default_branch": "master",
     "public": false,
+    "visibility_level": 0,
     "ssh_url_to_repo": "git@example.com:brightbox/puppet.git",
     "http_url_to_repo": "http://example.com/brightbox/puppet.git",
     "web_url": "http://example.com/brightbox/puppet",
@@ -117,6 +119,7 @@ Parameters:
   "description": null,
   "default_branch": "master",
   "public": false,
+  "visibility_level": 0,
   "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
   "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
   "web_url": "http://example.com/diaspora/diaspora-project-site",
@@ -234,7 +237,8 @@ Parameters:
 + `merge_requests_enabled` (optional)
 + `wiki_enabled` (optional) 
 + `snippets_enabled` (optional)
-+ `public` (optional)
++ `public` (optional) - if `true` same as setting visibility_level = 20
++ `visibility_level` (optional)
 
 
 ### Create project for user
@@ -256,7 +260,8 @@ Parameters:
 + `merge_requests_enabled` (optional)
 + `wiki_enabled` (optional) 
 + `snippets_enabled` (optional)
-+ `public` (optional)
++ `public` (optional) - if `true` same as setting visibility_level = 20
++ `visibility_level` (optional)
 
 
 ## Remove project
index 178a769..86bb888 100644 (file)
@@ -1,18 +1,40 @@
 Feature: Public Projects Feature
   Background:
     Given public project "Community"
+    And internal project "Internal"
     And private project "Enterprise"
 
   Scenario: I visit public area
     When I visit the public projects area
     Then I should see project "Community"
+    And I should not see project "Internal"
     And I should not see project "Enterprise"
 
   Scenario: I visit public project page
     When I visit project "Community" page
     Then I should see project "Community" home page
 
+  Scenario: I visit internal project page
+    When I visit project "Internal" page
+    Then page status code should be 404
+
+  Scenario: I visit private project page
+    When I visit project "Enterprise" page
+    Then page status code should be 404
+
   Scenario: I visit an empty public project page
     Given public empty project "Empty Public Project"
     When I visit empty project page
     Then I should see empty public project details
+
+  Scenario: I visit public area as user
+    Given I sign in as a user
+    When I visit the public projects area
+    Then I should see project "Community"
+    And I should see project "Internal"
+    And I should not see project "Enterprise"
+
+  Scenario: I visit internal project page as user
+    Given I sign in as a user
+    When I visit project "Internal" page
+    Then I should see project "Internal" home page
index e529238..8b61eba 100644 (file)
@@ -1,5 +1,7 @@
 class Spinach::Features::PublicProjectsFeature < Spinach::FeatureSteps
+  include SharedAuthentication
   include SharedPaths
+  include SharedProject
 
   step 'I should see project "Community"' do
     page.should have_content "Community"
@@ -23,11 +25,11 @@ class Spinach::Features::PublicProjectsFeature < Spinach::FeatureSteps
   end
 
   step 'public project "Community"' do
-    create :project_with_code, name: 'Community', public: true
+    create :project_with_code, name: 'Community', visibility_level: Gitlab::VisibilityLevel::PUBLIC
   end
 
   step 'public empty project "Empty Public Project"' do
-    create :project, name: 'Empty Public Project', public: true
+    create :project, name: 'Empty Public Project', visibility_level: Gitlab::VisibilityLevel::PUBLIC
   end
 
   step 'I visit empty project page' do
@@ -48,16 +50,38 @@ class Spinach::Features::PublicProjectsFeature < Spinach::FeatureSteps
     create :project, name: 'Enterprise'
   end
 
+  step 'I visit project "Enterprise" page' do
+    project = Project.find_by_name('Enterprise')
+    visit project_path(project)
+  end
+
   step 'I should see project "Community" home page' do
     within '.project-home-title' do
       page.should have_content 'Community'
     end
   end
 
-  private
+  step 'internal project "Internal"' do
+    create :project_with_code, name: 'Internal', visibility_level: Gitlab::VisibilityLevel::INTERNAL
+  end
 
-  def project
-    @project ||= Project.find_by_name("Community")
+  step 'I should see project "Internal"' do
+    page.should have_content "Internal"
+  end
+
+  step 'I should not see project "Internal"' do
+    page.should_not have_content "Internal"
+  end
+
+  step 'I visit project "Internal" page' do
+    project = Project.find_by_name('Internal')
+    visit project_path(project)
+  end
+
+  step 'I should see project "Internal" home page' do
+    within '.project-home-title' do
+      page.should have_content 'Internal'
+    end
   end
 end
 
index 2bdcbdc..90cb697 100644 (file)
@@ -31,11 +31,13 @@ module API
     end
 
     class Project < Grape::Entity
-      expose :id, :description, :default_branch, :public, :ssh_url_to_repo, :http_url_to_repo, :web_url
+      expose :id, :description, :default_branch
+      expose :public?, as: :public
+      expose :visibility_level, :ssh_url_to_repo, :http_url_to_repo, :web_url
       expose :owner, using: Entities::UserBasic
       expose :name, :name_with_namespace
       expose :path, :path_with_namespace
-      expose :issues_enabled, :merge_requests_enabled, :wall_enabled, :wiki_enabled, :snippets_enabled, :created_at, :last_activity_at, :public
+      expose :issues_enabled, :merge_requests_enabled, :wall_enabled, :wiki_enabled, :snippets_enabled, :created_at, :last_activity_at
       expose :namespace
       expose :forked_from_project, using: Entities::ForkedFromProject, :if => lambda{ | project, options | project.forked? }
     end
index b927e63..003533f 100644 (file)
@@ -11,6 +11,13 @@ module API
           end
           not_found!
         end
+        
+        def map_public_to_visibility_level(attrs)
+          publik = attrs.delete(:public)
+          publik = [ true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON' ].include?(publik)
+          attrs[:visibility_level] = Gitlab::VisibilityLevel::PUBLIC if !attrs[:visibility_level].present? && publik == true
+          attrs
+        end
       end
 
       # Get a projects list for authenticated user
@@ -76,7 +83,8 @@ module API
       #   wiki_enabled (optional)
       #   snippets_enabled (optional)
       #   namespace_id (optional) - defaults to user namespace
-      #   public (optional) - false by default
+      #   public (optional) - if true same as setting visibility_level = 20
+      #   visibility_level (optional) - 0 by default
       # Example Request
       #   POST /projects
       post do
@@ -90,7 +98,9 @@ module API
                                      :wiki_enabled,
                                      :snippets_enabled,
                                      :namespace_id,
-                                     :public]
+                                     :public,
+                                     :visibility_level]
+        attrs = map_public_to_visibility_level(attrs)
         @project = ::Projects::CreateContext.new(current_user, attrs).execute
         if @project.saved?
           present @project, with: Entities::Project
@@ -114,7 +124,8 @@ module API
       #   merge_requests_enabled (optional)
       #   wiki_enabled (optional)
       #   snippets_enabled (optional)
-      #   public (optional)
+      #   public (optional) - if true same as setting visibility_level = 20
+      #   visibility_level (optional)
       # Example Request
       #   POST /projects/user/:user_id
       post "user/:user_id" do
@@ -128,7 +139,9 @@ module API
                                      :merge_requests_enabled,
                                      :wiki_enabled,
                                      :snippets_enabled,
-                                     :public]
+                                     :public,
+                                     :visibility_level]
+        attrs = map_public_to_visibility_level(attrs)
         @project = ::Projects::CreateContext.new(user, attrs).execute
         if @project.saved?
           present @project, with: Entities::Project
@@ -290,7 +303,8 @@ module API
       #   GET /projects/search/:query
       get "/search/:query" do
         ids = current_user.authorized_projects.map(&:id)
-        projects = Project.where("(id in (?) OR public = true) AND (name LIKE (?))", ids, "%#{params[:query]}%")
+        visibility_levels = [ Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC ]
+        projects = Project.where("(id in (?) OR visibility_level in (?)) AND (name LIKE (?))", ids, visibility_levels, "%#{params[:query]}%")
         present paginate(projects), with: Entities::Project
       end
     end
index e234949..c629144 100644 (file)
@@ -58,7 +58,7 @@ module Grack
         end
 
       else
-        return unauthorized unless project.public
+        return unauthorized unless project.public?
       end
 
       if authorized_git_request?
@@ -80,7 +80,7 @@ module Grack
     def authorize_request(service)
       case service
       when 'git-upload-pack'
-        project.public || can?(user, :download_code, project)
+        can?(user, :download_code, project)
       when'git-receive-pack'
         refs.each do |ref|
           action = if project.protected_branch?(ref)
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
new file mode 100644 (file)
index 0000000..eada9bc
--- /dev/null
@@ -0,0 +1,42 @@
+# Gitlab::VisibilityLevel module
+#
+# Define allowed public modes that can be used for
+# GitLab projects to determine project public mode
+#
+module Gitlab
+  module VisibilityLevel
+    PRIVATE  = 0
+    INTERNAL = 10
+    PUBLIC   = 20
+
+    class << self
+      def values
+        options.values
+      end
+
+      def options
+        {
+          'Private'  => PRIVATE,
+          'Internal' => INTERNAL,
+          'Public'   => PUBLIC
+        }
+      end
+      
+      def allowed_for?(user, level)
+        user.is_admin? || !Gitlab.config.gitlab.restricted_visibility_levels.include?(level)
+      end
+    end
+
+    def private?
+      visibility_level_field == PRIVATE
+    end
+
+    def internal?
+      visibility_level_field == INTERNAL
+    end
+
+    def public?
+      visibility_level_field == PUBLIC
+    end
+  end
+end
index 8b2a49d..d5b1cb8 100644 (file)
@@ -7,6 +7,7 @@ describe Projects::CreateContext do
   describe :create_by_user do
     before do
       @user = create :user
+      @admin = create :user, admin: true
       @opts = {
         name: "GitLab",
         namespace: @user.namespace
@@ -37,7 +38,7 @@ describe Projects::CreateContext do
       it { @project.namespace.should == @group }
     end
 
-    context 'respect configured public setting' do
+    context 'respect configured visibility setting' do
       before(:each) do
         @settings = double("settings")
         @settings.stub(:issues) { true }
@@ -46,25 +47,90 @@ describe Projects::CreateContext do
         @settings.stub(:wall) { true }
         @settings.stub(:snippets) { true }
         stub_const("Settings", Class.new)
+        @restrictions = double("restrictions")
+        @restrictions.stub(:restricted_visibility_levels) { [] }
+        Settings.stub_chain(:gitlab).and_return(@restrictions)
         Settings.stub_chain(:gitlab, :default_projects_features).and_return(@settings)
       end
 
       context 'should be public when setting is public' do
         before do
-          @settings.stub(:public) { true }
+          @settings.stub(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
           @project = create_project(@user, @opts)
         end
 
-        it { @project.public.should be_true }
+        it { @project.public?.should be_true }
       end
 
-      context 'should be private when setting is not public' do
+      context 'should be private when setting is private' do
         before do
-          @settings.stub(:public) { false }
+          @settings.stub(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
           @project = create_project(@user, @opts)
         end
 
-        it { @project.public.should be_false }
+        it { @project.private?.should be_true }
+      end
+
+      context 'should be internal when setting is internal' do
+        before do
+          @settings.stub(:visibility_level) { Gitlab::VisibilityLevel::INTERNAL }
+          @project = create_project(@user, @opts)
+        end
+
+        it { @project.internal?.should be_true }
+      end
+    end
+
+    context 'respect configured visibility restrictions setting' do
+      before(:each) do
+        @settings = double("settings")
+        @settings.stub(:issues) { true }
+        @settings.stub(:merge_requests) { true }
+        @settings.stub(:wiki) { true }
+        @settings.stub(:wall) { true }
+        @settings.stub(:snippets) { true }
+        @settings.stub(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
+        stub_const("Settings", Class.new)
+        @restrictions = double("restrictions")
+        @restrictions.stub(:restricted_visibility_levels) { [ Gitlab::VisibilityLevel::PUBLIC ] }
+        Settings.stub_chain(:gitlab).and_return(@restrictions)
+        Settings.stub_chain(:gitlab, :default_projects_features).and_return(@settings)
+      end
+
+      context 'should be private when option is public' do
+        before do
+          @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+          @project = create_project(@user, @opts)
+        end
+
+        it { @project.private?.should be_true }
+      end
+
+      context 'should be public when option is public for admin' do
+        before do
+          @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+          @project = create_project(@admin, @opts)
+        end
+
+        it { @project.public?.should be_true }
+      end
+
+      context 'should be private when option is private' do
+        before do
+          @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+          @project = create_project(@user, @opts)
+        end
+
+        it { @project.private?.should be_true }
+      end
+
+      context 'should be internal when option is internal' do
+        before do
+          @opts.merge!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+          @project = create_project(@user, @opts)
+        end
+
+        it { @project.internal?.should be_true }
       end
     end
   end
@@ -73,3 +139,4 @@ describe Projects::CreateContext do
     Projects::CreateContext.new(user, opts).execute
   end
 end
+
diff --git a/spec/contexts/projects_update_context_spec.rb b/spec/contexts/projects_update_context_spec.rb
new file mode 100644 (file)
index 0000000..edcaf84
--- /dev/null
@@ -0,0 +1,111 @@
+require 'spec_helper'
+
+describe Projects::UpdateContext do
+  before(:each) { ActiveRecord::Base.observers.enable(:user_observer) }
+  after(:each) { ActiveRecord::Base.observers.disable(:user_observer) }
+
+  describe :update_by_user do
+    before do
+      @user = create :user
+      @admin = create :user, admin: true
+      @project = create :project, creator_id: @user.id, namespace: @user.namespace
+      @opts = { project: {} }
+    end
+
+    context 'should be private when updated to private' do
+      before do
+       @created_private = @project.private?
+
+        @opts[:project].merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+        update_project(@project, @user, @opts)
+      end
+
+      it { @created_private.should be_true }
+      it { @project.private?.should be_true }
+    end
+
+    context 'should be internal when updated to internal' do
+      before do
+        @created_private = @project.private?
+
+        @opts[:project].merge!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+        update_project(@project, @user, @opts)
+      end
+
+      it { @created_private.should be_true }
+      it { @project.internal?.should be_true }
+    end
+
+    context 'should be public when updated to public' do
+      before do
+        @created_private = @project.private?
+
+        @opts[:project].merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+        update_project(@project, @user, @opts)
+      end
+
+      it { @created_private.should be_true }
+      it { @project.public?.should be_true }
+    end
+
+    context 'respect configured visibility restrictions setting' do
+      before(:each) do
+        @restrictions = double("restrictions")
+        @restrictions.stub(:restricted_visibility_levels) { [ Gitlab::VisibilityLevel::PUBLIC ] }
+        Settings.stub_chain(:gitlab).and_return(@restrictions)
+      end
+
+      context 'should be private when updated to private' do
+        before do
+          @created_private = @project.private?
+
+          @opts[:project].merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+          update_project(@project, @user, @opts)
+        end
+
+        it { @created_private.should be_true }
+        it { @project.private?.should be_true }
+      end
+
+      context 'should be internal when updated to internal' do
+        before do
+          @created_private = @project.private?
+
+          @opts[:project].merge!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+          update_project(@project, @user, @opts)
+        end
+
+        it { @created_private.should be_true }
+        it { @project.internal?.should be_true }
+      end
+
+      context 'should be private when updated to public' do
+        before do
+          @created_private = @project.private?
+
+          @opts[:project].merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+          update_project(@project, @user, @opts)
+        end
+
+        it { @created_private.should be_true }
+        it { @project.private?.should be_true }
+      end
+
+      context 'should be public when updated to public by admin' do
+        before do
+          @created_private = @project.private?
+
+          @opts[:project].merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+          update_project(@project, @admin, @opts)
+        end
+
+        it { @created_private.should be_true }
+        it { @project.public?.should be_true }
+      end
+    end
+  end
+
+  def update_project(project, user, opts)
+    Projects::UpdateContext.new(project, user, opts).execute
+  end
+end
\ No newline at end of file
index 58f747e..c25743e 100644 (file)
@@ -3,23 +3,39 @@ require 'spec_helper'
 describe SearchContext do
   let(:found_namespace) { create(:namespace, name: 'searchable namespace', path:'another_thing') }
   let(:user) { create(:user, namespace: found_namespace) }
-  let!(:found_project) { create(:project, name: 'searchable_project', creator_id: user.id, namespace: found_namespace, public: false) }
+  let!(:found_project) { create(:project, name: 'searchable_project', creator_id: user.id, namespace: found_namespace, visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
 
   let(:unfound_namespace) { create(:namespace, name: 'unfound namespace', path: 'yet_something_else') }
-  let!(:unfound_project) { create(:project, name: 'unfound_project', creator_id: user.id, namespace: unfound_namespace, public: false) }
-  let(:public_namespace) { create(:namespace, path: 'something_else',name: 'searchable public namespace') }
-  let(:other_user) { create(:user, namespace: public_namespace) }
-  let!(:public_project) { create(:project, name: 'searchable_public_project', creator_id: other_user.id, namespace: public_namespace, public: true) }
+  let!(:unfound_project) { create(:project, name: 'unfound_project', creator_id: user.id, namespace: unfound_namespace, visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
+  
+  let(:internal_namespace) { create(:namespace, path: 'something_internal',name: 'searchable internal namespace') }
+  let(:internal_user) { create(:user, namespace: internal_namespace) }
+  let!(:internal_project) { create(:project, name: 'searchable_internal_project', creator_id: internal_user.id, namespace: internal_namespace, visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
+  
+  let(:public_namespace) { create(:namespace, path: 'something_public',name: 'searchable public namespace') }
+  let(:public_user) { create(:user, namespace: public_namespace) }
+  let!(:public_project) { create(:project, name: 'searchable_public_project', creator_id: public_user.id, namespace: public_namespace, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
 
   describe '#execute' do
     it 'public projects should be searchable' do
-      context = SearchContext.new([found_project.id], {search_code:  false, search: "searchable"})
+      context = SearchContext.new([found_project.id], nil, {search_code:  false, search: "searchable"})
       results = context.execute
       results[:projects].should == [found_project, public_project]
     end
 
+    it 'internal projects should be searchable' do
+      context = SearchContext.new([found_project.id], user, {search_code:  false, search: "searchable"})
+      results = context.execute
+      # can't seem to rely on the return order, so check this way
+      #subject { results[:projects] }
+      results[:projects].should have(3).items
+      results[:projects].should include(found_project)
+      results[:projects].should include(internal_project)
+      results[:projects].should include(public_project)
+    end
+
     it 'namespace name should be searchable' do
-      context = SearchContext.new([found_project.id], {search_code:  false, search: "searchable namespace"})
+      context = SearchContext.new([found_project.id], user, {search_code:  false, search: "searchable namespace"})
       results = context.execute
       results[:projects].should == [found_project]
     end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
new file mode 100644 (file)
index 0000000..5abccd2
--- /dev/null
@@ -0,0 +1,251 @@
+require 'spec_helper'
+
+describe "Internal Project Access" do
+  let(:project) { create(:project_with_code) }
+
+  let(:master) { create(:user) }
+  let(:guest) { create(:user) }
+  let(:reporter) { create(:user) }
+
+  before do
+    # internal project
+    project.visibility_level = Gitlab::VisibilityLevel::INTERNAL
+    project.save!
+
+    # full access
+    project.team << [master, :master]
+
+    # readonly
+    project.team << [reporter, :reporter]
+
+  end
+
+  describe "Project should be internal" do
+    subject { project }
+
+    its(:internal?) { should be_true }
+  end
+
+  describe "GET /:project_path" do
+    subject { project_path(project) }
+
+    it { should be_allowed_for master }
+    it { should be_allowed_for reporter }
+    it { should be_allowed_for :admin }
+    it { should be_allowed_for guest }
+    it { should be_allowed_for :user }
+    it { should be_denied_for :visitor }
+  end
+
+  describe "GET /:project_path/tree/master" do
+    subject { project_tree_path(project, project.repository.root_ref) }
+
+    it { should be_allowed_for master }
+    it { should be_allowed_for reporter }
+    it { should be_allowed_for :admin }
+    it { should be_allowed_for guest }
+    it { should be_allowed_for :user }
+    it { should be_denied_for :visitor }
+  end
+
+  describe "GET /:project_path/commits/master" do
+    subject { project_commits_path(project, project.repository.root_ref, limit: 1) }
+
+    it { should be_allowed_for master }
+    it { should be_allowed_for reporter }
+    it { should be_allowed_for :admin }
+    it { should be_allowed_for guest }
+    it { should be_allowed_for :user }
+    it { should be_denied_for :visitor }
+  end
+
+  describe "GET /:project_path/commit/:sha" do
+    subject { project_commit_path(project, project.repository.commit) }
+
+    it { should be_allowed_for master }
+    it { should be_allowed_for reporter }
+    it { should be_allowed_for :admin }
+    it { should be_allowed_for guest }
+    it { should be_allowed_for :user }
+    it { should be_denied_for :visitor }
+  end
+
+  describe "GET /:project_path/compare" do
+    subject { project_compare_index_path(project) }
+
+    it { should be_allowed_for master }
+    it { should be_allowed_for reporter }
+    it { should be_allowed_for :admin }
+    it { should be_allowed_for guest }
+    it { should be_allowed_for :user }
+    it { should be_denied_for :visitor }
+  end
+
+  describe "GET /:project_path/team" do
+    subject { project_team_index_path(project) }
+
+    it { should be_allowed_for master }
+    it { should be_denied_for reporter }
+    it { should be_allowed_for :admin }
+    it { should be_denied_for guest }
+    it { should be_denied_for :user }
+    it { should be_denied_for :visitor }
+  end
+
+  describe "GET /:project_path/wall" do
+    subject { project_wall_path(project) }
+
+    it { should be_allowed_for master }
+    it { should be_allowed_for reporter }
+    it { should be_allowed_for :admin }
+    it { should be_allowed_for guest }
+    it { should be_allowed_for :user }
+    it { should be_denied_for :visitor }
+  end
+
+  describe "GET /:project_path/blob" do
+    before do
+      commit = project.repository.commit
+      path = commit.tree.contents.select { |i| i.is_a?(Grit::Blob) }.first.name
+      @blob_path = project_blob_path(project, File.join(commit.id, path))
+    end
+
+    it { @blob_path.should be_allowed_for master }
+    it { @blob_path.should be_allowed_for reporter }
+    it { @blob_path.should be_allowed_for :admin }
+    it { @blob_path.should be_allowed_for guest }
+    it { @blob_path.should be_allowed_for :user }
+    it { @blob_path.should be_denied_for :visitor }
+  end
+
+  describe "GET /:project_path/edit" do
+    subject { edit_project_path(project) }
+
+    it { should be_allowed_for master }
+    it { should be_denied_for reporter }
+    it { should be_allowed_for :admin }
+    it { should be_denied_for guest }
+    it { should be_denied_for :user }
+    it { should be_denied_for :visitor }
+  end
+
+  describe "GET /:project_path/deploy_keys" do
+    subject { project_deploy_keys_path(project) }
+
+    it { should be_allowed_for master }
+    it { should be_denied_for reporter }
+    it { should be_allowed_for :admin }
+    it { should be_denied_for guest }
+    it { should be_denied_for :user }
+    it { should be_denied_for :visitor }
+  end
+
+  describe "GET /:project_path/issues" do
+    subject { project_issues_path(project) }
+
+    it { should be_allowed_for master }
+    it { should be_allowed_for reporter }
+    it { should be_allowed_for :admin }
+    it { should be_allowed_for guest }
+    it { should be_allowed_for :user }
+    it { should be_denied_for :visitor }
+  end
+
+  describe "GET /:project_path/snippets" do
+    subject { project_snippets_path(project) }
+
+    it { should be_allowed_for master }
+    it { should be_allowed_for reporter }
+    it { should be_allowed_for :admin }
+    it { should be_allowed_for guest }
+    it { should be_allowed_for :user }
+    it { should be_denied_for :visitor }
+  end
+
+  describe "GET /:project_path/snippets/new" do
+    subject { new_project_snippet_path(project) }
+
+    it { should be_allowed_for master }
+    it { should be_allowed_for reporter }
+    it { should be_allowed_for :admin }
+    it { should be_denied_for guest }
+    it { should be_denied_for :user }
+    it { should be_denied_for :visitor }
+  end
+
+  describe "GET /:project_path/merge_requests" do
+    subject { project_merge_requests_path(project) }
+
+    it { should be_allowed_for master }
+    it { should be_allowed_for reporter }
+    it { should be_allowed_for :admin }
+    it { should be_allowed_for guest }
+    it { should be_allowed_for :user }
+    it { should be_denied_for :visitor }
+  end
+
+  describe "GET /:project_path/merge_requests/new" do
+    subject { new_project_merge_request_path(project) }
+
+    it { should be_allowed_for master }
+    it { should be_denied_for reporter }
+    it { should be_allowed_for :admin }
+    it { should be_denied_for guest }
+    it { should be_denied_for :user }
+    it { should be_denied_for :visitor }
+  end
+
+  describe "GET /:project_path/branches/recent" do
+    subject { recent_project_branches_path(project) }
+
+    it { should be_allowed_for master }
+    it { should be_allowed_for reporter }
+    it { should be_allowed_for :admin }
+    it { should be_allowed_for guest }
+    it { should be_allowed_for :user }
+    it { should be_denied_for :visitor }
+  end
+
+  describe "GET /:project_path/branches" do
+    subject { project_branches_path(project) }
+
+    before do
+      # Speed increase
+      Project.any_instance.stub(:branches).and_return([])
+    end
+
+    it { should be_allowed_for master }
+    it { should be_allowed_for reporter }
+    it { should be_allowed_for :admin }
+    it { should be_allowed_for guest }
+    it { should be_allowed_for :user }
+    it { should be_denied_for :visitor }
+  end
+
+  describe "GET /:project_path/tags" do
+    subject { project_tags_path(project) }
+
+    before do
+      # Speed increase
+      Project.any_instance.stub(:tags).and_return([])
+    end
+
+    it { should be_allowed_for master }
+    it { should be_allowed_for reporter }
+    it { should be_allowed_for :admin }
+    it { should be_allowed_for guest }
+    it { should be_allowed_for :user }
+    it { should be_denied_for :visitor }
+  end
+
+  describe "GET /:project_path/hooks" do
+    subject { project_hooks_path(project) }
+
+    it { should be_allowed_for master }
+    it { should be_denied_for reporter }
+    it { should be_allowed_for :admin }
+    it { should be_denied_for guest }
+    it { should be_denied_for :user }
+    it { should be_denied_for :visitor }
+  end
+end
index 7f3f8c5..481d8ce 100644 (file)
@@ -15,6 +15,12 @@ describe "Private Project Access" do
     project.team << [reporter, :reporter]
   end
 
+  describe "Project should be private" do
+    subject { project }
+
+    its(:private?) { should be_true }
+  end
+
   describe "GET /:project_path" do
     subject { project_path(project) }
 
index 267643f..3f10164 100644 (file)
@@ -9,7 +9,7 @@ describe "Public Project Access" do
 
   before do
     # public project
-    project.public = true
+    project.visibility_level = Gitlab::VisibilityLevel::PUBLIC
     project.save!
 
     # full access
index d5803d8..0167d51 100644 (file)
 #  merge_requests_enabled :boolean          default(TRUE), not null
 #  wiki_enabled           :boolean          default(TRUE), not null
 #  namespace_id           :integer
-#  public                 :boolean          default(FALSE), not null
 #  issues_tracker         :string(255)      default("gitlab"), not null
 #  issues_tracker_id      :string(255)
 #  snippets_enabled       :boolean          default(TRUE), not null
 #  last_activity_at       :datetime
 #  imported               :boolean          default(FALSE), not null
 #  import_url             :string(255)
+#  visibility_level       :integer          default(0), not null
 #
 
 require 'spec_helper'
index e4cef6c..7322d79 100644 (file)
@@ -132,15 +132,45 @@ describe API::API do
     end
 
     it "should set a project as public" do
+      project = attributes_for(:project, { visibility_level: Gitlab::VisibilityLevel::PUBLIC })
+      post api("/projects", user), project
+      json_response['public'].should be_true
+      json_response['visibility_level'].should == Gitlab::VisibilityLevel::PUBLIC
+    end
+
+    it "should set a project as public using :public" do
       project = attributes_for(:project, { public: true })
       post api("/projects", user), project
       json_response['public'].should be_true
+      json_response['visibility_level'].should == Gitlab::VisibilityLevel::PUBLIC
+    end
+
+    it "should set a project as internal" do
+      project = attributes_for(:project, { visibility_level: Gitlab::VisibilityLevel::INTERNAL })
+      post api("/projects", user), project
+      json_response['public'].should be_false
+      json_response['visibility_level'].should == Gitlab::VisibilityLevel::INTERNAL
+    end
+
+    it "should set a project as internal overriding :public" do
+      project = attributes_for(:project, { public: true, visibility_level: Gitlab::VisibilityLevel::INTERNAL })
+      post api("/projects", user), project
+      json_response['public'].should be_false
+      json_response['visibility_level'].should == Gitlab::VisibilityLevel::INTERNAL
     end
 
     it "should set a project as private" do
+      project = attributes_for(:project, { visibility_level: Gitlab::VisibilityLevel::PRIVATE })
+      post api("/projects", user), project
+      json_response['public'].should be_false
+      json_response['visibility_level'].should == Gitlab::VisibilityLevel::PRIVATE
+    end
+
+    it "should set a project as private using :public" do
       project = attributes_for(:project, { public: false })
       post api("/projects", user), project
       json_response['public'].should be_false
+      json_response['visibility_level'].should == Gitlab::VisibilityLevel::PRIVATE
     end
   end
 
@@ -183,19 +213,46 @@ describe API::API do
     end
 
     it "should set a project as public" do
+      project = attributes_for(:project, { visibility_level: Gitlab::VisibilityLevel::PUBLIC })
+      post api("/projects/user/#{user.id}", admin), project
+      json_response['public'].should be_true
+      json_response['visibility_level'].should == Gitlab::VisibilityLevel::PUBLIC
+    end
+
+    it "should set a project as public using :public" do
       project = attributes_for(:project, { public: true })
       post api("/projects/user/#{user.id}", admin), project
       json_response['public'].should be_true
+      json_response['visibility_level'].should == Gitlab::VisibilityLevel::PUBLIC
+    end
 
+    it "should set a project as internal" do
+      project = attributes_for(:project, { visibility_level: Gitlab::VisibilityLevel::INTERNAL })
+      post api("/projects/user/#{user.id}", admin), project
+      json_response['public'].should be_false
+      json_response['visibility_level'].should == Gitlab::VisibilityLevel::INTERNAL
     end
 
-    it "should set a project as private" do
-      project = attributes_for(:project, { public: false })
+    it "should set a project as internal overriding :public" do
+      project = attributes_for(:project, { public: true, visibility_level: Gitlab::VisibilityLevel::INTERNAL })
       post api("/projects/user/#{user.id}", admin), project
       json_response['public'].should be_false
+      json_response['visibility_level'].should == Gitlab::VisibilityLevel::INTERNAL
+    end
 
+    it "should set a project as private" do
+      project = attributes_for(:project, { visibility_level: Gitlab::VisibilityLevel::PRIVATE })
+      post api("/projects/user/#{user.id}", admin), project
+      json_response['public'].should be_false
+      json_response['visibility_level'].should == Gitlab::VisibilityLevel::PRIVATE
     end
 
+    it "should set a project as private using :public" do
+      project = attributes_for(:project, { public: false })
+      post api("/projects/user/#{user.id}", admin), project
+      json_response['public'].should be_false
+      json_response['visibility_level'].should == Gitlab::VisibilityLevel::PRIVATE
+    end
   end
 
   describe "GET /projects/:id" do
@@ -649,10 +706,10 @@ describe API::API do
 
   describe :fork_admin do
     let(:project_fork_target) { create(:project) }
-    let(:project_fork_source) { create(:project, public: true) }
+    let(:project_fork_source) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
 
     describe "POST /projects/:id/fork/:forked_from_id" do
-      let(:new_project_fork_source) { create(:project, public: true) }
+      let(:new_project_fork_source) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
 
       it "shouldn't available for non admin users" do
         post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user)
@@ -721,8 +778,10 @@ describe API::API do
     let!(:post) { create(:project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) }
     let!(:pre_post) { create(:project, name: "pre_#{query}_post", creator_id: user.id, namespace: user.namespace) }
     let!(:unfound) { create(:project, name: 'unfound', creator_id: user.id, namespace: user.namespace) }
-    let!(:public) { create(:project, name: "another #{query}",public: true) }
-    let!(:unfound_public) { create(:project, name: 'unfound public', public: true) }
+    let!(:internal) { create(:project, name: "internal #{query}", visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
+    let!(:unfound_internal) { create(:project, name: 'unfound internal', visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
+    let!(:public) { create(:project, name: "public #{query}", visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+    let!(:unfound_public) { create(:project, name: 'unfound public', visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
 
     context "when unauthenticated" do
       it "should return authentication error" do
@@ -736,7 +795,7 @@ describe API::API do
         get api("/projects/search/#{query}",user)
         response.status.should == 200
         json_response.should be_an Array
-        json_response.size.should == 5
+        json_response.size.should == 6
         json_response.each {|project| project['name'].should =~ /.*query.*/}
       end
     end
@@ -746,8 +805,8 @@ describe API::API do
         get api("/projects/search/#{query}", user2)
         response.status.should == 200
         json_response.should be_an Array
-        json_response.size.should == 1
-        json_response.first['name'].should == "another #{query}"
+        json_response.size.should == 2
+        json_response.each {|project| project['name'].should =~ /(internal|public) query/}
       end
     end
   end