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  */
Filter(boost::optional<const std::string&> jsonFilter, const Manifest* manifest)47 Collections::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 
addCollection(const char* collection, const Manifest& manifest)154 void 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 
addCollection(cJSON* object, const Manifest& manifest)176 void 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 
dump() const210 void Collections::Filter::dump() const {
211     std::cerr << *this << std::endl;
212 }
213 
operator <<(std::ostream& os, const Collections::Filter& filter)214 std::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