all repos — postern @ main

Modern mail management

internal/policy/user.go (view raw)

  1package policy
  2
  3import (
  4	"context"
  5	"database/sql"
  6	"errors"
  7	"log/slog"
  8
  9	"postern/internal/db"
 10	"postern/internal/model"
 11
 12	"go.starlark.net/starlark"
 13)
 14
 15type UsersModule struct {
 16	db *db.DB
 17}
 18
 19func NewUsersModule(db *db.DB) *UsersModule {
 20	return &UsersModule{
 21		db: db,
 22	}
 23}
 24
 25func (u *UsersModule) String() string       { return "<module users>" }
 26func (u *UsersModule) Type() string         { return "module" }
 27func (u *UsersModule) Freeze()              {}
 28func (u *UsersModule) Truth() starlark.Bool { return true }
 29func (u *UsersModule) Hash() (uint32, error) {
 30	return 0, nil
 31}
 32
 33func (u *UsersModule) Attr(name string) (starlark.Value, error) {
 34	switch name {
 35	case "get":
 36		return starlark.NewBuiltin("get", u.usersGet), nil
 37	case "get_by_address":
 38		return starlark.NewBuiltin("get_by_address", u.usersGetByAddress), nil
 39	}
 40	return nil, nil
 41}
 42
 43func (u *UsersModule) AttrNames() []string {
 44	return []string{"get", "get_by_address"}
 45}
 46
 47type User struct {
 48	Name       starlark.String
 49	Disabled   starlark.Bool
 50	Addresses  starlark.Tuple
 51	gatekeeper *UserGatekeeper
 52	user       model.User
 53}
 54
 55func (u User) String() string {
 56	return "<User " + u.Name.GoString() + ">"
 57}
 58
 59func (u User) Type() string         { return "User" }
 60func (u User) Freeze()              {}
 61func (u User) Truth() starlark.Bool { return true }
 62func (u User) Hash() (uint32, error) {
 63	return 0, nil
 64}
 65
 66func (u User) Attr(name string) (starlark.Value, error) {
 67	switch name {
 68	case "name":
 69		return u.Name, nil
 70	case "is_disabled":
 71		return u.Disabled, nil
 72	case "addresses":
 73		return u.Addresses, nil
 74	case "gatekeeper":
 75		if u.gatekeeper == nil {
 76			return starlark.None, nil
 77		}
 78		return u.gatekeeper, nil
 79	}
 80	return nil, nil
 81}
 82
 83func (u User) AttrNames() []string {
 84	return []string{"name", "is_disabled", "addresses", "gatekeeper"}
 85}
 86
 87func (u *UsersModule) usersGet(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) {
 88	var username starlark.String
 89	if err := starlark.UnpackPositionalArgs("get", args, nil, 1, &username); err != nil {
 90		return nil, err
 91	}
 92
 93	user, err := u.db.GetUser(context.Background(), username.GoString())
 94	if err != nil {
 95		if errors.Is(err, sql.ErrNoRows) {
 96			return starlark.None, nil
 97		}
 98		return nil, err
 99	}
100
101	return UserToStarlark(user, u.db), nil
102}
103
104func (u *UsersModule) usersGetByAddress(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) {
105	var address starlark.String
106	if err := starlark.UnpackPositionalArgs("get_by_address", args, nil, 1, &address); err != nil {
107		return nil, err
108	}
109
110	user, err := u.db.GetUserForAddress(context.Background(), address.GoString())
111	if err != nil {
112		if errors.Is(err, sql.ErrNoRows) {
113			return starlark.None, nil
114		}
115		return nil, err
116	}
117
118	return UserToStarlark(user, u.db), nil
119}
120
121func UserToStarlark(user model.User, database *db.DB) User {
122	addresses := make([]starlark.Value, len(user.Addresses))
123	for i, addr := range user.Addresses {
124		addresses[i] = starlark.String(addr)
125	}
126
127	return User{
128		Name:       starlark.String(user.Name),
129		Disabled:   false,
130		Addresses:  addresses,
131		gatekeeper: &UserGatekeeper{db: database, user: user},
132		user:       user,
133	}
134}
135
136// UserGatekeeper is the per-User gatekeeper sub-module exposed as
137// user.gatekeeper in Starlark.
138type UserGatekeeper struct {
139	db   *db.DB
140	user model.User
141}
142
143func (g *UserGatekeeper) String() string       { return "<UserGatekeeper>" }
144func (g *UserGatekeeper) Type() string         { return "UserGatekeeper" }
145func (g *UserGatekeeper) Freeze()              {}
146func (g *UserGatekeeper) Truth() starlark.Bool { return true }
147func (g *UserGatekeeper) Hash() (uint32, error) {
148	return 0, nil
149}
150
151func (g *UserGatekeeper) Attr(name string) (starlark.Value, error) {
152	switch name {
153	case "get_decision":
154		return starlark.NewBuiltin("gatekeeper.get_decision", g.getDecision), nil
155	case "route_to":
156		return starlark.NewBuiltin("gatekeeper.route_to", g.routeTo), nil
157	}
158	return nil, nil
159}
160
161func (g *UserGatekeeper) AttrNames() []string {
162	return []string{"get_decision", "route_to"}
163}
164
165func (g *UserGatekeeper) getDecision(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) {
166	var msg MessageContext
167	if err := starlark.UnpackPositionalArgs("get_decision", args, nil, 1, &msg); err != nil {
168		return nil, err
169	}
170
171	fromAddress := msg.HeaderFrom.Address
172	slog.Info("retrieving gatekeeper decision", "module", "get_decision", "user", g.user.Name, "from_address", fromAddress)
173	destination, err := g.db.GetGatekeepDecision(context.Background(), g.user.ID, fromAddress)
174	if err != nil {
175		if errors.Is(err, sql.ErrNoRows) {
176			slog.Info("gatekeeper decision not found", "module", "get_decision")
177			return starlark.None, nil
178		}
179		return nil, err
180	}
181
182	slog.Info("got gatekeeper decision", "module", "get_decision", "destination", destination)
183	return starlark.String(destination), nil
184}
185
186func (g *UserGatekeeper) routeTo(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) {
187	var from Address
188	var destination starlark.String
189	if err := starlark.UnpackPositionalArgs("route_to", args, nil, 2, &from, &destination); err != nil {
190		return nil, err
191	}
192
193	err := g.db.SetGatekeeperDecision(context.Background(), g.user.ID, from.Address, destination.GoString())
194	if err != nil {
195		return nil, err
196	}
197
198	slog.Info("stored gatekeeper route", "user", g.user.ID, "from_address", from.Address, "destination", destination.GoString())
199
200	return starlark.None, nil
201}