...

Source file src/edge-infra.dev/pkg/edge/bsl-reconciler/utils.go

Documentation: edge-infra.dev/pkg/edge/bsl-reconciler

     1  package edgebsl
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"math/rand"
     8  	"net/http"
     9  	"reflect"
    10  	"strings"
    11  	"unicode"
    12  
    13  	"github.com/go-resty/resty/v2"
    14  	"github.com/sethvargo/go-password/password"
    15  
    16  	bslerror "edge-infra.dev/pkg/edge/api/apierror/bsl"
    17  	"edge-infra.dev/pkg/edge/api/bsl/types"
    18  	"edge-infra.dev/pkg/edge/api/graph/model"
    19  	"edge-infra.dev/pkg/edge/bsl"
    20  	logger "edge-infra.dev/pkg/lib/logging"
    21  )
    22  
    23  var (
    24  	RoleMap = map[model.Role]interface{}{
    25  		model.RoleEdgeOrgAdmin:         model.AllEdgeOrgAdmin,
    26  		model.RoleEdgeBannerAdmin:      model.AllEdgeBannerAdmin,
    27  		model.RoleEdgeBannerViewer:     model.AllEdgeBannerViewer,
    28  		model.RoleEdgeSuperAdmin:       model.AllEdgeSuperAdmin,
    29  		model.RoleEdgeBannerOperator:   model.AllEdgeBannerOperator,
    30  		model.RoleEdgeEngineeringLeads: model.AllEdgeEngineeringLeads,
    31  		model.RoleEdgeOiAdmin:          model.AllEdgeOiAdmin,
    32  		model.RoleEdgeSuperUser:        []string{},
    33  		model.RoleEdgeL1:               []string{},
    34  		model.RoleEdgeL2:               []string{},
    35  		model.RoleEdgeL3:               []string{},
    36  		model.RoleEdgeL4:               []string{},
    37  	}
    38  	// ErrorEmptyPage error returned when page data is empty.
    39  	ErrorEmptyPage = errors.New("no page data was returned")
    40  	// ErrorPaginationDone error returned when this is no more page data.
    41  	ErrorPaginationDone = errors.New("last page of orgs")
    42  	// ErrorInvalidCredentials error returned when the provided credentials are invalid.
    43  	ErrorInvalidCredentials = errors.New("user credentials are invalid")
    44  	// ErrorInsufficientPrivileges error returned when the action requires elevated privileges.
    45  	ErrorInsufficientPrivileges = errors.New("request action requires elevated privileges")
    46  	// ErrorFailedValidation error returned when the request is missing required fields.
    47  	ErrorFailedValidation = errors.New("the request failed validation")
    48  	// ErrorResourceAlreadyExists error returned when the resources already exists.
    49  	ErrorResourceAlreadyExists = errors.New("the resources already exists")
    50  	// ErrorGeneric a generic error returned.
    51  	ErrorGeneric = errors.New("an error occurred")
    52  )
    53  
    54  // GetBSLClient convert config to bsl client
    55  func (b *BslConfig) GetBSLClient(ctx context.Context, org string) (*bsl.Request, error) {
    56  	if b.Client == nil {
    57  		bslClient := bsl.NewBSLClient(types.BSPConfig{
    58  			Endpoint:           b.RootURI,
    59  			Root:               b.RootOrg,
    60  			OrganizationPrefix: b.OrgPrefix,
    61  		})
    62  		bslClient.SetDefaultAccessKey(b.AccessKey.SharedKey, b.AccessKey.SecretKey)
    63  		bslClient.SetTimeout(defaultTimeout)
    64  		bslClient.OnAfterResponse(func(_ *resty.Client, response *resty.Response) error {
    65  			return handleHTTPStatusError(response.StatusCode())
    66  		})
    67  		b.Client = bslClient
    68  	}
    69  	req, err := b.Client.WithBackendOrgAccessKey(ctx, org) // use the same access key for every request.
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  	return req.SetExactOrg(org), err
    74  }
    75  
    76  func (b *BslConfig) GetAllEdgeOrgs(ctx context.Context) ([]AllEdgeOrgsPageContent, error) {
    77  	// Contains aggregated list of BSL orgs
    78  	var fullOrgsList []AllEdgeOrgsPageContent
    79  
    80  	allEdgeOrgs, err := b.GetBSLOrgs(ctx)
    81  
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  
    86  	fullOrgsList = append(fullOrgsList, allEdgeOrgs.PageContent...)
    87  
    88  	var envFilteredList []AllEdgeOrgsPageContent
    89  	for _, org := range fullOrgsList {
    90  		if strings.HasPrefix(org.OrganizationName, b.OrgPrefix) && (org.Parent || strings.Count(org.FullyQualifiedName, "/") < 4) {
    91  			envFilteredList = append(envFilteredList, org)
    92  		}
    93  	}
    94  
    95  	return envFilteredList, nil
    96  }
    97  
    98  func (b *BslConfig) GetBSLOrgs(ctx context.Context) (*AllEdgeOrgs, error) {
    99  	client, err := b.GetBSLClient(ctx, "edge")
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  
   104  	allEdgeOrgs := &AllEdgeOrgs{}
   105  
   106  	if err := client.JSON(http.MethodGet, getEdgeOrgs, allEdgeOrgs); err != nil {
   107  		return nil, err
   108  	}
   109  
   110  	return allEdgeOrgs, nil
   111  }
   112  
   113  // CreateEdgeOrgGroups creates edge org groups.
   114  func (b *BslConfig) CreateEdgeOrgGroups(ctx context.Context, organizationName string) error {
   115  	client, err := b.GetBSLClient(ctx, organizationName)
   116  	if err != nil {
   117  		return err
   118  	}
   119  	for key := range RoleMap {
   120  		g := EdgeOrgGroup{
   121  			GroupName:        string(key),
   122  			GroupDescription: fmt.Sprintf("%s edge user group", key),
   123  		}
   124  
   125  		if err := client.SetPayload(g).Post(createBslGroupPath); err != nil {
   126  			var e *bslerror.Error
   127  			if errors.As(err, &e) && !e.Is(ErrorResourceAlreadyExists) {
   128  				return err
   129  			}
   130  		}
   131  	}
   132  	return nil
   133  }
   134  
   135  // Check any of the edge role user group(s) contains role(s)
   136  // that is not defined in RoleMap (defined above), if un-matched then
   137  // Revoke role(s) from the respective user group to match RoleMap
   138  
   139  func (b *BslConfig) CleanUpGroupRoles(ctx context.Context, organizationName string) error {
   140  	for userGroupName, fullRolesList := range RoleMap {
   141  		// Get list of roles for respective user group
   142  		mappedRoles := reflect.ValueOf(fullRolesList)
   143  		mappedRolesLen := mappedRoles.Len()
   144  		if mappedRolesLen > 0 {
   145  			// Get list of roles assigned to user group from BSL
   146  			groupRolesList, err := b.GetGroupRoles(ctx, userGroupName.String(), organizationName)
   147  			if err != nil {
   148  				return err
   149  			}
   150  
   151  			// Get list of Roles to remove from the respective user group
   152  			rolesToRevoke := getRolesToRevoke(mappedRoles, groupRolesList)
   153  
   154  			if len(rolesToRevoke) > 0 {
   155  				err := b.RevokeRolesFromGroup(ctx, organizationName, userGroupName, rolesToRevoke)
   156  
   157  				if err != nil {
   158  					return err
   159  				}
   160  			}
   161  		}
   162  	}
   163  	return nil
   164  }
   165  
   166  // userGroupRolesList: Roles currently assigned in BSL for a user group
   167  // mappedRolesForUserGroup: Desired role(s) for a user group
   168  // Compiles a list of role(s) to remove that is assigned to current user group
   169  // However, is not present in respective user group list of roles
   170  // defined in RoleMap above
   171  func getRolesToRevoke(mappedRolesForUserGroup reflect.Value, userGroupRolesList []BSLRole) []string {
   172  	rolesToRevoke := make([]string, 0)
   173  
   174  	for _, grantedGroupRole := range userGroupRolesList {
   175  		// if granted role in BSL is not in RoleMap for user group
   176  		// add to roles list to be removed
   177  		if !hasRole(grantedGroupRole.Name, mappedRolesForUserGroup) {
   178  			rolesToRevoke = append(rolesToRevoke, grantedGroupRole.Name)
   179  		}
   180  	}
   181  
   182  	return rolesToRevoke
   183  }
   184  
   185  // mappedRolesForUserGroup: list of roles for a user group defined in RoleMap above
   186  // grantedRole: BSL Role assigned to a user group currently in BSL
   187  // Check if the role assigned to a user group in BSL is present in RoleMap for respective UserGroup
   188  
   189  func hasRole(grantedRole string, mappedRolesForUserGroup reflect.Value) bool {
   190  	roleFound := false
   191  
   192  	for i := 0; i < mappedRolesForUserGroup.Len(); i++ {
   193  		roleNameInMap := mappedRolesForUserGroup.Index(i).String()
   194  
   195  		if roleNameInMap == grantedRole {
   196  			return true
   197  		}
   198  	}
   199  
   200  	return roleFound
   201  }
   202  
   203  func (b *BslConfig) GetGroupRoles(ctx context.Context, userGroupName, organizationName string) ([]BSLRole, error) {
   204  	client, err := b.GetBSLClient(ctx, organizationName)
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  
   209  	groupList := &GetGroupRolesResponse{}
   210  	getUserGroupRolesURL := fmt.Sprintf(getUserGroupGrantedRoles, userGroupName)
   211  	if err := client.JSON(http.MethodGet, getUserGroupRolesURL, groupList); err != nil {
   212  		return nil, err
   213  	}
   214  
   215  	return groupList.Content, nil
   216  }
   217  
   218  // Revoke role(s) from a user group
   219  func (b *BslConfig) RevokeRolesFromGroup(ctx context.Context, organizationName string, userGroupName model.Role, roles []string) error {
   220  	log := logger.NewLogger()
   221  	rolesToRevoke := createBSLRole(userGroupName, roles)
   222  
   223  	client, err := b.GetBSLClient(ctx, organizationName)
   224  	if err != nil {
   225  		return err
   226  	}
   227  
   228  	//Remove roles from userGroup specified
   229  	if err := client.SetPayload(rolesToRevoke).Post(revokeBslRoleFromGroupPath); err != nil {
   230  		log.Error(err, "Error removing roles from BSL user group", rolesToRevoke)
   231  		return err
   232  	}
   233  
   234  	log.Info("Removed Roles from User Group", rolesToRevoke)
   235  
   236  	return nil
   237  }
   238  
   239  // AssignRolesToGroups assign roles to organizations
   240  func (b *BslConfig) AssignRolesToGroups(ctx context.Context, organizationName string) error {
   241  	client, err := b.GetBSLClient(ctx, organizationName)
   242  	if err != nil {
   243  		return err
   244  	}
   245  	for k, v := range RoleMap {
   246  		assignRolesToGroupPayload := createBSLRole(k, v)
   247  		hasRoleMapping := len(assignRolesToGroupPayload.Roles) > 0
   248  
   249  		if hasRoleMapping {
   250  			if err := client.SetPayload(assignRolesToGroupPayload).Post(grantBslRolePath); err != nil {
   251  				return err
   252  			}
   253  		}
   254  	}
   255  	return nil
   256  }
   257  
   258  func createBSLRole(k model.Role, v interface{}) *BSLRoles {
   259  	r := &BSLRoles{
   260  		Groups: []EdgeOrgGroup{
   261  			{
   262  				GroupName: string(k),
   263  			},
   264  		},
   265  		Roles: []BSLRole{},
   266  	}
   267  	rv := reflect.ValueOf(v)
   268  	for i := 0; i < rv.Len(); i++ {
   269  		r.Roles = append(r.Roles, BSLRole{Name: rv.Index(i).String()})
   270  	}
   271  	return r
   272  }
   273  
   274  // CreateBSLUser creates a new bff user in an organization.
   275  func (b *BslConfig) CreateBSLUser(ctx context.Context, organizationName, BFFUsername string) (string, error) {
   276  	client, err := b.GetBSLClient(ctx, organizationName)
   277  	if err != nil {
   278  		return "", err
   279  	}
   280  	if err := client.SetPayload(newBFFUserRequest(BFFUsername)).Post(createBslUserPath); err != nil {
   281  		return "", err
   282  	}
   283  	password, err := generateBFFUserPassword()
   284  	if err != nil {
   285  		return "", err
   286  	}
   287  	return password, b.CreateBSLUserPassword(ctx, organizationName, BFFUsername, password)
   288  }
   289  
   290  func (b *BslConfig) CreateBSLUserAccessKey(ctx context.Context, organization, username string) (*AccessKeyResponse, error) {
   291  	client, err := b.GetBSLClient(ctx, organization)
   292  	if err != nil {
   293  		return nil, err
   294  	}
   295  	res := &AccessKeyResponse{}
   296  	return res, client.SetPayload(newAccessKeyRequest(organization, username)).JSON(http.MethodPost, createUserAccessKeyPath, res)
   297  }
   298  
   299  // AssignBSLUserToGroup assigns the bff user to a group.
   300  func (b *BslConfig) AssignBSLUserToGroup(ctx context.Context, organizationName, groupName, username string) error {
   301  	client, err := b.GetBSLClient(ctx, organizationName)
   302  	if err != nil {
   303  		return err
   304  	}
   305  	return client.SetPayload(newEdgeOrgGroupMembership(groupName, username)).Post(grantRoleToRootUser)
   306  }
   307  
   308  // CreateBSLUserPassword creates a new bff user password by resetting it.
   309  func (b *BslConfig) CreateBSLUserPassword(ctx context.Context, organization, username, password string) error {
   310  	client, err := b.GetBSLClient(ctx, organization)
   311  	if err != nil {
   312  		return err
   313  	}
   314  	return client.SetPayload(newBFFUserPasswordResetRequest(username, password)).Put(resetBslUserPasswordPath)
   315  }
   316  
   317  // CreateEnterpriseUnitType creates a new enterprise unit type in BSL.
   318  func (b *BslConfig) CreateEnterpriseUnitType(ctx context.Context, organization, name, description string) error {
   319  	client, err := b.GetBSLClient(ctx, organization)
   320  	if err != nil {
   321  		return err
   322  	}
   323  	return client.SetPayload(newEnterpriseTypeRequest(name, description)).Post(createEnterpriseUnitType)
   324  }
   325  
   326  // newBFFUserRequest creates a new user account request.
   327  func newBFFUserRequest(username string) *BFFUserRequest {
   328  	return &BFFUserRequest{
   329  		Username:        username,
   330  		Email:           "bffuser@ncr.com",
   331  		FullName:        "Edge BFF",
   332  		GivenName:       "BFF",
   333  		FamilyName:      "Edge",
   334  		TelephoneNumber: "000-000-0000",
   335  		Status:          "ACTIVE",
   336  		Address: &BFFUserAddress{
   337  			City:       "Atlanta",
   338  			Country:    "USA",
   339  			PostalCode: "30303",
   340  			State:      "GA",
   341  			Street:     "Spring St",
   342  		},
   343  	}
   344  }
   345  
   346  // newEnterpriseTypeRequest creates a new enterprise type request.
   347  func newEnterpriseTypeRequest(name, description string) *EnterpriseType {
   348  	return &EnterpriseType{
   349  		Name:        name,
   350  		Description: description,
   351  	}
   352  }
   353  
   354  // newEdgeOrgGroupMembership creates a new edge org group membership.
   355  func newEdgeOrgGroupMembership(groupName string, username string) *EdgeOrgGroupMembership {
   356  	return &EdgeOrgGroupMembership{
   357  		GroupName: groupName,
   358  		Members: []EdgeOrgGroupMember{
   359  			{
   360  				Username: username,
   361  			},
   362  		},
   363  	}
   364  }
   365  
   366  // newBFFUserPasswordResetRequest startManager a new password reset request.
   367  func newBFFUserPasswordResetRequest(username, password string) *BFFUserPasswordReset {
   368  	return &BFFUserPasswordReset{
   369  		Username: username,
   370  		Password: password,
   371  	}
   372  }
   373  
   374  func newAccessKeyRequest(organization, username string) *AccessKeyRequest {
   375  	return &AccessKeyRequest{UserID: UserID{Username: bsl.CreateFullAccountName(&types.AuthUser{Organization: organization, Username: username})}}
   376  }
   377  
   378  // OrgNameToK8sName converts an org name to a kubernetes compatible spec.Name value
   379  // Example /customers/mock-customer-edge-alpha/ is converted to mock-customer-edge-alpha
   380  func OrgNameToK8sName(name string) string {
   381  	name = strings.ToLower(name)
   382  	res := strings.Trim(name, "//") //nolint
   383  	rootOrgSepIndex := strings.Index(res, "/")
   384  	res = strings.ReplaceAll(res, " ", "-")
   385  	res = strings.ReplaceAll(res, "/", "-")
   386  	return res[rootOrgSepIndex+1:]
   387  }
   388  
   389  // generateBFFUserPassword generates a new unique password.
   390  func generateBFFUserPassword() (string, error) {
   391  	pass, err := password.Generate(20, 10, 0, false, true)
   392  	if err != nil {
   393  		return "", err
   394  	}
   395  	allowedSpecialChars := []string{"!", "@", "#", "$", "*", "&", "(", ")"}
   396  	randomSpecialChar := allowedSpecialChars[rand.Intn((len(allowedSpecialChars)))] //nolint gosec
   397  	pass = fmt.Sprintf("%s%s", pass, randomSpecialChar)
   398  	if !checkPasswordRequirements(pass) {
   399  		pass, _ = generateBFFUserPassword()
   400  	}
   401  	return pass, err
   402  }
   403  
   404  // checkPasswordRequirements ensures that the generated password adheres
   405  // to the BSL password requirements which are listed below.
   406  // Requirements: Atleast One Uppercase Letter, One Lowercase Letter,
   407  // One Digit, One non-alphanumeric character
   408  func checkPasswordRequirements(password string) bool {
   409  	var uppercase, lowercase, digit = false, false, false
   410  	for _, value := range password {
   411  		if unicode.IsUpper(value) {
   412  			uppercase = true
   413  		} else if unicode.IsLower(value) {
   414  			lowercase = true
   415  		} else if unicode.IsDigit(value) {
   416  			digit = true
   417  		}
   418  	}
   419  	return uppercase && lowercase && digit
   420  }
   421  
   422  // newBSLUsername creates a new bff username for BSL with a random string suffix
   423  func newBSLUsername(length int, suffix bool) string {
   424  	letterBytes := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
   425  	b := make([]byte, length)
   426  	for i := range b {
   427  		b[i] = letterBytes[rand.Intn(len(letterBytes))] //nolint
   428  	}
   429  	if suffix {
   430  		return fmt.Sprintf("%s-%s", strings.ToLower(string(b)), BFFUsername)
   431  	}
   432  	return strings.ToLower(string(b))
   433  }
   434  
   435  // handleHTTPStatusError convert HTTP status code to errors.
   436  func handleHTTPStatusError(status int) error {
   437  	switch status {
   438  	case http.StatusOK:
   439  		return nil
   440  	case http.StatusUnauthorized:
   441  		return ErrorInvalidCredentials
   442  	case http.StatusForbidden:
   443  		return ErrorInsufficientPrivileges
   444  	case http.StatusBadRequest:
   445  		return ErrorFailedValidation
   446  	case http.StatusConflict:
   447  		return ErrorResourceAlreadyExists
   448  	case http.StatusNoContent:
   449  		return nil
   450  	default:
   451  		return ErrorGeneric
   452  	}
   453  }
   454  

View as plain text