...

Source file src/edge-infra.dev/pkg/edge/api/services/oi_service.go

Documentation: edge-infra.dev/pkg/edge/api/services

     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  	/*
    59  	   Command name validation regex with the following rules:
    60  	   - alphanumeric characters, forward slashes, periods, dashes, and underscores [a-zA-Z0-9\/\.\-\_]
    61  	   - at least one character
    62  	*/
    63  	commandValidation = regexp.MustCompile(`^[a-zA-Z0-9-/._]+$`)
    64  	/*
    65  		Privilige name validation regex with the following rules:
    66  		- The string starts with a letter ([a-zA-Z]).
    67  		- The string is followed by one or more alphanumeric characters or dashes ([a-zA-Z0-9\-]+).
    68  	*/
    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  		// Don't validate the supplied OiRole is a valid edge role. This is
    76  		// because we may need to clean up incorrect data that was manually
    77  		// added to the database
    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  	// Find out if the statement was successful by checking if a row was deleted
    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  		// Query was successful and we can exit
   102  		return result, nil
   103  	}
   104  
   105  	// Otherwise, return an error indicating the role mapping is unknown.
   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  // scans a *sql.Rows of role_name, privilege_name rows into a []*model.OiRole.
   132  // Expects the sql.Rows to be ordered by role_name
   133  func (o *operatorInterventionService) scanOiRoleMappingRows(rows *sql.Rows) ([]*model.OiRoleMapping, error) {
   134  	defer rows.Close()
   135  
   136  	roles := []*model.OiRoleMapping{}
   137  
   138  	// Index of current Role mapping in slice. Start at -1 to make first
   139  	// iteration of loop nicer so that we can increment to the 0th index on the
   140  	// first iteration
   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  		// The sql query should sort the results by the role name
   152  		// If the rolename is the same as the previous role name we can
   153  		// immediately append the privilege to the previous role mapping
   154  		if roleName == prevRole {
   155  			roles[idx].Privileges = append(
   156  				roles[idx].Privileges,
   157  				&model.Privilege{Name: privilegeName},
   158  			)
   159  		} else {
   160  			// Otherwise create a new role mapping for the new role at the end
   161  			// of the slice, and add a single privilege to it.
   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  	/*
   185  		Requirements for insert:
   186  		1. Multiple inserts should take a single DB query. Additional queries may be done if there is unexpected behaviour, e.g. duplicate mappings or missing privileges.
   187  		2. All or nothing. If one privilege out of the full set is missing, no mappings should be inserted into the DB.
   188  		3. If a mapping already exists in the DB the mapping should remain unchanged and a success result returned.
   189  		4. If there is one or more missing privileges, the returned error must identify all missing privileges.
   190  
   191  		Algorithm:
   192  			Attempt to insert all (role, privilege) pairs in a single query
   193  			Count the number of additions made in the query
   194  			If equal to the number of pairs:
   195  				Everything has been added successfully
   196  				Commit the transaction and return with success
   197  			If not equal: (this indicates there is either a duplicate pair already present in the DB or one of the privileges don't exist in the ea_rules_privileges table)
   198  				Query for all privileges in the ea_rules_privileges table
   199  				Find any privs in the (role, privilege) pairs that are not present in ea_rules_privileges
   200  				If there are missing privileges:
   201  					Return an error for every missing privilege
   202  				If none are missing:
   203  					This indicates the missing additions are from pre-existing duplicate entries
   204  					This is ok
   205  					Return success result
   206  	*/
   207  
   208  	// Start a transaction
   209  	transaction, err := o.SQLDB.BeginTx(ctx, nil)
   210  	if err != nil {
   211  		return nil, err
   212  	}
   213  
   214  	defer func() {
   215  		// All or nothing behaviour - rollback if there were any errors
   216  		if err != nil || (roleMappingResponse != nil && len(roleMappingResponse.Errors) != 0) {
   217  			err = errors.Join(err, transaction.Rollback())
   218  		} else {
   219  			// Only commit transaction on no errors
   220  			err = transaction.Commit()
   221  		}
   222  	}()
   223  
   224  	sqlStatementData, errorResponse := processRoleMappings(roleMappings)
   225  	if sqlStatementData.SQLStatement == "" {
   226  		// No statement indicates all roles/privileges were invalid
   227  		// We inspect the returned statement here rather than a non-empty
   228  		// errorResponse as this gives us the ability to later detect if any of
   229  		// the supplied privileges are not known within the DB.
   230  		return &model.UpdateOperatorInterventionRoleMappingResponse{Errors: errorResponse}, nil
   231  	}
   232  
   233  	if len(sqlStatementData.Privileges) > 500 {
   234  		// protect the database from a massive bulk insert attempt
   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  	// Check how many rows were inserted. If it's not equal to the number that
   244  	// we attempted to insert, then either there was duplicate mappings that
   245  	// made no changes, or there was privileges that don't exist in the
   246  	// ea_rules_privileges table. If the former, then we can continue onwards,
   247  	// but if it's the latter then we should return an error
   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  		// Add all missing privileges to the list of errors to be returned,
   260  		// if there were no missing privileges then this will add no errors to
   261  		// the list
   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  // processRoleMappingsResult encapsulates the results of processing role mappings.
   275  type processRoleMappingsResult struct {
   276  	SQLStatement   string
   277  	QueryArguments []any
   278  	Privileges     []string
   279  }
   280  
   281  // processRoleMappings builds up a valid SQL Statement to insert all role
   282  // mappings. Additionally returns the slice of args to be passed when executing
   283  // the query, and a slice of all privilege names added for all roles. The
   284  // model.OiErrorResponse return value indicates if any of the incoming roles or
   285  // privileges are invalid.
   286  func processRoleMappings(AddRoleMappings []*model.UpdateOperatorInterventionRoleMappingInput) (processRoleMappingsResult, []*model.OperatorInterventionErrorResponse) {
   287  	roles, privileges, errorResponse := decomposeRoleMappings(AddRoleMappings)
   288  
   289  	// If all roles are invalid and there is nothing to insert exit early
   290  	if len(roles) == 0 {
   291  		return processRoleMappingsResult{}, errorResponse
   292  	}
   293  
   294  	params, args := generateQueryParameters(roles, privileges)
   295  
   296  	// Build up full sql statement including query placeholder values
   297  	stmt := generateSQLQuery(params)
   298  
   299  	return processRoleMappingsResult{
   300  		SQLStatement:   stmt,
   301  		QueryArguments: args,
   302  		Privileges:     privileges,
   303  	}, errorResponse
   304  }
   305  
   306  // generateSQLQuery generates a valid SQL Query which can be used to insert
   307  // role mappings in the DB. This query includes a variable number of placeholder
   308  // values to insert all mappings in a single query, and uses
   309  // [sqlquery.OiInsertRoleMappingPartialQueryBegin].
   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  // decomposeRoleMappings is used to generate 2 slices, one of roles and one of
   319  // privileges. The slices should be a 1-to-1 mapping of the role -> privilege
   320  // pairs that should be added to the DB, and must be equivalent in length.
   321  // The model.OiErrorResponse return value is used to indicate if any of the
   322  // supplied roles are not valid Edge Roles.
   323  func decomposeRoleMappings(AddRoleMappings []*model.UpdateOperatorInterventionRoleMappingInput) ([]string, []string, []*model.OperatorInterventionErrorResponse) {
   324  	var errorResponse []*model.OperatorInterventionErrorResponse
   325  
   326  	// Lists of roles and prvileges, the lists should be a 1-to-1 mapping for
   327  	// each mapping to be added to the db, i.e. if multiple privileges are to be
   328  	// added for a single role, the roles slice will contain duplicate entries
   329  	var roles []string
   330  	var privileges []string
   331  
   332  	// Build up the full set of roles and privileges to be added to the DB
   333  	for _, roleMapping := range AddRoleMappings {
   334  		if !model.Role(roleMapping.Role).IsValid() {
   335  			// Don't attempt to add an invalid role, add the role to the list of
   336  			// errors and continue to the next role to make sure the returned
   337  			// list of invalid values is as full as possible
   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  // generateQueryParameters accepts 2 slices, representing the 1-to-1 mapping
   360  // between roles and privileges to be added to the DB. It creates a slice of the
   361  // placeholder values to be inserted into the
   362  // [sqlquery.OiInsertRoleMappingPartialQueryBegin] query, along with a slice of
   363  // args to be passed when executing the sql query.
   364  func generateQueryParameters(roles []string, privileges []string) ([]string, []any) {
   365  	// List of roles and privileges to be passed to query, alternating between role
   366  	// and  privilege, e.g. `[]string{"EDGE_ORG_ADMIN", "ea-basic", "EDGE_BANNER_ADMIN", "ea-read"}`
   367  	var args []any
   368  
   369  	// Slice of query placeholder values, e.g. `[]string{"($1, $2)", "($3, $4)"}`
   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  // findMissingPrivs returns a subset of the passed in privileges. This subset
   382  // corresponds to the elements of privileges which are not present within the
   383  // ea_rules_privileges table. If all privileges are present in the db table an
   384  // empty slice is returned.
   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  // difference returns all members of the superset that are not present in the
   410  // subset
   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  	// Validate that payload list is non-nil
   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  	// validate the command names using the regex
   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  	// We have no need to see the result as we are not expecting any
   465  	// conflicts to be returned from rulesengine
   466  	_, err := o.reng.AddCommands(ctx, payload)
   467  	return &model.CreateOperatorInterventionCommandResponse{}, err
   468  }
   469  
   470  //nolint:dupl
   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  // CreatePrivileges creates operator intervention privileges.
   502  // It takes a context and a slice of CreateOperatorInterventionPrivilegeInput as input.
   503  // It returns a CreateOperatorInterventionPrivilegeResponse and an error.
   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  	// Validate that payload list is non-nil
   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  	// validate the privilege names using the regex
   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  // ReadPrivileges retrieves operator intervention privileges based on the provided privilege names.
   542  // It returns a list of privileges that match the provided names, or all privileges if no names are provided.
   543  // If an error occurs during the retrieval process, it returns nil and the error.
   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  // DeletePrivilege deletes the operator intervention privilege based on the provided payload.
   562  // It returns a response containing any errors encountered during the deletion process.
   563  //
   564  //nolint:dupl
   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 { //nolint
   594  	// Rulesengine Dataset struct does not log anything so it is ok to use
   595  	// a brand new logger for the sake of initialization
   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