| 1 | package calendar |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "database/sql" |
| 7 | "errors" |
| 8 | "net/http" |
| 9 | |
| 10 | "amadan.net/oficina/home/connect" |
| 11 | "amadan.net/oficina/home/connect/linkstore" |
| 12 | "amadan.net/rastrillo/rastrillo/csrf" |
| 13 | "amadan.net/rastrillo/rastrillo/sessions" |
| 14 | ) |
| 15 | |
| 16 | type homeConnection struct { |
| 17 | bridge *connect.Bridge |
| 18 | routes *connect.Routes |
| 19 | sessions *sessions.Sessions |
| 20 | admitted allowlist |
| 21 | } |
| 22 | |
| 23 | func (a *App) newHome(settings *connect.DecodedCredentials, list allowlist) (*homeConnection, error) { |
| 24 | writer := a.db.Writer() |
| 25 | sess, err := sessions.New(sessions.Config{DB: writer, Origin: a.cfg.Origin, Logger: a.logger}) |
| 26 | if err != nil { |
| 27 | return nil, err |
| 28 | } |
| 29 | read := func(r *http.Request) (*connect.Session, error) { |
| 30 | s, ok := sess.From(r) |
| 31 | if !ok { |
| 32 | return nil, nil |
| 33 | } |
| 34 | return &connect.Session{Subject: s.Subject, Method: s.Method, AuthTime: s.AuthTime, At: s.At}, nil |
| 35 | } |
| 36 | lookup := func(ctx context.Context, q linkstore.Queryer, id int64) (string, error) { |
| 37 | var address string |
| 38 | if err := q.QueryRowContext(ctx, `SELECT address FROM people WHERE id=?`, id).Scan(&address); err != nil { |
| 39 | return "", err |
| 40 | } |
| 41 | if !list.allows(address) { |
| 42 | return "", sql.ErrNoRows |
| 43 | } |
| 44 | return address, nil |
| 45 | } |
| 46 | local := func(ctx context.Context, s connect.Session) (connect.Person, error) { |
| 47 | if s.Method != "magiclink" && s.Method != "keymail" { |
| 48 | return connect.Person{}, sql.ErrNoRows |
| 49 | } |
| 50 | var p connect.Person |
| 51 | if err := writer.QueryRowContext(ctx, `SELECT id,address FROM people WHERE address=?`, normalizeAddress(s.Subject)).Scan(&p.ID, &p.Label); err != nil { |
| 52 | return p, err |
| 53 | } |
| 54 | if !list.allows(p.Label) { |
| 55 | return connect.Person{}, sql.ErrNoRows |
| 56 | } |
| 57 | return p, nil |
| 58 | } |
| 59 | page := func(w http.ResponseWriter, r *http.Request, state, label string, status int) { |
| 60 | data := connect.Page(state, label) |
| 61 | // Where sign-out lands is the Oficina door, in the door's words. |
| 62 | tree, title, view := pages["home"], data.Title, any(data) |
| 63 | if state == "signedout" { |
| 64 | notice := signedOutDoor(r, data.Action) |
| 65 | tree, title, view = pages["signin"], notice.Page(), signinData{SignedOut: ¬ice} |
| 66 | } |
| 67 | var buf bytes.Buffer |
| 68 | if err := tree.ExecuteTemplate(&buf, "layout", a.h.page(r, title, view)); err != nil { |
| 69 | a.h.errorPage(w, r, http.StatusInternalServerError, "") |
| 70 | return |
| 71 | } |
| 72 | w.Header().Set("Cache-Control", "no-store") |
| 73 | w.Header().Set("Content-Type", "text/html; charset=utf-8") |
| 74 | w.WriteHeader(status) |
| 75 | _, _ = buf.WriteTo(w) |
| 76 | } |
| 77 | b, err := connect.New(connect.Config{DB: writer, Origin: a.cfg.Origin, Issuer: settings.Issuer, ConsumerID: settings.ConsumerID, Keys: settings.Keys, DEK: settings.DEK, Namespace: "oficina/calendar", SessionCookie: sess.CookieName(), ReadSession: read, LookupLocalSession: local, LookupPerson: lookup, Render: page, MintHomeSession: func(w http.ResponseWriter, r *http.Request, s connect.Session) error { |
| 78 | return sess.SignIn(w, r, sessions.Session{Subject: s.Subject, Method: s.Method, AuthTime: s.AuthTime}) |
| 79 | }}) |
| 80 | if err != nil { |
| 81 | return nil, err |
| 82 | } |
| 83 | routes, err := connect.NewRoutes(b, connect.RoutesConfig{Namespace: "oficina/calendar", Origin: a.cfg.Origin, Issuer: settings.Issuer, SessionCookie: sess.CookieName(), DEK: settings.DEK, ReadSession: read, SignOut: sess.SignOut, LocalSignin: a.h.signin, Protect: csrf.Protect(a.cfg.Origin), Render: page}) |
| 84 | if err != nil { |
| 85 | return nil, err |
| 86 | } |
| 87 | return &homeConnection{bridge: b, routes: routes, sessions: sess, admitted: list}, nil |
| 88 | } |
| 89 | |
| 90 | func (a *App) homePerson(r *http.Request) (Person, error) { |
| 91 | p, err := a.home.bridge.ResolveSession(r) |
| 92 | if err != nil { |
| 93 | return Person{}, err |
| 94 | } |
| 95 | var person Person |
| 96 | if err = a.db.G.WithContext(r.Context()).First(&person, p.ID).Error; err != nil { |
| 97 | return Person{}, err |
| 98 | } |
| 99 | if person.ID != p.ID || person.Address != p.Label { |
| 100 | return Person{}, errors.New("calendar: identity unavailable") |
| 101 | } |
| 102 | return person, nil |
| 103 | } |
| 104 | |
| 105 | // homeSignin is GET /signin with Home on. connect's Signin shortcuts a |
| 106 | // signed-in person straight to where they were headed; but a session whose |
| 107 | // person is no longer admitted (removed from the team, or a Home link to a |
| 108 | // deleted person) is not signed in for Calendar, and the shortcut would end |
| 109 | // on requirePerson's 404 with nowhere to go. So /signin first asks the same |
| 110 | // question requirePerson does. Not admitted: the session ends here and |
| 111 | // /signin is asked again without it, which starts a fresh exchange (as |
| 112 | // sheets' homeRequire and docs' admission check do). A storage failure is |
| 113 | // a 503, never a sign-out. |
| 114 | func (a *App) homeSignin(w http.ResponseWriter, r *http.Request) { |
| 115 | s, ok := a.home.sessions.From(r) |
| 116 | if ok && (s.Method == connect.HomeMethod || s.Method == "magiclink" || s.Method == "keymail") { |
| 117 | admitted := false |
| 118 | if s.Method == connect.HomeMethod { |
| 119 | _, err := a.homePerson(r) |
| 120 | if errors.Is(err, linkstore.ErrStorage) { |
| 121 | w.Header().Set("Cache-Control", "no-store") |
| 122 | a.h.errorPage(w, r, http.StatusServiceUnavailable, "") |
| 123 | return |
| 124 | } |
| 125 | admitted = err == nil |
| 126 | } else { |
| 127 | admitted = a.home.admitted.allows(normalizeAddress(s.Subject)) |
| 128 | } |
| 129 | if !admitted { |
| 130 | a.home.sessions.SignOut(w, r) |
| 131 | w.Header().Set("Cache-Control", "no-store") |
| 132 | http.Redirect(w, r, r.URL.RequestURI(), http.StatusSeeOther) |
| 133 | return |
| 134 | } |
| 135 | } |
| 136 | a.home.routes.Signin(w, r) |
| 137 | } |
| 138 | |