Added an API token for each User to use when making API requests. (#3920)
The API key will be displayed on My Account page with a link to reset or generate a new one. All existing users will have a token generated by the migration. git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@3217 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
parent
9f59cd64ab
commit
aa9951b38b
@ -108,6 +108,19 @@ class MyController < ApplicationController
|
|||||||
redirect_to :action => 'account'
|
redirect_to :action => 'account'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Create a new API key
|
||||||
|
def reset_api_key
|
||||||
|
if request.post?
|
||||||
|
if User.current.api_token
|
||||||
|
User.current.api_token.destroy
|
||||||
|
User.current.reload
|
||||||
|
end
|
||||||
|
User.current.api_key
|
||||||
|
flash[:notice] = l(:notice_api_access_key_reseted)
|
||||||
|
end
|
||||||
|
redirect_to :action => 'account'
|
||||||
|
end
|
||||||
|
|
||||||
# User's page layout configuration
|
# User's page layout configuration
|
||||||
def page_layout
|
def page_layout
|
||||||
@user = User.current
|
@user = User.current
|
||||||
|
@ -39,6 +39,7 @@ class User < Principal
|
|||||||
has_many :changesets, :dependent => :nullify
|
has_many :changesets, :dependent => :nullify
|
||||||
has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
|
has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
|
||||||
has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
|
has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
|
||||||
|
has_one :api_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='api'"
|
||||||
belongs_to :auth_source
|
belongs_to :auth_source
|
||||||
|
|
||||||
# Active non-anonymous users scope
|
# Active non-anonymous users scope
|
||||||
@ -192,6 +193,12 @@ class User < Principal
|
|||||||
token = self.rss_token || Token.create(:user => self, :action => 'feeds')
|
token = self.rss_token || Token.create(:user => self, :action => 'feeds')
|
||||||
token.value
|
token.value
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Return user's API key (a 40 chars long string), used to access the API
|
||||||
|
def api_key
|
||||||
|
token = self.api_token || Token.create(:user => self, :action => 'api')
|
||||||
|
token.value
|
||||||
|
end
|
||||||
|
|
||||||
# Return an array of project ids for which the user has explicitly turned mail notifications on
|
# Return an array of project ids for which the user has explicitly turned mail notifications on
|
||||||
def notified_projects_ids
|
def notified_projects_ids
|
||||||
@ -210,6 +217,11 @@ class User < Principal
|
|||||||
token && token.user.active? ? token.user : nil
|
token && token.user.active? ? token.user : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.find_by_api_key(key)
|
||||||
|
token = Token.find_by_action_and_value('api', key)
|
||||||
|
token && token.user.active? ? token.user : nil
|
||||||
|
end
|
||||||
|
|
||||||
# Makes find_by_mail case-insensitive
|
# Makes find_by_mail case-insensitive
|
||||||
def self.find_by_mail(mail)
|
def self.find_by_mail(mail)
|
||||||
find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
|
find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
|
||||||
|
@ -2,7 +2,25 @@
|
|||||||
|
|
||||||
<p><%=l(:field_login)%>: <strong><%= @user.login %></strong><br />
|
<p><%=l(:field_login)%>: <strong><%= @user.login %></strong><br />
|
||||||
<%=l(:field_created_on)%>: <%= format_time(@user.created_on) %></p>
|
<%=l(:field_created_on)%>: <%= format_time(@user.created_on) %></p>
|
||||||
|
|
||||||
|
|
||||||
|
<h4><%= l(:label_feeds_access_key) %></h4>
|
||||||
|
|
||||||
|
<p>
|
||||||
<% if @user.rss_token %>
|
<% if @user.rss_token %>
|
||||||
<p><%= l(:label_feeds_access_key_created_on, distance_of_time_in_words(Time.now, @user.rss_token.created_on)) %>
|
<%= l(:label_feeds_access_key_created_on, distance_of_time_in_words(Time.now, @user.rss_token.created_on)) %>
|
||||||
(<%= link_to l(:button_reset), {:action => 'reset_rss_key'}, :method => :post %>)</p>
|
<% else %>
|
||||||
|
<%= l(:label_missing_feeds_access_key) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
(<%= link_to l(:button_reset), {:action => 'reset_rss_key'}, :method => :post %>)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4><%= l(:label_api_access_key) %></h4>
|
||||||
|
<p>
|
||||||
|
<% if @user.api_token %>
|
||||||
|
<%= l(:label_api_access_key_created_on, distance_of_time_in_words(Time.now, @user.api_token.created_on)) %>
|
||||||
|
<% else %>
|
||||||
|
<%= l(:label_missing_api_access_key) %>
|
||||||
|
<% end %>
|
||||||
|
(<%= link_to l(:button_reset), {:action => 'reset_api_key'}, :method => :post %>)
|
||||||
|
</p>
|
||||||
|
@ -51,6 +51,18 @@
|
|||||||
<p><%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %></p>
|
<p><%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<% if @user.api_token %>
|
||||||
|
<h3><%=l(:label_api_access_key) %></h3>
|
||||||
|
<div class="box">
|
||||||
|
<p>
|
||||||
|
<%= link_to_function(l(:text_show), "$('api-access-key').show();")%>
|
||||||
|
<pre id='api-access-key'><%= @user.api_key %></pre>
|
||||||
|
</p>
|
||||||
|
<%= javascript_tag("$('api-access-key').hide();") %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
@ -142,6 +142,7 @@ en:
|
|||||||
notice_email_sent: "An email was sent to {{value}}"
|
notice_email_sent: "An email was sent to {{value}}"
|
||||||
notice_email_error: "An error occurred while sending mail ({{value}})"
|
notice_email_error: "An error occurred while sending mail ({{value}})"
|
||||||
notice_feeds_access_key_reseted: Your RSS access key was reset.
|
notice_feeds_access_key_reseted: Your RSS access key was reset.
|
||||||
|
notice_api_access_key_reseted: Your API access key was reset.
|
||||||
notice_failed_to_save_issues: "Failed to save {{count}} issue(s) on {{total}} selected: {{ids}}."
|
notice_failed_to_save_issues: "Failed to save {{count}} issue(s) on {{total}} selected: {{ids}}."
|
||||||
notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
|
notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
|
||||||
notice_account_pending: "Your account was created and is now pending administrator approval."
|
notice_account_pending: "Your account was created and is now pending administrator approval."
|
||||||
@ -668,6 +669,8 @@ en:
|
|||||||
label_language_based: Based on user's language
|
label_language_based: Based on user's language
|
||||||
label_sort_by: "Sort by {{value}}"
|
label_sort_by: "Sort by {{value}}"
|
||||||
label_send_test_email: Send a test email
|
label_send_test_email: Send a test email
|
||||||
|
label_feeds_access_key: RSS access key
|
||||||
|
label_missing_feeds_access_key: Missing a RSS access key
|
||||||
label_feeds_access_key_created_on: "RSS access key created {{value}} ago"
|
label_feeds_access_key_created_on: "RSS access key created {{value}} ago"
|
||||||
label_module_plural: Modules
|
label_module_plural: Modules
|
||||||
label_added_time_by: "Added by {{author}} {{age}} ago"
|
label_added_time_by: "Added by {{author}} {{age}} ago"
|
||||||
@ -729,6 +732,9 @@ en:
|
|||||||
label_copy_target: Target
|
label_copy_target: Target
|
||||||
label_copy_same_as_target: Same as target
|
label_copy_same_as_target: Same as target
|
||||||
label_display_used_statuses_only: Only display statuses that are used by this tracker
|
label_display_used_statuses_only: Only display statuses that are used by this tracker
|
||||||
|
label_api_access_key: API access key
|
||||||
|
label_missing_api_access_key: Missing an API access key
|
||||||
|
label_api_access_key_created_on: "API access key created {{value}} ago"
|
||||||
|
|
||||||
button_login: Login
|
button_login: Login
|
||||||
button_submit: Submit
|
button_submit: Submit
|
||||||
@ -836,6 +842,7 @@ en:
|
|||||||
text_wiki_page_nullify_children: "Keep child pages as root pages"
|
text_wiki_page_nullify_children: "Keep child pages as root pages"
|
||||||
text_wiki_page_destroy_children: "Delete child pages and all their descendants"
|
text_wiki_page_destroy_children: "Delete child pages and all their descendants"
|
||||||
text_wiki_page_reassign_children: "Reassign child pages to this parent page"
|
text_wiki_page_reassign_children: "Reassign child pages to this parent page"
|
||||||
|
text_show: Show
|
||||||
|
|
||||||
default_role_manager: Manager
|
default_role_manager: Manager
|
||||||
default_role_developper: Developer
|
default_role_developper: Developer
|
||||||
|
13
db/migrate/20091221004949_add_api_keys_for_users.rb
Normal file
13
db/migrate/20091221004949_add_api_keys_for_users.rb
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
class AddApiKeysForUsers < ActiveRecord::Migration
|
||||||
|
def self.up
|
||||||
|
say_with_time("Generating API keys for active users") do
|
||||||
|
User.active.all(:include => :api_token).each do |user|
|
||||||
|
user.api_key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.down
|
||||||
|
# No-op
|
||||||
|
end
|
||||||
|
end
|
@ -163,4 +163,38 @@ class MyControllerTest < ActionController::TestCase
|
|||||||
should_redirect_to('my account') {'/my/account' }
|
should_redirect_to('my account') {'/my/account' }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "POST to reset_api_key" do
|
||||||
|
context "with an existing api_token" do
|
||||||
|
setup do
|
||||||
|
@previous_token_value = User.find(2).api_key # Will generate one if it's missing
|
||||||
|
post :reset_api_key
|
||||||
|
end
|
||||||
|
|
||||||
|
should "destroy the existing token" do
|
||||||
|
assert_not_equal @previous_token_value, User.find(2).api_key
|
||||||
|
end
|
||||||
|
|
||||||
|
should "create a new token" do
|
||||||
|
assert User.find(2).api_token
|
||||||
|
end
|
||||||
|
|
||||||
|
should_set_the_flash_to /reset/
|
||||||
|
should_redirect_to('my account') {'/my/account' }
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with no api_token" do
|
||||||
|
setup do
|
||||||
|
assert_nil User.find(2).api_token
|
||||||
|
post :reset_api_key
|
||||||
|
end
|
||||||
|
|
||||||
|
should "create a new token" do
|
||||||
|
assert User.find(2).api_token
|
||||||
|
end
|
||||||
|
|
||||||
|
should_set_the_flash_to /reset/
|
||||||
|
should_redirect_to('my account') {'/my/account' }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -126,7 +126,9 @@ class UserTest < ActiveSupport::TestCase
|
|||||||
assert !anon.new_record?
|
assert !anon.new_record?
|
||||||
assert_kind_of AnonymousUser, anon
|
assert_kind_of AnonymousUser, anon
|
||||||
end
|
end
|
||||||
|
|
||||||
|
should_have_one :rss_token
|
||||||
|
|
||||||
def test_rss_key
|
def test_rss_key
|
||||||
assert_nil @jsmith.rss_token
|
assert_nil @jsmith.rss_token
|
||||||
key = @jsmith.rss_key
|
key = @jsmith.rss_key
|
||||||
@ -135,7 +137,55 @@ class UserTest < ActiveSupport::TestCase
|
|||||||
@jsmith.reload
|
@jsmith.reload
|
||||||
assert_equal key, @jsmith.rss_key
|
assert_equal key, @jsmith.rss_key
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
should_have_one :api_token
|
||||||
|
|
||||||
|
context "User#api_key" do
|
||||||
|
should "generate a new one if the user doesn't have one" do
|
||||||
|
user = User.generate_with_protected!(:api_token => nil)
|
||||||
|
assert_nil user.api_token
|
||||||
|
|
||||||
|
key = user.api_key
|
||||||
|
assert_equal 40, key.length
|
||||||
|
user.reload
|
||||||
|
assert_equal key, user.api_key
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the existing api token value" do
|
||||||
|
user = User.generate_with_protected!
|
||||||
|
token = Token.generate!(:action => 'api')
|
||||||
|
user.api_token = token
|
||||||
|
assert user.save
|
||||||
|
|
||||||
|
assert_equal token.value, user.api_key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "User#find_by_api_key" do
|
||||||
|
should "return nil if no matching key is found" do
|
||||||
|
assert_nil User.find_by_api_key('zzzzzzzzz')
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return nil if the key is found for an inactive user" do
|
||||||
|
user = User.generate_with_protected!(:status => User::STATUS_LOCKED)
|
||||||
|
token = Token.generate!(:action => 'api')
|
||||||
|
user.api_token = token
|
||||||
|
user.save
|
||||||
|
|
||||||
|
assert_nil User.find_by_api_key(token.value)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the user if the key is found for an active user" do
|
||||||
|
user = User.generate_with_protected!(:status => User::STATUS_ACTIVE)
|
||||||
|
token = Token.generate!(:action => 'api')
|
||||||
|
user.api_token = token
|
||||||
|
user.save
|
||||||
|
|
||||||
|
assert_equal user, User.find_by_api_key(token.value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def test_roles_for_project
|
def test_roles_for_project
|
||||||
# user with a role
|
# user with a role
|
||||||
roles = @jsmith.roles_for_project(Project.find(1))
|
roles = @jsmith.roles_for_project(Project.find(1))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user