repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

commit
f60c265
parent
f1dc8fc
author
Eric Bower
date
2025-12-26 10:53:17 -0500 EST
refactor: use `resolveTopic` to determine topic name
2 files changed,  +158, -146
M pkg/apps/pipe/cli.go
+80, -146
  1@@ -9,7 +9,6 @@ import (
  2 	"log/slog"
  3 	"slices"
  4 	"strings"
  5-	"text/tabwriter"
  6 	"time"
  7 
  8 	"github.com/antoniomika/syncmap"
  9@@ -321,42 +320,46 @@ func (handler *CliHandler) pub(cmd *CliCmd, topic string, clientID string) error
 10 		topic = uuid.NewString()
 11 	}
 12 
 13-	var withoutUser string
 14-	var name string
 15 	msgFlag := ""
 16-
 17-	if cmd.isAdmin && strings.HasPrefix(topic, "/") {
 18-		name = strings.TrimPrefix(topic, "/")
 19-	} else {
 20-		name = toTopic(cmd.userName, topic)
 21-		if *public {
 22-			name = toPublicTopic(topic)
 23-			msgFlag = "-p "
 24-			withoutUser = name
 25-		} else {
 26-			withoutUser = topic
 27-		}
 28+	if *public {
 29+		msgFlag = "-p "
 30 	}
 31 
 32+	// Initial resolution to get the topic name for access storage
 33+	initialResult := resolveTopic(TopicResolveInput{
 34+		UserName: cmd.userName,
 35+		Topic:    topic,
 36+		IsAdmin:  cmd.isAdmin,
 37+		IsPublic: *public,
 38+	})
 39+	name := initialResult.Name
 40+
 41 	var accessListCreator bool
 42 	_, loaded := handler.Access.LoadOrStore(name, accessList)
 43 	if !loaded {
 44 		defer func() {
 45 			handler.Access.Delete(name)
 46 		}()
 47-
 48 		accessListCreator = true
 49 	}
 50 
 51-	if accessList, ok := handler.Access.Load(withoutUser); ok && len(accessList) > 0 && !cmd.isAdmin {
 52-		if checkAccess(accessList, cmd.userName, cmd.sesh) || accessListCreator {
 53-			name = withoutUser
 54-		} else if !*public {
 55-			name = toTopic(cmd.userName, withoutUser)
 56-		} else {
 57-			topic = uuid.NewString()
 58-			name = toPublicTopic(topic)
 59-		}
 60+	// Check for existing access list and resolve final topic name
 61+	existingAccessList, hasExistingAccess := handler.Access.Load(initialResult.WithoutUser)
 62+	result := resolveTopic(TopicResolveInput{
 63+		UserName:           cmd.userName,
 64+		Topic:              topic,
 65+		IsAdmin:            cmd.isAdmin,
 66+		IsPublic:           *public,
 67+		ExistingAccessList: existingAccessList,
 68+		HasExistingAccess:  hasExistingAccess,
 69+		IsAccessCreator:    accessListCreator,
 70+		HasUserAccess:      checkAccess(existingAccessList, cmd.userName, cmd.sesh),
 71+	})
 72+	name = result.Name
 73+
 74+	if result.GenerateNewTopic {
 75+		topic = uuid.NewString()
 76+		name = toPublicTopic(topic)
 77 	}
 78 
 79 	if !*clean {
 80@@ -524,20 +527,14 @@ func (handler *CliHandler) sub(cmd *CliCmd, topic string, clientID string) error
 81 		accessList = parseArgList(*access)
 82 	}
 83 
 84-	var withoutUser string
 85-	var name string
 86-
 87-	if cmd.isAdmin && strings.HasPrefix(topic, "/") {
 88-		name = strings.TrimPrefix(topic, "/")
 89-	} else {
 90-		name = toTopic(cmd.userName, topic)
 91-		if *public {
 92-			name = toPublicTopic(topic)
 93-			withoutUser = name
 94-		} else {
 95-			withoutUser = topic
 96-		}
 97-	}
 98+	// Initial resolution to get the topic name for access storage
 99+	initialResult := resolveTopic(TopicResolveInput{
100+		UserName: cmd.userName,
101+		Topic:    topic,
102+		IsAdmin:  cmd.isAdmin,
103+		IsPublic: *public,
104+	})
105+	name := initialResult.Name
106 
107 	var accessListCreator bool
108 
109@@ -549,14 +546,22 @@ func (handler *CliHandler) sub(cmd *CliCmd, topic string, clientID string) error
110 		accessListCreator = true
111 	}
112 
113-	if accessList, ok := handler.Access.Load(withoutUser); ok && len(accessList) > 0 && !cmd.isAdmin {
114-		if checkAccess(accessList, cmd.userName, cmd.sesh) || accessListCreator {
115-			name = withoutUser
116-		} else if !*public {
117-			name = toTopic(cmd.userName, withoutUser)
118-		} else {
119-			return fmt.Errorf("access denied")
120-		}
121+	// Check for existing access list and resolve final topic name
122+	existingAccessList, hasExistingAccess := handler.Access.Load(initialResult.WithoutUser)
123+	result := resolveTopic(TopicResolveInput{
124+		UserName:           cmd.userName,
125+		Topic:              topic,
126+		IsAdmin:            cmd.isAdmin,
127+		IsPublic:           *public,
128+		ExistingAccessList: existingAccessList,
129+		HasExistingAccess:  hasExistingAccess,
130+		IsAccessCreator:    accessListCreator,
131+		HasUserAccess:      checkAccess(existingAccessList, cmd.userName, cmd.sesh),
132+	})
133+	name = result.Name
134+
135+	if result.AccessDenied {
136+		return fmt.Errorf("access denied")
137 	}
138 
139 	err := handler.PubSub.Sub(
140@@ -612,23 +617,20 @@ func (handler *CliHandler) pipe(cmd *CliCmd, topic string, clientID string) erro
141 		topic = uuid.NewString()
142 	}
143 
144-	var withoutUser string
145-	var name string
146 	flagMsg := ""
147-
148-	if cmd.isAdmin && strings.HasPrefix(topic, "/") {
149-		name = strings.TrimPrefix(topic, "/")
150-	} else {
151-		name = toTopic(cmd.userName, topic)
152-		if *public {
153-			name = toPublicTopic(topic)
154-			flagMsg = "-p "
155-			withoutUser = name
156-		} else {
157-			withoutUser = topic
158-		}
159+	if *public {
160+		flagMsg = "-p "
161 	}
162 
163+	// Initial resolution to get the topic name for access storage
164+	initialResult := resolveTopic(TopicResolveInput{
165+		UserName: cmd.userName,
166+		Topic:    topic,
167+		IsAdmin:  cmd.isAdmin,
168+		IsPublic: *public,
169+	})
170+	name := initialResult.Name
171+
172 	var accessListCreator bool
173 
174 	_, loaded := handler.Access.LoadOrStore(name, accessList)
175@@ -639,15 +641,23 @@ func (handler *CliHandler) pipe(cmd *CliCmd, topic string, clientID string) erro
176 		accessListCreator = true
177 	}
178 
179-	if accessList, ok := handler.Access.Load(withoutUser); ok && len(accessList) > 0 && !cmd.isAdmin {
180-		if checkAccess(accessList, cmd.userName, cmd.sesh) || accessListCreator {
181-			name = withoutUser
182-		} else if !*public {
183-			name = toTopic(cmd.userName, withoutUser)
184-		} else {
185-			topic = uuid.NewString()
186-			name = toPublicTopic(topic)
187-		}
188+	// Check for existing access list and resolve final topic name
189+	existingAccessList, hasExistingAccess := handler.Access.Load(initialResult.WithoutUser)
190+	result := resolveTopic(TopicResolveInput{
191+		UserName:           cmd.userName,
192+		Topic:              topic,
193+		IsAdmin:            cmd.isAdmin,
194+		IsPublic:           *public,
195+		ExistingAccessList: existingAccessList,
196+		HasExistingAccess:  hasExistingAccess,
197+		IsAccessCreator:    accessListCreator,
198+		HasUserAccess:      checkAccess(existingAccessList, cmd.userName, cmd.sesh),
199+	})
200+	name = result.Name
201+
202+	if result.GenerateNewTopic {
203+		topic = uuid.NewString()
204+		name = toPublicTopic(topic)
205 	}
206 
207 	if isCreator && !*clean {
208@@ -740,82 +750,6 @@ func flagCheck(cmd *flag.FlagSet, posArg string, cmdArgs []string) bool {
209 	return true
210 }
211 
212-func NewTabWriter(out io.Writer) *tabwriter.Writer {
213-	return tabwriter.NewWriter(out, 0, 0, 1, ' ', tabwriter.TabIndent)
214-}
215-
216-// scope topic to user by prefixing name.
217-func toTopic(userName, topic string) string {
218-	if strings.HasPrefix(topic, userName+"/") {
219-		return topic
220-	}
221-	return fmt.Sprintf("%s/%s", userName, topic)
222-}
223-
224-func toPublicTopic(topic string) string {
225-	if strings.HasPrefix(topic, "public/") {
226-		return topic
227-	}
228-	return fmt.Sprintf("public/%s", topic)
229-}
230-
231-// TopicResolveInput contains all inputs needed for topic resolution.
232-type TopicResolveInput struct {
233-	UserName           string
234-	Topic              string
235-	IsAdmin            bool
236-	IsPublic           bool
237-	AccessList         []string
238-	ExistingAccessList []string
239-	HasExistingAccess  bool
240-	IsAccessCreator    bool
241-	HasUserAccess      bool
242-}
243-
244-// TopicResolveOutput contains the resolved topic name and any error.
245-type TopicResolveOutput struct {
246-	Name             string
247-	WithoutUser      string
248-	AccessDenied     bool
249-	GenerateNewTopic bool
250-}
251-
252-// resolveTopic determines the final topic name based on user, flags, and access control.
253-func resolveTopic(input TopicResolveInput) TopicResolveOutput {
254-	var name string
255-	var withoutUser string
256-
257-	if input.IsAdmin && strings.HasPrefix(input.Topic, "/") {
258-		name = strings.TrimPrefix(input.Topic, "/")
259-		return TopicResolveOutput{Name: name, WithoutUser: withoutUser}
260-	}
261-
262-	name = toTopic(input.UserName, input.Topic)
263-	if input.IsPublic {
264-		name = toPublicTopic(input.Topic)
265-		withoutUser = name
266-	} else {
267-		withoutUser = input.Topic
268-	}
269-
270-	if input.HasExistingAccess && len(input.ExistingAccessList) > 0 && !input.IsAdmin {
271-		if input.HasUserAccess || input.IsAccessCreator {
272-			name = withoutUser
273-		} else if !input.IsPublic {
274-			name = toTopic(input.UserName, withoutUser)
275-		} else {
276-			return TopicResolveOutput{
277-				Name:             name,
278-				WithoutUser:      withoutUser,
279-				AccessDenied:     true,
280-				GenerateNewTopic: true,
281-			}
282-		}
283-	}
284-
285-	return TopicResolveOutput{Name: name, WithoutUser: withoutUser}
286-}
287-
288 func clientInfo(clients []*psub.Client, isAdmin bool, clientType string) string {
289 	if len(clients) == 0 {
290 		return ""
A pkg/apps/pipe/topic.go
+78, -0
 1@@ -0,0 +1,78 @@
 2+package pipe
 3+
 4+import (
 5+	"fmt"
 6+	"strings"
 7+)
 8+
 9+// toTopic scopes a topic to user by prefixing name.
10+func toTopic(userName, topic string) string {
11+	if strings.HasPrefix(topic, userName+"/") {
12+		return topic
13+	}
14+	return fmt.Sprintf("%s/%s", userName, topic)
15+}
16+
17+func toPublicTopic(topic string) string {
18+	if strings.HasPrefix(topic, "public/") {
19+		return topic
20+	}
21+	return fmt.Sprintf("public/%s", topic)
22+}
23+
24+// TopicResolveInput contains all inputs needed for topic resolution.
25+type TopicResolveInput struct {
26+	UserName           string
27+	Topic              string
28+	IsAdmin            bool
29+	IsPublic           bool
30+	AccessList         []string
31+	ExistingAccessList []string
32+	HasExistingAccess  bool
33+	IsAccessCreator    bool
34+	HasUserAccess      bool
35+}
36+
37+// TopicResolveOutput contains the resolved topic name and any error.
38+type TopicResolveOutput struct {
39+	Name             string
40+	WithoutUser      string
41+	AccessDenied     bool
42+	GenerateNewTopic bool
43+}
44+
45+// resolveTopic determines the final topic name based on user, flags, and access control.
46+func resolveTopic(input TopicResolveInput) TopicResolveOutput {
47+	var name string
48+	var withoutUser string
49+
50+	if input.IsAdmin && strings.HasPrefix(input.Topic, "/") {
51+		name = strings.TrimPrefix(input.Topic, "/")
52+		return TopicResolveOutput{Name: name, WithoutUser: withoutUser}
53+	}
54+
55+	name = toTopic(input.UserName, input.Topic)
56+	if input.IsPublic {
57+		name = toPublicTopic(input.Topic)
58+		withoutUser = name
59+	} else {
60+		withoutUser = input.Topic
61+	}
62+
63+	if input.HasExistingAccess && len(input.ExistingAccessList) > 0 && !input.IsAdmin {
64+		if input.HasUserAccess || input.IsAccessCreator {
65+			name = withoutUser
66+		} else if !input.IsPublic {
67+			name = toTopic(input.UserName, withoutUser)
68+		} else {
69+			return TopicResolveOutput{
70+				Name:             name,
71+				WithoutUser:      withoutUser,
72+				AccessDenied:     true,
73+				GenerateNewTopic: true,
74+			}
75+		}
76+	}
77+
78+	return TopicResolveOutput{Name: name, WithoutUser: withoutUser}
79+}