Browse files

Fixes bug 945894 - Exposed SuperSearch in the public API. r=peterbe

  • Loading branch information...
1 parent 9beda86 commit 6493522ff9912cc00b09bec77df4725dc085c5e9 @adngdb adngdb committed Mar 20, 2014
View
100 webapp-django/crashstats/api/tests/test_views.py
@@ -1841,3 +1841,103 @@ def mocked_get(url, params, **options):
for __ in range(10):
response = self.client.get(url)
eq_(response.status_code, 200)
+
+ @mock.patch('requests.get')
+ def test_SuperSearch(self, rget):
+
+ def mocked_get(url, params, **options):
+ if '/supersearch' in url:
+ ok_('exploitability' not in params)
+ return Response({
+ 'hits': [
+ {
+ 'signature': 'abcdef',
+ 'product': 'WaterWolf',
+ 'version': '1.0',
+ 'email': 'thebig@lebowski.net',
+ 'exploitability': 'high',
+ 'url': 'http://embarassing.website.com',
+ 'user_comments': 'hey I am thebig@lebowski.net',
+ }
+ ],
+ 'facets': {
+ 'signature': []
+ },
+ 'total': 0
+ })
+
+ raise NotImplementedError(url)
+
+ rget.side_effect = mocked_get
+
+ url = reverse('api:model_wrapper', args=('SuperSearch',))
+ response = self.client.get(url)
+ eq_(response.status_code, 200)
+ res = json.loads(response.content)
+
+ ok_(res['hits'])
+ ok_(res['facets'])
+
+ # Verify forbidden fields are not exposed.
+ ok_('email' not in res['hits'])
+ ok_('exploitability' not in res['hits'])
+ ok_('url' not in res['hits'])
+
+ # Verify user comments are scrubbed.
+ ok_('thebig@lebowski.net' not in res['hits'][0]['user_comments'])
+
+ # Verify it's not possible to use restricted parameters.
+ response = self.client.get(url, {'exploitability': 'high'})
+ eq_(response.status_code, 200)
+
+ @mock.patch('requests.get')
+ def test_SuperSearchUnredacted(self, rget):
+
+ def mocked_get(url, params, **options):
+ if '/supersearch' in url:
+ ok_('exploitability' in params)
+ return Response({
+ 'hits': [
+ {
+ 'signature': 'abcdef',
+ 'product': 'WaterWolf',
+ 'version': '1.0',
+ 'email': 'thebig@lebowski.net',
+ 'exploitability': 'high',
+ 'url': 'http://embarassing.website.com',
+ 'user_comments': 'hey I am thebig@lebowski.net',
+ }
+ ],
+ 'facets': {
+ 'signature': []
+ },
+ 'total': 0
+ })
+
+ raise NotImplementedError(url)
+
+ rget.side_effect = mocked_get
+
+ url = reverse('api:model_wrapper', args=('SuperSearchUnredacted',))
+ response = self.client.get(url, {'exploitability': 'high'})
+ eq_(response.status_code, 403)
+
+ # Log in to get permissions.
+ user = self._login()
+ self._add_permission(user, 'view_pii')
+ self._add_permission(user, 'view_exploitability')
+
+ response = self.client.get(url, {'exploitability': 'high'})
+ eq_(response.status_code, 200)
+ res = json.loads(response.content)
+
+ ok_(res['hits'])
+ ok_(res['facets'])
+
+ # Verify forbidden fields are exposed.
+ ok_('email' in res['hits'][0])
+ ok_('exploitability' in res['hits'][0])
+ ok_('url' in res['hits'][0])
+
+ # Verify user comments are not scrubbed.
+ ok_('thebig@lebowski.net' in res['hits'][0]['user_comments'])
View
28 webapp-django/crashstats/api/views.py
@@ -13,12 +13,20 @@
from ratelimit.decorators import ratelimit
from waffle.decorators import waffle_switch
+import crashstats.supersearch.models
from crashstats.crashstats import models
from crashstats.crashstats import utils
from crashstats.tokens.models import Token
from .cleaner import Cleaner
+# List of all modules that contain models we want to expose.
+MODELS_MODULES = (
+ models,
+ crashstats.supersearch.models,
+)
+
+
# See http://www.iana.org/assignments/http-status-codes
REASON_PHRASES = {
100: 'CONTINUE',
@@ -163,9 +171,14 @@ def has_permissions(user, permissions):
def model_wrapper(request, model_name):
if model_name in BLACKLIST:
raise http.Http404("Don't know what you're talking about!")
- try:
- model = getattr(models, model_name)
- except AttributeError:
+
+ for source in MODELS_MODULES:
+ try:
+ model = getattr(source, model_name)
+ break
+ except AttributeError:
+ pass
+ else:
raise http.Http404('no model called `%s`' % model_name)
required_permissions = getattr(model, 'API_REQUIRED_PERMISSIONS', None)
@@ -274,8 +287,11 @@ def documentation(request):
endpoints = [
]
- for name in dir(models):
- model = getattr(models, name)
+ all_models = []
+ for source in MODELS_MODULES:
+ all_models += [getattr(source, x) for x in dir(source)]
+
+ for model in all_models:
try:
if not issubclass(model, models.SocorroMiddleware):
continue
@@ -315,7 +331,7 @@ def _describe_model(model):
methods = []
if model.get:
methods.append('GET')
- elif models.post:
+ elif model.post:
methods.append('POST')
docstring = model.__doc__
View
133 webapp-django/crashstats/supersearch/models.py
@@ -1,14 +1,147 @@
import json
+from crashstats import scrubber
from crashstats.crashstats import models
from . import forms
+SUPERSEARCH_HITS_WHITELIST = (
+ 'additional_minidumps',
+ 'addons',
+ 'addons_checked',
+ 'address',
+ 'app_notes',
+ 'build_id',
+ 'client_crash_date',
+ 'completeddatetime',
+ 'cpu_info',
+ 'cpu_name',
+ 'crashedThread',
+ 'crash_time',
+ 'date',
+ 'distributor',
+ 'distributor_version',
+ 'flash_version',
+ 'hangid',
+ 'hang_type',
+ 'id',
+ 'install_age',
+ 'java_stack_trace',
+ 'last_crash',
+ 'platform',
+ 'platform_version',
+ 'plugin_filename',
+ 'plugin_name',
+ 'plugin_version',
+ 'processor_notes',
+ 'process_type',
+ 'product',
+ 'productid',
+ 'reason',
+ 'release_channel',
+ 'ReleaseChannel',
+ 'signature',
+ 'startedDateTime',
+ 'success',
+ 'topmost_filenames',
+ 'truncated',
+ 'uptime',
+ 'user_comments',
+ 'uuid',
+ 'version',
+ 'winsock_lsp',
+ 'accessibility',
+ 'adapter_device_id',
+ 'adapter_vendor_id',
+ 'android_board',
+ 'android_brand',
+ 'android_cpu_abi',
+ 'android_cpu_abi2',
+ 'android_device',
+ 'android_display',
+ 'android_fingerprint',
+ 'android_hardware',
+ 'android_manufacturer',
+ 'android_model',
+ 'android_version',
+ 'async_shutdown_timeout',
+ 'available_page_file',
+ 'available_physical_memory',
+ 'available_virtual_memory',
+ 'b2g_os_version',
+ 'bios_manufacturer',
+ 'cpu_usage_flash_process1',
+ 'cpu_usage_flash_process2',
+ 'dom_ipc_enabled',
+ 'em_check_compatibility',
+ 'frame_poison_base',
+ 'frame_poison_size',
+ 'is_garbage_collecting',
+ 'min_arm_version',
+ 'number_of_processors',
+ 'oom_allocation_size',
+ 'plugin_cpu_usage',
+ 'plugin_hang',
+ 'plugin_hang_ui_duration',
+ 'startup_time',
+ 'system_memory_use_percentage',
+ 'theme',
+ 'throttleable',
+ 'total_virtual_memory',
+ 'vendor',
+ 'additional_minidumps',
+ 'throttle_rate',
+ 'useragent_locale',
+ # deliberately not including:
+ # email
+ # url
+ # exploitability
+)
+
+
class SuperSearch(models.SocorroMiddleware):
URL_PREFIX = '/supersearch/'
+ API_WHITELIST = {
+ 'hits': SUPERSEARCH_HITS_WHITELIST
+ }
+
+ API_CLEAN_SCRUB = (
+ ('user_comments', scrubber.EMAIL),
+ ('user_comments', scrubber.URL),
+ )
+
+ # Generate the list of possible parameters from the associated form.
+ # This way we only manage one list of parameters.
+ possible_params = tuple(
+ x for x in forms.SearchForm([], [], [], False, False).fields
+ ) + (
+ '_facets',
+ '_results_offset',
+ '_results_number',
+ '_return_query',
+ )
+
+
+class SuperSearchUnredacted(SuperSearch):
+
+ API_WHITELIST = {
+ 'hits': SUPERSEARCH_HITS_WHITELIST + (
+ 'email',
+ 'exploitability',
+ 'url',
+ )
+ }
+
+ API_CLEAN_SCRUB = None
+
+ API_REQUIRED_PERMISSIONS = (
+ 'crashstats.view_exploitability',
+ 'crashstats.view_pii'
+ )
+
# Generate the list of possible parameters from the associated form.
# This way we only manage one list of parameters.
possible_params = tuple(
View
8 webapp-django/crashstats/supersearch/static/supersearch/css/search.less
@@ -90,6 +90,14 @@
display: inline;
width: 80%;
}
+
+ div.public-api-url {
+ margin: 10px 0;
+ }
+ input[name=_public_api_url] {
+ padding: 5px 8px;
+ width: 100%;
+ }
}
textarea {
View
21 webapp-django/crashstats/supersearch/static/supersearch/js/socorro/search.js
@@ -6,6 +6,7 @@ $(function () {
var fieldsURL = form.data('fields-url');
var resultsURL = form.data('results-url');
var customURL = form.data('custom-url');
+ var publicApiUrl = form.data('public-api-url');
var submitButton = $('button[type=submit]', form);
var newLineBtn = $('.new-line');
@@ -15,6 +16,7 @@ $(function () {
var optionsElt = $('fieldset.options', form);
var facetsInput = $('input[name=_facets]', form);
var columnsInput = $('input[name=_columns_fake]', form);
+ var publicApiUrlInput = $('input[name=_public_api_url]', form);
// From http://stackoverflow.com/questions/5914020/
function padStr(i) {
@@ -121,6 +123,14 @@ $(function () {
if ('page' in params) {
delete params.page;
}
+
+ // Remove all private parameters (beginning with a _).
+ for (var p in params) {
+ if (p.charAt(0) === '_') {
+ delete params[p];
+ }
+ }
+
return params;
}
@@ -148,9 +158,17 @@ $(function () {
return '?' + queryString;
}
+ function updatePublicApiUrl(params) {
+ // Update the public API URL.
+ var queryString = $.param(params, true);
+ publicApiUrlInput.val(BASE_URL + publicApiUrl + '?' + queryString);
+ }
+
submitButton.click(function (e) {
e.preventDefault();
var params = form.dynamicForm('getParams');
+ updatePublicApiUrl(params);
+
var url = prepareResultsQueryString(params);
window.history.pushState(params, 'Search results', url);
@@ -173,6 +191,7 @@ $(function () {
queryString = $.param(params, true);
showResults(resultsURL + '?' + queryString);
params = getFilteredParams(params);
+ updatePublicApiUrl(params);
form.dynamicForm('setParams', params);
}
else {
@@ -246,6 +265,8 @@ $(function () {
// from it and show the results. This will avoid strange behaviors
// that can be caused by manually set parameters, for example.
var params = form.dynamicForm('getParams');
+ updatePublicApiUrl(params);
+
params.page = page;
var url = prepareResultsQueryString(params);
showResults(resultsURL + url);
View
6 webapp-django/crashstats/supersearch/templates/supersearch/search.html
@@ -19,6 +19,7 @@
data-fields-url="{{ url('supersearch.search_fields') }}"
data-results-url="{{ url('supersearch.search_results') }}"
data-custom-url="{{ url('supersearch.search_custom') }}"
+ data-public-api-url="{{ url('api:model_wrapper', 'SuperSearch') }}"
>
<button type="submit" id="search-button">Search</button>
<button class="new-line">new line</button>
@@ -39,6 +40,10 @@
Show columns:
<input type="text" name="_columns_fake" value="{{ columns | join(', ') }}">
<input type="hidden" name="_columns" value="{{ columns | join(', ') }}">
+ <div class="public-api-url">
+ Public API URL:
+ <input type="text" name="_public_api_url">
+ </div>
</div>
</fieldset>
</form>
@@ -77,6 +82,7 @@
<script>
var COLUMNS = {{ possible_columns | json_dumps }};
var FACETS = {{ possible_facets | json_dumps }};
+var BASE_URL = location.protocol + '//' + location.host;
</script>
{% compress js %}
View
6 webapp-django/crashstats/supersearch/views.py
@@ -19,7 +19,7 @@
from crashstats.crashstats.views import pass_default_context
from . import forms
from .form_fields import split_on_operator
-from .models import SuperSearch, Query
+from .models import SuperSearchUnredacted, Query
ALL_POSSIBLE_FIELDS = (
@@ -258,7 +258,7 @@ def search_results(request):
current_query.urlencode()
)
- api = SuperSearch()
+ api = SuperSearchUnredacted()
try:
search_results = api.get(**params)
except models.BadStatusCodeError, e:
@@ -403,7 +403,7 @@ def search_custom(request, default_context=None):
else:
# Get the JSON query that supersearch generates and show it.
params['_return_query'] = 'true'
- api = SuperSearch()
+ api = SuperSearchUnredacted()
try:
query = api.get(**params)
except models.BadStatusCodeError, e:

0 comments on commit 6493522

Please sign in to comment.