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

Commit

Permalink
Introduce application tokens
Browse files Browse the repository at this point in the history
Right now the Docker client stores the credentials in plain
format on the host. This is really bad from a security point of view,
especially for users using LDAP to authenticated.

This commit introduces the concept of "application tokens". Each user
can have at most 5 application tokens. The tokens are created in a
random secure way by Portus and are stored inside of its database after
being hashed via bcrypt.

The application tokens can be used by all the programs authenticating
against a Docker registry protected by Portus (e.g.: the docker cli
client). They cannot be used to log into Portus' web interface.

The application tokens can be revoked at any time by using a new UI.

Signed-off-by: Flavio Castelli <[email protected]>
  • Loading branch information
flavio committed Dec 10, 2015
1 parent efab477 commit b399f90
Show file tree
Hide file tree
Showing 29 changed files with 573 additions and 3 deletions.
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ gem "redcarpet"
gem "font-awesome-rails"
gem "bootstrap-typeahead-rails"

# Used to store application tokens. This is already a Rails depedency. However
# better safe than sorry...
gem "bcrypt"

# This is already a Rails dependency, but we use it to run portusctl
gem "thor"

Expand Down
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ DEPENDENCIES
active_record_union
awesome_print
base32
bcrypt
bootstrap-sass (~> 3.3.4)
bootstrap-typeahead-rails
byebug
Expand Down
12 changes: 12 additions & 0 deletions app/assets/javascripts/registrations.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
$(document).on "page:change", ->
$('#add_application_token_btn').on 'click', (event) ->
$('#add_application_token_form').toggle 400, "swing", ->
if $('#add_application_token_form').is(':visible')
$('#add_team_btn i').addClass("fa-minus-circle")
$('#add_team_btn i').removeClass("fa-plus-circle")
$('#team_name').focus()
layout_resizer()
else
$('#add_team_btn i').removeClass("fa-minus-circle")
$('#add_team_btn i').addClass("fa-plus-circle")
layout_resizer()
6 changes: 6 additions & 0 deletions app/assets/stylesheets/activities.scss
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@
&.change-description {
background: $activity-change-description;
}
&.application-token-created {
background: $activity-application-token-created;
}
&.application-token-destroyed {
background: $activity-application-token-destroyed;
}
}
}
}
2 changes: 2 additions & 0 deletions app/assets/stylesheets/variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ $activity-remove-member-team: $yellow;
$activity-change-role: $orange;
$activity-team-created: $pink;
$activity-repo-pushed: $purple;
$activity-application-token-created: $orange-dark;
$activity-application-token-destroyed: $red;


// placeholder
Expand Down
15 changes: 15 additions & 0 deletions app/controllers/api/v2/tokens_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@
# use in order to perform operation into the registry. This is the last step in
# the authentication process for Portus' point of view.
class Api::V2::TokensController < Api::BaseController
before_action :attempt_authentication_against_application_tokens

# Try to perform authentication using the application tokens. The password
# provided via HTTP basic auth is going to be checked against the application
# tokens a user might have created.
# If the user has a valid application token then the other forms of
# authentication (Portus' database, LDAP) are going to be skipped.
def attempt_authentication_against_application_tokens
user = authenticate_with_http_basic do |username, password|
user = User.find_by(username: username)
user if user && user.application_token_valid?(password)
end
sign_in(user, store: true) if user
end

# Returns the token that the docker client should use in order to perform
# operation into the private registry.
def show
Expand Down
39 changes: 39 additions & 0 deletions app/controllers/application_tokens_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# ApplicationTokensController manages the creation/removal of application tokens
class ApplicationTokensController < ApplicationController
respond_to :js

# POST /application_tokens
def create
@plain_token = Devise.friendly_token

@application_token = ApplicationToken.new(create_params)
@application_token.user = current_user
@application_token.token_salt = BCrypt::Engine.generate_salt
@application_token.token_hash = BCrypt::Engine.hash_secret(
@plain_token,
@application_token.token_salt)

if @application_token.save
@application_token.create_activity!(:create, current_user)
respond_with @application_token
else
respond_with @application_token.errors, status: :unprocessable_entity
end
end

# DELETE /application_token/1
def destroy
@application_token = ApplicationToken.find(params[:id])
@application_token.create_activity!(:destroy, current_user)
@application_token.destroy

respond_with @application_token
end

private

def create_params
permitted = [:application]
params.require(:application_token).permit(permitted)
end
end
25 changes: 25 additions & 0 deletions app/models/application_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class ApplicationToken < ActiveRecord::Base
include PublicActivity::Common

belongs_to :user

validates :application, uniqueness: { scope: "user_id" }

validate :limit_number_of_tokens_per_user, on: :create

def limit_number_of_tokens_per_user
max_reached = ApplicationToken.where(user_id: user_id).count >= User::APPLICATION_TOKENS_MAX
errors.add(
:base,
"Users cannot have more than #{User::APPLICATION_TOKENS_MAX} " \
"application tokens") if max_reached
end

# Create the activity regarding this application token.
def create_activity!(type, owner)
create_activity(
type,
owner: owner,
parameters: { application: application })
end
end
16 changes: 16 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ class User < ActiveRecord::Base
USERNAME_CHARS = "a-z0-9"
USERNAME_FORMAT = /\A[#{USERNAME_CHARS}]{4,30}\Z/

APPLICATION_TOKENS_MAX = 5

validates :username, presence: true, uniqueness: true,
format: {
with: USERNAME_FORMAT,
Expand All @@ -18,6 +20,7 @@ class User < ActiveRecord::Base
has_many :team_users
has_many :teams, through: :team_users
has_many :stars
has_many :application_tokens

scope :not_portus, -> { where.not username: "portus" }
scope :enabled, -> { not_portus.where enabled: true }
Expand Down Expand Up @@ -112,6 +115,19 @@ def self.search_from_query(members, query)
enabled.where.not(id: members).where(arel_table[:username].matches(query))
end

# Looks for an application token that matches with the plain one provided
# as parameter.
# Return true if there's an application token matching it, false otherwise
def application_token_valid?(plain_token)
application_tokens.each do |t|
if t.token_hash == BCrypt::Engine.hash_secret(plain_token, t.token_salt)
return true
end
end

false
end

protected

# Returns whether the given user can be disabled or not. The following rules
Expand Down
11 changes: 10 additions & 1 deletion app/policies/public_activity/activity_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,16 @@ def resolve
"(team_users.user_id = ? OR namespaces.public = ?)",
"Repository", user.id, true)

team_activities.union_all(namespace_activities).union_all(repository_activities).distinct
# Show application tokens activities related only to the current user
application_token_activities = @scope
.where("activities.trackable_type = ? AND activities.owner_id = ?",
"ApplicationToken", user.id)

team_activities
.union_all(namespace_activities)
.union_all(repository_activities)
.union_all(application_token_activities)
.distinct
end
end
end
13 changes: 13 additions & 0 deletions app/views/application_tokens/_application_token.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
tr id="application_token_#{application_token.id}"
td= application_token.application
td
a[class="btn btn-default"
data-placement="left"
data-toggle="popover"
data-title="Please confirm"
data-content='<p>Are you sure you want to remove this token?</p><a class="btn btn-default">No</a> <a class="btn btn-primary" data-method="delete" rel="nofollow" data-remote="true" href="#{url_for(application_token)}">Yes</a>'
data-html="true"
tabindex="0"
role="button"
]
i.fa.fa-trash.fa-lg
13 changes: 13 additions & 0 deletions app/views/application_tokens/create.js.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<% if @application_token.errors.any? %>
$('#alert p').html("<%= escape_javascript(@application_token.errors.full_messages.join('<br/>')) %>");
$('#alert').fadeIn();
<% else %>
$("<%= escape_javascript(render @application_token) %>").appendTo("#application_tokens");
$('#alert p').html("New token created: <code><%= @plain_token %></code>");
$('#alert').fadeIn();
$('#add_application_token_form').fadeOut();
<% if current_user.application_tokens.count >= User::APPLICATION_TOKENS_MAX %>
$('#add_application_token_btn').attr("disabled", "disabled");
<% end %>
<% end %>

11 changes: 11 additions & 0 deletions app/views/application_tokens/destroy.js.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<% if @application_token.errors.any? %>
$('#alert p').html("<%= escape_javascript(@application_token.errors.full_messages.join('<br/>')) %>");
$('#alert').fadeIn();
<% else %>
$("#application_token_<%= @application_token.id %>").remove();
$('#alert p').html("<em>\"<%= @application_token.application %>\"</em> token has been removed");
$('#alert').fadeIn();
<% if current_user.application_tokens.count < User::APPLICATION_TOKENS_MAX %>
$('#add_application_token_btn').removeAttr("disabled");
<% end %>
<% end %>
35 changes: 35 additions & 0 deletions app/views/devise/registrations/edit.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,41 @@
.actions
= f.submit('Update', class: 'btn btn-primary', disabled: true)

#add_application_token_form.collapse
= form_for :application_token, url: application_tokens_path, remote: true, html: {id: 'new-application-token-form', class: 'form-horizontal', role: 'form'} do |f|
.form-group
= f.label :application, {class: 'control-label col-md-2'}
.col-md-7
= f.text_field(:application, class: 'form-control', required: true, placeholder: "Application's name")
.form-group
.col-md-offset-2.col-md-7
= f.submit('Create', class: 'btn btn-primary')

.panel.panel-default
.panel-heading
h5
' Application tokens
.pull-right
a#add_application_token_btn.btn.btn-xs.btn-link.js-toggle-button[
disabled="#{current_user.application_tokens.count >= User::APPLICATION_TOKENS_MAX ? 'disabled' : ''}"
role="button"
]
i.fa.fa-plus-circle
| Create new token
.panel-body
.table-responsive
table.table.table-striped.table-hover
colgroup
col.col-90
col.col-10
thead
tr
th Application
th Remove
tbody#application_tokens
- current_user.application_tokens.each do |token|
= render partial: 'application_tokens/application_token', locals: {application_token: token}

- if current_user.email?
- unless current_user.admin? && @admin_count == 1
.panel.panel-default
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
= CSV.generate_line(["application token", "#{activity.parameters[:application]}", "create", "-", activity.owner.username, activity.created_at, "-"])
15 changes: 15 additions & 0 deletions app/views/public_activity/application_token/_create.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
li
.activity-type.application-token-created
i.fa.fa-key
.user-image
= user_image_tag(activity.owner.email)
.description
h6
strong
= activity.owner.username
| created a token for the "
em= activity.parameters[:application]
| " application
small
i.fa.fa-clock-o
= activity_time_tag activity.created_at
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
= CSV.generate_line(["application token", "#{activity.parameters[:application]}", "destroy", "-", activity.owner.username, activity.created_at, "-"])
15 changes: 15 additions & 0 deletions app/views/public_activity/application_token/_destroy.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
li
.activity-type.application-token-destroyed
i.fa.fa-key
.user-image
= user_image_tag(activity.owner.email)
.description
h6
strong
= activity.owner.username
| removed the token of "
em= activity.parameters[:application]
| " application
small
i.fa.fa-clock-o
= activity_time_tag activity.created_at
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
resources :comments, only: [:create, :destroy]
end

resources :application_tokens, only: [:create, :destroy]

devise_for :users, controllers: { registrations: "auth/registrations",
sessions: "auth/sessions",
passwords: "passwords" }
Expand Down
12 changes: 12 additions & 0 deletions db/migrate/20151117181723_create_application_tokens.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class CreateApplicationTokens < ActiveRecord::Migration
def change
create_table :application_tokens do |t|
t.string :application, null: false
t.string :token_hash, null:false
t.string :token_salt, null:false
t.integer :user_id, null:false
end

add_index :application_tokens, :user_id
end
end
12 changes: 11 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 20151124150353) do
ActiveRecord::Schema.define(version: 20151207153613) do

create_table "activities", force: :cascade do |t|
t.integer "trackable_id", limit: 4
Expand All @@ -31,6 +31,15 @@
add_index "activities", ["recipient_id", "recipient_type"], name: "index_activities_on_recipient_id_and_recipient_type", using: :btree
add_index "activities", ["trackable_id", "trackable_type"], name: "index_activities_on_trackable_id_and_trackable_type", using: :btree

create_table "application_tokens", force: :cascade do |t|
t.string "application", limit: 255, null: false
t.string "token_hash", limit: 255, null: false
t.string "token_salt", limit: 255, null: false
t.integer "user_id", limit: 4, null: false
end

add_index "application_tokens", ["user_id"], name: "index_application_tokens_on_user_id", using: :btree

create_table "comments", force: :cascade do |t|
t.text "body", limit: 65535
t.integer "repository_id", limit: 4
Expand Down Expand Up @@ -85,6 +94,7 @@
t.integer "namespace_id", limit: 4
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "source_url", limit: 255, default: "", null: false
end

add_index "repositories", ["name", "namespace_id"], name: "index_repositories_on_name_and_namespace_id", unique: true, using: :btree
Expand Down
Loading

0 comments on commit b399f90

Please sign in to comment.