diff --git a/api/permissions.py b/api/permissions.py deleted file mode 100644 index 4de11b4f9..000000000 --- a/api/permissions.py +++ /dev/null @@ -1,29 +0,0 @@ -from rest_framework import permissions -from django.conf import settings - - -class WhiteListPermission(permissions.BasePermission): - """ - This class is used in our Django Rest Framework to check - that the incoming request is from a whitelisted IP. - - In practice, it is used to only allow requests to our backend API - to come directly from an api.data.gov proxy. - """ - - def has_permission(self, request, view): - if not settings.REST_FRAMEWORK['WHITELIST']: - # if no WHITELIST, then permission is allowed - return True - - forwarded = request.META.get('HTTP_X_FORWARDED_FOR') - if forwarded: - ip_addresses = [f.strip() for f in forwarded.split(',')] - else: - ip_addresses = [request.META['REMOTE_ADDR']] - - for ip in ip_addresses: - if ip in settings.REST_FRAMEWORK['WHITELIST']: - return True - - return False diff --git a/api/tests/test_permissions.py b/api/tests/test_permissions.py deleted file mode 100644 index acf0b71b1..000000000 --- a/api/tests/test_permissions.py +++ /dev/null @@ -1,64 +0,0 @@ -from unittest import mock -from django.test import SimpleTestCase, override_settings - -from ..permissions import WhiteListPermission - - -@override_settings(REST_FRAMEWORK={'WHITELIST': ['1.1.2.2']}) -class WhiteListPermissionTests(SimpleTestCase): - @override_settings(REST_FRAMEWORK={'WHITELIST': None}) - def test_it_returns_true_when_no_whitelist_setting(self): - w = WhiteListPermission() - self.assertTrue(w.has_permission(None, None)) - - def test_it_returns_true_when_forwarded_for_ip_is_whitelisted(self): - w = WhiteListPermission() - req = mock.MagicMock( - META={'HTTP_X_FORWARDED_FOR': '5.5.5.5, 1.1.2.2, 2.2.3.3'} - ) - self.assertTrue(w.has_permission(req, None)) - req = mock.MagicMock( - META={'HTTP_X_FORWARDED_FOR': ' 1.1.2.2 '} - ) - self.assertTrue(w.has_permission(req, None)) - - def test_it_returns_false_when_forwarded_for_ip_is_not_whitelisted(self): - w = WhiteListPermission() - req = mock.MagicMock( - META={'HTTP_X_FORWARDED_FOR': '5.5.5.5, 2.2.3.3'} - ) - self.assertFalse(w.has_permission(req, None)) - - def test_it_returns_true_when_remote_addr_is_whitelisted(self): - w = WhiteListPermission() - req = mock.MagicMock( - META={'REMOTE_ADDR': '1.1.2.2'} - ) - self.assertTrue(w.has_permission(req, None)) - - def test_it_returns_false_when_remote_addr_is_not_whitelisted(self): - w = WhiteListPermission() - req = mock.MagicMock( - META={'REMOTE_ADDR': '5.6.7.8'} - ) - self.assertFalse(w.has_permission(req, None)) - - def test_it_returns_false_when_neither_header_has_whitelisted_ip(self): - w = WhiteListPermission() - req = mock.MagicMock( - META={ - 'HTTP_X_FORWARDED_FOR': '5.5.5.5, 2.2.3.3', - 'REMOTE_ADDR': '5.6.7.8' - } - ) - self.assertFalse(w.has_permission(req, None)) - - def test_it_prefers_forwarded_for_to_remote_addr(self): - w = WhiteListPermission() - req = mock.MagicMock( - META={ - 'HTTP_X_FORWARDED_FOR': '5.5.5.5, 2.2.3.3', - 'REMOTE_ADDR': '1.1.2.2' - } - ) - self.assertFalse(w.has_permission(req, None)) diff --git a/data_explorer/templates/base.html b/data_explorer/templates/base.html index 46551378a..52f194585 100644 --- a/data_explorer/templates/base.html +++ b/data_explorer/templates/base.html @@ -33,10 +33,6 @@ - - diff --git a/docs/api.md b/docs/api.md index 8cf4b7de1..68fd14eda 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2,26 +2,17 @@ CALC's back end exposes a public API for its labor rates data. This API is used by CALC's front end Data Explorer application, and can also be accessed by any third-party application over the public internet. -# Local development vs. deployed instances - -When developing CALC locally, the API is served from the relative URL prefix `/api/` (for example `http://localhost:8000/api/rates`). - -In its deployed instances (development, staging, and production), CALC's public API is fronted by an [API Umbrella][] instance on [api.data.gov](https://api.data.gov) which proxies all API requests to CALC. This allows CALC to not have to concern itself with details like rate limiting. The production CALC API is available at `https://api.data.gov/gsa/calc/`. - -In order to ensure that API requests work both in local development and in deployed instances, CALC's front end code should not simply make requests against the relative `/api/` URLs. Instead, a global JavaScript variable called `API_HOST` is available for use as a prefix to requests. When developing locally, it will be set to `/api/`, but on CALC's development, staging, and production deployments it will be an absolute URL. - -[API Umbrella]: https://apiumbrella.io/ - ## API endpoints -The following documentation assumes you're trying to access the API from the production instance via a tool like `curl` at `https://api.data.gov/gsa/calc/`. In development, use `http://localhost:8000/api/` or rely on the `API_HOST` variable. +The following documentation assumes you're trying to access the API from the production instance via a tool like `curl` at `https://calc.gsa.gov/api/`. +In development, use `http://localhost:8000/api/`. ### `/rates/` You can access labor rate information at `/rates/`. ``` -https://api.data.gov/gsa/calc/rates/ +https://calc.gsa.gov/api/rates/ ``` #### Labor Categories @@ -29,7 +20,7 @@ https://api.data.gov/gsa/calc/rates/ You can search for prices of specific labor categories by using the `q` parameter. For example: ``` -https://api.data.gov/gsa/calc/rates/?q=accountant +https://calc.gsa.gov/api/rates/?q=accountant ``` You can change the way that labor categories are searched by using the `query_type` parameter, which can be either: @@ -41,13 +32,13 @@ You can change the way that labor categories are searched by using the `query_ty You can search for multiple labor categories separated by a comma. ``` -https://api.data.gov/gsa/calc/rates/?q=trainer,instructor +https://calc.gsa.gov/api/rates/?q=trainer,instructor ``` If any of the labor categories you'd like included in your search has a comma, you can surround that labor category with quotation marks: ``` -https://api.data.gov/gsa/calc/rates/?q="engineer, senior",instructor +https://calc.gsa.gov/api/rates/?q="engineer, senior",instructor ``` All of the query types are case-insensitive. @@ -59,14 +50,14 @@ All of the query types are case-insensitive. You can also filter by the minimum years of experience and maximum years of experience. For example: ``` -https://api.data.gov/gsa/calc/rates/?&min_experience=5&max_experience=10&q=technical +https://calc.gsa.gov/api/rates/?&min_experience=5&max_experience=10&q=technical ``` Or, you can filter with a single, comma-separated range. For example, if you wanted results with more than five years and less than ten years of experience: ``` -https://api.data.gov/gsa/calc/rates/?experience_range=5,10 +https://calc.gsa.gov/api/rates/?experience_range=5,10 ``` ##### Education @@ -82,20 +73,20 @@ These filters accept one or more (comma-separated) education values: * `PHD` (Ph.D). ``` -https://api.data.gov/gsa/calc/rates/?education=AA,BA +https://calc.gsa.gov/api/rates/?education=AA,BA ``` Use `min_education` to get all results that meet and exceed the selected education. The following example will return results that have an education level of `MA` or `PHD`: ``` -https://api.data.gov/gsa/calc/rates/?min_education=MA +https://calc.gsa.gov/api/rates/?min_education=MA ``` The default pagination is set to 200. You can paginate using the `page` parameter: ``` -https://api.data.gov/gsa/calc/rates/?q=translator&page=2 +https://calc.gsa.gov/api/rates/?q=translator&page=2 ``` #### Price Filters @@ -103,15 +94,15 @@ https://api.data.gov/gsa/calc/rates/?q=translator&page=2 You can filter by price with any of the `price` (exact match), `price__lte` (price is less than or equal to) or `price__gte` (price is greater than or equal to) parameters: ``` -https://api.data.gov/gsa/calc/rates/?price=95 -https://api.data.gov/gsa/calc/rates/?price__lte=95 -https://api.data.gov/gsa/calc/rates/?price__gte=95 +https://calc.gsa.gov/api/rates/?price=95 +https://calc.gsa.gov/api/rates/?price__lte=95 +https://calc.gsa.gov/api/rates/?price__gte=95 ``` The `price__lte` and `price__gte` parameters may be used together to search for a price range: ``` -https://api.data.gov/gsa/calc/rates/?price__gte=95&price__lte=105 +https://calc.gsa.gov/api/rates/?price__gte=95&price__lte=105 ``` #### Excluding Records @@ -119,7 +110,7 @@ https://api.data.gov/gsa/calc/rates/?price__gte=95&price__lte=105 You can also exclude specific records from the results by passing in an `exclude` parameter and a comma-separated list of ids: ``` -https://api.data.gov/gsa/calc/rates/?q=environmental+technician&exclude=875173,875749 +https://calc.gsa.gov/api/rates/?q=environmental+technician&exclude=875173,875749 ``` #### Other Filters @@ -134,7 +125,7 @@ Other parameters allow you to filter by: Here is an example with all four parameters (`schedule`, `sin`, `site`, and `business_size`) included: ``` -https://api.data.gov/gsa/calc/rates/?schedule=mobis&sin=874&site=customer&business_size=s +https://calc.gsa.gov/api/rates/?schedule=mobis&sin=874&site=customer&business_size=s ``` For schedules, there are 8 different values that will return results (case-insensitive): diff --git a/docs/deploy.md b/docs/deploy.md index 0628266bc..bdfeaae0c 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -249,55 +249,6 @@ cd /home/vcap/app source /home/vcap/app/.profile.d/python.sh ``` -### Setting up the API - -As mentioned in the [API documentation](api.md), CALC's public API -is actually proxied by api.data.gov. - -In order to configure the proxying between api.data.gov and CALC, -you will need to obtain an administrative account on api.data.gov. -For more information on doing this, see the [api.data.gov User Manual][]. - -You'll then want to tell api.data.gov what host it will listen for, and -what host your API backend is listening on. For example: - - - - - - - - - - -
Frontend HostBackend Host
api.data.govcalc-prod.app.cloud.gov
- -You will also want to configure your API backend on -api.data.gov with one **Matching URL Prefixes** entry. -The **Backend Prefix** should always be `/api/`, while the -**Frontend Prefix** is up to you. Here's an example: - - - - - - - - - - -
Frontend PrefixBackend Prefix
/gsa/calc//api/
- -Now you'll need to configure `API_HOST` on your CALC instance to be -the combination of your **Frontend Host** and **Frontend Prefix**. -For example, given the earlier examples listed above, your -`API_HOST` setting on CALC would be `https://api.data.gov/gsa/calc/`. - -Finally, as mentioned in the [Securing your API backend][] section of the -user manual, you will likely need to configure `WHITELISTED_IPS` on -your CALC instance to ensure that clients can't bypass rate limiting by -directly contacting your CALC instance. - ### Testing production deployments Because reverse proxies like CloudFront can be misconfigured to prevent diff --git a/docs/environment.md b/docs/environment.md index ee3c61bb7..5eb84450a 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -80,17 +80,6 @@ string), the boolean is true; otherwise, it's false. If this is undefined and `DEBUG` is true, then a built-in Fake UAA Provider will be used to "simulate" cloud.gov login. -* `WHITELISTED_IPS` is a comma-separated string of IP addresses that specifies - IPs that the REST API will accept requests from. Any IPs not in the list - attempting to access the API will receive a 403 Forbidden response. - Example: `127.0.0.1,192.168.1.1`. - -* `API_HOST` is the relative or absolute URL used to access the - API hosted by CALC. It defaults to `/api/` but may need to be changed - if the API has a proxy in front of it, as it likely will be if deployed - on government infrastructure. For more information, see - the [API documentation](api.md). - * `SECURITY_HEADERS_ON_ERROR_ONLY` is a boolean value that indicates whether security-related response headers (such as `X-XSS-Protection`) should only be added on error (status code >= 400) responses. This setting diff --git a/frontend/source/js/data-explorer/api.js b/frontend/source/js/data-explorer/api.js index e1893b0d2..5618b7d0b 100644 --- a/frontend/source/js/data-explorer/api.js +++ b/frontend/source/js/data-explorer/api.js @@ -3,8 +3,8 @@ import xhr from 'xhr'; import * as qs from 'querystring'; export default class API { - constructor(basePath = '') { - this.basePath = window.API_HOST || basePath; + constructor(basePath = '/api/') { + this.basePath = basePath; if (this.basePath.charAt(this.basePath.length - 1) !== '/') { this.basePath += '/'; } diff --git a/frontend/source/js/data-explorer/constants.js b/frontend/source/js/data-explorer/constants.js index 68837f4f4..0aa0102d9 100644 --- a/frontend/source/js/data-explorer/constants.js +++ b/frontend/source/js/data-explorer/constants.js @@ -125,6 +125,4 @@ export const QUERY_TYPE_LABELS = { export const MAX_QUERY_LENGTH = 255; -export const API_HOST = window.API_HOST; - -export const API_RATES_CSV = `${API_HOST}rates/csv/`; +export const API_RATES_CSV = '/rates/csv/'; diff --git a/frontend/source/js/data-explorer/tests/api.test.js b/frontend/source/js/data-explorer/tests/api.test.js index 3217ce754..48f6cca8b 100644 --- a/frontend/source/js/data-explorer/tests/api.test.js +++ b/frontend/source/js/data-explorer/tests/api.test.js @@ -18,12 +18,6 @@ describe('API constructor', () => { const api2 = new API('/api2/'); expect(api2.basePath).toMatch('/api2/'); }); - - it('uses window.API_HOST if defined', () => { - window.API_HOST = 'whatever'; - const api = new API(); - expect(api.basePath).toMatch('whatever/'); - }); }); describe('API get', () => { diff --git a/frontend/templates/tests.html b/frontend/templates/tests.html index 7f3fac1bf..4a2bf727c 100644 --- a/frontend/templates/tests.html +++ b/frontend/templates/tests.html @@ -9,9 +9,6 @@ -
diff --git a/hourglass/context_processors.py b/hourglass/context_processors.py index 140f82fa9..d26b912d8 100644 --- a/hourglass/context_processors.py +++ b/hourglass/context_processors.py @@ -9,11 +9,6 @@ def canonical_url(request): return {'canonical_url': get_canonical_url(request)} -def api_host(request): - '''Include API_HOST in all request contexts''' - return {'API_HOST': settings.API_HOST} - - def show_debug_ui(request): '''Include show_debug_ui in all request contexts''' return {'show_debug_ui': settings.DEBUG and not settings.HIDE_DEBUG_UI} diff --git a/hourglass/settings.py b/hourglass/settings.py index 3d52a3884..aee59d0a4 100644 --- a/hourglass/settings.py +++ b/hourglass/settings.py @@ -17,7 +17,7 @@ from .settings_utils import (load_cups_from_vcap_services, load_redis_url_from_vcap_services, - get_whitelisted_ips, is_running_tests) + is_running_tests) BASE_DIR = os.path.dirname(os.path.dirname(__file__)) @@ -73,8 +73,6 @@ SERVER_EMAIL = os.environ['SERVER_EMAIL'] HELP_EMAIL = os.environ.get('HELP_EMAIL', DEFAULT_FROM_EMAIL) -API_HOST = os.environ.get('API_HOST', '/api/') - GA_TRACKING_ID = os.environ.get('GA_TRACKING_ID', '') NON_PROD_INSTANCE_NAME = os.environ.get('NON_PROD_INSTANCE_NAME', '') @@ -88,7 +86,6 @@ 'OPTIONS': { 'context_processors': [ 'hourglass.context_processors.canonical_url', - 'hourglass.context_processors.api_host', 'hourglass.context_processors.show_debug_ui', 'hourglass.context_processors.google_analytics_tracking_id', 'hourglass.context_processors.help_email', @@ -230,10 +227,6 @@ REST_FRAMEWORK = { 'COERCE_DECIMAL_TO_STRING': False, - 'WHITELIST': get_whitelisted_ips(), - 'DEFAULT_PERMISSION_CLASSES': ( - 'api.permissions.WhiteListPermission', - ), } LOGGING: Dict[str, Any] = { diff --git a/hourglass/settings_utils.py b/hourglass/settings_utils.py index 0bcba7e21..40691d594 100644 --- a/hourglass/settings_utils.py +++ b/hourglass/settings_utils.py @@ -1,7 +1,7 @@ import os import sys import json -from typing import List, Optional +from typing import List Environ = os._Environ @@ -29,18 +29,6 @@ def load_cups_from_vcap_services(name: str='calc-env', env[key] = value -def get_whitelisted_ips(env: Environ=os.environ) -> Optional[List[str]]: - ''' - Detects if WHITELISTED_IPS is in the environment; if not, - returns None. if so, parses WHITELISTED_IPS as a comma-separated - string and returns a list of values. - ''' - if 'WHITELISTED_IPS' not in env: - return None - - return [s.strip() for s in env['WHITELISTED_IPS'].split(',')] - - def load_redis_url_from_vcap_services(name: str, env: Environ=os.environ) -> None: ''' diff --git a/hourglass/tests/tests.py b/hourglass/tests/tests.py index 81ec73beb..c5ae0ef47 100644 --- a/hourglass/tests/tests.py +++ b/hourglass/tests/tests.py @@ -13,7 +13,6 @@ from .. import healthcheck, __version__ from ..settings_utils import (load_cups_from_vcap_services, load_redis_url_from_vcap_services, - get_whitelisted_ips, is_running_tests) @@ -236,20 +235,6 @@ def test_redis_url_is_loaded(self): 'redis://:the_password@the_host:1234') -class GetWhitelistedIPsTest(unittest.TestCase): - - def test_returns_none_when_not_in_env(self): - env = {} - self.assertIsNone(get_whitelisted_ips(env)) - - def test_returns_whitelisted_ips_list(self): - env = { - 'WHITELISTED_IPS': '1.2.3.4,1.2.3.8, 1.2.3.16' - } - ips = get_whitelisted_ips(env) - self.assertListEqual(ips, ['1.2.3.4', '1.2.3.8', '1.2.3.16']) - - @override_settings( # This will make tests run faster. PASSWORD_HASHERS=['django.contrib.auth.hashers.MD5PasswordHasher'], diff --git a/production_tests/tests.py b/production_tests/tests.py index 5cc29937e..32fdde7f2 100644 --- a/production_tests/tests.py +++ b/production_tests/tests.py @@ -1,22 +1,9 @@ -import re from urllib.parse import urlparse, parse_qs from .util import ProductionTestCase class ProductionTests(ProductionTestCase): - api_url = None - - def get_api_url(self): - if self.api_url: - return self.api_url - res = self.client.get('/') - m = re.search(r'var API_HOST = "([^"]+)"', - res.content.decode('utf-8')) - api_url = m.group(1) - self.api_url = api_url - return api_url - def test_oauth2_redirect_uri_has_correct_domain(self): ''' Mitigation against https://github.com/18F/calc/pull/1187. @@ -93,9 +80,8 @@ def test_api_supports_cors(self): ''' Mitigation against https://github.com/18F/calc/issues/1307. ''' - api_url = self.get_api_url() res = self.client.get( - api_url + 'search/?format=json&q=zzzzzzzz&query_type=match_all', + '/api/search/?format=json&q=zzzzzzzz&query_type=match_all', headers={'Origin': self.ORIGIN} ) self.assertEqual(res.status_code, 200) @@ -103,9 +89,8 @@ def test_api_supports_cors(self): self.assertEqual(res.headers['Access-Control-Allow-Origin'], '*') def test_api_passes_json_accept_header(self): - api_url = self.get_api_url() res = self.client.get( - api_url + 'search/?q=zzzzzzzz&query_type=match_all', + '/api/search/?q=zzzzzzzz&query_type=match_all', headers={'Accept': 'application/json'} ) self.assertEqual(res.status_code, 200)