mirror of
https://github.com/LukeHagar/sailpoint-cli.git
synced 2025-12-06 04:21:15 +00:00
379 lines
9.2 KiB
Go
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
|
|
}
|