package edgebsl import ( "context" "errors" "fmt" "math/rand" "net/http" "reflect" "strings" "unicode" "github.com/go-resty/resty/v2" "github.com/sethvargo/go-password/password" bslerror "edge-infra.dev/pkg/edge/api/apierror/bsl" "edge-infra.dev/pkg/edge/api/bsl/types" "edge-infra.dev/pkg/edge/api/graph/model" "edge-infra.dev/pkg/edge/bsl" logger "edge-infra.dev/pkg/lib/logging" ) var ( RoleMap = map[model.Role]interface{}{ model.RoleEdgeOrgAdmin: model.AllEdgeOrgAdmin, model.RoleEdgeBannerAdmin: model.AllEdgeBannerAdmin, model.RoleEdgeBannerViewer: model.AllEdgeBannerViewer, model.RoleEdgeSuperAdmin: model.AllEdgeSuperAdmin, model.RoleEdgeBannerOperator: model.AllEdgeBannerOperator, model.RoleEdgeEngineeringLeads: model.AllEdgeEngineeringLeads, model.RoleEdgeOiAdmin: model.AllEdgeOiAdmin, model.RoleEdgeSuperUser: []string{}, model.RoleEdgeL1: []string{}, model.RoleEdgeL2: []string{}, model.RoleEdgeL3: []string{}, model.RoleEdgeL4: []string{}, } // ErrorEmptyPage error returned when page data is empty. ErrorEmptyPage = errors.New("no page data was returned") // ErrorPaginationDone error returned when this is no more page data. ErrorPaginationDone = errors.New("last page of orgs") // ErrorInvalidCredentials error returned when the provided credentials are invalid. ErrorInvalidCredentials = errors.New("user credentials are invalid") // ErrorInsufficientPrivileges error returned when the action requires elevated privileges. ErrorInsufficientPrivileges = errors.New("request action requires elevated privileges") // ErrorFailedValidation error returned when the request is missing required fields. ErrorFailedValidation = errors.New("the request failed validation") // ErrorResourceAlreadyExists error returned when the resources already exists. ErrorResourceAlreadyExists = errors.New("the resources already exists") // ErrorGeneric a generic error returned. ErrorGeneric = errors.New("an error occurred") ) // GetBSLClient convert config to bsl client func (b *BslConfig) GetBSLClient(ctx context.Context, org string) (*bsl.Request, error) { if b.Client == nil { bslClient := bsl.NewBSLClient(types.BSPConfig{ Endpoint: b.RootURI, Root: b.RootOrg, OrganizationPrefix: b.OrgPrefix, }) bslClient.SetDefaultAccessKey(b.AccessKey.SharedKey, b.AccessKey.SecretKey) bslClient.SetTimeout(defaultTimeout) bslClient.OnAfterResponse(func(_ *resty.Client, response *resty.Response) error { return handleHTTPStatusError(response.StatusCode()) }) b.Client = bslClient } req, err := b.Client.WithBackendOrgAccessKey(ctx, org) // use the same access key for every request. if err != nil { return nil, err } return req.SetExactOrg(org), err } func (b *BslConfig) GetAllEdgeOrgs(ctx context.Context) ([]AllEdgeOrgsPageContent, error) { // Contains aggregated list of BSL orgs var fullOrgsList []AllEdgeOrgsPageContent allEdgeOrgs, err := b.GetBSLOrgs(ctx) if err != nil { return nil, err } fullOrgsList = append(fullOrgsList, allEdgeOrgs.PageContent...) var envFilteredList []AllEdgeOrgsPageContent for _, org := range fullOrgsList { if strings.HasPrefix(org.OrganizationName, b.OrgPrefix) && (org.Parent || strings.Count(org.FullyQualifiedName, "/") < 4) { envFilteredList = append(envFilteredList, org) } } return envFilteredList, nil } func (b *BslConfig) GetBSLOrgs(ctx context.Context) (*AllEdgeOrgs, error) { client, err := b.GetBSLClient(ctx, "edge") if err != nil { return nil, err } allEdgeOrgs := &AllEdgeOrgs{} if err := client.JSON(http.MethodGet, getEdgeOrgs, allEdgeOrgs); err != nil { return nil, err } return allEdgeOrgs, nil } // CreateEdgeOrgGroups creates edge org groups. func (b *BslConfig) CreateEdgeOrgGroups(ctx context.Context, organizationName string) error { client, err := b.GetBSLClient(ctx, organizationName) if err != nil { return err } for key := range RoleMap { g := EdgeOrgGroup{ GroupName: string(key), GroupDescription: fmt.Sprintf("%s edge user group", key), } if err := client.SetPayload(g).Post(createBslGroupPath); err != nil { var e *bslerror.Error if errors.As(err, &e) && !e.Is(ErrorResourceAlreadyExists) { return err } } } return nil } // Check any of the edge role user group(s) contains role(s) // that is not defined in RoleMap (defined above), if un-matched then // Revoke role(s) from the respective user group to match RoleMap func (b *BslConfig) CleanUpGroupRoles(ctx context.Context, organizationName string) error { for userGroupName, fullRolesList := range RoleMap { // Get list of roles for respective user group mappedRoles := reflect.ValueOf(fullRolesList) mappedRolesLen := mappedRoles.Len() if mappedRolesLen > 0 { // Get list of roles assigned to user group from BSL groupRolesList, err := b.GetGroupRoles(ctx, userGroupName.String(), organizationName) if err != nil { return err } // Get list of Roles to remove from the respective user group rolesToRevoke := getRolesToRevoke(mappedRoles, groupRolesList) if len(rolesToRevoke) > 0 { err := b.RevokeRolesFromGroup(ctx, organizationName, userGroupName, rolesToRevoke) if err != nil { return err } } } } return nil } // userGroupRolesList: Roles currently assigned in BSL for a user group // mappedRolesForUserGroup: Desired role(s) for a user group // Compiles a list of role(s) to remove that is assigned to current user group // However, is not present in respective user group list of roles // defined in RoleMap above func getRolesToRevoke(mappedRolesForUserGroup reflect.Value, userGroupRolesList []BSLRole) []string { rolesToRevoke := make([]string, 0) for _, grantedGroupRole := range userGroupRolesList { // if granted role in BSL is not in RoleMap for user group // add to roles list to be removed if !hasRole(grantedGroupRole.Name, mappedRolesForUserGroup) { rolesToRevoke = append(rolesToRevoke, grantedGroupRole.Name) } } return rolesToRevoke } // mappedRolesForUserGroup: list of roles for a user group defined in RoleMap above // grantedRole: BSL Role assigned to a user group currently in BSL // Check if the role assigned to a user group in BSL is present in RoleMap for respective UserGroup func hasRole(grantedRole string, mappedRolesForUserGroup reflect.Value) bool { roleFound := false for i := 0; i < mappedRolesForUserGroup.Len(); i++ { roleNameInMap := mappedRolesForUserGroup.Index(i).String() if roleNameInMap == grantedRole { return true } } return roleFound } func (b *BslConfig) GetGroupRoles(ctx context.Context, userGroupName, organizationName string) ([]BSLRole, error) { client, err := b.GetBSLClient(ctx, organizationName) if err != nil { return nil, err } groupList := &GetGroupRolesResponse{} getUserGroupRolesURL := fmt.Sprintf(getUserGroupGrantedRoles, userGroupName) if err := client.JSON(http.MethodGet, getUserGroupRolesURL, groupList); err != nil { return nil, err } return groupList.Content, nil } // Revoke role(s) from a user group func (b *BslConfig) RevokeRolesFromGroup(ctx context.Context, organizationName string, userGroupName model.Role, roles []string) error { log := logger.NewLogger() rolesToRevoke := createBSLRole(userGroupName, roles) client, err := b.GetBSLClient(ctx, organizationName) if err != nil { return err } //Remove roles from userGroup specified if err := client.SetPayload(rolesToRevoke).Post(revokeBslRoleFromGroupPath); err != nil { log.Error(err, "Error removing roles from BSL user group", rolesToRevoke) return err } log.Info("Removed Roles from User Group", rolesToRevoke) return nil } // AssignRolesToGroups assign roles to organizations func (b *BslConfig) AssignRolesToGroups(ctx context.Context, organizationName string) error { client, err := b.GetBSLClient(ctx, organizationName) if err != nil { return err } for k, v := range RoleMap { assignRolesToGroupPayload := createBSLRole(k, v) hasRoleMapping := len(assignRolesToGroupPayload.Roles) > 0 if hasRoleMapping { if err := client.SetPayload(assignRolesToGroupPayload).Post(grantBslRolePath); err != nil { return err } } } return nil } func createBSLRole(k model.Role, v interface{}) *BSLRoles { r := &BSLRoles{ Groups: []EdgeOrgGroup{ { GroupName: string(k), }, }, Roles: []BSLRole{}, } rv := reflect.ValueOf(v) for i := 0; i < rv.Len(); i++ { r.Roles = append(r.Roles, BSLRole{Name: rv.Index(i).String()}) } return r } // CreateBSLUser creates a new bff user in an organization. func (b *BslConfig) CreateBSLUser(ctx context.Context, organizationName, BFFUsername string) (string, error) { client, err := b.GetBSLClient(ctx, organizationName) if err != nil { return "", err } if err := client.SetPayload(newBFFUserRequest(BFFUsername)).Post(createBslUserPath); err != nil { return "", err } password, err := generateBFFUserPassword() if err != nil { return "", err } return password, b.CreateBSLUserPassword(ctx, organizationName, BFFUsername, password) } func (b *BslConfig) CreateBSLUserAccessKey(ctx context.Context, organization, username string) (*AccessKeyResponse, error) { client, err := b.GetBSLClient(ctx, organization) if err != nil { return nil, err } res := &AccessKeyResponse{} return res, client.SetPayload(newAccessKeyRequest(organization, username)).JSON(http.MethodPost, createUserAccessKeyPath, res) } // AssignBSLUserToGroup assigns the bff user to a group. func (b *BslConfig) AssignBSLUserToGroup(ctx context.Context, organizationName, groupName, username string) error { client, err := b.GetBSLClient(ctx, organizationName) if err != nil { return err } return client.SetPayload(newEdgeOrgGroupMembership(groupName, username)).Post(grantRoleToRootUser) } // CreateBSLUserPassword creates a new bff user password by resetting it. func (b *BslConfig) CreateBSLUserPassword(ctx context.Context, organization, username, password string) error { client, err := b.GetBSLClient(ctx, organization) if err != nil { return err } return client.SetPayload(newBFFUserPasswordResetRequest(username, password)).Put(resetBslUserPasswordPath) } // CreateEnterpriseUnitType creates a new enterprise unit type in BSL. func (b *BslConfig) CreateEnterpriseUnitType(ctx context.Context, organization, name, description string) error { client, err := b.GetBSLClient(ctx, organization) if err != nil { return err } return client.SetPayload(newEnterpriseTypeRequest(name, description)).Post(createEnterpriseUnitType) } // newBFFUserRequest creates a new user account request. func newBFFUserRequest(username string) *BFFUserRequest { return &BFFUserRequest{ Username: username, Email: "bffuser@ncr.com", FullName: "Edge BFF", GivenName: "BFF", FamilyName: "Edge", TelephoneNumber: "000-000-0000", Status: "ACTIVE", Address: &BFFUserAddress{ City: "Atlanta", Country: "USA", PostalCode: "30303", State: "GA", Street: "Spring St", }, } } // newEnterpriseTypeRequest creates a new enterprise type request. func newEnterpriseTypeRequest(name, description string) *EnterpriseType { return &EnterpriseType{ Name: name, Description: description, } } // newEdgeOrgGroupMembership creates a new edge org group membership. func newEdgeOrgGroupMembership(groupName string, username string) *EdgeOrgGroupMembership { return &EdgeOrgGroupMembership{ GroupName: groupName, Members: []EdgeOrgGroupMember{ { Username: username, }, }, } } // newBFFUserPasswordResetRequest startManager a new password reset request. func newBFFUserPasswordResetRequest(username, password string) *BFFUserPasswordReset { return &BFFUserPasswordReset{ Username: username, Password: password, } } func newAccessKeyRequest(organization, username string) *AccessKeyRequest { return &AccessKeyRequest{UserID: UserID{Username: bsl.CreateFullAccountName(&types.AuthUser{Organization: organization, Username: username})}} } // OrgNameToK8sName converts an org name to a kubernetes compatible spec.Name value // Example /customers/mock-customer-edge-alpha/ is converted to mock-customer-edge-alpha func OrgNameToK8sName(name string) string { name = strings.ToLower(name) res := strings.Trim(name, "//") //nolint rootOrgSepIndex := strings.Index(res, "/") res = strings.ReplaceAll(res, " ", "-") res = strings.ReplaceAll(res, "/", "-") return res[rootOrgSepIndex+1:] } // generateBFFUserPassword generates a new unique password. func generateBFFUserPassword() (string, error) { pass, err := password.Generate(20, 10, 0, false, true) if err != nil { return "", err } allowedSpecialChars := []string{"!", "@", "#", "$", "*", "&", "(", ")"} randomSpecialChar := allowedSpecialChars[rand.Intn((len(allowedSpecialChars)))] //nolint gosec pass = fmt.Sprintf("%s%s", pass, randomSpecialChar) if !checkPasswordRequirements(pass) { pass, _ = generateBFFUserPassword() } return pass, err } // checkPasswordRequirements ensures that the generated password adheres // to the BSL password requirements which are listed below. // Requirements: Atleast One Uppercase Letter, One Lowercase Letter, // One Digit, One non-alphanumeric character func checkPasswordRequirements(password string) bool { var uppercase, lowercase, digit = false, false, false for _, value := range password { if unicode.IsUpper(value) { uppercase = true } else if unicode.IsLower(value) { lowercase = true } else if unicode.IsDigit(value) { digit = true } } return uppercase && lowercase && digit } // newBSLUsername creates a new bff username for BSL with a random string suffix func newBSLUsername(length int, suffix bool) string { letterBytes := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" b := make([]byte, length) for i := range b { b[i] = letterBytes[rand.Intn(len(letterBytes))] //nolint } if suffix { return fmt.Sprintf("%s-%s", strings.ToLower(string(b)), BFFUsername) } return strings.ToLower(string(b)) } // handleHTTPStatusError convert HTTP status code to errors. func handleHTTPStatusError(status int) error { switch status { case http.StatusOK: return nil case http.StatusUnauthorized: return ErrorInvalidCredentials case http.StatusForbidden: return ErrorInsufficientPrivileges case http.StatusBadRequest: return ErrorFailedValidation case http.StatusConflict: return ErrorResourceAlreadyExists case http.StatusNoContent: return nil default: return ErrorGeneric } }