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}