From fc70ae13a47f976dfa773586daee982be6231654 Mon Sep 17 00:00:00 2001 From: Adnan Maolood Date: Tue, 8 Nov 2022 07:16:22 -0500 Subject: [PATCH] api: Implement tracker dump signature verification Changes the ticket and comment signature format to remove useless fields. Also updates the frontend to use the GraphQL API for tracker dump imports. --- api/trackers/import.go | 236 +++++++++++------- todosrht/blueprints/settings.py | 141 ++++++++--- todosrht/templates/tracker-import-export.html | 15 +- todosrht/tracker_import.py | 207 --------------- todosrht/webhooks.py | 2 - 5 files changed, 264 insertions(+), 337 deletions(-) delete mode 100644 todosrht/tracker_import.py diff --git a/api/trackers/import.go b/api/trackers/import.go index 51cc0a0..4f2ab0c 100644 --- a/api/trackers/import.go +++ b/api/trackers/import.go @@ -7,65 +7,51 @@ import ( "errors" "fmt" "io" + "log" "strings" "time" + "git.sr.ht/~sircmpwn/core-go/crypto" "git.sr.ht/~sircmpwn/core-go/database" "git.sr.ht/~sircmpwn/todo.sr.ht/api/graph/model" "git.sr.ht/~sircmpwn/todo.sr.ht/api/loaders" ) type TrackerDump struct { - Owner Owner `json:"owner"` - Name string `json:"name"` - Labels []Label `json:"labels"` - Tickets []Ticket `json:"tickets"` -} - -type Owner struct { - CanonicalName string `json:"canonical_name"` - Name string `json:"name"` - Email string `json:"email"` - URL string `json:"url"` - Location string `json:"location"` - Bio string `json:"bio"` + ID int `json:"id"` + Owner User `json:"owner"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Name string `json:"name"` + Description string `json:"description"` + Labels []Label `json:"labels"` + Tickets []Ticket `json:"tickets"` } type Label struct { - Name string `json:"name"` - Colors struct { - Background string `json:"background"` - Foreground string `json:"text"` - } `json:"colors"` - Created time.Time `json:"created"` - Tracker Tracker `json:"tracker"` -} - -type Tracker struct { - ID int `json:"id"` - Owner User `json:"owner"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - Name string `json:"name"` + ID int `json:"id"` + Created time.Time `json:"created"` + Name string `json:"name"` + BackgroundColor string `json:"background_color"` + ForegroundColor string `json:"foreground_color"` } type Ticket struct { - ID int `json:"id"` - Ref string `json:"ref"` - Tracker Tracker `json:"tracker"` - Subject string `json:"title"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - Submitter *Participant `json:"submitter"` // null in shorter ticket dicts - Body string `json:"description"` - Status string `json:"status"` - Resolution string `json:"resolution"` - Labels []string `json:"labels"` - Assignees []User `json:"assignees"` - Upstream string `json:"upstream"` - Signature string `json:"X-Payload-Signature"` - Nonce string `json:"X-Payload-Nonce"` - Events []Event `json:"events"` + ID int `json:"id"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Submitter Participant `json:"submitter"` + Ref string `json:"ref"` + Subject string `json:"subject"` + Body string `json:"body"` + Status string `json:"status"` + Resolution string `json:"resolution"` + Labels []string `json:"labels"` + Assignees []User `json:"assignees"` + Upstream string `json:"upstream"` + Signature string `json:"X-Payload-Signature"` + Nonce string `json:"X-Payload-Nonce"` + Events []Event `json:"events"` } type Event struct { @@ -76,8 +62,7 @@ type Event struct { OldResolution *string `json:"old_resolution"` NewStatus *string `json:"new_status"` NewResolution *string `json:"new_resolution"` - User *Participant `json:"user"` - Ticket *Ticket `json:"ticket"` + Participant *Participant `json:"participant"` Comment *Comment `json:"comment"` Label *string `json:"label"` ByUser *Participant `json:"by_user"` @@ -89,6 +74,7 @@ type Event struct { type Participant struct { Type string `json:"type"` + UserID int `json:"user_id"` CanonicalName string `json:"canonical_name"` Name string `json:"name"` Address string `json:"address"` @@ -97,15 +83,33 @@ type Participant struct { } type User struct { + ID int `json:"id"` CanonicalName string `json:"canonical_name"` Name string `json:"name"` } type Comment struct { - ID int `json:"id"` - Created time.Time `json:"created"` - Submitter Participant `json:"submitter"` - Text string `json:"text"` + ID int `json:"id"` + Created time.Time `json:"created"` + Author Participant `json:"author"` + Text string `json:"text"` +} + +type TicketSignatureData struct { + TrackerID int `json:"tracker_id"` + TicketID int `json:"ticket_id"` + Subject string `json:"subject"` + Body string `json:"body"` + SubmitterID int `json:"submitter_id"` + Upstream string `json:"upstream"` +} + +type CommentSignatureData struct { + TrackerID int `json:"tracker_id"` + TicketID int `json:"ticket_id"` + Comment string `json:"comment"` + AuthorID int `json:"author_id"` + Upstream string `json:"upstream"` } func importParticipant(ctx context.Context, part Participant, upstream, ourUpstream string) (int, error) { @@ -192,29 +196,9 @@ func importTrackerDump(ctx context.Context, trackerID int, dump io.Reader, ourUp return err } - // Create labels - labelIDs := map[string]int{} - if err := database.WithTx(ctx, nil, func(tx *sql.Tx) error { - for _, label := range tracker.Labels { - row := tx.QueryRowContext(ctx, ` - INSERT INTO label ( - created, updated, tracker_id, name, color, text_color - ) VALUES ( - $1, $1, $2, $3, $4, $5 - ) RETURNING id - `, label.Created, trackerID, label.Name, label.Colors.Background, label.Colors.Foreground) - var labelID int - if err := row.Scan(&labelID); err != nil { - return err - } - labelIDs[label.Name] = labelID - } - return nil - }); err != nil { - return err - } - defer func() { + r := recover() + if err := database.WithTx(ctx, nil, func(tx *sql.Tx) error { _, err := tx.ExecContext(ctx, ` UPDATE tracker @@ -225,9 +209,31 @@ func importTrackerDump(ctx context.Context, trackerID int, dump io.Reader, ourUp }); err != nil { panic(err) } + + if r != nil { + panic(r) + } }() if err := database.WithTx(ctx, nil, func(tx *sql.Tx) error { + // Create labels + labelIDs := map[string]int{} + for _, label := range tracker.Labels { + row := tx.QueryRowContext(ctx, ` + INSERT INTO label ( + created, updated, tracker_id, name, color, text_color + ) VALUES ( + $1, $1, $2, $3, $4, $5 + ) RETURNING id + `, label.Created, trackerID, label.Name, + label.BackgroundColor, label.ForegroundColor) + var labelID int + if err := row.Scan(&labelID); err != nil { + return err + } + labelIDs[label.Name] = labelID + } + var nextTicketID int row := tx.QueryRowContext(ctx, `SELECT next_ticket_id FROM tracker WHERE id = $1`, @@ -244,11 +250,32 @@ func importTrackerDump(ctx context.Context, trackerID int, dump io.Reader, ourUp var maxTicketID int for _, ticket := range tracker.Tickets { - submitterID, err := importParticipant(ctx, *ticket.Submitter, ticket.Upstream, ourUpstream) + submitterID, err := importParticipant(ctx, ticket.Submitter, ticket.Upstream, ourUpstream) if err != nil { return err } + ticketAuthenticity := model.AUTH_UNAUTHENTICATED + if ticket.Upstream == ourUpstream && ticket.Submitter.Type == "user" { + sigdata := TicketSignatureData{ + TrackerID: tracker.ID, + TicketID: ticket.ID, + Subject: ticket.Subject, + Body: ticket.Body, + SubmitterID: ticket.Submitter.UserID, + Upstream: ticket.Upstream, + } + payload, err := json.Marshal(sigdata) + if err != nil { + panic(err) + } + if crypto.VerifyWebhook(payload, ticket.Nonce, ticket.Signature) { + ticketAuthenticity = model.AUTH_AUTHENTIC + } else { + ticketAuthenticity = model.AUTH_TAMPERED + } + } + // Compute the max ticket ID. We can't use the number of tickets as // the next ticket ID because that won't include deleted tickets if ticket.ID > maxTicketID { @@ -268,9 +295,9 @@ func importTrackerDump(ctx context.Context, trackerID int, dump io.Reader, ourUp RETURNING id `, ticket.Created, ticket.Updated, trackerID, ticket.ID, submitterID, ticket.Subject, ticket.Body, - model.TicketStatus(strings.ToUpper(ticket.Status)).ToInt(), - model.TicketResolution(strings.ToUpper(ticket.Resolution)).ToInt(), - model.AUTH_UNAUTHENTICATED) + model.TicketStatus(ticket.Status).ToInt(), + model.TicketResolution(ticket.Resolution).ToInt(), + ticketAuthenticity) var ticketPKID int if err := row.Scan(&ticketPKID); err != nil { return err @@ -307,26 +334,51 @@ func importTrackerDump(ctx context.Context, trackerID int, dump io.Reader, ourUp var eventType int for _, etype := range event.EventType { + etype := model.EventType(etype) + if !etype.IsValid() { + fmt.Errorf("failed to import ticket #%d: invalid event type %s", ticket.ID, etype) + } eventType |= eventTypeMap[etype] } if eventType == 0 { return fmt.Errorf("failed to import ticket #%d: invalid ticket event", ticket.ID, eventType) } - if event.User != nil { - userPartID, err := importParticipant(ctx, *event.User, event.Upstream, ourUpstream) + if event.Participant != nil { + eventPartID, err := importParticipant(ctx, *event.Participant, event.Upstream, ourUpstream) if err != nil { return err } - partID = &userPartID + partID = &eventPartID } if eventType&model.EVENT_COMMENT != 0 { - submitterID, err := importParticipant(ctx, event.Comment.Submitter, event.Upstream, ourUpstream) + authorID, err := importParticipant(ctx, event.Comment.Author, event.Upstream, ourUpstream) if err != nil { return err } + commentAuthenticity := model.AUTH_UNAUTHENTICATED + if event.Upstream == ourUpstream && event.Comment.Author.Type == "user" { + log.Println(event.Comment.Author.UserID) + sigdata := CommentSignatureData{ + TrackerID: tracker.ID, + TicketID: ticket.ID, + Comment: event.Comment.Text, + AuthorID: event.Comment.Author.UserID, + Upstream: event.Upstream, + } + payload, err := json.Marshal(sigdata) + if err != nil { + panic(err) + } + if crypto.VerifyWebhook(payload, event.Nonce, event.Signature) { + commentAuthenticity = model.AUTH_AUTHENTIC + } else { + commentAuthenticity = model.AUTH_TAMPERED + } + } + row := tx.QueryRowContext(ctx, ` INSERT INTO ticket_comment ( created, updated, submitter_id, ticket_id, text, @@ -334,8 +386,8 @@ func importTrackerDump(ctx context.Context, trackerID int, dump io.Reader, ourUp ) VALUES ( $1, $1, $2, $3, $4, $5 ) RETURNING id - `, event.Comment.Created, submitterID, ticketPKID, event.Comment.Text, - model.AUTH_UNAUTHENTICATED) + `, event.Comment.Created, authorID, ticketPKID, event.Comment.Text, + commentAuthenticity) var _commentID int if err := row.Scan(&_commentID); err != nil { return err @@ -453,14 +505,14 @@ func convertResolutionToInt(resolution *string) *int { return &resolutionInt } -var eventTypeMap = map[string]int{ - "created": model.EVENT_CREATED, - "comment": model.EVENT_COMMENT, - "status_change": model.EVENT_STATUS_CHANGE, - "label_added": model.EVENT_LABEL_ADDED, - "label_removed": model.EVENT_LABEL_REMOVED, - "assigned_user": model.EVENT_ASSIGNED_USER, - "unassigned_user": model.EVENT_UNASSIGNED_USER, - "user_mentioned": model.EVENT_USER_MENTIONED, - "ticket_mentioned": model.EVENT_TICKET_MENTIONED, +var eventTypeMap = map[model.EventType]int{ + model.EventTypeCreated: model.EVENT_CREATED, + model.EventTypeComment: model.EVENT_COMMENT, + model.EventTypeStatusChange: model.EVENT_STATUS_CHANGE, + model.EventTypeLabelAdded: model.EVENT_LABEL_ADDED, + model.EventTypeLabelRemoved: model.EVENT_LABEL_REMOVED, + model.EventTypeAssignedUser: model.EVENT_ASSIGNED_USER, + model.EventTypeUnassignedUser: model.EVENT_UNASSIGNED_USER, + model.EventTypeUserMentioned: model.EVENT_USER_MENTIONED, + model.EventTypeTicketMentioned: model.EVENT_TICKET_MENTIONED, } diff --git a/todosrht/blueprints/settings.py b/todosrht/blueprints/settings.py index 1579ba6..5c1abc5 100644 --- a/todosrht/blueprints/settings.py +++ b/todosrht/blueprints/settings.py @@ -9,7 +9,7 @@ from srht.crypto import sign_payload from srht.database import db from srht.oauth import current_user, loginrequired from srht.flask import date_handler, session -from srht.graphql import exec_gql +from srht.graphql import exec_gql, GraphQLOperation, GraphQLUpload from srht.validation import Validation from tempfile import NamedTemporaryFile from todosrht.access import get_tracker @@ -17,7 +17,6 @@ from todosrht.trackers import get_recent_users from todosrht.types import Event, EventType, Ticket, TicketAccess, Visibility from todosrht.types import ParticipantType, UserAccess, User from todosrht.urls import tracker_url -from todosrht.tracker_import import tracker_import settings = Blueprint("settings", __name__) @@ -243,20 +242,57 @@ def export_POST(owner, name): if current_user.id != tracker.owner_id: abort(403) + upstream = get_origin("todo.sr.ht", external=True) + + def participant_to_dict(self): + if self.participant_type == ParticipantType.user: + return { + "type": "user", + "user_id": self.user.id, + "canonical_name": self.user.canonical_name, + "name": self.user.username, + } + elif self.participant_type == ParticipantType.email: + return { + "type": "email", + "address": self.email, + "name": self.email_name, + } + elif self.participant_type == ParticipantType.external: + return { + "type": "external", + "external_id": self.external_id, + "external_url": self.external_url, + } + assert False + dump = list() tickets = Ticket.query.filter(Ticket.tracker_id == tracker.id).all() for ticket in tickets: - td = ticket.to_dict() - td["upstream"] = get_origin("todo.sr.ht", external=True) + td = { + "id": ticket.scoped_id, + "created": ticket.created, + "updated": ticket.updated, + "submitter": participant_to_dict(ticket.submitter), + "ref": ticket.ref(), + "subject": ticket.title, + "body": ticket.description, + "status": ticket.status.name.upper(), + "resolution": ticket.resolution.name.upper(), + "labels": [l.name for l in ticket.labels], + "assignees": [u.to_dict(short=True) for u in ticket.assigned_users], + } + td["upstream"] = upstream if ticket.submitter.participant_type == ParticipantType.user: sigdata = OrderedDict({ - "description": ticket.description, - "ref": ticket.ref(), - "submitter": ticket.submitter.user.canonical_name, - "title": ticket.title, - "upstream": get_origin("todo.sr.ht", external=True), + "tracker_id": tracker.id, + "ticket_id": ticket.scoped_id, + "subject": ticket.title, + "body": ticket.description, + "submitter_id": ticket.submitter.user.id, + "upstream": upstream, }) - sigdata = json.dumps(sigdata) + sigdata = json.dumps(sigdata, separators=(',',':')) signature = sign_payload(sigdata) td.update(signature) @@ -264,27 +300,64 @@ def export_POST(owner, name): if any(events): td["events"] = list() for event in events: - ev = event.to_dict() - ev["upstream"] = get_origin("todo.sr.ht", external=True) + ev = { + "id": event.id, + "created": event.created, + "event_type": [t.name.upper() for t in EventType if t in event.event_type], + "old_status": event.old_status.name.upper() + if event.old_status else None, + "old_resolution": event.old_resolution.name.upper() + if event.old_resolution else None, + "new_status": event.new_status.name.upper() + if event.new_status else None, + "new_resolution": event.new_resolution.name.upper() + if event.new_resolution else None, + "participant": participant_to_dict(event.participant) + if event.participant else None, + "ticket_id": event.ticket.scoped_id + if event.ticket else None, + "comment": { + "id": event.comment.id, + "created": event.comment.created, + "author": participant_to_dict(event.comment.submitter), + "text": event.comment.text, + } if event.comment else None, + "label": event.label.name if event.label else None, + "by_user": participant_to_dict(event.by_participant) + if event.by_participant else None, + "from_ticket_id": event.from_ticket.scoped_id + if event.from_ticket else None, + } + ev["upstream"] = upstream if (EventType.comment in event.event_type and event.participant.participant_type == ParticipantType.user): sigdata = OrderedDict({ + "tracker_id": tracker.id, + "ticket_id": ticket.scoped_id, "comment": event.comment.text, - "id": event.id, - "ticket": event.ticket.ref(), - "user": event.participant.user.canonical_name, - "upstream": get_origin("todo.sr.ht", external=True), + "author_id": event.comment.submitter.user.id, + "upstream": upstream }) - sigdata = json.dumps(sigdata) + sigdata = json.dumps(sigdata, separators=(',',':')) signature = sign_payload(sigdata) ev.update(signature) td["events"].append(ev) dump.append(td) dump = json.dumps({ - "owner": tracker.owner.to_dict(), + "id": tracker.id, + "owner": tracker.owner.to_dict(short=True), + "created": tracker.created, + "updated": tracker.updated, "name": tracker.name, - "labels": [l.to_dict() for l in tracker.labels], + "description": tracker.description, + "labels": [{ + "id": l.id, + "created": l.created, + "name": l.name, + "background_color": l.color, + "foreground_color": l.text_color, + } for l in tracker.labels], "tickets": dump, }, default=date_handler) with NamedTemporaryFile() as ntf: @@ -304,22 +377,32 @@ def import_POST(owner, name): if current_user.id != tracker.owner_id: abort(403) - dump = request.files.get("dump") valid = Validation(request) + dump = request.files.get("dump") valid.expect(dump is not None, "Tracker dump file is required", field="dump") + if not valid.ok: return render_template("tracker-import-export.html", view="import/export", tracker=tracker, **valid.kwargs) - try: - dump = dump.stream.read() - dump = gzip.decompress(dump) - dump = json.loads(dump) - except: - abort(400) - tracker_import.delay(dump, tracker.id) + op = GraphQLOperation(""" + mutation ImportTrackerDump($trackerId: Int!, $dump: Upload!) { + importTrackerDump(trackerId: $trackerId, dump: $dump) + } + """) + + dump = GraphQLUpload( + dump.filename, + dump.stream, + "application/octet-stream", + ) + op.var("trackerId", tracker.id) + op.var("dump", dump) + op.execute("todo.sr.ht", valid=valid) + + if not valid.ok: + return render_template("tracker-import-export.html", + view="import/export", tracker=tracker, **valid.kwargs) - tracker.import_in_progress = True - db.session.commit() return redirect(tracker_url(tracker)) diff --git a/todosrht/templates/tracker-import-export.html b/todosrht/templates/tracker-import-export.html index edd162b..0e43e69 100644 --- a/todosrht/templates/tracker-import-export.html +++ b/todosrht/templates/tracker-import-export.html @@ -36,20 +36,21 @@ after preparing the following JSON data as UTF-8:

{
-  "description": $description,
-  "ref": "{{tracker.ref()}}#$id",
-  "submitter": "~$username",
-  "title": $title,
+  "tracker_id": $trackerId,
+  "ticket_id": $ticketId,
+  "subject": $subject,
+  "body": $body,
+  "submitter_id": "$userId",
   "upstream": "{{get_origin('todo.sr.ht', external=True)}}",
 }

Comments use the same process with the following JSON data as UTF-8:

{
+  "tracker_id": $trackerId,
+  "ticket_id": $ticketId,
   "comment": $comment,
-  "id": $id,
-  "ticket": "{{tracker.ref()}}#$id",
-  "user": "~$username",
+  "author_id": "$userId",
   "upstream": "{{get_origin('todo.sr.ht', external=True)}}",
 }

diff --git a/todosrht/tracker_import.py b/todosrht/tracker_import.py deleted file mode 100644 index e68a59f..0000000 --- a/todosrht/tracker_import.py +++ /dev/null @@ -1,207 +0,0 @@ -import json -from collections import OrderedDict -from datetime import datetime, timezone -from srht.crypto import verify_payload -from srht.config import get_origin -from srht.database import db -from todosrht.tickets import submit_ticket -from todosrht.tickets import get_participant_for_email -from todosrht.tickets import get_participant_for_external -from todosrht.tickets import get_participant_for_user -from todosrht.types import Event, EventType, Tracker, Ticket, TicketComment -from todosrht.types import ParticipantType, User -from todosrht.types import Label, TicketLabel -from todosrht.types import TicketStatus, TicketResolution -from todosrht.types import TicketAuthenticity -from todosrht.webhooks import worker - -our_upstream = get_origin("todo.sr.ht", external=True) - -def _parse_date(date): - date = datetime.fromisoformat(date) - date = date.astimezone(timezone.utc).replace(tzinfo=None) - return date - -def _import_participant(pdata, upstream): - if pdata["type"] == "user": - if upstream == our_upstream: - user = User.query.filter(User.username == pdata["name"]).first() - else: - user = None - - if user: - submitter = get_participant_for_user(user) - else: - submitter = get_participant_for_external( - pdata["canonical_name"], - upstream + "/" + pdata["canonical_name"]) - elif pdata["type"] == "email": - submitter = get_participant_for_email(pdata["address"], pdata["name"]) - elif pdata["type"] == "external": - submitter = get_participant_for_external( - pdata["external_id"], pdata["external_url"]) - return submitter - -def _import_comment(ticket, event, edata): - cdata = edata["comment"] - comment = TicketComment() - submitter = _import_participant(cdata["submitter"], edata["upstream"]) - comment.submitter_id = submitter.id - comment.ticket_id = ticket.id - comment.text = cdata["text"] - comment.authenticity = TicketAuthenticity.unauthenticated - comment.created = _parse_date(cdata["created"]) - comment.updated = comment.created - comment._no_autoupdate = True - signature, nonce = edata.get("X-Payload-Signature"), edata.get("X-Payload-Nonce") - if (signature and nonce - and edata["upstream"] == our_upstream - and submitter.participant_type == ParticipantType.user): - # TODO: Validate signatures from trusted third-parties - sigdata = OrderedDict({ - "comment": comment.text, - "id": edata["id"], # not important to verify this - "ticket": edata["ticket"]["ref"], # not important to verify this - "user": submitter.user.canonical_name, - "upstream": edata["upstream"], - }) - sigdata = json.dumps(sigdata) - if verify_payload(sigdata, signature, nonce): - comment.authenticity = TicketAuthenticity.authentic - else: - comment.authenticity = TicketAuthenticity.tampered - ticket.comment_count += 1 - db.session.add(comment) - db.session.flush() - event.comment_id = comment.id - -def _tracker_import(dump, tracker): - ldict = dict() - for ldata in dump["labels"]: - label = Label() - label.tracker_id = tracker.id - label.name = ldata["name"] - label.color = ldata["colors"]["background"] - label.text_color = ldata["colors"]["text"] - db.session.add(label) - db.session.flush() - ldict[label.name] = label.id - tickets = sorted(dump["tickets"], key=lambda t: t["id"]) - for tdata in tickets: - for field in [ - "id", "title", "created", "description", "status", "resolution", - "labels", "assignees", "upstream", "events", "submitter", - ]: - if not field in tdata: - print("Invalid ticket data") - continue - ticket = Ticket.query.filter( - Ticket.tracker_id == tracker.id, - Ticket.scoped_id == tdata["id"]).one_or_none() - if ticket: - print(f"Ticket {tdata['id']} already imported - skipping") - continue - submitter = _import_participant(tdata["submitter"], tdata["upstream"]) - ticket = submit_ticket(tracker, submitter, - tdata["title"], tdata["description"], importing=True) - try: - created = _parse_date(tdata["created"]) - except ValueError: - created = datetime.utcnow() - ticket._no_autoupdate = True - ticket.created = created - ticket.updated = created - ticket.status = TicketStatus[tdata["status"]] - ticket.resolution = TicketResolution[tdata["resolution"]] - ticket.authenticity = TicketAuthenticity.unauthenticated - for label in tdata["labels"]: - tl = TicketLabel() - tl.ticket_id = ticket.id - tl.label_id = ldict.get(label) - tl.user_id = tracker.owner_id - db.session.add(tl) - # TODO: assignees - signature, nonce = tdata.get("X-Payload-Signature"), tdata.get("X-Payload-Nonce") - if (signature and nonce - and tdata["upstream"] == our_upstream - and submitter.participant_type == ParticipantType.user): - # TODO: Validate signatures from trusted third-parties - sigdata = OrderedDict({ - "description": ticket.description, - "ref": tdata["ref"], # not important to verify this - "submitter": ticket.submitter.user.canonical_name, - "title": ticket.title, - "upstream": tdata["upstream"], - }) - sigdata = json.dumps(sigdata) - if verify_payload(sigdata, signature, nonce): - ticket.authenticity = TicketAuthenticity.authentic - else: - ticket.authenticity = TicketAuthenticity.tampered - for edata in tdata.get("events", []): - for field in [ - "created", "event_type", "old_status", "new_status", - "old_resolution", "new_resolution", "user", "ticket", - "comment", "label", "by_user", "from_ticket", "upstream", - ]: - if not field in edata: - print("Invalid ticket event") - return - event = Event() - for etype in edata["event_type"]: - if event.event_type == None: - event.event_type = EventType[etype] - else: - event.event_type |= EventType[etype] - if event.event_type == None: - print("Invalid ticket event") - continue - if EventType.comment in event.event_type: - _import_comment(ticket, event, edata) - if EventType.status_change in event.event_type: - if edata["old_status"]: - event.old_status = TicketStatus[edata["old_status"]] - if edata["new_status"]: - event.new_status = TicketStatus[edata["new_status"]] - if EventType.label_added in event.event_type: - event.label_id = ldict.get(edata["label"]) - if not event.label_id: - continue - if EventType.label_removed in event.event_type: - event.label_id = ldict.get(edata["label"]) - if not event.label_id: - continue - if EventType.assigned_user in event.event_type: - by_participant = _import_participant( - edata["by_user"], edata["upstream"]) - event.by_participant_id = by_participant.id - if EventType.unassigned_user in event.event_type: - by_participant = _import_participant( - edata["by_user"], edata["upstream"]) - event.by_participant_id = by_participant.id - if EventType.user_mentioned in event.event_type: - continue # Magic event type, do not import - if EventType.ticket_mentioned in event.event_type: - continue # TODO: Could reference tickets imported in later iters - event.created = _parse_date(edata["created"]) - event.updated = event.created - event._no_autoupdate = True - event.ticket_id = ticket.id - participant = _import_participant(edata["user"], edata["upstream"]) - event.participant_id = participant.id - db.session.add(event) - print(f"Imported {ticket.ref()}") - -@worker.task -def tracker_import(dump, tracker_id): - tracker = Tracker.query.get(tracker_id) - try: - _tracker_import(dump, tracker) - except: - # TODO: Tell user that the import failed? - db.session.rollback() - tracker = Tracker.query.get(tracker_id) - raise - finally: - tracker.import_in_progress = False - db.session.commit() diff --git a/todosrht/webhooks.py b/todosrht/webhooks.py index eb8e08a..5a06bab 100644 --- a/todosrht/webhooks.py +++ b/todosrht/webhooks.py @@ -15,8 +15,6 @@ webhooks_broker = cfg("todo.sr.ht", "webhooks") worker = make_worker(broker=webhooks_broker) webhook_metrics_collector = RedisQueueCollector(webhooks_broker, "srht_webhooks", "Webhook queue length") -import todosrht.tracker_import - class UserWebhook(CeleryWebhook): events = [ Event("tracker:create", "trackers:read"),