Implement restore and recover APIs

Signed-off-by: Volkan Özçelik <me@volkan.io>
This commit is contained in:
Volkan Özçelik 2025-02-08 09:26:59 -08:00
parent 49acd4c191
commit b866ae7e9b
No known key found for this signature in database
GPG Key ID: FDA6FFBBC9465A7F
20 changed files with 302 additions and 53 deletions

View File

@ -10,7 +10,9 @@ import (
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/data"
impl "github.com/spiffe/spike-sdk-go/api/internal/impl/api"
"github.com/spiffe/spike-sdk-go/api/internal/impl/api/acl"
"github.com/spiffe/spike-sdk-go/api/internal/impl/api/operator"
"github.com/spiffe/spike-sdk-go/api/internal/impl/api/secret"
"github.com/spiffe/spike-sdk-go/spiffe"
)
@ -83,7 +85,7 @@ func (a *Api) CreatePolicy(
name string, spiffeIdPattern string, pathPattern string,
permissions []data.PolicyPermission,
) error {
return impl.CreatePolicy(a.source,
return acl.CreatePolicy(a.source,
name, spiffeIdPattern, pathPattern, permissions)
}
@ -107,7 +109,7 @@ func (a *Api) CreatePolicy(
// return
// }
func (a *Api) DeletePolicy(name string) error {
return impl.DeletePolicy(a.source, name)
return acl.DeletePolicy(a.source, name)
}
// GetPolicy retrieves a policy from the system using its Id.
@ -141,7 +143,7 @@ func (a *Api) DeletePolicy(name string) error {
//
// log.Printf("Found policy: %+v", policy)
func (a *Api) GetPolicy(name string) (*data.Policy, error) {
return impl.GetPolicy(a.source, name)
return acl.GetPolicy(a.source, name)
}
// ListPolicies retrieves all policies from the system.
@ -185,7 +187,7 @@ func (a *Api) GetPolicy(name string) (*data.Policy, error) {
// log.Printf("Found policy: %+v", policy)
// }
func (a *Api) ListPolicies() (*[]data.Policy, error) {
return impl.ListPolicies(a.source)
return acl.ListPolicies(a.source)
}
// DeleteSecretVersions deletes specified versions of a secret at the given
@ -207,7 +209,7 @@ func (a *Api) ListPolicies() (*[]data.Policy, error) {
//
// err := DeleteSecretVersions("secret/path", []string{"1", "2"})
func (a *Api) DeleteSecretVersions(path string, versions []int) error {
return impl.DeleteSecret(a.source, path, versions)
return secret.Delete(a.source, path, versions)
}
// DeleteSecret deletes specified secret at the given path
@ -226,9 +228,9 @@ func (a *Api) DeleteSecretVersions(path string, versions []int) error {
//
// Example:
//
// err := DeleteSecret("secret/path")
// err := Delete("secret/path")
func (a *Api) DeleteSecret(path string) error {
return impl.DeleteSecret(a.source, path, []int{})
return secret.Delete(a.source, path, []int{})
}
// GetSecretVersion retrieves a specific version of a secret at the given
@ -249,7 +251,7 @@ func (a *Api) DeleteSecret(path string) error {
func (a *Api) GetSecretVersion(
path string, version int,
) (*data.Secret, error) {
return impl.GetSecret(a.source, path, version)
return secret.Get(a.source, path, version)
}
// GetSecret retrieves the secret at the given path.
@ -264,9 +266,9 @@ func (a *Api) GetSecretVersion(
//
// Example:
//
// secret, err := GetSecret("secret/path")
// secret, err := Get("secret/path")
func (a *Api) GetSecret(path string) (*data.Secret, error) {
return impl.GetSecret(a.source, path, 0)
return secret.Get(a.source, path, 0)
}
// ListSecretKeys retrieves all secret keys.
@ -278,9 +280,9 @@ func (a *Api) GetSecret(path string) (*data.Secret, error) {
//
// Example:
//
// keys, err := ListSecretKeys()
// keys, err := ListKeys()
func (a *Api) ListSecretKeys() (*[]string, error) {
return impl.ListSecretKeys(a.source)
return secret.ListKeys(a.source)
}
// GetSecretMetadata retrieves a specific version of a secret metadata at the
@ -297,11 +299,11 @@ func (a *Api) ListSecretKeys() (*[]string, error) {
//
// Example:
//
// metadata, err := GetSecretMetadata("secret/path", 1)
// metadata, err := GetMetadata("secret/path", 1)
func (a *Api) GetSecretMetadata(
path string, version int,
) (*data.SecretMetadata, error) {
return impl.GetSecretMetadata(a.source, path, version)
return secret.GetMetadata(a.source, path, version)
}
// PutSecret creates or updates a secret at the specified path with the given
@ -317,9 +319,9 @@ func (a *Api) GetSecretMetadata(
//
// Example:
//
// err := PutSecret("secret/path", map[string]string{"key": "value"})
// err := Put("secret/path", map[string]string{"key": "value"})
func (a *Api) PutSecret(path string, data map[string]string) error {
return impl.PutSecret(a.source, path, data)
return secret.Put(a.source, path, data)
}
// UndeleteSecret restores previously deleted versions of a secret at the
@ -336,7 +338,47 @@ func (a *Api) PutSecret(path string, data map[string]string) error {
//
// Example:
//
// err := UndeleteSecret("secret/path", []string{"1", "2"})
// err := Undelete("secret/path", []string{"1", "2"})
func (a *Api) UndeleteSecret(path string, versions []int) error {
return impl.UndeleteSecret(a.source, path, versions)
return secret.Undelete(a.source, path, versions)
}
// Recover return recovery partitions for SPIKE Nexus to be used in a
// break-the-glass recovery operation if SPIKE Nexus auto-recovery mechanism
// isn't successful.
//
// The returned shared are sensitive and should be securely stored out-of-band
// in encrypted form.
//
// Returns:
// - []string: Array of recovery shards
// - error: nil on success, unauthorized error if not authorized, or
// wrapped error on request/parsing failure
//
// Example:
//
// shards, err := Recover()
func (a *Api) Recover() (*[]string, error) {
return operator.Recover(a.source)
}
// Restore SPIKE Nexus backing using recovery shards when SPIKE Keepers cannot
// provide adequate shards and SPIKE Nexus cannot recall its root key either.
//
// This is a break-the-glass superuser-only operation that a well-architected
// SPIKE deployment should not need.
//
// Parameters:
// - shard: the shared to seed.
//
// Returns:
// - *RestorationStatus: Status of the restoration process if successful
// - error: nil on success, unauthorized error if not authorized, or
// wrapped error on request/parsing failure
//
// Example:
//
// status, err := Restore("randomShardString")
func (a *Api) Restore(shard string) (*data.RestorationStatus, error) {
return operator.Restore(a.source, shard)
}

View File

@ -1,3 +1,7 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package data
import "time"

View File

@ -0,0 +1,11 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package data
type RestorationStatus struct {
ShardsCollected int `json:"collected"`
ShardsRemaining int `json:"remaining"`
Restored bool `json:"restored"`
}

View File

@ -6,15 +6,18 @@ package reqres
import "github.com/spiffe/spike-sdk-go/api/entity/data"
type RestoreRequest struct{}
type RestoreRequest struct {
Shard string `json:"shard"`
}
type RestoreResponse struct {
Shards []string `json:"shards"`
Err data.ErrorCode `json:"err,omitempty"`
data.RestorationStatus
Err data.ErrorCode `json:"err,omitempty"`
}
type RecoverRequest struct {
Shard string `json:"shard"`
}
type RecoverResponse struct {
Err data.ErrorCode `json:"err,omitempty"`
Shards []string `json:"shards"`
Err data.ErrorCode `json:"err,omitempty"`
}

View File

@ -2,7 +2,7 @@
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package api
package acl
import (
"encoding/json"

View File

@ -2,7 +2,7 @@
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package api
package acl
import (
"encoding/json"

View File

@ -2,7 +2,7 @@
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package api
package acl
import (
"encoding/json"
@ -56,7 +56,9 @@ import (
// }
//
// log.Printf("Found policy: %+v", policy)
func GetPolicy(source *workloadapi.X509Source, id string) (*data.Policy, error) {
func GetPolicy(
source *workloadapi.X509Source, id string,
) (*data.Policy, error) {
r := reqres.PolicyReadRequest{Id: id}
mr, err := json.Marshal(r)

View File

@ -2,7 +2,7 @@
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package api
package acl
import (
"encoding/json"

View File

@ -0,0 +1,74 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package operator
import (
"encoding/json"
"errors"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/internal/url"
"github.com/spiffe/spike-sdk-go/net"
)
// Recover makes a request to initiate recovery of secrets, returning the
// recovery shards.
//
// Parameters:
// - source: X509Source used for mTLS client authentication
//
// Returns:
// - *[]string: Array of recovery shard identifiers if successful, nil if
// not found
// - error: nil on success, error if:
// - Failed to marshal recover request
// - Failed to create mTLS client
// - Request failed (except for not found case)
// - Failed to parse response body
// - Server returned error in response
//
// Example:
//
// shards, err := Recover(x509Source)
func Recover(source *workloadapi.X509Source) (*[]string, error) {
r := reqres.RecoverRequest{}
mr, err := json.Marshal(r)
if err != nil {
return nil, errors.Join(
errors.New("recover: failed to marshal recover request"),
err,
)
}
client, err := net.CreateMtlsClient(source)
if err != nil {
return nil, err
}
body, err := net.Post(client, url.Recover(), mr)
if err != nil {
if errors.Is(err, net.ErrNotFound) {
return nil, nil
}
return nil, err
}
var res reqres.RecoverResponse
err = json.Unmarshal(body, &res)
if err != nil {
return nil, errors.Join(
errors.New("recover: Problem parsing response body"),
err,
)
}
if res.Err != "" {
return nil, errors.New(string(res.Err))
}
return &res.Shards, nil
}

View File

@ -0,0 +1,81 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package operator
import (
"encoding/json"
"errors"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/data"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/internal/url"
"github.com/spiffe/spike-sdk-go/net"
)
// Restore submits a recovery shard to continue the restoration process.
//
// Parameters:
// - source: X509Source used for mTLS client authentication
// - shard: Recovery shard identifier to submit
//
// Returns:
// - *RestorationStatus: Status containing shards collected, remaining, and
// restoration state if successful, nil if not found
// - error: nil on success, error if:
// - Failed to marshal restore request
// - Failed to create mTLS client
// - Request failed (except for not found case)
// - Failed to parse response body
// - Server returned error in response
//
// Example:
//
// status, err := Restore(x509Source, "randomshardentry")
func Restore(
source *workloadapi.X509Source, shard string,
) (*data.RestorationStatus, error) {
r := reqres.RestoreRequest{Shard: shard}
mr, err := json.Marshal(r)
if err != nil {
return nil, errors.Join(
errors.New("restore: failed to marshal recover request"),
err,
)
}
client, err := net.CreateMtlsClient(source)
if err != nil {
return nil, err
}
body, err := net.Post(client, url.Restore(), mr)
if err != nil {
if errors.Is(err, net.ErrNotFound) {
return nil, nil
}
return nil, err
}
var res reqres.RestoreResponse
err = json.Unmarshal(body, &res)
if err != nil {
return nil, errors.Join(
errors.New("recover: Problem parsing response body"),
err,
)
}
if res.Err != "" {
return nil, errors.New(string(res.Err))
}
return &data.RestorationStatus{
ShardsCollected: res.ShardsCollected,
ShardsRemaining: res.ShardsRemaining,
Restored: res.Restored,
}, nil
}

View File

@ -2,7 +2,7 @@
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package api
package secret
import (
"encoding/json"
@ -15,7 +15,7 @@ import (
"github.com/spiffe/spike-sdk-go/net"
)
// DeleteSecret deletes specified versions of a secret at the given path using
// Delete deletes specified versions of a secret at the given path using
// mTLS authentication.
//
// It converts string version numbers to integers, constructs a delete request,
@ -34,7 +34,7 @@ import (
// Example:
//
// err := deleteSecret(x509Source, "secret/path", []string{"1", "2"})
func DeleteSecret(source *workloadapi.X509Source,
func Delete(source *workloadapi.X509Source,
path string, versions []int) error {
r := reqres.SecretDeleteRequest{
Path: path,

View File

@ -2,7 +2,7 @@
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package api
package secret
import (
"encoding/json"
@ -17,7 +17,7 @@ import (
"github.com/spiffe/spike-sdk-go/net"
)
// GetSecret retrieves a specific version of a secret at the given path using
// Get retrieves a specific version of a secret at the given path using
// mTLS authentication.
//
// Parameters:
@ -33,7 +33,7 @@ import (
// Example:
//
// secret, err := getSecret(x509Source, "secret/path", 1)
func GetSecret(source *workloadapi.X509Source,
func Get(source *workloadapi.X509Source,
path string, version int) (*data.Secret, error) {
r := reqres.SecretReadRequest{
Path: path,

View File

@ -2,7 +2,7 @@
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package api
package secret
import (
"encoding/json"
@ -15,7 +15,7 @@ import (
"github.com/spiffe/spike-sdk-go/net"
)
// ListSecretKeys retrieves all secret keys using mTLS authentication.
// ListKeys retrieves all secret keys using mTLS authentication.
//
// Parameters:
// - source: X509Source for mTLS client authentication
@ -28,7 +28,7 @@ import (
// Example:
//
// keys, err := listSecretKeys(x509Source)
func ListSecretKeys(source *workloadapi.X509Source) (*[]string, error) {
func ListKeys(source *workloadapi.X509Source) (*[]string, error) {
r := reqres.SecretListRequest{}
mr, err := json.Marshal(r)
if err != nil {

View File

@ -2,7 +2,7 @@
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package api
package secret
import (
"encoding/json"
@ -16,7 +16,7 @@ import (
"github.com/spiffe/spike-sdk-go/net"
)
// GetSecretMetadata retrieves a specific version of a secret metadata at the
// GetMetadata retrieves a specific version of a secret metadata at the
// given path using mTLS authentication.
//
// Parameters:
@ -32,7 +32,7 @@ import (
// Example:
//
// metadata, err := getSecretMetadata(x509Source, "secret/path", 1)
func GetSecretMetadata(
func GetMetadata(
source *workloadapi.X509Source, path string, version int,
) (*data.SecretMetadata, error) {
r := reqres.SecretMetadataRequest{

View File

@ -2,7 +2,7 @@
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package api
package secret
import (
"encoding/json"
@ -15,7 +15,7 @@ import (
"github.com/spiffe/spike-sdk-go/net"
)
// PutSecret creates or updates a secret at the specified path with the given
// Put creates or updates a secret at the specified path with the given
// values using mTLS authentication.
//
// Parameters:
@ -29,8 +29,9 @@ import (
//
// Example:
//
// err := putSecret(x509Source, "secret/path", map[string]string{"key": "value"})
func PutSecret(source *workloadapi.X509Source,
// err := putSecret(x509Source, "secret/path",
// map[string]string{"key": "value"})
func Put(source *workloadapi.X509Source,
path string, values map[string]string) error {
r := reqres.SecretPutRequest{

View File

@ -2,7 +2,7 @@
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package api
package secret
import (
"encoding/json"
@ -15,7 +15,7 @@ import (
"github.com/spiffe/spike-sdk-go/net"
)
// UndeleteSecret restores previously deleted versions of a secret at the
// Undelete restores previously deleted versions of a secret at the
// specified path using mTLS authentication.
//
// Parameters:
@ -31,7 +31,7 @@ import (
// Example:
//
// err := undeleteSecret(x509Source, "secret/path", []string{"1", "2"})
func UndeleteSecret(source *workloadapi.X509Source,
func Undelete(source *workloadapi.X509Source,
path string, versions []int) error {
var vv []int
if len(versions) == 0 {

View File

@ -28,4 +28,7 @@ const spikeNexusUrlInit ApiUrl = "/v1/auth/initialization"
const spikeNexusUrlPolicy ApiUrl = "/v1/acl/policy"
const spikeNexusUrlRecover ApiUrl = "/v1/operator/recover"
const spikeNexusUrlRestore ApiUrl = "/v1/operator/restore"
const spikeKeeperUrlKeep ApiUrl = "/v1/store/keep"

View File

@ -0,0 +1,27 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package url
import (
"net/url"
"github.com/spiffe/spike-sdk-go/api/internal/env"
)
func Restore() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
string(spikeNexusUrlRestore),
)
return u
}
func Recover() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
string(spikeNexusUrlRecover),
)
return u
}

View File

@ -10,7 +10,7 @@ import (
"github.com/spiffe/spike-sdk-go/api/internal/env"
)
// UrlPolicyCreate returns the URL for creating a policy.
// PolicyCreate returns the URL for creating a policy.
func PolicyCreate() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
@ -19,7 +19,7 @@ func PolicyCreate() string {
return u
}
// UrlPolicyList returns the URL for listing policies.
// PolicyList returns the URL for listing policies.
func PolicyList() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
@ -30,7 +30,7 @@ func PolicyList() string {
return u + "?" + params.Encode()
}
// UrlPolicyDelete returns the URL for deleting a policy.
// PolicyDelete returns the URL for deleting a policy.
func PolicyDelete() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
@ -41,7 +41,7 @@ func PolicyDelete() string {
return u + "?" + params.Encode()
}
// UrlPolicyGet returns the URL for getting a policy.
// PolicyGet returns the URL for getting a policy.
func PolicyGet() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),

View File

@ -1,9 +1,10 @@
package validation
import (
"regexp"
"github.com/google/uuid"
"github.com/spiffe/spike-sdk-go/api/entity/data"
"regexp"
"github.com/spiffe/spike-sdk-go/api/errors"
)