Skip to content

Commit

Permalink
Merge pull request #51 from sahinakyol/spike-46
Browse files Browse the repository at this point in the history
Merging this in.

I'll do a damage assessment later :) 

The `main` branch is already broken anyway :) .
  • Loading branch information
v0lkan authored Dec 2, 2024
2 parents b48e0a5 + 1241274 commit dacbbfd
Show file tree
Hide file tree
Showing 12 changed files with 322 additions and 23 deletions.
2 changes: 2 additions & 0 deletions app/nexus/internal/route/base/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
145 changes: 145 additions & 0 deletions app/nexus/internal/route/store/secret/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,148 @@ func RouteGetSecret(
log.Log().Info("routeGetSecret", "msg", data.ErrSuccess)
return nil
}

// 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

requestBody := net.ReadRequestBody(w, r)
if requestBody == nil {
return errors.New("failed to read request body")
}

request := net.HandleRequest[
reqres.SecretReadRequest, reqres.SecretReadResponse](
requestBody, w,
reqres.SecretReadResponse{Err: data.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.StatusOK, responseBody, w)
log.Log().Info("routeGetSecret", "msg", "OK")
return nil
}

func rawSecretResponseMapper(rawSecret *store.Secret) reqres.SecretMetadataResponse {
versions := make(map[int]reqres.SecretMetadataVersionResponse)
for versionNum, version := range rawSecret.Versions {
versions[versionNum] = reqres.SecretMetadataVersionResponse{
CreatedTime: version.CreatedTime,
Version: version.Version,
DeletedTime: version.DeletedTime,
}
}

metadata := reqres.SecretRawMetadataResponse{
CurrentVersion: rawSecret.Metadata.CurrentVersion,
OldestVersion: rawSecret.Metadata.OldestVersion,
CreatedTime: rawSecret.Metadata.CreatedTime,
UpdatedTime: rawSecret.Metadata.UpdatedTime,
MaxVersions: rawSecret.Metadata.MaxVersions,
}

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: data.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
}
40 changes: 39 additions & 1 deletion app/nexus/internal/state/base/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -106,3 +109,38 @@ 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)
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
}
12 changes: 6 additions & 6 deletions app/spike/internal/cmd/secret/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,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
Expand All @@ -54,9 +54,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 := spike.CheckInitState(source)
Expand Down
116 changes: 116 additions & 0 deletions app/spike/internal/cmd/secret/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ package secret

import (
"fmt"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"strings"
"time"

"github.com/spf13/cobra"
"github.com/spiffe/go-spiffe/v2/workloadapi"
Expand Down Expand Up @@ -82,3 +85,116 @@ func newSecretGetCommand(source *workloadapi.X509Source) *cobra.Command {

return getCmd
}

// 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 <path>",
Short: "Get secrets metadata from the specified path",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
state, err := spike.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 := spike.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))
}

formatTime := func(t time.Time) string {
return t.Format("2006-01-02 15:04:05 MST")
}

if response.Metadata != (reqres.SecretRawMetadataResponse{}) {
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))
}
printSeparator()
}
}

if response.Err != "" {
fmt.Printf("\nError: %s\n", response.Err)
printSeparator()
}
}
1 change: 0 additions & 1 deletion app/spike/internal/cmd/secret/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package secret

import (
"fmt"

"github.com/spf13/cobra"
"github.com/spiffe/go-spiffe/v2/workloadapi"
spike "github.com/spiffe/spike-sdk-go/api"
Expand Down
1 change: 1 addition & 0 deletions app/spike/internal/cmd/secret/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions app/spike/internal/cmd/secret/put.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,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
Expand Down
Loading

0 comments on commit dacbbfd

Please sign in to comment.