package policy import ( "context" "database/sql" "errors" "log/slog" "postern/internal/db" "postern/internal/model" "go.starlark.net/starlark" ) type UsersModule struct { db *db.DB } func NewUsersModule(db *db.DB) *UsersModule { return &UsersModule{ db: db, } } func (u *UsersModule) String() string { return "" } func (u *UsersModule) Type() string { return "module" } func (u *UsersModule) Freeze() {} func (u *UsersModule) Truth() starlark.Bool { return true } func (u *UsersModule) Hash() (uint32, error) { return 0, nil } func (u *UsersModule) Attr(name string) (starlark.Value, error) { switch name { case "get": return starlark.NewBuiltin("get", u.usersGet), nil case "get_by_address": return starlark.NewBuiltin("get_by_address", u.usersGetByAddress), nil } return nil, nil } func (u *UsersModule) AttrNames() []string { return []string{"get", "get_by_address"} } type User struct { Name starlark.String Disabled starlark.Bool Addresses starlark.Tuple gatekeeper *UserGatekeeper user model.User } func (u User) String() string { return "" } func (u User) Type() string { return "User" } func (u User) Freeze() {} func (u User) Truth() starlark.Bool { return true } func (u User) Hash() (uint32, error) { return 0, nil } func (u User) Attr(name string) (starlark.Value, error) { switch name { case "name": return u.Name, nil case "is_disabled": return u.Disabled, nil case "addresses": return u.Addresses, nil case "gatekeeper": if u.gatekeeper == nil { return starlark.None, nil } return u.gatekeeper, nil } return nil, nil } func (u User) AttrNames() []string { return []string{"name", "is_disabled", "addresses", "gatekeeper"} } func (u *UsersModule) usersGet(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) { var username starlark.String if err := starlark.UnpackPositionalArgs("get", args, nil, 1, &username); err != nil { return nil, err } user, err := u.db.GetUser(context.Background(), username.GoString()) if err != nil { if errors.Is(err, sql.ErrNoRows) { return starlark.None, nil } return nil, err } return UserToStarlark(user, u.db), nil } func (u *UsersModule) usersGetByAddress(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) { var address starlark.String if err := starlark.UnpackPositionalArgs("get_by_address", args, nil, 1, &address); err != nil { return nil, err } user, err := u.db.GetUserForAddress(context.Background(), address.GoString()) if err != nil { if errors.Is(err, sql.ErrNoRows) { return starlark.None, nil } return nil, err } return UserToStarlark(user, u.db), nil } func UserToStarlark(user model.User, database *db.DB) User { addresses := make([]starlark.Value, len(user.Addresses)) for i, addr := range user.Addresses { addresses[i] = starlark.String(addr) } return User{ Name: starlark.String(user.Name), Disabled: false, Addresses: addresses, gatekeeper: &UserGatekeeper{db: database, user: user}, user: user, } } // UserGatekeeper is the per-User gatekeeper sub-module exposed as // user.gatekeeper in Starlark. type UserGatekeeper struct { db *db.DB user model.User } func (g *UserGatekeeper) String() string { return "" } func (g *UserGatekeeper) Type() string { return "UserGatekeeper" } func (g *UserGatekeeper) Freeze() {} func (g *UserGatekeeper) Truth() starlark.Bool { return true } func (g *UserGatekeeper) Hash() (uint32, error) { return 0, nil } func (g *UserGatekeeper) Attr(name string) (starlark.Value, error) { switch name { case "get_decision": return starlark.NewBuiltin("gatekeeper.get_decision", g.getDecision), nil case "route_to": return starlark.NewBuiltin("gatekeeper.route_to", g.routeTo), nil } return nil, nil } func (g *UserGatekeeper) AttrNames() []string { return []string{"get_decision", "route_to"} } func (g *UserGatekeeper) getDecision(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) { var msg MessageContext if err := starlark.UnpackPositionalArgs("get_decision", args, nil, 1, &msg); err != nil { return nil, err } fromAddress := msg.HeaderFrom.Address slog.Info("retrieving gatekeeper decision", "module", "get_decision", "user", g.user.Name, "from_address", fromAddress) destination, err := g.db.GetGatekeepDecision(context.Background(), g.user.ID, fromAddress) if err != nil { if errors.Is(err, sql.ErrNoRows) { slog.Info("gatekeeper decision not found", "module", "get_decision") return starlark.None, nil } return nil, err } slog.Info("got gatekeeper decision", "module", "get_decision", "destination", destination) return starlark.String(destination), nil } func (g *UserGatekeeper) routeTo(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) { var from Address var destination starlark.String if err := starlark.UnpackPositionalArgs("route_to", args, nil, 2, &from, &destination); err != nil { return nil, err } err := g.db.SetGatekeeperDecision(context.Background(), g.user.ID, from.Address, destination.GoString()) if err != nil { return nil, err } slog.Info("stored gatekeeper route", "user", g.user.ID, "from_address", from.Address, "destination", destination.GoString()) return starlark.None, nil }