Skip to content
This repository has been archived by the owner on Apr 17, 2023. It is now read-only.

Commit

Permalink
add internal policy for namespaces
Browse files Browse the repository at this point in the history
An "internal" (or "protected") namespace allows only logged-in users to
use the repositories belonging to it. Other than that, it behaves just
like a public namespace.

Resolves #606

Signed-off-by: Thomas Hipp <[email protected]>
  • Loading branch information
Thomas Hipp committed Jul 5, 2016
1 parent a50f221 commit 46d1d0b
Show file tree
Hide file tree
Showing 34 changed files with 404 additions and 134 deletions.
28 changes: 15 additions & 13 deletions app/controllers/namespaces_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ class NamespacesController < ApplicationController
include ChangeNameDescription

respond_to :html, :js
before_action :set_namespace, only: [:toggle_public, :show, :update]
before_action :set_namespace, only: [:change_visibility, :show, :update]
before_action :check_team, only: [:create]
before_action :check_role, only: [:create]

Expand Down Expand Up @@ -77,17 +77,18 @@ def typeahead
end
end

# PATCH/PUT /namespace/1/toggle_public
def toggle_public
# PATCH/PUT /namespace/1/change_visibility
def change_visibility
authorize @namespace

@namespace.update_attributes(public: !(@namespace.public?))
if @namespace.public?
@namespace.create_activity :public, owner: current_user
else
@namespace.create_activity :private, owner: current_user
end
render template: "namespaces/toggle_public", locals: { namespace: @namespace }
# Update the visibility if needed
return if params[:visibility] == @namespace.visibility

return unless @namespace.update_attributes(visibility: params[:visibility])
@namespace.create_activity :change_visibility,
owner: current_user,
parameters: { visibility: @namespace.visibility }
render template: "namespaces/change_visibility", locals: { namespace: @namespace }
end

private
Expand All @@ -98,9 +99,10 @@ def fetch_namespace
ns = params.require(:namespace).permit(:namespace, :description)

@namespace = Namespace.new(
team: @team,
name: ns["namespace"],
registry: Registry.get
team: @team,
name: ns["namespace"],
visibility: Namespace.visibilities[:visibility_private],
registry: Registry.get
)
@namespace.description = ns["description"] if ns["description"]
@namespace
Expand Down
16 changes: 14 additions & 2 deletions app/models/namespace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
# created_at :datetime not null
# updated_at :datetime not null
# team_id :integer
# public :boolean default("0")
# registry_id :integer not null
# global :boolean default("0")
# description :text(65535)
# visibility :integer
#
# Indexes
#
Expand Down Expand Up @@ -40,13 +40,25 @@ class Namespace < ActiveRecord::Base
belongs_to :registry
belongs_to :team

validates :public, inclusion: { in: [true] }, if: :global?
enum visibility: [:visibility_private, :visibility_protected, :visibility_public]

validate :global_namespace_cannot_be_private
validates :name,
presence: true,
uniqueness: { scope: "registry_id" },
length: { maximum: MAX_NAME_LENGTH },
namespace: true

# global_namespace_cannot_be_private adds an error and returns false if the
# visibility of the global namespace is set to private. Otherwise, it returns
# true. This function is used to validate the visibility.
def global_namespace_cannot_be_private
if global? && visibility_private?
errors.add(:visibility, "global namespace cannot be private")
return false
end
true
end
# From the given repository name that can be prefix by the name of the
# namespace, returns two values:
# 1. The namespace where the given repository belongs to.
Expand Down
2 changes: 1 addition & 1 deletion app/models/registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def create_namespaces!
Namespace.create!(
name: "portus_global_namespace_#{count}",
registry: self,
public: true,
visibility: Namespace.visibilities[:visibility_public],
global: true,
description: "The global namespace for the registry #{Registry.name}.",
team: team)
Expand Down
1 change: 1 addition & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def create_personal_namespace!
namespace = Namespace.find_or_create_by!(
team: team,
name: namespace_name,
visibility: Namespace.visibilities[:visibility_private],
description: default_description,
registry: Registry.get # TODO: fix once we handle more registries
)
Expand Down
5 changes: 3 additions & 2 deletions app/policies/comment_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ def initialize(user, comment)

# Allows the admin to write comments
# Allows all members of a team to comment on their repos
# Allows all users to comment on repos under a public namespace
# Allows all users to comment on repos under a protected or public namespace
def create?
@user.admin? ||
@comment.repository.namespace.public? ||
@comment.repository.namespace.visibility_public? ||
@comment.repository.namespace.visibility_protected? ||
@comment.repository.namespace.team.team_users.where(user_id: @user.id).any?
end

Expand Down
16 changes: 11 additions & 5 deletions app/policies/namespace_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ def initialize(user, namespace)

def pull?
# Even non-logged in users can pull from a public namespace.
return true if namespace.public?
return true if namespace.visibility_public?

# From now on, all the others require to be logged in.
raise Pundit::NotAuthorizedError, "must be logged in" unless user

# Logged-in users can pull from a protected namespace even if they are
# not part of the team.
return true if namespace.visibility_protected?

# All the members of the team have READ access or anyone if
# the namespace is public
# Everybody can pull from the global namespace
Expand Down Expand Up @@ -40,9 +44,10 @@ def index?
alias_method :create?, :push?
alias_method :update?, :push?

def toggle_public?
def change_visibility?
raise Pundit::NotAuthorizedError, "must be logged in" unless user
!namespace.global? && (user.admin? || namespace.team.owners.exists?(user.id))
(namespace.global? && user.admin?) || \
(!namespace.global? && (user.admin? || namespace.team.owners.exists?(user.id)))
end

# Only owners and admins can change the team ownership.
Expand All @@ -63,9 +68,10 @@ def resolve
scope
.joins(team: [:team_users])
.where(
"(namespaces.public = :public OR team_users.user_id = :user_id) AND " \
"(namespaces.visibility = :visibility OR team_users.user_id = :user_id) AND " \
"namespaces.global = :global AND namespaces.name != :username",
public: true, user_id: user.id, global: false, username: user.username)
visibility: Namespace.visibilities[:visibility_public],
user_id: user.id, global: false, username: user.username)
.distinct
end
end
Expand Down
9 changes: 5 additions & 4 deletions app/policies/public_activity/activity_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ def resolve
"INNER JOIN team_users ON teams.id = team_users.team_id")
.where("activities.trackable_type = ? AND " \
"(team_users.user_id = ? OR " \
"((activities.key = ? OR activities.key = ?) AND namespaces.public = ?))",
"Namespace", user.id, "namespace.public", "namespace.private", true)
"( activities.key = ? AND namespaces.visibility = ?))",
"Namespace", user.id, "namespace.change_visibility",
Namespace.visibilities[:visibility_public])

# Show tag events for repositories inside of namespaces controller by
# a team the user is part of, or tags part of public namespaces.
Expand All @@ -34,8 +35,8 @@ def resolve
"INNER JOIN teams ON namespaces.team_id = teams.id " \
"INNER JOIN team_users ON teams.id = team_users.team_id")
.where("activities.trackable_type = ? AND " \
"(team_users.user_id = ? OR namespaces.public = ?)",
"Repository", user.id, true)
"(team_users.user_id = ? OR namespaces.visibility = ?)",
"Repository", user.id, Namespace.visibilities[:visibility_public])

# Show application tokens activities related only to the current user
application_token_activities = @scope
Expand Down
8 changes: 5 additions & 3 deletions app/policies/repository_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ def initialize(user, repository)

def show?
@user.admin? ||
@repository.namespace.public? ||
@repository.namespace.visibility_public? ||
@repository.namespace.visibility_protected? ||
@repository.namespace.team.users.exists?(user.id)
end

Expand All @@ -28,9 +29,10 @@ def resolve
# the repository belongs to the current_user
@scope
.joins(namespace: { team: :users })
.where("namespaces.public = :namespace_public OR " \
.where("namespaces.visibility = :namespace_visibility OR " \
"users.id = :user_id",
namespace_public: true, user_id: @user.id)
namespace_visibility: Namespace.visibilities[:visibility_public],
user_id: @user.id)
.distinct
end
end
Expand Down
7 changes: 5 additions & 2 deletions app/policies/webhook_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,12 @@ def resolve
namespaces = Namespace
.joins(team: [:team_users])
.where(
"(namespaces.public = :public OR team_users.user_id = :user_id) AND " \
"(namespaces.visibility = :public OR namespaces.visibility = :protected OR "\
"team_users.user_id = :user_id) AND " \
"namespaces.global = :global AND namespaces.name != :username",
public: true, user_id: user.id, global: false, username: user.username)
public: Namespace.visibilities[:visibility_public],
protected: Namespace.visibilities[:visibility_protected], user_id: user.id,
global: false, username: user.username)
.pluck(:id)

scope.includes(:headers, :deliveries).where(namespace: namespaces)
Expand Down
4 changes: 2 additions & 2 deletions app/views/admin/namespaces/index.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
th Repositories
th Webhooks
th Created At
th Public
th Visibility
tbody
- @special_namespaces.each do |namespace|
= render partial: 'namespaces/namespace', locals: {namespace: namespace}
Expand All @@ -39,7 +39,7 @@
th Repositories
th Webhooks
th Created At
th Public
th Visibility
tbody
- @namespaces.each do |namespace|
= render partial: 'namespaces/namespace', locals: {namespace: namespace}
Expand Down
43 changes: 32 additions & 11 deletions app/views/namespaces/_namespace.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,38 @@ tr id="namespace_#{namespace.id}"
td= namespace.webhooks.count
td= time_tag namespace.created_at
td
- if can_manage_namespace?(namespace) && !namespace.global
a.btn.btn-default[data-remote="true"
div.btn-group.pull-right
- if !namespace.global?
a.btn[
id="private"
class=(namespace.visibility_private? ? "btn-primary" : "btn-default")
class=("disabled" if !can_manage_namespace?(namespace))
title=(!namespace.global? ? "Team members can pull images from this namespace" : "The global namespace cannot be private")
data-remote="true"
data-method="put"
rel="nofollow"
href=url_for(toggle_public_namespace_path(namespace))
href=url_for(change_visibility_namespace_path(namespace, visibility: "visibility_private"))
]
- if namespace.public?
i.fa.fa-lg class="fa-toggle-on"
- else
i.fa.fa-lg class="fa-toggle-off" title="Click so anyone can pull from this namespace"
- elsif namespace.public?
i.fa.fa-lg class="fa-toggle-on" title="Anyone can pull images from this namespace"
- else
i.fa.fa-lg class="fa-toggle-off"
i.fa.fa-lock
a.btn[
id="protected"
class=(namespace.visibility_protected? ? "btn-primary" : "btn-default")
class=("disabled" if !can_manage_namespace?(namespace))
title="Logged-in users can pull images from this namespace"
data-remote="true"
data-method="put"
rel="nofollow"
href=url_for(change_visibility_namespace_path(namespace, visibility: "visibility_protected"))
]
i.fa.fa-users
a.btn[
id="public"
class=(namespace.visibility_public? ? "btn-primary" : "btn-default")
class=("disabled" if !can_manage_namespace?(namespace))
title="Anyone can pull images from this namespace"
data-remote="true"
data-method="put"
rel="nofollow"
href=url_for(change_visibility_namespace_path(namespace, visibility: "visibility_public"))
]
i.fa.fa.fa-globe
39 changes: 39 additions & 0 deletions app/views/namespaces/change_visibility.js.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
var ns = $('#namespace_<%= namespace.id %> td a.btn');

<% if namespace.global? %>
<% if namespace.visibility_public? %>
$(ns[0]).addClass("btn-default")
$(ns[0]).removeClass("btn-primary")
$(ns[1]).addClass("btn-primary")
$(ns[1]).removeClass("btn-default")
<% elsif namespace.visibility_protected? %>
$(ns[0]).addClass("btn-primary")
$(ns[0]).removeClass("btn-default")
$(ns[1]).addClass("btn-default")
$(ns[1]).removeClass("btn-primary")
<% end %>
<% else %>
<% if namespace.visibility_public? %>
$(ns[0]).addClass("btn-default")
$(ns[0]).removeClass("btn-primary")
$(ns[1]).addClass("btn-default")
$(ns[1]).removeClass("btn-primary")
$(ns[2]).addClass("btn-primary")
$(ns[2]).removeClass("btn-default")
<% elsif namespace.visibility_protected? %>
$(ns[0]).addClass("btn-default")
$(ns[0]).removeClass("btn-primary")
$(ns[1]).addClass("btn-primary")
$(ns[1]).removeClass("btn-default")
$(ns[2]).addClass("btn-default")
$(ns[2]).removeClass("btn-primary")
<% else %>
// private
$(ns[0]).addClass("btn-primary")
$(ns[0]).removeClass("btn-default")
$(ns[1]).addClass("btn-default")
$(ns[1]).removeClass("btn-primary")
$(ns[2]).addClass("btn-default")
$(ns[2]).removeClass("btn-primary")
<% end %>
<% end %>
4 changes: 2 additions & 2 deletions app/views/namespaces/index.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
th Repositories
th Webhooks
th Created
th Public
th Visibility
tbody
- @special_namespaces.each do |namespace|
= render partial: 'namespaces/namespace', locals: {namespace: namespace}
Expand Down Expand Up @@ -76,7 +76,7 @@
th Repositories
th Webhooks
th Created At
th Public
th Visibility
tbody#accessible-namespaces
- @namespaces.each do |namespace|
= render partial: 'namespaces/namespace', locals: {namespace: namespace}
Expand Down
13 changes: 0 additions & 13 deletions app/views/namespaces/toggle_public.js.erb

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
= CSV.generate_line(['namespace', activity.trackable.name, 'change visibility', '-', activity.owner.display_username, activity.created_at, "is #{activity.parameters[:visibility].sub('visibility_', '')}"])
15 changes: 15 additions & 0 deletions app/views/public_activity/namespace/_change_visibility.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
li
.activitie-container
.activity-type.change-namespace-visibility
i.fa.fa-unlock
.user-image
= user_image_tag(activity.owner.email)
.description
h6
strong
= "#{activity.owner.display_username} set the "
= link_to activity.trackable.name, activity.trackable
= " namespace as #{activity.parameters[:visibility].sub('visibility_', '')}"
small
i.fa.fa-clock-o
= activity_time_tag activity.created_at
Loading

0 comments on commit 46d1d0b

Please sign in to comment.