Monorepo for Tangled
tangled.org
1package knotacl
2
3import (
4 "context"
5 "errors"
6 "log/slog"
7 "slices"
8 "sync"
9 "time"
10
11 "golang.org/x/sync/errgroup"
12
13 "tangled.org/core/appview/db"
14 "tangled.org/core/appview/knotcompat"
15 "tangled.org/core/appview/models"
16 "tangled.org/core/appview/pages"
17 "tangled.org/core/appview/pages/repoinfo"
18 "tangled.org/core/consts"
19 "tangled.org/core/rbac"
20)
21
22var ErrKnotUnreachable = errors.New("knot unreachable")
23
24const (
25 pickerFanoutBudget = 3 * time.Second
26 pickerFanoutConcurrency = 16
27)
28
29type Service struct {
30 dev bool
31 log *slog.Logger
32 leg *legacyReader
33 nat *nativeReader
34}
35
36func NewService(enforcer *rbac.Enforcer, execer db.Execer, dev bool, logger *slog.Logger) *Service {
37 return &Service{
38 dev: dev,
39 log: logger,
40 leg: &legacyReader{enforcer: enforcer},
41 nat: &nativeReader{client: newCache(NewClient(dev, logger), cacheTTL, nil), execer: execer},
42 }
43}
44
45func (s *Service) reader(ctx context.Context, host string) reader {
46 if knotcompat.KnotHasCapability(ctx, host, s.dev, consts.CapKnotACL) {
47 return s.nat
48 }
49 return s.leg
50}
51
52func (s *Service) RolesInRepo(ctx context.Context, repo *models.Repo, userDid string) repoinfo.RolesInRepo {
53 return repoinfo.RolesInRepo{Roles: s.repoPerms(ctx, repo, userDid)}
54}
55
56func (s *Service) HasRepoPermission(ctx context.Context, repo *models.Repo, userDid, perm string) bool {
57 return slices.Contains(s.repoPerms(ctx, repo, userDid), perm)
58}
59
60func (s *Service) HasRepoPermissionErr(ctx context.Context, repo *models.Repo, userDid, perm string) (bool, error) {
61 perms, err := s.repoPermsErr(ctx, repo, userDid)
62 if err != nil {
63 return false, err
64 }
65 return slices.Contains(perms, perm), nil
66}
67
68func (s *Service) IsRepoCreateAllowed(ctx context.Context, host, userDid string) bool {
69 return s.reader(ctx, host).isRepoCreateAllowed(ctx, host, userDid)
70}
71
72func (s *Service) KnotMembers(ctx context.Context, host string) []string {
73 return s.reader(ctx, host).knotMembers(ctx, host)
74}
75
76func (s *Service) Collaborators(ctx context.Context, repo *models.Repo) []pages.Collaborator {
77 return s.reader(ctx, repo.Knot).collaborators(ctx, repo)
78}
79
80func (s *Service) IsKnotMember(ctx context.Context, host, userDid string) bool {
81 return s.reader(ctx, host).isKnotMember(ctx, host, userDid)
82}
83
84func (s *Service) InvalidateMembers(host string) {
85 s.nat.client.InvalidateMembers(host)
86}
87
88func (s *Service) InvalidateCollaborators(host, repoDid string) {
89 s.nat.client.InvalidateCollaborators(host, repoDid)
90}
91
92func (s *Service) KnotsForUser(ctx context.Context, userDid string) []string {
93 legacyKnots, err := s.leg.enforcer.GetKnotsForUser(userDid)
94 if err != nil {
95 s.log.Error("knotsForUser: enforcer lookup failed, returning a partial list", "did", userDid, "err", err)
96 }
97
98 regs, err := db.GetRegistrations(s.nat.execer)
99 if err != nil {
100 s.log.Error("knotsForUser: registrations lookup failed, skipping native knots", "did", userDid, "err", err)
101 }
102 domains := dedup(filterMap(regs, func(r models.Registration) (string, bool) {
103 return r.Domain, r.Registered != nil
104 }))
105 owned := filterMap(regs, func(r models.Registration) (string, bool) {
106 return r.Domain, r.Registered != nil && r.ByDid == userDid
107 })
108 nativeMember := s.nativeMemberships(ctx, domains, userDid)
109
110 all := make([]string, 0, len(legacyKnots)+len(owned)+len(nativeMember))
111 all = append(all, legacyKnots...)
112 all = append(all, owned...)
113 all = append(all, nativeMember...)
114 return dedup(all)
115}
116
117func (s *Service) nativeMemberships(ctx context.Context, domains []string, userDid string) []string {
118 ctx, cancel := context.WithTimeout(ctx, pickerFanoutBudget)
119 defer cancel()
120
121 g, gctx := errgroup.WithContext(ctx)
122 g.SetLimit(pickerFanoutConcurrency)
123
124 var mu sync.Mutex
125 var hits []string
126
127 for _, host := range domains {
128 g.Go(func() error {
129 if !knotcompat.KnotHasCapability(gctx, host, s.dev, consts.CapKnotACL) {
130 return nil
131 }
132 members, err := s.nat.client.GetKnotMembers(gctx, host)
133 if err != nil || !slices.Contains(members, userDid) {
134 return nil
135 }
136 mu.Lock()
137 hits = append(hits, host)
138 mu.Unlock()
139 return nil
140 })
141 }
142 _ = g.Wait()
143 return hits
144}
145
146func (s *Service) repoPerms(ctx context.Context, repo *models.Repo, userDid string) []string {
147 perms, _ := s.repoPermsErr(ctx, repo, userDid)
148 return perms
149}
150
151func (s *Service) repoPermsErr(ctx context.Context, repo *models.Repo, userDid string) ([]string, error) {
152 return s.reader(ctx, repo.Knot).repoPerms(ctx, repo, userDid)
153}
154
155func filterMap[T, U any](items []T, f func(T) (U, bool)) []U {
156 var out []U
157 for _, it := range items {
158 if u, ok := f(it); ok {
159 out = append(out, u)
160 }
161 }
162 return out
163}