-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmain.go
346 lines (299 loc) · 11.6 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
package main
import (
"fmt"
"os"
"reflect"
"strings"
"time"
"github.com/spf13/cobra"
)
const DATE_FORMAT = "2006-01-02"
type Config struct {
username string
password string
server string
org string
start time.Time
end time.Time
insecure bool
debug bool
verbose bool
}
type APIClient interface {
// Returns the service topology from skywalking, which needs to be normalized to services in
// TSB via the 'aggregated metrics' names in each TSB Service.
GetTopology(start, end time.Time) (*TopologyResponse, error)
// Calls TSB's ListServices endpoint
GetServices() ([]Service, error)
// Service FQN -> Group FQN
LookupSecurityGroups(services []Service) map[string]string // TODO: multi-error
// Takes (Service FQN -> Group FQN) and returns the group policy and services in each group
GetSecurityGroups(serviceFQNToGroupFQN map[string]string) map[string]PolicyAndServices // TODO: multi-error
}
type Runtime struct {
start time.Time
end time.Time
server string
debug bool
verbose bool
client APIClient
}
var debug = func(format string, a ...any) { fmt.Printf(format+"\n", a...) }
func main() {
// flags
var (
startFlag string
endFlag string
noverbose bool
)
// static & runtime configs
var (
cfg = &Config{}
runtime = &Runtime{}
)
cmd := &cobra.Command{
Use: "generate-authz-tool",
Short: "generate-authz-tool: a simple tool for creating TSB authz policies from TSB traffic data",
PreRunE: func(cmd *cobra.Command, args []string) error {
// Set up the app based on config+flags
if !cfg.debug {
debug = func(fmt string, args ...any) {}
}
if noverbose {
cfg.verbose = false
}
if cfg.server == "" {
return fmt.Errorf("Server address (-s or --server) can't be empty, need an address like 'tsb.yourcorp.com' or an IP like '127.0.1.10'.")
} else {
// normalize the name; in the client code we prefix every call with `https`, so
// strip any prefix on input so that both address with protocol and without work
cfg.server = strings.TrimPrefix(cfg.server, "https://")
cfg.server = strings.TrimPrefix(cfg.server, "http://")
debug("got TSB string %q", cfg.server)
}
if start, err := time.Parse(DATE_FORMAT, startFlag); err != nil {
return fmt.Errorf("failed to parse start time %q: %w", startFlag, err)
} else {
cfg.start = start
}
if end, err := time.Parse(DATE_FORMAT, endFlag); err != nil {
return fmt.Errorf("failed to parse start time %q: %w", endFlag, err)
} else {
cfg.end = end
}
runtime = &Runtime{
start: cfg.start,
end: cfg.end,
server: cfg.server,
debug: cfg.debug,
verbose: cfg.verbose,
client: NewTSBHttpClient(cfg),
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
debugLogJSON := func(data interface{}) { debugLogJSON(runtime, data) }
// Do the work: get the topology and services
top, err := runtime.client.GetTopology(runtime.start, runtime.end)
if err != nil {
return fmt.Errorf("failed to get server topology: %w", err)
}
debugLogJSON(top)
services, err := runtime.client.GetServices()
if err != nil {
return fmt.Errorf("failed to get service list: %w", err)
}
debugLogJSON(services)
// take the data and build the graph of services; we get back a map of service FQN
// to the set of clients that call it (a map of client FQN to Service object)
clients := buildGraph(top, services)
groupNames := runtime.client.LookupSecurityGroups(services)
groupToPolicy := runtime.client.GetSecurityGroups(groupNames)
updates := make(map[string]*SecuritySetting)
creates := make(map[string]*CreateSecuritySettingsRequest)
noChange := make(map[string]*SecuritySetting)
for group, policyAndServices := range groupToPolicy {
debug("processing %q on behalf of %d services, got callers:", group, len(policyAndServices.services))
accounts := callersForServiceSet(policyAndServices.services, clients)
// set up a default policy if one didn't exist
if policyAndServices.existingPolicy == nil {
debug("could not find an existing policy for %q; creating one", group)
description := fmt.Sprintf("Generated %s by 'go run github.com/tetrateio/generate-authz-tool' based on traffic from %s to %s for services: %s",
time.Now().Format(DATE_FORMAT), cfg.start.Format(DATE_FORMAT), cfg.end.Format(DATE_FORMAT), strings.Join(setToString(policyAndServices.services), ", "))
createReq := NewSecuritySetting("default", "Default", description, accounts)
debugLogJSON(createReq)
creates[group] = createReq
} else {
// There's an existing policy, let's compute the new one and compare the two
policy := newAuthorizationSettings(accounts)
if reflect.DeepEqual(policy, policyAndServices.existingPolicy) {
// policies are the same, include this in the "no change" set
noChange[policyAndServices.existingPolicy.FQN] = policyAndServices.existingPolicy
debug("no change in policy for %q", policyAndServices.existingPolicy.FQN)
debugLogJSON(policyAndServices.existingPolicy)
} else {
if cfg.debug {
debug("previous policy for %q:", policyAndServices.existingPolicy.FQN)
debugLogJSON(policyAndServices.existingPolicy)
debug("updated policy for %q:", policyAndServices.existingPolicy.FQN)
debugLogJSON(policy)
}
// new policy is different than the old policy, update it
policyAndServices.existingPolicy.Authorization = policy
updates[group] = policyAndServices.existingPolicy
}
}
}
printOutput(runtime, clients, groupToPolicy, creates, updates, noChange)
return err
},
}
cmd.Flags().StringVarP(&cfg.server, "server", "s", "", "Address of the TSB API server, e.g. some.tsb.address.example.com. REQUIRED")
cmd.Flags().StringVarP(&cfg.username, "http-auth-user", "u", "", "Username to call TSB with via HTTP Basic Auth. REQUIRED")
cmd.Flags().StringVarP(&cfg.password, "http-auth-password", "p", "", "Password to call TSB with via HTTP Basic Auth. REQUIRED")
cmd.Flags().StringVar(&cfg.org, "org", "tetrate", "TSB org to query against")
cmd.Flags().StringVar(&startFlag, "start", fmt.Sprintf(time.Now().Add(-5*24*time.Hour).Format(DATE_FORMAT)),
"Start of the time range to query the topology in YYYY-MM-DD format")
cmd.Flags().StringVar(&endFlag, "end", fmt.Sprintf(time.Now().Format(DATE_FORMAT)),
"End of the time range to query the topology in YYYY-MM-DD format")
cmd.Flags().BoolVarP(&cfg.insecure, "insecure", "k", false, "Skip certificate verification when calling TSB")
cmd.Flags().BoolVar(&cfg.debug, "debug", false, "Enable debug logging")
cmd.Flags().BoolVar(&cfg.verbose, "verbose", true, "Enable verbose output, explaining why policy was generated; otherwise only the policy documents are printed.")
cmd.Flags().BoolVar(&noverbose, "noverbose", false, "Disable verbose output; overrides --verbose (equivalent to --verbose=false)")
if err := cmd.Execute(); err != nil {
os.Exit(-1)
}
}
func printOutput(runtime *Runtime, clients map[string]map[string]*Service, groupToPolicy map[string]PolicyAndServices, creates map[string]*CreateSecuritySettingsRequest, updates map[string]*SecuritySetting, noChange map[string]*SecuritySetting) {
if runtime.verbose {
fmt.Printf("Observed the following traffic in the system from %s to %s:\n",
runtime.start.Format(DATE_FORMAT), runtime.end.Format(DATE_FORMAT))
for target, callers := range clients {
fmt.Println("\n " + target + " is called by:")
if len(callers) == 0 {
fmt.Println(" nothing")
}
for caller := range callers {
fmt.Println(" - " + caller)
}
}
fmt.Println("\nThey belong to the following groups:")
for group, pAndS := range groupToPolicy {
fmt.Println("\n " + group + " configures:")
if len(pAndS.services) == 0 {
fmt.Println(" nothing")
}
for svc := range pAndS.services {
fmt.Println(" - " + svc)
}
}
fmt.Println()
}
if len(creates) > 0 {
fmt.Println("The following policies need to be created:\n")
for group, create := range creates {
url := settingsFQNFromGroupName(runtime.server, group)
fmt.Printf("POST %q\n", url)
fmt.Println(marshalToString(create))
}
}
if len(updates) > 0 {
fmt.Println("\nThe following policies need to be updated:\n")
for _, update := range updates {
fmt.Printf("PUT https://%s/v2/%s\n", runtime.server, update.FQN)
fmt.Println(marshalToString(update))
}
}
if len(noChange) > 0 {
fmt.Println("\nThe following settings already match the observed traffic in the system:\n")
for group, setting := range noChange {
fmt.Println("- " + group + ":")
if runtime.verbose {
fmt.Println(marshalToString(setting))
}
}
}
}
// Normalizes the topology response and service list into a Graph of Service FQN to the set of callers (with their Service definition)
func buildGraph(top *TopologyResponse, services []Service) map[string]map[string]*Service {
servicesByTopKey := make(map[string]*Service)
for _, svc := range services {
local := svc
for _, metric := range svc.Metrics {
debug("service %q has FQN %q", metric.AggregationKey, local.FQN)
servicesByTopKey[metric.AggregationKey] = &local
}
}
idToTopKey := make(map[string]string)
for _, node := range top.Nodes {
debug("node ID %q belongs to %q", node.ID, node.AggregationKey)
idToTopKey[node.ID] = node.AggregationKey
}
servicesByID := make(map[string]*Service)
for id, key := range idToTopKey {
if svc, ok := servicesByTopKey[key]; ok {
servicesByID[id] = svc
debug("id %q maps to service %q", id, svc.FQN)
} else {
debug("no service for key %q", key)
}
}
clients := make(map[string]map[string]*Service)
for _, traffic := range top.Calls {
debug("processing call %s", traffic.ID)
target, ok := servicesByID[traffic.Target]
if !ok {
debug("no service for key %s", traffic.Target)
continue
}
debug("computed target %s", target.FQN)
// first time we hit a new service, create the set of clients
if _, ok := clients[target.FQN]; !ok {
debug("first time seeing service %q, creating map to hold clients", target.FQN)
clients[target.FQN] = make(map[string]*Service)
}
// if the traffic source exists as a service, add it as one of our clients
if source, ok := servicesByID[traffic.Source]; ok {
debug("marked %q as client of %q", source.FQN, target.FQN)
clients[target.FQN][source.FQN] = source
} else {
debug("* no service for key %q", traffic.Source)
}
}
return clients
}
// We do updates per Group, so we want to get all of the callers for the entire group of services to update
// the group policy all at once.
func callersForServiceSet(services map[string]struct{}, graph map[string]map[string]*Service) []string {
collectSpiffeIDs := func(unique map[string]struct{}, callers map[string]*Service) map[string]struct{} {
for name, svc := range callers {
for _, id := range svc.SpiffeIds {
spiffeID := id
if idx := strings.Index(id, "/ns/"); idx > 0 {
// id[idx+4:] = /ns/<namespace>/sa/<service account> => <namespace>/sa/<service account>
// Replace('<namespace>/sa/<service account>', '/sa') => <namespace>/<service account>
spiffeID = strings.Replace(id[idx+4:], "/sa", "", 1)
}
debug("- %s (unique[%q] = %q)", name, id, spiffeID)
unique[spiffeID] = struct{}{}
}
}
return unique
}
unique := map[string]struct{}{}
for svc := range services {
callers, ok := graph[svc]
if !ok {
debug("didn't find clients for %q, skipping", svc)
continue
}
unique = collectSpiffeIDs(unique, callers)
debug("%d callers for the group after processing %q", len(unique), svc)
}
accounts := []string{}
for id := range unique {
accounts = append(accounts, id)
}
debug("accounts: %v", accounts)
return accounts
}