1 package services
2
3 import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "regexp"
9 "slices"
10 "strings"
11
12 sqlerr "edge-infra.dev/pkg/edge/api/apierror/sql"
13 "edge-infra.dev/pkg/edge/api/graph/model"
14 sqlquery "edge-infra.dev/pkg/edge/api/sql"
15 "edge-infra.dev/pkg/lib/fog"
16 rulesengine "edge-infra.dev/pkg/sds/emergencyaccess/rules"
17 rulesdata "edge-infra.dev/pkg/sds/emergencyaccess/rules/storage/database"
18 )
19
20 type OperatorInterventionService interface {
21 DeleteRoleMapping(ctx context.Context, mapping model.DeleteOperatorInterventionMappingInput) (*model.DeleteOperatorInterventionResponse, error)
22 RoleMappings(ctx context.Context, roles []string) ([]*model.OiRoleMapping, error)
23 UpdateRoleMappings(ctx context.Context, roleMappings []*model.UpdateOperatorInterventionRoleMappingInput) (*model.UpdateOperatorInterventionRoleMappingResponse, error)
24
25 ReadPrivileges(ctx context.Context, names []*model.OperatorInterventionPrivilegeInput) ([]*model.Privilege, error)
26 CreatePrivileges(ctx context.Context, input []*model.OperatorInterventionPrivilegeInput) (*model.CreateOperatorInterventionPrivilegeResponse, error)
27 DeletePrivilege(ctx context.Context, payload model.OperatorInterventionPrivilegeInput) (*model.DeleteOperatorInterventionPrivilegeResponse, error)
28
29 DeleteRule(ctx context.Context, payload model.DeleteOperatorInterventionRuleInput) (*model.DeleteOperatorInterventionRuleResponse, error)
30 ReadRules(ctx context.Context, privileges []*model.OperatorInterventionPrivilegeInput) ([]*model.Rule, error)
31 UpdateRules(ctx context.Context, rules []*model.UpdateOperatorInterventionRuleInput) (*model.UpdateOperatorInterventionRuleResponse, error)
32
33 ReadCommands(ctx context.Context, payload []*model.OperatorInterventionCommandInput) ([]*model.Command, error)
34 CreateCommands(ctx context.Context, mappings []*model.OperatorInterventionCommandInput) (*model.CreateOperatorInterventionCommandResponse, error)
35 DeleteCommand(ctx context.Context, payload model.OperatorInterventionCommandInput) (*model.DeleteOperatorInterventionCommandResponse, error)
36 }
37
38 type rulesEngine interface {
39 AddPrivileges(ctx context.Context, payload []rulesengine.PostPrivilegePayload) (rulesengine.AddNameResult, error)
40 DeletePrivilege(ctx context.Context, privilege string) (rulesengine.DeleteResult, error)
41 ReadPrivilegesWithFilter(ctx context.Context, names []string) ([]rulesengine.Privilege, error)
42
43 AddCommands(ctx context.Context, names []rulesengine.PostCommandPayload) (rulesengine.AddNameResult, error)
44 DeleteCommand(ctx context.Context, name string) (rulesengine.DeleteResult, error)
45 ReadCommandsWithFilter(ctx context.Context, names []string) ([]rulesengine.Command, error)
46
47 GetDefaultRules(ctx context.Context, privileges ...string) ([]rulesengine.ReturnRuleSet, error)
48 AddDefaultRulesForPrivileges(ctx context.Context, data rulesengine.RuleSets) (rulesengine.AddRuleResult, error)
49 DeleteDefaultRule(ctx context.Context, commandName, privilegeName string) (rulesengine.DeleteResult, error)
50 }
51
52 type operatorInterventionService struct {
53 SQLDB *sql.DB
54 reng rulesEngine
55 }
56
57 var (
58
63 commandValidation = regexp.MustCompile(`^[a-zA-Z0-9-/._]+$`)
64
69 privilegeValidation = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9-]+$`)
70 )
71
72 func (o *operatorInterventionService) DeleteRoleMapping(ctx context.Context, mapping model.DeleteOperatorInterventionMappingInput) (result *model.DeleteOperatorInterventionResponse, err error) {
73 result = &model.DeleteOperatorInterventionResponse{}
74 if mapping.Role == "" {
75
76
77
78 result.Errors = append(result.Errors, &model.OperatorInterventionErrorResponse{Type: model.OperatorInterventionErrorTypeUnknownRole, Role: &mapping.Role})
79 }
80 if mapping.Privilege.Name == "" {
81 result.Errors = append(result.Errors, &model.OperatorInterventionErrorResponse{Type: model.OperatorInterventionErrorTypeUnknownPrivilege, Privilege: &mapping.Privilege.Name})
82 }
83
84 if len(result.Errors) != 0 {
85 return result, nil
86 }
87
88 res, err := o.SQLDB.ExecContext(ctx, sqlquery.DeleteOIRoleMapping, mapping.Role, mapping.Privilege.Name)
89 if err != nil {
90 return nil, fmt.Errorf("error executing sql query: %w", err)
91 }
92
93
94
95 count, err := res.RowsAffected()
96 if err != nil {
97 return nil, fmt.Errorf("error finding rows affected: %w", err)
98 }
99
100 if count == 1 {
101
102 return result, nil
103 }
104
105
106 result.Errors = append(result.Errors, &model.OperatorInterventionErrorResponse{
107 Type: model.OperatorInterventionErrorTypeUnknownRoleMapping,
108 Role: &mapping.Role,
109 Privilege: &mapping.Privilege.Name,
110 })
111
112 return result, nil
113 }
114
115 func (o *operatorInterventionService) RoleMappings(ctx context.Context, roles []string) ([]*model.OiRoleMapping, error) {
116 var rows *sql.Rows
117 var err error
118 if len(roles) != 0 {
119 rows, err = o.SQLDB.QueryContext(ctx, sqlquery.GetOiRoleMappingSubset, roles)
120 } else {
121 rows, err = o.SQLDB.QueryContext(ctx, sqlquery.GetOIRoleMapping)
122 }
123
124 if err != nil {
125 return nil, err
126 }
127
128 return o.scanOiRoleMappingRows(rows)
129 }
130
131
132
133 func (o *operatorInterventionService) scanOiRoleMappingRows(rows *sql.Rows) ([]*model.OiRoleMapping, error) {
134 defer rows.Close()
135
136 roles := []*model.OiRoleMapping{}
137
138
139
140
141 var idx = -1
142 var prevRole string
143
144 for rows.Next() {
145 var roleName, privilegeName string
146 err := rows.Scan(&roleName, &privilegeName)
147 if err != nil {
148 return nil, err
149 }
150
151
152
153
154 if roleName == prevRole {
155 roles[idx].Privileges = append(
156 roles[idx].Privileges,
157 &model.Privilege{Name: privilegeName},
158 )
159 } else {
160
161
162 idx++
163 prevRole = roleName
164 roles = append(
165 roles,
166 &model.OiRoleMapping{
167 Role: model.Role(roleName),
168 Privileges: []*model.Privilege{
169 {Name: privilegeName},
170 },
171 },
172 )
173 }
174 }
175
176 if err := rows.Err(); err != nil {
177 return nil, sqlerr.Wrap(err)
178 }
179
180 return roles, nil
181 }
182
183 func (o *operatorInterventionService) UpdateRoleMappings(ctx context.Context, roleMappings []*model.UpdateOperatorInterventionRoleMappingInput) (roleMappingResponse *model.UpdateOperatorInterventionRoleMappingResponse, err error) {
184
207
208
209 transaction, err := o.SQLDB.BeginTx(ctx, nil)
210 if err != nil {
211 return nil, err
212 }
213
214 defer func() {
215
216 if err != nil || (roleMappingResponse != nil && len(roleMappingResponse.Errors) != 0) {
217 err = errors.Join(err, transaction.Rollback())
218 } else {
219
220 err = transaction.Commit()
221 }
222 }()
223
224 sqlStatementData, errorResponse := processRoleMappings(roleMappings)
225 if sqlStatementData.SQLStatement == "" {
226
227
228
229
230 return &model.UpdateOperatorInterventionRoleMappingResponse{Errors: errorResponse}, nil
231 }
232
233 if len(sqlStatementData.Privileges) > 500 {
234
235 return nil, fmt.Errorf("attempting to insert too many role mappings")
236 }
237
238 res, err := transaction.ExecContext(ctx, sqlStatementData.SQLStatement, sqlStatementData.QueryArguments...)
239 if err != nil {
240 return nil, fmt.Errorf("failed to execute query: %w", err)
241 }
242
243
244
245
246
247
248 noRows, err := res.RowsAffected()
249 if err != nil {
250 return nil, fmt.Errorf("error discovering number of rows: %w", err)
251 }
252
253 if int(noRows) != len(sqlStatementData.Privileges) {
254 missingPrivs, err := findMissingPrivs(ctx, transaction, sqlStatementData.Privileges)
255 if err != nil {
256 return nil, fmt.Errorf("error finding missing privileges: %w", err)
257 }
258
259
260
261
262 for _, priv := range missingPrivs {
263 priv := priv
264 errorResponse = append(errorResponse, &model.OperatorInterventionErrorResponse{
265 Type: model.OperatorInterventionErrorTypeUnknownPrivilege,
266 Privilege: &priv,
267 })
268 }
269 }
270
271 return &model.UpdateOperatorInterventionRoleMappingResponse{Errors: errorResponse}, nil
272 }
273
274
275 type processRoleMappingsResult struct {
276 SQLStatement string
277 QueryArguments []any
278 Privileges []string
279 }
280
281
282
283
284
285
286 func processRoleMappings(AddRoleMappings []*model.UpdateOperatorInterventionRoleMappingInput) (processRoleMappingsResult, []*model.OperatorInterventionErrorResponse) {
287 roles, privileges, errorResponse := decomposeRoleMappings(AddRoleMappings)
288
289
290 if len(roles) == 0 {
291 return processRoleMappingsResult{}, errorResponse
292 }
293
294 params, args := generateQueryParameters(roles, privileges)
295
296
297 stmt := generateSQLQuery(params)
298
299 return processRoleMappingsResult{
300 SQLStatement: stmt,
301 QueryArguments: args,
302 Privileges: privileges,
303 }, errorResponse
304 }
305
306
307
308
309
310 func generateSQLQuery(params []string) string {
311 return fmt.Sprintf("%s %s %s",
312 sqlquery.OiInsertRoleMappingPartialQueryBegin,
313 strings.Join(params, ", "),
314 sqlquery.OiInsertRoleMappingPartialQueryEnd,
315 )
316 }
317
318
319
320
321
322
323 func decomposeRoleMappings(AddRoleMappings []*model.UpdateOperatorInterventionRoleMappingInput) ([]string, []string, []*model.OperatorInterventionErrorResponse) {
324 var errorResponse []*model.OperatorInterventionErrorResponse
325
326
327
328
329 var roles []string
330 var privileges []string
331
332
333 for _, roleMapping := range AddRoleMappings {
334 if !model.Role(roleMapping.Role).IsValid() {
335
336
337
338 errorResponse = append(errorResponse, &model.OperatorInterventionErrorResponse{Type: model.OperatorInterventionErrorTypeUnknownRole, Role: &roleMapping.Role})
339 continue
340 }
341
342 for _, privilege := range roleMapping.Privileges {
343 privilege := privilege
344 if privilege.Name == "" {
345 errorResponse = append(errorResponse, &model.OperatorInterventionErrorResponse{
346 Type: model.OperatorInterventionErrorTypeUnknownPrivilege,
347 Privilege: &privilege.Name,
348 })
349 continue
350 }
351
352 roles = append(roles, roleMapping.Role)
353 privileges = append(privileges, privilege.Name)
354 }
355 }
356 return roles, privileges, errorResponse
357 }
358
359
360
361
362
363
364 func generateQueryParameters(roles []string, privileges []string) ([]string, []any) {
365
366
367 var args []any
368
369
370 var params []string
371
372 for idx := range roles {
373 args = append(args, roles[idx], privileges[idx])
374 roleIdx := fmt.Sprintf("$%d", idx*2+1)
375 privIdx := fmt.Sprintf("$%d", idx*2+2)
376 params = append(params, fmt.Sprintf(`(%s, %s)`, roleIdx, privIdx))
377 }
378 return params, args
379 }
380
381
382
383
384
385 func findMissingPrivs(ctx context.Context, tx *sql.Tx, privileges []string) ([]string, error) {
386 rows, err := tx.QueryContext(ctx, sqlquery.GetOiPrivilegesSubset, privileges)
387 if err != nil {
388 return nil, fmt.Errorf("error selecting privileges: %w", err)
389 }
390 defer rows.Close()
391
392 foundPrivs := []string{}
393 for rows.Next() {
394 var privName string
395 err := rows.Scan(&privName)
396 if err != nil {
397 return nil, fmt.Errorf("error scanning privs row: %w", err)
398 }
399 foundPrivs = append(foundPrivs, privName)
400 }
401
402 if err := rows.Err(); err != nil {
403 return nil, fmt.Errorf("error while reading privileges: %w", err)
404 }
405
406 return difference(privileges, foundPrivs), nil
407 }
408
409
410
411 func difference(superset []string, subset []string) []string {
412 diff := []string{}
413 for _, u := range superset {
414 if !slices.Contains(subset, u) {
415 diff = append(diff, u)
416 }
417 }
418 return diff
419 }
420
421 func (o *operatorInterventionService) ReadCommands(ctx context.Context, payload []*model.OperatorInterventionCommandInput) ([]*model.Command, error) {
422 var names []string
423 for _, command := range payload {
424 names = append(names, command.Name)
425 }
426 ret, err := o.reng.ReadCommandsWithFilter(ctx, names)
427 if err != nil {
428 return nil, err
429 }
430 var commands []*model.Command
431 for _, command := range ret {
432 commands = append(commands, &model.Command{Name: command.Name})
433 }
434 return commands, nil
435 }
436
437 func (o *operatorInterventionService) CreateCommands(ctx context.Context, mappings []*model.OperatorInterventionCommandInput) (*model.CreateOperatorInterventionCommandResponse, error) {
438 var payload []rulesengine.PostCommandPayload
439 for _, mapping := range mappings {
440 payload = append(payload, rulesengine.PostCommandPayload{Name: mapping.Name})
441 }
442
443 if len(payload) == 0 {
444 e := &model.OperatorInterventionErrorResponse{Type: model.OperatorInterventionErrorTypeInvalidInput}
445 return &model.CreateOperatorInterventionCommandResponse{
446 Errors: []*model.OperatorInterventionErrorResponse{e},
447 }, nil
448 }
449
450 var errs []*model.OperatorInterventionErrorResponse
451 for _, p := range payload {
452 p := p
453 if !commandValidation.MatchString(p.Name) {
454 errs = append(errs, &model.OperatorInterventionErrorResponse{
455 Type: model.OperatorInterventionErrorTypeInvalidInput,
456 Command: &p.Name})
457 }
458 }
459 if len(errs) > 0 {
460 return &model.CreateOperatorInterventionCommandResponse{
461 Errors: errs,
462 }, nil
463 }
464
465
466 _, err := o.reng.AddCommands(ctx, payload)
467 return &model.CreateOperatorInterventionCommandResponse{}, err
468 }
469
470
471 func (o *operatorInterventionService) DeleteCommand(ctx context.Context, payload model.OperatorInterventionCommandInput) (*model.DeleteOperatorInterventionCommandResponse, error) {
472 if payload.Name == "" {
473 e := &model.OperatorInterventionErrorResponse{Type: model.OperatorInterventionErrorTypeInvalidInput}
474 return &model.DeleteOperatorInterventionCommandResponse{
475 Errors: []*model.OperatorInterventionErrorResponse{e},
476 }, nil
477 }
478
479 ret, err := o.reng.DeleteCommand(ctx, payload.Name)
480 if err != nil {
481 return nil, err
482 }
483
484 var errs []*model.OperatorInterventionErrorResponse
485 for _, err := range ret.Errors {
486 commandCopy := payload.Name
487 errType := model.OperatorInterventionErrorTypeUnknownCommand
488 if err.Type == rulesengine.Conflict {
489 errType = model.OperatorInterventionErrorTypeConflict
490 }
491 errs = append(errs, &model.OperatorInterventionErrorResponse{
492 Type: errType,
493 Command: &commandCopy,
494 })
495 }
496 return &model.DeleteOperatorInterventionCommandResponse{
497 Errors: errs,
498 }, nil
499 }
500
501
502
503
504 func (o *operatorInterventionService) CreatePrivileges(ctx context.Context, input []*model.OperatorInterventionPrivilegeInput) (*model.CreateOperatorInterventionPrivilegeResponse, error) {
505 var payload []rulesengine.PostPrivilegePayload
506
507 for _, mapping := range input {
508 payload = append(payload, rulesengine.PostPrivilegePayload{Name: mapping.Name})
509 }
510
511 if len(payload) == 0 {
512 e := &model.OperatorInterventionErrorResponse{Type: model.OperatorInterventionErrorTypeInvalidInput}
513 return &model.CreateOperatorInterventionPrivilegeResponse{
514 Errors: []*model.OperatorInterventionErrorResponse{e},
515 }, nil
516 }
517
518 var errs []*model.OperatorInterventionErrorResponse
519 for _, p := range payload {
520 p := p
521 if !privilegeValidation.MatchString(p.Name) {
522 errs = append(errs, &model.OperatorInterventionErrorResponse{
523 Type: model.OperatorInterventionErrorTypeInvalidInput,
524 Privilege: &p.Name})
525 }
526 }
527 if len(errs) > 0 {
528 return &model.CreateOperatorInterventionPrivilegeResponse{
529 Errors: errs,
530 }, nil
531 }
532 _, err := o.reng.AddPrivileges(ctx, payload)
533 if err != nil {
534 return nil, err
535 }
536 return &model.CreateOperatorInterventionPrivilegeResponse{
537 Errors: nil,
538 }, nil
539 }
540
541
542
543
544 func (o *operatorInterventionService) ReadPrivileges(ctx context.Context, payload []*model.OperatorInterventionPrivilegeInput) ([]*model.Privilege, error) {
545 var names []string
546 for _, name := range payload {
547 names = append(names, name.Name)
548 }
549
550 ret, err := o.reng.ReadPrivilegesWithFilter(ctx, names)
551 if err != nil {
552 return nil, err
553 }
554 var privileges []*model.Privilege
555 for _, privilege := range ret {
556 privileges = append(privileges, &model.Privilege{Name: privilege.Name})
557 }
558 return privileges, nil
559 }
560
561
562
563
564
565 func (o *operatorInterventionService) DeletePrivilege(ctx context.Context, payload model.OperatorInterventionPrivilegeInput) (*model.DeleteOperatorInterventionPrivilegeResponse, error) {
566 if payload.Name == "" {
567 e := &model.OperatorInterventionErrorResponse{Type: model.OperatorInterventionErrorTypeInvalidInput}
568 return &model.DeleteOperatorInterventionPrivilegeResponse{
569 Errors: []*model.OperatorInterventionErrorResponse{e},
570 }, nil
571 }
572 ret, err := o.reng.DeletePrivilege(ctx, payload.Name)
573 if err != nil {
574 return nil, err
575 }
576 var errs []*model.OperatorInterventionErrorResponse
577 for _, err := range ret.Errors {
578 privilegeCopy := payload.Name
579 errType := model.OperatorInterventionErrorTypeUnknownPrivilege
580 if err.Type == rulesengine.Conflict {
581 errType = model.OperatorInterventionErrorTypeConflict
582 }
583 errs = append(errs, &model.OperatorInterventionErrorResponse{
584 Type: errType,
585 Privilege: &privilegeCopy,
586 })
587 }
588 return &model.DeleteOperatorInterventionPrivilegeResponse{
589 Errors: errs,
590 }, nil
591 }
592
593 func NewOperatorInterventionService(sqlDB *sql.DB) *operatorInterventionService {
594
595
596 ds := rulesdata.New(fog.New(), sqlDB)
597 reng := rulesengine.New(ds)
598 return &operatorInterventionService{
599 SQLDB: sqlDB,
600 reng: reng,
601 }
602 }
603
View as plain text