From 5b923cc77ab9c60ae7073f7b9f9c58d38716fad5 Mon Sep 17 00:00:00 2001 From: sahinakyol Date: Tue, 26 Nov 2024 09:01:02 +0100 Subject: [PATCH 1/5] spike-46 fix quickstart doc example --- docs/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index eb5c602..41c24d6 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -201,7 +201,7 @@ spike init # Register a secret: spike put /secrets/db-secret username=postgres password=postgres -spike get /secrets/db/secret +spike get /secrets/db-secret # Wil return: # username=postgres # password=postgres From c5330514787aaf3f0ded3c1d4971130941b6c483 Mon Sep 17 00:00:00 2001 From: sahinakyol Date: Tue, 26 Nov 2024 13:57:23 +0100 Subject: [PATCH 2/5] spike-46 add metadata api to cmd --- app/nexus/internal/route/store/secret/get.go | 86 ++++++++++++++------ app/nexus/internal/state/base/secret.go | 30 ++++++- app/spike/internal/cmd/secret/delete.go | 12 +-- app/spike/internal/cmd/secret/get.go | 66 +++++++++++++-- app/spike/internal/cmd/secret/put.go | 4 +- app/spike/internal/cmd/secret/undelete.go | 12 +-- app/spike/internal/net/store/get.go | 11 ++- docs/quickstart.md | 8 +- internal/entity/v1/reqres/secret.go | 34 ++++++-- 9 files changed, 201 insertions(+), 62 deletions(-) diff --git a/app/nexus/internal/route/store/secret/get.go b/app/nexus/internal/route/store/secret/get.go index 794391b..53a87d0 100644 --- a/app/nexus/internal/route/store/secret/get.go +++ b/app/nexus/internal/route/store/secret/get.go @@ -78,13 +78,39 @@ func RouteGetSecret( return errors.New("failed to parse request body") } - version := request.Version path := request.Path + version := request.Version + metadata := request.Metadata + + var responseBody []byte + if metadata { + rawSecret, err := state.GetRawSecret(path, version) + if err != nil { + return handleError(err, w) + } + + response := rawSecretResponseMapper(rawSecret) + responseBody = net.MarshalBody(response, w) + } else { + secret, err := state.GetSecret(path, version) + if err != nil { + return handleError(err, w) + } + + responseBody = net.MarshalBody(reqres.SecretReadResponse{Data: secret}, w) + } + + if responseBody == nil { + return errors.New("failed to marshal response body") + } + + net.Respond(http.StatusOK, responseBody, w) + log.Log().Info("routeGetSecret", "msg", "OK") + return nil +} - secret, err := state.GetSecret(path, version) - if err == nil { - log.Log().Info("routeGetSecret", "msg", "Secret found") - } else if errors.Is(err, store.ErrSecretNotFound) { +func handleError(err error, w http.ResponseWriter) error { + if errors.Is(err, store.ErrSecretNotFound) { log.Log().Info("routeGetSecret", "msg", "Secret not found") res := reqres.SecretReadResponse{Err: reqres.ErrNotFound} @@ -92,32 +118,42 @@ func RouteGetSecret( if responseBody == nil { return errors.New("failed to marshal response body") } - net.Respond(http.StatusNotFound, responseBody, w) - log.Log().Info("routeGetSecret", "msg", "not found") return nil - } else { - log.Log().Info("routeGetSecret", - "msg", "Failed to retrieve secret", "err", err) - - responseBody := net.MarshalBody(reqres.SecretReadResponse{ - Err: "Internal server error"}, w, - ) - if responseBody == nil { - return errors.New("failed to marshal response body") - } - - net.Respond(http.StatusInternalServerError, responseBody, w) - log.Log().Info("routeGetSecret", "msg", "internal server error") - return err } - responseBody := net.MarshalBody(reqres.SecretReadResponse{Data: secret}, w) + log.Log().Info("routeGetSecret", "msg", "Failed to retrieve secret", "err", err) + responseBody := net.MarshalBody(reqres.SecretReadResponse{ + Err: "Internal server error"}, w, + ) if responseBody == nil { return errors.New("failed to marshal response body") } + net.Respond(http.StatusInternalServerError, responseBody, w) + return err +} + +func rawSecretResponseMapper(rawSecret *store.Secret) reqres.RawSecretResponse { + versions := make(map[int]reqres.RawSecretVersionResponse) + for versionNum, version := range rawSecret.Versions { + versions[versionNum] = reqres.RawSecretVersionResponse{ + Data: version.Data, + CreatedTime: version.CreatedTime, + Version: version.Version, + DeletedTime: version.DeletedTime, + } + } - net.Respond(http.StatusOK, responseBody, w) - log.Log().Info("routeGetSecret", "msg", "OK") - return nil + metadata := reqres.RawSecretMetadataResponse{ + CurrentVersion: rawSecret.Metadata.CurrentVersion, + OldestVersion: rawSecret.Metadata.OldestVersion, + CreatedTime: rawSecret.Metadata.CreatedTime, + UpdatedTime: rawSecret.Metadata.UpdatedTime, + MaxVersions: rawSecret.Metadata.MaxVersions, + } + + return reqres.RawSecretResponse{ + Versions: versions, + Metadata: metadata, + } } diff --git a/app/nexus/internal/state/base/secret.go b/app/nexus/internal/state/base/secret.go index e1b27b1..4516375 100644 --- a/app/nexus/internal/state/base/secret.go +++ b/app/nexus/internal/state/base/secret.go @@ -4,7 +4,10 @@ package base -import "github.com/spiffe/spike/app/nexus/internal/state/persist" +import ( + "github.com/spiffe/spike/app/nexus/internal/state/persist" + "github.com/spiffe/spike/pkg/store" +) // UpsertSecret stores or updates a secret at the specified path with the // provided values. It provides thread-safe access to the underlying key-value @@ -106,3 +109,28 @@ func GetSecret(path string, version int) (map[string]string, error) { return cachedSecret.Versions[version].Data, nil } + +func GetRawSecret(path string, version int) (*store.Secret, error) { + kvMu.RLock() + secret, err := kv.GetRawSecret(path) + kvMu.RUnlock() + + if err == nil { + return secret, nil + } + + cachedSecret := persist.ReadSecret(path, version) + if cachedSecret == nil { + return nil, err + } + + if version == 0 { + version = cachedSecret.Metadata.CurrentVersion + } + + kvMu.Lock() + kv.Put(path, cachedSecret.Versions[version].Data) + kvMu.Unlock() + + return cachedSecret, nil +} diff --git a/app/spike/internal/cmd/secret/delete.go b/app/spike/internal/cmd/secret/delete.go index edfd5c5..b5e7830 100644 --- a/app/spike/internal/cmd/secret/delete.go +++ b/app/spike/internal/cmd/secret/delete.go @@ -38,9 +38,9 @@ import ( // // Example Usage: // -// spike delete secret/apocalyptica # Deletes current version -// spike delete secret/apocalyptica -v 1,2,3 # Deletes specific versions -// spike delete secret/apocalyptica -v 0,1,2 # Deletes current version plus 1,2 +// spike secret delete secret/apocalyptica # Deletes current version +// spike secret delete secret/apocalyptica -v 1,2,3 # Deletes specific versions +// spike secret delete secret/apocalyptica -v 0,1,2 # Deletes current version plus 1,2 // // The command performs trust to ensure: // - Exactly one path argument is provided @@ -56,9 +56,9 @@ Version 0 refers to the current/latest version. If no version is specified, defaults to deleting the current version. Examples: - spike delete secret/apocalyptica # Deletes current version - spike delete secret/apocalyptica -v 1,2,3 # Deletes specific versions - spike delete secret/apocalyptica -v 0,1,2 # Deletes current version plus versions 1 and 2`, + spike secret delete secret/apocalyptica # Deletes current version + spike secret delete secret/apocalyptica -v 1,2,3 # Deletes specific versions + spike secret delete secret/apocalyptica -v 0,1,2 # Deletes current version plus versions 1 and 2`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { state, err := auth.CheckInitState(source) diff --git a/app/spike/internal/cmd/secret/get.go b/app/spike/internal/cmd/secret/get.go index 4187db7..fb77616 100644 --- a/app/spike/internal/cmd/secret/get.go +++ b/app/spike/internal/cmd/secret/get.go @@ -6,6 +6,9 @@ package secret import ( "fmt" + "github.com/spiffe/spike/internal/entity/v1/reqres" + "strings" + "time" "github.com/spf13/cobra" "github.com/spiffe/go-spiffe/v2/workloadapi" @@ -61,8 +64,9 @@ func newSecretGetCommand(source *workloadapi.X509Source) *cobra.Command { path := args[0] version, _ := cmd.Flags().GetInt("version") + metadata, _ := cmd.Flags().GetBool("metadata") - secret, err := store.GetSecret(source, path, version) + secret, err := store.GetSecret(source, path, version, metadata) if err != nil { fmt.Println("Error reading secret:", err.Error()) return @@ -73,14 +77,66 @@ func newSecretGetCommand(source *workloadapi.X509Source) *cobra.Command { return } - data := secret.Data - for k, v := range data { - fmt.Printf("%s: %s\n", k, v) - } + printSecretResponse(secret) }, } getCmd.Flags().IntP("version", "v", 0, "Specific version to retrieve") + getCmd.Flags().BoolP("metadata", "m", false, "Show metadata instead of secret values") + getCmd.Flags().Bool("versions", false, "List all versions of the secret") return getCmd } + +func printSecretResponse(response *reqres.SecretReadResponse) { + printSeparator := func() { + fmt.Println(strings.Repeat("-", 50)) + } + + formatTime := func(t time.Time) string { + return t.Format("2006-01-02 15:04:05 MST") + } + + if len(response.Data) > 0 { + fmt.Println("Current Secret Data:") + printSeparator() + for k, v := range response.Data { + fmt.Printf("%-20s: %s\n", k, v) + } + printSeparator() + } + + if response.Metadata != (reqres.RawSecretMetadataResponse{}) { + fmt.Println("\nMetadata:") + printSeparator() + fmt.Printf("Current Version : %d\n", response.Metadata.CurrentVersion) + fmt.Printf("Oldest Version : %d\n", response.Metadata.OldestVersion) + fmt.Printf("Created Time : %s\n", formatTime(response.Metadata.CreatedTime)) + fmt.Printf("Last Updated : %s\n", formatTime(response.Metadata.UpdatedTime)) + fmt.Printf("Max Versions : %d\n", response.Metadata.MaxVersions) + printSeparator() + } + + if len(response.Versions) > 0 { + fmt.Println("\nSecret Versions:") + printSeparator() + + for version, versionData := range response.Versions { + fmt.Printf("Version %d:\n", version) + fmt.Printf(" Created: %s\n", formatTime(versionData.CreatedTime)) + if versionData.DeletedTime != nil { + fmt.Printf(" Deleted: %s\n", formatTime(*versionData.DeletedTime)) + } + fmt.Println(" Data:") + for k, v := range versionData.Data { + fmt.Printf(" %-18s: %s\n", k, v) + } + printSeparator() + } + } + + if response.Err != "" { + fmt.Printf("\nError: %s\n", response.Err) + printSeparator() + } +} diff --git a/app/spike/internal/cmd/secret/put.go b/app/spike/internal/cmd/secret/put.go index f7d9c78..c4f08cb 100644 --- a/app/spike/internal/cmd/secret/put.go +++ b/app/spike/internal/cmd/secret/put.go @@ -32,8 +32,8 @@ import ( // // Example Usage: // -// spike put secret/myapp username=admin password=secret -// spike put secret/config host=localhost port=8080 +// spike secret put secret/myapp username=admin password=secret +// spike secret put secret/config host=localhost port=8080 // // The command will: // 1. Verify SPIKE initialization status via admin token diff --git a/app/spike/internal/cmd/secret/undelete.go b/app/spike/internal/cmd/secret/undelete.go index e7f3dbb..0381766 100644 --- a/app/spike/internal/cmd/secret/undelete.go +++ b/app/spike/internal/cmd/secret/undelete.go @@ -38,9 +38,9 @@ import ( // // Example Usage: // -// spike undelete secret/ella # Restores current version -// spike undelete secret/ella -v 1,2,3 # Restores specific versions -// spike undelete secret/ella -v 0,1,2 # Restores current version plus 1,2 +// spike secret undelete secret/ella # Restores current version +// spike secret undelete secret/ella -v 1,2,3 # Restores specific versions +// spike secret undelete secret/ella -v 0,1,2 # Restores current version plus 1,2 // // The command performs trust to ensure: // - Exactly one path argument is provided @@ -59,9 +59,9 @@ Version 0 refers to the current/latest version. If no version is specified, defaults to undeleting the current version. Examples: - spike undelete secret/ella # Undeletes current version - spike undelete secret/ella -v 1,2,3 # Undeletes specific versions - spike undelete secret/ella -v 0,1,2 # Undeletes current version plus versions 1 and 2`, + spike secret undelete secret/ella # Undeletes current version + spike secret undelete secret/ella -v 1,2,3 # Undeletes specific versions + spike secret undelete secret/ella -v 0,1,2 # Undeletes current version plus versions 1 and 2`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { state, err := auth.CheckInitState(source) diff --git a/app/spike/internal/net/store/get.go b/app/spike/internal/net/store/get.go index a6b9d4e..960e95d 100644 --- a/app/spike/internal/net/store/get.go +++ b/app/spike/internal/net/store/get.go @@ -10,7 +10,6 @@ import ( "github.com/spiffe/go-spiffe/v2/workloadapi" - "github.com/spiffe/spike/app/spike/internal/entity/data" "github.com/spiffe/spike/app/spike/internal/net/api" "github.com/spiffe/spike/internal/auth" "github.com/spiffe/spike/internal/entity/v1/reqres" @@ -33,11 +32,11 @@ import ( // Example: // // secret, err := GetSecret(x509Source, "secret/path", 1) -func GetSecret(source *workloadapi.X509Source, - path string, version int) (*data.Secret, error) { +func GetSecret(source *workloadapi.X509Source, path string, version int, metadata bool) (*reqres.SecretReadResponse, error) { r := reqres.SecretReadRequest{ - Path: path, - Version: version, + Path: path, + Version: version, + Metadata: metadata, } mr, err := json.Marshal(r) @@ -73,5 +72,5 @@ func GetSecret(source *workloadapi.X509Source, return nil, errors.New(string(res.Err)) } - return &data.Secret{Data: res.Data}, nil + return &res, nil } diff --git a/docs/quickstart.md b/docs/quickstart.md index d8c1e40..014628c 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -244,16 +244,16 @@ Let test **SPIKE** by creating a secret spike init # Register a secret: -spike put /secrets/db-secret username=postgres password=postgres +spike secret put /secrets/db-secret username=postgres password=postgres -spike get /secrets/db-secret +spike secret get /secrets/db-secret # Wil return: # username=postgres # password=postgres -spike delete /secrets/db-secret # Deleting the current secret +spike secret delete /secrets/db-secret # Deleting the current secret -spike get /secrets/db-secret +spike secret get /secrets/db-secret # WIll be empty. ``` diff --git a/internal/entity/v1/reqres/secret.go b/internal/entity/v1/reqres/secret.go index 97f4f54..4934f23 100644 --- a/internal/entity/v1/reqres/secret.go +++ b/internal/entity/v1/reqres/secret.go @@ -6,8 +6,6 @@ package reqres import ( "time" - - "github.com/spiffe/spike/internal/entity/data" ) // SecretResponseMetadata is meta information about secrets for internal @@ -33,15 +31,17 @@ type SecretPutResponse struct { // SecretReadRequest is for getting secrets type SecretReadRequest struct { - Path string `json:"path"` - Version int `json:"version,omitempty"` // Optional specific version + Path string `json:"path"` + Version int `json:"version,omitempty"` // Optional specific version + Metadata bool `json:"metadata,omitempty"` // Optional specific version } // SecretReadResponse is for getting secrets type SecretReadResponse struct { - data.Secret - Data map[string]string `json:"data"` - Err ErrorCode `json:"err,omitempty"` + Data map[string]string `json:"data"` + Versions map[int]RawSecretVersionResponse `json:"versions,omitempty"` + Metadata RawSecretMetadataResponse `json:"metadata,omitempty"` + Err ErrorCode `json:"err,omitempty"` } // SecretDeleteRequest for soft-deleting secret versions @@ -77,3 +77,23 @@ type SecretListResponse struct { Keys []string `json:"keys"` Err ErrorCode `json:"err,omitempty"` } + +type RawSecretResponse struct { + Versions map[int]RawSecretVersionResponse `json:"versions,omitempty"` + Metadata RawSecretMetadataResponse `json:"metadata,omitempty"` +} + +type RawSecretVersionResponse struct { + Data map[string]string `json:"data"` + CreatedTime time.Time `json:"createdTime"` + Version int `json:"version"` + DeletedTime *time.Time `json:"deletedTime"` +} + +type RawSecretMetadataResponse struct { + CurrentVersion int `json:"currentVersion"` + OldestVersion int `json:"oldestVersion"` + CreatedTime time.Time `json:"createdTime"` + UpdatedTime time.Time `json:"updatedTime"` + MaxVersions int `json:"maxVersions"` +} From cb129b8a437520ba395ac26db55a5c84ea3c754e Mon Sep 17 00:00:00 2001 From: sahinakyol Date: Tue, 26 Nov 2024 14:07:52 +0100 Subject: [PATCH 3/5] spike-46 add doc to get_raw_secret func --- app/nexus/internal/state/base/secret.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/nexus/internal/state/base/secret.go b/app/nexus/internal/state/base/secret.go index 4516375..6c416e4 100644 --- a/app/nexus/internal/state/base/secret.go +++ b/app/nexus/internal/state/base/secret.go @@ -110,6 +110,16 @@ func GetSecret(path string, version int) (map[string]string, error) { return cachedSecret.Versions[version].Data, nil } +// GetRawSecret retrieves a secret with metadata from the specified path and version. +// It provides thread-safe read access to the secret store. +// +// Parameters: +// - path: The location of the secret to retrieve +// - version: The specific version of the secret to fetch +// +// Returns: +// - *store.Secret: The secret type +// - bool: Whether the secret was found func GetRawSecret(path string, version int) (*store.Secret, error) { kvMu.RLock() secret, err := kv.GetRawSecret(path) From b5a24785672e2c4ea12e2586483e7d3efa9c52ec Mon Sep 17 00:00:00 2001 From: sahinakyol Date: Wed, 27 Nov 2024 20:23:55 +0100 Subject: [PATCH 4/5] spike-46 seperate metadata command from get secret command and add additional explanatory comments --- app/nexus/internal/route/base/route.go | 2 + app/nexus/internal/route/store/secret/get.go | 160 ++++++++++++++----- app/spike/internal/cmd/secret/get.go | 100 +++++++++--- app/spike/internal/cmd/secret/new.go | 1 + app/spike/internal/net/api/secret.go | 11 ++ app/spike/internal/net/store/get.go | 66 +++++++- internal/entity/v1/reqres/secret.go | 38 +++-- internal/net/action.go | 1 + 8 files changed, 301 insertions(+), 78 deletions(-) diff --git a/app/nexus/internal/route/base/route.go b/app/nexus/internal/route/base/route.go index a4a1d61..76800ba 100644 --- a/app/nexus/internal/route/base/route.go +++ b/app/nexus/internal/route/base/route.go @@ -53,6 +53,8 @@ func Route( return policy.RouteDeletePolicy case a == net.ActionNexusList && p == net.SpikeNexusUrlPolicy: return policy.RouteListPolicies + case a == net.ActionNexusGet && p == net.SpikeNexusUrlSecretsMetadata: + return secret.RouteGetSecretMetadata default: return net.Fallback } diff --git a/app/nexus/internal/route/store/secret/get.go b/app/nexus/internal/route/store/secret/get.go index 53a87d0..a21af69 100644 --- a/app/nexus/internal/route/store/secret/get.go +++ b/app/nexus/internal/route/store/secret/get.go @@ -80,26 +80,14 @@ func RouteGetSecret( path := request.Path version := request.Version - metadata := request.Metadata - var responseBody []byte - if metadata { - rawSecret, err := state.GetRawSecret(path, version) - if err != nil { - return handleError(err, w) - } - - response := rawSecretResponseMapper(rawSecret) - responseBody = net.MarshalBody(response, w) - } else { - secret, err := state.GetSecret(path, version) - if err != nil { - return handleError(err, w) - } - - responseBody = net.MarshalBody(reqres.SecretReadResponse{Data: secret}, w) + secret, err := state.GetSecret(path, version) + if err != nil { + return handleError(err, w) } + responseBody := net.MarshalBody(reqres.SecretReadResponse{Data: secret}, w) + if responseBody == nil { return errors.New("failed to marshal response body") } @@ -109,42 +97,114 @@ func RouteGetSecret( return nil } -func handleError(err error, w http.ResponseWriter) error { - if errors.Is(err, store.ErrSecretNotFound) { - log.Log().Info("routeGetSecret", "msg", "Secret not found") +// RouteGetSecretMetadata handles requests to retrieve a secret metadata at a specific path +// and version. +// +// This endpoint requires a valid admin JWT token for authentication. The +// function retrieves a secret based on the provided path and optional version +// number. If no version is specified, the latest version is returned. +// +// The function follows these steps: +// 1. Validates the JWT token +// 2. Validates and unmarshal the request body +// 3. Attempts to retrieve the secret metadata +// 4. Returns the secret metadata or an appropriate error response +// +// Parameters: +// - w: http.ResponseWriter to write the HTTP response +// - r: *http.Request containing the incoming HTTP request +// - audit: *log.AuditEntry for logging audit information +// +// Returns: +// - error: if an error occurs during request processing. +// +// Request body format: +// +// { +// "path": string, // Path to the secret +// "version": int // Optional: specific version to retrieve +// } +// +// Response format on success (200 OK): +// +// "versions": { // map[int]SecretMetadataVersionResponse +// +// "version": { // SecretMetadataVersionResponse object +// "createdTime": "", // time.Time +// "version": 0, // int +// "deletedTime": null // *time.Time (pointer, can be null) +// } +// }, +// +// "metadata": { // SecretRawMetadataResponse object +// +// "currentVersion": 0, // int +// "oldestVersion": 0, // int +// "createdTime": "", // time.Time +// "updatedTime": "", // time.Time +// "maxVersions": 0 // int +// }, +// +// "err": null // ErrorCode +// +// Error responses: +// - 401 Unauthorized: Invalid or missing JWT token +// - 400 Bad Request: Invalid request body +// - 404 Not Found: Secret doesn't exist at specified path/version +// +// All operations are logged using structured logging. +func RouteGetSecretMetadata( + w http.ResponseWriter, r *http.Request, audit *log.AuditEntry, +) error { + log.Log().Info("routeGetSecretMetadata", "method", r.Method, "path", r.URL.Path, + "query", r.URL.RawQuery) + audit.Action = log.AuditRead - res := reqres.SecretReadResponse{Err: reqres.ErrNotFound} - responseBody := net.MarshalBody(res, w) - if responseBody == nil { - return errors.New("failed to marshal response body") - } - net.Respond(http.StatusNotFound, responseBody, w) - return nil + requestBody := net.ReadRequestBody(w, r) + if requestBody == nil { + return errors.New("failed to read request body") } - log.Log().Info("routeGetSecret", "msg", "Failed to retrieve secret", "err", err) - responseBody := net.MarshalBody(reqres.SecretReadResponse{ - Err: "Internal server error"}, w, + request := net.HandleRequest[ + reqres.SecretReadRequest, reqres.SecretReadResponse]( + requestBody, w, + reqres.SecretReadResponse{Err: reqres.ErrBadInput}, ) + if request == nil { + return errors.New("failed to parse request body") + } + + path := request.Path + version := request.Version + + rawSecret, err := state.GetRawSecret(path, version) + if err != nil { + return handleError(err, w) + } + + response := rawSecretResponseMapper(rawSecret) + responseBody := net.MarshalBody(response, w) + if responseBody == nil { return errors.New("failed to marshal response body") } - net.Respond(http.StatusInternalServerError, responseBody, w) - return err + + net.Respond(http.StatusOK, responseBody, w) + log.Log().Info("routeGetSecret", "msg", "OK") + return nil } -func rawSecretResponseMapper(rawSecret *store.Secret) reqres.RawSecretResponse { - versions := make(map[int]reqres.RawSecretVersionResponse) +func rawSecretResponseMapper(rawSecret *store.Secret) reqres.SecretMetadataResponse { + versions := make(map[int]reqres.SecretMetadataVersionResponse) for versionNum, version := range rawSecret.Versions { - versions[versionNum] = reqres.RawSecretVersionResponse{ - Data: version.Data, + versions[versionNum] = reqres.SecretMetadataVersionResponse{ CreatedTime: version.CreatedTime, Version: version.Version, DeletedTime: version.DeletedTime, } } - metadata := reqres.RawSecretMetadataResponse{ + metadata := reqres.SecretRawMetadataResponse{ CurrentVersion: rawSecret.Metadata.CurrentVersion, OldestVersion: rawSecret.Metadata.OldestVersion, CreatedTime: rawSecret.Metadata.CreatedTime, @@ -152,8 +212,32 @@ func rawSecretResponseMapper(rawSecret *store.Secret) reqres.RawSecretResponse { MaxVersions: rawSecret.Metadata.MaxVersions, } - return reqres.RawSecretResponse{ + return reqres.SecretMetadataResponse{ Versions: versions, Metadata: metadata, } } + +func handleError(err error, w http.ResponseWriter) error { + if errors.Is(err, store.ErrSecretNotFound) { + log.Log().Info("routeGetSecret", "msg", "Secret not found") + + res := reqres.SecretReadResponse{Err: reqres.ErrNotFound} + responseBody := net.MarshalBody(res, w) + if responseBody == nil { + return errors.New("failed to marshal response body") + } + net.Respond(http.StatusNotFound, responseBody, w) + return nil + } + + log.Log().Info("routeGetSecret", "msg", "Failed to retrieve secret", "err", err) + responseBody := net.MarshalBody(reqres.SecretReadResponse{ + Err: "Internal server error"}, w, + ) + if responseBody == nil { + return errors.New("failed to marshal response body") + } + net.Respond(http.StatusInternalServerError, responseBody, w) + return err +} diff --git a/app/spike/internal/cmd/secret/get.go b/app/spike/internal/cmd/secret/get.go index fb77616..347418e 100644 --- a/app/spike/internal/cmd/secret/get.go +++ b/app/spike/internal/cmd/secret/get.go @@ -64,9 +64,8 @@ func newSecretGetCommand(source *workloadapi.X509Source) *cobra.Command { path := args[0] version, _ := cmd.Flags().GetInt("version") - metadata, _ := cmd.Flags().GetBool("metadata") - secret, err := store.GetSecret(source, path, version, metadata) + secret, err := store.GetSecret(source, path, version) if err != nil { fmt.Println("Error reading secret:", err.Error()) return @@ -77,18 +76,92 @@ func newSecretGetCommand(source *workloadapi.X509Source) *cobra.Command { return } - printSecretResponse(secret) + data := secret.Data + for k, v := range data { + fmt.Printf("%s: %s\n", k, v) + } }, } getCmd.Flags().IntP("version", "v", 0, "Specific version to retrieve") - getCmd.Flags().BoolP("metadata", "m", false, "Show metadata instead of secret values") - getCmd.Flags().Bool("versions", false, "List all versions of the secret") return getCmd } -func printSecretResponse(response *reqres.SecretReadResponse) { +// newSecretMetadataGetCommand creates and returns a new cobra.Command for retrieving +// secrets. It configures a command that fetches and displays secret data from a +// specified path. +// +// Parameters: +// - source: X.509 source for workload API authentication +// +// The command accepts a single argument: +// - path: Location of the secret to retrieve +// +// Flags: +// - --version, -v (int): Specific version of the secret to retrieve +// (default 0) where 0 represents the current version +// +// Returns: +// - *cobra.Command: Configured get command +// +// The command will: +// 1. Verify SPIKE initialization status via admin token +// 2. Retrieve the secret metadata from the specified path and version +// 3. Display all metadata fields and secret versions +// +// Error cases: +// - SPIKE not initialized: Prompts user to run 'spike init' +// - Secret not found: Displays appropriate message +// - Read errors: Displays error message +func newSecretMetadataGetCommand(source *workloadapi.X509Source) *cobra.Command { + getMetadataCmd := &cobra.Command{ + Use: "metadata", + Short: "Manage secrets", + } + var getCmd = &cobra.Command{ + Use: "get ", + Short: "Get secrets metadata from the specified path", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + state, err := auth.CheckInitState(source) + if err != nil { + fmt.Println("Failed to check initialization state:") + fmt.Println(err.Error()) + return + } + + if state == data.NotInitialized { + fmt.Println("Please initialize SPIKE first by running 'spike init'.") + return + } + + path := args[0] + version, _ := cmd.Flags().GetInt("version") + + secret, err := store.GetSecretMetadata(source, path, version) + if err != nil { + fmt.Println("Error reading secret:", err.Error()) + return + } + + if secret == nil { + fmt.Println("Secret not found.") + return + } + + printSecretResponse(secret) + }, + } + + getCmd.Flags().IntP("version", "v", 0, "Specific version to retrieve") + + getMetadataCmd.AddCommand(getCmd) + return getMetadataCmd +} + +// printSecretResponse prints secret metadata +func printSecretResponse(response *reqres.SecretMetadataResponse) { printSeparator := func() { fmt.Println(strings.Repeat("-", 50)) } @@ -97,16 +170,7 @@ func printSecretResponse(response *reqres.SecretReadResponse) { return t.Format("2006-01-02 15:04:05 MST") } - if len(response.Data) > 0 { - fmt.Println("Current Secret Data:") - printSeparator() - for k, v := range response.Data { - fmt.Printf("%-20s: %s\n", k, v) - } - printSeparator() - } - - if response.Metadata != (reqres.RawSecretMetadataResponse{}) { + if response.Metadata != (reqres.SecretRawMetadataResponse{}) { fmt.Println("\nMetadata:") printSeparator() fmt.Printf("Current Version : %d\n", response.Metadata.CurrentVersion) @@ -127,10 +191,6 @@ func printSecretResponse(response *reqres.SecretReadResponse) { if versionData.DeletedTime != nil { fmt.Printf(" Deleted: %s\n", formatTime(*versionData.DeletedTime)) } - fmt.Println(" Data:") - for k, v := range versionData.Data { - fmt.Printf(" %-18s: %s\n", k, v) - } printSeparator() } } diff --git a/app/spike/internal/cmd/secret/new.go b/app/spike/internal/cmd/secret/new.go index 2245910..2d0f82c 100644 --- a/app/spike/internal/cmd/secret/new.go +++ b/app/spike/internal/cmd/secret/new.go @@ -17,6 +17,7 @@ func NewSecretCommand(source *workloadapi.X509Source) *cobra.Command { cmd.AddCommand(newSecretUndeleteCommand(source)) cmd.AddCommand(newSecretListCommand(source)) cmd.AddCommand(newSecretGetCommand(source)) + cmd.AddCommand(newSecretMetadataGetCommand(source)) cmd.AddCommand(newSecretPutCommand(source)) return cmd diff --git a/app/spike/internal/net/api/secret.go b/app/spike/internal/net/api/secret.go index 566b7fc..3c56377 100644 --- a/app/spike/internal/net/api/secret.go +++ b/app/spike/internal/net/api/secret.go @@ -58,3 +58,14 @@ func UrlSecretList() string { params.Add(net.KeyApiAction, string(net.ActionNexusList)) return u + "?" + params.Encode() } + +// UrlSecretMetadataGet returns the URL for getting a secret metadata. +func UrlSecretMetadataGet() string { + u, _ := url.JoinPath( + env.NexusApiRoot(), + string(net.SpikeNexusUrlSecretsMetadata), + ) + params := url.Values{} + params.Add(net.KeyApiAction, string(net.ActionNexusGet)) + return u + "?" + params.Encode() +} diff --git a/app/spike/internal/net/store/get.go b/app/spike/internal/net/store/get.go index 960e95d..31af1e9 100644 --- a/app/spike/internal/net/store/get.go +++ b/app/spike/internal/net/store/get.go @@ -7,6 +7,7 @@ package store import ( "encoding/json" "errors" + "github.com/spiffe/spike/internal/entity/data" "github.com/spiffe/go-spiffe/v2/workloadapi" @@ -32,11 +33,10 @@ import ( // Example: // // secret, err := GetSecret(x509Source, "secret/path", 1) -func GetSecret(source *workloadapi.X509Source, path string, version int, metadata bool) (*reqres.SecretReadResponse, error) { +func GetSecret(source *workloadapi.X509Source, path string, version int) (*data.Secret, error) { r := reqres.SecretReadRequest{ - Path: path, - Version: version, - Metadata: metadata, + Path: path, + Version: version, } mr, err := json.Marshal(r) @@ -72,5 +72,63 @@ func GetSecret(source *workloadapi.X509Source, path string, version int, metadat return nil, errors.New(string(res.Err)) } + return &data.Secret{Data: res.Data}, nil +} + +// GetSecretMetadata retrieves a specific version of a secret metadata at the given path using +// mTLS authentication. +// +// Parameters: +// - source: X509Source for mTLS client authentication +// - path: Path to the secret to retrieve +// - version: Version number of the secret to retrieve +// +// Returns: +// - *Secret: Secret metadata if found, nil if secret not found +// - error: nil on success, unauthorized error if not logged in, or +// wrapped error on request/parsing failure +// +// Example: +// +// metadata, err := GetSecretMetadata(x509Source, "secret/path", 1) +func GetSecretMetadata(source *workloadapi.X509Source, path string, version int) (*reqres.SecretMetadataResponse, error) { + r := reqres.SecretReadRequest{ + Path: path, + Version: version, + } + + mr, err := json.Marshal(r) + if err != nil { + return nil, errors.Join( + errors.New("getSecret: I am having problem generating the payload"), + err, + ) + } + + client, err := net.CreateMtlsClient(source, auth.IsNexus) + if err != nil { + return nil, err + } + + body, err := net.Post(client, api.UrlSecretMetadataGet(), mr) + if err != nil { + if errors.Is(err, net.ErrNotFound) { + return nil, nil + } + return nil, err + } + + var res reqres.SecretMetadataResponse + err = json.Unmarshal(body, &res) + if err != nil { + return nil, errors.Join( + errors.New("getSecret: Problem parsing response body"), + err, + ) + } + if res.Err != "" { + return nil, errors.New(string(res.Err)) + } + return &res, nil } diff --git a/internal/entity/v1/reqres/secret.go b/internal/entity/v1/reqres/secret.go index 4934f23..9bd2a0d 100644 --- a/internal/entity/v1/reqres/secret.go +++ b/internal/entity/v1/reqres/secret.go @@ -31,17 +31,14 @@ type SecretPutResponse struct { // SecretReadRequest is for getting secrets type SecretReadRequest struct { - Path string `json:"path"` - Version int `json:"version,omitempty"` // Optional specific version - Metadata bool `json:"metadata,omitempty"` // Optional specific version + Path string `json:"path"` + Version int `json:"version,omitempty"` // Optional specific version } // SecretReadResponse is for getting secrets type SecretReadResponse struct { - Data map[string]string `json:"data"` - Versions map[int]RawSecretVersionResponse `json:"versions,omitempty"` - Metadata RawSecretMetadataResponse `json:"metadata,omitempty"` - Err ErrorCode `json:"err,omitempty"` + Data map[string]string `json:"data"` + Err ErrorCode `json:"err,omitempty"` } // SecretDeleteRequest for soft-deleting secret versions @@ -78,19 +75,28 @@ type SecretListResponse struct { Err ErrorCode `json:"err,omitempty"` } -type RawSecretResponse struct { - Versions map[int]RawSecretVersionResponse `json:"versions,omitempty"` - Metadata RawSecretMetadataResponse `json:"metadata,omitempty"` +// SecretMetadataReadRequest for get secrets metadata +type SecretMetadataReadRequest struct { + Path string `json:"path"` + Version int `json:"version,omitempty"` // Optional specific version +} + +// SecretMetadataResponse for secrets versions and metadata +type SecretMetadataResponse struct { + Versions map[int]SecretMetadataVersionResponse `json:"versions,omitempty"` + Metadata SecretRawMetadataResponse `json:"metadata,omitempty"` + Err ErrorCode `json:"err,omitempty"` } -type RawSecretVersionResponse struct { - Data map[string]string `json:"data"` - CreatedTime time.Time `json:"createdTime"` - Version int `json:"version"` - DeletedTime *time.Time `json:"deletedTime"` +// SecretMetadataVersionResponse for secrets version +type SecretMetadataVersionResponse struct { + CreatedTime time.Time `json:"createdTime"` + Version int `json:"version"` + DeletedTime *time.Time `json:"deletedTime"` } -type RawSecretMetadataResponse struct { +// SecretRawMetadataResponse for secrets raw metadata +type SecretRawMetadataResponse struct { CurrentVersion int `json:"currentVersion"` OldestVersion int `json:"oldestVersion"` CreatedTime time.Time `json:"createdTime"` diff --git a/internal/net/action.go b/internal/net/action.go index 772ded5..6f193bd 100644 --- a/internal/net/action.go +++ b/internal/net/action.go @@ -11,6 +11,7 @@ const KeyApiAction = "action" type ApiUrl string const SpikeNexusUrlSecrets ApiUrl = "/v1/store/secrets" +const SpikeNexusUrlSecretsMetadata ApiUrl = "/v1/store/secrets/metadata" const SpikeNexusUrlLogin ApiUrl = "/v1/auth/login" const SpikeNexusUrlInit ApiUrl = "/v1/auth/initialization" From fd46ae195018eb7327059d6de22c212c822c6115 Mon Sep 17 00:00:00 2001 From: sahinakyol Date: Wed, 27 Nov 2024 20:33:41 +0100 Subject: [PATCH 5/5] spike-46 move metadata url to new url.go --- internal/net/action.go | 2 -- internal/net/url.go | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/internal/net/action.go b/internal/net/action.go index fc764fd..5af8ee7 100644 --- a/internal/net/action.go +++ b/internal/net/action.go @@ -7,8 +7,6 @@ package net type SpikeNexusApiAction string const KeyApiAction = "action" -const SpikeNexusUrlSecretsMetadata ApiUrl = "/v1/store/secrets/metadata" - const ActionNexusCheck SpikeNexusApiAction = "check" const ActionNexusGet SpikeNexusApiAction = "get" const ActionNexusDelete SpikeNexusApiAction = "delete" diff --git a/internal/net/url.go b/internal/net/url.go index 80105f9..326af8c 100644 --- a/internal/net/url.go +++ b/internal/net/url.go @@ -7,9 +7,8 @@ package net type ApiUrl string const SpikeNexusUrlSecrets ApiUrl = "/v1/store/secrets" +const SpikeNexusUrlSecretsMetadata ApiUrl = "/v1/store/secrets/metadata" const SpikeNexusUrlLogin ApiUrl = "/v1/auth/login" const SpikeNexusUrlInit ApiUrl = "/v1/auth/initialization" - const SpikeNexusUrlPolicy ApiUrl = "/v1/acl/policy" - const SpikeKeeperUrlKeep ApiUrl = "/v1/store/keep"