exceptions: Use HTTP 401 code for authentication errors.

This commit is contained in:
Tim Abbott 2021-07-05 11:28:24 -07:00
parent 4f9c7cae0a
commit af47fa705e
7 changed files with 48 additions and 29 deletions

View File

@ -11,6 +11,18 @@ below features are supported.
## Changes in Zulip 5.0 ## Changes in Zulip 5.0
**Feature level 76**
* [`POST /fetch_api_key`](/api/fetch_api_key), [`POST
/dev_fetch_api_key`](/api/dev_fetch_api_key): The HTTP status for
authentication errors is now 401. This was previously 403.
* All API endpoints now use the HTTP 401 error status for API requests
involving a deactivated user or realm. This was previously 403.
* Mobile push notifications now include the `mentioned_user_group_id`
and `mentioned_user_group_name` fields when a user group containing
the user is mentioned. Previously, these were indistinguishable
from personal mentions (as both types have `trigger="mention"`).
**Feature level 75** **Feature level 75**
* [`POST /register`](/api/register-queue), `PATCH /realm`: Replaced `allow_community_topic_editing` * [`POST /register`](/api/register-queue), `PATCH /realm`: Replaced `allow_community_topic_editing`

View File

@ -19,6 +19,10 @@ errors common to many endpoints:
{generate_code_example|/rest-error-handling:post|fixture(400)} {generate_code_example|/rest-error-handling:post|fixture(400)}
{generate_code_example|/rest-error-handling:post|fixture(401)}
{generate_code_example|/rest-error-handling:post|fixture(403)}
{generate_code_example|/rest-error-handling:post|fixture(429)} {generate_code_example|/rest-error-handling:post|fixture(429)}
The `retry-after` paremeter in the response indicates how many seconds The `retry-after` paremeter in the response indicates how many seconds
@ -49,5 +53,3 @@ limit.
**Changes**: The `code` field in the response is new in Zulip 4.0 **Changes**: The `code` field in the response is new in Zulip 4.0
(feature level 36). (feature level 36).
{generate_code_example|/rest-error-handling:post|fixture(403)}

View File

@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.4.3"
# Changes should be accompanied by documentation explaining what the # Changes should be accompanied by documentation explaining what the
# new level means in templates/zerver/api/changelog.md, as well as # new level means in templates/zerver/api/changelog.md, as well as
# "**Changes**" entries in the endpoint's documentation in `zulip.yaml`. # "**Changes**" entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 75 API_FEATURE_LEVEL = 76
# Bump the minor PROVISION_VERSION to indicate that folks should provision # Bump the minor PROVISION_VERSION to indicate that folks should provision
# only when going from an old version of the code to a newer version. Bump # only when going from an old version of the code to a newer version. Bump

View File

@ -280,8 +280,7 @@ class StreamAdministratorRequired(JsonableError):
class AuthenticationFailedError(JsonableError): class AuthenticationFailedError(JsonableError):
# Generic class for authentication failures # Generic class for authentication failures
code: ErrorCode = ErrorCode.AUTHENTICATION_FAILED code: ErrorCode = ErrorCode.AUTHENTICATION_FAILED
# Bug: Shouldn't this be 401? http_status_code = 401
http_status_code = 403
def __init__(self) -> None: def __init__(self) -> None:
pass pass

View File

@ -11151,9 +11151,9 @@ paths:
- $ref: "#/components/schemas/InvalidApiKeyError" - $ref: "#/components/schemas/InvalidApiKeyError"
- $ref: "#/components/schemas/MissingArgumentError" - $ref: "#/components/schemas/MissingArgumentError"
- $ref: "#/components/schemas/UserNotAuthorizedError" - $ref: "#/components/schemas/UserNotAuthorizedError"
"403": "401":
description: | description: |
Forbidden. Unauthorized.
content: content:
application/json: application/json:
schema: schema:
@ -12855,6 +12855,9 @@ components:
description: | description: |
## User account deactivated ## User account deactivated
**Changes**: These errors used the HTTP 403 status code
before Zulip 5.0 (feature level 76).
A typical failed json response for when user's account is deactivated A typical failed json response for when user's account is deactivated
RateLimitedError: RateLimitedError:
allOf: allOf:
@ -12882,6 +12885,9 @@ components:
description: | description: |
## Realm deactivated ## Realm deactivated
**Changes**: These errors used the HTTP 403 status code
before Zulip 5.0 (feature level 76).
A typical failed json response for when user's organization is deactivated A typical failed json response for when user's organization is deactivated
################### ###################

View File

@ -4057,7 +4057,7 @@ class FetchAPIKeyTest(ZulipTestCase):
result = self.client_post( result = self.client_post(
"/api/v1/fetch_api_key", dict(username=self.email, password="wrong") "/api/v1/fetch_api_key", dict(username=self.email, password="wrong")
) )
self.assert_json_error(result, "Your username or password is incorrect", 403) self.assert_json_error(result, "Your username or password is incorrect", 401)
def test_invalid_subdomain(self) -> None: def test_invalid_subdomain(self) -> None:
with mock.patch("zerver.views.auth.get_realm_from_request", return_value=None): with mock.patch("zerver.views.auth.get_realm_from_request", return_value=None):
@ -4074,7 +4074,7 @@ class FetchAPIKeyTest(ZulipTestCase):
dict(username=self.email, password=initial_password(self.email)), dict(username=self.email, password=initial_password(self.email)),
) )
self.assert_json_error_contains( self.assert_json_error_contains(
result, "Password authentication is disabled in this organization", 403 result, "Password authentication is disabled in this organization", 401
) )
@override_settings(AUTHENTICATION_BACKENDS=("zproject.backends.ZulipLDAPAuthBackend",)) @override_settings(AUTHENTICATION_BACKENDS=("zproject.backends.ZulipLDAPAuthBackend",))
@ -4101,14 +4101,14 @@ class FetchAPIKeyTest(ZulipTestCase):
"/api/v1/fetch_api_key", "/api/v1/fetch_api_key",
dict(username="hamlet", password=self.ldap_password("hamlet")), dict(username="hamlet", password=self.ldap_password("hamlet")),
) )
self.assert_json_error(result, "Your username or password is incorrect", 403) self.assert_json_error(result, "Your username or password is incorrect", 401)
self.change_ldap_user_attr("hamlet", "department", "testWrongRealm") self.change_ldap_user_attr("hamlet", "department", "testWrongRealm")
result = self.client_post( result = self.client_post(
"/api/v1/fetch_api_key", "/api/v1/fetch_api_key",
dict(username="hamlet", password=self.ldap_password("hamlet")), dict(username="hamlet", password=self.ldap_password("hamlet")),
) )
self.assert_json_error(result, "Your username or password is incorrect", 403) self.assert_json_error(result, "Your username or password is incorrect", 401)
self.change_ldap_user_attr("hamlet", "department", "zulip") self.change_ldap_user_attr("hamlet", "department", "zulip")
result = self.client_post( result = self.client_post(
@ -4134,7 +4134,7 @@ class FetchAPIKeyTest(ZulipTestCase):
"/api/v1/fetch_api_key", "/api/v1/fetch_api_key",
dict(username="hamlet", password=self.ldap_password("hamlet")), dict(username="hamlet", password=self.ldap_password("hamlet")),
) )
self.assert_json_error(result, "Your username or password is incorrect", 403) self.assert_json_error(result, "Your username or password is incorrect", 401)
self.change_ldap_user_attr("hamlet", "test2", "testing") self.change_ldap_user_attr("hamlet", "test2", "testing")
# Check with only one set # Check with only one set
@ -4142,7 +4142,7 @@ class FetchAPIKeyTest(ZulipTestCase):
"/api/v1/fetch_api_key", "/api/v1/fetch_api_key",
dict(username="hamlet", password=self.ldap_password("hamlet")), dict(username="hamlet", password=self.ldap_password("hamlet")),
) )
self.assert_json_error(result, "Your username or password is incorrect", 403) self.assert_json_error(result, "Your username or password is incorrect", 401)
self.change_ldap_user_attr("hamlet", "test1", "test") self.change_ldap_user_attr("hamlet", "test1", "test")
# Setting org_membership to not cause django_ldap_auth to warn, when synchronising # Setting org_membership to not cause django_ldap_auth to warn, when synchronising
@ -4177,7 +4177,7 @@ class FetchAPIKeyTest(ZulipTestCase):
"/api/v1/fetch_api_key", "/api/v1/fetch_api_key",
dict(username="hamlet", password=self.ldap_password("hamlet")), dict(username="hamlet", password=self.ldap_password("hamlet")),
) )
self.assert_json_error(result, "Your username or password is incorrect", 403) self.assert_json_error(result, "Your username or password is incorrect", 401)
# Override access with `org_membership` # Override access with `org_membership`
self.change_ldap_user_attr("hamlet", "department", "zulip") self.change_ldap_user_attr("hamlet", "department", "zulip")
@ -4196,7 +4196,7 @@ class FetchAPIKeyTest(ZulipTestCase):
"/api/v1/fetch_api_key", "/api/v1/fetch_api_key",
dict(username="hamlet", password=self.ldap_password("hamlet")), dict(username="hamlet", password=self.ldap_password("hamlet")),
) )
self.assert_json_error(result, "Your username or password is incorrect", 403) self.assert_json_error(result, "Your username or password is incorrect", 401)
def test_inactive_user(self) -> None: def test_inactive_user(self) -> None:
do_deactivate_user(self.user_profile, acting_user=None) do_deactivate_user(self.user_profile, acting_user=None)
@ -4204,7 +4204,7 @@ class FetchAPIKeyTest(ZulipTestCase):
"/api/v1/fetch_api_key", "/api/v1/fetch_api_key",
dict(username=self.email, password=initial_password(self.email)), dict(username=self.email, password=initial_password(self.email)),
) )
self.assert_json_error_contains(result, "Account is deactivated", 403) self.assert_json_error_contains(result, "Account is deactivated", 401)
def test_deactivated_realm(self) -> None: def test_deactivated_realm(self) -> None:
do_deactivate_realm(self.user_profile.realm, acting_user=None) do_deactivate_realm(self.user_profile.realm, acting_user=None)
@ -4212,7 +4212,7 @@ class FetchAPIKeyTest(ZulipTestCase):
"/api/v1/fetch_api_key", "/api/v1/fetch_api_key",
dict(username=self.email, password=initial_password(self.email)), dict(username=self.email, password=initial_password(self.email)),
) )
self.assert_json_error_contains(result, "This organization has been deactivated", 403) self.assert_json_error_contains(result, "This organization has been deactivated", 401)
def test_old_weak_password_after_hasher_change(self) -> None: def test_old_weak_password_after_hasher_change(self) -> None:
user_profile = self.example_user("hamlet") user_profile = self.example_user("hamlet")
@ -4234,7 +4234,7 @@ class FetchAPIKeyTest(ZulipTestCase):
dict(username=self.email, password=password), dict(username=self.email, password=password),
) )
self.assert_json_error( self.assert_json_error(
result, "Your password has been disabled and needs to be reset", 403 result, "Your password has been disabled and needs to be reset", 401
) )
@ -4260,17 +4260,17 @@ class DevFetchAPIKeyTest(ZulipTestCase):
def test_unregistered_user(self) -> None: def test_unregistered_user(self) -> None:
email = "foo@zulip.com" email = "foo@zulip.com"
result = self.client_post("/api/v1/dev_fetch_api_key", dict(username=email)) result = self.client_post("/api/v1/dev_fetch_api_key", dict(username=email))
self.assert_json_error_contains(result, "Your username or password is incorrect", 403) self.assert_json_error_contains(result, "Your username or password is incorrect", 401)
def test_inactive_user(self) -> None: def test_inactive_user(self) -> None:
do_deactivate_user(self.user_profile, acting_user=None) do_deactivate_user(self.user_profile, acting_user=None)
result = self.client_post("/api/v1/dev_fetch_api_key", dict(username=self.email)) result = self.client_post("/api/v1/dev_fetch_api_key", dict(username=self.email))
self.assert_json_error_contains(result, "Account is deactivated", 403) self.assert_json_error_contains(result, "Account is deactivated", 401)
def test_deactivated_realm(self) -> None: def test_deactivated_realm(self) -> None:
do_deactivate_realm(self.user_profile.realm, acting_user=None) do_deactivate_realm(self.user_profile.realm, acting_user=None)
result = self.client_post("/api/v1/dev_fetch_api_key", dict(username=self.email)) result = self.client_post("/api/v1/dev_fetch_api_key", dict(username=self.email))
self.assert_json_error_contains(result, "This organization has been deactivated", 403) self.assert_json_error_contains(result, "This organization has been deactivated", 401)
def test_dev_auth_disabled(self) -> None: def test_dev_auth_disabled(self) -> None:
with mock.patch("zerver.views.development.dev_login.dev_auth_enabled", return_value=False): with mock.patch("zerver.views.development.dev_login.dev_auth_enabled", return_value=False):

View File

@ -1096,7 +1096,7 @@ class DeactivatedRealmTest(ZulipTestCase):
}, },
) )
self.assert_json_error_contains( self.assert_json_error_contains(
result, "This organization has been deactivated", status_code=403 result, "This organization has been deactivated", status_code=401
) )
result = self.api_post( result = self.api_post(
@ -1128,7 +1128,7 @@ class DeactivatedRealmTest(ZulipTestCase):
realm.save() realm.save()
result = self.client_post("/json/fetch_api_key", {"password": test_password}) result = self.client_post("/json/fetch_api_key", {"password": test_password})
self.assert_json_error_contains( self.assert_json_error_contains(
result, "This organization has been deactivated", status_code=403 result, "This organization has been deactivated", status_code=401
) )
def test_webhook_deactivated_realm(self) -> None: def test_webhook_deactivated_realm(self) -> None:
@ -1143,7 +1143,7 @@ class DeactivatedRealmTest(ZulipTestCase):
data = self.webhook_fixture_data("jira", "created_v2") data = self.webhook_fixture_data("jira", "created_v2")
result = self.client_post(url, data, content_type="application/json") result = self.client_post(url, data, content_type="application/json")
self.assert_json_error_contains( self.assert_json_error_contains(
result, "This organization has been deactivated", status_code=403 result, "This organization has been deactivated", status_code=401
) )
@ -1246,7 +1246,7 @@ class InactiveUserTest(ZulipTestCase):
"to": self.example_email("othello"), "to": self.example_email("othello"),
}, },
) )
self.assert_json_error_contains(result, "Account is deactivated", status_code=403) self.assert_json_error_contains(result, "Account is deactivated", status_code=401)
result = self.api_post( result = self.api_post(
self.example_user("hamlet"), self.example_user("hamlet"),
@ -1275,7 +1275,7 @@ class InactiveUserTest(ZulipTestCase):
change_user_is_active(user_profile, False) change_user_is_active(user_profile, False)
result = self.client_post("/json/fetch_api_key", {"password": test_password}) result = self.client_post("/json/fetch_api_key", {"password": test_password})
self.assert_json_error_contains(result, "Account is deactivated", status_code=403) self.assert_json_error_contains(result, "Account is deactivated", status_code=401)
def test_login_deactivated_user(self) -> None: def test_login_deactivated_user(self) -> None:
""" """
@ -1341,7 +1341,7 @@ class InactiveUserTest(ZulipTestCase):
url = f"/api/v1/external/jira?api_key={api_key}&stream=jira_custom" url = f"/api/v1/external/jira?api_key={api_key}&stream=jira_custom"
data = self.webhook_fixture_data("jira", "created_v2") data = self.webhook_fixture_data("jira", "created_v2")
result = self.client_post(url, data, content_type="application/json") result = self.client_post(url, data, content_type="application/json")
self.assert_json_error_contains(result, "Account is deactivated", status_code=403) self.assert_json_error_contains(result, "Account is deactivated", status_code=401)
class TestIncomingWebhookBot(ZulipTestCase): class TestIncomingWebhookBot(ZulipTestCase):
@ -1689,7 +1689,7 @@ class TestAuthenticatedJsonPostViewDecorator(ZulipTestCase):
# we deactivate user manually because do_deactivate_user removes user session # we deactivate user manually because do_deactivate_user removes user session
change_user_is_active(user_profile, False) change_user_is_active(user_profile, False)
self.assert_json_error_contains( self.assert_json_error_contains(
self._do_test(user_profile), "Account is deactivated", status_code=403 self._do_test(user_profile), "Account is deactivated", status_code=401
) )
do_reactivate_user(user_profile, acting_user=None) do_reactivate_user(user_profile, acting_user=None)
@ -1702,7 +1702,7 @@ class TestAuthenticatedJsonPostViewDecorator(ZulipTestCase):
self.assert_json_error_contains( self.assert_json_error_contains(
self._do_test(user_profile), self._do_test(user_profile),
"This organization has been deactivated", "This organization has been deactivated",
status_code=403, status_code=401,
) )
do_reactivate_realm(user_profile.realm) do_reactivate_realm(user_profile.realm)