def settings_info_POST(owner_name, repo_name): owner, repo = check_access(owner_name, repo_name, UserAccess.manage) if isinstance(repo, BaseRedirectMixin): repo = repo.new_repo valid = Validation(request) desc = valid.optional("description", default=repo.description) visibility = valid.optional("visibility", cls=RepoVisibility, default=repo.visibility) repo.visibility = visibility repo.description = desc db.session.commit() return redirect("/{}/{}/settings/info".format(owner_name, repo_name))
def edit_POST(task): record = GitHubPRToBuild._GitHubPRToBuildRecord.query.filter( GitHubPRToBuild._GitHubPRToBuildRecord.task_id == task.id).one_or_none() valid = Validation(request) automerge = valid.optional("automerge", cls=bool, default=False) secrets = valid.optional("secrets", cls=bool, default=False) record.automerge = bool(automerge) record.secrets = bool(secrets) if not record.private: record.secrets = False db.session.commit() session["saved"] = True return redirect(url_for("html.edit_task", task_id=task.id))
def ticket_edit_POST(owner, name, ticket_id): tracker, _ = get_tracker(owner, name) if not tracker: abort(404) ticket, access = get_ticket(tracker, ticket_id) if not ticket: abort(404) if not TicketAccess.edit in access: abort(401) valid = Validation(request) title = valid.require("title", friendly_name="Title") desc = valid.optional("description") valid.expect(not title or 3 <= len(title) <= 2048, "Title must be between 3 and 2048 characters.", field="title") valid.expect(not desc or len(desc) < 16384, "Description must be no more than 16384 characters.", field="description") if not valid.ok: return render_template("edit_ticket.html", tracker=tracker, ticket=ticket, **valid.kwargs) ticket.title = title ticket.description = desc db.session.commit() return redirect(ticket_url(ticket))
def submit_POST(): valid = Validation(request) _manifest = valid.require("manifest", friendly_name="Manifest") max_len = Job.manifest.prop.columns[0].type.length note = valid.optional("note", default="Submitted on the web") valid.expect(not _manifest or len(_manifest) < max_len, "Manifest must be less than {} bytes".format(max_len), field="manifest") if not valid.ok: return render_template("submit.html", **valid.kwargs) try: manifest = Manifest(yaml.safe_load(_manifest)) except Exception as ex: valid.error(str(ex), field="manifest") return render_template("submit.html", **valid.kwargs) job = Job(current_user, _manifest) job.note = note db.session.add(job) db.session.flush() for task in manifest.tasks: t = Task(job, task.name) db.session.add(t) db.session.flush() # assigns IDs for ordering purposes queue_build(job, manifest) # commits the session return redirect("/~" + current_user.username + "/job/" + str(job.id))
def edit_POST(task): record = GitLabCommitToBuild._GitLabCommitToBuildRecord.query.filter( GitLabCommitToBuild._GitLabCommitToBuildRecord.task_id == task.id ).one_or_none() valid = Validation(request) secrets = valid.optional("secrets", cls=bool, default=False) record.secrets = bool(secrets) db.session.commit() return redirect(url_for("html.edit_task", task_id=task.id))
def ticket_comment_POST(owner, name, ticket_id): tracker, access = get_tracker(owner, name) if not tracker: abort(404) ticket, access = get_ticket(tracker, ticket_id) if not ticket: abort(404) valid = Validation(request) text = valid.optional("comment") resolve = valid.optional("resolve") resolution = valid.optional("resolution") reopen = valid.optional("reopen") valid.expect(not text or 3 <= len(text) <= 16384, "Comment must be between 3 and 16384 characters.", field="comment") valid.expect(text or resolve or reopen, "Comment is required", field="comment") if (resolve or reopen) and TicketAccess.edit not in access: abort(403) if resolve: try: resolution = TicketResolution(int(resolution)) except Exception as ex: abort(400, "Invalid resolution") else: resolution = None if not valid.ok: ctx = get_ticket_context(ticket, tracker, access) return render_template("ticket.html", **ctx, **valid.kwargs) comment = add_comment(current_user, ticket, text=text, resolve=resolve, resolution=resolution, reopen=reopen) return redirect(ticket_url(ticket, comment))
def edit_POST(task): record = GitLabMRToBuild._GitLabMRToBuildRecord.query.filter( GitLabMRToBuild._GitLabMRToBuildRecord.task_id == task.id).one_or_none() valid = Validation(request) # TODO: Check if the repo is public/private and enable secrets if so secrets = valid.optional("secrets", cls=bool, default=False) record.secrets = bool(secrets) db.session.commit() return redirect(url_for("html.edit_task", task_id=task.id))
def create(): if not current_app.repo_api: abort(501) valid = Validation(request) repo = current_app.repo_api.create_repo(valid, current_user) if not valid.ok: return render_template("create.html", **valid.kwargs) another = valid.optional("another") if another == "on": return redirect("/create?another") else: return redirect("/~{}/{}".format(current_user.username, repo.name))
def jobs_POST(): valid = Validation(request) _manifest = valid.require("manifest", cls=str) max_len = Job.manifest.prop.columns[0].type.length valid.expect(not _manifest or len(_manifest) < max_len, "Manifest must be less than {} bytes".format(max_len), field="manifest") note = valid.optional("note", cls=str) read = valid.optional("access:read", ["*"], list) write = valid.optional("access:write", [current_token.user.username], list) secrets = valid.optional("secrets", cls=bool, default=True) tags = valid.optional("tags", [], list) valid.expect( all(re.match(r"^[A-Za-z0-9_.-]+$", tag) for tag in tags), "Invalid tag name, tags must use lowercase alphanumeric characters, underscores, dashes, or dots", field="tags") triggers = valid.optional("triggers", list(), list) execute = valid.optional("execute", True, bool) if not valid.ok: return valid.response try: manifest = Manifest(yaml.safe_load(_manifest)) except Exception as ex: valid.error(str(ex)) return valid.response # TODO: access controls job = Job(current_token.user, _manifest) job.note = note if tags: job.tags = "/".join(tags) job.secrets = secrets db.session.add(job) db.session.flush() for task in manifest.tasks: t = Task(job, task.name) db.session.add(t) db.session.flush() # assigns IDs for ordering purposes for index, trigger in enumerate(triggers): _valid = Validation(trigger) action = _valid.require("action", TriggerType) condition = _valid.require("condition", TriggerCondition) if not _valid.ok: _valid.copy(valid, "triggers[{}]".format(index)) return valid.response # TODO: Validate details based on trigger type t = Trigger(job) t.trigger_type = action t.condition = condition t.details = json.dumps(trigger) db.session.add(t) if execute: queue_build(job, manifest) # commits the session else: db.session.commit() return {"id": job.id}
def create_POST(): valid = Validation(request) name = valid.require("tracker_name", friendly_name="Name") desc = valid.optional("tracker_desc") if not valid.ok: return render_template("tracker-create.html", **valid.kwargs), 400 valid.expect(2 < len(name) < 256, "Must be between 2 and 256 characters", field="tracker_name") valid.expect(not valid.ok or name[0] in string.ascii_lowercase, "Must begin with a lowercase letter", field="tracker_name") valid.expect(not valid.ok or name_re.match(name), "Only lowercase alphanumeric characters or -.", field="tracker_name") valid.expect(not desc or len(desc) < 4096, "Must be less than 4096 characters", field="tracker_desc") if not valid.ok: return render_template("tracker-create.html", **valid.kwargs), 400 tracker = (Tracker.query.filter( Tracker.owner_id == current_user.id).filter( Tracker.name == name)).first() valid.expect(not tracker, "A tracker by this name already exists", field="tracker_name") if not valid.ok: return render_template("tracker-create.html", **valid.kwargs), 400 tracker = Tracker() tracker.owner_id = current_user.id tracker.name = name tracker.description = desc db.session.add(tracker) db.session.flush() sub = TicketSubscription() sub.tracker_id = tracker.id sub.user_id = current_user.id db.session.add(sub) db.session.commit() if "create-configure" in valid: return redirect( url_for(".settings_details_GET", owner=current_user.username, name=name)) return redirect(tracker_url(tracker))
def settings_access_POST(owner_name, repo_name): owner, repo = check_access(owner_name, repo_name, UserAccess.manage) if isinstance(repo, BaseRedirectMixin): repo = repo.new_repo valid = Validation(request) user = valid.require("user", friendly_name="User") mode = valid.optional("access", cls=AccessMode, default=AccessMode.ro) if not valid.ok: return render_template("settings_access.html", owner=owner, repo=repo, **valid.kwargs) # TODO: Group access if user[0] == "~": user = user[1:] User = current_app.User user = User.query.filter(User.username == user).first() valid.expect( user, "I don't know this user. Have they logged into this service before?", field="user") valid.expect( not user or user.id != current_user.id, "You can't adjust your own access controls. You always have full read/write access.", field="user") if not valid.ok: return render_template("settings_access.html", owner=owner, repo=repo, **valid.kwargs) Access = current_app.Access grant = (Access.query.filter(Access.repo_id == repo.id, Access.user_id == user.id)).first() if not grant: grant = Access() grant.repo_id = repo.id grant.user_id = user.id db.session.add(grant) grant.mode = mode db.session.commit() return redirect("/{}/{}/settings/access".format(owner.canonical_name, repo.name))
def settings_POST(owner_name, list_name): owner, ml, access = get_list(owner_name, list_name) if not ml: abort(404) if ml.owner_id != current_user.id: abort(401) valid = Validation(request) list_desc = valid.optional("list_desc") if list_desc == "": list_desc = None valid.expect(not list_desc or 16 < len(list_desc) < 2048, "Description must be between 16 and 2048 characters.", field="list_desc") if not valid.ok: return render_template("list-settings.html", list=ml, owner=owner, access_type_list=ListAccess, access_help_map=access_help_map, **valid.kwargs) def process(perm): bitfield = ListAccess.none for access in ListAccess: if access in [ListAccess.none]: continue if valid.optional("perm_{}_{}".format(perm, access.name)) != None: bitfield |= access return bitfield ml.description = list_desc ml.nonsubscriber_permissions = process("nonsub") ml.subscriber_permissions = process("sub") ml.account_permissions = process("account") db.session.commit() return redirect( url_for("archives.archive", owner_name=owner_name, list_name=list_name))
def settings_details_POST(owner, name): tracker, access = get_tracker(owner, name) if not tracker: abort(404) if current_user.id != tracker.owner_id: abort(403) valid = Validation(request) desc = valid.optional("tracker_desc", default=tracker.description) valid.expect(not desc or len(desc) < 4096, "Must be less than 4096 characters", field="tracker_desc") if not valid.ok: return render_template("tracker-details.html", tracker=tracker, **valid.kwargs), 400 tracker.description = desc db.session.commit() return redirect(tracker_url(tracker))
def create_list_POST(): valid = Validation(request) list_name = valid.require("list_name", friendly_name="Name") list_desc = valid.optional("list_desc") if not valid.ok: return render_template("create.html", **valid.kwargs) valid.expect(re.match(r'^[a-z._-][a-z0-9._-]*$', list_name), "Name must match [a-z._-][a-z0-9._-]*", field="list_name") existing = (List.query.filter(List.owner_id == current_user.id).filter( List.name.ilike(list_name)).first()) valid.expect(not existing, "This name is already in use.", field="list_name") valid.expect(not list_desc or 16 < len(list_desc) < 2048, "Description must be between 16 and 2048 characters.", field="list_desc") if not valid.ok: return render_template("create.html", **valid.kwargs) ml = List() ml.owner_id = current_user.id ml.name = list_name ml.description = list_desc db.session.add(ml) db.session.commit() # Auto-subscribe the owner sub = Subscription() sub.user_id = current_user.id sub.list_id = ml.id sub.confirmed = True db.session.add(sub) db.session.commit() return redirect( url_for("archives.archive", owner_name=current_user.canonical_name, list_name=ml.name))
def login_POST(): if current_user: return redirect("/") valid = Validation(request) username = valid.require("username", friendly_name="Username") password = valid.require("password", friendly_name="Password") return_to = valid.optional("return_to", "/") if not valid.ok: return render_template("login.html", valid=valid), 400 user = User.query.filter(User.username == username.lower()).one_or_none() valid.expect(user is not None, "Username or password incorrect") if valid.ok: valid.expect( bcrypt.checkpw(password.encode('utf-8'), user.password.encode('utf-8')), "Username or password incorrect") if not valid.ok: metrics.meta_logins_failed.inc() return render_template("login.html", username=username, valid=valid) factors = UserAuthFactor.query \ .filter(UserAuthFactor.user_id == user.id).all() if any(factors): session['extra_factors'] = [f.id for f in factors] session['authorized_user'] = user.id session['return_to'] = return_to return get_challenge(factors[0]) login_user(user, remember=True) audit_log("logged in") db.session.commit() metrics.meta_logins_success.inc() return redirect(return_to)
def invoice_POST(invoice_id): invoice = Invoice.query.filter(Invoice.id == invoice_id).one_or_none() if not invoice: abort(404) if (invoice.user_id != current_user.id and current_user.user_type != UserType.admin): abort(401) valid = Validation(request) bill_to = valid.optional("address-to") if not bill_to: bill_to = "~" + invoice.user.username bill_from = [l for l in [ cfg("meta.sr.ht::billing", "address-line1", default=None), cfg("meta.sr.ht::billing", "address-line2", default=None), cfg("meta.sr.ht::billing", "address-line3", default=None), cfg("meta.sr.ht::billing", "address-line4", default=None) ] if l] # Split bill_to to first row (rendered as heading) and others [bill_from_head, *bill_from_tail] = bill_from or [None] html = render_template("billing-invoice-pdf.html", invoice=invoice, amount=f"${invoice.cents / 100:.2f}", source=invoice.source, created=invoice.created.strftime("%Y-%m-%d"), valid_thru=invoice.valid_thru.strftime("%Y-%m-%d"), bill_to=bill_to, bill_from_head=bill_from_head, bill_from_tail=bill_from_tail) pdf = HTML(string=html).write_pdf() filename = f"invoice_{invoice.id}.pdf" headers = [('Content-Disposition', f'attachment; filename="{filename}"')] return Response(pdf, mimetype="application/pdf", headers=headers)
def tracker_submit_POST(owner, name): tracker, access = get_tracker(owner, name, True) if not tracker: abort(404) if not TicketAccess.submit in access: abort(403) valid = Validation(request) title = valid.require("title", friendly_name="Title") desc = valid.optional("description") another = valid.optional("another") valid.expect(not title or 3 <= len(title) <= 2048, "Title must be between 3 and 2048 characters.", field="title") valid.expect(not desc or len(desc) < 16384, "Description must be no more than 16384 characters.", field="description") if not valid.ok: db.session.commit() # Unlock tracker row return return_tracker(tracker, access, **valid.kwargs), 400 ticket = Ticket() ticket.submitter_id = current_user.id ticket.tracker_id = tracker.id ticket.scoped_id = tracker.next_ticket_id tracker.next_ticket_id += 1 ticket.title = title ticket.description = desc db.session.add(ticket) tracker.updated = datetime.utcnow() # TODO: Handle unique constraint failure (contention) and retry? db.session.commit() event = Event() event.event_type = EventType.created event.user_id = current_user.id event.ticket_id = ticket.id db.session.add(event) db.session.flush() ticket_url = url_for("ticket.ticket_GET", owner=tracker.owner.canonical_name, name=name, ticket_id=ticket.scoped_id) subscribed = False for sub in tracker.subscriptions: notification = EventNotification() notification.user_id = sub.user_id notification.event_id = event.id db.session.add(notification) if sub.user_id == ticket.submitter_id: subscribed = True continue notify(sub, "new_ticket", "{}/{}/#{}: {}".format(tracker.owner.canonical_name, tracker.name, ticket.scoped_id, ticket.title), headers={ "From": "~{} <{}>".format(current_user.username, notify_from), "Sender": smtp_user, }, ticket=ticket, ticket_url=ticket_url) if not subscribed: sub = TicketSubscription() sub.ticket_id = ticket.id sub.user_id = current_user.id db.session.add(sub) db.session.commit() if another: session["another"] = True return redirect( url_for(".tracker_GET", owner=tracker.owner.canonical_name, name=name)) else: return redirect(ticket_url)
def profile_POST(): valid = Validation(request) user = User.query.filter(User.id == current_user.id).one() email = valid.optional("email", user.email) email = email.strip() url = valid.optional("url", user.url) location = valid.optional("location", user.location) bio = valid.optional("bio", user.bio) valid.expect(not url or 0 <= len(url) <= 256, "URL must fewer than 256 characters.", "url") valid.expect(not url or valid_url(url), "URL must be a valid http or https URL", "url") valid.expect(not location or 0 <= len(location) <= 256, "Location must fewer than 256 characters.", "location") valid.expect(not bio or 0 <= len(bio) <= 4096, "Bio must fewer than 4096 characters.", "bio") if not valid.ok: return render_template("profile.html", email=email, url=url, location=location, bio=bio, valid=valid), 400 user.url = url user.location = location user.bio = bio new_email = user.email != email if new_email: valid.expect( len(email) <= 256, "Email must be no more than 256 characters.", "email") prev = User.query.filter(User.email == email).first() valid.expect(not prev, "This email address is already in use.", "email") if not valid.ok: return render_template("profile.html", email=email, url=url, location=location, bio=bio, valid=valid), 400 user.new_email = email user.gen_confirmation_hash() send_email('update_email_old', user.email, 'Your {} email address is changing'.format(site_name), new_email=email) send_email('update_email_new', user.new_email, 'Confirm your {} email address change'.format(site_name), new_email=email) audit_log("updated profile") db.session.commit() UserWebhook.deliver(UserWebhook.Events.profile_update, user.to_dict(), UserWebhook.Subscription.user_id == user.id) return render_template("profile.html", new_email=new_email)
def secrets_POST(): valid = Validation(request) name = valid.optional("name") secret_type = valid.require("secret_type", friendly_name="Secret Type") valid.expect(not name or 3 < len(name) < 512, "Name must be between 3 and 512 characters", field="name") if secret_type is not None: try: secret_type = SecretType(secret_type) except: valid.error("{} is not a valid secret type".format(secret_type), field="secret_type") if secret_type == SecretType.plaintext_file: _secret = valid.optional("secret") secret_file = valid.optional("file-file", max_file_size=16384) for f in ["secret", "file-file"]: valid.expect(bool(_secret) ^ bool(secret_file), "Either secret text or file have to be provided", field=f) if _secret: _secret = _secret.replace('\r\n', '\n') if not _secret.endswith('\n'): _secret += '\n' else: _secret = secret_file else: _secret = valid.require("secret", friendly_name="Secret") if isinstance(_secret, str): _secret = _secret.encode() if not valid.ok: return render_template("secrets.html", **valid.kwargs) secret = Secret(current_user, secret_type) if secret_type == SecretType.pgp_key: try: key, _ = pgpy.PGPKey.from_blob(_secret) if key.is_protected: valid.error("PGP key cannot be passphrase protected.", field="secret") except Exception as ex: valid.error("Invalid PGP key.", field="secret") elif secret_type == SecretType.plaintext_file: file_path = valid.require("file-path", friendly_name="Path") file_mode = valid.require("file-mode", friendly_name="Mode") if not valid.ok: return render_template("secrets.html", **valid.kwargs) try: file_mode = int(file_mode, 8) except: valid.error("Must be specified in octal", field="file-mode") if not valid.ok: return render_template("secrets.html", **valid.kwargs) secret.path = file_path secret.mode = file_mode if not valid.ok: return render_template("secrets.html", **valid.kwargs) secret.name = name secret.secret = _secret db.session.add(secret) db.session.commit() session["message"] = "Successfully added secret {}.".format(secret.uuid) return redirect("/secrets")
def register_POST(): valid = Validation(request) is_open = cfg("meta.sr.ht::settings", "registration") == "yes" username = valid.require("username", friendly_name="Username") email = valid.require("email", friendly_name="Email address") password = valid.require("password", friendly_name="Password") invite_hash = valid.optional("invite_hash") invite = None if not valid.ok: return render_template("register.html", is_open=(is_open or invite_hash is not None), **valid.kwargs), 400 if not is_open: if not invite_hash: abort(401) else: invite = (Invite.query.filter( Invite.invite_hash == invite_hash).filter( Invite.recipient_id == None)).one_or_none() if not invite: abort(401) email = email.strip() user = User.query.filter(User.username == username).first() valid.expect(user is None, "This username is already in use.", "username") user = User.query.filter(User.email == email).first() valid.expect(user is None, "This email address is already in use.", "email") valid.expect(2 <= len(username) <= 30, "Username must contain between 2 and 30 characters.", "username") valid.expect(re.match("^[a-z_]", username), "Username must start with a lowercase letter or underscore.", "username") valid.expect( re.match("^[a-z0-9_]+$", username), "Username may contain only lowercase letters, numbers and " "underscores", "username") valid.expect(username not in username_blacklist, "This username is not available", "username") valid.expect( len(email) <= 256, "Email must be no more than 256 characters.", "email") valid.expect(8 <= len(password) <= 512, "Password must be between 8 and 512 characters.", "password") if not valid.ok: return render_template("register.html", is_open=(is_open or invite_hash is not None), **valid.kwargs), 400 user = User(username) user.email = email user.password = bcrypt.hashpw(password.encode('utf-8'), salt=bcrypt.gensalt()).decode('utf-8') user.invites = cfg("meta.sr.ht::settings", "user-invites", default=0) send_email( 'confirm', user.email, 'Confirm your {} account'.format(site_name), headers={ "From": f"{cfg('sr.ht', 'owner-name')} <*****@*****.**>", "To": "{} <{}>".format(user.username, user.email), "Reply-To": f"{cfg('sr.ht', 'owner-name')} <{cfg('sr.ht', 'owner-email')}>", }, user=user) db.session.add(user) if invite: db.session.flush() invite.recipient_id = user.id metrics.meta_registrations.inc() db.session.commit() return redirect("/registered")