def notify(self, check): from hc.api.models import TokenBucket if not TokenBucket.authorize_telegram(self.channel.telegram_id): return "Rate limit exceeded" text = tmpl("telegram_message.html", check=check) return self.send(self.channel.telegram_id, text)
def login_totp(request): # Expect an unauthenticated user if request.user.is_authenticated: return HttpResponseBadRequest() if "2fa_user" not in request.session: return HttpResponseBadRequest() user_id, email, timestamp = request.session["2fa_user"] if timestamp + 300 < time.time(): return redirect("hc-login") try: user = User.objects.get(id=user_id, email=email) except User.DoesNotExist: return HttpResponseBadRequest() if not user.profile.totp: return HttpResponseBadRequest() totp = pyotp.totp.TOTP(user.profile.totp) if request.method == "POST": # To guard against brute-forcing TOTP codes, we allow # 96 attempts per user per 24h. if not TokenBucket.authorize_totp_attempt(user): return render(request, "try_later.html") form = forms.TotpForm(totp, request.POST) if form.is_valid(): # We blacklist an used TOTP code for 90 seconds, # so an attacker cannot reuse a stolen code. if not TokenBucket.authorize_totp_code(user, form.cleaned_data["code"]): return render(request, "try_later.html") request.session.pop("2fa_user") auth_login(request, user, "hc.accounts.backends.EmailBackend") return _redirect_after_login(request) else: form = forms.TotpForm(totp) return render(request, "accounts/login_totp.html", {"form": form})
def clean_identity(self): v = self.cleaned_data["identity"] if not TokenBucket.authorize_login_email(v): raise forms.ValidationError("Too many attempts, please try later.") try: self.user = User.objects.get(email=v) except User.DoesNotExist: raise forms.ValidationError("Unknown email address.") return v
def notify(self, check, notification=None) -> None: if not settings.SIGNAL_CLI_SOCKET: raise TransportError("Signal notifications are not enabled") from hc.api.models import TokenBucket if not TokenBucket.authorize_signal(self.channel.phone_number): raise TransportError("Rate limit exceeded") ctx = {"check": check, "down_checks": self.down_checks(check)} text = tmpl("signal_message.html", **ctx) self.send(self.channel.phone_number, text)
def clean(self): username = self.cleaned_data.get("email") password = self.cleaned_data.get("password") if username and password: if not TokenBucket.authorize_login_password(username): raise forms.ValidationError("Too many attempts, please try later.") self.user = authenticate(username=username, password=password) if self.user is None or not self.user.is_active: raise forms.ValidationError("Incorrect email or password.") return self.cleaned_data
def notify(self, check) -> None: from hc.api.models import TokenBucket if not TokenBucket.authorize_telegram(self.channel.telegram_id): raise TransportError("Rate limit exceeded") ctx = {"check": check, "down_checks": self.down_checks(check)} text = tmpl("telegram_message.html", **ctx) try: self.send(self.channel.telegram_id, text) except MigrationRequiredError as e: # Save the new chat_id, then try sending again: self.channel.update_telegram_id(e.new_chat_id) self.send(self.channel.telegram_id, text)
def notify(self, check) -> None: pieces = self.channel.value.split("|") user_key, down_prio = pieces[0], pieces[1] # The third element, if present, is the priority for "up" events up_prio = down_prio if len(pieces) == 3: up_prio = pieces[2] from hc.api.models import TokenBucket if not TokenBucket.authorize_pushover(user_key): raise TransportError("Rate limit exceeded") # If down events have the emergency priority, # send a cancel call first if check.status == "up" and down_prio == "2": url = self.CANCEL_TMPL % check.unique_key cancel_payload = {"token": settings.PUSHOVER_API_TOKEN} self.post(url, data=cancel_payload) ctx = {"check": check, "down_checks": self.down_checks(check)} text = tmpl("pushover_message.html", **ctx) title = tmpl("pushover_title.html", **ctx) prio = up_prio if check.status == "up" else down_prio payload = { "token": settings.PUSHOVER_API_TOKEN, "user": user_key, "message": text, "title": title, "html": 1, "priority": int(prio), "tags": check.unique_key, } # Emergency notification if prio == "2": payload["retry"] = settings.PUSHOVER_EMERGENCY_RETRY_DELAY payload["expire"] = settings.PUSHOVER_EMERGENCY_EXPIRATION self.post(self.URL, data=payload)
def test_it_handles_insufficient_tokens(self): TokenBucket.objects.create(value="em-" + ALICE_HASH, tokens=0.04) r = TokenBucket.authorize_login_email("*****@*****.**") self.assertFalse(r)
def project(request, code): if request.user.is_superuser: q = Project.objects else: q = request.profile.projects() try: project = q.get(code=code) except Project.DoesNotExist: return HttpResponseNotFound() is_owner = project.owner_id == request.user.id ctx = { "page": "project", "project": project, "is_owner": is_owner, "show_api_keys": "show_api_keys" in request.GET, "project_name_status": "default", "api_status": "default", "team_status": "default", } if request.method == "POST": if "create_api_keys" in request.POST: project.set_api_keys() project.save() ctx["show_api_keys"] = True ctx["api_keys_created"] = True ctx["api_status"] = "success" elif "revoke_api_keys" in request.POST: project.api_key = "" project.api_key_readonly = "" project.save() ctx["api_keys_revoked"] = True ctx["api_status"] = "info" elif "show_api_keys" in request.POST: ctx["show_api_keys"] = True elif "invite_team_member" in request.POST: if not is_owner or not project.can_invite(): return HttpResponseForbidden() form = InviteTeamMemberForm(request.POST) if form.is_valid(): if not TokenBucket.authorize_invite(request.user): return render(request, "try_later.html") email = form.cleaned_data["email"] try: user = User.objects.get(email=email) except User.DoesNotExist: user = _make_user(email, with_project=False) project.invite(user) ctx["team_member_invited"] = email ctx["team_status"] = "success" elif "remove_team_member" in request.POST: if not is_owner: return HttpResponseForbidden() form = RemoveTeamMemberForm(request.POST) if form.is_valid(): q = User.objects q = q.filter(email=form.cleaned_data["email"]) q = q.filter(memberships__project=project) farewell_user = q.first() if farewell_user is None: return HttpResponseBadRequest() farewell_user.profile.current_project = None farewell_user.profile.save() Member.objects.filter(project=project, user=farewell_user).delete() ctx["team_member_removed"] = form.cleaned_data["email"] ctx["team_status"] = "info" elif "set_project_name" in request.POST: form = ProjectNameForm(request.POST) if form.is_valid(): project.name = form.cleaned_data["name"] project.save() if request.profile.current_project == project: request.profile.current_project.name = project.name ctx["project_name_updated"] = True ctx["project_name_status"] = "success" # Count members right before rendering the template, in case # we just invited or removed someone ctx["num_members"] = project.member_set.count() return render(request, "accounts/project.html", ctx)
def project(request, code): project = get_object_or_404(Project, code=code) is_owner = project.owner_id == request.user.id if request.user.is_superuser or is_owner: rw = True else: membership = get_object_or_404(Member, project=project, user=request.user) rw = membership.rw ctx = { "page": "project", "rw": rw, "project": project, "is_owner": is_owner, "show_api_keys": "show_api_keys" in request.GET, } if request.method == "POST": if not rw: return HttpResponseForbidden() if "create_api_keys" in request.POST: project.set_api_keys() project.save() ctx["show_api_keys"] = True ctx["api_keys_created"] = True ctx["api_status"] = "success" elif "revoke_api_keys" in request.POST: project.api_key = "" project.api_key_readonly = "" project.save() ctx["api_keys_revoked"] = True ctx["api_status"] = "info" elif "show_api_keys" in request.POST: ctx["show_api_keys"] = True elif "invite_team_member" in request.POST: if not is_owner: return HttpResponseForbidden() form = forms.InviteTeamMemberForm(request.POST) if form.is_valid(): email = form.cleaned_data["email"] invite_suggestions = project.invite_suggestions() if not invite_suggestions.filter(email=email).exists(): # We're inviting a new user. Are we within team size limit? if not project.can_invite_new_users(): return HttpResponseForbidden() # And are we not hitting a rate limit? if not TokenBucket.authorize_invite(request.user): return render(request, "try_later.html") try: user = User.objects.get(email=email) except User.DoesNotExist: user = _make_user(email, with_project=False) if project.invite(user, rw=form.cleaned_data["rw"]): ctx["team_member_invited"] = email ctx["team_status"] = "success" else: ctx["team_member_duplicate"] = email ctx["team_status"] = "info" elif "remove_team_member" in request.POST: if not is_owner: return HttpResponseForbidden() form = forms.RemoveTeamMemberForm(request.POST) if form.is_valid(): q = User.objects q = q.filter(email=form.cleaned_data["email"]) q = q.filter(memberships__project=project) farewell_user = q.first() if farewell_user is None: return HttpResponseBadRequest() Member.objects.filter(project=project, user=farewell_user).delete() ctx["team_member_removed"] = form.cleaned_data["email"] ctx["team_status"] = "info" elif "set_project_name" in request.POST: form = forms.ProjectNameForm(request.POST) if form.is_valid(): project.name = form.cleaned_data["name"] project.save() ctx["project_name_updated"] = True ctx["project_name_status"] = "success" elif "transfer_project" in request.POST: if not is_owner: return HttpResponseForbidden() form = forms.TransferForm(request.POST) if form.is_valid(): # Look up the proposed new owner email = form.cleaned_data["email"] try: membership = project.member_set.filter( user__email=email).get() except Member.DoesNotExist: return HttpResponseBadRequest() # Revoke any previous transfer requests project.member_set.update(transfer_request_date=None) # Initiate the new request membership.transfer_request_date = now() membership.save() # Send an email notification profile = Profile.objects.for_user(membership.user) profile.send_transfer_request(project) ctx["transfer_initiated"] = True ctx["transfer_status"] = "success" elif "cancel_transfer" in request.POST: if not is_owner: return HttpResponseForbidden() project.member_set.update(transfer_request_date=None) ctx["transfer_cancelled"] = True ctx["transfer_status"] = "success" elif "accept_transfer" in request.POST: tr = project.transfer_request() if not tr or tr.user != request.user: return HttpResponseForbidden() if not tr.can_accept(): return HttpResponseBadRequest() with transaction.atomic(): # 1. Reuse the existing membership, and change its user tr.user = project.owner tr.transfer_request_date = None tr.save() # 2. Change project's owner project.owner = request.user project.save() ctx["is_owner"] = True messages.success(request, "You are now the owner of this project!") elif "reject_transfer" in request.POST: tr = project.transfer_request() if not tr or tr.user != request.user: return HttpResponseForbidden() tr.transfer_request_date = None tr.save() return render(request, "accounts/project.html", ctx)
def project(request, code): if request.user.is_superuser: q = Project.objects else: q = request.profile.projects() try: project = q.get(code=code) except Project.DoesNotExist: return HttpResponseNotFound() is_owner = project.owner_id == request.user.id invite_suggestions = project.invite_suggestions() ctx = { "page": "project", "project": project, "is_owner": is_owner, "show_api_keys": "show_api_keys" in request.GET, "project_name_status": "default", "api_status": "default", "team_status": "default", "invite_suggestions": invite_suggestions, } if request.method == "POST": if "create_api_keys" in request.POST: project.set_api_keys() project.save() ctx["show_api_keys"] = True ctx["api_keys_created"] = True ctx["api_status"] = "success" elif "revoke_api_keys" in request.POST: project.api_key = "" project.api_key_readonly = "" project.save() ctx["api_keys_revoked"] = True ctx["api_status"] = "info" elif "show_api_keys" in request.POST: ctx["show_api_keys"] = True elif "invite_team_member" in request.POST: if not is_owner: return HttpResponseForbidden() form = InviteTeamMemberForm(request.POST) if form.is_valid(): email = form.cleaned_data["email"] if not invite_suggestions.filter(email=email).exists(): # We're inviting a new user. Are we within team size limit? if not project.can_invite_new_users(): return HttpResponseForbidden() # And are we not hitting a rate limit? if not TokenBucket.authorize_invite(request.user): return render(request, "try_later.html") try: user = User.objects.get(email=email) except User.DoesNotExist: user = _make_user(email, with_project=False) project.invite(user) ctx["team_member_invited"] = email ctx["team_status"] = "success" elif "remove_team_member" in request.POST: if not is_owner: return HttpResponseForbidden() form = RemoveTeamMemberForm(request.POST) if form.is_valid(): q = User.objects q = q.filter(email=form.cleaned_data["email"]) q = q.filter(memberships__project=project) farewell_user = q.first() if farewell_user is None: return HttpResponseBadRequest() Member.objects.filter(project=project, user=farewell_user).delete() ctx["team_member_removed"] = form.cleaned_data["email"] ctx["team_status"] = "info" elif "set_project_name" in request.POST: form = ProjectNameForm(request.POST) if form.is_valid(): project.name = form.cleaned_data["name"] project.save() ctx["project_name_updated"] = True ctx["project_name_status"] = "success" return render(request, "accounts/project.html", ctx)