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

Commit

Permalink
Merge pull request #625 from SUSE/authorization-tokens
Browse files Browse the repository at this point in the history
Introduce application tokens
  • Loading branch information
flavio committed Dec 10, 2015
2 parents 72327fd + b399f90 commit be469f3
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 be469f3

Please sign in to comment.