1 package loaddata
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "os"
10 "slices"
11 "strings"
12
13 "github.com/peterbourgon/ff/v3"
14 "github.com/shurcooL/graphql"
15
16 "edge-infra.dev/pkg/edge/api/client"
17 "edge-infra.dev/pkg/edge/api/graph/model"
18 "edge-infra.dev/pkg/edge/edgeadmin/commands/operatorintervention/edgeextension"
19 "edge-infra.dev/pkg/edge/edgeadmin/commands/operatorintervention/format"
20 "edge-infra.dev/pkg/edge/edgecli"
21 "edge-infra.dev/pkg/edge/edgecli/flagutil"
22 "edge-infra.dev/pkg/lib/cli/command"
23 "edge-infra.dev/pkg/lib/cli/rags"
24 )
25
26
27 type config struct {
28 RoleMappings map[string][]string `json:"ROLE_MAPPING"`
29 Rules map[string][]string `json:"RULES"`
30 }
31
32 func (cfg config) Validate() error {
33 var err error
34 if len(cfg.RoleMappings) == 0 && len(cfg.Rules) == 0 {
35 err = errors.Join(err, errors.New("empty config"))
36 }
37 for role, privileges := range cfg.RoleMappings {
38
39 if len(privileges) == 0 {
40 err = errors.Join(err, fmt.Errorf("role %q has no privileges", role))
41 }
42 for _, privilege := range privileges {
43
44 if privilege == "" {
45 err = errors.Join(err, fmt.Errorf("role %q has empty privilege", role))
46 }
47 }
48 }
49 for privilege, commands := range cfg.Rules {
50
51 if len(commands) == 0 {
52 err = errors.Join(err, fmt.Errorf("privilege %q has no commands", privilege))
53 }
54 for _, command := range commands {
55
56 if command == "" {
57 err = errors.Join(err, fmt.Errorf("privilege %q has empty command", privilege))
58 }
59 }
60 }
61 return err
62 }
63
64 const (
65 longHelpString = `
66 This command loads Operator Intervention configuration from a file and stores it in the database. This allows for bulk configuration of role mappings and rules.
67 Any privileges or commands added through this operation will be added automatically to the database. Duplicate entries will be ignored.
68
69 The configuration file should contain role mappings and rules in JSON format as follows:
70 {
71 "ROLE_MAPPING": {
72 "EDGE_BANNER_ADMIN": [
73 "ea-read",
74 "ea-banner-admin"
75 ]
76 },
77 "RULES": {
78 "ea-write": [
79 "ls",
80 "cat"
81 ],
82 "ea-banner-admin": [
83 "kubectl"
84 ]
85 }
86 }
87 Note that the top level keys are optional.
88
89 This file can then be saved to and passed to the binary using the --file flag as follows:
90
91 ~/edge-infra$ edgeadmin operatorintervention loaddata --file /path/to/file.json
92 `
93 )
94
95
96 func NewCmd(cfg *edgecli.Config) *command.Command {
97 var (
98 edge = &edgeextension.Ext{Cfg: cfg}
99 )
100
101 var cmd *command.Command
102 cmd = &command.Command{
103 ShortUsage: "edgeadmin operatorintervention loaddata",
104 ShortHelp: "loads operator intervention configuration from a file to the database",
105 LongHelp: longHelpString,
106 Options: []ff.Option{},
107 Flags: []*rags.Rag{
108 {
109 Name: flagutil.LoadData,
110 Usage: "path to file with rolemappings and rules",
111 Value: &rags.String{},
112 Required: true,
113 },
114 },
115
116 Extensions: []command.Extension{edge},
117
118 Exec: func(ctx context.Context, _ []string) error {
119 configPath := flagutil.GetStringFlag(cmd.Rags, flagutil.LoadData)
120
121 config, err := loadConfig(configPath)
122 if err != nil {
123 fmt.Printf("error loading config \n")
124 return err
125 }
126
127 err = loadData(ctx, edge.Client, config)
128 if err != nil {
129 fmt.Println("an error occurred while loading data")
130 return err
131 }
132
133 return nil
134 },
135 }
136 return cmd
137 }
138
139
140
141 func loadConfig(configPath string) (config, error) {
142 data, err := os.ReadFile(configPath)
143 if err != nil {
144 return config{}, err
145 }
146 var conf config
147 decoder := json.NewDecoder(bytes.NewReader(data))
148
149 decoder.DisallowUnknownFields()
150 if err = decoder.Decode(&conf); err != nil {
151 return config{}, err
152 }
153 if decoder.More() {
154 return config{}, errors.New("error multiple objects in config file")
155 }
156 if err := conf.Validate(); err != nil {
157 return config{}, fmt.Errorf("error invalid config: %w", err)
158 }
159 return conf, nil
160 }
161
162
163
164
165
166
167 func createVariables(conf config) map[string]interface{} {
168
169
170 var allPrivs []string
171 var allCommands []string
172
173
174 roleMappings := make([]*model.UpdateOperatorInterventionRoleMappingInput, 0, len(conf.RoleMappings))
175 for role, privileges := range conf.RoleMappings {
176
177 allPrivs = append(allPrivs, privileges...)
178
179
180 roleMappings = append(roleMappings, &model.UpdateOperatorInterventionRoleMappingInput{
181 Role: role,
182 Privileges: convertToPrivilegeInputs(privileges),
183 })
184 }
185
186 rules := make([]model.UpdateOperatorInterventionRuleInput, 0, len(conf.Rules))
187 for privilege, commands := range conf.Rules {
188
189 allPrivs = append(allPrivs, privilege)
190 allCommands = append(allCommands, commands...)
191
192
193 rules = append(rules, model.UpdateOperatorInterventionRuleInput{
194 Privilege: &model.OperatorInterventionPrivilegeInput{
195 Name: privilege,
196 },
197 Commands: convertToCommandInputs(commands),
198 })
199 }
200
201
202 slices.Sort(allPrivs)
203 allPrivs = slices.Compact(allPrivs)
204
205 privileges := make([]model.OperatorInterventionPrivilegeInput, 0, len(allPrivs))
206 for _, priv := range allPrivs {
207 privileges = append(privileges, model.OperatorInterventionPrivilegeInput{
208 Name: priv,
209 })
210 }
211
212
213 slices.Sort(allCommands)
214 allCommands = slices.Compact(allCommands)
215
216 commands := make([]model.OperatorInterventionCommandInput, 0, len(allCommands))
217 for _, com := range allCommands {
218 commands = append(commands, model.OperatorInterventionCommandInput{
219 Name: com,
220 })
221 }
222
223 return map[string]interface{}{
224
225 "skipCommands": graphql.Boolean(len(commands) == 0),
226 "skipPrivileges": graphql.Boolean(len(privileges) == 0),
227 "skipRules": graphql.Boolean(len(rules) == 0),
228 "skipRoleMappings": graphql.Boolean(len(roleMappings) == 0),
229
230 "roleMappings": roleMappings,
231 "privileges": privileges,
232 "commands": commands,
233 "rules": rules,
234 }
235 }
236
237
238
239 func convertToCommandInputs(commands []string) []*model.OperatorInterventionCommandInput {
240 out := []*model.OperatorInterventionCommandInput{}
241 for _, command := range commands {
242 out = append(out, &model.OperatorInterventionCommandInput{
243 Name: command,
244 })
245 }
246 return out
247 }
248
249
250
251 func convertToPrivilegeInputs(privileges []string) []*model.OperatorInterventionPrivilegeInput {
252 out := []*model.OperatorInterventionPrivilegeInput{}
253 for _, privilege := range privileges {
254 out = append(out, &model.OperatorInterventionPrivilegeInput{
255 Name: privilege,
256 })
257 }
258 return out
259 }
260
261
262
263
264
265 type oiLoadDataMutation struct {
266 CreateOperatorInterventionCommands struct {
267 model.CreateOperatorInterventionCommandResponse
268 } `graphql:"createOperatorInterventionCommands(commands: $commands) @skip(if: $skipCommands)"`
269 CreateOperatorInterventionPrivileges struct {
270 model.CreateOperatorInterventionPrivilegeResponse
271 } `graphql:"createOperatorInterventionPrivileges(privileges: $privileges) @skip(if: $skipPrivileges)"`
272 UpdateOperatorInterventionRules struct {
273 model.UpdateOperatorInterventionRuleResponse
274 } `graphql:"updateOperatorInterventionRules(rules: $rules) @skip(if: $skipRules)"`
275 UpdateOperatorInterventionRoleMappings struct {
276 model.UpdateOperatorInterventionRoleMappingResponse
277 } `graphql:"updateOperatorInterventionRoleMappings(roleMappings: $roleMappings) @skip(if: $skipRoleMappings)"`
278 }
279
280 func loadData(ctx context.Context, cl *client.EdgeClient, conf config) error {
281 variables := createVariables(conf)
282
283 var mutation oiLoadDataMutation
284 err := cl.Mutate(ctx, &mutation, variables)
285 if err != nil {
286 return err
287 }
288
289 output, err := generateAllOutput(variables, mutation)
290
291
292
293
294 fmt.Print(output)
295 return err
296 }
297
298
299
300
301
302 func generateAllOutput(variables map[string]interface{}, mutation oiLoadDataMutation) (string, error) {
303 var failed bool
304 var output []string
305
306 skipComamands := variables["skipCommands"]
307 if skip, ok := skipComamands.(graphql.Boolean); ok && bool(!skip) {
308 if len(mutation.CreateOperatorInterventionCommands.Errors) != 0 {
309 failed = true
310 }
311 output = append(output, format.GenerateApplyOutput("Commands", mutation.CreateOperatorInterventionCommands.Errors))
312 }
313
314 skipPrivileges := variables["skipPrivileges"]
315 if skip, ok := skipPrivileges.(graphql.Boolean); ok && bool(!skip) {
316 if len(mutation.CreateOperatorInterventionPrivileges.Errors) != 0 {
317 failed = true
318 }
319 output = append(output, format.GenerateApplyOutput("Privileges", mutation.CreateOperatorInterventionPrivileges.Errors))
320 }
321
322 skipRules := variables["skipRules"]
323 if skip, ok := skipRules.(graphql.Boolean); ok && bool(!skip) {
324 if len(mutation.UpdateOperatorInterventionRules.Errors) != 0 {
325 failed = true
326 }
327 output = append(output, format.GenerateApplyOutput("Rules", mutation.UpdateOperatorInterventionRules.Errors))
328 }
329
330 skipRoleMappings := variables["skipRoleMappings"]
331 if skip, ok := skipRoleMappings.(graphql.Boolean); ok && bool(!skip) {
332 if len(mutation.UpdateOperatorInterventionRoleMappings.Errors) != 0 {
333 failed = true
334 }
335 output = append(output, format.GenerateApplyOutput("Role Mappings", mutation.UpdateOperatorInterventionRoleMappings.Errors))
336 }
337
338 var err error
339 if failed {
340 err = fmt.Errorf("error during mutation")
341 }
342 return strings.Join(output, "\n"), err
343 }
344
View as plain text