1/* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */ 2/* 3 * Copyright 2017 Couchbase, Inc 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18#include "collections/filter.h" 19#include "collections/collections_dockey.h" 20#include "collections/collections_types.h" 21 22#include <JSON_checker.h> 23#include <cJSON.h> 24#include <cJSON_utils.h> 25#include <memcached/engine_error.h> 26#include <platform/make_unique.h> 27 28#include <iostream> 29 30/** 31 * Construct a Collections::Filter, optionally using a JSON document with the 32 * following format specifying which collections are allowed DcpStreams created 33 * from a DcpProducer who owns this. 34 * 35 * Format: 36 * {"collection":["collection1", "collection2"]} 37 * 38 * Each collection in the "collection" array must be found in the Manifest 39 * 40 * @param jsonFilter an optional std::string. If this is not initialised than 41 * a legacy (non-collection) DcpProducer is being opened. 42 * @param manifest the Manifest (bucket manifest) to check the filter is valid 43 * which can be null in the case when the bucket has not been told the 44 * manifest. 45 * @throws invalid_argument for input errors (with detailed message) 46 */ 47Collections::Filter::Filter(boost::optional<const std::string&> jsonFilter, 48 const Manifest* manifest) 49 : defaultAllowed(false), passthrough(false), systemEventsAllowed(true) { 50 // If the jsonFilter is not initialised we are building a filter for a 51 // legacy DCP stream, one which could only ever support $default 52 if (!jsonFilter.is_initialized()) { 53 // 1. If there's no manifest, we'll allow the construction, $default may 54 // or may not exist, streamRequest will re-check. 55 // 2. If manifest is specified then we can check for $default. 56 if (!manifest || manifest->doesDefaultCollectionExist()) { 57 defaultAllowed = true; 58 59 // This filter is for a 'legacy' producer which will not know about 60 // system events. 61 systemEventsAllowed = false; 62 return; 63 } else { 64 throw cb::engine_error(cb::engine_errc::unknown_collection, 65 "Filter::Filter no $default"); 66 } 67 } 68 69 auto json = jsonFilter.get(); 70 71 // The filter is the empty string. Create this Filter to be: 72 // 1. passthrough - all collections allowed 73 // 2. defaultAllowed - all $default items allowed 74 // 3. systemEventsAllowed - the client will be informed of system events 75 // this is already set by the initializer list. 76 if (json.empty()) { 77 passthrough = true; 78 defaultAllowed = true; 79 return; 80 } 81 82 if (!checkUTF8JSON(reinterpret_cast<const unsigned char*>(json.data()), 83 json.size())) { 84 throw cb::engine_error(cb::engine_errc::invalid_arguments, 85 "Filter::Filter input not valid jsonFilter:" + 86 jsonFilter.get()); 87 } 88 89 unique_cJSON_ptr cjson(cJSON_Parse(json.c_str())); 90 if (!cjson) { 91 throw cb::engine_error(cb::engine_errc::invalid_arguments, 92 "Filter::Filter cJSON cannot parse jsonFilter:" + 93 jsonFilter.get()); 94 } 95 96 // Now before processing the JSON we must have a manifest. We cannot create 97 // a filtered producer without a manifest. 98 if (!manifest) { 99 throw cb::engine_error(cb::engine_errc::no_collections_manifest, 100 "Filter::Filter no manifest"); 101 } 102 103 // @todo null check manifest to go past this point. Will be done along with 104 // an appropriate error-code 105 106 auto jsonCollections = cJSON_GetObjectItem(cjson.get(), "collections"); 107 bool nameFound = false, uidFound = false; 108 if (!jsonCollections || jsonCollections->type != cJSON_Array) { 109 throw cb::engine_error( 110 cb::engine_errc::invalid_arguments, 111 "Filter::Filter cannot find collections:" + 112 (!jsonCollections 113 ? "nullptr" 114 : std::to_string(jsonCollections->type)) + 115 ", jsonFilter:" + jsonFilter.get()); 116 } else { 117 for (int ii = 0; ii < cJSON_GetArraySize(jsonCollections); ii++) { 118 auto collection = cJSON_GetArrayItem(jsonCollections, ii); 119 if (!(collection && (collection->type == cJSON_String || 120 collection->type == cJSON_Object))) { 121 throw cb::engine_error( 122 cb::engine_errc::invalid_arguments, 123 "Filter::Filter cannot find " 124 "valid collection for index:" + 125 std::to_string(ii) + ", collection:" + 126 (!collection 127 ? "nullptr" 128 : std::to_string(collection->type)) + 129 ", jsonFilter:" + jsonFilter.get()); 130 } else { 131 if (collection->type == cJSON_String) { 132 // Can throw.. 133 addCollection(collection->valuestring, *manifest); 134 nameFound = true; 135 } else { 136 addCollection(collection, *manifest); 137 uidFound = true; 138 } 139 } 140 } 141 } 142 143 // Validate that the input hasn't mixed and matched name/uid vs name 144 // require one or the other. 145 if (uidFound && nameFound) { 146 throw cb::engine_error( 147 cb::engine_errc::invalid_arguments, 148 "Filter::Filter mixed name/uid not allowed jsonFilter:" + 149 jsonFilter.get()); 150 } 151 type = uidFound ? Type::NameUid : Type::Name; 152} 153 154void Collections::Filter::addCollection(const char* collection, 155 const Manifest& manifest) { 156 // Is this the default collection? 157 if (DefaultCollectionIdentifier == collection) { 158 if (manifest.doesDefaultCollectionExist()) { 159 defaultAllowed = true; 160 } else { 161 throw cb::engine_error(cb::engine_errc::unknown_collection, 162 "Filter::addCollection no $default"); 163 } 164 } else { 165 auto itr = manifest.find(collection); 166 if (itr != manifest.end()) { 167 filter.push_back({collection, {}}); 168 } else { 169 throw cb::engine_error(cb::engine_errc::unknown_collection, 170 "Filter::addCollection unknown collection:" + 171 std::string(collection)); 172 } 173 } 174} 175 176void Collections::Filter::addCollection(cJSON* object, 177 const Manifest& manifest) { 178 auto jsonName = cJSON_GetObjectItem(object, "name"); 179 auto jsonUID = cJSON_GetObjectItem(object, "uid"); 180 181 if (!jsonName || jsonName->type != cJSON_String) { 182 throw cb::engine_error( 183 cb::engine_errc::invalid_arguments, 184 "Filter::Filter invalid collection name:" + 185 (!jsonName ? "nullptr" 186 : std::to_string(jsonName->type))); 187 } 188 189 if (!jsonUID || jsonUID->type != cJSON_String) { 190 throw cb::engine_error( 191 cb::engine_errc::invalid_arguments, 192 "Filter::Filter invalid collection uid:" + 193 (!jsonUID ? "nullptr" : std::to_string(jsonUID->type))); 194 } 195 196 auto entry = manifest.find( 197 {{jsonName->valuestring}, makeUid(jsonUID->valuestring)}); 198 199 if (entry == manifest.end()) { 200 throw cb::engine_error( 201 cb::engine_errc::unknown_collection, 202 "Filter::Filter: cannot add unknown collection:" + 203 std::string(jsonName->valuestring) + ":" + 204 std::string(jsonUID->valuestring)); 205 } else { 206 filter.push_back({cb::to_string(entry->getName()), {entry->getUid()}}); 207 } 208} 209 210void Collections::Filter::dump() const { 211 std::cerr << *this << std::endl; 212} 213 214std::ostream& Collections::operator<<(std::ostream& os, 215 const Collections::Filter& filter) { 216 os << "Collections::Filter" 217 << ": passthrough:" << filter.passthrough 218 << ", defaultAllowed:" << filter.defaultAllowed 219 << ", systemEventsAllowed:" << filter.systemEventsAllowed; 220 switch (filter.getType()) { 221 case Collections::Filter::Type::Name: 222 os << ", type:name"; 223 break; 224 case Collections::Filter::Type::NameUid: 225 os << ", type:name-uid"; 226 break; 227 } 228 os << ", filter.size:" << filter.filter.size() << std::endl; 229 for (const auto& entry : filter.filter) { 230 os << entry.first; 231 if (entry.second.is_initialized()) { 232 os << ":" << entry.second.get(); 233 } 234 os << std::endl; 235 } 236 return os; 237} 238