mirror of https://github.com/zulip/zulip.git
backends: Implementation of restricting certain backends by plan.
Only affects zulipchat, by being based on the BILLING_ENABLED setting. The restricted backends in this commit are - AzureAD - restricted to Standard plan - SAML - restricted to Plus plan, although it was already practically restricted due to requiring server-side configuration to be done by us This restriction is placed upon **enabling** a backend - so organizations that already have a backend enabled, will continue to be able to use it. This allows us to make exceptions and enable a backend for an org manually via the shell, and to grandfather organizations into keeping the backend they have been relying on.
This commit is contained in:
parent
fdbdf8c620
commit
da9e4e6e54
|
@ -290,7 +290,20 @@ export function dispatch_normal_event(event) {
|
||||||
switch (event.property) {
|
switch (event.property) {
|
||||||
case "default":
|
case "default":
|
||||||
for (const [key, value] of Object.entries(event.data)) {
|
for (const [key, value] of Object.entries(event.data)) {
|
||||||
|
if (key === "authentication_methods") {
|
||||||
|
for (const [auth_method, enabled] of Object.entries(
|
||||||
|
event.data.authentication_methods,
|
||||||
|
)) {
|
||||||
|
realm.realm_authentication_methods[auth_method].enabled =
|
||||||
|
enabled;
|
||||||
|
}
|
||||||
|
settings_org.populate_auth_methods(
|
||||||
|
event.data.authentication_methods,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
realm["realm_" + key] = value;
|
realm["realm_" + key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.hasOwn(realm_settings, key)) {
|
if (Object.hasOwn(realm_settings, key)) {
|
||||||
settings_org.sync_realm_settings(key);
|
settings_org.sync_realm_settings(key);
|
||||||
}
|
}
|
||||||
|
@ -305,11 +318,6 @@ export function dispatch_normal_event(event) {
|
||||||
message_live_update.rerender_messages_view();
|
message_live_update.rerender_messages_view();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (event.data.authentication_methods !== undefined) {
|
|
||||||
settings_org.populate_auth_methods(
|
|
||||||
event.data.authentication_methods,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case "icon":
|
case "icon":
|
||||||
realm.realm_icon_url = event.data.icon_url;
|
realm.realm_icon_url = event.data.icon_url;
|
||||||
|
|
|
@ -88,9 +88,24 @@ export function get_property_value(property_name, for_realm_default_settings, su
|
||||||
return "no_restriction";
|
return "no_restriction";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (property_name === "realm_authentication_methods") {
|
||||||
|
return realm_authentication_methods_to_boolean_dict();
|
||||||
|
}
|
||||||
|
|
||||||
return realm[property_name];
|
return realm[property_name];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function realm_authentication_methods_to_boolean_dict() {
|
||||||
|
const auth_method_to_bool = {};
|
||||||
|
for (const [auth_method_name, auth_method_info] of Object.entries(
|
||||||
|
realm.realm_authentication_methods,
|
||||||
|
)) {
|
||||||
|
auth_method_to_bool[auth_method_name] = auth_method_info.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth_method_to_bool;
|
||||||
|
}
|
||||||
|
|
||||||
export function extract_property_name($elem, for_realm_default_settings) {
|
export function extract_property_name($elem, for_realm_default_settings) {
|
||||||
if (for_realm_default_settings) {
|
if (for_realm_default_settings) {
|
||||||
// ID for realm_user_default_settings elements are of the form
|
// ID for realm_user_default_settings elements are of the form
|
||||||
|
|
|
@ -388,18 +388,29 @@ function can_configure_auth_methods() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function populate_auth_methods(auth_methods) {
|
export function populate_auth_methods(auth_method_to_bool_map) {
|
||||||
if (!meta.loaded) {
|
if (!meta.loaded) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const $auth_methods_list = $("#id_realm_authentication_methods").expectOne();
|
const $auth_methods_list = $("#id_realm_authentication_methods").expectOne();
|
||||||
auth_methods = settings_components.sort_object_by_key(auth_methods);
|
auth_method_to_bool_map = settings_components.sort_object_by_key(auth_method_to_bool_map);
|
||||||
let rendered_auth_method_rows = "";
|
let rendered_auth_method_rows = "";
|
||||||
for (const [auth_method, value] of Object.entries(auth_methods)) {
|
for (const [auth_method, value] of Object.entries(auth_method_to_bool_map)) {
|
||||||
rendered_auth_method_rows += render_settings_admin_auth_methods_list({
|
// Certain authentication methods are not available to be enabled without
|
||||||
|
// purchasing a plan, so we need to disable them in this UI.
|
||||||
|
// The restriction only applies to **enabling** the auth method, so this
|
||||||
|
// logic is dependent on the current value.
|
||||||
|
// The reason for that is that if for any reason, the auth method is already
|
||||||
|
// enabled (for example, because it was manually enabled for the organization
|
||||||
|
// by request, as an exception) - the organization should be able to disable it
|
||||||
|
// if they don't want it anymore.
|
||||||
|
const cant_be_enabled =
|
||||||
|
!realm.realm_authentication_methods[auth_method].available && !value;
|
||||||
|
|
||||||
|
const render_args = {
|
||||||
method: auth_method,
|
method: auth_method,
|
||||||
enabled: value,
|
enabled: value,
|
||||||
disable_configure_auth_method: !can_configure_auth_methods(),
|
disable_configure_auth_method: !can_configure_auth_methods() || cant_be_enabled,
|
||||||
// The negated character class regexp serves as an allowlist - the replace() will
|
// The negated character class regexp serves as an allowlist - the replace() will
|
||||||
// remove *all* symbols *but* digits (\d) and lowecase letters (a-z),
|
// remove *all* symbols *but* digits (\d) and lowecase letters (a-z),
|
||||||
// so that we can make assumptions on this string elsewhere in the code.
|
// so that we can make assumptions on this string elsewhere in the code.
|
||||||
|
@ -407,7 +418,14 @@ export function populate_auth_methods(auth_methods) {
|
||||||
// 1) It contains at least one allowed symbol
|
// 1) It contains at least one allowed symbol
|
||||||
// 2) No two auth method names are identical after this allowlist filtering
|
// 2) No two auth method names are identical after this allowlist filtering
|
||||||
prefix: "id_authmethod" + auth_method.toLowerCase().replaceAll(/[^\da-z]/g, "") + "_",
|
prefix: "id_authmethod" + auth_method.toLowerCase().replaceAll(/[^\da-z]/g, "") + "_",
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (cant_be_enabled) {
|
||||||
|
render_args.unavailable_reason =
|
||||||
|
realm.realm_authentication_methods[auth_method].unavailable_reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
rendered_auth_method_rows += render_settings_admin_auth_methods_list(render_args);
|
||||||
}
|
}
|
||||||
$auth_methods_list.html(rendered_auth_method_rows);
|
$auth_methods_list.html(rendered_auth_method_rows);
|
||||||
}
|
}
|
||||||
|
@ -967,7 +985,8 @@ export function build_page() {
|
||||||
populate_realm_domains_label(realm.realm_domains);
|
populate_realm_domains_label(realm.realm_domains);
|
||||||
|
|
||||||
// Populate authentication methods table
|
// Populate authentication methods table
|
||||||
populate_auth_methods(realm.realm_authentication_methods);
|
|
||||||
|
populate_auth_methods(settings_components.realm_authentication_methods_to_boolean_dict());
|
||||||
|
|
||||||
for (const property_name of simple_dropdown_properties) {
|
for (const property_name of simple_dropdown_properties) {
|
||||||
settings_components.set_property_dropdown_value(property_name);
|
settings_components.set_property_dropdown_value(property_name);
|
||||||
|
|
|
@ -4,5 +4,6 @@
|
||||||
prefix=prefix
|
prefix=prefix
|
||||||
is_checked=enabled
|
is_checked=enabled
|
||||||
label=method
|
label=method
|
||||||
is_disabled=disable_configure_auth_method}}
|
is_disabled=disable_configure_auth_method
|
||||||
|
tooltip_text=unavailable_reason}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -564,13 +564,16 @@ run_test("realm settings", ({override}) => {
|
||||||
realm.realm_allow_message_editing = false;
|
realm.realm_allow_message_editing = false;
|
||||||
realm.realm_message_content_edit_limit_seconds = 0;
|
realm.realm_message_content_edit_limit_seconds = 0;
|
||||||
realm.realm_edit_topic_policy = 3;
|
realm.realm_edit_topic_policy = 3;
|
||||||
|
realm.realm_authentication_methods = {Google: {enabled: false, available: true}};
|
||||||
override(settings_org, "populate_auth_methods", noop);
|
override(settings_org, "populate_auth_methods", noop);
|
||||||
dispatch(event);
|
dispatch(event);
|
||||||
assert_same(realm.realm_create_multiuse_invite_group, 3);
|
assert_same(realm.realm_create_multiuse_invite_group, 3);
|
||||||
assert_same(realm.realm_allow_message_editing, true);
|
assert_same(realm.realm_allow_message_editing, true);
|
||||||
assert_same(realm.realm_message_content_edit_limit_seconds, 5);
|
assert_same(realm.realm_message_content_edit_limit_seconds, 5);
|
||||||
assert_same(realm.realm_edit_topic_policy, 4);
|
assert_same(realm.realm_edit_topic_policy, 4);
|
||||||
assert_same(realm.realm_authentication_methods, {Google: true});
|
assert_same(realm.realm_authentication_methods, {
|
||||||
|
Google: {enabled: true, available: true},
|
||||||
|
});
|
||||||
|
|
||||||
event = event_fixtures.realm__update_dict__icon;
|
event = event_fixtures.realm__update_dict__icon;
|
||||||
override(realm_icon, "rerender", noop);
|
override(realm_icon, "rerender", noop);
|
||||||
|
|
|
@ -34,7 +34,7 @@ from zerver.models import (
|
||||||
)
|
)
|
||||||
from zerver.models.realms import get_org_type_display_name, get_realm
|
from zerver.models.realms import get_org_type_display_name, get_realm
|
||||||
from zerver.models.users import get_system_bot
|
from zerver.models.users import get_system_bot
|
||||||
from zproject.backends import all_implemented_backend_names
|
from zproject.backends import all_default_backend_names
|
||||||
|
|
||||||
if settings.CORPORATE_ENABLED:
|
if settings.CORPORATE_ENABLED:
|
||||||
from corporate.lib.support import get_realm_support_url
|
from corporate.lib.support import get_realm_support_url
|
||||||
|
@ -261,11 +261,10 @@ def do_create_realm(
|
||||||
create_system_user_groups_for_realm(realm)
|
create_system_user_groups_for_realm(realm)
|
||||||
set_default_for_realm_permission_group_settings(realm)
|
set_default_for_realm_permission_group_settings(realm)
|
||||||
|
|
||||||
# We create realms with all authentications methods enabled by default.
|
|
||||||
RealmAuthenticationMethod.objects.bulk_create(
|
RealmAuthenticationMethod.objects.bulk_create(
|
||||||
[
|
[
|
||||||
RealmAuthenticationMethod(name=backend_name, realm=realm)
|
RealmAuthenticationMethod(name=backend_name, realm=realm)
|
||||||
for backend_name in all_implemented_backend_names()
|
for backend_name in all_default_backend_names()
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -210,6 +210,57 @@ def parse_and_set_setting_value_if_required(
|
||||||
return parsed_value, setting_value_changed
|
return parsed_value, setting_value_changed
|
||||||
|
|
||||||
|
|
||||||
|
def get_realm_authentication_methods_for_page_params_api(
|
||||||
|
realm: Realm, authentication_methods: Dict[str, bool]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
# To avoid additional queries, this expects passing in the authentication_methods
|
||||||
|
# dictionary directly, which is useful when the caller already has to fetch it
|
||||||
|
# for other purposes - and that's the circumstance in which this function is
|
||||||
|
# currently used. We can trivially make this argument optional if needed.
|
||||||
|
|
||||||
|
from zproject.backends import AUTH_BACKEND_NAME_MAP
|
||||||
|
|
||||||
|
result_dict: Dict[str, Dict[str, Union[str, bool]]] = {
|
||||||
|
backend_name: {"enabled": enabled, "available": True}
|
||||||
|
for backend_name, enabled in authentication_methods.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
if not settings.BILLING_ENABLED:
|
||||||
|
return result_dict
|
||||||
|
|
||||||
|
# The rest of the function is only for the mechanism of restricting
|
||||||
|
# certain backends based on the realm's plan type on Zulip Cloud.
|
||||||
|
|
||||||
|
from corporate.models import CustomerPlan
|
||||||
|
|
||||||
|
for backend_name in result_dict:
|
||||||
|
available_for = AUTH_BACKEND_NAME_MAP[backend_name].available_for_cloud_plans
|
||||||
|
|
||||||
|
if available_for is not None and realm.plan_type not in available_for:
|
||||||
|
result_dict[backend_name]["available"] = False
|
||||||
|
|
||||||
|
required_upgrade_plan_number = min(
|
||||||
|
set(available_for).intersection({Realm.PLAN_TYPE_STANDARD, Realm.PLAN_TYPE_PLUS})
|
||||||
|
)
|
||||||
|
if required_upgrade_plan_number == Realm.PLAN_TYPE_STANDARD:
|
||||||
|
required_upgrade_plan_name = CustomerPlan.name_from_tier(
|
||||||
|
CustomerPlan.TIER_CLOUD_STANDARD
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
assert required_upgrade_plan_number == Realm.PLAN_TYPE_PLUS
|
||||||
|
required_upgrade_plan_name = CustomerPlan.name_from_tier(
|
||||||
|
CustomerPlan.TIER_CLOUD_PLUS
|
||||||
|
)
|
||||||
|
|
||||||
|
result_dict[backend_name]["unavailable_reason"] = _(
|
||||||
|
"You need to upgrade to the {required_upgrade_plan_name} plan to use this authentication method."
|
||||||
|
).format(required_upgrade_plan_name=required_upgrade_plan_name)
|
||||||
|
else:
|
||||||
|
result_dict[backend_name]["available"] = True
|
||||||
|
|
||||||
|
return result_dict
|
||||||
|
|
||||||
|
|
||||||
def validate_authentication_methods_dict_from_api(
|
def validate_authentication_methods_dict_from_api(
|
||||||
realm: Realm, authentication_methods: Dict[str, bool]
|
realm: Realm, authentication_methods: Dict[str, bool]
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -222,6 +273,32 @@ def validate_authentication_methods_dict_from_api(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if settings.BILLING_ENABLED:
|
||||||
|
validate_plan_for_authentication_methods(realm, authentication_methods)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_plan_for_authentication_methods(
|
||||||
|
realm: Realm, authentication_methods: Dict[str, bool]
|
||||||
|
) -> None:
|
||||||
|
from zproject.backends import AUTH_BACKEND_NAME_MAP
|
||||||
|
|
||||||
|
old_authentication_methods = realm.authentication_methods_dict()
|
||||||
|
newly_enabled_authentication_methods = {
|
||||||
|
name
|
||||||
|
for name, enabled in authentication_methods.items()
|
||||||
|
if enabled and not old_authentication_methods.get(name, False)
|
||||||
|
}
|
||||||
|
for name in newly_enabled_authentication_methods:
|
||||||
|
available_for = AUTH_BACKEND_NAME_MAP[name].available_for_cloud_plans
|
||||||
|
if available_for is not None and realm.plan_type not in available_for:
|
||||||
|
# This should only be feasible via the API, since app UI should prevent
|
||||||
|
# trying to enable an unavailable authentication method.
|
||||||
|
raise JsonableError(
|
||||||
|
_("Authentication method {name} is not available on your current plan.").format(
|
||||||
|
name=name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def do_set_realm_authentication_methods(
|
def do_set_realm_authentication_methods(
|
||||||
realm: Realm, authentication_methods: Dict[str, bool], *, acting_user: Optional[UserProfile]
|
realm: Realm, authentication_methods: Dict[str, bool], *, acting_user: Optional[UserProfile]
|
||||||
|
@ -561,6 +638,8 @@ def do_change_realm_org_type(
|
||||||
def do_change_realm_plan_type(
|
def do_change_realm_plan_type(
|
||||||
realm: Realm, plan_type: int, *, acting_user: Optional[UserProfile]
|
realm: Realm, plan_type: int, *, acting_user: Optional[UserProfile]
|
||||||
) -> None:
|
) -> None:
|
||||||
|
from zproject.backends import AUTH_BACKEND_NAME_MAP
|
||||||
|
|
||||||
old_value = realm.plan_type
|
old_value = realm.plan_type
|
||||||
|
|
||||||
if plan_type == Realm.PLAN_TYPE_LIMITED:
|
if plan_type == Realm.PLAN_TYPE_LIMITED:
|
||||||
|
@ -582,6 +661,19 @@ def do_change_realm_plan_type(
|
||||||
realm, "can_access_all_users_group", everyone_system_group, acting_user=acting_user
|
realm, "can_access_all_users_group", everyone_system_group, acting_user=acting_user
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If downgrading, disable authentication methods that are not available on the new plan.
|
||||||
|
if settings.BILLING_ENABLED:
|
||||||
|
realm_authentication_methods = realm.authentication_methods_dict()
|
||||||
|
for backend_name, enabled in realm_authentication_methods.items():
|
||||||
|
if enabled and plan_type < old_value:
|
||||||
|
available_for = AUTH_BACKEND_NAME_MAP[backend_name].available_for_cloud_plans
|
||||||
|
if available_for is not None and plan_type not in available_for:
|
||||||
|
realm_authentication_methods[backend_name] = False
|
||||||
|
if realm_authentication_methods != realm.authentication_methods_dict():
|
||||||
|
do_set_realm_authentication_methods(
|
||||||
|
realm, realm_authentication_methods, acting_user=acting_user
|
||||||
|
)
|
||||||
|
|
||||||
realm.plan_type = plan_type
|
realm.plan_type = plan_type
|
||||||
realm.save(update_fields=["plan_type"])
|
realm.save(update_fields=["plan_type"])
|
||||||
RealmAuditLog.objects.create(
|
RealmAuditLog.objects.create(
|
||||||
|
|
|
@ -41,7 +41,7 @@ from zerver.models import (
|
||||||
Subscription,
|
Subscription,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
)
|
)
|
||||||
from zproject.backends import all_implemented_backend_names
|
from zproject.backends import all_default_backend_names
|
||||||
|
|
||||||
# stubs
|
# stubs
|
||||||
ZerverFieldsT: TypeAlias = Dict[str, Any]
|
ZerverFieldsT: TypeAlias = Dict[str, Any]
|
||||||
|
@ -378,7 +378,7 @@ def build_realm(
|
||||||
zerver_realmplayground=[],
|
zerver_realmplayground=[],
|
||||||
zerver_realmauthenticationmethod=[
|
zerver_realmauthenticationmethod=[
|
||||||
{"realm": realm_id, "name": name, "id": i}
|
{"realm": realm_id, "name": name, "id": i}
|
||||||
for i, name in enumerate(all_implemented_backend_names(), start=1)
|
for i, name in enumerate(all_default_backend_names(), start=1)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
return realm
|
return realm
|
||||||
|
|
|
@ -10,6 +10,7 @@ from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from version import API_FEATURE_LEVEL, ZULIP_MERGE_BASE, ZULIP_VERSION
|
from version import API_FEATURE_LEVEL, ZULIP_MERGE_BASE, ZULIP_VERSION
|
||||||
from zerver.actions.default_streams import default_stream_groups_to_dicts_sorted
|
from zerver.actions.default_streams import default_stream_groups_to_dicts_sorted
|
||||||
|
from zerver.actions.realm_settings import get_realm_authentication_methods_for_page_params_api
|
||||||
from zerver.actions.users import get_owned_bot_dicts
|
from zerver.actions.users import get_owned_bot_dicts
|
||||||
from zerver.lib import emoji
|
from zerver.lib import emoji
|
||||||
from zerver.lib.alert_words import user_alert_words
|
from zerver.lib.alert_words import user_alert_words
|
||||||
|
@ -270,7 +271,11 @@ def fetch_initial_state_data(
|
||||||
# these manual entries are for those realm settings that don't
|
# these manual entries are for those realm settings that don't
|
||||||
# fit into that framework.
|
# fit into that framework.
|
||||||
realm_authentication_methods_dict = realm.authentication_methods_dict()
|
realm_authentication_methods_dict = realm.authentication_methods_dict()
|
||||||
state["realm_authentication_methods"] = realm_authentication_methods_dict
|
state["realm_authentication_methods"] = (
|
||||||
|
get_realm_authentication_methods_for_page_params_api(
|
||||||
|
realm, realm_authentication_methods_dict
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# We pretend these features are disabled because anonymous
|
# We pretend these features are disabled because anonymous
|
||||||
# users can't access them. In the future, we may want to move
|
# users can't access them. In the future, we may want to move
|
||||||
|
@ -1193,13 +1198,19 @@ def apply_event(
|
||||||
)
|
)
|
||||||
elif event["op"] == "update_dict":
|
elif event["op"] == "update_dict":
|
||||||
for key, value in event["data"].items():
|
for key, value in event["data"].items():
|
||||||
state["realm_" + key] = value
|
if key == "authentication_methods":
|
||||||
|
state_realm_authentication_methods = state["realm_authentication_methods"]
|
||||||
|
for auth_method, enabled in value.items():
|
||||||
|
state_realm_authentication_methods[auth_method]["enabled"] = enabled
|
||||||
|
|
||||||
# It's a bit messy, but this is where we need to
|
# It's a bit messy, but this is where we need to
|
||||||
# update the state for whether password authentication
|
# update the state for whether password authentication
|
||||||
# is enabled on this server.
|
# is enabled on this server.
|
||||||
if key == "authentication_methods":
|
|
||||||
state["realm_password_auth_enabled"] = value["Email"] or value["LDAP"]
|
state["realm_password_auth_enabled"] = value["Email"] or value["LDAP"]
|
||||||
state["realm_email_auth_enabled"] = value["Email"]
|
state["realm_email_auth_enabled"] = value["Email"]
|
||||||
|
|
||||||
|
else:
|
||||||
|
state["realm_" + key] = value
|
||||||
elif event["op"] == "deactivated":
|
elif event["op"] == "deactivated":
|
||||||
# The realm has just been deactivated. If our request had
|
# The realm has just been deactivated. If our request had
|
||||||
# arrived a moment later, we'd have rendered the
|
# arrived a moment later, we'd have rendered the
|
||||||
|
|
|
@ -81,6 +81,7 @@ from zerver.models.groups import SystemGroups
|
||||||
from zerver.models.realms import get_realm
|
from zerver.models.realms import get_realm
|
||||||
from zerver.models.recipients import get_huddle_hash
|
from zerver.models.recipients import get_huddle_hash
|
||||||
from zerver.models.users import get_system_bot, get_user_profile_by_id
|
from zerver.models.users import get_system_bot, get_user_profile_by_id
|
||||||
|
from zproject.backends import AUTH_BACKEND_NAME_MAP
|
||||||
|
|
||||||
realm_tables = [
|
realm_tables = [
|
||||||
("zerver_realmauthenticationmethod", RealmAuthenticationMethod, "realmauthenticationmethod"),
|
("zerver_realmauthenticationmethod", RealmAuthenticationMethod, "realmauthenticationmethod"),
|
||||||
|
@ -897,6 +898,24 @@ def import_uploads(
|
||||||
future.result()
|
future.result()
|
||||||
|
|
||||||
|
|
||||||
|
def disable_restricted_authentication_methods(data: TableData) -> None:
|
||||||
|
"""
|
||||||
|
Should run only with settings.BILLING_ENABLED. Ensures that we only
|
||||||
|
enable authentication methods that are available without needing a plan.
|
||||||
|
If the organization upgrades to a paid plan, or gets a sponsorship,
|
||||||
|
they can enable the restricted authentication methods in their settings.
|
||||||
|
"""
|
||||||
|
realm_authentication_methods = data["zerver_realmauthenticationmethod"]
|
||||||
|
non_restricted_methods = []
|
||||||
|
for auth_method in realm_authentication_methods:
|
||||||
|
if AUTH_BACKEND_NAME_MAP[auth_method["name"]].available_for_cloud_plans is None:
|
||||||
|
non_restricted_methods.append(auth_method)
|
||||||
|
else:
|
||||||
|
logging.warning("Dropped restricted authentication method: %s", auth_method["name"])
|
||||||
|
|
||||||
|
data["zerver_realmauthenticationmethod"] = non_restricted_methods
|
||||||
|
|
||||||
|
|
||||||
# Importing data suffers from a difficult ordering problem because of
|
# Importing data suffers from a difficult ordering problem because of
|
||||||
# models that reference each other circularly. Here is a correct order.
|
# models that reference each other circularly. Here is a correct order.
|
||||||
#
|
#
|
||||||
|
@ -1078,6 +1097,10 @@ def do_import_realm(import_dir: Path, subdomain: str, processes: int = 1) -> Rea
|
||||||
|
|
||||||
re_map_foreign_keys(data, "zerver_defaultstream", "stream", related_table="stream")
|
re_map_foreign_keys(data, "zerver_defaultstream", "stream", related_table="stream")
|
||||||
re_map_foreign_keys(data, "zerver_realmemoji", "author", related_table="user_profile")
|
re_map_foreign_keys(data, "zerver_realmemoji", "author", related_table="user_profile")
|
||||||
|
|
||||||
|
if settings.BILLING_ENABLED:
|
||||||
|
disable_restricted_authentication_methods(data)
|
||||||
|
|
||||||
for table, model, related_table in realm_tables:
|
for table, model, related_table in realm_tables:
|
||||||
re_map_foreign_keys(data, table, "realm", related_table="realm")
|
re_map_foreign_keys(data, table, "realm", related_table="realm")
|
||||||
update_model_ids(model, data, related_table)
|
update_model_ids(model, data, related_table)
|
||||||
|
|
|
@ -14,7 +14,7 @@ from zerver.models import (
|
||||||
)
|
)
|
||||||
from zerver.models.clients import get_client
|
from zerver.models.clients import get_client
|
||||||
from zerver.models.users import get_system_bot
|
from zerver.models.users import get_system_bot
|
||||||
from zproject.backends import all_implemented_backend_names
|
from zproject.backends import all_default_backend_names
|
||||||
|
|
||||||
|
|
||||||
def server_initialized() -> bool:
|
def server_initialized() -> bool:
|
||||||
|
@ -41,11 +41,10 @@ def create_internal_realm() -> None:
|
||||||
create_system_user_groups_for_realm(realm)
|
create_system_user_groups_for_realm(realm)
|
||||||
set_default_for_realm_permission_group_settings(realm)
|
set_default_for_realm_permission_group_settings(realm)
|
||||||
|
|
||||||
# We create realms with all authentications methods enabled by default.
|
|
||||||
RealmAuthenticationMethod.objects.bulk_create(
|
RealmAuthenticationMethod.objects.bulk_create(
|
||||||
[
|
[
|
||||||
RealmAuthenticationMethod(name=backend_name, realm=realm)
|
RealmAuthenticationMethod(name=backend_name, realm=realm)
|
||||||
for backend_name in all_implemented_backend_names()
|
for backend_name in all_default_backend_names()
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -711,13 +711,12 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub
|
||||||
on the server, this will not return an entry for "Email")."""
|
on the server, this will not return an entry for "Email")."""
|
||||||
# This mapping needs to be imported from here due to the cyclic
|
# This mapping needs to be imported from here due to the cyclic
|
||||||
# dependency.
|
# dependency.
|
||||||
from zproject.backends import AUTH_BACKEND_NAME_MAP, all_implemented_backend_names
|
from zproject.backends import AUTH_BACKEND_NAME_MAP
|
||||||
|
|
||||||
ret: Dict[str, bool] = {}
|
ret: Dict[str, bool] = {}
|
||||||
supported_backends = [type(backend) for backend in supported_auth_backends()]
|
supported_backends = [type(backend) for backend in supported_auth_backends()]
|
||||||
|
|
||||||
for backend_name in all_implemented_backend_names():
|
for backend_name, backend_class in AUTH_BACKEND_NAME_MAP.items():
|
||||||
backend_class = AUTH_BACKEND_NAME_MAP[backend_name]
|
|
||||||
if backend_class in supported_backends:
|
if backend_class in supported_backends:
|
||||||
ret[backend_name] = False
|
ret[backend_name] = False
|
||||||
for realm_authentication_method in RealmAuthenticationMethod.objects.filter(
|
for realm_authentication_method in RealmAuthenticationMethod.objects.filter(
|
||||||
|
|
|
@ -14529,15 +14529,35 @@ paths:
|
||||||
realm_authentication_methods:
|
realm_authentication_methods:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
description: |
|
description: |
|
||||||
Boolean describing whether the authentication method (i.e. its key)
|
Boolean describing whether the authentication method (i.e. its key)
|
||||||
is enabled in this organization.
|
is enabled in this organization.
|
||||||
|
available:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
description: |
|
||||||
|
Boolean describing whether the authentication method is available for use.
|
||||||
|
If false, the organization is not eligible to enable the authentication
|
||||||
|
method.
|
||||||
|
unavailable_reason:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Reason why the authentication method is unavailable. This field is optional
|
||||||
|
and is only present when 'available' is false.
|
||||||
|
additionalProperties: false
|
||||||
|
description: |
|
||||||
|
Dictionary describing the properties of the named authentication method for the
|
||||||
|
organization - its enabled status and availability for use by the
|
||||||
|
organization.
|
||||||
description: |
|
description: |
|
||||||
Present if `realm` is present in `fetch_event_types`.
|
Present if `realm` is present in `fetch_event_types`.
|
||||||
|
|
||||||
Dictionary of authentication method keys with boolean values that
|
Dictionary of authentication method keys mapped to dictionaries that
|
||||||
describe whether the named authentication method is enabled for the
|
describe the properties of the named authentication method for the
|
||||||
|
organization - its enabled status and availability for use by the
|
||||||
organization.
|
organization.
|
||||||
|
|
||||||
Clients should use this to implement server-settings UI to change which
|
Clients should use this to implement server-settings UI to change which
|
||||||
|
|
|
@ -7200,6 +7200,48 @@ class TestAdminSetBackends(ZulipTestCase):
|
||||||
"Invalid authentication method: NoSuchBackend. Valid methods are: ['Dev', 'Email']",
|
"Invalid authentication method: NoSuchBackend. Valid methods are: ['Dev', 'Email']",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with self.settings(
|
||||||
|
AUTHENTICATION_BACKENDS=(
|
||||||
|
"zproject.backends.EmailAuthBackend",
|
||||||
|
"zproject.backends.DevAuthBackend",
|
||||||
|
"zproject.backends.AzureADAuthBackend",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
realm = get_realm("zulip")
|
||||||
|
self.assertEqual(
|
||||||
|
realm.authentication_methods_dict(), {"Dev": True, "Email": True, "AzureAD": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
# AzureAD is not available without a Standard plan, but we start off with it enabled.
|
||||||
|
# Disabling the backend should work.
|
||||||
|
result = self.client_patch(
|
||||||
|
"/json/realm",
|
||||||
|
{
|
||||||
|
"authentication_methods": orjson.dumps(
|
||||||
|
# Github is not a supported authentication backend right now.
|
||||||
|
{"Email": True, "Dev": True, "AzureAD": False}
|
||||||
|
).decode()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
self.assertEqual(
|
||||||
|
realm.authentication_methods_dict(), {"Dev": True, "Email": True, "AzureAD": False}
|
||||||
|
)
|
||||||
|
|
||||||
|
# However, due to the lack of the necessary plan, enabling will fail.
|
||||||
|
result = self.client_patch(
|
||||||
|
"/json/realm",
|
||||||
|
{
|
||||||
|
"authentication_methods": orjson.dumps(
|
||||||
|
# Github is not a supported authentication backend right now.
|
||||||
|
{"Email": True, "Dev": True, "AzureAD": True}
|
||||||
|
).decode()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assert_json_error(
|
||||||
|
result, "Authentication method AzureAD is not available on your current plan."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EmailValidatorTestCase(ZulipTestCase):
|
class EmailValidatorTestCase(ZulipTestCase):
|
||||||
def test_valid_email(self) -> None:
|
def test_valid_email(self) -> None:
|
||||||
|
|
|
@ -369,6 +369,128 @@ class HomeTest(ZulipTestCase):
|
||||||
self.assertCountEqual(page_params, expected_keys)
|
self.assertCountEqual(page_params, expected_keys)
|
||||||
self.assertIsNone(page_params["state_data"])
|
self.assertIsNone(page_params["state_data"])
|
||||||
|
|
||||||
|
def test_realm_authentication_methods(self) -> None:
|
||||||
|
realm = get_realm("zulip")
|
||||||
|
self.login("desdemona")
|
||||||
|
|
||||||
|
with self.settings(
|
||||||
|
AUTHENTICATION_BACKENDS=(
|
||||||
|
"zproject.backends.EmailAuthBackend",
|
||||||
|
"zproject.backends.SAMLAuthBackend",
|
||||||
|
"zproject.backends.AzureADAuthBackend",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
result = self._get_home_page()
|
||||||
|
state_data = self._get_page_params(result)["state_data"]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
state_data["realm_authentication_methods"],
|
||||||
|
{
|
||||||
|
"Email": {"enabled": True, "available": True},
|
||||||
|
"AzureAD": {
|
||||||
|
"enabled": True,
|
||||||
|
"available": False,
|
||||||
|
"unavailable_reason": "You need to upgrade to the Zulip Cloud Standard plan to use this authentication method.",
|
||||||
|
},
|
||||||
|
"SAML": {
|
||||||
|
"enabled": True,
|
||||||
|
"available": False,
|
||||||
|
"unavailable_reason": "You need to upgrade to the Zulip Cloud Plus plan to use this authentication method.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now try with BILLING_ENABLED=False. This simulates a self-hosted deployment
|
||||||
|
# instead of Zulip Cloud. In this case, all authentication methods should be available.
|
||||||
|
with self.settings(BILLING_ENABLED=False):
|
||||||
|
result = self._get_home_page()
|
||||||
|
state_data = self._get_page_params(result)["state_data"]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
state_data["realm_authentication_methods"],
|
||||||
|
{
|
||||||
|
"Email": {"enabled": True, "available": True},
|
||||||
|
"AzureAD": {
|
||||||
|
"enabled": True,
|
||||||
|
"available": True,
|
||||||
|
},
|
||||||
|
"SAML": {
|
||||||
|
"enabled": True,
|
||||||
|
"available": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.settings(
|
||||||
|
AUTHENTICATION_BACKENDS=(
|
||||||
|
"zproject.backends.EmailAuthBackend",
|
||||||
|
"zproject.backends.SAMLAuthBackend",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
result = self._get_home_page()
|
||||||
|
state_data = self._get_page_params(result)["state_data"]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
state_data["realm_authentication_methods"],
|
||||||
|
{
|
||||||
|
"Email": {"enabled": True, "available": True},
|
||||||
|
"SAML": {
|
||||||
|
"enabled": True,
|
||||||
|
"available": False,
|
||||||
|
"unavailable_reason": "You need to upgrade to the Zulip Cloud Plus plan to use this authentication method.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Changing the plan_type to Standard grants access to AzureAD, but not SAML:
|
||||||
|
do_change_realm_plan_type(realm, Realm.PLAN_TYPE_STANDARD, acting_user=None)
|
||||||
|
|
||||||
|
with self.settings(
|
||||||
|
AUTHENTICATION_BACKENDS=(
|
||||||
|
"zproject.backends.EmailAuthBackend",
|
||||||
|
"zproject.backends.SAMLAuthBackend",
|
||||||
|
"zproject.backends.AzureADAuthBackend",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
result = self._get_home_page()
|
||||||
|
state_data = self._get_page_params(result)["state_data"]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
state_data["realm_authentication_methods"],
|
||||||
|
{
|
||||||
|
"Email": {"enabled": True, "available": True},
|
||||||
|
"AzureAD": {
|
||||||
|
"enabled": True,
|
||||||
|
"available": True,
|
||||||
|
},
|
||||||
|
"SAML": {
|
||||||
|
"enabled": True,
|
||||||
|
"available": False,
|
||||||
|
"unavailable_reason": "You need to upgrade to the Zulip Cloud Plus plan to use this authentication method.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now upgrade to Plus and verify that both SAML and AzureAD are available.
|
||||||
|
do_change_realm_plan_type(realm, Realm.PLAN_TYPE_PLUS, acting_user=None)
|
||||||
|
result = self._get_home_page()
|
||||||
|
state_data = self._get_page_params(result)["state_data"]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
state_data["realm_authentication_methods"],
|
||||||
|
{
|
||||||
|
"Email": {"enabled": True, "available": True},
|
||||||
|
"AzureAD": {
|
||||||
|
"enabled": True,
|
||||||
|
"available": True,
|
||||||
|
},
|
||||||
|
"SAML": {
|
||||||
|
"enabled": True,
|
||||||
|
"available": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def test_sentry_keys(self) -> None:
|
def test_sentry_keys(self) -> None:
|
||||||
def home_params() -> Dict[str, Any]:
|
def home_params() -> Dict[str, Any]:
|
||||||
result = self._get_home_page()
|
result = self._get_home_page()
|
||||||
|
|
|
@ -1577,6 +1577,52 @@ class RealmImportExportTest(ExportFile):
|
||||||
|
|
||||||
self.assertEqual(message_ids, [555, 888, 999])
|
self.assertEqual(message_ids, [555, 888, 999])
|
||||||
|
|
||||||
|
def test_import_of_authentication_methods(self) -> None:
|
||||||
|
with self.settings(
|
||||||
|
AUTHENTICATION_BACKENDS=(
|
||||||
|
"zproject.backends.EmailAuthBackend",
|
||||||
|
"zproject.backends.AzureADAuthBackend",
|
||||||
|
"zproject.backends.SAMLAuthBackend",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
realm = get_realm("zulip")
|
||||||
|
authentication_methods_dict = realm.authentication_methods_dict()
|
||||||
|
for auth_method in authentication_methods_dict:
|
||||||
|
authentication_methods_dict[auth_method] = True
|
||||||
|
do_set_realm_authentication_methods(
|
||||||
|
realm, authentication_methods_dict, acting_user=None
|
||||||
|
)
|
||||||
|
|
||||||
|
self.export_realm(realm)
|
||||||
|
|
||||||
|
with self.settings(BILLING_ENABLED=False), self.assertLogs(level="INFO"):
|
||||||
|
do_import_realm(get_output_dir(), "test-zulip")
|
||||||
|
|
||||||
|
imported_realm = Realm.objects.get(string_id="test-zulip")
|
||||||
|
self.assertEqual(
|
||||||
|
realm.authentication_methods_dict(),
|
||||||
|
imported_realm.authentication_methods_dict(),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.export_realm(realm)
|
||||||
|
|
||||||
|
with self.settings(BILLING_ENABLED=True), self.assertLogs(level="WARN") as mock_warn:
|
||||||
|
do_import_realm(get_output_dir(), "test-zulip2")
|
||||||
|
|
||||||
|
imported_realm = Realm.objects.get(string_id="test-zulip2")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
imported_realm.authentication_methods_dict(),
|
||||||
|
{"Email": True, "AzureAD": False, "SAML": False},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
mock_warn.output,
|
||||||
|
[
|
||||||
|
"WARNING:root:Dropped restricted authentication method: AzureAD",
|
||||||
|
"WARNING:root:Dropped restricted authentication method: SAML",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def test_plan_type(self) -> None:
|
def test_plan_type(self) -> None:
|
||||||
user = self.example_user("hamlet")
|
user = self.example_user("hamlet")
|
||||||
realm = user.realm
|
realm = user.realm
|
||||||
|
|
|
@ -30,6 +30,7 @@ from zerver.actions.realm_settings import (
|
||||||
do_reactivate_realm,
|
do_reactivate_realm,
|
||||||
do_scrub_realm,
|
do_scrub_realm,
|
||||||
do_send_realm_reactivation_email,
|
do_send_realm_reactivation_email,
|
||||||
|
do_set_realm_authentication_methods,
|
||||||
do_set_realm_property,
|
do_set_realm_property,
|
||||||
do_set_realm_user_default_setting,
|
do_set_realm_user_default_setting,
|
||||||
)
|
)
|
||||||
|
@ -851,6 +852,44 @@ class RealmTest(ZulipTestCase):
|
||||||
self.assertEqual(get_realm("onpremise").message_visibility_limit, None)
|
self.assertEqual(get_realm("onpremise").message_visibility_limit, None)
|
||||||
self.assertEqual(get_realm("onpremise").upload_quota_gb, None)
|
self.assertEqual(get_realm("onpremise").upload_quota_gb, None)
|
||||||
|
|
||||||
|
def test_initial_auth_methods(self) -> None:
|
||||||
|
with self.settings(
|
||||||
|
BILLING_ENABLED=True,
|
||||||
|
DEVELOPMENT=False,
|
||||||
|
AUTHENTICATION_BACKENDS=(
|
||||||
|
"zproject.backends.EmailAuthBackend",
|
||||||
|
"zproject.backends.AzureADAuthBackend",
|
||||||
|
"zproject.backends.SAMLAuthBackend",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
# Test a Cloud-like realm creation.
|
||||||
|
# Only the auth backends available on the free plan should be enabled.
|
||||||
|
realm = do_create_realm("hosted", "hosted")
|
||||||
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_LIMITED)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
realm.authentication_methods_dict(),
|
||||||
|
{
|
||||||
|
"Email": True,
|
||||||
|
"AzureAD": False,
|
||||||
|
"SAML": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now make sure that a self-hosted server creates realms with all auth methods enabled.
|
||||||
|
with self.settings(BILLING_ENABLED=False):
|
||||||
|
realm = do_create_realm("onpremise", "onpremise")
|
||||||
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_SELF_HOSTED)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
realm.authentication_methods_dict(),
|
||||||
|
{
|
||||||
|
"Email": True,
|
||||||
|
"AzureAD": True,
|
||||||
|
"SAML": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def test_change_org_type(self) -> None:
|
def test_change_org_type(self) -> None:
|
||||||
realm = get_realm("zulip")
|
realm = get_realm("zulip")
|
||||||
iago = self.example_user("iago")
|
iago = self.example_user("iago")
|
||||||
|
@ -944,6 +983,38 @@ class RealmTest(ZulipTestCase):
|
||||||
self.assertEqual(realm.message_visibility_limit, None)
|
self.assertEqual(realm.message_visibility_limit, None)
|
||||||
self.assertEqual(realm.upload_quota_gb, None)
|
self.assertEqual(realm.upload_quota_gb, None)
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
BILLING_ENABLED=True,
|
||||||
|
AUTHENTICATION_BACKENDS=(
|
||||||
|
"zproject.backends.EmailAuthBackend",
|
||||||
|
"zproject.backends.AzureADAuthBackend",
|
||||||
|
"zproject.backends.SAMLAuthBackend",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_realm_authentication_methods_after_downgrade(self) -> None:
|
||||||
|
realm = get_realm("zulip")
|
||||||
|
iago = self.example_user("iago")
|
||||||
|
|
||||||
|
do_change_realm_plan_type(realm, Realm.PLAN_TYPE_STANDARD, acting_user=iago)
|
||||||
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD)
|
||||||
|
|
||||||
|
do_set_realm_authentication_methods(
|
||||||
|
realm, {"Email": True, "AzureAD": True, "SAML": True}, acting_user=None
|
||||||
|
)
|
||||||
|
|
||||||
|
do_change_realm_plan_type(realm, Realm.PLAN_TYPE_LIMITED, acting_user=iago)
|
||||||
|
realm.refresh_from_db()
|
||||||
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_LIMITED)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
realm.authentication_methods_dict(),
|
||||||
|
{
|
||||||
|
"Email": True,
|
||||||
|
"AzureAD": False,
|
||||||
|
"SAML": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def test_message_retention_days(self) -> None:
|
def test_message_retention_days(self) -> None:
|
||||||
self.login("iago")
|
self.login("iago")
|
||||||
realm = get_realm("zulip")
|
realm = get_realm("zulip")
|
||||||
|
|
|
@ -21,6 +21,7 @@ from zerver.actions.realm_settings import (
|
||||||
do_set_realm_zulip_update_announcements_stream,
|
do_set_realm_zulip_update_announcements_stream,
|
||||||
parse_and_set_setting_value_if_required,
|
parse_and_set_setting_value_if_required,
|
||||||
validate_authentication_methods_dict_from_api,
|
validate_authentication_methods_dict_from_api,
|
||||||
|
validate_plan_for_authentication_methods,
|
||||||
)
|
)
|
||||||
from zerver.decorator import require_realm_admin, require_realm_owner
|
from zerver.decorator import require_realm_admin, require_realm_owner
|
||||||
from zerver.forms import check_subdomain_available as check_subdomain
|
from zerver.forms import check_subdomain_available as check_subdomain
|
||||||
|
@ -205,6 +206,7 @@ def update_realm(
|
||||||
validate_authentication_methods_dict_from_api(realm, authentication_methods)
|
validate_authentication_methods_dict_from_api(realm, authentication_methods)
|
||||||
if True not in authentication_methods.values():
|
if True not in authentication_methods.values():
|
||||||
raise JsonableError(_("At least one authentication method must be enabled."))
|
raise JsonableError(_("At least one authentication method must be enabled."))
|
||||||
|
validate_plan_for_authentication_methods(realm, authentication_methods)
|
||||||
|
|
||||||
if video_chat_provider is not None and video_chat_provider not in {
|
if video_chat_provider is not None and video_chat_provider not in {
|
||||||
p["id"] for p in Realm.VIDEO_CHAT_PROVIDERS.values()
|
p["id"] for p in Realm.VIDEO_CHAT_PROVIDERS.values()
|
||||||
|
|
|
@ -129,9 +129,24 @@ from zproject.settings_types import OIDCIdPConfigDict
|
||||||
redis_client = get_redis_client()
|
redis_client = get_redis_client()
|
||||||
|
|
||||||
|
|
||||||
def all_implemented_backend_names() -> List[str]:
|
def all_default_backend_names() -> List[str]:
|
||||||
|
if not settings.BILLING_ENABLED or settings.DEVELOPMENT:
|
||||||
|
# If billing isn't enabled, it's a self-hosted server
|
||||||
|
# and has access to all authentication backends.
|
||||||
|
#
|
||||||
|
# In DEVELOPMENT, we have BILLING_ENABLED=True, but
|
||||||
|
# nonetheless we want to enable all backends by default
|
||||||
|
# for convenience - we shouldn't add additional steps to the
|
||||||
|
# process of setting up a backend for testing.
|
||||||
return list(AUTH_BACKEND_NAME_MAP.keys())
|
return list(AUTH_BACKEND_NAME_MAP.keys())
|
||||||
|
|
||||||
|
# By default, only enable backends that are available without requiring a plan.
|
||||||
|
return [
|
||||||
|
name
|
||||||
|
for name, backend in AUTH_BACKEND_NAME_MAP.items()
|
||||||
|
if backend.available_for_cloud_plans is None
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# This first batch of methods is used by other code in Zulip to check
|
# This first batch of methods is used by other code in Zulip to check
|
||||||
# whether a given authentication backend is enabled for a given realm.
|
# whether a given authentication backend is enabled for a given realm.
|
||||||
|
@ -428,6 +443,11 @@ class ZulipAuthMixin:
|
||||||
name = "undefined"
|
name = "undefined"
|
||||||
_logger: Optional[logging.Logger] = None
|
_logger: Optional[logging.Logger] = None
|
||||||
|
|
||||||
|
# Describes which plans gives access to this authentication method on zulipchat.com.
|
||||||
|
# None means the backend is available regardless of the plan.
|
||||||
|
# Otherwise, it should be a list of Realm.plan_type values that give access to the backend.
|
||||||
|
available_for_cloud_plans: Optional[List[int]] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def logger(self) -> logging.Logger:
|
def logger(self) -> logging.Logger:
|
||||||
if self._logger is None:
|
if self._logger is None:
|
||||||
|
@ -2150,6 +2170,12 @@ class AzureADAuthBackend(SocialAuthMixin, AzureADOAuth2):
|
||||||
auth_backend_name = "AzureAD"
|
auth_backend_name = "AzureAD"
|
||||||
display_icon = staticfiles_storage.url("images/authentication_backends/azuread-icon.png")
|
display_icon = staticfiles_storage.url("images/authentication_backends/azuread-icon.png")
|
||||||
|
|
||||||
|
available_for_cloud_plans = [
|
||||||
|
Realm.PLAN_TYPE_STANDARD,
|
||||||
|
Realm.PLAN_TYPE_STANDARD_FREE,
|
||||||
|
Realm.PLAN_TYPE_PLUS,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@external_auth_method
|
@external_auth_method
|
||||||
class GitLabAuthBackend(SocialAuthMixin, GitLabOAuth2):
|
class GitLabAuthBackend(SocialAuthMixin, GitLabOAuth2):
|
||||||
|
@ -2552,6 +2578,8 @@ class SAMLAuthBackend(SocialAuthMixin, SAMLAuth):
|
||||||
# provide a registration flow prompt for them to set their name.
|
# provide a registration flow prompt for them to set their name.
|
||||||
full_name_validated = True
|
full_name_validated = True
|
||||||
|
|
||||||
|
available_for_cloud_plans = [Realm.PLAN_TYPE_PLUS]
|
||||||
|
|
||||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
if settings.SAML_REQUIRE_LIMIT_TO_SUBDOMAINS:
|
if settings.SAML_REQUIRE_LIMIT_TO_SUBDOMAINS:
|
||||||
idps_without_limit_to_subdomains = [
|
idps_without_limit_to_subdomains = [
|
||||||
|
|
Loading…
Reference in New Issue