Exemple #1
0
    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)
Exemple #2
0
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})
Exemple #3
0
    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
Exemple #4
0
    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)
Exemple #5
0
    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
Exemple #6
0
    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)
Exemple #7
0
    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)
Exemple #9
0
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)
Exemple #10
0
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)
Exemple #11
0
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)