Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: chef/chef
base: master
...
head fork: RichardN/chef
Checking mergeability… Don't worry, you can still create the pull request.
  • 4 commits
  • 31 files changed
  • 0 commit comments
  • 2 contributors
Showing with 1,318 additions and 177 deletions.
  1. +8 −1 Rakefile
  2. +16 −8 chef-server-api/app/controllers/users.rb
  3. +3 −1 chef-server-api/bin/chef-server
  4. +10 −1 chef-server-api/config/router.rb
  5. +10 −4 chef-server-webui/app/controllers/users.rb
  6. +1 −1  chef-server-webui/app/views/users/edit.html.haml
  7. +4 −4 chef-server-webui/app/views/users/index.html.haml
  8. +1 −1  chef-server-webui/bin/chef-server-webui
  9. +9 −7 chef-server-webui/config/init.rb
  10. +4 −4 chef-server-webui/config/router.rb
  11. +0 −3  chef/lib/chef.rb
  12. +1 −0  chef/lib/chef/config.rb
  13. +1 −1  chef/lib/chef/index_queue/consumer.rb
  14. +0 −2  chef/lib/chef/rest.rb
  15. +165 −0 chef/lib/chef/web_ui_user/cdb_auth_module.rb
  16. +220 −0 chef/lib/chef/web_ui_user/ldap_auth_module.rb
  17. +62 −118 chef/lib/chef/webui_user.rb
  18. +4 −4 chef/spec/unit/cookbook/syntax_check_spec.rb
  19. +381 −0 chef/spec/unit/webui_ldap_user_spec.rb
  20. +162 −17 chef/spec/unit/webui_user_spec.rb
  21. +7 −0 cucumber.yml
  22. +34 −0 features/api/users/authenticate_user_api.feature
  23. +31 −0 features/api/users/create_user_api.feature
  24. +30 −0 features/api/users/delete_user_api.feature
  25. +33 −0 features/api/users/list_users_api.feature
  26. +24 −0 features/api/users/show_users_api.feature
  27. +62 −0 features/api/users/update_users_api.feature
  28. +1 −0  features/data/config/knife.rb
  29. +2 −0  features/data/config/server.rb
  30. +26 −0 features/steps/fixture_steps.rb
  31. +6 −0 features/support/env.rb
View
9 Rakefile
@@ -142,6 +142,7 @@ def start_chef_webui(type="normal")
if mcid # parent
@chef_webui_pid = mcid
else # child
+ sleep 5 # As we are using the API exclusively including for creating the test user, need to hold on a couple of ticks
case type
when "normal"
puts "Starting chef webui for development with './chef-server/bin/chef-server-webui -a thin -l debug -N'"
@@ -319,7 +320,7 @@ begin
end
namespace :api do
- [ :nodes, :roles, :clients ].each do |api|
+ [ :nodes, :roles, :clients, :users ].each do |api|
Cucumber::Rake::Task.new(api) do |apitask|
apitask.profile = "api_#{api.to_s}"
end
@@ -332,6 +333,12 @@ begin
end
end
+ namespace :users do
+ Cucumber::Rake::Task.new("authenticate") do |t|
+ t.profile = "api_users_authenticate"
+ end
+ end
+
namespace :nodes do
Cucumber::Rake::Task.new("sync") do |t|
t.profile = "api_nodes_sync"
View
24 chef-server-api/app/controllers/users.rb
@@ -17,25 +17,27 @@
require File.join("chef", "webui_user")
+require "uri"
class Users < Application
provides :json
before :authenticate_every
before :is_admin, :only => [ :create, :destroy, :update ]
+ log_params_filtered :given_password, :new_password, :confirm_new_password
# GET to /users
def index
@user_list = Chef::WebUIUser.cdb_list
- display(@user_list.inject({}) { |r,n| r[n] = absolute_url(:user, n); r })
+ display(@user_list.inject({}) { |r,n| r[n] = absolute_url(:user, URI.escape(n,URI::REGEXP::PATTERN::RESERVED)); r })
end
# GET to /users/:id
def show
begin
- @user = Chef::WebUIUser.cdb_load(params[:id])
+ @user = Chef::WebUIUser.cdb_load(URI.unescape(params[:id]))
rescue Chef::Exceptions::CouchDBNotFound => e
- raise NotFound, "Cannot load user #{params[:id]}"
+ raise NotFound, "Cannot load user #{URI.unescape(params[:id])}"
end
display @user
end
@@ -43,9 +45,9 @@ def show
# PUT to /users/:id
def update
begin
- Chef::WebUIUser.cdb_load(params[:id])
+ Chef::WebUIUser.cdb_load(URI.unescape(params[:id]))
rescue Chef::Exceptions::CouchDBNotFound => e
- raise NotFound, "Cannot load user #{params[:id]}"
+ raise NotFound, "Cannot load user #{URI.unescape(params[:id])}"
end
@user = params['inflated_object']
@user.cdb_save
@@ -63,14 +65,20 @@ def create
else
raise Conflict, "User already exists"
end
- display({ :uri => absolute_url(:user, @user.name) })
+ display({ :uri => absolute_url(:user, @user.escaped_name) } )
+ end
+
+ # POST to /users/:id/authentication
+ def authenticate
+ @user = Chef::WebUIUser.cdb_load(URI.unescape(params[:id]))
+ display({ :authenticated => @user.cdb_verify_password(params[:given_password]) })
end
def destroy
begin
- @user = Chef::WebUIUser.cdb_load(params[:id])
+ @user = Chef::WebUIUser.cdb_load(URI.unescape(params[:id]))
rescue Chef::Exceptions::CouchDBNotFound => e
- raise NotFound, "Cannot load user #{params[:id]}"
+ raise NotFound, "Cannot load user #{URI.unescape(params[:id])}"
end
@user.cdb_destroy
display @user
View
4 chef-server-api/bin/chef-server
@@ -29,7 +29,6 @@ $:.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
require 'chef'
require 'chef-server-api'
-
# Ensure the chef gem we load is the same version as the chef server
unless defined?(Chef)
gem "chef", "=" + CHEF_SERVER_VERSION
@@ -69,6 +68,9 @@ else
)
end
+#Select the authentication module
+Chef::WebUIUser.select_authentication_module
+
Chef::Log.init(Chef::Config[:log_location])
Chef::Log.level = Chef::Config[:log_level]
View
11 chef-server-api/config/router.rb
@@ -1,5 +1,14 @@
Merb::Router.prepare do
- resources :users
+ # Users
+
+
+ resources :users, :id => /[^\/]+/
+ match('/users/:id/authentication',
+ :id => /[^\/]+/,
+ :method => 'post').
+ to(:controller => "users", :action => "authenticate")
+
+
# Nodes
resources :nodes, :id => /[^\/]+/
View
14 chef-server-webui/app/controllers/users.rb
@@ -30,7 +30,8 @@ class Users < Application
def index
begin
authorized_user
- @users = Chef::WebUIUser.list
+ # Replace the URLs from the rest query with URL escaped usernames
+ @users = Chef::WebUIUser.list.keys.inject({}) { |memo,u| memo[u]=URI.escape(u,URI::REGEXP::PATTERN::RESERVED) ; memo }
render
rescue => e
Chef::Log.error("#{e}\n#{e.backtrace.join("\n")}")
@@ -76,7 +77,9 @@ def update
end
if not params[:new_password].nil? and not params[:new_password].length == 0
- @user.set_password(params[:new_password], params[:confirm_new_password])
+# @user.set_password(params[:new_password], params[:confirm_new_password])
+ @user.new_password = params[:new_password]
+ @user.confirm_new_password = params[:confirm_new_password]
end
if params[:openid].length == 0 or params[:openid].nil?
@@ -110,7 +113,8 @@ def create
authorized_user
@user = Chef::WebUIUser.new
@user.name = params[:name]
- @user.set_password(params[:password], params[:password2])
+ @user.new_password = params[:password]
+ @user.confirm_new_password = params[:password2]
@user.admin = true if params[:admin]
(params[:openid].length == 0 || params[:openid].nil?) ? @user.set_openid(nil) : @user.set_openid(URI.parse(params[:openid]).normalize.to_s)
@user.create
@@ -179,8 +183,10 @@ def set_user_and_redirect
def redirect_to_list_users(message)
@_message = message
- @users = Chef::WebUIUser.list
+ @users = Chef::WebUIUser.list.keys.inject({}) { |memo,u| memo[u]=URI.escape(u,URI::REGEXP::PATTERN::RESERVED) ; memo }
render :index
end
+
+
end
View
2  chef-server-webui/app/views/users/edit.html.haml
@@ -3,4 +3,4 @@
%h2.title= "User: #{h @user.name}"
.inner
= partial('navigation', :active => 'edit')
- = partial('form', :header => "Edit User #{@user.name}", :form_id => 'edit_user', :submit_name => "Save User", :submit_id => "edit_user_button", :form_for => 'edit', :form_url => url(:users_update, @user.name) )
+ = partial('form', :header => "Edit User #{@user.name}", :form_id => 'edit_user', :submit_name => "Save User", :submit_id => "edit_user_button", :form_for => 'edit', :form_url => url(:users_update, @user.escaped_name) )
View
8 chef-server-webui/app/views/users/index.html.haml
@@ -11,10 +11,10 @@
%th &nbsp;
%th.last &nbsp;
- even = false
- - @users.sort.each do |user, user_url|
+ - @users.sort.each do |user, user_url_safe|
%tr{:class => even ? "even": "odd" }
- %td{:colspan => 2}= link_to user, url(:users_show, :user_id => user)
+ %td{:colspan => 2}= link_to user, url(:users_show, :user_id => user_url_safe )
%td
- = link_to('Edit', url(:users_edit, :user_id => user))
+ = link_to('Edit', url(:users_edit, :user_id => user_url_safe))
|
- = link_to('Delete', url(:users_delete, :user_id => user), :method => "delete", :confirm => "Really delete User #{user}? There is no undo.")
+ = link_to('Delete', url(:users_delete, :user_id => user_url_safe), :method => "delete", :confirm => "Really delete User #{user}? There is no undo.")
View
2  chef-server-webui/bin/chef-server-webui
@@ -56,7 +56,7 @@ else
File.join("/etc", "chef", "server.rb")
)
end
-
+Chef::WebUIUser.select_authentication_module
Chef::Log.init(Chef::Config[:log_location])
Chef::Log.level = Chef::Config[:log_level]
View
16 chef-server-webui/config/init.rb
@@ -21,7 +21,7 @@
# code and views.
#
-
+puts "starting init"
require "merb-core"
require "merb-haml"
require "merb-assets"
@@ -59,10 +59,12 @@
Chef::Config[:client_key] = Chef::Config[:web_ui_key]
# Create the default admin user "admin" if no admin user exists
-unless Chef::WebUIUser.admin_exist
- user = Chef::WebUIUser.new
- user.name = Chef::Config[:web_ui_admin_user_name]
- user.set_password(Chef::Config[:web_ui_admin_default_password])
- user.admin = true
- user.save
+if Chef::WebUIUser.auth_module_name == 'CDBAuthModuleClassMethods'
+ unless Chef::WebUIUser.admin_exist
+ user = Chef::WebUIUser.new
+ user.name = Chef::Config[:web_ui_admin_user_name]
+ user.new_password = user.confirm_new_password = Chef::Config[:web_ui_admin_default_password]
+ user.admin = true
+ user.save
+ end
end
View
8 chef-server-webui/config/router.rb
@@ -54,10 +54,10 @@
match('/users/complete').to(:controller => 'users', :action => 'complete').name(:users_complete)
match('/users/logout').to(:controller => 'users', :action => 'logout').name(:users_logout)
match('/users/new').to(:controller => 'users', :action => 'new').name(:users_new)
- match('/users/:user_id/edit').to(:controller => 'users', :action => 'edit').name(:users_edit)
- match('/users/:user_id').to(:controller => 'users', :action => 'show').name(:users_show)
- match('/users/:user_id/delete', :method => 'delete').to(:controller => 'users', :action => 'destroy').name(:users_delete)
- match('/users/:user_id/update', :method => 'put').to(:controller => 'users', :action => 'update').name(:users_update)
+ match('/users/:user_id/edit', :user_id => /[^\/]+/).to(:controller => 'users', :action => 'edit').name(:users_edit)
+ match('/users/:user_id', :user_id => /[^\/]+/).to(:controller => 'users', :action => 'show').name(:users_show)
+ match('/users/:user_id/delete', :user_id => /[^\/]+/, :method => 'delete').to(:controller => 'users', :action => 'destroy').name(:users_delete)
+ match('/users/:user_id/update', :user_id => /[^\/]+/, :method => 'put').to(:controller => 'users', :action => 'update').name(:users_update)
match('/').to(:controller => 'nodes', :action =>'index').name(:top)
end
View
3  chef/lib/chef.rb
@@ -15,7 +15,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
-
require 'chef/version'
require 'extlib'
@@ -25,11 +24,9 @@
require 'chef/providers'
require 'chef/resources'
require 'chef/shell_out'
-
require 'chef/daemon'
require 'chef/webui_user'
require 'chef/openid_registration'
-
require 'chef/run_status'
require 'chef/handler'
require 'chef/handler/json_file'
View
1  chef/lib/chef/config.rb
@@ -181,6 +181,7 @@ def self.inspect
web_ui_key "/etc/chef/webui.pem"
web_ui_admin_user_name "admin"
web_ui_admin_default_password "p@ssw0rd1"
+ web_ui_authentication_module lambda{Chef::WebUIUser::CDBAuthModule}
# Server Signing CA
#
View
2  chef/lib/chef/index_queue/consumer.rb
@@ -73,4 +73,4 @@ def assert_method_whitelisted(method_name)
end
end
-end
+end
View
2  chef/lib/chef/rest.rb
@@ -194,10 +194,8 @@ def run_request(method, url, headers={}, data=false, limit=nil, raw=false)
def api_request(method, url, headers={}, data=false)
json_body = data ? data.to_json : nil
headers = build_headers(method, url, headers, json_body)
-
retriable_rest_request(method, url, json_body, headers) do |rest_request|
response = rest_request.call {|r| r.read_body}
-
if response.kind_of?(Net::HTTPSuccess)
if response['content-type'] =~ /json/
JSON.parse(response.body.chomp)
View
165 chef/lib/chef/web_ui_user/cdb_auth_module.rb
@@ -0,0 +1,165 @@
+#
+# Author:: Richard Nicholas (<richard.nicholas@betfair.com>)
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Nuo Yan (<nuo@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require "chef/couchdb"
+
+class Chef
+ class WebUIUser
+ module CDBAuthModule
+
+ def name=(n)
+ # gsub no longer needed as names are URI escaped for use outside the REST API
+ @name = n # .gsub(/\./, '_')
+ end
+
+ def set_openid(given_openid)
+ @openid = given_openid
+ end
+
+ def admin=(new_admin_status)
+ @admin = new_admin_status
+ end
+
+ def couchdb=(couch_db_instance)
+ @couchdb = couch_db_instance
+ end
+
+ def openid=(new_open_id)
+ @openid = new_open_id
+ end
+
+ def couchdb
+ @couchdb || Chef::CouchDB.new
+ end
+
+ def self.included(base)
+ base.extend Chef::WebUIUser::CDBAuthModuleClassMethods
+ end
+
+ # Set the password for this object.
+ def cdb_set_password(password, confirm_password=password)
+ raise ArgumentError, "Passwords do not match" unless password == confirm_password
+ raise ArgumentError, "Password cannot be blank" if (password.nil? || password.length==0)
+ raise ArgumentError, "Password must be a minimum of 6 characters" if password.length < 6
+ generate_salt
+ @password = encrypt_password(password)
+ end
+
+ # Verify the password for this object
+ def cdb_verify_password(given_password)
+ encrypt_password(given_password) == @password
+ end
+
+ # Remove this WebUIUser from the CouchDB
+ def cdb_destroy
+ couchdb.delete("webui_user", @name, @couchdb_rev)
+ end
+
+ # Save this WebUIUser to the CouchDB
+ def cdb_save
+ cdb_set_password( @new_password, @confirm_new_password ) if @new_password
+ @new_password, @confirm_new_password = nil, nil # Just to ensure that we don't save them!
+ results = couchdb.store("webui_user", @name, self)
+ @couchdb_rev = results["rev"]
+ results
+ end
+
+ def instance_auth_module_name
+ "CDBAuthModule"
+ end
+
+ protected
+
+ def generate_salt
+ @salt = Time.now.to_s
+ chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
+ 30.times { @salt << chars[rand(chars.size-1)] }
+ @salt
+ end
+
+ def encrypt_password(password)
+ Digest::SHA1.hexdigest("--#{salt}--#{password}--")
+ end
+
+ end
+
+ module CDBAuthModuleClassMethods
+
+ DESIGN_DOCUMENT = {
+ "version" => 3,
+ "language" => "javascript",
+ "views" => {
+ "all" => {
+ "map" => <<-EOJS
+ function(doc) {
+ if (doc.chef_type == "webui_user") {
+ emit(doc.name, doc);
+ }
+ }
+ EOJS
+ },
+ "all_id" => {
+ "map" => <<-EOJS
+ function(doc) {
+ if (doc.chef_type == "webui_user") {
+ emit(doc.name, doc.name);
+ }
+ }
+ EOJS
+ },
+ },
+ }
+
+ def auth_module_name
+ 'CDBAuthModuleClassMethods'
+ end
+
+ # List all the Chef::WebUIUser objects in the CouchDB. If inflate is set to true, you will get
+ # the full list of all registration objects. Otherwise, you'll just get the IDs
+ def cdb_list(inflate=false)
+ rs = Chef::CouchDB.new.list("users", inflate)
+ rs["rows"].collect { |r| r[inflate ? "value" : "key" ]}
+ end
+
+ # Load an WebUIUser by name from CouchDB
+ def cdb_load(name)
+ Chef::CouchDB.new.load("webui_user", name)
+ end
+
+ # Whether or not there is an WebUIUser with this key.
+ def cdb_has_key?(name)
+ Chef::CouchDB.new.has_key?("webui_user", name)
+ end
+
+ #return true if an admin user exists. this is pretty expensive (O(n)), should think of a better way (nuo)
+ def cdb_admin_exist
+ users = self.cdb_list
+ users.any?{ |u| self.cdb_load(u).admin }
+ end
+
+ # Set up our CouchDB design document
+ def create_design_document(couchdb=nil)
+ couchdb ||= Chef::CouchDB.new
+ couchdb.create_design_document("users", DESIGN_DOCUMENT)
+ end
+
+ end
+ end
+end
View
220 chef/lib/chef/web_ui_user/ldap_auth_module.rb
@@ -0,0 +1,220 @@
+#
+# Author:: Richard Nicholas (<richard.nicholas@betfair.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+require "net/ldap"
+require "chef/config"
+require "chef/couchdb"
+require "chef/web_ui_user/cdb_auth_module"
+
+class Chef
+ class WebUIUser
+ class LDAPUser
+
+ attr_accessor :user
+
+ def initialize(opts = {})
+ @user = opts['user']
+ @group_membership_attribute = ( opts['group_membership_attribute'] ||= Chef::Config[:ldap_group_attribute]).to_sym
+ @admin_groups = opts['admin_groups'] ||= Chef::Config[:ldap_chef_admin_groups]
+ @user_groups = opts['user_groups'] ||= Chef::Config[:ldap_chef_user_groups]
+ end
+
+ def self.load(results)
+ if !results
+ nil
+ elsif results.size == 0
+ nil
+ elsif results.size != 1
+ raise ArgumentError, "Tried to bind user to multiple entries."
+ else
+ LDAPUser.new('user' => results.first,
+ 'group_membership_attribute' => @group_membership_attribute,
+ 'admin_groups' => @admin_groups,
+ 'user_groups' => @user_groups )
+ end
+ end
+
+ def member_of_group(group_name_list)
+ if group_name_list.kind_of?(Array)
+ group_name_list.any?{ |g| member_of_group(g) }
+ else
+ [@user[@group_membership_attribute.to_s]].include?(group_name_list)
+ end
+ end
+
+ def is_admin?
+ member_of_group(@admin_groups)
+ end
+
+ def is_user?
+ member_of_group(@user_groups) || is_admin?
+ end
+
+ end #LDAPUser
+
+ class LDAPConnection
+
+ # Create a new LDAP connection
+ def initialize(opts = {})
+ @port = opts['port'] ||= Chef::Config[:ldap_port].to_i
+ @hosts = [(opts['hosts'] ||= Chef::Config[:ldap_hosts])].flatten
+ @method = Chef::Config[:ldap_encryption] ? :simple_tls : nil
+ @method = ( opts['encrypt'] ? :simple_tls : nil ) if opts.has_key?('encrypt')
+ @username = opts['username'] ||= Chef::Config[:ldap_bind_user]
+ @password = opts['password'] ||= Chef::Config[:ldap_initial_bind_password]
+ @auth = { :method => :simple, :username => @username, :password => @password }
+ @base = opts['base'] ||= Chef::Config[:ldap_base_root_initial_bind] ||= Chef::Config[:ldap_base_root].dup
+ @conn = Net::LDAP.new( :base => @base, :host => @hosts.first, :port => @port, :auth => @auth, :encryption => @method )
+ end
+
+ # Bind to the LDAP connection, with fallback on failure to alternative hosts
+ def bind
+ # Turn hosts into an array if it isn't one
+ [@hosts].flatten.any? do |h|
+ @conn.host = h
+ @conn.bind
+ end
+ end
+
+ # Bind with credentials given to a specified search path, search_root can be a Proc, which is called with name as the parameter
+ # to allow for the username to be pre-processed to allow e.g for names to be input in the Active Directory domain\userid style
+ #
+ # User_field can also be a Proc and this is called with the given username to allow for more complex filter operations
+ def bind_as(username, password, search_root = Chef::Config[:ldap_base_root],
+ user_attribute = Chef::Config[:ldap_user_attribute],
+ user_preprocess = Chef::Config[:ldap_user_preprocess])
+ if bind
+ search_root = LDAPConnection.call_if_proc(search_root, username)
+ actual_username = LDAPConnection.call_if_proc(user_preprocess, username, username)
+ search_filter = LDAPConnection.call_if_proc(user_attribute, actual_username, "(#{user_attribute}=#{LDAPConnection.ldap_escape(actual_username)})")
+ Chef::WebUIUser::LDAPUser.load(@conn.bind_as(:base => search_root, :password => password, :filter => search_filter))
+ else
+ raise ArgumentError, "Unable to bind to any LDAP server"
+ end
+ end
+
+ # Performs an LDAP search for the username.
+ def ldap_search_for(username, search_root = Chef::Config[:ldap_base_root],
+ user_attribute = Chef::Config[:ldap_user_attribute],
+ user_preprocess = Chef::Config[:ldap_user_preprocess])
+ if bind
+ search_root = LDAPConnection.call_if_proc(search_root, username)
+ actual_username = LDAPConnection.call_if_proc(user_preprocess, username, username)
+ search_filter = LDAPConnection.call_if_proc(user_attribute, actual_username, "(#{user_attribute}=#{LDAPConnection.ldap_escape(actual_username)})")
+ Chef::WebUIUser::LDAPUser.load(@conn.search(:filter => search_filter, :base => search_root))
+ else
+ raise ArgumentError, "Unable to bind to any LDAP server"
+ end
+ end
+
+ # Escapes the string so that it safe for use in LDAP search operations
+ def self.ldap_escape(cn)
+ cn.gsub(/[*()\\\00\/]/) { |c| "\\#{c.unpack('H*')[0]}" }
+ end
+
+ private
+
+ # If "thing" is a proc, return the result of calling it now with the given parameter
+ # If "thing" is something else return the given parameter, or the third parameter if present
+ def self.call_if_proc(thing, param, return_if_not_proc = thing )
+ thing.kind_of?(Proc) ? thing.call(param) : return_if_not_proc
+ end
+
+ end
+
+ module LDAPAuthModule
+
+ include Chef::WebUIUser::CDBAuthModule
+
+ def self.included(base)
+ base.extend Chef::WebUIUser::LDAPAuthModuleClassMethods
+ end
+
+ # (Don't!) Set the password for this object. In normal use this shouldn't be called as errors are caught elsewhere with
+ # the REST interface.
+ def cdb_set_password(password,confirm_password=password)
+ raise ArgumentError, "Passwords are controlled by the LDAP provider" if password || password != ""
+ end
+
+ # Verify the password for this object
+ def cdb_verify_password(given_password)
+ begin
+ ldap_conn = Chef::WebUIUser::LDAPConnection.new
+ auth_user = ldap_conn.bind_as(@name,given_password)
+ rescue
+ raise ArgumentError, "#{ldap_conn.get_operation_result.message} #{ldap_conn.get_operation_result.code}"
+ end
+ auth_user.is_user?
+ end
+
+ # Save updates to the user providing that they do not clash with LDAP settings
+ def cdb_save
+ raise_error_if_present :new_password, :confirm_new_password
+ ldap_conn = Chef::WebUIUser::LDAPConnection.new
+ ldap_user = ldap_conn.ldap_search_for(name)
+ if ldap_user && ldap_user.is_user?
+ admin = ldap_user.is_admin?
+ results = couchdb.store("webui_user", name, self)
+ @couchdb_rev = results["rev"]
+ results
+ else
+ raise ArgumentError, "Cannot save as Chef user not found in LDAP"
+ end
+ end
+
+ def instance_auth_module_name
+ "LDAPAuthModule"
+ end
+
+ end
+
+ module LDAPAuthModuleClassMethods
+
+ include Chef::WebUIUser::CDBAuthModuleClassMethods
+
+ def auth_module_name
+ 'LDAPAuthModuleClassMethods'
+ end
+
+ # Load an WebUIUser by name from LDAP. If the user is not in LDAP and is in couchDB, get it from there.
+ # We have to do this so that old (deleted from LDAP) users can be deleted from couchdb.
+ def cdb_load(name)
+ ldap_conn = Chef::WebUIUser::LDAPConnection.new
+ ldap_user = ldap_conn.ldap_search_for(name)
+ begin
+ u = super(name)
+ rescue Chef::Exceptions::CouchDBNotFound => e
+ # If the user exists in LDAP and not in couchdb, store a basic new user in couchdb
+ if ldap_user && ldap_user.is_user?
+ u = Chef::WebUIUser.new('name' => name )
+ u.admin = ldap_user.is_admin?
+ u.cdb_save
+ else
+ raise Chef::Exceptions::CouchDBNotFound,"User not found in LDAP or CouchDB"
+ end
+ end
+ # override admin setting with setting from LDAP if present
+ u.admin = ldap_user.is_admin? if ldap_user
+ u
+ end
+ end
+
+ end
+end
+
View
180 chef/lib/chef/webui_user.rb
@@ -16,82 +16,72 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+#if Chef::Config && Chef::Config.class == Module
+ # Whoops.. Chef::Config is pointing at the Config Module used by rbconfig!
+ # This seems to happen when called via Knife. Reload config to fix the problem
+# load 'chef/config.rb'
+#else
+ require 'chef/config'
+#end
-require 'chef/config'
require 'chef/mixin/params_validate'
-require 'chef/couchdb'
require 'chef/index_queue'
require 'digest/sha1'
require 'json'
-
+require 'uri'
+
+Dir[File.join(File.dirname(__FILE__), 'web_ui_user', '*.rb')].sort.each { |lib| require lib }
class Chef
class WebUIUser
- attr_accessor :name, :validated, :admin, :openid, :couchdb
- attr_reader :password, :salt, :couchdb_id, :couchdb_rev
+ attr_accessor :validated, :admin, :name, :openid
+ attr_reader :password, :salt, :couchdb_id, :couchdb_rev, :authentication_status, :ui_suppressed_fields
+ attr_accessor :new_password, :confirm_new_password
include Chef::Mixin::ParamsValidate
include Chef::IndexQueue::Indexable
- DESIGN_DOCUMENT = {
- "version" => 3,
- "language" => "javascript",
- "views" => {
- "all" => {
- "map" => <<-EOJS
- function(doc) {
- if (doc.chef_type == "webui_user") {
- emit(doc.name, doc);
- }
- }
- EOJS
- },
- "all_id" => {
- "map" => <<-EOJS
- function(doc) {
- if (doc.chef_type == "webui_user") {
- emit(doc.name, doc.name);
- }
- }
- EOJS
- },
- },
- }
-
+
+ def self.select_authentication_module(auth_module_proc=Chef::Config[:web_ui_authentication_module])
+ self.send(:include,auth_module_proc.call)
+ end
+
# Create a new Chef::WebUIUser object.
def initialize(opts={})
@name, @salt, @password = opts['name'], opts['salt'], opts['password']
+ @new_password, @confirm_new_password = opts['new_password'], opts['confirm_new_password']
@openid, @couchdb_rev, @couchdb_id = opts['openid'], opts['_rev'], opts['_id']
@admin = false
- @couchdb = Chef::CouchDB.new
- end
-
- def name=(n)
- @name = n.gsub(/\./, '_')
end
-
+
def admin?
admin
end
-
- # Set the password for this object.
- def set_password(password, confirm_password=password)
- raise ArgumentError, "Passwords do not match" unless password == confirm_password
- raise ArgumentError, "Password cannot be blank" if (password.nil? || password.length==0)
- raise ArgumentError, "Password must be a minimum of 6 characters" if password.length < 6
- generate_salt
- @password = encrypt_password(password)
- end
-
- def set_openid(given_openid)
- @openid = given_openid
- end
-
+
def verify_password(given_password)
- encrypt_password(given_password) == @password
- end
+ r = Chef::REST.new(Chef::Config[:chef_server_url])
+ r.post_rest("users/#{URI.escape(name,URI::REGEXP::PATTERN::RESERVED)}/authentication",{ :given_password => given_password })["authenticated"]
+ end
+ # Transform the node to a Hash
+ def to_hash
+ # TODO: DRY this and to_json up!
+ result = {
+ 'name' => name,
+ 'salt' => salt,
+ 'password' => password,
+ 'openid' => openid,
+ 'admin' => admin,
+ 'chef_type' => 'webui_user'
+ }
+ result["_id"] = @couchdb_id if @couchdb_id
+ result["_rev"] = @couchdb_rev if @couchdb_rev
+ result["new_password"] = @new_password if @new_password
+ result["confirm_new_password"] = @confirm_new_password if @confirm_new_password
+ result
+ end
+
# Serialize this object as a hash
def to_json(*a)
attributes = Hash.new
@@ -107,6 +97,8 @@ def to_json(*a)
}
result["_id"] = @couchdb_id if @couchdb_id
result["_rev"] = @couchdb_rev if @couchdb_rev
+ result["new_password"] = @new_password if @new_password
+ result["confirm_new_password"] = @confirm_new_password if @confirm_new_password
result.to_json(*a)
end
@@ -116,18 +108,7 @@ def self.json_create(o)
me.admin = o["admin"]
me
end
-
- # List all the Chef::WebUIUser objects in the CouchDB. If inflate is set to true, you will get
- # the full list of all registration objects. Otherwise, you'll just get the IDs
- def self.cdb_list(inflate=false)
- rs = Chef::CouchDB.new.list("users", inflate)
- if inflate
- rs["rows"].collect { |r| r["value"] }
- else
- rs["rows"].collect { |r| r["key"] }
- end
- end
-
+
def self.list(inflate=false)
r = Chef::REST.new(Chef::Config[:chef_server_url])
if inflate
@@ -141,45 +122,23 @@ def self.list(inflate=false)
end
end
- # Load an WebUIUser by name from CouchDB
- def self.cdb_load(name)
- Chef::CouchDB.new.load("webui_user", name)
- end
-
# Load a User by name
def self.load(name)
- r = Chef::REST.new(Chef::Config[:chef_server_url])
- r.get_rest("users/#{name}")
- end
-
-
- # Whether or not there is an WebUIUser with this key.
- def self.has_key?(name)
- Chef::CouchDB.new.has_key?("webui_user", name)
- end
-
- # Remove this WebUIUser from the CouchDB
- def cdb_destroy
- couchdb.delete("webui_user", @name, @couchdb_rev)
+ r = Chef::REST.new(Chef::Config[:chef_server_url])
+ r.get_rest("users/#{URI.escape(name,URI::REGEXP::PATTERN::RESERVED)}")
end
# Remove this WebUIUser via the REST API
def destroy
r = Chef::REST.new(Chef::Config[:chef_server_url])
- r.delete_rest("users/#{@name}")
- end
-
- # Save this WebUIUser to the CouchDB
- def cdb_save
- results = couchdb.store("webui_user", @name, self)
- @couchdb_rev = results["rev"]
+ r.delete_rest("users/#{URI.escape(name,URI::REGEXP::PATTERN::RESERVED)}")
end
# Save this WebUIUser via the REST API
def save
r = Chef::REST.new(Chef::Config[:chef_server_url])
begin
- r.put_rest("users/#{@name}", self)
+ r.put_rest("users/#{URI.escape(name,URI::REGEXP::PATTERN::RESERVED)}", self)
rescue Net::HTTPServerException => e
if e.response.code == "404"
r.post_rest("users", self)
@@ -196,37 +155,22 @@ def create
r.post_rest("users", self)
self
end
-
- # Set up our CouchDB design document
- def self.create_design_document(couchdb=nil)
- couchdb ||= Chef::CouchDB.new
- couchdb.create_design_document("users", DESIGN_DOCUMENT)
- end
-
- #return true if an admin user exists. this is pretty expensive (O(n)), should think of a better way (nuo)
+
def self.admin_exist
- users = self.cdb_list
- users.each do |u|
- user = self.cdb_load(u)
- if user.admin
- return user.name
- end
- end
- nil
+ self.list.any?{ |u,url| self.load(u).admin? }
end
- protected
-
- def generate_salt
- @salt = Time.now.to_s
- chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
- 1.upto(30) { |i| @salt << chars[rand(chars.size-1)] }
- @salt
- end
-
- def encrypt_password(password)
- Digest::SHA1.hexdigest("--#{salt}--#{password}--")
+ def escaped_name
+ URI.escape(name, URI::REGEXP::PATTERN::RESERVED)
+ end
+
+
+
+ def raise_error_if_present(*args)
+ args.each do |arg|
+ raise ArgumentError, "#{arg} cannot be set with the #{self.instance_auth_module_name}" if self.send(arg) && self.send(arg) != ''
end
-
+ end
+
end
end
View
8 chef/spec/unit/cookbook/syntax_check_spec.rb
@@ -111,11 +111,11 @@
end
it "lists the ruby files in the cookbook" do
- @syntax_check.ruby_files.should == @ruby_files
+ @syntax_check.ruby_files.sort.should == @ruby_files.sort
end
it "lists the erb templates in the cookbook" do
- @syntax_check.template_files.should == @template_files
+ @syntax_check.template_files.sort.should == @template_files.sort
end
end
@@ -131,11 +131,11 @@
describe "and the files have not been syntax checked previously" do
it "shows that all ruby files require a syntax check" do
- @syntax_check.untested_ruby_files.should == @ruby_files
+ @syntax_check.untested_ruby_files.sort.should == @ruby_files.sort
end
it "shows that all template files require a syntax check" do
- @syntax_check.untested_template_files.should == @template_files
+ @syntax_check.untested_template_files.sort.should == @template_files.sort
end
it "removes a ruby file from the list of untested files after it is marked as validated" do
View
381 chef/spec/unit/webui_ldap_user_spec.rb
@@ -0,0 +1,381 @@
+#
+# Author:: Richard Nicholas (<richard.nicholas@betfair.com>)
+# Copyright:: Copyright (c) 2010
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper"))
+
+describe Chef::WebUIUser, "with LDAP authentication module" do
+
+ before(:all) do
+ # Next line is brutal, but we need to be sure that only the authentication we want is loaded!
+ begin
+ Chef.send(:remove_const,:WebUIUser)
+ rescue
+ end
+ Dir[File.join(File.dirname(__FILE__),'..','..','lib','chef', 'web_ui_user', '*.rb')].sort.each { |lib| load lib }
+ load File.join(File.dirname(__FILE__),'..','..','lib','chef','webui_user.rb')
+ Chef::Config.stub!(:[]).with(:web_ui_authentication_module).and_return(lambda{Chef::WebUIUser::LDAPAuthModule})
+ Chef::WebUIUser.select_authentication_module
+ end
+
+ before(:each) do
+ @webui_user = Chef::WebUIUser.new
+ Chef::Config.stub!(:[]).with(:ldap_hosts).and_return(["fail_ldap_host","work_ldap_host"])
+ Chef::Config.stub!(:[]).with(:ldap_port).and_return(389)
+ Chef::Config.stub!(:[]).with(:ldap_bind_user).and_return("bobby@example.com")
+ Chef::Config.stub!(:[]).with(:ldap_bind_password).and_return("valid_password")
+ Chef::Config.stub!(:[]).with(:ldap_chef_user_groups).and_return("cn=chef_user_group,dc=example,dc=com")
+ Chef::Config.stub!(:[]).with(:ldap_chef_admin_groups).and_return(["cn=chef_admin_group,dc=example,dc=com","cn=other_chef_admin_group,dc=example,dc=com"])
+ Chef::Config.stub!(:[]).with(:ldap_user_attribute).and_return("sAMAccountName")
+ Chef::Config.stub!(:[]).with(:ldap_group_attribute).and_return("memberOf")
+ Chef::Config.stub!(:[]).with(:ldap_base_root).and_return("dc=example,dc=com")
+ end
+
+ it "should be using LDAP instance methods" do
+ Chef::WebUIUser.new.instance_auth_module_name.should == "LDAPAuthModule"
+ end
+
+ it "should be using LDAP class methods" do
+ Chef::WebUIUser.auth_module_name.should == "LDAPAuthModuleClassMethods"
+ end
+
+
+
+ it "can be initialized with a hash to set instance variables" do
+ opt_hsh = {'name'=>'mud', 'salt'=>'just_a_lil', 'password'=>'beefded',
+ 'openid'=>'notsomuch', '_rev'=>'0', '_id'=>'tehid',
+ 'new_password'=>'blahblah', 'confirm_new_password'=>'blahblahtoo'}
+ webui_user = Chef::WebUIUser.new(opt_hsh)
+ webui_user.name.should == 'mud'
+ webui_user.salt.should == 'just_a_lil'
+ webui_user.password.should == 'beefded'
+ webui_user.openid.should == 'notsomuch'
+ webui_user.couchdb_rev.should == '0'
+ webui_user.couchdb_id.should == 'tehid'
+ webui_user.new_password.should == 'blahblah'
+ webui_user.confirm_new_password.should == 'blahblahtoo'
+ end
+
+ describe "when bad authentication module is selected" do
+
+ it "should raise an error if a string is supplied" do
+ Chef::Config.stub!(:[]).with(:web_ui_authentication_module).and_return('Chef::WebUIUser::LDAPAuthModule')
+ lambda {Chef::WebUIUser.select_authorisation_module}.should raise_error(NoMethodError)
+ end
+
+ it "should raise an error if a non existing module is selected" do
+ Chef::Config.stub!(:[]).with(:web_ui_authentication_module).and_return(lambda{Chef::WebUIUser::NonExistantModule})
+ lambda {Chef::WebUIUser.select_authorisation_module}.should raise_error(NameError)
+ end
+
+
+ end
+
+ describe Chef::WebUIUser::LDAPConnection do
+
+ before do
+ @ldap_conn = mock("Net::LDAP")
+ @ldap_user = mock("Net::LDAP::Entry")
+ Net::LDAP.stub(:new).and_return(@ldap_conn)
+ @ldap_conn.stub(:bind).and_return(true)
+ @ldap_conn.stub(:host=)
+ @ldap_conn.stub(:search).and_return([@ldap_user])
+ @ldap_user.stub(:memberOf).and_return(["cn=random_group,dc=example,dc=com","cn=other_chef_admin_group,dc=example,dc=com"])
+ end
+
+ it "should be a kind of LDAPConnection" do
+ Chef::WebUIUser::LDAPConnection.new.should be_an_instance_of Chef::WebUIUser::LDAPConnection
+ end
+
+ it "can be initialised with a hash to set instance variables" do
+ opt_hsh = {'port'=>1234, 'hosts'=>["mum","dad"], 'encrypt'=>"brzt%!",
+ 'username' => "bind_user", 'password' => "a_password"}
+ ldc = Chef::WebUIUser::LDAPConnection.new(opt_hsh)
+ ldc.instance_variable_get(:@port).should == 1234
+ ldc.instance_variable_get(:@hosts).should == ["mum","dad"]
+ ldc.instance_variable_get(:@method).should == :simple_tls
+ ldc.instance_variable_get(:@auth).should == {:method=>:simple,:username=>"bind_user",:password=>"a_password"}
+ end
+
+ it "can be initialised without encryption with a hash to set instance variables" do
+ opt_hsh = {'port'=>1234, 'hosts'=>["mum","dad"], 'encrypt'=>false }
+ ldc = Chef::WebUIUser::LDAPConnection.new(opt_hsh)
+ ldc.instance_variable_get(:@port).should == 1234
+ ldc.instance_variable_get(:@hosts).should == ["mum","dad"]
+ ldc.instance_variable_get(:@method).should == nil
+ end
+
+ it "should bind given hosts that connect" do
+ Chef::WebUIUser::LDAPConnection.new.bind.should be_true
+ end
+
+ it "should bind to the second host if the first bind fails" do
+ @ldap_conn.stub(:bind).and_return(false,true)
+ @ldap_conn.should_receive(:host=).once.with("fail_ldap_host")
+ @ldap_conn.should_receive(:host=).once.with("work_ldap_host")
+ Chef::WebUIUser::LDAPConnection.new.bind
+ end
+
+ it "should bind to the first host if the first bind succeeds" do
+ @ldap_conn.stub(:bind).and_return(true)
+ @ldap_conn.should_receive(:host=).once.with("fail_ldap_host")
+ Chef::WebUIUser::LDAPConnection.new.bind
+ end
+
+ it "should attempt twice and fail if the first bind fails" do
+ @ldap_conn.stub(:bind).and_return(false)
+ @ldap_conn.should_receive(:host=).once.with("fail_ldap_host")
+ @ldap_conn.should_receive(:host=).once.with("work_ldap_host")
+ Chef::WebUIUser::LDAPConnection.new.bind.should == false
+ end
+
+ it "should bind_as the supplied credentials" do
+ @ldap_conn.should_receive(:bind_as).once.with(:base=>"dc=example,dc=com",:filter=>"(sAMAccountName=Spongebob)",:password=>"Squarepants").and_return([Chef::WebUIUser::LDAPUser.new])
+ Chef::WebUIUser::LDAPConnection.new.bind_as("Spongebob","Squarepants")
+ end
+
+ it "should perform complex binds controlled by lambdas" do
+ Chef::Config.stub(:[]).with(:ldap_base_root).and_return( lambda {|n| "dc=#{n.split("\\").first},dc=example,dc=com"} )
+ Chef::Config.stub(:[]).with(:ldap_user_preprocess).and_return( lambda {|n| "#{n.split("\\").last}"} )
+ Chef::Config.stub(:[]).with(:ldap_user_attribute).and_return( lambda {|n| "(&(sAMAccountName=#{Chef::WebUIUser::LDAPConnection.ldap_escape(n)})(extraCheck=1234))"} )
+ @ldap_conn.should_receive(:bind_as).once.with(:base=>"dc=mydom,dc=example,dc=com",:filter=>"(&(sAMAccountName=Spongebob)(extraCheck=1234))",:password=>"Squarepants").and_return([Chef::WebUIUser::LDAPUser.new])
+ Chef::WebUIUser::LDAPConnection.new.bind_as("mydom\\Spongebob","Squarepants")
+ end
+
+ it "should perform complex binds with lambdas passed as parameters" do
+ @ldap_conn.should_receive(:bind_as).once.with(:base=>"dc=mydom,dc=example,dc=com",:filter=>"(&(sAMAccountName=Spongebob)(extraCheck=1234))",:password=>"Squarepants")
+ Chef::WebUIUser::LDAPConnection.new.bind_as("mydom\\Spongebob","Squarepants",
+ lambda {|n| "dc=#{n.split("\\").first},dc=example,dc=com"},
+ lambda {|n| "(&(sAMAccountName=#{Chef::WebUIUser::LDAPConnection.ldap_escape(n)})(extraCheck=1234))"},
+ lambda {|n| "#{n.split("\\").last}"} )
+ end
+
+ it "should perform searches" do
+ @ldap_conn.should_receive(:search).once.with(:base=>"dc=example,dc=com",:filter=>"(sAMAccountName=Spongebob)")
+ Chef::WebUIUser::LDAPConnection.new.ldap_search_for("Spongebob")
+ end
+
+ it "should perform searches with lambdas passed as parameters" do
+ @ldap_conn.should_receive(:search).once.with(:base=>"dc=mydom,dc=example,dc=com",:filter=>"(&(sAMAccountName=Spongebob)(extraCheck=1234))")
+ Chef::WebUIUser::LDAPConnection.new.ldap_search_for("mydom\\Spongebob",
+ lambda {|n| "dc=#{n.split("\\").first},dc=example,dc=com"},
+ lambda {|n| "(&(sAMAccountName=#{Chef::WebUIUser::LDAPConnection.ldap_escape(n)})(extraCheck=1234))"},
+ lambda {|n| "#{n.split("\\").last}"} )
+ end
+
+ it "should perform searches with lambdas as the default settings" do
+ Chef::Config.stub(:[]).with(:ldap_base_root).and_return( lambda {|n| "dc=#{n.split("\\").first},dc=example,dc=com"} )
+ Chef::Config.stub(:[]).with(:ldap_user_preprocess).and_return( lambda {|n| "#{n.split("\\").last}"} )
+ Chef::Config.stub(:[]).with(:ldap_user_attribute).and_return( lambda {|n| "(&(sAMAccountName=#{Chef::WebUIUser::LDAPConnection.ldap_escape(n)})(extraCheck=1234))"} )
+ @ldap_conn.should_receive(:search).once.with(:base=>"dc=mydom,dc=example,dc=com",:filter=>"(&(sAMAccountName=Spongebob)(extraCheck=1234))")
+ Chef::WebUIUser::LDAPConnection.new.ldap_search_for("mydom\\Spongebob")
+ end
+
+ it "should escape usernames to prevent LDAP paramete injection" do
+ @ldap_conn.should_receive(:search).once.with(:base=>"dc=example,dc=com",:filter=>"(sAMAccountName=Spongebob\\2a\\29\\28cn=\\2a\\29)" )
+ Chef::WebUIUser::LDAPConnection.new.ldap_search_for("Spongebob*)(cn=*)")
+ end
+
+ end
+
+ describe "when setting a password" do
+
+ it "raises an error when an attempt is made to set the password" do
+ lambda {@webui_user.cdb_set_password("valid_pwd", "valid_pwd")}.should raise_error(ArgumentError, /Passwords are controlled by the LDAP provider/)
+ end
+
+ end
+
+ describe "when setting or verifying a password via the new_password values" do
+
+ before do
+ @webui_user.name = "ldap_test_user"
+ @ldap_conn = mock("Net::LDAP")
+ @ldap_user = mock("Net::LDAP::Entry")
+ Net::LDAP.stub(:new).and_return(@ldap_conn)
+ @ldap_conn.stub(:bind).and_return(true)
+ @ldap_conn.stub(:host=)
+ @ldap_conn.stub(:search).and_return([@ldap_user])
+ @ldap_user.stub(:[]).with("memberOf").and_return(["cn=random_group,dc=example,dc=com","cn=other_chef_admin_group,dc=example,dc=com"])
+ @couchdb = mock("Chef::CouchDB")
+ Chef::CouchDB.stub(:new).and_return(@couchdb)
+ @couchdb.stub(:[]).with("rev").and_return(1)
+ @couchdb.stub(:store).and_return(@couchdb)
+ end
+
+ it "won't change the password when none given" do
+ @webui_user.new_password = ""
+ @webui_user.confirm_new_password = ""
+ lambda{@webui_user.cdb_save}.should_not raise_error
+ end
+
+ it "keeps bad password values when a save fails due to password problems" do
+ @webui_user.new_password = "2shrt"
+ @webui_user.confirm_new_password = "2shrt"
+ lambda {@webui_user.cdb_save}
+ @webui_user.new_password.should == "2shrt"
+ end
+
+ it "keeps bad confirm_password values when a save fails due to password problems" do
+ @webui_user.new_password = "validpassword"
+ @webui_user.confirm_new_password = "incorrectpass"
+ lambda {@webui_user.cdb_save}
+ @webui_user.confirm_new_password.should == "incorrectpass"
+ end
+
+ it "should raise an error when a new password is set" do
+ @webui_user.new_password = "validpassword"
+ @webui_user.confirm_new_password = "validpassword"
+ lambda {@webui_user.cdb_save}.should raise_error(ArgumentError, "new_password cannot be set with the LDAPAuthModule")
+ end
+
+ end
+
+ describe "when doing CRUD operations via API" do
+
+ before do
+ @webui_user.name = "test_user"
+ @rest = mock("Chef::REST")
+ Chef::REST.stub!(:new).and_return(@rest)
+ end
+
+ it "finds users by name via GET" do
+ @rest.should_receive(:get_rest).with("users/mud")
+ Chef::WebUIUser.load("mud")
+ end
+
+ it "finds all ids in the database via GET" do
+ @rest.should_receive(:get_rest).with("users")
+ Chef::WebUIUser.list
+ end
+
+ it "finds all documents in the database via GET" do
+ robots = Chef::WebUIUser.new("name"=>"we_robots")
+ happy = Chef::WebUIUser.new("name"=>"are_happy_robots")
+ query_results = [robots,happy]
+ query_obj = mock("Chef::Search::Query")
+ query_obj.should_receive(:search).with(:user).and_yield(query_results.first).and_yield(query_results.last)
+ Chef::Search::Query.stub!(:new).and_return(query_obj)
+ Chef::WebUIUser.list(true).should == {"we_robots" => robots, "are_happy_robots" => happy}
+ end
+
+ it "updates via PUT when saving" do
+ @rest.should_receive(:put_rest).with("users/test_user", @webui_user)
+ @webui_user.save
+ end
+
+ it "falls back to creating via POST if updating returns 404" do
+ response = mock("Net::HTTPResponse", :code => "404")
+ not_found = Net::HTTPServerException.new("404", response)
+ @rest.should_receive(:put_rest).with("users/test_user", @webui_user).and_raise(not_found)
+ @rest.should_receive(:post_rest).with("users", @webui_user)
+ @webui_user.save
+ end
+
+ it "creates via POST" do
+ @rest.should_receive(:post_rest).with("users", @webui_user)
+ @webui_user.create
+ end
+
+ it "deletes itself with DELETE" do
+ @rest.should_receive(:delete_rest).with("users/test_user")
+ @webui_user.destroy
+ end
+
+ end
+
+ describe "when loading a user that isn't in the couch DB but is in the LDAP data stores" do
+
+ before do
+ @ldap_conn = mock("Net::LDAP")
+ @ldap_user = mock("Net::LDAP::Entry")
+ Net::LDAP.stub(:new).and_return(@ldap_conn)
+ @ldap_conn.stub(:bind).and_return(true)
+ @ldap_conn.stub(:host=)
+ @ldap_conn.stub(:search).and_return([@ldap_user])
+ @ldap_user.stub("sAMAccountName").and_return("ldap_test_user")
+ @ldap_user.stub(:[]).with("memberOf").and_return(["cn=random_group,dc=example,dc=com","cn=other_chef_admin_group,dc=example,dc=com"])
+ @couchdb = mock("Chef::CouchDB")
+ Chef::CouchDB.stub(:new).and_return(@couchdb)
+ @couchdb.stub(:[]).with("rev").and_return(1)
+ new_u = Chef::WebUIUser.new("name"=>'ldap_test')
+ @couchdb.stub(:load).with("webui_user","ldap_test_user").and_raise(Chef::Exceptions::CouchDBNotFound)
+ end
+
+ it "should create an admin user when the user is in an Admin group in LDAP" do
+ @couchdb.should_receive(:store) do |a,b,c|
+ a.should == "webui_user"
+ b.should == "ldap_test_user"
+ c.should be_an_instance_of(Chef::WebUIUser)
+ c.admin.should == true
+ c.name.should == "ldap_test_user"
+ {'rev'=>1}
+ end
+ Chef::WebUIUser.cdb_load("ldap_test_user")
+ end
+
+ it "should create a non admin user when the user is in a User group in LDAP" do
+ @ldap_user.stub(:[]).with("memberOf").and_return(["cn=chef_user_group,dc=example,dc=com"])
+ @couchdb.should_receive(:store) do |a,b,c|
+ a.should == "webui_user"
+ b.should == "ldap_test_user"
+ c.should be_an_instance_of(Chef::WebUIUser)
+ c.admin.should == false
+ c.name.should == "ldap_test_user"
+ {'rev'=>1}
+ end
+ Chef::WebUIUser.cdb_load("ldap_test_user")
+ end
+
+ it "should create a non admin user when LDAP returns a single item instead of an array for memberOf" do
+ @ldap_user.stub(:[]).with("memberOf").and_return("cn=chef_user_group,dc=example,dc=com")
+ @couchdb.should_receive(:store) do |a,b,c|
+ a.should == "webui_user"
+ b.should == "ldap_test_user"
+ c.should be_an_instance_of(Chef::WebUIUser)
+ c.admin.should == false
+ c.name.should == "ldap_test_user"
+ {'rev'=>1}
+ end
+ Chef::WebUIUser.cdb_load("ldap_test_user")
+ end
+
+ it "should raise a Not Found error when the LDAP user is not in a chef group" do
+ @ldap_user.stub(:[]).with("memberOf").and_return(["cn=any_random_group,dc=example,dc=com"])
+ lambda{Chef::WebUIUser.cdb_load("ldap_test_user")}.should raise_error(Chef::Exceptions::CouchDBNotFound)
+ end
+
+ it "should raise a Not Found error when the LDAP user has a single memberOf and it is not a Chef group" do
+ @ldap_user.stub(:[]).with("memberOf").and_return("cn=any_random_group,dc=example,dc=com")
+ lambda{Chef::WebUIUser.cdb_load("ldap_test_user")}.should raise_error(Chef::Exceptions::CouchDBNotFound)
+ end
+
+ it "should raise a Not Found error when the LDAP user returns nil for memberOf" do
+ @ldap_user.stub(:[]).with("memberOf").and_return(nil)
+ lambda{Chef::WebUIUser.cdb_load("ldap_test_user")}.should raise_error(Chef::Exceptions::CouchDBNotFound)
+ end
+
+ it "should raise a Not Found error when the LDAP user returns an empty array for memberOf" do
+ @ldap_user.stub(:[]).with("memberOf").and_return([])
+ lambda{Chef::WebUIUser.cdb_load("ldap_test_user")}.should raise_error(Chef::Exceptions::CouchDBNotFound)
+ end
+
+
+
+
+ end
+end
View
179 chef/spec/unit/webui_user_spec.rb
@@ -15,14 +15,36 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
-
+
require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper"))
-describe Chef::WebUIUser do
+describe "Chef::WebUIUser" do
+
+ before(:all) do
+ # Next line is brutal, but we need to be sure that only the authentication we want is loaded!
+ begin
+ Chef.send(:remove_const,:WebUIUser)
+ rescue
+ end
+ # Now we need to force the authentication modules to reload as the previous line removed them from memory
+ # and they won't come back until we load the files again
+ Dir[File.join(File.dirname(__FILE__),'..','..','lib','chef', 'web_ui_user', '*.rb')].sort.each { |lib| load lib }
+ load File.join(File.dirname(__FILE__),'..','..','lib','chef','webui_user.rb')
+ Chef::Config.stub!(:[]).with(:web_ui_authentication_module).and_return(lambda{Chef::WebUIUser::CDBAuthModule})
+ Chef::WebUIUser.select_authentication_module
+ end
before do
@webui_user = Chef::WebUIUser.new
end
+
+ it "should be using couchdb Class methods" do
+ Chef::WebUIUser.auth_module_name.should == 'CDBAuthModuleClassMethods'
+ end
+
+ it "should be using couchdb Instance methods" do
+ Chef::WebUIUser.new.instance_auth_module_name.should == 'CDBAuthModule'
+ end
it "stores the admin status of the user" do
@webui_user.admin.should be_false
@@ -31,15 +53,16 @@
@webui_user.should be_admin
end
- it "stores the name with underscores subbed for dots" do
+ it "stores the name" do
@webui_user.name.should be_nil
@webui_user.name = "foo.bar.baz"
- @webui_user.name.should == "foo_bar_baz"
+ @webui_user.name.should == "foo.bar.baz"
end
it "can be initialized with a hash to set instance variables" do
opt_hsh = {'name'=>'mud', 'salt'=>'just_a_lil', 'password'=>'beefded',
- 'openid'=>'notsomuch', '_rev'=>'0', '_id'=>'tehid'}
+ 'openid'=>'notsomuch', '_rev'=>'0', '_id'=>'tehid',
+ 'new_password'=>'blahblah', 'confirm_new_password'=>'blahblahtoo'}
webui_user = Chef::WebUIUser.new(opt_hsh)
webui_user.name.should == 'mud'
webui_user.salt.should == 'just_a_lil'
@@ -47,38 +70,111 @@
webui_user.openid.should == 'notsomuch'
webui_user.couchdb_rev.should == '0'
webui_user.couchdb_id.should == 'tehid'
+ webui_user.new_password.should == 'blahblah'
+ webui_user.confirm_new_password.should == 'blahblahtoo'
end
-
+
describe "when setting or verifying a password" do
+
it "raises an error when the password doesn't match the confirmation password" do
- lambda {@webui_user.set_password("nomnomnom", "yukyukyuk")}.should raise_error(ArgumentError, /do not match/)
+ lambda {@webui_user.cdb_set_password("nomnomnom", "yukyukyuk")}.should raise_error(ArgumentError, /do not match/)
end
it "doesn't allow blank passwords" do
- lambda {@webui_user.set_password("", "")}.should raise_error(ArgumentError, /cannot be blank/)
+ lambda {@webui_user.cdb_set_password("", "")}.should raise_error(ArgumentError, /cannot be blank/)
end
it "doesn't allow passwords less than 6 characters" do
- lambda {@webui_user.set_password("2shrt", "2shrt")}.should raise_error(ArgumentError, /minimum of 6 characters/)
+ lambda {@webui_user.cdb_set_password("2shrt", "2shrt")}.should raise_error(ArgumentError, /minimum of 6 characters/)
end
it "generates a salt and hashes the password when the password is valid" do
- @webui_user.set_password("valid_pw", "valid_pw")
+ @webui_user.cdb_set_password("valid_pw", "valid_pw")
@webui_user.salt.should_not be_nil
@webui_user.password.should match(/[0-9a-f]{32}/)
end
it "verifies a correct password" do
- @webui_user.set_password("valid_pw", "valid_pw")
- @webui_user.verify_password("valid_pw").should be_true
+ @webui_user.cdb_set_password("valid_pw", "valid_pw")
+ @webui_user.cdb_verify_password("valid_pw").should be_true
end
it "doesn't verify an incorrect password" do
- @webui_user.set_password("valid_pw", "valid_pw")
- @webui_user.verify_password("invalid_pw").should be_false
+ @webui_user.cdb_set_password("valid_pw", "valid_pw")
+ @webui_user.cdb_verify_password("invalid_pw").should be_false
end
end
-
+
+ describe "when setting or verifying a password via the new_password values" do
+
+ before do
+ @webui_user.name = "relaxed_test"
+ @couchdb = mock("Chef::Couchdb")
+ Chef::CouchDB.stub(:new).and_return(@couchdb)
+ @couchdb.stub(:store).and_return(@couchdb)
+ @couchdb.stub(:[]).and_return(1)
+ @webui_user.instance_variable_set(:@couchdb, @couchdb)
+ end
+
+ it "raises an error when the password doesn't match the confirmation password" do
+ @webui_user.new_password = "nomnomnom"
+ @webui_user.confirm_new_password = "yukyukyuk"
+ lambda {@webui_user.cdb_save}.should raise_error(ArgumentError, /do not match/)
+ end
+
+ it "won't change the password when none given" do
+ @webui_user.cdb_set_password("validpassword", "validpassword")
+ @webui_user.cdb_save
+ @webui_user.new_password = ""
+ @webui_user.confirm_new_password = ""
+ lambda{@webui_user.cdb_save}
+ @webui_user.cdb_verify_password("validpassword").should be true
+ end
+
+ it "doesn't allow passwords less than 6 characters" do
+ @webui_user.new_password = "2shrt"
+ @webui_user.confirm_new_password = "2shrt"
+ lambda {@webui_user.cdb_save}.should raise_error(ArgumentError, /minimum of 6 characters/)
+ end
+
+ it "generates a salt and hashes the password when the password is valid" do
+ @webui_user.new_password = "valid_pw"
+ @webui_user.confirm_new_password = "valid_pw"
+ @webui_user.cdb_save
+ @webui_user.salt.should_not be_nil
+ @webui_user.password.should match(/[0-9a-f]{32}/)
+ end
+
+ it "doesn't allow unencrypted passwords to persist a successful save" do
+ @webui_user.new_password = "valid_pw"
+ @webui_user.confirm_new_password = "valid_pw"
+ @webui_user.cdb_save
+ @webui_user.new_password.should_not == "valid_pw"
+ end
+
+ it "doesn't allow unencrypted password confirmations to persist a successful save" do
+ @webui_user.new_password = "valid_pw"
+ @webui_user.confirm_new_password = "valid_pw"
+ @webui_user.cdb_save
+ @webui_user.confirm_new_password.should_not == "valid_pw"
+ end
+
+ it "keeps bad password values when a save fails due to password problems" do
+ @webui_user.new_password = "2shrt"
+ @webui_user.confirm_new_password = "2shrt"
+ lambda {@webui_user.cdb_save}
+ @webui_user.new_password.should == "2shrt"
+ end
+
+ it "keeps bad confirm_password values when a save fails due to password problems" do
+ @webui_user.new_password = "validpassword"
+ @webui_user.confirm_new_password = "incorrectpass"
+ lambda {@webui_user.cdb_save}
+ @webui_user.confirm_new_password.should == "incorrectpass"
+ end
+
+ end
+
describe "when doing CRUD operations via API" do
before do
@webui_user.name = "test_user"
@@ -90,7 +186,7 @@
@rest.should_receive(:get_rest).with("users/mud")
Chef::WebUIUser.load("mud")
end
-
+
it "finds all ids in the database via GET" do
@rest.should_receive(:get_rest).with("users")
Chef::WebUIUser.list
@@ -129,6 +225,55 @@
@webui_user.destroy
end
end
+
+ describe "when doing CRUD operations via API with a complex username" do
+ before do
+ @webui_user.name = "cn=foo,dc=example,dc=com"
+ @rest = mock("Chef::REST")
+ Chef::REST.stub!(:new).and_return(@rest)
+ end
+
+ it "escapes the name before attempting to GET it" do
+ @rest.should_receive(:get_rest).with("users/cn%3Dbar%2Cdc%3Dexample%2Cdc%3Dcom")
+ Chef::WebUIUser.load("cn=bar,dc=example,dc=com")
+ end
+
+ it "updates via PUT to the escaped name when saving" do
+ @rest.should_receive(:put_rest).with("users/cn%3Dfoo%2Cdc%3Dexample%2Cdc%3Dcom",