Commit 3f0799a9 authored by Daniel DeLeo's avatar Daniel DeLeo
Browse files

Validate data bag names/item ids before fetching them

also, log a more helpful error if loading fails
parent 015753e8
......@@ -8,9 +8,9 @@
# 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.
......@@ -28,19 +28,21 @@ require 'extlib'
require 'chef/json_compat'
class Chef
class DataBag
class DataBag
include Chef::Mixin::FromFile
include Chef::Mixin::ParamsValidate
include Chef::IndexQueue::Indexable
VALID_NAME = /^[\-[:alnum:]_]+$/
DESIGN_DOCUMENT = {
"version" => 2,
"language" => "javascript",
"views" => {
"all" => {
"map" => <<-EOJS
function(doc) {
function(doc) {
if (doc.chef_type == "data_bag") {
emit(doc.name, doc);
}
......@@ -49,7 +51,7 @@ class Chef
},
"all_id" => {
"map" => <<-EOJS
function(doc) {
function(doc) {
if (doc.chef_type == "data_bag") {
emit(doc.name, doc.name);
}
......@@ -68,21 +70,27 @@ class Chef
}
}
def self.validate_name!(name)
unless name =~ VALID_NAME
raise Exceptions::InvalidDataBagName, "DataBags must have a name matching #{VALID_NAME.inspect}, you gave #{name.inspect}"
end
end
attr_accessor :couchdb_rev, :couchdb_id, :couchdb
# Create a new Chef::DataBag
def initialize(couchdb=nil)
@name = ''
@name = ''
@couchdb_rev = nil
@couchdb_id = nil
@couchdb = (couchdb || Chef::CouchDB.new)
end
def name(arg=nil)
def name(arg=nil)
set_or_return(
:name,
arg,
:regex => /^[\-[:alnum:]_]+$/
:regex => VALID_NAME
)
end
......@@ -96,7 +104,7 @@ class Chef
result
end
# Serialize this object as a hash
# Serialize this object as a hash
def to_json(*a)
to_hash.to_json(*a)
end
......@@ -108,7 +116,7 @@ class Chef
def self.chef_server_rest
Chef::REST.new(Chef::Config[:chef_server_url])
end
# Create a Chef::Role from JSON
def self.json_create(o)
bag = new
......@@ -118,7 +126,7 @@ class Chef
bag.index_id = bag.couchdb_id
bag
end
# List all the Chef::DataBag objects in the CouchDB. If inflate is set to true, you will get
# the full list of all Roles, fully inflated.
def self.cdb_list(inflate=false, couchdb=nil)
......@@ -126,7 +134,7 @@ class Chef
lookup = (inflate ? "value" : "key")
rs["rows"].collect { |r| r[lookup] }
end
def self.list(inflate=false)
if inflate
# Can't search for all data bags like other objects, fall back to N+1 :(
......@@ -138,17 +146,17 @@ class Chef
Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("data")
end
end
# Load a Data Bag by name from CouchDB
def self.cdb_load(name, couchdb=nil)
(couchdb || Chef::CouchDB.new).load("data_bag", name)
end
# Load a Data Bag by name via the RESTful API
def self.load(name)
Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("data/#{name}")
end
# Remove this Data Bag from CouchDB
def cdb_destroy
removed = @couchdb.delete("data_bag", @name, @couchdb_rev)
......@@ -159,17 +167,17 @@ class Chef
end
removed
end
def destroy
chef_server_rest.delete_rest("data/#{@name}")
end
# Save this Data Bag to the CouchDB
def cdb_save
results = @couchdb.store("data_bag", @name, self)
@couchdb_rev = results["rev"]
end
# Save the Data Bag via RESTful API
def save
begin
......@@ -180,7 +188,7 @@ class Chef
end
self
end
#create a data bag via RESTful API
def create
chef_server_rest.post_rest("data", self)
......@@ -190,7 +198,7 @@ class Chef
# List all the items in this Bag from CouchDB
# The self.load method does this through the REST API
def list(inflate=false)
rs = nil
rs = nil
if inflate
rs = @couchdb.get_view("data_bags", "entries", :include_docs => true, :startkey => @name, :endkey => @name)
rs["rows"].collect { |r| r["doc"] }
......@@ -199,12 +207,12 @@ class Chef
rs["rows"].collect { |r| r["value"] }
end
end
# Set up our CouchDB design document
def self.create_design_document(couchdb=nil)
(couchdb || Chef::CouchDB.new).create_design_document("data_bags", DESIGN_DOCUMENT)
end
# As a string
def to_s
"data_bag[#{@name}]"
......
......@@ -8,9 +8,9 @@
# 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.
......@@ -33,18 +33,20 @@ class Chef
class DataBagItem
extend Forwardable
include Chef::Mixin::FromFile
include Chef::Mixin::ParamsValidate
include Chef::IndexQueue::Indexable
VALID_ID = /^[\-[:alnum:]_]+$/
DESIGN_DOCUMENT = {
"version" => 1,
"language" => "javascript",
"views" => {
"all" => {
"map" => <<-EOJS
function(doc) {
function(doc) {
if (doc.chef_type == "data_bag_item") {
emit(doc.name, doc);
}
......@@ -53,7 +55,7 @@ class Chef
},
"all_id" => {
"map" => <<-EOJS
function(doc) {
function(doc) {
if (doc.chef_type == "data_bag_item") {
emit(doc.name, doc.name);
}
......@@ -63,12 +65,18 @@ class Chef
}
}
def self.validate_id!(id_str)
if id_str.nil? || ( id_str !~ VALID_ID )
raise Exceptions::InvalidDataBagItemID, "Data Bag items must have an id matching #{VALID_ID.inspect}, you gave: #{id_str.inspect}"
end
end
# Define all Hash's instance methods as delegating to @raw_data
def_delegators(:@raw_data, *(Hash.instance_methods - Object.instance_methods))
attr_accessor :couchdb_rev, :couchdb_id, :couchdb
attr_reader :raw_data
# Create a new Chef::DataBagItem
def initialize(couchdb=nil)
@couchdb_rev = nil
......@@ -79,25 +87,30 @@ class Chef
end
def chef_server_rest
Chef::REST.new(Chef::Config[:chef_server_url])
Chef::REST.new(Chef::Config[:chef_server_url])
end
def self.chef_server_rest
Chef::REST.new(Chef::Config[:chef_server_url])
Chef::REST.new(Chef::Config[:chef_server_url])
end
def raw_data
@raw_data
end
def validate_id!(id_str)
self.class.validate_id!(id_str)
end
def raw_data=(new_data)
raise Exceptions::ValidationFailed, "Data Bag Items must contain a Hash or Mash!" unless new_data.kind_of?(Hash) || new_data.kind_of?(Mash)
raise Exceptions::ValidationFailed, "Data Bag Items must have an id key in the hash! #{new_data.inspect}" unless new_data.has_key?("id")
raise Exceptions::ValidationFailed, "Data Bag Item id does not match alphanumeric/-/_!" unless new_data["id"] =~ /^[\-[:alnum:]_]+$/
unless new_data.respond_to?(:[]) && new_data.respond_to?(:keys)
raise Exceptions::ValidationFailed, "Data Bag Items must contain a Hash or Mash!"
end
validate_id!(new_data["id"])
@raw_data = new_data
end
def data_bag(arg=nil)
def data_bag(arg=nil)
set_or_return(
:data_bag,
arg,
......@@ -112,7 +125,7 @@ class Chef
def object_name
raise Exceptions::ValidationFailed, "You must have an 'id' or :id key in the raw data" unless raw_data.has_key?('id')
raise Exceptions::ValidationFailed, "You must have declared what bag this item belongs to!" unless data_bag
id = raw_data['id']
"data_bag_item_#{data_bag}_#{id}"
end
......@@ -129,7 +142,7 @@ class Chef
result
end
# Serialize this object as a hash
# Serialize this object as a hash
def to_json(*a)
result = {
"name" => self.object_name,
......@@ -157,7 +170,7 @@ class Chef
o.delete("json_class")
o.delete("name")
if o.has_key?("_rev")
bag_item.couchdb_rev = o["_rev"]
bag_item.couchdb_rev = o["_rev"]
o.delete("_rev")
end
if o.has_key?("_id")
......@@ -173,7 +186,7 @@ class Chef
def self.cdb_load(data_bag, name, couchdb=nil)
(couchdb || Chef::CouchDB.new).load("data_bag_item", object_name(data_bag, name))
end
# Load a Data Bag Item by name via RESTful API
def self.load(data_bag, name)
item = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("data/#{data_bag}/#{name}")
......@@ -185,22 +198,22 @@ class Chef
item
end
end
# Remove this Data Bag Item from CouchDB
def cdb_destroy
Chef::Log.debug "destroying data bag item: #{self.inspect}"
@couchdb.delete("data_bag_item", object_name, @couchdb_rev)
end
def destroy(data_bag=data_bag, databag_item=name)
chef_server_rest.delete_rest("data/#{data_bag}/#{databag_item}")
end
# Save this Data Bag Item to CouchDB
def cdb_save
@couchdb_rev = @couchdb.store("data_bag_item", object_name, self)["rev"]
end
# Save this Data Bag Item via RESTful API
def save(item_id=@raw_data['id'])
r = chef_server_rest
......@@ -208,17 +221,17 @@ class Chef
r.put_rest("data/#{data_bag}/#{item_id}", @raw_data)
rescue Net::HTTPServerException => e
raise e unless e.response.code == "404"
r.post_rest("data/#{data_bag}", @raw_data)
r.post_rest("data/#{data_bag}", @raw_data)
end
self
end
# Create this Data Bag Item via RESTful API
def create
chef_server_rest.post_rest("data/#{data_bag}", @raw_data)
chef_server_rest.post_rest("data/#{data_bag}", @raw_data)
self
end
end
# Set up our CouchDB design document
def self.create_design_document(couchdb=nil)
(couchdb || Chef::CouchDB.new).create_design_document("data_bag_items", DESIGN_DOCUMENT)
......
......@@ -6,9 +6,9 @@
# 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.
......@@ -25,11 +25,11 @@ class Chef
class Env < RuntimeError; end
class Exec < RuntimeError; end
class ErlCall < RuntimeError; end
class FileNotFound < RuntimeError; end
class FileNotFound < RuntimeError; end
class Package < RuntimeError; end
class Service < RuntimeError; end
class Route < RuntimeError; end
class SearchIndex < RuntimeError; end
class SearchIndex < RuntimeError; end
class Override < RuntimeError; end
class UnsupportedAction < RuntimeError; end
class MissingLibrary < RuntimeError; end
......@@ -65,5 +65,8 @@ class Chef
class IllegalChecksumRevert < RuntimeError; end
class MissingParentDirectory < RuntimeError; end
class UnresolvableGitReference < RuntimeError; end
class InvalidEnvironmentRunListSpecification < ArgumentError; end
class InvalidDataBagItemID < ArgumentError; end
class InvalidDataBagName < ArgumentError; end
end
end
......@@ -6,9 +6,9 @@
# 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.
......@@ -119,36 +119,45 @@ class Chef
# false:: If the current platform is not in the list
def platform?(*args)
has_platform = false
args.flatten.each do |platform|
has_platform = true if platform == node[:platform]
end
has_platform
end
def search(*args, &block)
# If you pass a block, or have at least the start argument, do raw result parsing
#
#
# Otherwise, do the iteration for the end user
if Kernel.block_given? || args.length >= 4
if Kernel.block_given? || args.length >= 4
Chef::Search::Query.new.search(*args, &block)
else
else
results = Array.new
Chef::Search::Query.new.search(*args) do |o|
results << o
results << o
end
results
end
end
def data_bag(bag)
rbag = Chef::DataBag.load(bag)
DataBag.validate_name!(bag)
rbag = DataBag.load(bag)
rbag.keys
rescue Exception
Log.error("Failed to list data bag items in data bag: #{bag.inspect}")
raise
end
def data_bag_item(bag, item)
Chef::DataBagItem.load(bag, item)
DataBag.validate_name!(bag)
DataBagItem.validate_id!(item)
DataBagItem.load(bag, item)
rescue Exception
Log.error("Failed to load data bag item: #{bag.inspect} #{item.inspect}")
raise
end
end
......
......@@ -91,6 +91,38 @@ describe Chef::Mixin::Language do
end
describe "when loading data bags and items" do
it "lists the items in a data bag" do
Chef::DataBag.should_receive(:load).with("bag_name").and_return("item_1" => "http://url_for/item_1", "item_2" => "http://url_for/item_2")
@language.data_bag("bag_name").should == %w[item_1 item_2]
end
it "validates the name of the data bag you're trying to load" do
lambda {@language.data_bag("!# %^&& ")}.should raise_error(Chef::Exceptions::InvalidDataBagName)
end
it "fetches a data bag item" do
@item = Chef::DataBagItem.new
@item.data_bag("bag_name")
@item.raw_data = {"id" => "item_name", "FUU" => "FUU"}
Chef::DataBagItem.should_receive(:load).with("bag_name", "item_name").and_return(@item)
@language.data_bag_item("bag_name", "item_name").should == @item
end
it "validates the name of the data bag you're trying to load an item from" do
lambda {@language.data_bag_item(" %%^& ", "item_name")}.should raise_error(Chef::Exceptions::InvalidDataBagName)
end
it "validates the id of the data bag item you're trying to load" do
lambda {@language.data_bag_item("bag_name", " 987 (*&()")}.should raise_error(Chef::Exceptions::InvalidDataBagItemID)
end
it "validates that the id of the data bag item is not nil" do
lambda {@language.data_bag_item("bag_name", nil)}.should raise_error(Chef::Exceptions::InvalidDataBagItemID)
end
end
end
describe Chef::Mixin::Language::PlatformDependentValue do
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment