From d35ccc4a83f3768d436e2e07fea7c62cad50c38b Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Fri, 15 Nov 2024 12:56:50 -0800 Subject: [PATCH] Revert "feat(core): adds storeFile to save encrypted profiles to disk and updates auth to propagate tlsNoVerify (#420)" This reverts commit f709e014bf3f82a2808eae5df76b3667730c36ef. --- .github/scripts/verify-checksums.sh | 72 +++----- .github/scripts/zip-builds.sh | 52 ++---- Makefile | 5 +- cmd/auth-login.go | 9 +- cmd/root.go | 6 - docs/man/_index.md | 7 - e2e/profile.bats | 5 +- go.mod | 11 +- go.sum | 30 ++-- pkg/auth/auth.go | 13 +- pkg/cli/flagValues.go | 29 +--- pkg/profiles/profile.go | 87 +++++----- pkg/profiles/profileConfig.go | 70 ++------ pkg/profiles/storeFile.go | 258 ---------------------------- pkg/utils/validators.go | 23 +-- 15 files changed, 149 insertions(+), 528 deletions(-) delete mode 100644 pkg/profiles/storeFile.go diff --git a/.github/scripts/verify-checksums.sh b/.github/scripts/verify-checksums.sh index 271144b2..8aaedac3 100755 --- a/.github/scripts/verify-checksums.sh +++ b/.github/scripts/verify-checksums.sh @@ -2,60 +2,32 @@ # Check if the required arguments are provided if [ $# -ne 2 ]; then - echo "Usage: $0 " + echo "Usage: $0 " exit 1 fi -# Assign arguments to variables -output_dir="$1" -checksum_file="$2" -checksum_path="${output_dir}/${checksum_file}" # Full path to the checksum file -lock_file="${checksum_path}.lock" # Append .lock to the full path of the checksum file - -# Ensure the checksum file exists -if [ ! -f "$checksum_path" ]; then - echo "ERROR: Checksum file $checksum_path does not exist." - exit 1 -fi - -# Wait for the lock file to be available for reading -exec 200<"$lock_file" # Open lock file descriptor for reading -flock -s 200 # Acquire shared lock (will wait if exclusive lock is held) - echo "Verifying checksums..." -echo "Looking for checksum file: $checksum_path" +# Location of the checksum file +checksumFile=$1/$2 +outputDir=$1 + +echo "Looking for checksum file: $checksumFile" +test -f "$checksumFile" || { echo "ERROR: Checksum file not found!"; exit 1; } # Iterate over each line in the checksum file while read -r line; do - # Extract checksum and filename from the line - expected_checksum=$(echo "$line" | awk '{print $1}') - filename=$(echo "$line" | awk '{print $2}') - - # Construct the full path to the file - file_path="$output_dir/$filename" - - # Check if the file exists - if [ ! -f "$file_path" ]; then - echo "ERROR: File $filename not found in $output_dir" - continue - fi - - # Calculate the actual checksum of the file - actual_checksum=$(shasum -a 256 "$file_path" | awk '{print $1}') - - # Compare the expected and actual checksums - if [ "$expected_checksum" != "$actual_checksum" ]; then - echo "ERROR: Checksum for $filename does not match." - else - echo "Checksum for $filename is correct." - fi -done < "$checksum_path" - -# Release the lock and close the lock file descriptor -flock -u 200 -exec 200>&- - -# Clean up the lock file -rm -f "$lock_file" - -echo "Checksum verification completed." + # Extract the expected checksum and filename from each line + read -ra ADDR <<< "$line" # Read the line into an array + expectedChecksum="${ADDR[0]}" + fileName="${ADDR[2]}" + + # Calculate the actual checksum of the file + actualChecksum=$(shasum -a 256 "$outputDir/$fileName" | awk '{print $1}') + + # Compare the expected checksum with the actual checksum + if [ "$expectedChecksum" == "$actualChecksum" ]; then + echo "SUCCESS: Checksum for $fileName is valid." + else + echo "ERROR: Checksum for $fileName does not match." + fi +done < "$checksumFile" \ No newline at end of file diff --git a/.github/scripts/zip-builds.sh b/.github/scripts/zip-builds.sh index 4720fc5c..0b0e0e91 100755 --- a/.github/scripts/zip-builds.sh +++ b/.github/scripts/zip-builds.sh @@ -16,41 +16,23 @@ mkdir -p "$output_dir" # Create a checksums file checksums_file="$output_dir/${build_semver}_checksums.txt" -touch "$checksums_file" - -# Define a lock file for parallel-safe writing to checksums -checksums_lockfile="${checksums_file}.lock" +touch $checksums_file # Iterate over each binary file for binary_file in "$binary_dir"/*; do - ( - compressed="" - if [[ $binary_file == *.exe ]]; then - # If the file is a Windows binary, zip it - filename=$(basename "$binary_file") - compressed="${filename%.exe}.zip" - zip -j "$output_dir/$compressed" "$binary_file" - else - # For other binaries, tar and gzip them - filename=$(basename "$binary_file") - compressed="${filename}.tar.gz" - tar -czf "$output_dir/$compressed" "$binary_file" - fi - - # Compute checksum and append it to the checksums file using a lock - checksum="$(shasum -a 256 "$output_dir/$compressed" | awk '{print $1}')" - ( - flock -x 200 - echo "$checksum $compressed" >> "$checksums_file" - ) 200>"$checksums_lockfile" - - ) & -done - -# Echo message indicating background tasks are running -echo "All zip and tar processes started. Waiting for them to finish..." - -# Wait for all background processes to complete -wait - -echo "All compression and checksum operations completed." + compressed="" + if [[ $binary_file == *.exe ]]; then + # If the file is a Windows binary, zip it + filename=$(basename "$binary_file") + compressed="${filename%.exe}.zip" + zip -j "$output_dir/$compressed" "$binary_file" + else + # For other binaries, tar and gzip them + filename=$(basename "$binary_file") + compressed="${filename}.tar.gz" + tar -czf "$output_dir/$compressed" "$binary_file" + fi + + # Append checksums to the file + echo "$(cat "$output_dir/$compressed" | shasum -a 256) $compressed" >> $checksums_file +done \ No newline at end of file diff --git a/Makefile b/Makefile index 9eb694ae..d7a511a7 100644 --- a/Makefile +++ b/Makefile @@ -58,10 +58,10 @@ build-%: go build $(GO_BUILD_FLAGS) \ -o $(GO_BUILD_PREFIX)-$(word 1,$(subst -, ,$*))-$(word 2,$(subst -, ,$*))$(word 3,$(subst -, ,$*)) -zip-builds: $(addprefix build-,$(PLATFORMS)) +zip-builds: ./.github/scripts/zip-builds.sh $(BINARY_NAME)-$(CURR_VERSION) $(TARGET_DIR) $(OUTPUT_DIR) -verify-checksums: zip-builds +verify-checksums: ./.github/scripts/verify-checksums.sh $(OUTPUT_DIR) $(BINARY_NAME)-$(CURR_VERSION)_checksums.txt # Target for running the project (adjust as necessary for your project) @@ -93,4 +93,3 @@ test-bats: build-test .PHONY: clean clean: rm -rf $(TARGET_DIR) - rm -rf $(OUTPUT_DIR) diff --git a/cmd/auth-login.go b/cmd/auth-login.go index 664fe96e..21ca8467 100644 --- a/cmd/auth-login.go +++ b/cmd/auth-login.go @@ -13,16 +13,11 @@ func auth_codeLogin(cmd *cobra.Command, args []string) { _, cp := InitProfile(c, false) c.Print("Initiating login...") - - // Use profile values as defaults, with command-line overrides - tlsNoVerify := c.FlagHelper.GetOptionalBoolWithDefault("tls-no-verify", cp.GetTLSNoVerify()) - clientId := c.FlagHelper.GetOptionalStringWithDefault("client-id", cp.GetAuthCredentials().ClientId) - tok, publicClientID, err := auth.LoginWithPKCE( cmd.Context(), cp.GetEndpoint(), - clientId, - tlsNoVerify, + c.FlagHelper.GetOptionalString("client-id"), + c.FlagHelper.GetOptionalBool("tls-no-verify"), ) if err != nil { c.Println("failed") diff --git a/cmd/root.go b/cmd/root.go index 9757242c..3ddeff90 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -279,11 +279,5 @@ func init() { rootCmd.GetDocFlag("with-access-token").Default, rootCmd.GetDocFlag("with-access-token").Description, ) - - RootCmd.PersistentFlags().String( - rootCmd.GetDocFlag("profile-driver").Name, - rootCmd.GetDocFlag("profile-driver").Default, - rootCmd.GetDocFlag("profile-driver").Description, - ) RootCmd.AddGroup(&cobra.Group{ID: TDF}) } diff --git a/docs/man/_index.md b/docs/man/_index.md index 1fd5bcee..b8705f14 100644 --- a/docs/man/_index.md +++ b/docs/man/_index.md @@ -39,11 +39,4 @@ command: - name: debug description: enable debug output default: false - - name: profile-driver - description: storage driver for managing profiles - enum: - - keyring - - in-memory - - file - default: file --- diff --git a/e2e/profile.bats b/e2e/profile.bats index 5a536a65..f0ed9cb8 100755 --- a/e2e/profile.bats +++ b/e2e/profile.bats @@ -65,7 +65,10 @@ teardown() { @test "profile create" { run_otdfctl profile create test http://localhost:8080 assert_output --regexp "Creating profile .* ok" - + + run_otdfctl profile create test localhost:8080 + assert_output --regexp "Failed .* invalid scheme" + # TODO figure out how to test the case where the profile already exists } diff --git a/go.mod b/go.mod index 2362f9b9..5607251b 100644 --- a/go.mod +++ b/go.mod @@ -15,9 +15,9 @@ require ( github.com/go-jose/go-jose/v3 v3.0.3 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.6.0 - github.com/opentdf/platform/lib/flattening v0.1.2 - github.com/opentdf/platform/protocol/go v0.2.22 - github.com/opentdf/platform/sdk v0.3.20 + github.com/opentdf/platform/lib/flattening v0.1.1 + github.com/opentdf/platform/protocol/go v0.2.20 + github.com/opentdf/platform/sdk v0.3.19 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 @@ -86,6 +86,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect @@ -108,9 +109,9 @@ require ( go.opentelemetry.io/otel/metric v1.29.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.26.0 // indirect + golang.org/x/crypto v0.25.0 // indirect golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect - golang.org/x/net v0.28.0 // indirect + golang.org/x/net v0.27.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.27.0 // indirect golang.org/x/text v0.17.0 // indirect diff --git a/go.sum b/go.sum index 4ddb2d18..0e380399 100644 --- a/go.sum +++ b/go.sum @@ -215,16 +215,22 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opentdf/platform/lib/fixtures v0.2.8 h1:lGYrMnbORtU62lxsJi8qPsxjFuNIkc4Dop8rVkH6pD0= -github.com/opentdf/platform/lib/fixtures v0.2.8/go.mod h1:8yCSe+oUzW9jbM573r9qgE68rjwDMNzktObiGVsO/W8= -github.com/opentdf/platform/lib/flattening v0.1.2 h1:7/fUlBY08PR6UItfVU2CVF5rcCxf5oZZ4MGLABj4NAU= -github.com/opentdf/platform/lib/flattening v0.1.2/go.mod h1:Gs/T+6FGZKk9OAdz2Jf1R8CTGeNRYrq1lZGDeYT3hrY= +github.com/opentdf/platform/lib/fixtures v0.2.7 h1:2LxWmLBBISONVJnVDH8yMsV72VHQyirua0DwDBBoq+g= +github.com/opentdf/platform/lib/fixtures v0.2.7/go.mod h1:8yCSe+oUzW9jbM573r9qgE68rjwDMNzktObiGVsO/W8= +github.com/opentdf/platform/lib/flattening v0.1.1 h1:la1f6PcRsc+yLH8+9UEr0ux6IRKu+6+oMaMVt05+8HU= +github.com/opentdf/platform/lib/flattening v0.1.1/go.mod h1:eyG7pe5UZlV+GI5/CymQD3xTAJxNhnP9M4QnBzaad1M= github.com/opentdf/platform/lib/ocrypto v0.1.6 h1:rd4ctCZOE/c3qDJORtkSK9tw6dEXb+jbJXRRk4LcxII= github.com/opentdf/platform/lib/ocrypto v0.1.6/go.mod h1:ne+l8Q922OdzA0xesK3XJmfECBnn5vLSGYU3/3OhiHM= -github.com/opentdf/platform/protocol/go v0.2.22 h1:C/jjtwu5yTon8g0ewuN29QE7VXSQHyb2dx9W0U6Oqok= -github.com/opentdf/platform/protocol/go v0.2.22/go.mod h1:skpOCVuWSjUHazLKOkh3nSB057OB4sHICe7MpmJY9KU= -github.com/opentdf/platform/sdk v0.3.20 h1:zyBAZLhQaIv4X2twyPbmbdBd9Vc1vsTwxr1BIuESJWg= -github.com/opentdf/platform/sdk v0.3.20/go.mod h1:O4tyqjK9sJwp+6jUeiJjECe9TQfqaD1kTr6wgsRxkWc= +github.com/opentdf/platform/protocol/go v0.2.18 h1:s+TVZkOPGCzy7WyObtJWJNaFeOGDUTuSmAsq3omvugY= +github.com/opentdf/platform/protocol/go v0.2.18/go.mod h1:WqDcnFQJb0v8ivRQPidbehcL8ils5ZSZYXkuv0nyvsI= +github.com/opentdf/platform/protocol/go v0.2.20 h1:FPU1ZcXvPm/QeE2nqgbD/HMTOCICQSD0DoncQbAZ1ws= +github.com/opentdf/platform/protocol/go v0.2.20/go.mod h1:TWIuf387VeR3q0TL4nAMKQTWEqqID+8Yjao76EX9Dto= +github.com/opentdf/platform/sdk v0.3.17 h1:Uo/kTMneB18i0gZNfTRtvw34bGLFUc8BEnA/BMK0VVs= +github.com/opentdf/platform/sdk v0.3.17/go.mod h1:c2+nrsRLvLf2OOryXnNy0iGZN/TScc21Pul7uqKVXIs= +github.com/opentdf/platform/sdk v0.3.18 h1:IY6fNrOfQD9lF/hZp9ewZsH0PMuLe17HlSE1A5kyIWc= +github.com/opentdf/platform/sdk v0.3.18/go.mod h1:u+XZhVRsMq5blukCFCHcjk6HLCp4Y5mmIQu7GhtKQ3E= +github.com/opentdf/platform/sdk v0.3.19 h1:4Ign6HPrxOH6ZllLO/cI6joSuqz8CqPlpxpTKunpMQs= +github.com/opentdf/platform/sdk v0.3.19/go.mod h1:u+XZhVRsMq5blukCFCHcjk6HLCp4Y5mmIQu7GhtKQ3E= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= @@ -332,8 +338,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -345,8 +351,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index bfd78b58..84fda51f 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -214,7 +214,7 @@ const ( // Facilitates an auth code PKCE flow to obtain OIDC tokens. // Spawns a local server to handle the callback and opens a browser window in each respective OS. -func Login(ctx context.Context, platformEndpoint, tokenURL, authURL, publicClientID string, tlsNoVerify bool) (*oauth2.Token, error) { +func Login(ctx context.Context, platformEndpoint, tokenURL, authURL, publicClientID string) (*oauth2.Token, error) { // Generate random hash and encryption keys for cookie handling hashKey := make([]byte, keyLength) encryptKey := make([]byte, keyLength) @@ -239,16 +239,9 @@ func Login(ctx context.Context, platformEndpoint, tokenURL, authURL, publicClien }, } - var cookieOpts []httphelper.CookieHandlerOpt - if tlsNoVerify { - cookieOpts = append(cookieOpts, httphelper.WithUnsecure()) - } - - cookiehandler := httphelper.NewCookieHandler(hashKey, encryptKey, cookieOpts...) + cookiehandler := httphelper.NewCookieHandler(hashKey, encryptKey) relyingParty, err := oidcrp.NewRelyingPartyOAuth(conf, - // respect tlsNoVerify - oidcrp.WithHTTPClient(utils.NewHttpClient(tlsNoVerify)), // allow cookie handling for PKCE oidcrp.WithCookieHandler(cookiehandler), // use PKCE @@ -278,7 +271,7 @@ func LoginWithPKCE(ctx context.Context, host, publicClientID string, tlsNoVerify return nil, "", fmt.Errorf("failed to get platform configuration: %w", err) } - tok, err := Login(ctx, host, pc.tokenEndpoint, pc.authzEndpoint, pc.publicClientID, tlsNoVerify) + tok, err := Login(ctx, host, pc.tokenEndpoint, pc.authzEndpoint, pc.publicClientID) if err != nil { return nil, "", fmt.Errorf("failed to login: %w", err) } diff --git a/pkg/cli/flagValues.go b/pkg/cli/flagValues.go index aed53b2d..3d5c0b9a 100644 --- a/pkg/cli/flagValues.go +++ b/pkg/cli/flagValues.go @@ -52,18 +52,11 @@ func (f flagHelper) GetOptionalID(idFlag string) string { } func (f flagHelper) GetOptionalString(flag string) string { - return f.GetOptionalStringWithDefault(flag, "") -} - -// GetOptionalStringWithDefault retrieves a string flag, or returns the default value if the flag is not set -func (f flagHelper) GetOptionalStringWithDefault(flagName string, defaultValue string) string { - if f.cmd.Flags().Changed(flagName) { - value, err := f.cmd.Flags().GetString(flagName) - if err == nil { - return value - } + p := f.cmd.Flag(flag) + if p == nil { + return "" } - return defaultValue + return p.Value.String() } func (f flagHelper) GetStringSlice(flag string, v []string, opts FlagsStringSliceOptions) []string { @@ -89,18 +82,8 @@ func (f flagHelper) GetRequiredInt32(flag string) int32 { } func (f flagHelper) GetOptionalBool(flag string) bool { - return f.GetOptionalBoolWithDefault(flag, false) -} - -// GetOptionalBoolWithDefault retrieves a boolean flag, or returns the default value if the flag is not set -func (f flagHelper) GetOptionalBoolWithDefault(flagName string, defaultValue bool) bool { - if f.cmd.Flags().Changed(flagName) { - value, err := f.cmd.Flags().GetBool(flagName) - if err == nil { - return value - } - } - return defaultValue + v, _ := f.cmd.Flags().GetBool(flag) + return v } func (f flagHelper) GetRequiredBool(flag string) bool { diff --git a/pkg/profiles/profile.go b/pkg/profiles/profile.go index 856ec955..d5ff08b4 100644 --- a/pkg/profiles/profile.go +++ b/pkg/profiles/profile.go @@ -4,13 +4,11 @@ import ( "errors" ) -// Define constants for the different storage drivers and store keys -const ( - PROFILE_DRIVER_KEYRING ProfileDriver = "keyring" - PROFILE_DRIVER_IN_MEMORY ProfileDriver = "in-memory" - PROFILE_DRIVER_FILE ProfileDriver = "file" - PROFILE_DRIVER_DEFAULT = PROFILE_DRIVER_FILE +// TODO: +// - add a version +// - add a migration path +const ( STORE_KEY_PROFILE = "profile" STORE_KEY_GLOBAL = "global" ) @@ -26,7 +24,17 @@ type Profile struct { currentProfileStore *ProfileStore } -// Variadic functions to set different storage drivers +type CurrentProfileStore struct { + StoreInterface + ProfileConfig +} + +const ( + PROFILE_DRIVER_KEYRING ProfileDriver = "keyring" + PROFILE_DRIVER_IN_MEMORY ProfileDriver = "in-memory" + PROFILE_DRIVER_DEFAULT = PROFILE_DRIVER_KEYRING +) + type ( profileConfigVariadicFunc func(profileConfig) profileConfig ProfileDriver string @@ -46,31 +54,26 @@ func WithKeyringStore() profileConfigVariadicFunc { } } -func WithFileStore() profileConfigVariadicFunc { - return func(c profileConfig) profileConfig { - c.driver = PROFILE_DRIVER_FILE - return c - } -} - -// newStoreFactory returns a storage interface based on the configured driver func newStoreFactory(driver ProfileDriver) NewStoreInterface { switch driver { case PROFILE_DRIVER_KEYRING: return NewKeyringStore case PROFILE_DRIVER_IN_MEMORY: return NewMemoryStore - case PROFILE_DRIVER_FILE: - return NewFileStore default: return nil } } -// New creates a new Profile with the specified configuration options +// create a new profile and load global config func New(opts ...profileConfigVariadicFunc) (*Profile, error) { var err error + newStoreFactory("hello") + if testProfile != nil { + return testProfile, nil + } + config := profileConfig{ driver: PROFILE_DRIVER_DEFAULT, } @@ -78,7 +81,7 @@ func New(opts ...profileConfigVariadicFunc) (*Profile, error) { config = opt(config) } - // Validate and initialize the store + // check if the store driver is valid newStore := newStoreFactory(config.driver) if newStore == nil { return nil, errors.New("invalid store driver") @@ -88,7 +91,7 @@ func New(opts ...profileConfigVariadicFunc) (*Profile, error) { config: config, } - // Load global configuration + // load global config p.globalStore, err = LoadGlobalConfig(newStore) if err != nil { return nil, err @@ -97,16 +100,14 @@ func New(opts ...profileConfigVariadicFunc) (*Profile, error) { return p, nil } -// GetGlobalConfig returns the global configuration func (p *Profile) GetGlobalConfig() *GlobalStore { return p.globalStore } -// AddProfile adds a new profile to the current configuration -func (p *Profile) AddProfile(profileName, endpoint string, tlsNoVerify, setDefault bool) error { +func (p *Profile) AddProfile(profileName string, endpoint string, tlsNoVerify bool, setDefault bool) error { var err error - // Check if the profile already exists + // check if profile already exists if p.globalStore.ProfileExists(profileName) { return errors.New("profile already exists") } @@ -120,7 +121,7 @@ func (p *Profile) AddProfile(profileName, endpoint string, tlsNoVerify, setDefau return err } - // Add profile to global configuration + // add profile to global config if err := p.globalStore.AddProfile(profileName); err != nil { return err } @@ -132,76 +133,80 @@ func (p *Profile) AddProfile(profileName, endpoint string, tlsNoVerify, setDefau return nil } -// GetCurrentProfile retrieves the current profile store func (p *Profile) GetCurrentProfile() (*ProfileStore, error) { if p.currentProfileStore == nil { return nil, errors.New("no current profile set") } + return p.currentProfileStore, nil } -// GetProfile retrieves a specified profile func (p *Profile) GetProfile(profileName string) (*ProfileStore, error) { if !p.globalStore.ProfileExists(profileName) { return nil, errors.New("profile does not exist") } + return LoadProfileStore(newStoreFactory(p.config.driver), profileName) } -// ListProfiles returns a list of profile names func (p *Profile) ListProfiles() []string { return p.globalStore.ListProfiles() } -// UseProfile sets the current profile to the specified profile name func (p *Profile) UseProfile(profileName string) (*ProfileStore, error) { var err error - // If current profile is already set to this, return it - if p.currentProfileStore != nil && p.currentProfileStore.config.Name == profileName { - return p.currentProfileStore, nil + // check if current profile is already set + if p.currentProfileStore != nil { + if p.currentProfileStore.config.Name == profileName { + return p.currentProfileStore, nil + } } - // Set current profile + // set current profile p.currentProfileStore, err = p.GetProfile(profileName) return p.currentProfileStore, err } -// UseDefaultProfile sets the current profile to the default profile func (p *Profile) UseDefaultProfile() (*ProfileStore, error) { defaultProfile := p.globalStore.GetDefaultProfile() if defaultProfile == "" { return nil, errors.New("no default profile set") } + return p.UseProfile(defaultProfile) } -// SetDefaultProfile sets a specified profile as the default profile func (p *Profile) SetDefaultProfile(profileName string) error { if !p.globalStore.ProfileExists(profileName) { return errors.New("profile does not exist") } + return p.globalStore.SetDefaultProfile(profileName) } -// DeleteProfile removes a profile from storage func (p *Profile) DeleteProfile(profileName string) error { - // Check if the profile exists + // check if profile exists if !p.globalStore.ProfileExists(profileName) { return errors.New("profile does not exist") } - // Retrieve the profile + // get profile profile, err := LoadProfileStore(newStoreFactory(p.config.driver), profileName) if err != nil { return err } - // Remove profile from global configuration + // remove profile from global config (will error if profile is default) if err := p.globalStore.RemoveProfile(profileName); err != nil { return err } - // Delete profile configuration - return profile.Delete() + // delete profile config + err = profile.Delete() + if err != nil { + return err + } + + return nil } diff --git a/pkg/profiles/profileConfig.go b/pkg/profiles/profileConfig.go index 999356cc..f4946a7a 100644 --- a/pkg/profiles/profileConfig.go +++ b/pkg/profiles/profileConfig.go @@ -1,40 +1,27 @@ package profiles import ( - "fmt" - "time" - "github.com/opentdf/otdfctl/pkg/config" "github.com/opentdf/otdfctl/pkg/utils" ) -// URN-based namespace template without UUID, using only profile name for uniqueness -var URNNamespaceTemplate = fmt.Sprintf("urn:opentdf:%s:profile:v1", config.AppName) // e.g., urn:opentdf:otdfctl:profile:v1: - -// ProfileStore manages profile configurations and handles storage type ProfileStore struct { - store StoreInterface + store StoreInterface + config ProfileConfig } -// ProfileConfig defines the structure of a profile with flexible attributes and timestamps type ProfileConfig struct { - Name string `json:"profile"` // Profile name (unique identifier) - Endpoint string `json:"endpoint"` // Profile endpoint - TlsNoVerify bool `json:"tlsNoVerify"` // TLS verification setting - AuthCredentials AuthCredentials `json:"authCredentials"` // Authentication credentials - Attributes map[string]interface{} `json:"attributes"` // Flexible map of additional attributes - CreatedAt time.Time `json:"createdAt"` // Timestamp for profile creation - UpdatedAt time.Time `json:"updatedAt"` // Timestamp for last profile update - Version string `json:"version"` // profile version + Name string `json:"profile"` + Endpoint string `json:"endpoint"` + TlsNoVerify bool `json:"tlsNoVerify"` + AuthCredentials AuthCredentials `json:"authCredentials"` } -// NewProfileStore creates a new profile store with flexible attributes and timestamps func NewProfileStore(newStore NewStoreInterface, profileName string, endpoint string, tlsNoVerify bool) (*ProfileStore, error) { if err := validateProfileName(profileName); err != nil { return nil, err } - u, err := utils.NormalizeEndpoint(endpoint) if err != nil { return nil, err @@ -46,16 +33,11 @@ func NewProfileStore(newStore NewStoreInterface, profileName string, endpoint st Name: profileName, Endpoint: u.String(), TlsNoVerify: tlsNoVerify, - Attributes: make(map[string]interface{}), // Empty map for flexible attributes - CreatedAt: time.Now().UTC(), // Set creation time - UpdatedAt: time.Now().UTC(), // Set initial update time - Version: URNNamespaceTemplate, // Set profile version to URN-based namespace template }, } return p, nil } -// LoadProfileStore loads an existing profile using its profile name func LoadProfileStore(newStore NewStoreInterface, profileName string) (*ProfileStore, error) { if err := validateProfileName(profileName); err != nil { return nil, err @@ -67,67 +49,49 @@ func LoadProfileStore(newStore NewStoreInterface, profileName string) (*ProfileS return p, p.Get() } -// Get loads the profile configuration into p.config func (p *ProfileStore) Get() error { return p.store.Get(&p.config) } -// Save saves the current profile configuration to storage and updates UpdatedAt timestamp func (p *ProfileStore) Save() error { - p.config.UpdatedAt = time.Now().UTC() return p.store.Set(p.config) } -// Delete removes the profile from storage func (p *ProfileStore) Delete() error { return p.store.Delete() } -// Generate a unique namespace for a profile using only the profile name -func (p *ProfileConfig) GetNamespace() string { - return URNNamespaceTemplate -} - -// GetProfileName retrieves the profile name +// Profile Name func (p *ProfileStore) GetProfileName() string { return p.config.Name } -// SetAttribute allows adding or updating an attribute in the profile's Attributes map -func (p *ProfileStore) SetAttribute(key string, value interface{}) error { - p.config.Attributes[key] = value - return p.Save() -} - -// GetAttribute retrieves an attribute by key from the profile's Attributes map -func (p *ProfileStore) GetAttribute(key string) (interface{}, bool) { - value, exists := p.config.Attributes[key] - return value, exists -} - -// GetEndpoint retrieves the Endpoint value from ProfileConfig +// Endpoint func (p *ProfileStore) GetEndpoint() string { return p.config.Endpoint } -// SetEndpoint updates the Endpoint in ProfileConfig after normalizing it, then saves the profile func (p *ProfileStore) SetEndpoint(endpoint string) error { u, err := utils.NormalizeEndpoint(endpoint) if err != nil { return err } p.config.Endpoint = u.String() - return p.Save() // Save the updated profile configuration + return p.Save() } -// GetTLSNoVerify retrieves the TlsNoVerify setting from ProfileConfig +// TLS No Verify func (p *ProfileStore) GetTLSNoVerify() bool { return p.config.TlsNoVerify } +func (p *ProfileStore) SetTLSNoVerify(tlsNoVerify bool) error { + p.config.TlsNoVerify = tlsNoVerify + return p.Save() +} + // utility functions -// getStoreKey generates a unique key for storing the profile using the profile name -func getStoreKey(name string) string { - return fmt.Sprintf("%s-%s", STORE_KEY_PROFILE, name) +func getStoreKey(n string) string { + return STORE_KEY_PROFILE + "-" + n } diff --git a/pkg/profiles/storeFile.go b/pkg/profiles/storeFile.go deleted file mode 100644 index 5f5f8a53..00000000 --- a/pkg/profiles/storeFile.go +++ /dev/null @@ -1,258 +0,0 @@ -package profiles - -import ( - "bytes" - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "strings" - "time" - - "github.com/zalando/go-keyring" -) - -type FileStore struct { - namespace string - key string - filePath string -} - -// Metadata structure for unencrypted metadata about the encrypted file -type FileMetadata struct { - ProfileName string `json:"profile_name"` - CreatedAt string `json:"created_at"` - EncryptionAlg string `json:"encryption_alg"` - Version string `json:"version"` -} -const ( - aes256KeyLength = 32 - ownerPermissionsRW = 0o600 - ownerPermissionsRWX = 0o700 -) - -// Generates a safe, hashed filename from namespace and key -func hashNamespaceAndKey(namespace string, key string) string { - hash := sha256.Sum256([]byte(namespace + ":" + key)) - return hex.EncodeToString(hash[:]) -} - -// NewFileStore is the constructor function for FileStore, setting the file path based on executable directory or environment variable and hashed filename -var NewFileStore NewStoreInterface = func(namespace string, key string) StoreInterface { - // Check for OTDFCTL_PROFILE_PATH environment variable - baseDir := os.Getenv("OTDFCTL_PROFILE_PATH") - if baseDir == "" { - // If environment variable is not set, use the "profiles" directory relative to the executable - execPath, err := os.Executable() - if err != nil { - panic("unable to determine the executable path for profile storage") - } - execDir := filepath.Dir(execPath) - baseDir = filepath.Join(execDir, "profiles") - } - - // Ensure the base directory exists with owner-only access including execute - if err := os.MkdirAll(baseDir, ownerPermissionsRWX); err != nil { - panic(fmt.Sprintf("failed to create profiles directory %s: please check directory permissions", baseDir)) - } - - // Check for read/write permissions by creating and removing a temp file - testFilePath := filepath.Join(baseDir, ".tmp_profile_rw_test") - testFile, err := os.Create(testFilePath) - if err != nil { - panic(fmt.Sprintf("unable to write to profiles directory %s: please ensure write permissions are granted", baseDir)) - } - testFile.Close() - if err := os.Remove(testFilePath); err != nil { - panic(fmt.Sprintf("unable to delete temp file in profiles directory %s: please ensure delete permissions are granted", baseDir)) - } - - // Generate the filename hashed for uniqueness - // Note: other stores use the config.AppName, but want to rely on something more resilient like the namespace - fileName := hashNamespaceAndKey(URNNamespaceTemplate, key) - filePath := filepath.Join(baseDir, fileName+".enc") - - return &FileStore{ - namespace: namespace, - key: key, - filePath: filePath, - } -} - -// Exists checks if the encrypted file exists -func (f *FileStore) Exists() bool { - _, err := os.Stat(f.filePath) - return err == nil -} - -// Get retrieves and decrypts data from the file -func (f *FileStore) Get(value interface{}) error { - key, err := f.getEncryptionKey() - if err != nil { - return err - } - - encryptedData, err := os.ReadFile(f.filePath) - if err != nil { - return err - } - - data, err := decryptData(key, encryptedData) - if err != nil { - return err - } - - return json.NewDecoder(bytes.NewReader(data)).Decode(value) -} - -// Set encrypts and saves data to the file, also saving metadata -func (f *FileStore) Set(value interface{}) error { - key, err := f.getEncryptionKey() - if err != nil { - return err - } - - var b bytes.Buffer - if err := json.NewEncoder(&b).Encode(value); err != nil { - return err - } - - encryptedData, err := encryptData(key, b.Bytes()) - if err != nil { - return err - } - - // Write the encrypted profile file with proper permissions - if err := os.WriteFile(f.filePath, encryptedData, ownerPermissionsRW); err != nil { - return fmt.Errorf("failed to write encrypted profile to %s: %w", f.filePath, err) - } - - // Save metadata as well - profileName := f.key // or extract from value if it's part of a ProfileConfig struct - return f.SaveMetadata(profileName) -} - -// Delete removes the encrypted file and metadata file from disk -func (f *FileStore) Delete() error { - if err := os.Remove(f.filePath); err != nil { - return err - } - - // Remove the extension from filePath and add .nfo for the metadata file - metadataFilePath := strings.TrimSuffix(f.filePath, filepath.Ext(f.filePath)) + ".nfo" - return os.Remove(metadataFilePath) -} - -// getEncryptionKey retrieves the encryption key from the keyring or generates it if absent -func (f *FileStore) getEncryptionKey() ([]byte, error) { - - // Try retrieving the key as a string from the keyring - keyStr, err := keyring.Get(URNNamespaceTemplate, f.key) - if errors.Is(err, keyring.ErrNotFound) { - // Generate a new key if not found - key := make([]byte, aes256KeyLength) - if _, err := rand.Read(key); err != nil { - return nil, err - } - - // Convert key to string for storage in the keyring - if err := keyring.Set(URNNamespaceTemplate, f.key, string(key)); err != nil { - return nil, err - } - - return key, nil - } else if err != nil { - return nil, err - } - - // Convert the stored string key back to []byte for use - return []byte(keyStr), nil -} - -// encryptData encrypts data using AES-GCM -func encryptData(key, data []byte) ([]byte, error) { - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - aesGCM, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - - nonce := make([]byte, aesGCM.NonceSize()) - if _, err := rand.Read(nonce); err != nil { - return nil, err - } - - // Encrypt the data with a separate destination buffer - ciphertext := aesGCM.Seal(nil, nonce, data, nil) - - // Prepend the nonce to the ciphertext - result := make([]byte, len(nonce)+len(ciphertext)) - copy(result, nonce) - copy(result[len(nonce):], ciphertext) - return result, nil -} - -// decryptData decrypts data using AES-GCM -func decryptData(key, encryptedData []byte) ([]byte, error) { - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - aesGCM, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - - nonceSize := aesGCM.NonceSize() - if len(encryptedData) < nonceSize { - return nil, errors.New("invalid encrypted data") - } - - nonce, ciphertext := encryptedData[:nonceSize], encryptedData[nonceSize:] - return aesGCM.Open(nil, nonce, ciphertext, nil) -} - -// SaveMetadata writes unencrypted metadata to a .nfo file -func (f *FileStore) SaveMetadata(profileName string) error { - metadata := FileMetadata{ - ProfileName: profileName, - CreatedAt: time.Now().Format(time.RFC3339), - EncryptionAlg: "AES-256-GCM", - Version: URNNamespaceTemplate, - } - - data, err := json.MarshalIndent(metadata, "", " ") - if err != nil { - return err - } - - metadataFilePath := strings.TrimSuffix(f.filePath, filepath.Ext(f.filePath)) + ".nfo" - return os.WriteFile(metadataFilePath, data, ownerPermissionsRW) -} - -// LoadMetadata loads and parses metadata from a .nfo file -func (f *FileStore) LoadMetadata() (*FileMetadata, error) { - metadataFilePath := strings.TrimSuffix(f.filePath, filepath.Ext(f.filePath)) + ".nfo" - data, err := os.ReadFile(metadataFilePath) - if err != nil { - return nil, err - } - - var metadata FileMetadata - if err := json.Unmarshal(data, &metadata); err != nil { - return nil, err - } - - return &metadata, nil -} diff --git a/pkg/utils/validators.go b/pkg/utils/validators.go index c0b87c89..8569aaf9 100644 --- a/pkg/utils/validators.go +++ b/pkg/utils/validators.go @@ -6,39 +6,28 @@ import ( "strings" ) -// NormalizeEndpoint validates, defaults, and standardizes an endpoint URL by setting default scheme/port and removing trailing slashes. func NormalizeEndpoint(endpoint string) (*url.URL, error) { if endpoint == "" { return nil, errors.New("endpoint is required") } - - // Add default scheme if missing - if !strings.Contains(endpoint, "://") { - endpoint = "https://" + endpoint - } - - // Parse the URL u, err := url.Parse(endpoint) if err != nil { return nil, err } - - // Validate scheme and set default ports for http/https switch u.Scheme { case "http": if u.Port() == "" { - u.Host = u.Hostname() + ":80" + u.Host += ":80" } case "https": if u.Port() == "" { - u.Host = u.Hostname() + ":443" + u.Host += ":443" } default: - return nil, errors.New("invalid scheme: only http and https are supported") + return nil, errors.New("invalid scheme") + } + for strings.HasSuffix(u.Path, "/") { + u.Path = strings.TrimSuffix(u.Path, "/") } - - // Trim trailing slashes from path - u.Path = strings.TrimRight(u.Path, "/") - return u, nil }