From aa9951b38b27c7465a313fc72b73b819b292e9b2 Mon Sep 17 00:00:00 2001 From: Eric Davis Date: Wed, 23 Dec 2009 06:27:28 +0000 Subject: [PATCH] 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 --- app/controllers/my_controller.rb | 13 +++++ app/models/user.rb | 12 +++++ app/views/my/_sidebar.rhtml | 22 +++++++- app/views/my/account.rhtml | 12 +++++ config/locales/en.yml | 7 +++ .../20091221004949_add_api_keys_for_users.rb | 13 +++++ test/functional/my_controller_test.rb | 34 ++++++++++++ test/unit/user_test.rb | 52 ++++++++++++++++++- 8 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20091221004949_add_api_keys_for_users.rb diff --git a/app/controllers/my_controller.rb b/app/controllers/my_controller.rb index 64687d87e..f68675991 100644 --- a/app/controllers/my_controller.rb +++ b/app/controllers/my_controller.rb @@ -108,6 +108,19 @@ class MyController < ApplicationController redirect_to :action => 'account' 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 def page_layout @user = User.current diff --git a/app/models/user.rb b/app/models/user.rb index 4cfa2b47b..39fdb165a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -39,6 +39,7 @@ class User < Principal has_many :changesets, :dependent => :nullify has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' 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 # Active non-anonymous users scope @@ -192,6 +193,12 @@ class User < Principal token = self.rss_token || Token.create(:user => self, :action => 'feeds') token.value 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 def notified_projects_ids @@ -210,6 +217,11 @@ class User < Principal token && token.user.active? ? token.user : nil 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 def self.find_by_mail(mail) find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase]) diff --git a/app/views/my/_sidebar.rhtml b/app/views/my/_sidebar.rhtml index d30eacf90..1f511bdd2 100644 --- a/app/views/my/_sidebar.rhtml +++ b/app/views/my/_sidebar.rhtml @@ -2,7 +2,25 @@

<%=l(:field_login)%>: <%= @user.login %>
<%=l(:field_created_on)%>: <%= format_time(@user.created_on) %>

+ + +

<%= l(:label_feeds_access_key) %>

+ +

<% if @user.rss_token %> -

<%= 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 %>)

+<%= l(:label_feeds_access_key_created_on, distance_of_time_in_words(Time.now, @user.rss_token.created_on)) %> +<% else %> +<%= l(:label_missing_feeds_access_key) %> +<% end %> +(<%= link_to l(:button_reset), {:action => 'reset_rss_key'}, :method => :post %>) +

+ +

<%= l(:label_api_access_key) %>

+

+<% 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 %>) +

diff --git a/app/views/my/account.rhtml b/app/views/my/account.rhtml index 018414ee2..1b8347ccd 100644 --- a/app/views/my/account.rhtml +++ b/app/views/my/account.rhtml @@ -51,6 +51,18 @@

<%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %>

<% end %> + +<% if @user.api_token %> +

<%=l(:label_api_access_key) %>

+
+

+ <%= link_to_function(l(:text_show), "$('api-access-key').show();")%> +

<%= @user.api_key %>
+

+ <%= javascript_tag("$('api-access-key').hide();") %> +
+<% end %> + <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index cab7ff4a7..d75897bef 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -142,6 +142,7 @@ en: notice_email_sent: "An email was sent to {{value}}" notice_email_error: "An error occurred while sending mail ({{value}})" 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_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." @@ -668,6 +669,8 @@ en: label_language_based: Based on user's language label_sort_by: "Sort by {{value}}" 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_module_plural: Modules label_added_time_by: "Added by {{author}} {{age}} ago" @@ -729,6 +732,9 @@ en: label_copy_target: Target label_copy_same_as_target: Same as target 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_submit: Submit @@ -836,6 +842,7 @@ en: 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_reassign_children: "Reassign child pages to this parent page" + text_show: Show default_role_manager: Manager default_role_developper: Developer diff --git a/db/migrate/20091221004949_add_api_keys_for_users.rb b/db/migrate/20091221004949_add_api_keys_for_users.rb new file mode 100644 index 000000000..36fc7e1b3 --- /dev/null +++ b/db/migrate/20091221004949_add_api_keys_for_users.rb @@ -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 diff --git a/test/functional/my_controller_test.rb b/test/functional/my_controller_test.rb index b87180745..877095dfb 100644 --- a/test/functional/my_controller_test.rb +++ b/test/functional/my_controller_test.rb @@ -163,4 +163,38 @@ class MyControllerTest < ActionController::TestCase should_redirect_to('my account') {'/my/account' } 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 diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 2a4996539..a94870dbc 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -126,7 +126,9 @@ class UserTest < ActiveSupport::TestCase assert !anon.new_record? assert_kind_of AnonymousUser, anon end - + + should_have_one :rss_token + def test_rss_key assert_nil @jsmith.rss_token key = @jsmith.rss_key @@ -135,7 +137,55 @@ class UserTest < ActiveSupport::TestCase @jsmith.reload assert_equal key, @jsmith.rss_key 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 # user with a role roles = @jsmith.roles_for_project(Project.find(1)) -- 2.39.5