def update_user_custom_profile_data( request: HttpRequest, user_profile: UserProfile, data: List[Dict[str, Union[int, str]]]=REQ(validator=check_list( check_dict([('id', check_int)])))) -> HttpResponse: for item in data: field_id = item['id'] try: field = CustomProfileField.objects.get(id=field_id) except CustomProfileField.DoesNotExist: return json_error(_('Field id {id} not found.').format(id=field_id)) validators = CustomProfileField.FIELD_VALIDATORS extended_validators = CustomProfileField.EXTENDED_FIELD_VALIDATORS field_type = field.field_type value = item['value'] var_name = '{}'.format(field.name) if field_type in validators: validator = validators[field_type] result = validator(var_name, value) else: # Check extended validators. extended_validator = extended_validators[field_type] field_data = field.field_data result = extended_validator(var_name, field_data, value) if result is not None: return json_error(result) do_update_user_custom_profile_data(user_profile, data) # We need to call this explicitly otherwise constraints are not check return json_success()
def api_fetch_api_key(request, username=REQ(), password=REQ()): # type: (HttpRequest, str, str) -> HttpResponse return_data = {} # type: Dict[str, bool] if username == "google-oauth2-token": user_profile = authenticate(google_oauth2_token=password, realm_subdomain=get_subdomain(request), return_data=return_data) else: user_profile = authenticate(username=username, password=password, realm_subdomain=get_subdomain(request), return_data=return_data) if return_data.get("inactive_user") == True: return json_error(_("Your account has been disabled."), data={"reason": "user disable"}, status=403) if return_data.get("inactive_realm") == True: return json_error(_("Your realm has been deactivated."), data={"reason": "realm deactivated"}, status=403) if return_data.get("password_auth_disabled") == True: return json_error(_("Password auth is disabled in your team."), data={"reason": "password auth disabled"}, status=403) if user_profile is None: if return_data.get("valid_attestation") == True: # We can leak that the user is unregistered iff they present a valid authentication string for the user. return json_error(_("This user is not registered; do so from a browser."), data={"reason": "unregistered"}, status=403) return json_error(_("Your username or password is incorrect."), data={"reason": "incorrect_creds"}, status=403) return json_success({"api_key": user_profile.api_key, "email": user_profile.email})
def json_change_settings(request, user_profile, full_name=REQ(), old_password=REQ(default=""), new_password=REQ(default=""), confirm_password=REQ(default="")): # type: (HttpRequest, UserProfile, text_type, text_type, text_type, text_type) -> HttpResponse if new_password != "" or confirm_password != "": if new_password != confirm_password: return json_error(_("New password must match confirmation password!")) if not authenticate(username=user_profile.email, password=old_password): return json_error(_("Wrong password!")) do_change_password(user_profile, new_password) result = {} if user_profile.full_name != full_name and full_name.strip() != "": if name_changes_disabled(user_profile.realm): # Failingly silently is fine -- they can't do it through the UI, so # they'd have to be trying to break the rules. pass else: new_full_name = full_name.strip() if len(new_full_name) > UserProfile.MAX_NAME_LENGTH: return json_error(_("Name too long!")) do_change_full_name(user_profile, new_full_name) result['full_name'] = new_full_name return json_success(result)
def update_realm_custom_profile_field(request: HttpRequest, user_profile: UserProfile, field_id: int, name: str=REQ(), hint: str=REQ(default=''), field_data: ProfileFieldData=REQ(default={}, converter=ujson.loads), ) -> HttpResponse: if not name.strip(): return json_error(_("Name cannot be blank.")) error = hint_validator('hint', hint) if error: return json_error(error, data={'field': 'hint'}) error = validate_field_data(field_data) if error: return json_error(error) realm = user_profile.realm try: field = CustomProfileField.objects.get(realm=realm, id=field_id) except CustomProfileField.DoesNotExist: return json_error(_('Field id {id} not found.').format(id=field_id)) try: try_update_realm_custom_profile_field(realm, field, name, hint=hint, field_data=field_data) except IntegrityError: return json_error(_('A field with that name already exists.')) return json_success()
def create_realm_custom_profile_field(request: HttpRequest, user_profile: UserProfile, name: str=REQ(), hint: str=REQ(default=''), field_data: ProfileFieldData=REQ(default={}, converter=ujson.loads), field_type: int=REQ(validator=check_int)) -> HttpResponse: if not name.strip(): return json_error(_("Name cannot be blank.")) error = hint_validator('hint', hint) if error: return json_error(error) field_types = [i[0] for i in CustomProfileField.FIELD_TYPE_CHOICES] if field_type not in field_types: return json_error(_("Invalid field type.")) error = validate_field_data(field_data) if error: return json_error(error) try: field = try_add_realm_custom_profile_field( realm=user_profile.realm, name=name, field_data=field_data, field_type=field_type, hint=hint, ) return json_success({'id': field.id}) except IntegrityError: return json_error(_("A field with that name already exists."))
def update_user_backend(request, user_profile, email, full_name=REQ(default="", validator=check_string), is_admin=REQ(default=None, validator=check_bool)): # type: (HttpRequest, UserProfile, text_type, Optional[text_type], Optional[bool]) -> HttpResponse try: target = get_user_profile_by_email(email) except UserProfile.DoesNotExist: return json_error(_('No such user')) if not user_profile.can_admin_user(target): return json_error(_('Insufficient permission')) if is_admin is not None: if not is_admin and check_last_admin(user_profile): return json_error(_('Cannot remove the only organization administrator')) do_change_is_admin(target, is_admin) if (full_name is not None and target.full_name != full_name and full_name.strip() != ""): # We don't respect `name_changes_disabled` here because the request # is on behalf of the administrator. new_full_name = full_name.strip() if len(new_full_name) > UserProfile.MAX_NAME_LENGTH: return json_error(_("Name too long!")) do_change_full_name(target, new_full_name) return json_success()
def api_stash_webhook(request, user_profile, stream=REQ(default='')): try: payload = ujson.loads(request.body) except ValueError: return json_error("Malformed JSON input") # We don't get who did the push, or we'd try to report that. try: repo_name = payload["repository"]["name"] project_name = payload["repository"]["project"]["name"] branch_name = payload["refChanges"][0]["refId"].split("/")[-1] commit_entries = payload["changesets"]["values"] commits = [(entry["toCommit"]["displayId"], entry["toCommit"]["message"].split("\n")[0]) for \ entry in commit_entries] head_ref = commit_entries[-1]["toCommit"]["displayId"] except KeyError as e: return json_error("Missing key %s in JSON" % (e.message,)) try: stream = request.GET['stream'] except (AttributeError, KeyError): stream = 'commits' subject = "%s/%s: %s" % (project_name, repo_name, branch_name) content = "`%s` was pushed to **%s** in **%s/%s** with:\n\n" % ( head_ref, branch_name, project_name, repo_name) content += "\n".join("* `%s`: %s" % ( commit[0], commit[1]) for commit in commits) check_send_message(user_profile, get_client("ZulipStashWebhook"), "stream", [stream], subject, content) return json_success()
def remove_subscriptions_backend(request, user_profile, streams_raw = REQ("subscriptions", validator=check_list(check_string)), principals = REQ(validator=check_list(check_string), default=None)): removing_someone_else = principals and \ set(principals) != set((user_profile.email,)) if removing_someone_else and not user_profile.is_realm_admin: # You can only unsubscribe other people from a stream if you are a realm # admin. return json_error("This action requires administrative rights") streams, _ = list_to_streams(streams_raw, user_profile) for stream in streams: if removing_someone_else and stream.invite_only and \ not subscribed_to_stream(user_profile, stream): # Even as an admin, you can't remove other people from an # invite-only stream you're not on. return json_error("Cannot administer invite-only streams this way") if principals: people_to_unsub = set(principal_to_user_profile( user_profile, principal) for principal in principals) else: people_to_unsub = set([user_profile]) result = dict(removed=[], not_subscribed=[]) # type: Dict[str, List[str]] (removed, not_subscribed) = bulk_remove_subscriptions(people_to_unsub, streams) for (subscriber, stream) in removed: result["removed"].append(stream.name) for (subscriber, stream) in not_subscribed: result["not_subscribed"].append(stream.name) return json_success(result)
def create_user_backend(request: HttpRequest, user_profile: UserProfile, email: str=REQ(), password: str=REQ(), full_name_raw: str=REQ("full_name"), short_name: str=REQ()) -> HttpResponse: full_name = check_full_name(full_name_raw) form = CreateUserForm({'full_name': full_name, 'email': email}) if not form.is_valid(): return json_error(_('Bad name or username')) # Check that the new user's email address belongs to the admin's realm # (Since this is an admin API, we don't require the user to have been # invited first.) realm = user_profile.realm try: email_allowed_for_realm(email, user_profile.realm) except DomainNotAllowedForRealmError: return json_error(_("Email '%(email)s' not allowed in this organization") % {'email': email}) except DisposableEmailError: return json_error(_("Disposable email addresses are not allowed in this organization")) except EmailContainsPlusError: return json_error(_("Email addresses containing + are not allowed.")) try: get_user_by_delivery_email(email, user_profile.realm) return json_error(_("Email '%s' already in use") % (email,)) except UserProfile.DoesNotExist: pass do_create_user(email, password, realm, full_name, short_name) return json_success()
def create_realm_custom_profile_field(request: HttpRequest, user_profile: UserProfile, name: str=REQ(), hint: str=REQ(default=''), field_data: ProfileFieldData=REQ(default={}, converter=ujson.loads), field_type: int=REQ(validator=check_int)) -> HttpResponse: validate_field_name_and_hint(name, hint) field_types = [i[0] for i in CustomProfileField.FIELD_TYPE_CHOICES] if field_type not in field_types: return json_error(_("Invalid field type.")) # Choice type field must have at least have one choice if field_type == CustomProfileField.CHOICE and len(field_data) < 1: return json_error(_("Field must have at least one choice.")) error = validate_field_data(field_data) if error: return json_error(error) try: field = try_add_realm_custom_profile_field( realm=user_profile.realm, name=name, field_data=field_data, field_type=field_type, hint=hint, ) return json_success({'id': field.id}) except IntegrityError: return json_error(_("A field with that name already exists."))
def invite_users_backend(request: HttpRequest, user_profile: UserProfile, invitee_emails_raw: str=REQ("invitee_emails"), invite_as_admin: Optional[bool]=REQ(validator=check_bool, default=False), ) -> HttpResponse: if user_profile.realm.invite_by_admins_only and not user_profile.is_realm_admin: return json_error(_("Must be an organization administrator")) if invite_as_admin and not user_profile.is_realm_admin: return json_error(_("Must be an organization administrator")) if not invitee_emails_raw: return json_error(_("You must specify at least one email address.")) invitee_emails = get_invitee_emails_set(invitee_emails_raw) stream_names = request.POST.getlist('stream') if not stream_names: return json_error(_("You must specify at least one stream for invitees to join.")) # We unconditionally sub you to the notifications stream if it # exists and is public. notifications_stream = user_profile.realm.notifications_stream # type: Optional[Stream] if notifications_stream and not notifications_stream.invite_only: stream_names.append(notifications_stream.name) streams = [] # type: List[Stream] for stream_name in stream_names: try: (stream, recipient, sub) = access_stream_by_name(user_profile, stream_name) except JsonableError: return json_error(_("Stream does not exist: %s. No invites were sent.") % (stream_name,)) streams.append(stream) do_invite_users(user_profile, invitee_emails, streams, invite_as_admin) return json_success()
def _wrapped_view_func(request, *args, **kwargs): # First try block attempts to get the credentials we need to do authentication try: # Grab the base64-encoded authentication string, decode it, and split it into # the email and API key auth_type, encoded_value = request.META['HTTP_AUTHORIZATION'].split() # case insensitive per RFC 1945 if auth_type.lower() != "basic": return json_error("Only Basic authentication is supported.") role, api_key = base64.b64decode(encoded_value).split(":") except ValueError: return json_error("Invalid authorization header for basic auth") except KeyError: return json_unauthorized("Missing authorization header for basic auth") # Now we try to do authentication or die try: # Could be a UserProfile or a Deployment profile = validate_api_key(role, api_key) except JsonableError as e: return json_unauthorized(e.error) request.user = profile process_client(request, profile) if isinstance(profile, UserProfile): request._email = profile.email else: request._email = "deployment:" + role profile.rate_limits = "" # Apply rate limiting return rate_limit()(view_func)(request, profile, *args, **kwargs)
def json_invite_users(request, user_profile, invitee_emails_raw=REQ("invitee_emails")): # type: (HttpRequest, UserProfile, str) -> HttpResponse if not invitee_emails_raw: return json_error(_("You must specify at least one email address.")) invitee_emails = get_invitee_emails_set(invitee_emails_raw) stream_names = request.POST.getlist('stream') if not stream_names: return json_error(_("You must specify at least one stream for invitees to join.")) # We unconditionally sub you to the notifications stream if it # exists and is public. notifications_stream = user_profile.realm.notifications_stream if notifications_stream and not notifications_stream.invite_only: stream_names.append(notifications_stream.name) streams = [] # type: List[Stream] for stream_name in stream_names: stream = get_stream(stream_name, user_profile.realm) if stream is None: return json_error(_("Stream does not exist: %s. No invites were sent.") % (stream_name,)) streams.append(stream) ret_error, error_data = do_invite_users(user_profile, invitee_emails, streams) if ret_error is not None: return json_error(data=error_data, msg=ret_error) else: return json_success()
def api_dev_fetch_api_key(request: HttpRequest, username: str=REQ()) -> HttpResponse: """This function allows logging in without a password on the Zulip mobile apps when connecting to a Zulip development environment. It requires DevAuthBackend to be included in settings.AUTHENTICATION_BACKENDS. """ if not dev_auth_enabled() or settings.PRODUCTION: return json_error(_("Dev environment not enabled.")) # Django invokes authenticate methods by matching arguments, and this # authentication flow will not invoke LDAP authentication because of # this condition of Django so no need to check if LDAP backend is # enabled. validate_login_email(username) subdomain = get_subdomain(request) realm = get_realm(subdomain) return_data = {} # type: Dict[str, bool] user_profile = authenticate(dev_auth_username=username, realm=realm, return_data=return_data) if return_data.get("inactive_realm"): return json_error(_("Your realm has been deactivated."), data={"reason": "realm deactivated"}, status=403) if return_data.get("inactive_user"): return json_error(_("Your account has been disabled."), data={"reason": "user disable"}, status=403) if user_profile is None: return json_error(_("This user is not registered."), data={"reason": "unregistered"}, status=403) do_login(request, user_profile) return json_success({"api_key": user_profile.api_key, "email": user_profile.email})
def process_exception(self, request, exception): if hasattr(exception, 'to_json_error_msg') and callable(exception.to_json_error_msg): return json_error(exception.to_json_error_msg()) if request.error_format == "JSON": logging.error(traceback.format_exc()) return json_error("Internal server error", status=500) return None
def add_bot_backend(request, user_profile, full_name=REQ(), short_name=REQ(), default_sending_stream_name=REQ('default_sending_stream', default=None), default_events_register_stream_name=REQ('default_events_register_stream', default=None), default_all_public_streams=REQ(validator=check_bool, default=None)): # type: (HttpRequest, UserProfile, text_type, text_type, Optional[text_type], Optional[text_type], Optional[bool]) -> HttpResponse short_name += "-bot" email = short_name + "@" + user_profile.realm.domain form = CreateUserForm({'full_name': full_name, 'email': email}) if not form.is_valid(): # We validate client-side as well return json_error(_('Bad name or username')) try: get_user_profile_by_email(email) return json_error(_("Username already in use")) except UserProfile.DoesNotExist: pass if len(request.FILES) == 0: avatar_source = UserProfile.AVATAR_FROM_GRAVATAR elif len(request.FILES) != 1: return json_error(_("You may only upload one file at a time")) else: user_file = list(request.FILES.values())[0] upload_avatar_image(user_file, user_profile, email) avatar_source = UserProfile.AVATAR_FROM_USER default_sending_stream = None if default_sending_stream_name is not None: default_sending_stream = stream_or_none(default_sending_stream_name, user_profile.realm) if default_sending_stream and not default_sending_stream.is_public() and not \ subscribed_to_stream(user_profile, default_sending_stream): return json_error(_('Insufficient permission')) default_events_register_stream = None if default_events_register_stream_name is not None: default_events_register_stream = stream_or_none(default_events_register_stream_name, user_profile.realm) if default_events_register_stream and not default_events_register_stream.is_public() and not \ subscribed_to_stream(user_profile, default_events_register_stream): return json_error(_('Insufficient permission')) bot_profile = do_create_user(email=email, password='', realm=user_profile.realm, full_name=full_name, short_name=short_name, active=True, bot_type=UserProfile.DEFAULT_BOT, bot_owner=user_profile, avatar_source=avatar_source, default_sending_stream=default_sending_stream, default_events_register_stream=default_events_register_stream, default_all_public_streams=default_all_public_streams) json_result = dict( api_key=bot_profile.api_key, avatar_url=avatar_url(bot_profile), default_sending_stream=get_stream_name(bot_profile.default_sending_stream), default_events_register_stream=get_stream_name(bot_profile.default_events_register_stream), default_all_public_streams=bot_profile.default_all_public_streams, ) return json_success(json_result)
def get_topics_backend(request, user_profile, stream_id=REQ(converter=to_non_negative_int)): # type: (HttpRequest, UserProfile, int) -> HttpResponse try: stream = Stream.objects.get(pk=stream_id) except Stream.DoesNotExist: return json_error(_("Invalid stream id")) if stream.realm_id != user_profile.realm_id: return json_error(_("Invalid stream id")) recipient = get_recipient(Recipient.STREAM, stream.id) if not stream.is_public(): if not is_active_subscriber(user_profile=user_profile, recipient=recipient): return json_error(_("Invalid stream id")) result = get_topic_history_for_stream( user_profile=user_profile, recipient=recipient, ) # Our data structure here is a list of tuples of # (topic name, unread count), and it's reverse chronological, # so the most recent topic is the first element of the list. return json_success(dict(topics=result))
def create_user_backend(request, user_profile, email=REQ(), password=REQ(), full_name=REQ(), short_name=REQ()): # type: (HttpRequest, UserProfile, text_type, text_type, text_type, text_type) -> HttpResponse form = CreateUserForm({'full_name': full_name, 'email': email}) if not form.is_valid(): return json_error(_('Bad name or username')) # Check that the new user's email address belongs to the admin's realm # (Since this is an admin API, we don't require the user to have been # invited first.) realm = user_profile.realm if not email_allowed_for_realm(email, user_profile.realm): return json_error(_("Email '%(email)s' does not belong to domain '%(domain)s'") % {'email': email, 'domain': realm.domain}) try: user_profile = get_user_profile_by_email(email) return json_error( _("Email '%s' already in use") % (email,), dict(api_key=user_profile.api_key) ) except UserProfile.DoesNotExist: pass user_profile = do_create_user(email, password, realm, full_name, short_name) return json_success(dict(api_key=user_profile.api_key))
def get_presence_backend(request: HttpRequest, user_profile: UserProfile, email: Text) -> HttpResponse: try: target = get_user(email, user_profile.realm) except UserProfile.DoesNotExist: return json_error(_('No such user')) if not target.is_active: return json_error(_('No such user')) if target.is_bot: return json_error(_('Presence is not supported for bot users.')) presence_dict = UserPresence.get_status_dict_by_user(target) if len(presence_dict) == 0: return json_error(_('No presence data for %s' % (target.email,))) # For initial version, we just include the status and timestamp keys result = dict(presence=presence_dict[target.email]) aggregated_info = result['presence']['aggregated'] aggr_status_duration = datetime_to_timestamp(timezone_now()) - aggregated_info['timestamp'] if aggr_status_duration > settings.OFFLINE_THRESHOLD_SECS: aggregated_info['status'] = 'offline' for val in result['presence'].values(): val.pop('client', None) val.pop('pushable', None) return json_success(result)
def api_zapier_webhook(request: HttpRequest, user_profile: UserProfile, payload: Dict[str, Any]=REQ(argument_type='body')) -> HttpResponse: if payload.get('type') == 'auth': # The bot's details are used by our Zapier app to format a connection # label for users to be able to distinguish between different Zulip # bots and API keys in their UI return json_success({ 'full_name': user_profile.full_name, 'email': user_profile.email, 'id': user_profile.id }) topic = payload.get('topic') content = payload.get('content') if topic is None: topic = payload.get('subject') # Backwards-compatibility if topic is None: return json_error(_("Topic can't be empty")) if content is None: return json_error(_("Content can't be empty")) check_send_webhook_message(request, user_profile, topic, content) return json_success()
def api_newrelic_webhook(request, user_profile, alert=REQ(validator=check_dict([]), default=None), deployment=REQ(validator=check_dict([]), default=None)): try: stream = request.GET['stream'] except (AttributeError, KeyError): return json_error("Missing stream parameter.") if alert: # Use the message as the subject because it stays the same for # "opened", "acknowledged", and "closed" messages that should be # grouped. subject = alert['message'] content = "%(long_description)s\n[View alert](%(alert_url)s)" % (alert) elif deployment: subject = "%s deploy" % (deployment['application_name']) content = """`%(revision)s` deployed by **%(deployed_by)s** %(description)s %(changelog)s""" % (deployment) else: return json_error("Unknown webhook request") check_send_message(user_profile, get_client("ZulipNewRelicWebhook"), "stream", [stream], subject, content) return json_success()
def api_jira_webhook(request, user_profile, client, payload=REQ(argument_type='body'), stream=REQ(default='jira')): # type: (HttpRequest, UserProfile, Client, Dict[str, Any], Text) -> HttpResponse event = payload.get('webhookEvent') if event == 'jira:issue_created': subject = get_issue_subject(payload) content = handle_created_issue_event(payload) elif event == 'jira:issue_deleted': subject = get_issue_subject(payload) content = handle_deleted_issue_event(payload) elif event == 'jira:issue_updated': subject = get_issue_subject(payload) content = handle_updated_issue_event(payload, user_profile) elif event in IGNORED_EVENTS: return json_success() else: if event is None: if not settings.TEST_SUITE: message = "Got JIRA event with None event type: {}".format(payload) logging.warning(message) return json_error(_("Event is not given by JIRA")) else: if not settings.TEST_SUITE: logging.warning("Got JIRA event type we don't support: {}".format(event)) return json_error(_("Got JIRA event type we don't support: {}".format(event))) check_send_message(user_profile, client, "stream", [stream], subject, content) return json_success()
def _wrapped_func_arguments(request, *args, **kwargs): # type: (HttpRequest, *Any, **Any) -> HttpResponse # First try block attempts to get the credentials we need to do authentication try: # Grab the base64-encoded authentication string, decode it, and split it into # the email and API key auth_type, credentials = request.META['HTTP_AUTHORIZATION'].split() # case insensitive per RFC 1945 if auth_type.lower() != "basic": return json_error(_("Only Basic authentication is supported.")) role, api_key = base64.b64decode(force_bytes(credentials)).decode('utf-8').split(":") except ValueError: json_error(_("Invalid authorization header for basic auth")) except KeyError: return json_unauthorized("Missing authorization header for basic auth") # Now we try to do authentication or die try: # Could be a UserProfile or a Deployment profile = validate_api_key(role, api_key, is_webhook) except JsonableError as e: return json_unauthorized(e.error) request.user = profile process_client(request, profile) if isinstance(profile, UserProfile): request._email = profile.email else: assert isinstance(profile, Deployment) # type: ignore # https://github.com/python/mypy/issues/1720#issuecomment-228596830 request._email = "deployment:" + role profile.rate_limits = "" # Apply rate limiting return rate_limit()(view_func)(request, profile, *args, **kwargs)
def json_invite_users(request, user_profile, invitee_emails_raw=REQ("invitee_emails"), body=REQ("custom_body", default=None)): # type: (HttpRequest, UserProfile, str, Optional[str]) -> HttpResponse if not invitee_emails_raw: return json_error(_("You must specify at least one email address.")) if body == '': body = None invitee_emails = get_invitee_emails_set(invitee_emails_raw) stream_names = request.POST.getlist('stream') if not stream_names: return json_error(_("You must specify at least one stream for invitees to join.")) # We unconditionally sub you to the notifications stream if it # exists and is public. notifications_stream = user_profile.realm.notifications_stream # type: Optional[Stream] if notifications_stream and not notifications_stream.invite_only: stream_names.append(notifications_stream.name) streams = [] # type: List[Stream] for stream_name in stream_names: try: (stream, recipient, sub) = access_stream_by_name(user_profile, stream_name) except JsonableError: return json_error(_("Stream does not exist: %s. No invites were sent.") % (stream_name,)) streams.append(stream) ret_error, error_data = do_invite_users(user_profile, invitee_emails, streams, body) if ret_error is not None: return json_error(data=error_data, msg=ret_error) else: return json_success()
def process_submessage(request: HttpRequest, user_profile: UserProfile, message_id: int=REQ(validator=check_int), msg_type: str=REQ(), content: str=REQ(), ) -> HttpResponse: message, user_message = access_message(user_profile, message_id) if not settings.ALLOW_SUB_MESSAGES: # nocoverage msg = 'Feature not enabled' return json_error(msg) try: data = ujson.loads(content) except Exception: return json_error(_("Invalid json for submessage")) do_add_submessage( sender_id=user_profile.id, message_id=message.id, msg_type=msg_type, content=content, data=data, ) return json_success()
def webathena_kerberos_login(request, user_profile, cred=REQ(default=None)): # type: (HttpRequest, UserProfile, Text) -> HttpResponse if cred is None: return json_error(_("Could not find Kerberos credential")) if not user_profile.realm.webathena_enabled: return json_error(_("Webathena login not enabled")) try: parsed_cred = ujson.loads(cred) user = parsed_cred["cname"]["nameString"][0] if user == "golem": # Hack for an mit.edu user whose Kerberos username doesn't # match what he zephyrs as user = "******" assert(user == user_profile.email.split("@")[0]) ccache = make_ccache(parsed_cred) except Exception: return json_error(_("Invalid Kerberos cache")) # TODO: Send these data via (say) rabbitmq try: subprocess.check_call(["ssh", settings.PERSONAL_ZMIRROR_SERVER, "--", "/home/zulip/zulip/bots/process_ccache", force_str(user), force_str(user_profile.api_key), force_str(base64.b64encode(ccache))]) except Exception: logging.exception("Error updating the user's ccache") return json_error(_("We were unable to setup mirroring for you")) return json_success()
def webathena_kerberos_login(request: HttpRequest, user_profile: UserProfile, cred: Text=REQ(default=None)) -> HttpResponse: global kerberos_alter_egos if cred is None: return json_error(_("Could not find Kerberos credential")) if not user_profile.realm.webathena_enabled: return json_error(_("Webathena login not enabled")) try: parsed_cred = ujson.loads(cred) user = parsed_cred["cname"]["nameString"][0] if user in kerberos_alter_egos: user = kerberos_alter_egos[user] assert(user == user_profile.email.split("@")[0]) ccache = make_ccache(parsed_cred) except Exception: return json_error(_("Invalid Kerberos cache")) # TODO: Send these data via (say) rabbitmq try: subprocess.check_call(["ssh", settings.PERSONAL_ZMIRROR_SERVER, "--", "/home/zulip/python-zulip-api/zulip/integrations/zephyr/process_ccache", force_str(user), force_str(user_profile.api_key), force_str(base64.b64encode(ccache))]) except Exception: logging.exception("Error updating the user's ccache") return json_error(_("We were unable to setup mirroring for you")) return json_success()
def downgrade(request: HttpRequest, user: UserProfile) -> HttpResponse: if not user.is_realm_admin and not user.is_billing_admin: return json_error(_('Access denied')) try: process_downgrade(user) except BillingError as e: return json_error(e.message, data={'error_description': e.description}) return json_success()
def deactivate_bot_backend(request, user_profile, email): try: target = get_user_profile_by_email(email) except UserProfile.DoesNotExist: return json_error('No such bot') if not target.is_bot: return json_error('No such bot') return _deactivate_user_profile_backend(request, user_profile, target)
def deactivate_bot_backend(request, user_profile, email): # type: (HttpRequest, UserProfile, Text) -> HttpResponse try: target = get_user_profile_by_email(email) except UserProfile.DoesNotExist: return json_error(_('No such bot')) if not target.is_bot: return json_error(_('No such bot')) return _deactivate_user_profile_backend(request, user_profile, target)
def process_exception(self, request, exception): if isinstance(exception, RateLimited): resp = json_error("API usage exceeded rate limit, try again in %s secs" % (request._ratelimit_secs_to_freedom,), status=429) resp['Retry-After'] = request._ratelimit_secs_to_freedom return resp
def csrf_failure(request, reason=""): if request.error_format == "JSON": return json_error("CSRF Error: %s" % (reason,), status=403) else: return html_csrf_failure(request, reason)
def csrf_failure(request, reason=""): # type: (HttpRequest, Optional[text_type]) -> HttpResponse if request.error_format == "JSON": return json_error(_("CSRF Error: %s") % (reason, ), status=403) else: return html_csrf_failure(request, reason)
def deactivate_user_backend(request: HttpRequest, user_profile: UserProfile, user_id: int) -> HttpResponse: target = access_user_by_id(user_profile, user_id) if check_last_admin(target): return json_error(_('Cannot deactivate the only organization administrator')) return _deactivate_user_profile_backend(request, user_profile, target)
def update_realm( request: HttpRequest, user_profile: UserProfile, name: Optional[str] = REQ(validator=check_string, default=None), description: Optional[str] = REQ(validator=check_string, default=None), emails_restricted_to_domains: Optional[bool] = REQ(validator=check_bool, default=None), disallow_disposable_email_addresses: Optional[bool] = REQ( validator=check_bool, default=None), invite_required: Optional[bool] = REQ(validator=check_bool, default=None), invite_by_admins_only: Optional[bool] = REQ(validator=check_bool, default=None), name_changes_disabled: Optional[bool] = REQ(validator=check_bool, default=None), email_changes_disabled: Optional[bool] = REQ(validator=check_bool, default=None), inline_image_preview: Optional[bool] = REQ(validator=check_bool, default=None), inline_url_embed_preview: Optional[bool] = REQ(validator=check_bool, default=None), create_stream_by_admins_only: Optional[bool] = REQ(validator=check_bool, default=None), add_emoji_by_admins_only: Optional[bool] = REQ(validator=check_bool, default=None), allow_message_deleting: Optional[bool] = REQ(validator=check_bool, default=None), message_content_delete_limit_seconds: Optional[int] = REQ( converter=to_non_negative_int, default=None), allow_message_editing: Optional[bool] = REQ(validator=check_bool, default=None), allow_community_topic_editing: Optional[bool] = REQ(validator=check_bool, default=None), mandatory_topics: Optional[bool] = REQ(validator=check_bool, default=None), message_content_edit_limit_seconds: Optional[int] = REQ( converter=to_non_negative_int, default=None), allow_edit_history: Optional[bool] = REQ(validator=check_bool, default=None), default_language: Optional[str] = REQ(validator=check_string, default=None), waiting_period_threshold: Optional[int] = REQ( converter=to_non_negative_int, default=None), authentication_methods: Optional[Dict[Any, Any]] = REQ(validator=check_dict([]), default=None), notifications_stream_id: Optional[int] = REQ(validator=check_int, default=None), signup_notifications_stream_id: Optional[int] = REQ(validator=check_int, default=None), message_retention_days: Optional[int] = REQ( converter=to_not_negative_int_or_none, default=None), send_welcome_emails: Optional[bool] = REQ(validator=check_bool, default=None), message_content_allowed_in_email_notifications: Optional[bool] = REQ( validator=check_bool, default=None), bot_creation_policy: Optional[int] = REQ( converter=to_not_negative_int_or_none, default=None), email_address_visibility: Optional[int] = REQ( converter=to_not_negative_int_or_none, default=None), default_twenty_four_hour_time: Optional[bool] = REQ(validator=check_bool, default=None), video_chat_provider: Optional[str] = REQ(validator=check_string, default=None), google_hangouts_domain: Optional[str] = REQ(validator=check_string, default=None), zoom_user_id: Optional[str] = REQ(validator=check_string, default=None), zoom_api_key: Optional[str] = REQ(validator=check_string, default=None), zoom_api_secret: Optional[str] = REQ(validator=check_string, default=None), ) -> HttpResponse: realm = user_profile.realm # Additional validation/error checking beyond types go here, so # the entire request can succeed or fail atomically. if default_language is not None and default_language not in get_available_language_codes( ): raise JsonableError(_("Invalid language '%s'" % (default_language, ))) if description is not None and len(description) > 1000: return json_error(_("Organization description is too long.")) if name is not None and len(name) > Realm.MAX_REALM_NAME_LENGTH: return json_error(_("Organization name is too long.")) if authentication_methods is not None and True not in list( authentication_methods.values()): return json_error( _("At least one authentication method must be enabled.")) if video_chat_provider == "Google Hangouts": try: validate_domain(google_hangouts_domain) except ValidationError as e: return json_error(_('Invalid domain: {}').format(e.messages[0])) if video_chat_provider == "Zoom": if not zoom_api_secret: # Use the saved Zoom API secret if a new value isn't being sent zoom_api_secret = user_profile.realm.zoom_api_secret if not zoom_user_id: return json_error(_('User ID cannot be empty')) if not zoom_api_key: return json_error(_('API key cannot be empty')) if not zoom_api_secret: return json_error(_('API secret cannot be empty')) # If any of the Zoom settings have changed, validate the Zoom credentials. # # Technically, we could call some other API endpoint that # doesn't create a video call link, but this is a nicer # end-to-end test, since it verifies that the Zoom API user's # scopes includes the ability to create video calls, which is # the only capabiility we use. if ((zoom_user_id != realm.zoom_user_id or zoom_api_key != realm.zoom_api_key or zoom_api_secret != realm.zoom_api_secret) and not request_zoom_video_call_url(zoom_user_id, zoom_api_key, zoom_api_secret)): return json_error( _('Invalid credentials for the %(third_party_service)s API.') % dict(third_party_service="Zoom")) # Additional validation of enum-style values if bot_creation_policy is not None and bot_creation_policy not in Realm.BOT_CREATION_POLICY_TYPES: return json_error(_("Invalid bot creation policy")) if email_address_visibility is not None and \ email_address_visibility not in Realm.EMAIL_ADDRESS_VISIBILITY_TYPES: return json_error(_("Invalid email address visibility policy")) # The user of `locals()` here is a bit of a code smell, but it's # restricted to the elements present in realm.property_types. # # TODO: It should be possible to deduplicate this function up # further by some more advanced usage of the # `REQ/has_request_variables` extraction. req_vars = { k: v for k, v in list(locals().items()) if k in realm.property_types } data = {} # type: Dict[str, Any] for k, v in list(req_vars.items()): if v is not None and getattr(realm, k) != v: do_set_realm_property(realm, k, v) if isinstance(v, str): data[k] = 'updated' else: data[k] = v # The following realm properties do not fit the pattern above # authentication_methods is not supported by the do_set_realm_property # framework because of its bitfield. if authentication_methods is not None and ( realm.authentication_methods_dict() != authentication_methods): do_set_realm_authentication_methods(realm, authentication_methods) data['authentication_methods'] = authentication_methods # The message_editing settings are coupled to each other, and thus don't fit # into the do_set_realm_property framework. if ((allow_message_editing is not None and realm.allow_message_editing != allow_message_editing) or (message_content_edit_limit_seconds is not None and realm.message_content_edit_limit_seconds != message_content_edit_limit_seconds) or (allow_community_topic_editing is not None and realm.allow_community_topic_editing != allow_community_topic_editing)): if allow_message_editing is None: allow_message_editing = realm.allow_message_editing if message_content_edit_limit_seconds is None: message_content_edit_limit_seconds = realm.message_content_edit_limit_seconds if allow_community_topic_editing is None: allow_community_topic_editing = realm.allow_community_topic_editing do_set_realm_message_editing(realm, allow_message_editing, message_content_edit_limit_seconds, allow_community_topic_editing) data['allow_message_editing'] = allow_message_editing data[ 'message_content_edit_limit_seconds'] = message_content_edit_limit_seconds data['allow_community_topic_editing'] = allow_community_topic_editing if (message_content_delete_limit_seconds is not None and realm.message_content_delete_limit_seconds != message_content_delete_limit_seconds): do_set_realm_message_deleting(realm, message_content_delete_limit_seconds) data[ 'message_content_delete_limit_seconds'] = message_content_delete_limit_seconds # Realm.notifications_stream and Realm.signup_notifications_stream are not boolean, # str or integer field, and thus doesn't fit into the do_set_realm_property framework. if notifications_stream_id is not None: if realm.notifications_stream is None or (realm.notifications_stream.id != notifications_stream_id): new_notifications_stream = None if notifications_stream_id >= 0: (new_notifications_stream, recipient, sub) = access_stream_by_id(user_profile, notifications_stream_id) do_set_realm_notifications_stream(realm, new_notifications_stream, notifications_stream_id) data['notifications_stream_id'] = notifications_stream_id if signup_notifications_stream_id is not None: if realm.signup_notifications_stream is None or ( realm.signup_notifications_stream.id != signup_notifications_stream_id): new_signup_notifications_stream = None if signup_notifications_stream_id >= 0: (new_signup_notifications_stream, recipient, sub) = access_stream_by_id(user_profile, signup_notifications_stream_id) do_set_realm_signup_notifications_stream( realm, new_signup_notifications_stream, signup_notifications_stream_id) data[ 'signup_notifications_stream_id'] = signup_notifications_stream_id return json_success(data)
def add_subscriptions_backend( request: HttpRequest, user_profile: UserProfile, streams_raw: Iterable[Dict[str, str]] = REQ( "subscriptions", validator=check_list( check_dict_only([('name', check_string)], optional_keys=[ ('color', check_color), ('description', check_capped_string( Stream.MAX_DESCRIPTION_LENGTH)), ]), )), invite_only: bool = REQ(validator=check_bool, default=False), stream_post_policy: int = REQ(validator=check_int_in( Stream.STREAM_POST_POLICY_TYPES), default=Stream.STREAM_POST_POLICY_EVERYONE), history_public_to_subscribers: Optional[bool] = REQ(validator=check_bool, default=None), message_retention_days: Union[str, int] = REQ(validator=check_string_or_int, default="realm_default"), announce: bool = REQ(validator=check_bool, default=False), principals: Sequence[Union[str, int]] = REQ(validator=check_variable_type( [check_list(check_string), check_list(check_int)]), default=[]), authorization_errors_fatal: bool = REQ(validator=check_bool, default=True), ) -> HttpResponse: stream_dicts = [] color_map = {} for stream_dict in streams_raw: # 'color' field is optional # check for its presence in the streams_raw first if 'color' in stream_dict: color_map[stream_dict['name']] = stream_dict['color'] if 'description' in stream_dict: # We don't allow newline characters in stream descriptions. stream_dict['description'] = stream_dict['description'].replace( "\n", " ") stream_dict_copy: Dict[str, Any] = {} for field in stream_dict: stream_dict_copy[field] = stream_dict[field] # Strip the stream name here. stream_dict_copy['name'] = stream_dict_copy['name'].strip() stream_dict_copy["invite_only"] = invite_only stream_dict_copy["stream_post_policy"] = stream_post_policy stream_dict_copy[ "history_public_to_subscribers"] = history_public_to_subscribers stream_dict_copy[ "message_retention_days"] = parse_message_retention_days( message_retention_days) stream_dicts.append(stream_dict_copy) # Validation of the streams arguments, including enforcement of # can_create_streams policy and check_stream_name policy is inside # list_to_streams. existing_streams, created_streams = \ list_to_streams(stream_dicts, user_profile, autocreate=True) authorized_streams, unauthorized_streams = \ filter_stream_authorization(user_profile, existing_streams) if len(unauthorized_streams) > 0 and authorization_errors_fatal: return json_error( _("Unable to access stream ({stream_name}).").format( stream_name=unauthorized_streams[0].name, )) # Newly created streams are also authorized for the creator streams = authorized_streams + created_streams if len(principals) > 0: if user_profile.realm.is_zephyr_mirror_realm and not all( stream.invite_only for stream in streams): return json_error( _("You can only invite other Zephyr mirroring users to private streams." )) if not user_profile.can_subscribe_other_users(): if user_profile.realm.invite_to_stream_policy == Realm.POLICY_ADMINS_ONLY: return json_error( _("Only administrators can modify other users' subscriptions." )) # Realm.POLICY_MEMBERS_ONLY only fails if the # user is a guest, which happens in the decorator above. assert user_profile.realm.invite_to_stream_policy == \ Realm.POLICY_FULL_MEMBERS_ONLY return json_error( _("Your account is too new to modify other users' subscriptions." )) subscribers = { principal_to_user_profile(user_profile, principal) for principal in principals } else: subscribers = {user_profile} (subscribed, already_subscribed) = bulk_add_subscriptions(streams, subscribers, acting_user=user_profile, color_map=color_map) # We can assume unique emails here for now, but we should eventually # convert this function to be more id-centric. email_to_user_profile: Dict[str, UserProfile] = dict() result: Dict[str, Any] = dict(subscribed=defaultdict(list), already_subscribed=defaultdict(list)) for (subscriber, stream) in subscribed: result["subscribed"][subscriber.email].append(stream.name) email_to_user_profile[subscriber.email] = subscriber for (subscriber, stream) in already_subscribed: result["already_subscribed"][subscriber.email].append(stream.name) bots = {subscriber.email: subscriber.is_bot for subscriber in subscribers} newly_created_stream_names = {s.name for s in created_streams} # Inform the user if someone else subscribed them to stuff, # or if a new stream was created with the "announce" option. notifications = [] if len(principals) > 0 and result["subscribed"]: for email, subscribed_stream_names in result["subscribed"].items(): if email == user_profile.email: # Don't send a Zulip if you invited yourself. continue if bots[email]: # Don't send invitation Zulips to bots continue # For each user, we notify them about newly subscribed streams, except for # streams that were newly created. notify_stream_names = set( subscribed_stream_names) - newly_created_stream_names if not notify_stream_names: continue msg = you_were_just_subscribed_message( acting_user=user_profile, stream_names=notify_stream_names, ) sender = get_system_bot(settings.NOTIFICATION_BOT) notifications.append( internal_prep_private_message( realm=user_profile.realm, sender=sender, recipient_user=email_to_user_profile[email], content=msg)) if announce and len(created_streams) > 0: notifications_stream = user_profile.realm.get_notifications_stream() if notifications_stream is not None: if len(created_streams) > 1: content = _( "@_**%(user_name)s|%(user_id)d** created the following streams: %(stream_str)s." ) else: content = _( "@_**%(user_name)s|%(user_id)d** created a new stream %(stream_str)s." ) content = content % { 'user_name': user_profile.full_name, 'user_id': user_profile.id, 'stream_str': ", ".join(f'#**{s.name}**' for s in created_streams) } sender = get_system_bot(settings.NOTIFICATION_BOT) topic = _('new streams') notifications.append( internal_prep_stream_message( realm=user_profile.realm, sender=sender, stream=notifications_stream, topic=topic, content=content, ), ) if not user_profile.realm.is_zephyr_mirror_realm and len( created_streams) > 0: sender = get_system_bot(settings.NOTIFICATION_BOT) for stream in created_streams: notifications.append( internal_prep_stream_message( realm=user_profile.realm, sender=sender, stream=stream, topic=Realm.STREAM_EVENTS_NOTIFICATION_TOPIC, content=_('Stream created by @_**{user_name}|{user_id}**.' ).format( user_name=user_profile.full_name, user_id=user_profile.id, ), ), ) if len(notifications) > 0: do_send_messages(notifications, mark_as_read=[user_profile.id]) result["subscribed"] = dict(result["subscribed"]) result["already_subscribed"] = dict(result["already_subscribed"]) if not authorization_errors_fatal: result["unauthorized"] = [s.name for s in unauthorized_streams] return json_success(result)
def api_fetch_google_client_id(request): # type: (HttpRequest) -> HttpResponse if not settings.GOOGLE_CLIENT_ID: return json_error(_("GOOGLE_CLIENT_ID is not configured"), status=400) return json_success({"google_client_id": settings.GOOGLE_CLIENT_ID})
def api_stripe_webhook(request, user_profile, payload=REQ(argument_type='body'), stream=REQ(default='test'), topic=REQ(default=None)): # type: (HttpRequest, UserProfile, Dict[str, Any], Text, Optional[Text]) -> HttpResponse body = None event_type = payload["type"] data_object = payload["data"]["object"] if event_type.startswith('charge'): charge_url = "https://dashboard.stripe.com/payments/{}" amount_string = amount(payload["data"]["object"]["amount"], payload["data"]["object"]["currency"]) if event_type.startswith('charge.dispute'): charge_id = data_object["charge"] link = charge_url.format(charge_id) body_template = "A charge dispute for **{amount}** has been {rest}.\n"\ "The charge in dispute {verb} **[{charge}]({link})**." if event_type == "charge.dispute.closed": rest = "closed as **{}**".format(data_object['status']) verb = 'was' else: rest = "created" verb = 'is' body = body_template.format(amount=amount_string, rest=rest, verb=verb, charge=charge_id, link=link) else: charge_id = data_object["id"] link = charge_url.format(charge_id) body_template = "A charge with id **[{charge_id}]({link})** for **{amount}** has {verb}." if event_type == "charge.failed": verb = "failed" else: verb = "succeeded" body = body_template.format(charge_id=charge_id, link=link, amount=amount_string, verb=verb) if topic is None: topic = "Charge {}".format(charge_id) elif event_type.startswith('customer'): object_id = data_object["id"] if event_type.startswith('customer.subscription'): link = "https://dashboard.stripe.com/subscriptions/{}".format( object_id) if event_type == "customer.subscription.created": amount_string = amount(data_object["plan"]["amount"], data_object["plan"]["currency"]) body_template = "A new customer subscription for **{amount}** " \ "every **{interval}** has been created.\n" \ "The subscription has id **[{id}]({link})**." body = body_template.format( amount=amount_string, interval=data_object['plan']['interval'], id=object_id, link=link) elif event_type == "customer.subscription.deleted": body_template = "The customer subscription with id **[{id}]({link})** was deleted." body = body_template.format(id=object_id, link=link) else: # customer.subscription.trial_will_end DAY = 60 * 60 * 24 # seconds in a day # days_left should always be three according to # https://stripe.com/docs/api/python#event_types, but do the # computation just to be safe. days_left = int( (data_object["trial_end"] - time.time() + DAY // 2) // DAY) body_template = "The customer subscription trial with id **[{id}]({link})** will end in {days} days." body = body_template.format(id=object_id, link=link, days=days_left) else: link = "https://dashboard.stripe.com/customers/{}".format( object_id) body_template = "{beginning} customer with id **[{id}]({link})** {rest}." if event_type == "customer.created": beginning = "A new" if data_object["email"] is None: rest = "has been created" else: rest = "and email **{}** has been created".format( data_object['email']) else: beginning = "A" rest = "has been deleted" body = body_template.format(beginning=beginning, id=object_id, link=link, rest=rest) if topic is None: topic = "Customer {}".format(object_id) elif event_type == "invoice.payment_failed": object_id = data_object['id'] link = "https://dashboard.stripe.com/invoices/{}".format(object_id) amount_string = amount(data_object["amount_due"], data_object["currency"]) body_template = "An invoice payment on invoice with id **[{id}]({link})** and "\ "with **{amount}** due has failed." body = body_template.format(id=object_id, amount=amount_string, link=link) if topic is None: topic = "Invoice {}".format(object_id) elif event_type.startswith('order'): object_id = data_object['id'] link = "https://dashboard.stripe.com/orders/{}".format(object_id) amount_string = amount(data_object["amount"], data_object["currency"]) body_template = "{beginning} order with id **[{id}]({link})** for **{amount}** has {end}." if event_type == "order.payment_failed": beginning = "An order payment on" end = "failed" elif event_type == "order.payment_succeeded": beginning = "An order payment on" end = "succeeded" else: beginning = "The" end = "been updated" body = body_template.format(beginning=beginning, id=object_id, link=link, amount=amount_string, end=end) if topic is None: topic = "Order {}".format(object_id) elif event_type.startswith('transfer'): object_id = data_object['id'] link = "https://dashboard.stripe.com/transfers/{}".format(object_id) amount_string = amount(data_object["amount"], data_object["currency"]) body_template = "The transfer with description **{description}** and id **[{id}]({link})** " \ "for amount **{amount}** has {end}." if event_type == "transfer.failed": end = 'failed' else: end = "been paid" body = body_template.format(description=data_object['description'], id=object_id, link=link, amount=amount_string, end=end) if topic is None: topic = "Transfer {}".format(object_id) if body is None: return json_error(_("We don't support {} event".format(event_type))) check_send_stream_message(user_profile, request.client, stream, topic, body) return json_success()
def send_message_backend(request, user_profile, message_type_name=REQ('type'), message_to=REQ('to', converter=extract_recipients, default=[]), forged=REQ(default=False), subject_name=REQ('subject', lambda x: x.strip(), None), message_content=REQ('content'), domain=REQ('domain', default=None), local_id=REQ(default=None), queue_id=REQ(default=None)): # type: (HttpRequest, UserProfile, text_type, List[text_type], bool, Optional[text_type], text_type, Optional[text_type], Optional[text_type], Optional[text_type]) -> HttpResponse client = request.client is_super_user = request.user.is_api_super_user if forged and not is_super_user: return json_error(_("User not authorized for this query")) realm = None if domain and domain != user_profile.realm.domain: if not is_super_user: # The email gateway bot needs to be able to send messages in # any realm. return json_error(_("User not authorized for this query")) realm = get_realm(domain) if not realm: return json_error(_("Unknown domain %s") % (domain, )) if client.name in [ "zephyr_mirror", "irc_mirror", "jabber_mirror", "JabberMirror" ]: # Here's how security works for mirroring: # # For private messages, the message must be (1) both sent and # received exclusively by users in your realm, and (2) # received by the forwarding user. # # For stream messages, the message must be (1) being forwarded # by an API superuser for your realm and (2) being sent to a # mirrored stream (any stream for the Zephyr and Jabber # mirrors, but only streams with names starting with a "#" for # IRC mirrors) # # The security checks are split between the below code # (especially create_mirrored_message_users which checks the # same-realm constraint) and recipient_for_emails (which # checks that PMs are received by the forwarding user) if "sender" not in request.POST: return json_error(_("Missing sender")) if message_type_name != "private" and not is_super_user: return json_error(_("User not authorized for this query")) (valid_input, mirror_sender) = \ create_mirrored_message_users(request, user_profile, message_to) if not valid_input: return json_error(_("Invalid mirrored message")) if client.name == "zephyr_mirror" and not user_profile.realm.is_zephyr_mirror_realm: return json_error(_("Invalid mirrored realm")) if (client.name == "irc_mirror" and message_type_name != "private" and not message_to[0].startswith("#")): return json_error(_("IRC stream names must start with #")) sender = mirror_sender else: sender = user_profile ret = check_send_message(sender, client, message_type_name, message_to, subject_name, message_content, forged=forged, forged_timestamp=request.POST.get('time'), forwarder_user_profile=user_profile, realm=realm, local_id=local_id, sender_queue_id=queue_id) return json_success({"id": ret})
def _wrapped_view_func(request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: if request.user.is_bot: return json_error(_("This endpoint does not accept bot requests.")) return view_func(request, *args, **kwargs)
def update_message_backend(request, user_profile, message_id=REQ(converter=to_non_negative_int), subject=REQ(default=None), propagate_mode=REQ(default="change_one"), content=REQ(default=None)): # type: (HttpRequest, UserProfile, int, Optional[text_type], Optional[str], Optional[text_type]) -> HttpResponse if not user_profile.realm.allow_message_editing: return json_error( _("Your organization has turned off message editing.")) try: message = Message.objects.select_related().get(id=message_id) except Message.DoesNotExist: raise JsonableError(_("Unknown message id")) # You only have permission to edit a message if: # 1. You sent it, OR: # 2. This is a topic-only edit for a (no topic) message, OR: # 3. This is a topic-only edit and you are an admin. if message.sender == user_profile: pass elif (content is None) and ((message.topic_name() == "(no topic)") or user_profile.is_realm_admin): pass else: raise JsonableError( _("You don't have permission to edit this message")) # If there is a change to the content, check that it hasn't been too long # Allow an extra 20 seconds since we potentially allow editing 15 seconds # past the limit, and in case there are network issues, etc. The 15 comes # from (min_seconds_to_edit + seconds_left_buffer) in message_edit.js; if # you change this value also change those two parameters in message_edit.js. edit_limit_buffer = 20 if content is not None and user_profile.realm.message_content_edit_limit_seconds > 0: deadline_seconds = user_profile.realm.message_content_edit_limit_seconds + edit_limit_buffer if (now() - message.pub_date) > datetime.timedelta( seconds=deadline_seconds): raise JsonableError( _("The time limit for editing this message has past")) if subject is None and content is None: return json_error(_("Nothing to change")) if subject is not None: subject = subject.strip() if subject == "": raise JsonableError(_("Topic can't be empty")) rendered_content = None links_for_embed = set() # type: Set[text_type] if content is not None: content = content.strip() if content == "": content = "(deleted)" content = truncate_body(content) # We exclude UserMessage.flags.historical rows since those # users did not receive the message originally, and thus # probably are not relevant for reprocessed alert_words, # mentions and similar rendering features. This may be a # decision we change in the future. ums = UserMessage.objects.filter(message=message.id, flags=~UserMessage.flags.historical) message_users = { get_user_profile_by_id(um.user_profile_id) for um in ums } # If rendering fails, the called code will raise a JsonableError. rendered_content = render_incoming_message(message, content=content, message_users=message_users) links_for_embed |= message.links_for_preview do_update_message(user_profile, message, subject, propagate_mode, content, rendered_content) if links_for_embed and getattr(settings, 'INLINE_URL_EMBED_PREVIEW', None): event_data = { 'message_id': message.id, 'message_content': message.content, 'urls': links_for_embed } queue_json_publish('embed_links', event_data, lambda x: None) return json_success()
def patch_bot_backend( request: HttpRequest, user_profile: UserProfile, bot_id: int, full_name: Optional[str]=REQ(default=None), bot_owner_id: Optional[int]=REQ(validator=check_int, default=None), config_data: Optional[Dict[str, str]]=REQ(default=None, validator=check_dict(value_validator=check_string)), service_payload_url: Optional[str]=REQ(validator=check_url, default=None), service_interface: Optional[int]=REQ(validator=check_int, default=1), default_sending_stream: Optional[str]=REQ(default=None), default_events_register_stream: Optional[str]=REQ(default=None), default_all_public_streams: Optional[bool]=REQ(default=None, validator=check_bool) ) -> HttpResponse: bot = access_bot_by_id(user_profile, bot_id) if full_name is not None: check_change_bot_full_name(bot, full_name, user_profile) if bot_owner_id is not None: try: owner = get_user_profile_by_id_in_realm(bot_owner_id, user_profile.realm) except UserProfile.DoesNotExist: return json_error(_('Failed to change owner, no such user')) if not owner.is_active: return json_error(_('Failed to change owner, user is deactivated')) if owner.is_bot: return json_error(_("Failed to change owner, bots can't own other bots")) previous_owner = bot.bot_owner if previous_owner != owner: do_change_bot_owner(bot, owner, user_profile) if default_sending_stream is not None: if default_sending_stream == "": stream = None # type: Optional[Stream] else: (stream, recipient, sub) = access_stream_by_name( user_profile, default_sending_stream) do_change_default_sending_stream(bot, stream) if default_events_register_stream is not None: if default_events_register_stream == "": stream = None else: (stream, recipient, sub) = access_stream_by_name( user_profile, default_events_register_stream) do_change_default_events_register_stream(bot, stream) if default_all_public_streams is not None: do_change_default_all_public_streams(bot, default_all_public_streams) if service_payload_url is not None: check_valid_interface_type(service_interface) assert service_interface is not None do_update_outgoing_webhook_service(bot, service_interface, service_payload_url) if config_data is not None: do_update_bot_config_data(bot, config_data) if len(request.FILES) == 0: pass elif len(request.FILES) == 1: user_file = list(request.FILES.values())[0] upload_avatar_image(user_file, user_profile, bot) avatar_source = UserProfile.AVATAR_FROM_USER do_change_avatar_fields(bot, avatar_source) else: return json_error(_("You may only upload one file at a time")) json_result = dict( full_name=bot.full_name, avatar_url=avatar_url(bot), service_interface = service_interface, service_payload_url = service_payload_url, config_data = config_data, default_sending_stream=get_stream_name(bot.default_sending_stream), default_events_register_stream=get_stream_name(bot.default_events_register_stream), default_all_public_streams=bot.default_all_public_streams, ) # Don't include the bot owner in case it is not set. # Default bots have no owner. if bot.bot_owner is not None: json_result['bot_owner'] = bot.bot_owner.email return json_success(json_result)
def add_bot_backend( request: HttpRequest, user_profile: UserProfile, full_name_raw: str=REQ("full_name"), short_name_raw: str=REQ("short_name"), bot_type: int=REQ(validator=check_int, default=UserProfile.DEFAULT_BOT), payload_url: Optional[str]=REQ(validator=check_url, default=""), service_name: Optional[str]=REQ(default=None), config_data: Dict[str, str]=REQ(default={}, validator=check_dict(value_validator=check_string)), interface_type: int=REQ(validator=check_int, default=Service.GENERIC), default_sending_stream_name: Optional[str]=REQ('default_sending_stream', default=None), default_events_register_stream_name: Optional[str]=REQ('default_events_register_stream', default=None), default_all_public_streams: Optional[bool]=REQ(validator=check_bool, default=None) ) -> HttpResponse: short_name = check_short_name(short_name_raw) if bot_type != UserProfile.INCOMING_WEBHOOK_BOT: service_name = service_name or short_name short_name += "-bot" full_name = check_full_name(full_name_raw) try: email = '%s@%s' % (short_name, user_profile.realm.get_bot_domain()) except InvalidFakeEmailDomain: return json_error(_("Can't create bots until FAKE_EMAIL_DOMAIN is correctly configured.\n" "Please contact your server administrator.")) form = CreateUserForm({'full_name': full_name, 'email': email}) if bot_type == UserProfile.EMBEDDED_BOT: if not settings.EMBEDDED_BOTS_ENABLED: return json_error(_("Embedded bots are not enabled.")) if service_name not in [bot.name for bot in EMBEDDED_BOTS]: return json_error(_("Invalid embedded bot name.")) if not form.is_valid(): # We validate client-side as well return json_error(_('Bad name or username')) try: get_user_by_delivery_email(email, user_profile.realm) return json_error(_("Username already in use")) except UserProfile.DoesNotExist: pass check_bot_name_available( realm_id=user_profile.realm_id, full_name=full_name, ) check_bot_creation_policy(user_profile, bot_type) check_valid_bot_type(user_profile, bot_type) check_valid_interface_type(interface_type) if len(request.FILES) == 0: avatar_source = UserProfile.AVATAR_FROM_GRAVATAR elif len(request.FILES) != 1: return json_error(_("You may only upload one file at a time")) else: avatar_source = UserProfile.AVATAR_FROM_USER default_sending_stream = None if default_sending_stream_name is not None: (default_sending_stream, ignored_rec, ignored_sub) = access_stream_by_name( user_profile, default_sending_stream_name) default_events_register_stream = None if default_events_register_stream_name is not None: (default_events_register_stream, ignored_rec, ignored_sub) = access_stream_by_name( user_profile, default_events_register_stream_name) if bot_type in (UserProfile.INCOMING_WEBHOOK_BOT, UserProfile.EMBEDDED_BOT) and service_name: check_valid_bot_config(bot_type, service_name, config_data) bot_profile = do_create_user(email=email, password=None, realm=user_profile.realm, full_name=full_name, short_name=short_name, bot_type=bot_type, bot_owner=user_profile, avatar_source=avatar_source, default_sending_stream=default_sending_stream, default_events_register_stream=default_events_register_stream, default_all_public_streams=default_all_public_streams) if len(request.FILES) == 1: user_file = list(request.FILES.values())[0] upload_avatar_image(user_file, user_profile, bot_profile) if bot_type in (UserProfile.OUTGOING_WEBHOOK_BOT, UserProfile.EMBEDDED_BOT): assert(isinstance(service_name, str)) add_service(name=service_name, user_profile=bot_profile, base_url=payload_url, interface=interface_type, token=generate_api_key()) if bot_type == UserProfile.INCOMING_WEBHOOK_BOT and service_name: set_bot_config(bot_profile, "integration_id", service_name) if bot_type in (UserProfile.INCOMING_WEBHOOK_BOT, UserProfile.EMBEDDED_BOT): for key, value in config_data.items(): set_bot_config(bot_profile, key, value) notify_created_bot(bot_profile) api_key = get_api_key(bot_profile) json_result = dict( api_key=api_key, avatar_url=avatar_url(bot_profile), default_sending_stream=get_stream_name(bot_profile.default_sending_stream), default_events_register_stream=get_stream_name(bot_profile.default_events_register_stream), default_all_public_streams=bot_profile.default_all_public_streams, ) return json_success(json_result)
def update_realm( request: HttpRequest, user_profile: UserProfile, name: Optional[str] = REQ( str_validator=check_capped_string(Realm.MAX_REALM_NAME_LENGTH), default=None ), description: Optional[str] = REQ( str_validator=check_capped_string(Realm.MAX_REALM_DESCRIPTION_LENGTH), default=None ), emails_restricted_to_domains: Optional[bool] = REQ(json_validator=check_bool, default=None), disallow_disposable_email_addresses: Optional[bool] = REQ( json_validator=check_bool, default=None ), invite_required: Optional[bool] = REQ(json_validator=check_bool, default=None), invite_to_realm_policy: Optional[int] = REQ( json_validator=check_int_in(Realm.COMMON_POLICY_TYPES), default=None ), name_changes_disabled: Optional[bool] = REQ(json_validator=check_bool, default=None), email_changes_disabled: Optional[bool] = REQ(json_validator=check_bool, default=None), avatar_changes_disabled: Optional[bool] = REQ(json_validator=check_bool, default=None), inline_image_preview: Optional[bool] = REQ(json_validator=check_bool, default=None), inline_url_embed_preview: Optional[bool] = REQ(json_validator=check_bool, default=None), add_emoji_by_admins_only: Optional[bool] = REQ(json_validator=check_bool, default=None), allow_message_deleting: Optional[bool] = REQ(json_validator=check_bool, default=None), message_content_delete_limit_seconds: Optional[int] = REQ( converter=to_non_negative_int, default=None ), allow_message_editing: Optional[bool] = REQ(json_validator=check_bool, default=None), allow_community_topic_editing: Optional[bool] = REQ(json_validator=check_bool, default=None), mandatory_topics: Optional[bool] = REQ(json_validator=check_bool, default=None), message_content_edit_limit_seconds: Optional[int] = REQ( converter=to_non_negative_int, default=None ), allow_edit_history: Optional[bool] = REQ(json_validator=check_bool, default=None), default_language: Optional[str] = REQ(default=None), waiting_period_threshold: Optional[int] = REQ(converter=to_non_negative_int, default=None), authentication_methods: Optional[Dict[str, Any]] = REQ( json_validator=check_dict([]), default=None ), notifications_stream_id: Optional[int] = REQ(json_validator=check_int, default=None), signup_notifications_stream_id: Optional[int] = REQ(json_validator=check_int, default=None), message_retention_days_raw: Optional[Union[int, str]] = REQ( "message_retention_days", json_validator=check_string_or_int, default=None ), send_welcome_emails: Optional[bool] = REQ(json_validator=check_bool, default=None), digest_emails_enabled: Optional[bool] = REQ(json_validator=check_bool, default=None), message_content_allowed_in_email_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None ), bot_creation_policy: Optional[int] = REQ( json_validator=check_int_in(Realm.BOT_CREATION_POLICY_TYPES), default=None ), create_stream_policy: Optional[int] = REQ( json_validator=check_int_in(Realm.COMMON_POLICY_TYPES), default=None ), invite_to_stream_policy: Optional[int] = REQ( json_validator=check_int_in(Realm.COMMON_POLICY_TYPES), default=None ), user_group_edit_policy: Optional[int] = REQ( json_validator=check_int_in(Realm.USER_GROUP_EDIT_POLICY_TYPES), default=None ), private_message_policy: Optional[int] = REQ( json_validator=check_int_in(Realm.PRIVATE_MESSAGE_POLICY_TYPES), default=None ), wildcard_mention_policy: Optional[int] = REQ( json_validator=check_int_in(Realm.WILDCARD_MENTION_POLICY_TYPES), default=None ), email_address_visibility: Optional[int] = REQ( json_validator=check_int_in(Realm.EMAIL_ADDRESS_VISIBILITY_TYPES), default=None ), default_twenty_four_hour_time: Optional[bool] = REQ(json_validator=check_bool, default=None), video_chat_provider: Optional[int] = REQ(json_validator=check_int, default=None), giphy_rating: Optional[int] = REQ(json_validator=check_int, default=None), default_code_block_language: Optional[str] = REQ(default=None), digest_weekday: Optional[int] = REQ( json_validator=check_int_in(Realm.DIGEST_WEEKDAY_VALUES), default=None ), ) -> HttpResponse: realm = user_profile.realm # Additional validation/error checking beyond types go here, so # the entire request can succeed or fail atomically. if default_language is not None and default_language not in get_available_language_codes(): raise JsonableError(_("Invalid language '{}'").format(default_language)) if authentication_methods is not None: if not user_profile.is_realm_owner: raise OrganizationOwnerRequired() if True not in list(authentication_methods.values()): return json_error(_("At least one authentication method must be enabled.")) if video_chat_provider is not None and video_chat_provider not in { p["id"] for p in Realm.VIDEO_CHAT_PROVIDERS.values() }: return json_error(_("Invalid video_chat_provider {}").format(video_chat_provider)) if giphy_rating is not None and giphy_rating not in { p["id"] for p in Realm.GIPHY_RATING_OPTIONS.values() }: return json_error(_("Invalid giphy_rating {}").format(giphy_rating)) message_retention_days: Optional[int] = None if message_retention_days_raw is not None: if not user_profile.is_realm_owner: raise OrganizationOwnerRequired() realm.ensure_not_on_limited_plan() message_retention_days = parse_message_retention_days( message_retention_days_raw, Realm.MESSAGE_RETENTION_SPECIAL_VALUES_MAP ) # The user of `locals()` here is a bit of a code smell, but it's # restricted to the elements present in realm.property_types. # # TODO: It should be possible to deduplicate this function up # further by some more advanced usage of the # `REQ/has_request_variables` extraction. req_vars = {k: v for k, v in list(locals().items()) if k in realm.property_types} data: Dict[str, Any] = {} for k, v in list(req_vars.items()): if v is not None and getattr(realm, k) != v: do_set_realm_property(realm, k, v, acting_user=user_profile) if isinstance(v, str): data[k] = "updated" else: data[k] = v # The following realm properties do not fit the pattern above # authentication_methods is not supported by the do_set_realm_property # framework because of its bitfield. if authentication_methods is not None and ( realm.authentication_methods_dict() != authentication_methods ): do_set_realm_authentication_methods(realm, authentication_methods, acting_user=user_profile) data["authentication_methods"] = authentication_methods # The message_editing settings are coupled to each other, and thus don't fit # into the do_set_realm_property framework. if ( (allow_message_editing is not None and realm.allow_message_editing != allow_message_editing) or ( message_content_edit_limit_seconds is not None and realm.message_content_edit_limit_seconds != message_content_edit_limit_seconds ) or ( allow_community_topic_editing is not None and realm.allow_community_topic_editing != allow_community_topic_editing ) ): if allow_message_editing is None: allow_message_editing = realm.allow_message_editing if message_content_edit_limit_seconds is None: message_content_edit_limit_seconds = realm.message_content_edit_limit_seconds if allow_community_topic_editing is None: allow_community_topic_editing = realm.allow_community_topic_editing do_set_realm_message_editing( realm, allow_message_editing, message_content_edit_limit_seconds, allow_community_topic_editing, acting_user=user_profile, ) data["allow_message_editing"] = allow_message_editing data["message_content_edit_limit_seconds"] = message_content_edit_limit_seconds data["allow_community_topic_editing"] = allow_community_topic_editing # Realm.notifications_stream and Realm.signup_notifications_stream are not boolean, # str or integer field, and thus doesn't fit into the do_set_realm_property framework. if notifications_stream_id is not None: if realm.notifications_stream is None or ( realm.notifications_stream.id != notifications_stream_id ): new_notifications_stream = None if notifications_stream_id >= 0: (new_notifications_stream, sub) = access_stream_by_id( user_profile, notifications_stream_id ) do_set_realm_notifications_stream( realm, new_notifications_stream, notifications_stream_id, acting_user=user_profile ) data["notifications_stream_id"] = notifications_stream_id if signup_notifications_stream_id is not None: if realm.signup_notifications_stream is None or ( realm.signup_notifications_stream.id != signup_notifications_stream_id ): new_signup_notifications_stream = None if signup_notifications_stream_id >= 0: (new_signup_notifications_stream, sub) = access_stream_by_id( user_profile, signup_notifications_stream_id ) do_set_realm_signup_notifications_stream( realm, new_signup_notifications_stream, signup_notifications_stream_id, acting_user=user_profile, ) data["signup_notifications_stream_id"] = signup_notifications_stream_id if default_code_block_language is not None: # Migrate '', used in the API to encode the default/None behavior of this feature. if default_code_block_language == "": data["default_code_block_language"] = None else: data["default_code_block_language"] = default_code_block_language return json_success(data)
def update_realm( request, user_profile, name=REQ(validator=check_string, default=None), description=REQ(validator=check_string, default=None), restricted_to_domain=REQ(validator=check_bool, default=None), invite_required=REQ(validator=check_bool, default=None), invite_by_admins_only=REQ(validator=check_bool, default=None), name_changes_disabled=REQ(validator=check_bool, default=None), email_changes_disabled=REQ(validator=check_bool, default=None), inline_image_preview=REQ(validator=check_bool, default=None), inline_url_embed_preview=REQ(validator=check_bool, default=None), create_stream_by_admins_only=REQ(validator=check_bool, default=None), add_emoji_by_admins_only=REQ(validator=check_bool, default=None), allow_message_editing=REQ(validator=check_bool, default=None), message_content_edit_limit_seconds=REQ(converter=to_non_negative_int, default=None), default_language=REQ(validator=check_string, default=None), waiting_period_threshold=REQ(converter=to_non_negative_int, default=None), authentication_methods=REQ(validator=check_dict([]), default=None), message_retention_days=REQ(converter=to_not_negative_int_or_none, default=None)): # type: (HttpRequest, UserProfile, Optional[str], Optional[str], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[int], Optional[str], Optional[int], Optional[dict], Optional[int]) -> HttpResponse realm = user_profile.realm # Additional validation/error checking beyond types go here, so # the entire request can succeed or fail atomically. if default_language is not None and default_language not in get_available_language_codes( ): raise JsonableError(_("Invalid language '%s'" % (default_language, ))) if description is not None and len(description) > 100: return json_error(_("Realm description cannot exceed 100 characters.")) if authentication_methods is not None and True not in list( authentication_methods.values()): return json_error( _("At least one authentication method must be enabled."), data={"reason": "no authentication"}, status=403) # The user of `locals()` here is a bit of a code smell, but it's # restricted to the elements present in realm.property_types. # # TODO: It should be possible to deduplicate this function up # further by some more advanced usage of the # `REQ/has_request_variables` extraction. req_vars = { k: v for k, v in list(locals().items()) if k in realm.property_types } data = {} # type: Dict[str, Any] for k, v in list(req_vars.items()): if v is not None and getattr(realm, k) != v: do_set_realm_property(realm, k, v) if isinstance(v, Text): data[k] = 'updated' else: data[k] = v # The following realm properties do not fit the pattern above # authentication_methods is not supported by the do_set_realm_property # framework because of its bitfield. if authentication_methods is not None and realm.authentication_methods_dict( ) != authentication_methods: do_set_realm_authentication_methods(realm, authentication_methods) data['authentication_methods'] = authentication_methods # The message_editing settings are coupled to each other, and thus don't fit # into the do_set_realm_property framework. if (allow_message_editing is not None and realm.allow_message_editing != allow_message_editing) or \ (message_content_edit_limit_seconds is not None and realm.message_content_edit_limit_seconds != message_content_edit_limit_seconds): if allow_message_editing is None: allow_message_editing = realm.allow_message_editing if message_content_edit_limit_seconds is None: message_content_edit_limit_seconds = realm.message_content_edit_limit_seconds do_set_realm_message_editing(realm, allow_message_editing, message_content_edit_limit_seconds) data['allow_message_editing'] = allow_message_editing data[ 'message_content_edit_limit_seconds'] = message_content_edit_limit_seconds return json_success(data)
def add_subscriptions_backend( request, user_profile, streams_raw=REQ("subscriptions", validator=check_list(check_dict([('name', check_string) ]))), invite_only=REQ(validator=check_bool, default=False), announce=REQ(validator=check_bool, default=False), principals=REQ(validator=check_list(check_string), default=None), authorization_errors_fatal=REQ(validator=check_bool, default=True)): # type: (HttpRequest, UserProfile, Iterable[Mapping[str, text_type]], bool, bool, Optional[List[text_type]], bool) -> HttpResponse stream_names = [] for stream_dict in streams_raw: stream_name = stream_dict["name"].strip() if len(stream_name) > Stream.MAX_NAME_LENGTH: return json_error( _("Stream name (%s) too long.") % (stream_name, )) if not valid_stream_name(stream_name): return json_error(_("Invalid stream name (%s).") % (stream_name, )) stream_names.append(stream_name) # Enforcement of can_create_streams policy is inside list_to_streams. existing_streams, created_streams = \ list_to_streams(stream_names, user_profile, autocreate=True, invite_only=invite_only) authorized_streams, unauthorized_streams = \ filter_stream_authorization(user_profile, existing_streams) if len(unauthorized_streams) > 0 and authorization_errors_fatal: return json_error( _("Unable to access stream (%s).") % unauthorized_streams[0].name) # Newly created streams are also authorized for the creator streams = authorized_streams + created_streams if principals is not None: if user_profile.realm.domain == 'mit.edu' and not all( stream.invite_only for stream in streams): return json_error( _("You can only invite other mit.edu users to invite-only streams." )) subscribers = set( principal_to_user_profile(user_profile, principal) for principal in principals) else: subscribers = set([user_profile]) (subscribed, already_subscribed) = bulk_add_subscriptions(streams, subscribers) result = dict(subscribed=defaultdict(list), already_subscribed=defaultdict(list)) # type: Dict[str, Any] for (subscriber, stream) in subscribed: result["subscribed"][subscriber.email].append(stream.name) for (subscriber, stream) in already_subscribed: result["already_subscribed"][subscriber.email].append(stream.name) private_streams = dict( (stream.name, stream.invite_only) for stream in streams) bots = dict( (subscriber.email, subscriber.is_bot) for subscriber in subscribers) # Inform the user if someone else subscribed them to stuff, # or if a new stream was created with the "announce" option. notifications = [] if principals and result["subscribed"]: for email, subscriptions in six.iteritems(result["subscribed"]): if email == user_profile.email: # Don't send a Zulip if you invited yourself. continue if bots[email]: # Don't send invitation Zulips to bots continue if len(subscriptions) == 1: msg = ("Hi there! We thought you'd like to know that %s just " "subscribed you to the%s stream [%s](%s)." % ( user_profile.full_name, " **invite-only**" if private_streams[subscriptions[0]] else "", subscriptions[0], stream_link(subscriptions[0]), )) else: msg = ("Hi there! We thought you'd like to know that %s just " "subscribed you to the following streams: \n\n" % (user_profile.full_name, )) for stream in subscriptions: msg += "* [%s](%s)%s\n" % (stream, stream_link(stream), " (**invite-only**)" if private_streams[stream] else "") if len([s for s in subscriptions if not private_streams[s]]) > 0: msg += "\nYou can see historical content on a non-invite-only stream by narrowing to it." notifications.append( internal_prep_message(settings.NOTIFICATION_BOT, "private", email, "", msg)) if announce and len(created_streams) > 0: notifications_stream = user_profile.realm.notifications_stream if notifications_stream is not None: if len(created_streams) > 1: stream_msg = "the following streams: %s" % \ (", ".join('`%s`' % (s.name,) for s in created_streams),) else: stream_msg = "a new stream `%s`" % (created_streams[0].name) stream_buttons = ' '.join( stream_button(s.name) for s in created_streams) msg = ("%s just created %s. %s" % (user_profile.full_name, stream_msg, stream_buttons)) notifications.append( internal_prep_message(settings.NOTIFICATION_BOT, "stream", notifications_stream.name, "Streams", msg, realm=notifications_stream.realm)) else: msg = ("Hi there! %s just created a new stream '%s'. %s" % (user_profile.full_name, created_streams[0].name, stream_button(created_streams[0].name))) for realm_user_dict in get_active_user_dicts_in_realm( user_profile.realm): # Don't announce to yourself or to people you explicitly added # (who will get the notification above instead). if realm_user_dict['email'] in principals or realm_user_dict[ 'email'] == user_profile.email: continue notifications.append( internal_prep_message(settings.NOTIFICATION_BOT, "private", realm_user_dict['email'], "", msg)) if len(notifications) > 0: do_send_messages(notifications) result["subscribed"] = dict(result["subscribed"]) result["already_subscribed"] = dict(result["already_subscribed"]) if not authorization_errors_fatal: result["unauthorized"] = [ stream.name for stream in unauthorized_streams ] return json_success(result)
def remote_server_post_analytics( request: HttpRequest, entity: Union[UserProfile, RemoteZulipServer], realm_counts: List[Dict[str, Any]] = REQ(validator=check_list( check_dict_only([ ('property', check_string), ('realm', check_int), ('id', check_int), ('end_time', check_float), ('subgroup', check_none_or(check_string)), ('value', check_int), ]))), installation_counts: List[Dict[str, Any]] = REQ(validator=check_list( check_dict_only([ ('property', check_string), ('id', check_int), ('end_time', check_float), ('subgroup', check_none_or(check_string)), ('value', check_int), ]))) ) -> HttpResponse: server = validate_entity(entity) validate_count_stats(server, RemoteRealmCount, realm_counts) validate_count_stats(server, RemoteInstallationCount, installation_counts) BATCH_SIZE = 1000 while len(realm_counts) > 0: batch = realm_counts[0:BATCH_SIZE] realm_counts = realm_counts[BATCH_SIZE:] objects_to_create = [] for item in batch: objects_to_create.append( RemoteRealmCount(property=item['property'], realm_id=item['realm'], remote_id=item['id'], server=server, end_time=datetime.datetime.fromtimestamp( item['end_time'], tz=timezone_utc), subgroup=item['subgroup'], value=item['value'])) try: RemoteRealmCount.objects.bulk_create(objects_to_create) except IntegrityError: logging.warning( "Invalid data saving RemoteRealmCount for server %s/%s" % (server.hostname, server.uuid)) return json_error(_("Invalid data.")) while len(installation_counts) > 0: batch = installation_counts[0:BATCH_SIZE] installation_counts = installation_counts[BATCH_SIZE:] objects_to_create = [] for item in batch: objects_to_create.append( RemoteInstallationCount( property=item['property'], remote_id=item['id'], server=server, end_time=datetime.datetime.fromtimestamp(item['end_time'], tz=timezone_utc), subgroup=item['subgroup'], value=item['value'])) try: RemoteInstallationCount.objects.bulk_create(objects_to_create) except IntegrityError: logging.warning( "Invalid data saving RemoteInstallationCount for server %s/%s" % (server.hostname, server.uuid)) return json_error(_("Invalid data.")) return json_success()
def json_change_settings( request: HttpRequest, user_profile: UserProfile, full_name: str = REQ(default=""), email: str = REQ(default=""), old_password: str = REQ(default=""), new_password: str = REQ(default="") ) -> HttpResponse: if not (full_name or new_password or email): return json_error(_("Please fill out all fields.")) if new_password != "": return_data: Dict[str, Any] = {} if email_belongs_to_ldap(user_profile.realm, user_profile.delivery_email): return json_error(_("Your Zulip password is managed in LDAP")) try: if not authenticate(request, username=user_profile.delivery_email, password=old_password, realm=user_profile.realm, return_data=return_data): return json_error(_("Wrong password!")) except RateLimited as e: secs_to_freedom = int(float(str(e))) return json_error( _("You're making too many attempts! Try again in {} seconds."). format(secs_to_freedom), ) if not check_password_strength(new_password): return json_error(_("New password is too weak!")) do_change_password(user_profile, new_password) # In Django 1.10, password changes invalidates sessions, see # https://docs.djangoproject.com/en/1.10/topics/auth/default/#session-invalidation-on-password-change # for details. To avoid this logging the user out of their own # session (which would provide a confusing UX at best), we # update the session hash here. update_session_auth_hash(request, user_profile) # We also save the session to the DB immediately to mitigate # race conditions. In theory, there is still a race condition # and to completely avoid it we will have to use some kind of # mutex lock in `django.contrib.auth.get_user` where session # is verified. To make that lock work we will have to control # the AuthenticationMiddleware which is currently controlled # by Django, request.session.save() result: Dict[str, Any] = {} new_email = email.strip() if user_profile.delivery_email != new_email and new_email != '': if user_profile.realm.email_changes_disabled and not user_profile.is_realm_admin: return json_error( _("Email address changes are disabled in this organization.")) error = validate_email_is_valid( new_email, get_realm_email_validator(user_profile.realm), ) if error: return json_error(error) try: validate_email_not_already_in_realm( user_profile.realm, new_email, verbose=False, ) except ValidationError as e: return json_error(e.message) do_start_email_change_process(user_profile, new_email) result['account_email'] = _( "Check your email for a confirmation link. ") if user_profile.full_name != full_name and full_name.strip() != "": if name_changes_disabled( user_profile.realm) and not user_profile.is_realm_admin: # Failingly silently is fine -- they can't do it through the UI, so # they'd have to be trying to break the rules. pass else: # Note that check_change_full_name strips the passed name automatically result['full_name'] = check_change_full_name( user_profile, full_name, user_profile) return json_success(result)
def api_fetch_api_key( request: HttpRequest, username: str = REQ(), password: str = REQ() ) -> HttpResponse: return_data = {} # type: Dict[str, bool] subdomain = get_subdomain(request) realm = get_realm(subdomain) if username == "google-oauth2-token": # This code path is auth for the legacy Android app user_profile = authenticate(google_oauth2_token=password, realm=realm, return_data=return_data) else: if not ldap_auth_enabled(realm=get_realm_from_request(request)): # In case we don't authenticate against LDAP, check for a valid # email. LDAP backend can authenticate against a non-email. validate_login_email(username) user_profile = authenticate(username=username, password=password, realm=realm, return_data=return_data) if return_data.get("inactive_user"): return json_error(_("Your account has been disabled."), data={"reason": "user disable"}, status=403) if return_data.get("inactive_realm"): return json_error(_("This organization has been deactivated."), data={"reason": "realm deactivated"}, status=403) if return_data.get("password_auth_disabled"): return json_error(_("Password auth is disabled in your team."), data={"reason": "password auth disabled"}, status=403) if user_profile is None: if return_data.get("valid_attestation"): # We can leak that the user is unregistered iff # they present a valid authentication string for the user. return json_error( _("This user is not registered; do so from a browser."), data={"reason": "unregistered"}, status=403) return json_error(_("Your username or password is incorrect."), data={"reason": "incorrect_creds"}, status=403) # Maybe sending 'user_logged_in' signal is the better approach: # user_logged_in.send(sender=user_profile.__class__, request=request, user=user_profile) # Not doing this only because over here we don't add the user information # in the session. If the signal receiver assumes that we do then that # would cause problems. email_on_new_login(sender=user_profile.__class__, request=request, user=user_profile) # Mark this request as having a logged-in user for our server logs. process_client(request, user_profile) request._email = user_profile.email api_key = get_api_key(user_profile) return json_success({ "api_key": api_key, "email": user_profile.delivery_email })
def send_message_backend(request: HttpRequest, user_profile: UserProfile, message_type_name: str=REQ('type'), req_to: Optional[str]=REQ('to', default=None), forged_str: Optional[str]=REQ("forged", default=None, documentation_pending=True), topic_name: Optional[str]=REQ_topic(), message_content: str=REQ('content'), widget_content: Optional[str]=REQ(default=None, documentation_pending=True), realm_str: Optional[str]=REQ('realm_str', default=None, documentation_pending=True), local_id: Optional[str]=REQ(default=None), queue_id: Optional[str]=REQ(default=None), delivery_type: str=REQ('delivery_type', default='send_now', documentation_pending=True), defer_until: Optional[str]=REQ('deliver_at', default=None, documentation_pending=True), tz_guess: Optional[str]=REQ('tz_guess', default=None, documentation_pending=True), ) -> HttpResponse: # If req_to is None, then we default to an # empty list of recipients. message_to: Union[Sequence[int], Sequence[str]] = [] if req_to is not None: if message_type_name == 'stream': stream_indicator = extract_stream_indicator(req_to) # For legacy reasons check_send_message expects # a list of streams, instead of a single stream. # # Also, mypy can't detect that a single-item # list populated from a Union[int, str] is actually # a Union[Sequence[int], Sequence[str]]. if isinstance(stream_indicator, int): message_to = [stream_indicator] else: message_to = [stream_indicator] else: message_to = extract_private_recipients(req_to) # Temporary hack: We're transitioning `forged` from accepting # `yes` to accepting `true` like all of our normal booleans. forged = forged_str is not None and forged_str in ["yes", "true"] client = request.client is_super_user = request.user.is_api_super_user if forged and not is_super_user: return json_error(_("User not authorized for this query")) realm = None if realm_str and realm_str != user_profile.realm.string_id: if not is_super_user: # The email gateway bot needs to be able to send messages in # any realm. return json_error(_("User not authorized for this query")) try: realm = get_realm(realm_str) except Realm.DoesNotExist: return json_error(_("Unknown organization '{}'").format(realm_str)) if client.name in ["zephyr_mirror", "irc_mirror", "jabber_mirror", "JabberMirror"]: # Here's how security works for mirroring: # # For private messages, the message must be (1) both sent and # received exclusively by users in your realm, and (2) # received by the forwarding user. # # For stream messages, the message must be (1) being forwarded # by an API superuser for your realm and (2) being sent to a # mirrored stream. # # The most important security checks are in # `create_mirrored_message_users` below, which checks the # same-realm constraint. if "sender" not in request.POST: return json_error(_("Missing sender")) if message_type_name != "private" and not is_super_user: return json_error(_("User not authorized for this query")) # For now, mirroring only works with recipient emails, not for # recipient user IDs. if not all(isinstance(to_item, str) for to_item in message_to): return json_error(_("Mirroring not allowed with recipient user IDs")) # We need this manual cast so that mypy doesn't complain about # create_mirrored_message_users not being able to accept a Sequence[int] # type parameter. message_to = cast(Sequence[str], message_to) try: mirror_sender = create_mirrored_message_users(request, user_profile, message_to) except InvalidMirrorInput: return json_error(_("Invalid mirrored message")) if client.name == "zephyr_mirror" and not user_profile.realm.is_zephyr_mirror_realm: return json_error(_("Zephyr mirroring is not allowed in this organization")) sender = mirror_sender else: if "sender" in request.POST: return json_error(_("Invalid mirrored message")) sender = user_profile if (delivery_type == 'send_later' or delivery_type == 'remind') and defer_until is None: return json_error(_("Missing deliver_at in a request for delayed message delivery")) if (delivery_type == 'send_later' or delivery_type == 'remind') and defer_until is not None: return handle_deferred_message(sender, client, message_type_name, message_to, topic_name, message_content, delivery_type, defer_until, tz_guess, forwarder_user_profile=user_profile, realm=realm) ret = check_send_message(sender, client, message_type_name, message_to, topic_name, message_content, forged=forged, forged_timestamp = request.POST.get('time'), forwarder_user_profile=user_profile, realm=realm, local_id=local_id, sender_queue_id=queue_id, widget_content=widget_content) return json_success({"id": ret})
def add_subscriptions_backend( request: HttpRequest, user_profile: UserProfile, streams_raw: Iterable[Mapping[str, str]]=REQ( "subscriptions", validator=check_list(check_dict([('name', check_string)]))), invite_only: bool=REQ(validator=check_bool, default=False), history_public_to_subscribers: Optional[bool]=REQ(validator=check_bool, default=None), announce: bool=REQ(validator=check_bool, default=False), principals: List[str]=REQ(validator=check_list(check_string), default=[]), authorization_errors_fatal: bool=REQ(validator=check_bool, default=True), ) -> HttpResponse: stream_dicts = [] for stream_dict in streams_raw: stream_dict_copy = {} # type: Dict[str, Any] for field in stream_dict: stream_dict_copy[field] = stream_dict[field] # Strip the stream name here. stream_dict_copy['name'] = stream_dict_copy['name'].strip() stream_dict_copy["invite_only"] = invite_only stream_dict_copy["history_public_to_subscribers"] = history_public_to_subscribers stream_dicts.append(stream_dict_copy) # Validation of the streams arguments, including enforcement of # can_create_streams policy and check_stream_name policy is inside # list_to_streams. existing_streams, created_streams = \ list_to_streams(stream_dicts, user_profile, autocreate=True) authorized_streams, unauthorized_streams = \ filter_stream_authorization(user_profile, existing_streams) if len(unauthorized_streams) > 0 and authorization_errors_fatal: return json_error(_("Unable to access stream (%s).") % unauthorized_streams[0].name) # Newly created streams are also authorized for the creator streams = authorized_streams + created_streams if len(principals) > 0: if user_profile.realm.is_zephyr_mirror_realm and not all(stream.invite_only for stream in streams): return json_error(_("You can only invite other Zephyr mirroring users to invite-only streams.")) subscribers = set(principal_to_user_profile(user_profile, principal) for principal in principals) else: subscribers = set([user_profile]) (subscribed, already_subscribed) = bulk_add_subscriptions(streams, subscribers, acting_user=user_profile) # We can assume unique emails here for now, but we should eventually # convert this function to be more id-centric. email_to_user_profile = dict() # type: Dict[str, UserProfile] result = dict(subscribed=defaultdict(list), already_subscribed=defaultdict(list)) # type: Dict[str, Any] for (subscriber, stream) in subscribed: result["subscribed"][subscriber.email].append(stream.name) email_to_user_profile[subscriber.email] = subscriber for (subscriber, stream) in already_subscribed: result["already_subscribed"][subscriber.email].append(stream.name) bots = dict((subscriber.email, subscriber.is_bot) for subscriber in subscribers) newly_created_stream_names = {s.name for s in created_streams} private_stream_names = {s.name for s in streams if s.invite_only} # Inform the user if someone else subscribed them to stuff, # or if a new stream was created with the "announce" option. notifications = [] if len(principals) > 0 and result["subscribed"]: for email, subscribed_stream_names in result["subscribed"].items(): if email == user_profile.email: # Don't send a Zulip if you invited yourself. continue if bots[email]: # Don't send invitation Zulips to bots continue # For each user, we notify them about newly subscribed streams, except for # streams that were newly created. notify_stream_names = set(subscribed_stream_names) - newly_created_stream_names if not notify_stream_names: continue msg = you_were_just_subscribed_message( acting_user=user_profile, stream_names=notify_stream_names, private_stream_names=private_stream_names ) sender = get_system_bot(settings.NOTIFICATION_BOT) notifications.append( internal_prep_private_message( realm=user_profile.realm, sender=sender, recipient_user=email_to_user_profile[email], content=msg)) if announce and len(created_streams) > 0 and settings.NOTIFICATION_BOT is not None: notifications_stream = user_profile.realm.get_notifications_stream() if notifications_stream is not None: if len(created_streams) > 1: stream_strs = ", ".join('#**%s**' % s.name for s in created_streams) stream_msg = "the following streams: %s" % (stream_strs,) else: stream_msg = "a new stream #**%s**." % created_streams[0].name msg = ("%s just created %s" % (user_profile.full_name, stream_msg)) sender = get_system_bot(settings.NOTIFICATION_BOT) stream_name = notifications_stream.name topic = 'Streams' notifications.append( internal_prep_stream_message( realm=user_profile.realm, sender=sender, stream_name=stream_name, topic=topic, content=msg)) if not user_profile.realm.is_zephyr_mirror_realm: for stream in created_streams: notifications.append(prep_stream_welcome_message(stream)) if len(notifications) > 0: do_send_messages(notifications) result["subscribed"] = dict(result["subscribed"]) result["already_subscribed"] = dict(result["already_subscribed"]) if not authorization_errors_fatal: result["unauthorized"] = [s.name for s in unauthorized_streams] return json_success(result)
def update_stream_backend( request: HttpRequest, user_profile: UserProfile, stream_id: int, description: Optional[str] = REQ(validator=check_capped_string( Stream.MAX_DESCRIPTION_LENGTH), default=None), is_private: Optional[bool] = REQ(validator=check_bool, default=None), is_announcement_only: Optional[bool] = REQ(validator=check_bool, default=None), stream_post_policy: Optional[int] = REQ(validator=check_int_in( Stream.STREAM_POST_POLICY_TYPES), default=None), history_public_to_subscribers: Optional[bool] = REQ(validator=check_bool, default=None), new_name: Optional[str] = REQ(validator=check_string, default=None), message_retention_days: Optional[Union[int, str]] = REQ( validator=check_string_or_int, default=None), ) -> HttpResponse: # We allow realm administrators to to update the stream name and # description even for private streams. stream = access_stream_for_delete_or_update(user_profile, stream_id) if message_retention_days is not None: if not user_profile.is_realm_owner: raise OrganizationOwnerRequired() user_profile.realm.ensure_not_on_limited_plan() message_retention_days_value = parse_message_retention_days( message_retention_days) do_change_stream_message_retention_days(stream, message_retention_days_value) if description is not None: if '\n' in description: # We don't allow newline characters in stream descriptions. description = description.replace("\n", " ") do_change_stream_description(stream, description) if new_name is not None: new_name = new_name.strip() if stream.name == new_name: return json_error(_("Stream already has that name!")) if stream.name.lower() != new_name.lower(): # Check that the stream name is available (unless we are # are only changing the casing of the stream name). check_stream_name_available(user_profile.realm, new_name) do_rename_stream(stream, new_name, user_profile) if is_announcement_only is not None: # is_announcement_only is a legacy way to specify # stream_post_policy. We can probably just delete this code, # since we're not aware of clients that used it, but we're # keeping it for backwards-compatibility for now. stream_post_policy = Stream.STREAM_POST_POLICY_EVERYONE if is_announcement_only: stream_post_policy = Stream.STREAM_POST_POLICY_ADMINS if stream_post_policy is not None: do_change_stream_post_policy(stream, stream_post_policy) # But we require even realm administrators to be actually # subscribed to make a private stream public. if is_private is not None: (stream, recipient, sub) = access_stream_by_id(user_profile, stream_id) do_change_stream_invite_only(stream, is_private, history_public_to_subscribers) return json_success()
def update_realm( request: HttpRequest, user_profile: UserProfile, name: Optional[str] = REQ(validator=check_string, default=None), description: Optional[str] = REQ(validator=check_string, default=None), restricted_to_domain: Optional[bool] = REQ(validator=check_bool, default=None), disallow_disposable_email_addresses: Optional[bool] = REQ( validator=check_bool, default=None), invite_required: Optional[bool] = REQ(validator=check_bool, default=None), invite_by_admins_only: Optional[bool] = REQ(validator=check_bool, default=None), name_changes_disabled: Optional[bool] = REQ(validator=check_bool, default=None), email_changes_disabled: Optional[bool] = REQ(validator=check_bool, default=None), inline_image_preview: Optional[bool] = REQ(validator=check_bool, default=None), inline_url_embed_preview: Optional[bool] = REQ(validator=check_bool, default=None), create_stream_by_admins_only: Optional[bool] = REQ(validator=check_bool, default=None), add_emoji_by_admins_only: Optional[bool] = REQ(validator=check_bool, default=None), allow_message_deleting: Optional[bool] = REQ(validator=check_bool, default=None), allow_message_editing: Optional[bool] = REQ(validator=check_bool, default=None), allow_community_topic_editing: Optional[bool] = REQ(validator=check_bool, default=None), mandatory_topics: Optional[bool] = REQ(validator=check_bool, default=None), message_content_edit_limit_seconds: Optional[int] = REQ( converter=to_non_negative_int, default=None), allow_edit_history: Optional[bool] = REQ(validator=check_bool, default=None), default_language: Optional[str] = REQ(validator=check_string, default=None), waiting_period_threshold: Optional[int] = REQ( converter=to_non_negative_int, default=None), authentication_methods: Optional[Dict[Any, Any]] = REQ(validator=check_dict([]), default=None), notifications_stream_id: Optional[int] = REQ(validator=check_int, default=None), signup_notifications_stream_id: Optional[int] = REQ(validator=check_int, default=None), message_retention_days: Optional[int] = REQ( converter=to_not_negative_int_or_none, default=None), send_welcome_emails: Optional[bool] = REQ(validator=check_bool, default=None), bot_creation_policy: Optional[int] = REQ( converter=to_not_negative_int_or_none, default=None) ) -> HttpResponse: realm = user_profile.realm # Additional validation/error checking beyond types go here, so # the entire request can succeed or fail atomically. if default_language is not None and default_language not in get_available_language_codes( ): raise JsonableError(_("Invalid language '%s'" % (default_language, ))) if description is not None and len(description) > 1000: return json_error(_("Organization description is too long.")) if name is not None and len(name) > Realm.MAX_REALM_NAME_LENGTH: return json_error(_("Organization name is too long.")) if authentication_methods is not None and True not in list( authentication_methods.values()): return json_error( _("At least one authentication method must be enabled.")) # Additional validation of permissions values to add new bot if bot_creation_policy is not None and bot_creation_policy not in Realm.BOT_CREATION_POLICY_TYPES: return json_error(_("Invalid bot creation policy")) # The user of `locals()` here is a bit of a code smell, but it's # restricted to the elements present in realm.property_types. # # TODO: It should be possible to deduplicate this function up # further by some more advanced usage of the # `REQ/has_request_variables` extraction. req_vars = { k: v for k, v in list(locals().items()) if k in realm.property_types } data = {} # type: Dict[str, Any] for k, v in list(req_vars.items()): if v is not None and getattr(realm, k) != v: do_set_realm_property(realm, k, v) if isinstance(v, Text): data[k] = 'updated' else: data[k] = v # The following realm properties do not fit the pattern above # authentication_methods is not supported by the do_set_realm_property # framework because of its bitfield. if authentication_methods is not None and ( realm.authentication_methods_dict() != authentication_methods): do_set_realm_authentication_methods(realm, authentication_methods) data['authentication_methods'] = authentication_methods # The message_editing settings are coupled to each other, and thus don't fit # into the do_set_realm_property framework. if ((allow_message_editing is not None and realm.allow_message_editing != allow_message_editing) or (message_content_edit_limit_seconds is not None and realm.message_content_edit_limit_seconds != message_content_edit_limit_seconds) or (allow_community_topic_editing is not None and realm.allow_community_topic_editing != allow_community_topic_editing)): if allow_message_editing is None: allow_message_editing = realm.allow_message_editing if message_content_edit_limit_seconds is None: message_content_edit_limit_seconds = realm.message_content_edit_limit_seconds if allow_community_topic_editing is None: allow_community_topic_editing = realm.allow_community_topic_editing do_set_realm_message_editing(realm, allow_message_editing, message_content_edit_limit_seconds, allow_community_topic_editing) data['allow_message_editing'] = allow_message_editing data[ 'message_content_edit_limit_seconds'] = message_content_edit_limit_seconds data['allow_community_topic_editing'] = allow_community_topic_editing # Realm.notifications_stream and Realm.signup_notifications_stream are not boolean, # Text or integer field, and thus doesn't fit into the do_set_realm_property framework. if notifications_stream_id is not None: if realm.notifications_stream is None or (realm.notifications_stream.id != notifications_stream_id): new_notifications_stream = None if notifications_stream_id >= 0: (new_notifications_stream, recipient, sub) = access_stream_by_id(user_profile, notifications_stream_id) do_set_realm_notifications_stream(realm, new_notifications_stream, notifications_stream_id) data['notifications_stream_id'] = notifications_stream_id if signup_notifications_stream_id is not None: if realm.signup_notifications_stream is None or ( realm.signup_notifications_stream.id != signup_notifications_stream_id): new_signup_notifications_stream = None if signup_notifications_stream_id >= 0: (new_signup_notifications_stream, recipient, sub) = access_stream_by_id(user_profile, signup_notifications_stream_id) do_set_realm_signup_notifications_stream( realm, new_signup_notifications_stream, signup_notifications_stream_id) data[ 'signup_notifications_stream_id'] = signup_notifications_stream_id return json_success(data)
def api_newrelic_webhook( request: HttpRequest, user_profile: UserProfile, payload: Dict[str, Any] = REQ(argument_type='body') ) -> HttpResponse: info = { "condition_name": payload.get('condition_name', 'Unknown condition'), "details": payload.get('details', 'No details.'), "incident_url": payload.get('incident_url', 'https://alerts.newrelic.com'), "incident_acknowledge_url": payload.get('incident_acknowledge_url', 'https://alerts.newrelic.com'), "status": payload.get('current_state', 'None'), "iso_timestamp": '', } unix_time = payload.get('timestamp', None) if unix_time is None: return json_error( _("The newrelic webhook requires timestamp in milliseconds")) duration = payload.get('duration', None) if duration is None: return json_error( _("The newrelic webhook requires duration in milliseconds")) try: # Timestamp - duration to get time alert started # timestamps from NewRelic are in ms iso_timestamp = datetime.fromtimestamp((unix_time - duration) / 1000) info['iso_timestamp'] = iso_timestamp except ValueError: return json_error( _("The newrelic webhook expects timestamp and duration in milliseconds" )) except TypeError: return json_error( _("The newrelic webhook expects timestamp and duration in milliseconds" )) # These are the three promised current_state values if 'open' in info['status']: content = OPEN_TEMPLATE.format(**info) elif 'acknowledged' in info['status']: content = DEFAULT_TEMPLATE.format(**info) elif 'closed' in info['status']: content = DEFAULT_TEMPLATE.format(**info) else: return json_error( _("The newrelic webhook requires current_state be in [open|acknowledged|closed]" )) topic_info = { "policy_name": payload.get('policy_name', 'Unknown Policy'), "incident_id": payload.get('incident_id', 'Unknown ID'), } topic = TOPIC_TEMPLATE.format(**topic_info) check_send_webhook_message(request, user_profile, topic, content) return json_success()
def patch_bot_backend(request, user_profile, email, full_name=REQ(default=None), bot_owner=REQ(default=None), default_sending_stream=REQ(default=None), default_events_register_stream=REQ(default=None), default_all_public_streams=REQ(default=None, validator=check_bool)): # type: (HttpRequest, UserProfile, Text, Optional[Text], Optional[Text], Optional[Text], Optional[Text], Optional[bool]) -> HttpResponse try: bot = get_user_profile_by_email(email) except UserProfile.DoesNotExist: return json_error(_('No such user')) if not user_profile.can_admin_user(bot): return json_error(_('Insufficient permission')) if full_name is not None: check_change_full_name(bot, full_name) if bot_owner is not None: owner = get_user_profile_by_email(bot_owner) do_change_bot_owner(bot, owner, user_profile) if default_sending_stream is not None: if default_sending_stream == "": stream = None # type: Optional[Stream] else: (stream, recipient, sub) = access_stream_by_name( user_profile, default_sending_stream) do_change_default_sending_stream(bot, stream) if default_events_register_stream is not None: if default_events_register_stream == "": stream = None else: (stream, recipient, sub) = access_stream_by_name( user_profile, default_events_register_stream) do_change_default_events_register_stream(bot, stream) if default_all_public_streams is not None: do_change_default_all_public_streams(bot, default_all_public_streams) if len(request.FILES) == 0: pass elif len(request.FILES) == 1: user_file = list(request.FILES.values())[0] upload_avatar_image(user_file, user_profile, bot) avatar_source = UserProfile.AVATAR_FROM_USER do_change_avatar_fields(bot, avatar_source) else: return json_error(_("You may only upload one file at a time")) json_result = dict( full_name=bot.full_name, avatar_url=avatar_url(bot), default_sending_stream=get_stream_name(bot.default_sending_stream), default_events_register_stream=get_stream_name(bot.default_events_register_stream), default_all_public_streams=bot.default_all_public_streams, ) # Don't include the bot owner in case it is not set. # Default bots have no owner. if bot.bot_owner is not None: json_result['bot_owner'] = bot.bot_owner.email return json_success(json_result)
def update_message_backend( request: HttpRequest, user_profile: UserMessage, message_id: int = REQ(converter=to_non_negative_int, path_only=True), stream_id: Optional[int] = REQ(converter=to_non_negative_int, default=None), topic_name: Optional[str] = REQ_topic(), propagate_mode: Optional[str] = REQ( default="change_one", str_validator=check_string_in(PROPAGATE_MODE_VALUES)), send_notification_to_old_thread: bool = REQ(default=True, validator=check_bool), send_notification_to_new_thread: bool = REQ(default=True, validator=check_bool), content: Optional[str] = REQ(default=None) ) -> HttpResponse: if not user_profile.realm.allow_message_editing: return json_error( _("Your organization has turned off message editing")) if propagate_mode != "change_one" and topic_name is None and stream_id is None: return json_error(_("Invalid propagate_mode without topic edit")) message, ignored_user_message = access_message(user_profile, message_id) is_no_topic_msg = (message.topic_name() == "(no topic)") # You only have permission to edit a message if: # you change this value also change those two parameters in message_edit.js. # 1. You sent it, OR: # 2. This is a topic-only edit for a (no topic) message, OR: # 3. This is a topic-only edit and you are an admin, OR: # 4. This is a topic-only edit and your realm allows users to edit topics. if message.sender == user_profile: pass elif (content is None) and (is_no_topic_msg or user_profile.is_realm_admin or user_profile.realm.allow_community_topic_editing): pass else: raise JsonableError( _("You don't have permission to edit this message")) # If there is a change to the content, check that it hasn't been too long # Allow an extra 20 seconds since we potentially allow editing 15 seconds # past the limit, and in case there are network issues, etc. The 15 comes # from (min_seconds_to_edit + seconds_left_buffer) in message_edit.js; if # you change this value also change those two parameters in message_edit.js. edit_limit_buffer = 20 if content is not None and user_profile.realm.message_content_edit_limit_seconds > 0: deadline_seconds = user_profile.realm.message_content_edit_limit_seconds + edit_limit_buffer if (timezone_now() - message.date_sent) > datetime.timedelta( seconds=deadline_seconds): raise JsonableError( _("The time limit for editing this message has passed")) # If there is a change to the topic, check that the user is allowed to # edit it and that it has not been too long. If this is not the user who # sent the message, they are not the admin, and the time limit for editing # topics is passed, raise an error. if content is None and message.sender != user_profile and not user_profile.is_realm_admin and \ not is_no_topic_msg: deadline_seconds = Realm.DEFAULT_COMMUNITY_TOPIC_EDITING_LIMIT_SECONDS + edit_limit_buffer if (timezone_now() - message.date_sent) > datetime.timedelta( seconds=deadline_seconds): raise JsonableError( _("The time limit for editing this message has passed")) if topic_name is None and content is None and stream_id is None: return json_error(_("Nothing to change")) if topic_name is not None: topic_name = topic_name.strip() if topic_name == "": raise JsonableError(_("Topic can't be empty")) rendered_content = None links_for_embed: Set[str] = set() prior_mention_user_ids: Set[int] = set() mention_user_ids: Set[int] = set() mention_data: Optional[bugdown.MentionData] = None if content is not None: content = content.strip() if content == "": content = "(deleted)" content = truncate_body(content) mention_data = bugdown.MentionData( realm_id=user_profile.realm.id, content=content, ) user_info = get_user_info_for_message_updates(message.id) prior_mention_user_ids = user_info['mention_user_ids'] # We render the message using the current user's realm; since # the cross-realm bots never edit messages, this should be # always correct. # Note: If rendering fails, the called code will raise a JsonableError. rendered_content = render_incoming_message( message, content, user_info['message_user_ids'], user_profile.realm, mention_data=mention_data) links_for_embed |= message.links_for_preview mention_user_ids = message.mentions_user_ids new_stream = None old_stream = None number_changed = 0 if stream_id is not None: if not user_profile.is_realm_admin: raise JsonableError( _("You don't have permission to move this message")) if content is not None: raise JsonableError( _("Cannot change message content while changing stream")) old_stream = get_stream_by_id(message.recipient.type_id) new_stream = get_stream_by_id(stream_id) if not (old_stream.is_public() and new_stream.is_public()): # We'll likely decide to relax this condition in the # future; it just requires more care with details like the # breadcrumb messages. raise JsonableError(_("Streams must be public")) number_changed = do_update_message( user_profile, message, new_stream, topic_name, propagate_mode, send_notification_to_old_thread, send_notification_to_new_thread, content, rendered_content, prior_mention_user_ids, mention_user_ids, mention_data) # Include the number of messages changed in the logs request._log_data['extra'] = f"[{number_changed}]" if links_for_embed: event_data = { 'message_id': message.id, 'message_content': message.content, # The choice of `user_profile.realm_id` rather than # `sender.realm_id` must match the decision made in the # `render_incoming_message` call earlier in this function. 'message_realm_id': user_profile.realm_id, 'urls': links_for_embed } queue_json_publish('embed_links', event_data) return json_success()
def json_change_settings(request, user_profile, full_name=REQ(default=""), email=REQ(default=""), old_password=REQ(default=""), new_password=REQ(default=""), confirm_password=REQ(default="")): # type: (HttpRequest, UserProfile, Text, Text, Text, Text, Text) -> HttpResponse if not (full_name or new_password or email): return json_error(_("No new data supplied")) if new_password != "" or confirm_password != "": if new_password != confirm_password: return json_error( _("New password must match confirmation password!")) if not authenticate(username=user_profile.email, password=old_password): return json_error(_("Wrong password!")) do_change_password(user_profile, new_password) # In Django 1.10, password changes invalidates sessions, see # https://docs.djangoproject.com/en/1.10/topics/auth/default/#session-invalidation-on-password-change # for details. To avoid this logging the user out of their own # session (which would provide a confusing UX at best), we # update the session hash here. update_session_auth_hash(request, user_profile) # We also save the session to the DB immediately to mitigate # race conditions. In theory, there is still a race condition # and to completely avoid it we will have to use some kind of # mutex lock in `django.contrib.auth.get_user` where session # is verified. To make that lock work we will have to control # the AuthenticationMiddleware which is currently controlled # by Django, request.session.save() result = {} # type: Dict[str, Any] new_email = email.strip() if user_profile.email != email and new_email != '': if user_profile.realm.email_changes_disabled: return json_error( _("Email address changes are disabled in this organization.")) error, skipped = validate_email(user_profile, new_email) if error: return json_error(error) if skipped: return json_error(skipped) do_start_email_change_process(user_profile, new_email) result['account_email'] = _( "Check your email for a confirmation link.") if user_profile.full_name != full_name and full_name.strip() != "": if name_changes_disabled(user_profile.realm): # Failingly silently is fine -- they can't do it through the UI, so # they'd have to be trying to break the rules. pass else: # Note that check_change_full_name strips the passed name automatically result['full_name'] = check_change_full_name( user_profile, full_name, user_profile) return json_success(result)