Files
sailpoint-cli/cmd/connector/validate/util.go
2023-11-06 09:54:59 -06:00

379 lines
9.2 KiB
Go

package connvalidate
import (
"fmt"
"log"
"math/rand"
"regexp"
"sort"
"strconv"
"strings"
connclient "github.com/sailpoint-oss/sailpoint-cli/cmd/connector/client"
)
// entitlementAttr returns the attribute for entitlements
func entitlementAttr(spec *connclient.ConnSpec) string {
for _, attr := range spec.AccountSchema.Attributes {
if attr.Entitlement {
return attr.Name
}
}
return ""
}
// accountEntitlements returns all entitlements on the account
func accountEntitlements(account *connclient.Account, spec *connclient.ConnSpec) ([]string, error) {
entitlementAttr := entitlementAttr(spec)
if entitlementAttr == "" {
return nil, fmt.Errorf("no entitlement attr found")
}
entitlements := []string{}
for _, identity := range account.Attributes[entitlementAttr].([]interface{}) {
entitlements = append(entitlements, identity.(string))
}
return entitlements, nil
}
// accountHasEntitlement returns whether or not an account has a specific entitlement
func accountHasEntitlement(account *connclient.Account, spec *connclient.ConnSpec, entitlementID string) bool {
entitlements, err := accountEntitlements(account, spec)
if err != nil {
panic(err.Error())
}
for _, id := range entitlements {
if id == entitlementID {
return true
}
}
return false
}
// Diff is a difference between two values
type diff struct {
Field string
A interface{}
B interface{}
}
// compareIntersection compares two objects and returns any differences between
// the fields that are common to both objects.
func compareIntersection(a map[string]interface{}, b map[string]interface{}) (diffs []diff) {
for key := range a {
if _, found := b[key]; !found {
continue
}
switch v := b[key].(type) {
case []interface{}:
var sliceB []string
for _, val := range v {
sliceB = append(sliceB, val.(string))
}
sliceA, ok := a[key].([]string)
if !ok {
log.Println("failed to convert to sliceA to slice of strings")
}
for i := range sliceA {
if sliceA[i] != sliceB[i] {
diffs = append(diffs, diff{
Field: key,
A: a[key],
B: b[key],
})
}
}
default:
if a[key] != b[key] {
diffs = append(diffs, diff{
Field: key,
A: a[key],
B: b[key],
})
}
}
}
return diffs
}
const (
fieldTypeStatic = "static"
fieldTypeGenerator = "generator"
generatorPassword = "Create Password"
generatorAccountId = "Create Unique Account ID"
)
// genCreateField generates a value for the provided account create template field
func genCreateField(field connclient.AccountCreateTemplateField) interface{} {
// Return typed based value if the field is in deprecated format
// TODO: Once we move away from the old format, this should also be removed
if field.Key == "" {
return genValueByTypeAndName(field)
}
// Return default value if field is set to static
if field.InitialValue.Type == fieldTypeStatic {
return field.InitialValue.Attributes.Value
}
// Build value for generator field
if field.InitialValue.Type == fieldTypeGenerator {
if field.InitialValue.Attributes.Name == generatorPassword {
return fmt.Sprintf("RandomPassword.%d", rand.Intn(65536))
}
if field.InitialValue.Attributes.Name == generatorAccountId {
template := field.InitialValue.Attributes.Template
counterRegex := regexp.MustCompile(`\$\(uniqueCounter\)`)
template = counterRegex.ReplaceAllString(template, strconv.Itoa(rand.Intn(65536)))
stringRegex := regexp.MustCompile(`\$\(.*?\)`)
template = stringRegex.ReplaceAllString(template, fmt.Sprintf("string%d", rand.Intn(99)))
return template
}
}
// For other cases including identity attributes, use the default way to generate value by type.
return genValueByTypeAndName(field)
}
// getFieldName returns the name of the field
// TODO: This is to support both key and name base field. Once the name based filds are gone, we can remove this helper method
func getFieldName(field connclient.AccountCreateTemplateField) string {
if field.Key == "" {
return field.Name
}
return field.Key
}
// genValueByTypeAndName generates attribute values base on field type and name
func genValueByTypeAndName(field connclient.AccountCreateTemplateField) interface{} {
switch field.Type {
case "string":
if getFieldName(field) == "email" || getFieldName(field) == "name" {
return fmt.Sprintf("test.%d@example.com", rand.Intn(65536))
} else if getFieldName(field) == "siteRole" {
return "Creator"
} else {
return fmt.Sprintf("string.%d", rand.Intn(65536))
}
case "boolean":
// TODO: we want to eventually remove these. These fields needs only for Smartsheet connectors
if getFieldName(field) == "admin" {
return false
}
if getFieldName(field) == "groupAdmin" {
return false
}
if getFieldName(field) == "licensedSheetCreator" {
return false
}
if getFieldName(field) == "resourceViewer" {
return false
}
return true
case "array":
// TODO: we need to avoid hardcoding any specific code in the validation suite.
// Freshservice connector only
if getFieldName(field) == "roles" {
return []string{"27000245813:entire_helpdesk"}
}
return nil
default:
panic(fmt.Sprintf("unknown type: %q", field.Type))
}
}
// testSchema verifies that value is of the expectedType
func testSchema(res *CheckResult, attrName string, value interface{}, expectedMulti bool, expectedType string) {
// Check if it's a multi value (array) and unwrap if necessary
// TODO should we check all values in the array?
isMulti := false
switch value.(type) {
case []interface{}:
if len(value.([]interface{})) > 0 {
value = value.([]interface{})[0]
} else {
value = nil
}
isMulti = true
}
if expectedMulti != isMulti {
res.errf("expected multi=%t but multi=%t", expectedMulti, isMulti)
}
switch value.(type) {
case string:
if expectedType == "int" {
_, err := strconv.Atoi(value.(string))
if err != nil {
res.errf("failed to convert int to string on field %s", attrName)
}
}
if expectedType != "string" && expectedType != "int" {
res.errf("%s expected type %q but was 'string'", attrName, expectedType)
}
case bool:
if expectedType != "boolean" {
res.errf("expected type %q but was 'boolean'", expectedType)
}
case float64:
if expectedType != "int" {
res.errf("expected type %q but was 'int'", expectedType)
}
case nil:
// If a value is nil we can't validate the type.
default:
res.errf("unknown type %T for %q", value, attrName)
}
}
// attrChange generates an attribute change event for the provided account and
// attribute.
func attrChange(acct *connclient.Account, attr *connclient.AccountSchemaAttribute) connclient.AttributeChange {
var op string
switch attr.Multi {
case true:
op = "Add"
case false:
op = "Set"
}
var newValue interface{}
switch attr.Type {
case "string":
if attr.Name == "email" {
newValue = fmt.Sprintf("test.%d@example.com", rand.Intn(65536))
} else {
newValue = fmt.Sprintf("string.%x", rand.Intn(16777216))
}
case "int":
if current, found := acct.Attributes[attr.Name]; found {
newValue = current.(int) + 1
} else {
newValue = 42
}
case "boolean":
// flip
if current, found := acct.Attributes[attr.Name]; found {
newValue = current.(bool)
} else {
newValue = true
}
}
return connclient.AttributeChange{
Op: op,
Attribute: attr.Name,
Value: newValue,
}
}
func isAvailableForUpdating(entitlements []string, entitlementID string) bool {
entID := strings.Split(entitlementID, ":")[0]
for _, ent := range entitlements {
if entID == strings.Split(ent, ":")[0] {
return false
}
}
return true
}
func getIdentity(input map[string]interface{}) string {
_, ok := input["email"]
if ok {
return input["email"].(string)
}
_, ok = input["username"]
if ok {
return input["username"].(string)
}
_, ok = input["name"]
if ok {
return input["name"].(string)
}
return fmt.Sprintf("test.%d@example.com", rand.Intn(65536))
}
func canonicalizeAttributes(attrs map[string]interface{}) {
for key, val := range attrs {
switch val.(type) {
case []interface{}:
var arrayOfStrings []string
for _, elem := range val.([]interface{}) {
arrayOfStrings = append(arrayOfStrings, fmt.Sprintf("%v", elem))
}
sort.Strings(arrayOfStrings)
attrs[key] = arrayOfStrings
case []float64:
var arrayOfFloats []float64
for _, elem := range val.([]float64) {
arrayOfFloats = append(arrayOfFloats, elem)
}
sort.Float64s(arrayOfFloats)
attrs[key] = arrayOfFloats
case []int:
var arrayOfInts []int
for _, elem := range val.([]int) {
arrayOfInts = append(arrayOfInts, elem)
}
sort.Ints(arrayOfInts)
attrs[key] = arrayOfInts
case []string:
var arrayOfStrings []string
for _, elem := range val.([]string) {
arrayOfStrings = append(arrayOfStrings, elem)
}
sort.Strings(arrayOfStrings)
attrs[key] = arrayOfStrings
}
}
}
func isCheckPossible(commands, checkCommands []string) (bool, []string) {
var result []string
commandsMap := make(map[string]bool)
for _, c := range commands {
commandsMap[c] = true
}
for _, cc := range checkCommands {
_, ok := commandsMap[cc]
if !ok {
result = append(result, cc)
}
}
if len(result) != 0 {
return false, result
}
return true, nil
}