github.com/DelineaXPM/dsv-cli@v1.40.6/cicd-integration/integration_test.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"flag"
     6  	"fmt"
     7  	"log"
     8  	"os"
     9  	"path"
    10  	"path/filepath"
    11  	"regexp"
    12  	"runtime"
    13  	"strconv"
    14  	"strings"
    15  	"testing"
    16  	"text/template"
    17  
    18  	"github.com/DelineaXPM/dsv-cli/constants"
    19  	"github.com/DelineaXPM/dsv-cli/utils/test_helpers"
    20  
    21  	"github.com/gobuffalo/uuid"
    22  	"golang.org/x/sys/execabs"
    23  )
    24  
    25  var update = flag.Bool("update", false, "update golden case files")
    26  
    27  var binaryName = constants.CmdRoot
    28  
    29  const configPath = "cicd-integration/.thy.yml"
    30  
    31  func TestCliArgs(t *testing.T) {
    32  	workDir, err := os.Getwd()
    33  	if err != nil {
    34  		t.Fatalf("[TestCliArgs] Unable to determine working directory: %v.", err)
    35  	}
    36  	binary := path.Join(workDir, binaryName+".test")
    37  
    38  	t.Logf("[TestCliArgs] Working directory: %s", workDir)
    39  	t.Logf("[TestCliArgs] Path to binary: %s", binary)
    40  
    41  	err = os.Mkdir("coverage", os.ModeDir)
    42  	targetArtifactDirectory := filepath.Join(".artifacts", "coverage", "integration")
    43  	if _, err := os.Stat(targetArtifactDirectory); os.IsNotExist(err) {
    44  		err = os.MkdirAll(targetArtifactDirectory, 0o755)
    45  		if err != nil {
    46  			t.Fatalf("unable to create code coverage directory %s: %v", targetArtifactDirectory, err)
    47  		}
    48  	}
    49  	t.Logf("[TestCliArgs] Coverage Results: %s", targetArtifactDirectory)
    50  
    51  	// if the error is not nil AND it's not an already exists error
    52  	if err != nil && !os.IsExist(err) {
    53  		t.Fatalf("[TestCliArgs] os.Mkdir(coverage, os.ModeDir): %v", err)
    54  	}
    55  	t.Log("[TestCliArgs] Successfully created the directory for coverage reports.")
    56  
    57  	for _, tt := range synchronousCases {
    58  		t.Run(tt.name, func(t *testing.T) {
    59  			outfile := filepath.Join(targetArtifactDirectory, tt.name+"coverage.out")
    60  
    61  			args := []string{"-test.coverprofile", outfile}
    62  			args = append(args, tt.args...)
    63  			args = append(args, "--config", configPath)
    64  
    65  			cmd := execabs.Command(binary, args...)
    66  			output, _ := cmd.CombinedOutput()
    67  
    68  			actual := string(output)
    69  			if strings.LastIndex(actual, "PASS") > -1 {
    70  				actual = actual[:strings.LastIndex(actual, "PASS")]
    71  			}
    72  			if strings.LastIndex(actual, "FAIL") > -1 {
    73  				actual = actual[:strings.LastIndex(actual, "FAIL")]
    74  			}
    75  			actualTrimmed := strings.TrimSpace(actual)
    76  
    77  			if *update {
    78  				if tt.output.MatchGoldenCase {
    79  					writeFixture(t, tt.name, []byte(actualTrimmed))
    80  				}
    81  				return
    82  			}
    83  
    84  			expected := tt.output.RegexMatch
    85  			if tt.output.MatchGoldenCase {
    86  				expected = loadFixture(t, tt.name)
    87  				expected = regexp.QuoteMeta(expected)
    88  				expected = "^" + expected + "$"
    89  			}
    90  
    91  			matcher := regexp.MustCompile(expected)
    92  			match := matcher.MatchString(actualTrimmed)
    93  			if !match {
    94  				t.Fatalf("actual:\n%s,\n expected:\n%s", actualTrimmed, expected)
    95  			}
    96  		})
    97  	}
    98  }
    99  
   100  var (
   101  	certPath       = strings.Join([]string{"cicd-integration", "data", "cert.pem"}, string(filepath.Separator))
   102  	privateKeyPath = strings.Join([]string{"cicd-integration", "data", "key.pem"}, string(filepath.Separator))
   103  	csrPath        = strings.Join([]string{"cicd-integration", "data", "csr.pem"}, string(filepath.Separator))
   104  )
   105  
   106  const (
   107  	manualKeyPath    = "thekey:first"
   108  	manualPrivateKey = "MnI1dTh4L0E/RChHK0tiUGVTaFZtWXEzczZ2OXkkQiY="
   109  	manualKeyNonce   = "S1NzeHdFcHB6b1Bz"
   110  	plaintext        = "hello there"
   111  	ciphertext       = "8Tns2mbY/w6YHoICfiDGQM+rDlQzwrZWpqK7"
   112  )
   113  
   114  func TestMain(m *testing.M) {
   115  	_, err := strconv.ParseBool(os.Getenv("GO_INTEGRATION_TEST"))
   116  	if err != nil {
   117  		fmt.Println("[SKIPPED]: GO_INTEGRATION_TEST must be set to 1/true to run integration tests")
   118  		return
   119  	}
   120  
   121  	var rootDir string
   122  	if out, err := execabs.Command("git", "rev-parse", "--show-toplevel").CombinedOutput(); err == nil {
   123  		rootDir = strings.TrimRight(string(out), " \n")
   124  	} else {
   125  		rootDir = "../"
   126  	}
   127  
   128  	if err := os.Chdir(rootDir); err != nil {
   129  		log.Fatal(err)
   130  	}
   131  
   132  	if err := test_helpers.AddEncryptionKey(os.Getenv("TEST_TENANT"), os.Getenv("USER_NAME"), os.Getenv("DSV_USER_PASSWORD")); err != nil {
   133  		log.Fatalf("could not create encryption key: %v", err)
   134  	}
   135  	makeCmd := execabs.Command("make", "build-test")
   136  	if err := makeCmd.Run(); err != nil {
   137  		log.Fatalf("could not make binary for %s: %v", binaryName, err)
   138  	}
   139  
   140  	cert, key, err := generateRootWithPrivateKey()
   141  	csr, err := generateCSR()
   142  	os.WriteFile(certPath, cert, 0o644)
   143  	os.WriteFile(privateKeyPath, key, 0o644)
   144  	os.WriteFile(csrPath, csr, 0o644)
   145  
   146  	defer os.Remove(certPath)
   147  	defer os.Remove(privateKeyPath)
   148  	defer os.Remove(csrPath)
   149  
   150  	// Before and after *all* tests, make sure any modifications to the config are reverted.
   151  	// Reading and writing the config before and after *each* test is not feasible, as there may be tests that
   152  	// intentionally modify the config to test for presence or absence of a property or modification of a value.
   153  	config, err := os.ReadFile(configPath)
   154  	if err != nil {
   155  		fmt.Printf("could not read config: %v", err)
   156  		os.Exit(1)
   157  	}
   158  	_ = os.Setenv("IS_SYSTEM_TEST", "true")
   159  	m.Run()
   160  	_ = os.Unsetenv("IS_SYSTEM_TEST")
   161  
   162  	err = os.WriteFile(configPath, config, 0o644)
   163  	if err != nil {
   164  		fmt.Printf("could not write config: %v", err)
   165  		os.Exit(1)
   166  	}
   167  }
   168  
   169  type outputValidation struct {
   170  	RegexMatch      string
   171  	MatchGoldenCase bool
   172  }
   173  
   174  func outputPattern(regex string) outputValidation {
   175  	return outputValidation{
   176  		RegexMatch: regex,
   177  	}
   178  }
   179  
   180  func outputEmpty() outputValidation {
   181  	return outputValidation{
   182  		RegexMatch: "^$",
   183  	}
   184  }
   185  
   186  func outputGolden() outputValidation {
   187  	return outputValidation{
   188  		MatchGoldenCase: true,
   189  	}
   190  }
   191  
   192  //nolint:gochecknoglobals // Yup, we know.
   193  var synchronousCases []struct {
   194  	name   string
   195  	args   []string
   196  	output outputValidation
   197  }
   198  
   199  func init() {
   200  	if err := generateThyYml(".thy.yml.template", ".thy.yml"); err != nil {
   201  		panic(err)
   202  	}
   203  
   204  	if err := generateThyYml("data/policy.json", "data/test_policy.json"); err != nil {
   205  		panic(err)
   206  	}
   207  
   208  	u, _ := uuid.NewV4()
   209  	t, _ := uuid.NewV4()
   210  
   211  	adminPass := os.Getenv("DSV_ADMIN_PASS")
   212  
   213  	secret1Name := u.String() + "z" // Avoid UUID detection on the API side.
   214  	secret1Tag := t.String()
   215  	//nolint:gosec // Not a hardcoded credentials.
   216  	secret1Desc := `desc of s1`
   217  	secret1Data := `{"field":"secret password"}`
   218  	secret1Attributes := fmt.Sprintf(`{"tag":"%s", "tll": 100}`, secret1Tag)
   219  	secret1DataFmt := `"field": "secret password"`
   220  
   221  	user1 := os.Getenv("USER1_NAME")
   222  	user1Pass := os.Getenv("DSV_USER1_PASSWORD")
   223  	groupName := u.String()
   224  
   225  	policyName := "secrets:" + secret1Name
   226  	p2, _ := uuid.NewV4()
   227  	policy2Name := "secrets:servers:" + p2.String()
   228  	policy2File := strings.Join([]string{"cicd-integration", "data", "test_policy.json"}, string(filepath.Separator))
   229  
   230  	existingRootSecret := "existingRoot"
   231  	certStoreSecret := "myroot"
   232  	leafSecretPath := "myleaf"
   233  
   234  	synchronousCases = []struct {
   235  		name   string
   236  		args   []string
   237  		output outputValidation
   238  	}{
   239  		// secret operations
   240  		// TODO investigate test setup, as the order of calls matters for some reason.
   241  		{"secret-create-1-pass", []string{"secret", "create", "--path", secret1Name, "--data", secret1Data, "--attributes", secret1Attributes, "--desc", secret1Desc, "-f", ".data", "-v"}, outputPattern(secret1DataFmt)},
   242  		{"secret-update-pass", []string{"secret", "update", "--path", secret1Name, "--desc", "updated secret", "-f", ".data", "-v"}, outputPattern(secret1DataFmt)},
   243  		{"secret-rollback-pass", []string{"secret", "rollback", "--path", secret1Name, "-f", ".data"}, outputPattern(secret1DataFmt)},
   244  		// {"secret-search-find-pass", []string{"secret", "search", secret1Name[:3], "'data.[0].name'"}, outputPattern(secret1Name)},
   245  		{"secret-search-tags", []string{"secret", "search", secret1Tag, "--search-field", "attributes.tag"}, outputPattern(secret1Name)},
   246  		{"secret-create-fail-dup", []string{"secret", "create", "--path", secret1Name, "--data", secret1Data, "", ".message"}, outputPattern(`"message": "error creating secret, secret at path already exists"`)},
   247  		{"secret-describe-1-pass", []string{"secret", "describe", "--path", secret1Name, "-f", ".description"}, outputPattern("^" + secret1Desc + "$")},
   248  		{"secret-read-1-pass", []string{"secret", "read", "--path", secret1Name, "-f", ".data"}, outputPattern(secret1DataFmt)},
   249  		{"secret-read-implicit-pass", []string{"secret", secret1Name, "-f", ".data"}, outputPattern(secret1DataFmt)},
   250  		{"secret-search-none-pass", []string{"secret", "search", "hjkl"}, outputPattern(`"data": \[\]`)},
   251  		{"secret-soft-delete", []string{"secret", "delete", secret1Name}, outputPattern("will be removed")},
   252  		{"secret-read-fail", []string{"secret", "read", secret1Name}, outputPattern("will be removed")},
   253  		{"secret-restore", []string{"secret", "restore", secret1Name}, outputEmpty()},
   254  
   255  		// policy operations
   256  		{"policy-help", []string{"policy", ""}, outputPattern(`Execute an action on a policy.*`)},
   257  		{"policy-create-pass", []string{"policy", "create", "--path", policyName, "--resources", policyName, "--actions", "read", "--subjects", "users:" + user1}, outputPattern(fmt.Sprintf(`"path":\s*"%s"`, policyName))},
   258  		{"policy-create-file-pass", []string{"policy", "create", "--path", policy2Name, "--data", "@" + policy2File}, outputPattern(fmt.Sprintf(`"path":\s*"%s"`, policy2Name))},
   259  		{"policy-read-pass", []string{"policy", "read", "--path", policyName}, outputPattern(fmt.Sprintf(`"path":\s*"%s"`, policyName))},
   260  		{"policy-search-pass", []string{"policy", "search", "--query", policyName}, outputPattern(fmt.Sprintf(`"path":\s*"%s"`, policyName))},
   261  		{"policy-update-pass", []string{"policy", "update", "--path", policyName, "--resources", policyName, "--actions", "read,delete", "--subjects", "users:" + user1}, outputPattern(`"delete"`)},
   262  		{"policy-rollback-pass", []string{"policy", "rollback", "--path", policyName}, outputPattern(fmt.Sprintf(`"path":\s*"%s"`, policyName))},
   263  		{"policy-soft-delete", []string{"policy", "delete", policyName}, outputPattern("will be removed")},
   264  		{"policy-read-fail", []string{"policy", "read", policyName}, outputPattern("will be removed")},
   265  		{"policy-restore", []string{"policy", "restore", policyName}, outputEmpty()},
   266  
   267  		// user operations
   268  		{"user-create-pass", []string{"user", "create", "--username", user1, "--password", user1Pass}, outputPattern(`"userName": "mrmittens"`)},
   269  
   270  		// group operations
   271  		{"group-help", []string{"group", ""}, outputPattern(`Execute an action on a group.*`)},
   272  		{"group-create-pass", []string{"group", "create", "--group-name", groupName, "--members", user1}, outputPattern(`.*` + "\"errors\": {}" + `.*`)},
   273  		{"group-read-pass", []string{"group", "read", groupName}, outputPattern(groupName)},
   274  		{"group-delete-member-pass", []string{"group", "delete-members", "--group-name", groupName, "--members", user1}, outputEmpty()},
   275  		{"group-read-pass", []string{"group", "read", groupName}, outputPattern(groupName)},
   276  		{"group-soft-delete", []string{"group", "delete", groupName}, outputPattern("will be removed")},
   277  		{"group-read-fail", []string{"group", "read", groupName}, outputPattern("will be removed")},
   278  		{"group-restore", []string{"group", "restore", groupName}, outputEmpty()},
   279  
   280  		// delegated access operations
   281  		{"user-auth-pass", []string{"auth", "-u", user1, "-p", user1Pass}, outputPattern(`"accessToken":\s*"[^"]+",\s*"expiresIn"`)},
   282  		{"user-auth-pass-failed", []string{"auth", "-u", user1, "-p", "user1fail"}, outputPattern(`{"code":401,"message":"unable to authenticate"}`)},
   283  		{"user-access-pass", []string{"secret", "read", secret1Name, "-u", user1, "-p", user1Pass}, outputPattern(secret1DataFmt)},
   284  		{"user-access-fail-action", []string{"secret", "update", secret1Name, "-u", user1, "-p", user1Pass, "-d", `{"field":"updated secret 1"}`}, outputPattern("Invalid permissions")},
   285  		{"user-access-fail-resource", []string{"secret", "read", "secret-idonthavepermissionon", "-u", user1, "-p", user1Pass, "-f", ".data"}, outputPattern("Invalid permissions")},
   286  
   287  		// cli-config operations
   288  		{"cli-config-help", []string{"cli-config", ""}, outputPattern(`Execute an action on the cli config.*`)},
   289  		{"cli-config-read-pass", []string{"cli-config", "read"}, outputGolden()},
   290  		// Force update to the config with the same correct password.
   291  		{"cli-config-update-pass", []string{"cli-config", "update", "auth.password", adminPass}, outputEmpty()},
   292  
   293  		// Make sure config now has a `securePassword` key.
   294  		{"cli-config-read-2-pass", []string{"cli-config", "read"}, outputPattern(`securePassword`)},
   295  
   296  		// Config will not be written, if auth fails upon password update.
   297  		{"cli-config-update-fail", []string{"cli-config", "update", "auth.password", "wrong-password"}, outputPattern(`Please check your credentials and try again.`)},
   298  
   299  		{"token-clear-pass", []string{"auth", "clear"}, outputEmpty()},
   300  		{"user-auth-success", []string{"auth"}, outputPattern(`accessToken`)},
   301  
   302  		{"cli-config-add-key", []string{"cli-config", "update", "key", "value"}, outputEmpty()},
   303  		{"cli-config-remove-key", []string{"cli-config", "update", "key", "0"}, outputEmpty()},
   304  
   305  		{"cli-config-update-2-pass", []string{"cli-config", "update", "auth.password", adminPass}, outputEmpty()},
   306  
   307  		// config operations
   308  		{"config-help", []string{"config", "--help"}, outputPattern(`Execute an action on the.*`)},
   309  		{"config-get-implicit-pass", []string{"config"}, outputPattern(`"permissionDocument":`)},
   310  		{"config-get-pass", []string{"config", "read"}, outputPattern(`"permissionDocument":`)},
   311  
   312  		// EaaS-Manual
   313  		{"crypto-manual-key-upload", []string{"crypto", "manual", "key-upload", "--path", manualKeyPath, "--private-key", manualPrivateKey, "--nonce", manualKeyNonce, "--scheme", "symmetric"}, outputPattern(`"version": "0"`)},
   314  		{"crypto-manual-key-read", []string{"crypto", "manual", "key-read", "--path", manualKeyPath}, outputPattern(`"version": "0"`)},
   315  		{"crypto-manual-encrypt", []string{"crypto", "manual", "encrypt", "--path", manualKeyPath, "--data", plaintext}, outputPattern(`"version": "0"`)},
   316  		{"crypto-manual-decrypt", []string{"crypto", "manual", "decrypt", "--path", manualKeyPath, "--data", ciphertext}, outputPattern(`"data": "hello there"`)},
   317  		{"crypto-manual-key-update", []string{"crypto", "manual", "key-update", "--path", manualKeyPath, "--private-key", manualPrivateKey}, outputPattern(`"version": "1"`)},
   318  
   319  		// PKI
   320  		{
   321  			"register-root-cert",
   322  			[]string{
   323  				"pki", "register", "--rootcapath", existingRootSecret,
   324  				"--certpath", "@" + certPath, "--privkeypath", "@" + privateKeyPath, "--domains", leafCommonName, "--maxttl", "250h",
   325  			},
   326  			outputPattern("certificate"),
   327  		},
   328  
   329  		{
   330  			"sign-with-root-cert",
   331  			[]string{
   332  				"pki", "sign", "--rootcapath", existingRootSecret,
   333  				"--csrpath", "@" + csrPath, "--ttl", "100H",
   334  			},
   335  			outputPattern("certificate"),
   336  		},
   337  
   338  		{
   339  			"generate-root-cert",
   340  			[]string{
   341  				"pki", "generate-root", "--rootcapath", certStoreSecret,
   342  				"--domains", leafCommonName, "--common-name", "thycotic.com", "--maxttl", "60d",
   343  			},
   344  			outputPattern("certificate"),
   345  		},
   346  
   347  		{
   348  			"generate-leaf-cert",
   349  			[]string{
   350  				"pki", "leaf", "--rootcapath", certStoreSecret,
   351  				"--common-name", leafCommonName, "--ttl", "5D", "--store-path", leafSecretPath,
   352  			},
   353  			outputPattern("certificate"),
   354  		},
   355  
   356  		{
   357  			"generate-ssh-cert",
   358  			[]string{
   359  				"pki", "ssh-cert", "--rootcapath", certStoreSecret, "--leafcapath",
   360  				leafSecretPath, "--principals", "root,ubuntu", "--ttl", "52w",
   361  			},
   362  			outputPattern("sshCertificate"),
   363  		},
   364  
   365  		{"user-update-pass", []string{"user", "update", "--username", user1, "--password", "New_password@2"}, outputPattern(`"userName": "mrmittens"`)},
   366  
   367  		// cleanup
   368  		{"secret-delete-1-pass", []string{"secret", "delete", secret1Name, "--force"}, outputEmpty()},
   369  		{"user-delete", []string{"user", "delete", user1, "--force"}, outputEmpty()},
   370  		{"policy-delete", []string{"policy", "delete", "--path", policyName, "--force"}, outputEmpty()},
   371  		{"policy2-delete", []string{"policy", "delete", "--path", policy2Name, "--force"}, outputEmpty()},
   372  		{"cert-secret-delete", []string{"secret", "delete", "--path", certStoreSecret, "--force"}, outputEmpty()},
   373  		{"rootCA-secret-delete", []string{"secret", "delete", "--path", existingRootSecret, "--force"}, outputEmpty()},
   374  		{"leafCA-secret-delete", []string{"secret", "delete", "--path", leafSecretPath, "--force"}, outputEmpty()},
   375  		{"crypto-manual-key-delete", []string{"crypto", "manual", "key-delete", "--path", manualKeyPath, "--force"}, outputEmpty()},
   376  	}
   377  }
   378  
   379  func fixturePath(t *testing.T, fixture string) string {
   380  	_, filename, _, ok := runtime.Caller(0)
   381  	if !ok {
   382  		t.Fatalf("problems recovering caller information")
   383  	}
   384  	return filepath.Join(filepath.Dir(filename), "cases", fixture)
   385  }
   386  
   387  func writeFixture(t *testing.T, fixture string, content []byte) {
   388  	err := os.WriteFile(fixturePath(t, fixture), content, 0o644)
   389  	if err != nil {
   390  		t.Fatal(err)
   391  	}
   392  }
   393  
   394  func loadFixture(t *testing.T, fixture string) string {
   395  	tmpl, err := template.ParseFiles(fixturePath(t, fixture))
   396  	if err != nil {
   397  		t.Fatal(err)
   398  	}
   399  	var tmplBytes bytes.Buffer
   400  	err = tmpl.Execute(&tmplBytes, envToMap())
   401  	if err != nil {
   402  		t.Fatal(err)
   403  	}
   404  	return tmplBytes.String()
   405  }
   406  
   407  func generateThyYml(inPath, outPath string) error {
   408  	t, err := template.ParseFiles(inPath)
   409  	if err != nil {
   410  		return err
   411  	}
   412  
   413  	outFile, err := os.Create(outPath)
   414  	if err != nil {
   415  		return err
   416  	}
   417  	defer outFile.Close()
   418  
   419  	return t.Execute(outFile, envToMap())
   420  }
   421  
   422  func envToMap() map[string]string {
   423  	evpMap := map[string]string{}
   424  
   425  	for _, v := range os.Environ() {
   426  		split := strings.Split(v, "=")
   427  		if len(split) == 2 {
   428  			evpMap[split[0]] = split[1]
   429  		}
   430  	}
   431  	return evpMap
   432  }