oficina / slides Public

Clone
git clone https://amadan.net/oficina/slides

Plain git — no account needed to clone.

Download

Download this file

1package slides
2
3import (
4 "bytes"
5 "context"
6 "database/sql"
7 "errors"
8 "fmt"
9 "net/http"
10
11 "amadan.net/oficina/home/connect"
12 "amadan.net/oficina/home/connect/linkstore"
13 "amadan.net/oficina/home/handoff"
14 "amadan.net/rastrillo/rastrillo"
15 "amadan.net/rastrillo/rastrillo/csrf"
16 "amadan.net/rastrillo/rastrillo/migrate"
17 "amadan.net/rastrillo/rastrillo/sessions"
18 "gorm.io/gorm"
19)
20
21var homeSchema = migrate.Merge(migrate.MustFromFS(handoff.Migrations, "home_handoff"), migrate.MustFromFS(linkstore.Migrations, linkstore.SchemaNamespace))
22var errHomeIdentity = errors.New("slides: Home identity refused")
23
24func neutralSession(s sessions.Session) connect.Session {
25 return connect.Session{Subject: s.Subject, Method: s.Method, AuthTime: s.AuthTime, At: s.At}
26}
27
28func (a *app) initHome(c *connect.DecodedCredentials) error {
29 if c == nil {
30 return nil
31 }
32 sess, err := sessions.New(sessions.Config{DB: a.db.Writer(), Origin: a.cfg.Origin, Logger: a.logger})
33 if err != nil {
34 return err
35 }
36 a.homeSessions = sess
37 read := func(r *http.Request) (*connect.Session, error) {
38 s, ok := sess.From(r)
39 if !ok {
40 return nil, nil
41 }
42 n := neutralSession(s)
43 return &n, nil
44 }
45 b, err := connect.New(connect.Config{DB: a.db.Writer(), Origin: a.cfg.Origin, Issuer: c.Issuer, ConsumerID: c.ConsumerID, Namespace: "oficina/slides", SessionCookie: sess.CookieName(), Keys: c.Keys, DEK: c.DEK, ReadSession: read,
46 MintHomeSession: func(w http.ResponseWriter, r *http.Request, s connect.Session) error {
47 return sess.SignIn(w, r, sessions.Session{Subject: s.Subject, Method: s.Method, AuthTime: s.AuthTime})
48 },
49 LookupLocalSession: func(ctx context.Context, s connect.Session) (connect.Person, error) {
50 if s.Method != "magiclink" && s.Method != "keymail" {
51 return connect.Person{}, errHomeIdentity
52 }
53 var u User
54 if err := a.db.G.WithContext(ctx).Where("email = ?", normalizeAddress(s.Subject)).Take(&u).Error; err != nil {
55 if errors.Is(err, gorm.ErrRecordNotFound) {
56 return connect.Person{}, sql.ErrNoRows
57 }
58 return connect.Person{}, err
59 }
60 if !a.allowed.allows(u.Email) {
61 return connect.Person{}, sql.ErrNoRows
62 }
63 return connect.Person{ID: u.ID, Label: u.Email}, nil
64 }, LookupPerson: func(ctx context.Context, q linkstore.Queryer, id int64) (string, error) {
65 var email string
66 if err := q.QueryRowContext(ctx, "SELECT email FROM users WHERE id = ?", id).Scan(&email); err != nil {
67 return "", err
68 }
69 if !a.allowed.allows(email) {
70 return "", sql.ErrNoRows
71 }
72 return email, nil
73 }, Render: a.renderHome})
74 if err != nil {
75 return err
76 }
77 a.home = b
78 a.directory = c.Directory
79 a.homeRoutes, err = connect.NewRoutes(b, connect.RoutesConfig{Namespace: "oficina/slides", Origin: a.cfg.Origin, Issuer: c.Issuer, DEK: c.DEK, SessionCookie: sess.CookieName(), ReadSession: read, SignOut: sess.SignOut, LocalSignin: a.signin, Protect: csrf.Protect(a.cfg.Origin), Render: a.renderHome, FormPolicy: connect.FormPolicy(c.Issuer)})
80 return err
81}
82
83// homeLinkView is the account-connection pages' data: connect's page
84// under this app's own bar.
85type homeLinkView struct {
86 shell
87 connect.PageData
88}
89
90// renderHome renders connect's account-connection pages, and the
91// signed-out page Sign out lands on, in this app's layout and bar.
92//
93// Referrer-Policy is strict-origin, not no-referrer (shared chrome spec
94// §9.5): these pages show the account menu once somebody is signed in,
95// and its Sign out chains to id.*/signout, which admits the hop by the
96// Referer's origin. strict-origin still sends no path or query.
97func (a *app) renderHome(w http.ResponseWriter, r *http.Request, state, label string, status int) {
98 w.Header().Set("Cache-Control", "no-store")
99 w.Header().Set("Referrer-Policy", "strict-origin")
100 if state == "signedout" {
101 // The same signed-out page standalone mode shows (auth.go), so
102 // Sign out lands on one page whichever way the person signed in.
103 a.renderSignedOut(w, r, status)
104 return
105 }
106 page := connect.Page(state, label, func(key string) string { return rastrillo.T(r, "slides.home."+key) })
107 var buf bytes.Buffer
108 if err := pages["home_link"].ExecuteTemplate(&buf, "layout", homeLinkView{shell: a.publicShell(r, titled(page.Title)), PageData: page}); err != nil {
109 a.logger.Error("slides: render Home account page", "err", err)
110 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
111 return
112 }
113 w.Header().Set("Content-Type", "text/html; charset=utf-8")
114 w.WriteHeader(status)
115 buf.WriteTo(w)
116}
117func (a *app) homeUser(r *http.Request, s sessions.Session) (User, error) {
118 if a.home == nil || s.Method != connect.HomeMethod {
119 return User{}, errHomeIdentity
120 }
121 person, err := a.home.Resolve(r.Context(), s.Subject)
122 if err != nil {
123 return User{}, err
124 }
125 var u User
126 if err := a.db.G.WithContext(r.Context()).Where("id = ?", person.ID).Take(&u).Error; err != nil {
127 if errors.Is(err, gorm.ErrRecordNotFound) {
128 return User{}, linkstore.ErrRefused
129 }
130 return User{}, fmt.Errorf("%w: %v", linkstore.ErrStorage, err)
131 }
132 if !a.allowed.allows(u.Email) {
133 return User{}, linkstore.ErrRefused
134 }
135 return u, nil
136}
137
138func (a *app) revokeHome(w http.ResponseWriter, r *http.Request, subject string) {
139 var u User
140 if a.home != nil && a.db.G.WithContext(r.Context()).Raw("SELECT u.* FROM users u JOIN home_identity_links l ON l.person_id=u.id WHERE l.issuer=? AND l.subject=?", a.cfg.Home.Issuer, subject).Scan(&u).Error == nil && u.ID != 0 && !a.allowed.allows(u.Email) {
141 a.offboard(w, r, u.Email)
142 return
143 }
144 if err := a.db.G.WithContext(r.Context()).Exec("DELETE FROM sessions WHERE method=? AND subject=?", connect.HomeMethod, subject).Error; err != nil {
145 a.logger.Error("slides: revoke Home session", "err", err)
146 }
147 if a.homeSessions != nil {
148 a.homeSessions.SignOut(w, r)
149 }
150 if r.Method == http.MethodGet || r.Method == http.MethodHead {
151 http.Redirect(w, r, "/signin?err=removed", http.StatusSeeOther)
152 } else {
153 w.WriteHeader(http.StatusForbidden)
154 }
155}
156
157type exportSessionKey struct{}
158
159func (a *app) exportPrincipal(r *http.Request) *http.Request {
160 s, ok := sessions.Current(r)
161 if !ok || s.Method != connect.HomeMethod {
162 return r
163 }
164 u, err := a.homeUser(r, s)
165 if err != nil {
166 return r
167 }
168 original := s
169 s.Subject = u.Email
170 return sessions.WithSession(r.WithContext(context.WithValue(r.Context(), exportSessionKey{}, original)), s)
171}
172func restoreExportPrincipal(r *http.Request) *http.Request {
173 if s, ok := r.Context().Value(exportSessionKey{}).(sessions.Session); ok {
174 return sessions.WithSession(r, s)
175 }
176 return r
177}
178