Compare commits
118 Commits
Author | SHA1 | Date |
---|---|---|
|
e7312f630a | |
|
3ec883d85a | |
|
6f50ebcc3b | |
|
7943a77e76 | |
|
fa7c888c1e | |
|
73f9090a5c | |
|
571d7a5c49 | |
|
6da43b2e92 | |
|
a13ecac1c0 | |
|
bbf51edf17 | |
|
9da8df0d37 | |
|
052208495d | |
|
bc5f5deafa | |
|
80803711cd | |
|
6879e44a62 | |
|
22f368c29c | |
|
fe4b7df59e | |
|
ed499a14d8 | |
|
5a94c7e59f | |
|
1df225b23d | |
|
5627fc6f30 | |
|
75d43543c1 | |
|
e8284b82bc | |
|
053e17ca09 | |
|
4bacaf4b5f | |
|
627bef8961 | |
|
2d73c0cea4 | |
|
b5fb33ad3c | |
|
1433709715 | |
|
bd50d02a9d | |
|
0a1ba6863d | |
|
50d077fec8 | |
|
570854bd99 | |
|
59174ca488 | |
|
d54056301a | |
|
f7cb455150 | |
|
68f6ca21b4 | |
|
44b9b3771c | |
|
2329d7fee8 | |
|
26308b23eb | |
|
335763b4b4 | |
|
c1ebd57c90 | |
|
b94f65e01d | |
|
a51ef1e36f | |
|
1862e854be | |
|
63c9ad76de | |
|
58e8662189 | |
|
66a7676eb5 | |
|
1feb7575eb | |
|
cafe36b8f6 | |
|
2f90c1325f | |
|
1bef34085e | |
|
110a67c829 | |
|
5e28bf4f11 | |
|
aeda8afdb6 | |
|
249bfff41c | |
|
decbd0c390 | |
|
ff7712c564 | |
|
0cbe642783 | |
|
244915b014 | |
|
b866ae7e9b | |
|
49acd4c191 | |
|
cc83f688b3 | |
|
c0f0918512 | |
|
293190de44 | |
|
5b2663479f | |
|
2b3aa80911 | |
|
c3b9a9f981 | |
|
7476e82c2a | |
|
8fd819d261 | |
|
4e93332327 | |
|
cc30d8fe5f | |
|
c0f910e50e | |
|
59f882ecbb | |
|
0d91ae5804 | |
|
0ef0af7351 | |
|
4afae14805 | |
|
625420f215 | |
|
b98ec8ee62 | |
|
95488908fb | |
|
5781a822b2 | |
|
aa0ec7f413 | |
|
035c0dd482 | |
|
201c41457c | |
|
72410e9e65 | |
|
b813778ec1 | |
|
bed86e5836 | |
|
5fc0273ed2 | |
|
0bac4107a0 | |
|
b5ee191ad0 | |
|
c9162e1867 | |
|
866efed7b4 | |
|
1fe2d43784 | |
|
68386cfe0c | |
|
843d7c2944 | |
|
128d88d2ea | |
|
d97a741aba | |
|
4a6d012fb0 | |
|
dd61ea4367 | |
|
0a462d13d7 | |
|
ccfdb6bc27 | |
|
b63a22537c | |
|
d89682d007 | |
|
a7c30300d9 | |
|
338675da46 | |
|
ceaec294ce | |
|
e0c63a296a | |
|
5de64b22ad | |
|
62296b93b1 | |
|
e4c83b032b | |
|
d3924614f0 | |
|
a94ed4837b | |
|
f32431b542 | |
|
1845d15e45 | |
|
9709d97fec | |
|
c0ffa728f1 | |
|
0eea713b69 | |
|
49efec7dd7 |
|
@ -0,0 +1,12 @@
|
||||||
|
# \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
# \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
# \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
# Lines starting with '#' are comments.
|
||||||
|
# Each line is a file pattern followed by one or more owners.
|
||||||
|
# More details are here: https://help.github.com/articles/about-codeowners/
|
||||||
|
|
||||||
|
## Code Owners (in no particular order) ##
|
||||||
|
|
||||||
|
* @v0lkan # Volkan Özçelik
|
||||||
|
* @kfox1111 # Kevin Fox
|
|
@ -0,0 +1,14 @@
|
||||||
|

|
||||||
|
|
||||||
|
This repository follows the [SPIFFE Code of Conduct][coc].
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
[coc]: https://github.com/spiffe/spiffe/blob/main/CODE-OF-CONDUCT.md
|
||||||
|
|
||||||
|
As a quick summary, while contributing to this repository, and engaging with
|
||||||
|
the SPIFFE community, **be a good citizen**, **be inclusive**, and
|
||||||
|
**be respectful**.
|
||||||
|
|
||||||
|
For details, please refer to the [SPIFFE Code of Conduct][coc].
|
|
@ -0,0 +1,31 @@
|
||||||
|

|
||||||
|
|
||||||
|
## Welcome
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to **SPIKE** 🤘.
|
||||||
|
|
||||||
|
We appreciate any help, be it in the form of code, documentation, design,
|
||||||
|
or even bug reports and feature requests.
|
||||||
|
|
||||||
|
When contributing to this repository, please first discuss the change you wish
|
||||||
|
to make via an issue, email, or any other method before making a change.
|
||||||
|
This way, we can avoid misunderstandings and wasted effort.
|
||||||
|
|
||||||
|
One great way to initiate such discussions is asking a question
|
||||||
|
[SPIFFE Slack Community][slack].
|
||||||
|
|
||||||
|
[slack]: https://slack.spiffe.io/ "Join SPIFFE on Slack"
|
||||||
|
|
||||||
|
Please note that [we have a code of conduct](CODE_OF_CONDUCT.md). We expect all
|
||||||
|
contributors to adhere to it in all interactions with the project.
|
||||||
|
|
||||||
|
Also make sure you read, understand and accept
|
||||||
|
[The Developer Certificate of Origin Contribution Guide](CONTRIBUTING_DCO.md)
|
||||||
|
as it is a requirement to contribute to this project and contains more details
|
||||||
|
about the contribution process.
|
||||||
|
|
||||||
|
## How To Run Tests
|
||||||
|
|
||||||
|
Before merging your changes, make sure all tests pass.
|
||||||
|
|
||||||
|
Turn test the SDK locally, run `go test ./...` on the project root.
|
|
@ -0,0 +1,114 @@
|
||||||
|

|
||||||
|
|
||||||
|
## Contributing to SPIKE
|
||||||
|
|
||||||
|
We welcome contributions from the community and first want to thank you for
|
||||||
|
taking the time to contribute!
|
||||||
|
|
||||||
|
Please familiarize yourself with our [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||||
|
before contributing.
|
||||||
|
|
||||||
|
Before you start working with SPIKE, please read our
|
||||||
|
[Developer Certificate of Origin](CONTRIBUTING_DCO.md). All contributions
|
||||||
|
to this repository must be signed as described on that page. Your signature
|
||||||
|
certifies that you wrote the patch or have the right to pass it on as an
|
||||||
|
open-source patch.
|
||||||
|
|
||||||
|
We appreciate any help, be it in the form of code, documentation, design,
|
||||||
|
or even bug reports and feature requests.
|
||||||
|
|
||||||
|
When contributing to this repository, please first discuss the change you wish
|
||||||
|
to make via an issue, email, or any other method before making a change.
|
||||||
|
This way, we can avoid misunderstandings and wasted effort.
|
||||||
|
|
||||||
|
One great way to initiate such discussions is asking a question
|
||||||
|
[SPIFFE Slack Community][slack].
|
||||||
|
|
||||||
|
[slack]: https://slack.spiffe.io/ "Join SPIFFE on Slack"
|
||||||
|
|
||||||
|
Please note that [we have a code of conduct](CODE_OF_CONDUCT.md). We expect all
|
||||||
|
contributors to adhere to it in all interactions with the project.
|
||||||
|
|
||||||
|
## Ways to contribute
|
||||||
|
|
||||||
|
We welcome many different types of contributions and not all of them need a
|
||||||
|
Pull request. Contributions may include:
|
||||||
|
|
||||||
|
* New features and proposals
|
||||||
|
* Documentation
|
||||||
|
* Bug fixes
|
||||||
|
* Issue Triage
|
||||||
|
* Answering questions and giving feedback
|
||||||
|
* Helping to onboard new contributors
|
||||||
|
* Other related activities
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
Please [quickstart guide][use-the-source] to learn how to build, deploy, and
|
||||||
|
test **SPIKE** from the source.
|
||||||
|
|
||||||
|
[use-the-source]: https://spike.ist/#/quickstart
|
||||||
|
|
||||||
|
The quickstart guide also includes common errors that you might find when
|
||||||
|
building, deploying, and testing **SPIKE**.
|
||||||
|
|
||||||
|
## Contribution Flow
|
||||||
|
|
||||||
|
This is a rough outline of what a contributor's workflow looks like:
|
||||||
|
|
||||||
|
* Make a fork of the repository within your GitHub account.
|
||||||
|
* Create a topic branch in your fork from where you want to base your work
|
||||||
|
* Make commits of logical units.
|
||||||
|
* Make sure your commit messages are with the proper format,
|
||||||
|
quality and descriptiveness (*see below*)
|
||||||
|
* Adhere to the code standards described below.
|
||||||
|
* Push your changes to the topic branch in your fork
|
||||||
|
* Ensure all components build and function properly.
|
||||||
|
* Update necessary `README.md` and other documents to reflect your changes.
|
||||||
|
* Keep pull requests as granular as possible. Reviewing large amounts of code
|
||||||
|
can be error-prone and time-consuming for the reviewers.
|
||||||
|
* Create a pull request containing that commit.
|
||||||
|
* Engage in the discussion under the pull request and proceed accordingly.
|
||||||
|
|
||||||
|
## Pull Request Checklist
|
||||||
|
|
||||||
|
Before submitting your pull request, we advise you to use the following:
|
||||||
|
|
||||||
|
1. Check if your code changes will pass local tests
|
||||||
|
(*i.e., `go test ./...` should exit with a `0` success status code*).
|
||||||
|
2. Ensure your commit messages are descriptive. We follow the conventions
|
||||||
|
on [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/).
|
||||||
|
Be sure to include any related GitHub issue references in the commit message.
|
||||||
|
See [GFM syntax](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown)
|
||||||
|
for referencing issues and commits.
|
||||||
|
3. Check the commits and commits messages and ensure they are free from typos.
|
||||||
|
|
||||||
|
## Reporting Bugs and Creating Issues
|
||||||
|
|
||||||
|
For specifics on what to include in your report, please follow the guidelines
|
||||||
|
in the issue and pull request templates when available.
|
||||||
|
|
||||||
|
## Ask for Help
|
||||||
|
|
||||||
|
The best way to reach us with a question when contributing is to ask on:
|
||||||
|
|
||||||
|
* The original GitHub issue
|
||||||
|
* [**SPIFFE Slack Workspace**][slack-invite]
|
||||||
|
|
||||||
|
### Code Standards
|
||||||
|
|
||||||
|
In **SPIKE**, we aim for a unified and clean codebase.
|
||||||
|
|
||||||
|
When contributing, please try to match the style of the code that you see in
|
||||||
|
the file you're working on. The file should look as if it was authored by a
|
||||||
|
single person after your changes.
|
||||||
|
|
||||||
|
For Go files, we require that you run `gofmt` before submitting your pull
|
||||||
|
request to ensure consistent formatting.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Before submitting your pull request, make sure your changes pass all the
|
||||||
|
existing tests, and add new ones if necessary.
|
||||||
|
|
||||||
|
[slack-invite]: https://slack.spiffe.io/ "Join SPIFFE Slack"
|
|
@ -0,0 +1,3 @@
|
||||||
|

|
||||||
|
|
||||||
|
See [`CODEOWNERS`](CODEOWNERS).
|
85
README.md
85
README.md
|
@ -23,6 +23,87 @@ Prerequisites:
|
||||||
2. `SPIFFE_ENDPOINT_SOCKET` environment variable set to address of the Workload
|
2. `SPIFFE_ENDPOINT_SOCKET` environment variable set to address of the Workload
|
||||||
API (e.g. `unix:///tmp/agent.sock`).
|
API (e.g. `unix:///tmp/agent.sock`).
|
||||||
|
|
||||||
// TODO: add more details and an example here.
|
## Usage Example
|
||||||
|
|
||||||
// TODO: add more documents COC, contributing, etc.
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
spike "github.com/spiffe/spike-sdk-go/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
api := spike.New() // Use the default Workload API Socket
|
||||||
|
defer api.Close() // Close the connection when done
|
||||||
|
|
||||||
|
path := "/tenants/demo/db/creds"
|
||||||
|
|
||||||
|
// Create a Secret
|
||||||
|
err := api.PutSecret(path, map[string]string{
|
||||||
|
"username": "SPIKE",
|
||||||
|
"password": "SPIKE_Rocks",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error writing secret:", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the Secret
|
||||||
|
secret, err := api.GetSecret(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error reading secret:", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if secret == nil {
|
||||||
|
fmt.Println("Secret not found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Secret found:")
|
||||||
|
|
||||||
|
data := secret.Data
|
||||||
|
for k, v := range data {
|
||||||
|
fmt.Printf("%s: %s\n", k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## A Note on Security
|
||||||
|
|
||||||
|
We take **SPIKE**'s security seriously. If you believe you have
|
||||||
|
found a vulnerability, please responsibily disclose it to
|
||||||
|
[security@spike.ist](mailto:security@spike.ist).
|
||||||
|
|
||||||
|
See [SECURITY.md](SECURITY.md) for additional details.
|
||||||
|
|
||||||
|
## Community
|
||||||
|
|
||||||
|
Open Source is better together.
|
||||||
|
|
||||||
|
If you are a security enthusiast, [join SPIFFE's Slack Workspace][spiffe-slack]
|
||||||
|
and let us change the world together 🤘.
|
||||||
|
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
To contribute to **SPIKE**, [follow the contributing
|
||||||
|
guidelines](CONTRIBUTING.md) to get started.
|
||||||
|
|
||||||
|
Use GitHub issues to request features or file bugs.
|
||||||
|
|
||||||
|
## Communications
|
||||||
|
|
||||||
|
* [SPIFFE **Slack** is where the community hangs out][spiffe-slack].
|
||||||
|
* [Send comments and suggestions to
|
||||||
|
**feedback@spike.ist**](mailto:feedback@spike.ist).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[Mozilla Public License v2.0](LICENSE).
|
||||||
|
|
||||||
|
[spiffe-slack]: https://slack.spiffe.io/
|
||||||
|
[spiffe]: https://spiffe.io/
|
||||||
|
[spike]: https://spike.ist/
|
||||||
|
[quickstart]: https://spike.ist/#/quickstart
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|

|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
This document outlines the security policy and procedures for reporting
|
||||||
|
security vulnerabilities in **SPIKE**, along with the
|
||||||
|
version support policy.
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Only the most recent version of **SPIKE** is currently being supported with
|
||||||
|
security updates.
|
||||||
|
|
||||||
|
Note that **SPIKE** consists of more than a single component, and during a
|
||||||
|
release cut, all components are signed and tagged with the same version.
|
||||||
|
|
||||||
|
After **SPIKE** hits a major 1.0.0. version, this will change,
|
||||||
|
and we will also have a support plan various major versions.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Send your vulnerability reports to [security@spike.ist](mailto:spike.ist).
|
||||||
|
|
||||||
|
We don't have an official turnover time, but if nobody gets back
|
||||||
|
to you within a week please send another email.
|
||||||
|
|
||||||
|
We take all vulnerability reports seriously, and you will be notified
|
||||||
|
if your report is accepted or declined, and what further actions we are going
|
||||||
|
to take on it.
|
|
@ -1,62 +0,0 @@
|
||||||
// \\ SPIKE: Secure your secrets with SPIFFE.
|
|
||||||
// \\\\\ Copyright 2024-present SPIKE contributors.
|
|
||||||
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
|
||||||
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/entity"
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/entity/v1/reqres"
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/url"
|
|
||||||
"github.com/spiffe/spike-sdk-go/net"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CreatePolicy(source *workloadapi.X509Source,
|
|
||||||
name string, spiffeIdPattern string, pathPattern string,
|
|
||||||
permissions []entity.PolicyPermission,
|
|
||||||
) error {
|
|
||||||
r := reqres.PolicyCreateRequest{
|
|
||||||
Name: name,
|
|
||||||
SpiffeIdPattern: spiffeIdPattern,
|
|
||||||
PathPattern: pathPattern,
|
|
||||||
Permissions: permissions,
|
|
||||||
}
|
|
||||||
|
|
||||||
mr, err := json.Marshal(r)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Join(
|
|
||||||
errors.New("createPolicy: I am having problem generating the payload"),
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var truer = func(string) bool { return true }
|
|
||||||
client, err := net.CreateMtlsClient(source, truer)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := net.Post(client, url.PolicyCreate(), mr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
res := reqres.PolicyCreateResponse{}
|
|
||||||
err = json.Unmarshal(body, &res)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Join(
|
|
||||||
errors.New("createPolicy: Problem parsing response body"),
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if res.Err != "" {
|
|
||||||
return errors.New(string(res.Err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
package api
|
|
||||||
|
|
||||||
// \\ SPIKE: Secure your secrets with SPIFFE.
|
|
||||||
// \\\\\ Copyright 2024-present SPIKE contributors.
|
|
||||||
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
|
||||||
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/entity/v1/reqres"
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/url"
|
|
||||||
"github.com/spiffe/spike-sdk-go/net"
|
|
||||||
)
|
|
||||||
|
|
||||||
func DeletePolicy(source *workloadapi.X509Source, id string) error {
|
|
||||||
r := reqres.PolicyDeleteRequest{
|
|
||||||
Id: id,
|
|
||||||
}
|
|
||||||
|
|
||||||
mr, err := json.Marshal(r)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Join(
|
|
||||||
errors.New("deletePolicy: I am having problem generating the payload"),
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var truer = func(string) bool { return true }
|
|
||||||
client, err := net.CreateMtlsClient(source, truer)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := net.Post(client, url.PolicyDelete(), mr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
res := reqres.PolicyDeleteResponse{}
|
|
||||||
err = json.Unmarshal(body, &res)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Join(
|
|
||||||
errors.New("deletePolicy: Problem parsing response body"),
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if res.Err != "" {
|
|
||||||
return errors.New(string(res.Err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
// \\ SPIKE: Secure your secrets with SPIFFE.
|
|
||||||
// \\\\\ Copyright 2024-present SPIKE contributors.
|
|
||||||
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
|
||||||
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/entity"
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/entity/v1/reqres"
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/url"
|
|
||||||
"github.com/spiffe/spike-sdk-go/net"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetPolicy(source *workloadapi.X509Source, id string) (*entity.Policy, error) {
|
|
||||||
r := reqres.PolicyReadRequest{Id: id}
|
|
||||||
|
|
||||||
mr, err := json.Marshal(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Join(
|
|
||||||
errors.New("getPolicy: I am having problem generating the payload"),
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var truer = func(string) bool { return true }
|
|
||||||
client, err := net.CreateMtlsClient(source, truer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := net.Post(client, url.PolicyGet(), mr)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, net.ErrNotFound) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var res reqres.PolicyReadResponse
|
|
||||||
err = json.Unmarshal(body, &res)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Join(
|
|
||||||
errors.New("getPolicy: Problem parsing response body"),
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if res.Err != "" {
|
|
||||||
return nil, errors.New(string(res.Err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return &res.Policy, nil
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
// \\ SPIKE: Secure your secrets with SPIFFE.
|
|
||||||
// \\\\\ Copyright 2024-present SPIKE contributors.
|
|
||||||
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
|
||||||
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/entity"
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/entity/v1/reqres"
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/url"
|
|
||||||
"github.com/spiffe/spike-sdk-go/net"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ListPolicies(source *workloadapi.X509Source) (*[]entity.Policy, error) {
|
|
||||||
r := reqres.PolicyListRequest{}
|
|
||||||
mr, err := json.Marshal(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Join(
|
|
||||||
errors.New("listPolicies: I am having problem generating the payload"),
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var truer = func(string) bool { return true }
|
|
||||||
client, err := net.CreateMtlsClient(source, truer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := net.Post(client, url.PolicyList(), mr)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, net.ErrNotFound) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var res reqres.PolicyListResponse
|
|
||||||
err = json.Unmarshal(body, &res)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Join(
|
|
||||||
errors.New("getPolicy: Problem parsing response body"),
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if res.Err != "" {
|
|
||||||
return nil, errors.New(string(res.Err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return &res.Policies, nil
|
|
||||||
}
|
|
|
@ -0,0 +1,373 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
||||||
|
|
||||||
|
"github.com/spiffe/spike-sdk-go/api/entity/data"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Api is the SPIKE API.
|
||||||
|
type Api struct {
|
||||||
|
source *workloadapi.X509Source
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates and returns a new instance of Api configured with a SPIFFE source.
|
||||||
|
func New() *Api {
|
||||||
|
defaultEndpointSocket := spiffe.EndpointSocket()
|
||||||
|
|
||||||
|
source, _, err := spiffe.Source(context.Background(), defaultEndpointSocket)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Api{source: source}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWithSource initializes a new Api instance with the given X509Source.
|
||||||
|
func NewWithSource(source *workloadapi.X509Source) *Api {
|
||||||
|
return &Api{source: source}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close releases any resources held by the Api instance.
|
||||||
|
// It ensures proper cleanup of the underlying source.
|
||||||
|
func (a *Api) Close() {
|
||||||
|
spiffe.CloseSource(a.source)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePolicy creates a new policy in the system. It establishes a mutual
|
||||||
|
// TLS connection using the X.509 source and sends a policy creation request
|
||||||
|
// to the server.
|
||||||
|
//
|
||||||
|
// The function takes the following parameters:
|
||||||
|
// - name string: The name of the policy to be created
|
||||||
|
// - spiffeIdPattern string: The SPIFFE ID pattern that this policy will apply
|
||||||
|
// to
|
||||||
|
// - pathPattern string: The path pattern that this policy will match against
|
||||||
|
// - permissions []data.PolicyPermission: A slice of PolicyPermission defining
|
||||||
|
// the access rights for this policy
|
||||||
|
//
|
||||||
|
// The function returns an error if any of the following operations fail:
|
||||||
|
// - Marshaling the policy creation request
|
||||||
|
// - Creating the mTLS client
|
||||||
|
// - Making the HTTP POST request
|
||||||
|
// - Unmarshaling the response
|
||||||
|
// - Server-side policy creation (indicated in the response)
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// permissions := []data.PolicyPermission{
|
||||||
|
// {
|
||||||
|
// Action: "read",
|
||||||
|
// Resource: "documents/*",
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// err = api.CreatePolicy(
|
||||||
|
// "doc-reader",
|
||||||
|
// "spiffe://example.org/service/*",
|
||||||
|
// "/api/documents/*",
|
||||||
|
// permissions,
|
||||||
|
// )
|
||||||
|
// if err != nil {
|
||||||
|
// log.Printf("Failed to create policy: %v", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
func (a *Api) CreatePolicy(
|
||||||
|
name string, spiffeIdPattern string, pathPattern string,
|
||||||
|
permissions []data.PolicyPermission,
|
||||||
|
) error {
|
||||||
|
return acl.CreatePolicy(a.source,
|
||||||
|
name, spiffeIdPattern, pathPattern, permissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeletePolicy removes an existing policy from the system using its name.
|
||||||
|
//
|
||||||
|
// The function takes the following parameters:
|
||||||
|
// - name string: The name of the policy to be deleted
|
||||||
|
//
|
||||||
|
// The function returns an error if any of the following operations fail:
|
||||||
|
// - Marshaling the policy deletion request
|
||||||
|
// - Creating the mTLS client
|
||||||
|
// - Making the HTTP POST request
|
||||||
|
// - Unmarshaling the response
|
||||||
|
// - Server-side policy deletion (indicated in the response)
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// err = api.DeletePolicy("doc-reader")
|
||||||
|
// if err != nil {
|
||||||
|
// log.Printf("Failed to delete policy: %v", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
func (a *Api) DeletePolicy(name string) error {
|
||||||
|
return acl.DeletePolicy(a.source, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPolicy retrieves a policy from the system using its name.
|
||||||
|
//
|
||||||
|
// The function takes the following parameters:
|
||||||
|
// - name string: The name of the policy to retrieve
|
||||||
|
//
|
||||||
|
// The function returns:
|
||||||
|
// - (*data.Policy, nil) if the policy is found
|
||||||
|
// - (nil, nil) if the policy is not found
|
||||||
|
// - (nil, error) if an error occurs during the operation
|
||||||
|
//
|
||||||
|
// Errors can occur during:
|
||||||
|
// - Marshaling the policy retrieval request
|
||||||
|
// - Creating the mTLS client
|
||||||
|
// - Making the HTTP POST request (except for not found cases)
|
||||||
|
// - Unmarshaling the response
|
||||||
|
// - Server-side policy retrieval (indicated in the response)
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// policy, err := api.GetPolicy("doc-reader")
|
||||||
|
// if err != nil {
|
||||||
|
// log.Printf("Error retrieving policy: %v", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// if policy == nil {
|
||||||
|
// log.Printf("Policy not found")
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// log.Printf("Found policy: %+v", policy)
|
||||||
|
func (a *Api) GetPolicy(name string) (*data.Policy, error) {
|
||||||
|
return acl.GetPolicy(a.source, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPolicies retrieves all policies from the system.
|
||||||
|
//
|
||||||
|
// The function returns:
|
||||||
|
// - (*[]data.Policy, nil) containing all policies if successful
|
||||||
|
// - (nil, nil) if no policies are found
|
||||||
|
// - (nil, error) if an error occurs during the operation
|
||||||
|
//
|
||||||
|
// Note: The returned slice pointer should be dereferenced before use:
|
||||||
|
//
|
||||||
|
// policies := *result
|
||||||
|
//
|
||||||
|
// Errors can occur during:
|
||||||
|
// - Marshaling the policy list request
|
||||||
|
// - Creating the mTLS client
|
||||||
|
// - Making the HTTP POST request (except for not found cases)
|
||||||
|
// - Unmarshaling the response
|
||||||
|
// - Server-side policy listing (indicated in the response)
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// result, err := api.ListPolicies()
|
||||||
|
// if err != nil {
|
||||||
|
// log.Printf("Error listing policies: %v", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// if result == nil {
|
||||||
|
// log.Printf("No policies found")
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// policies := *result
|
||||||
|
// for _, policy := range policies {
|
||||||
|
// log.Printf("Found policy: %+v", policy)
|
||||||
|
// }
|
||||||
|
func (a *Api) ListPolicies() (*[]data.Policy, error) {
|
||||||
|
return acl.ListPolicies(a.source)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSecretVersions deletes specified versions of a secret at the given
|
||||||
|
// path
|
||||||
|
//
|
||||||
|
// It constructs a delete request and sends it to the secrets API endpoint.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - path string: Path to the secret to delete
|
||||||
|
// - versions []int: Array of version numbers to delete
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: nil on success, unauthorized error if not logged in, or wrapped
|
||||||
|
// error on request/parsing failure
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// err := api.DeleteSecretVersions("secret/path", []int{1, 2})
|
||||||
|
func (a *Api) DeleteSecretVersions(path string, versions []int) error {
|
||||||
|
return secret.Delete(a.source, path, versions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSecret deletes the entire secret at the given path
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - path string: Path to the secret to delete
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: nil on success, unauthorized error if not logged in, or wrapped
|
||||||
|
// error on request/parsing failure
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// err := api.DeleteSecret("secret/path")
|
||||||
|
func (a *Api) DeleteSecret(path string) error {
|
||||||
|
return secret.Delete(a.source, path, []int{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSecretVersion retrieves a specific version of a secret at the given
|
||||||
|
// path.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - path string: Path to the secret to retrieve
|
||||||
|
// - version int: Version number of the secret to retrieve
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *data.Secret: Secret data 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:
|
||||||
|
//
|
||||||
|
// secret, err := api.GetSecretVersion("secret/path", 1)
|
||||||
|
func (a *Api) GetSecretVersion(
|
||||||
|
path string, version int,
|
||||||
|
) (*data.Secret, error) {
|
||||||
|
return secret.Get(a.source, path, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSecret retrieves the latest version of the secret at the given path.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - path string: Path to the secret to retrieve
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *data.Secret: Secret data 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:
|
||||||
|
//
|
||||||
|
// secret, err := api.GetSecret("secret/path")
|
||||||
|
func (a *Api) GetSecret(path string) (*data.Secret, error) {
|
||||||
|
return secret.Get(a.source, path, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListSecretKeys retrieves all secret keys.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *[]string: Pointer to array of secret keys if found, nil if none found
|
||||||
|
// - error: nil on success, unauthorized error if not logged in, or
|
||||||
|
// wrapped error on request/parsing failure
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// keys, err := api.ListSecretKeys()
|
||||||
|
func (a *Api) ListSecretKeys() (*[]string, error) {
|
||||||
|
return secret.ListKeys(a.source)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSecretMetadata retrieves metadata for a specific version of a secret at
|
||||||
|
// the given path.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - path string: Path to the secret to retrieve metadata for
|
||||||
|
// - version int: Version number of the secret to retrieve metadata for
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *data.SecretMetadata: 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 := api.GetSecretMetadata("secret/path", 1)
|
||||||
|
func (a *Api) GetSecretMetadata(
|
||||||
|
path string, version int,
|
||||||
|
) (*data.SecretMetadata, error) {
|
||||||
|
return secret.GetMetadata(a.source, path, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutSecret creates or updates a secret at the specified path with the given
|
||||||
|
// values.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - path string: Path where the secret should be stored
|
||||||
|
// - data map[string]string: Map of key-value pairs representing the secret
|
||||||
|
// data
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: nil on success, unauthorized error if not logged in, or
|
||||||
|
// wrapped error on request/parsing failure
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// err := api.PutSecret("secret/path", map[string]string{"key": "value"})
|
||||||
|
func (a *Api) PutSecret(path string, data map[string]string) error {
|
||||||
|
return secret.Put(a.source, path, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UndeleteSecret restores previously deleted versions of a secret at the
|
||||||
|
// specified path.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - path string: Path to the secret to restore
|
||||||
|
// - versions []int: Array of version numbers to restore. Empty array
|
||||||
|
// attempts no restoration
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: nil on success, unauthorized error if not logged in, or
|
||||||
|
// wrapped error on request/parsing failure
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// err := api.UndeleteSecret("secret/path", []int{1, 2})
|
||||||
|
func (a *Api) UndeleteSecret(path string, versions []int) error {
|
||||||
|
return secret.Undelete(a.source, path, versions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recover returns 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 shards are sensitive and should be securely stored out-of-band
|
||||||
|
// in encrypted form.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *[][32]byte: Pointer to array of recovery shards as 32-byte arrays
|
||||||
|
// - error: nil on success, unauthorized error if not authorized, or
|
||||||
|
// wrapped error on request/parsing failure
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// shards, err := api.Recover()
|
||||||
|
func (a *Api) Recover() (map[int]*[32]byte, 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 *[32]byte: Pointer to a 32-byte array containing the shard to seed
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *data.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 := api.Restore(shardPtr)
|
||||||
|
func (a *Api) Restore(index int, shard *[32]byte) (*data.RestorationStatus, error) {
|
||||||
|
return operator.Restore(a.source, index, shard)
|
||||||
|
}
|
|
@ -11,6 +11,8 @@ const ErrServerFault = ErrorCode("server_fault")
|
||||||
const ErrUnauthorized = ErrorCode("unauthorized")
|
const ErrUnauthorized = ErrorCode("unauthorized")
|
||||||
const ErrInternal = ErrorCode("internal_error")
|
const ErrInternal = ErrorCode("internal_error")
|
||||||
const ErrLowEntropy = ErrorCode("low_entropy")
|
const ErrLowEntropy = ErrorCode("low_entropy")
|
||||||
|
const ErrNotReady = ErrorCode("not_ready")
|
||||||
|
const ErrNotAlive = ErrorCode("not_alive")
|
||||||
const ErrAlreadyInitialized = ErrorCode("already_initialized")
|
const ErrAlreadyInitialized = ErrorCode("already_initialized")
|
||||||
const ErrNotFound = ErrorCode("not_found")
|
const ErrNotFound = ErrorCode("not_found")
|
||||||
const ErrSuccess = ErrorCode("success")
|
const ErrSuccess = ErrorCode("success")
|
|
@ -0,0 +1,29 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package data
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// SecretVersionInfo for secrets version
|
||||||
|
type SecretVersionInfo struct {
|
||||||
|
CreatedTime time.Time `json:"createdTime"`
|
||||||
|
Version int `json:"version"`
|
||||||
|
DeletedTime *time.Time `json:"deletedTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecretMetaDataContent for secrets raw metadata
|
||||||
|
type SecretMetaDataContent struct {
|
||||||
|
CurrentVersion int `json:"currentVersion"`
|
||||||
|
OldestVersion int `json:"oldestVersion"`
|
||||||
|
CreatedTime time.Time `json:"createdTime"`
|
||||||
|
UpdatedTime time.Time `json:"updatedTime"`
|
||||||
|
MaxVersions int `json:"maxVersions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecretMetadata for secrets metadata
|
||||||
|
type SecretMetadata struct {
|
||||||
|
Versions map[int]SecretVersionInfo `json:"versions,omitempty"`
|
||||||
|
Metadata SecretMetaDataContent `json:"metadata,omitempty"`
|
||||||
|
}
|
|
@ -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"`
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
// \\\\\ Copyright 2024-present SPIKE contributors.
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package entity
|
package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
|
@ -28,11 +28,11 @@ const PermissionSuper PolicyPermission = "super"
|
||||||
type Policy struct {
|
type Policy struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
SpiffeIdPattern string `json:"spiffe_id_pattern"`
|
SpiffeIdPattern string `json:"spiffeIdPattern"`
|
||||||
PathPattern string `json:"path_pattern"`
|
PathPattern string `json:"pathPattern"`
|
||||||
Permissions []PolicyPermission `json:"permissions"`
|
Permissions []PolicyPermission `json:"permissions"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
CreatedBy string `json:"created_by"`
|
CreatedBy string `json:"createdBy"`
|
||||||
|
|
||||||
// Unexported fields won't be serialized to JSON
|
// Unexported fields won't be serialized to JSON
|
||||||
IdRegex *regexp.Regexp `json:"-"`
|
IdRegex *regexp.Regexp `json:"-"`
|
|
@ -2,7 +2,7 @@
|
||||||
// \\\\\ Copyright 2024-present SPIKE contributors.
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package entity
|
package data
|
||||||
|
|
||||||
// Secret is the secret that returns from SPIKE Nexus mTLS REST API.
|
// Secret is the secret that returns from SPIKE Nexus mTLS REST API.
|
||||||
type Secret struct {
|
type Secret struct {
|
|
@ -4,7 +4,9 @@
|
||||||
|
|
||||||
package reqres
|
package reqres
|
||||||
|
|
||||||
import "github.com/spiffe/spike-sdk-go/api/internal/entity/data"
|
import (
|
||||||
|
"github.com/spiffe/spike-sdk-go/api/entity/data"
|
||||||
|
)
|
||||||
|
|
||||||
// FallbackResponse is a generic response for any error.
|
// FallbackResponse is a generic response for any error.
|
||||||
type FallbackResponse struct {
|
type FallbackResponse struct {
|
|
@ -0,0 +1,29 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package reqres
|
||||||
|
|
||||||
|
import "github.com/spiffe/spike-sdk-go/api/entity/data"
|
||||||
|
|
||||||
|
// RestoreRequest for disaster recovery.
|
||||||
|
type RestoreRequest struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Shard *[32]byte `json:"shard"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestoreResponse for disaster recovery.
|
||||||
|
type RestoreResponse struct {
|
||||||
|
data.RestorationStatus
|
||||||
|
Err data.ErrorCode `json:"err,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecoverRequest for disaster recovery.
|
||||||
|
type RecoverRequest struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecoverResponse for disaster recovery.
|
||||||
|
type RecoverResponse struct {
|
||||||
|
Shards map[int]*[32]byte `json:"shards"`
|
||||||
|
Err data.ErrorCode `json:"err,omitempty"`
|
||||||
|
}
|
|
@ -5,54 +5,63 @@
|
||||||
package reqres
|
package reqres
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/spiffe/spike-sdk-go/api/entity"
|
"github.com/spiffe/spike-sdk-go/api/entity/data"
|
||||||
data "github.com/spiffe/spike-sdk-go/api/internal/entity/data"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PolicyCreateRequest for policy creation.
|
||||||
type PolicyCreateRequest struct {
|
type PolicyCreateRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
SpiffeIdPattern string `json:"spiffe_id_pattern"`
|
SpiffeIdPattern string `json:"spiffedPattern"`
|
||||||
PathPattern string `json:"path_pattern"`
|
PathPattern string `json:"pathPattern"`
|
||||||
Permissions []entity.PolicyPermission `json:"permissions"`
|
Permissions []data.PolicyPermission `json:"permissions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PolicyCreateResponse for policy creation.
|
||||||
type PolicyCreateResponse struct {
|
type PolicyCreateResponse struct {
|
||||||
Id string `json:"id,omitempty"`
|
Id string `json:"id,omitempty"`
|
||||||
Err data.ErrorCode `json:"err,omitempty"`
|
Err data.ErrorCode `json:"err,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PolicyReadRequest to read a policy.
|
||||||
type PolicyReadRequest struct {
|
type PolicyReadRequest struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PolicyReadResponse to read a policy.
|
||||||
type PolicyReadResponse struct {
|
type PolicyReadResponse struct {
|
||||||
entity.Policy
|
data.Policy
|
||||||
Err data.ErrorCode `json:"err,omitempty"`
|
Err data.ErrorCode `json:"err,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PolicyDeleteRequest to delete a policy.
|
||||||
type PolicyDeleteRequest struct {
|
type PolicyDeleteRequest struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PolicyDeleteResponse to delete a policy.
|
||||||
type PolicyDeleteResponse struct {
|
type PolicyDeleteResponse struct {
|
||||||
Err data.ErrorCode `json:"err,omitempty"`
|
Err data.ErrorCode `json:"err,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PolicyListRequest to list policies.
|
||||||
type PolicyListRequest struct{}
|
type PolicyListRequest struct{}
|
||||||
|
|
||||||
|
// PolicyListResponse to list policies.
|
||||||
type PolicyListResponse struct {
|
type PolicyListResponse struct {
|
||||||
Policies []entity.Policy `json:"policies"`
|
Policies []data.Policy `json:"policies"`
|
||||||
Err data.ErrorCode `json:"err,omitempty"`
|
Err data.ErrorCode `json:"err,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PolicyAccessCheckRequest to validate policy access.
|
||||||
type PolicyAccessCheckRequest struct {
|
type PolicyAccessCheckRequest struct {
|
||||||
SpiffeId string `json:"spiffe_id"`
|
SpiffeId string `json:"spiffeId"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PolicyAccessCheckResponse to validate policy access,.
|
||||||
type PolicyAccessCheckResponse struct {
|
type PolicyAccessCheckResponse struct {
|
||||||
Allowed bool `json:"allowed"`
|
Allowed bool `json:"allowed"`
|
||||||
MatchingPolicies []string `json:"matching_policies"`
|
MatchingPolicies []string `json:"matchingPolicies"`
|
||||||
Err data.ErrorCode `json:"err,omitempty"`
|
Err data.ErrorCode `json:"err,omitempty"`
|
||||||
}
|
}
|
|
@ -5,18 +5,19 @@
|
||||||
package reqres
|
package reqres
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/spiffe/spike-sdk-go/api/entity"
|
"github.com/spiffe/spike-sdk-go/api/entity/data"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/entity/data"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SecretResponseMetadata is meta information about secrets for internal
|
// SecretMetadataRequest for get secrets metadata
|
||||||
// tracking.
|
type SecretMetadataRequest struct {
|
||||||
type SecretResponseMetadata struct {
|
Path string `json:"path"`
|
||||||
CreatedTime time.Time `json:"created_time"`
|
Version int `json:"version,omitempty"` // Optional specific version
|
||||||
Version int `json:"version"`
|
}
|
||||||
DeletedTime *time.Time `json:"deleted_time,omitempty"`
|
|
||||||
|
// SecretMetadataResponse for secrets versions and metadata
|
||||||
|
type SecretMetadataResponse struct {
|
||||||
|
data.SecretMetadata
|
||||||
|
Err data.ErrorCode `json:"err,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecretPutRequest for creating/updating secrets
|
// SecretPutRequest for creating/updating secrets
|
||||||
|
@ -28,7 +29,6 @@ type SecretPutRequest struct {
|
||||||
|
|
||||||
// SecretPutResponse is after successful secret write
|
// SecretPutResponse is after successful secret write
|
||||||
type SecretPutResponse struct {
|
type SecretPutResponse struct {
|
||||||
SecretResponseMetadata
|
|
||||||
Err data.ErrorCode `json:"err,omitempty"`
|
Err data.ErrorCode `json:"err,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,9 +40,8 @@ type SecretReadRequest struct {
|
||||||
|
|
||||||
// SecretReadResponse is for getting secrets
|
// SecretReadResponse is for getting secrets
|
||||||
type SecretReadResponse struct {
|
type SecretReadResponse struct {
|
||||||
entity.Secret
|
data.Secret
|
||||||
Data map[string]string `json:"data"`
|
Err data.ErrorCode `json:"err,omitempty"`
|
||||||
Err data.ErrorCode `json:"err,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecretDeleteRequest for soft-deleting secret versions
|
// SecretDeleteRequest for soft-deleting secret versions
|
||||||
|
@ -53,8 +52,7 @@ type SecretDeleteRequest struct {
|
||||||
|
|
||||||
// SecretDeleteResponse after soft-delete
|
// SecretDeleteResponse after soft-delete
|
||||||
type SecretDeleteResponse struct {
|
type SecretDeleteResponse struct {
|
||||||
Metadata SecretResponseMetadata `json:"metadata"`
|
Err data.ErrorCode `json:"err,omitempty"`
|
||||||
Err data.ErrorCode `json:"err,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecretUndeleteRequest for recovering soft-deleted versions
|
// SecretUndeleteRequest for recovering soft-deleted versions
|
||||||
|
@ -65,8 +63,7 @@ type SecretUndeleteRequest struct {
|
||||||
|
|
||||||
// SecretUndeleteResponse after recovery
|
// SecretUndeleteResponse after recovery
|
||||||
type SecretUndeleteResponse struct {
|
type SecretUndeleteResponse struct {
|
||||||
Metadata SecretResponseMetadata `json:"metadata"`
|
Err data.ErrorCode `json:"err,omitempty"`
|
||||||
Err data.ErrorCode `json:"err,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecretListRequest for listing secrets
|
// SecretListRequest for listing secrets
|
|
@ -0,0 +1,28 @@
|
||||||
|
package reqres
|
||||||
|
|
||||||
|
import "github.com/spiffe/spike-sdk-go/api/entity/data"
|
||||||
|
|
||||||
|
// ShardContributionRequest represents a request to submit a shard contribution.
|
||||||
|
// KeeperId specifies the identifier of the keeper responsible for the shard.
|
||||||
|
// Shard represents the shard data being contributed to the system.
|
||||||
|
// Version optionally specifies the version of the shard being submitted.
|
||||||
|
type ShardContributionRequest struct {
|
||||||
|
Shard *[32]byte `json:"shard"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShardContributionResponse represents the response structure for a shard
|
||||||
|
// contribution.
|
||||||
|
type ShardContributionResponse struct {
|
||||||
|
Err data.ErrorCode `json:"err,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShardRequest represents a request to handle data partitioning or sharding.
|
||||||
|
type ShardRequest struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShardResponse represents the result of an operation on a specific data shard.
|
||||||
|
// The struct includes the shard identifier and an associated error code.
|
||||||
|
type ShardResponse struct {
|
||||||
|
Shard *[32]byte `json:"shard"`
|
||||||
|
Err data.ErrorCode
|
||||||
|
}
|
|
@ -13,3 +13,7 @@ var ErrReadFailure = errors.New("failed to read request body")
|
||||||
var ErrMarshalFailure = errors.New("failed to marshal response body")
|
var ErrMarshalFailure = errors.New("failed to marshal response body")
|
||||||
var ErrAlreadyInitialized = errors.New("already initialized")
|
var ErrAlreadyInitialized = errors.New("already initialized")
|
||||||
var ErrMissingRootKey = errors.New("missing root key")
|
var ErrMissingRootKey = errors.New("missing root key")
|
||||||
|
var ErrInvalidInput = errors.New("invalid input")
|
||||||
|
var ErrInvalidPermission = errors.New("invalid permission")
|
||||||
|
var ErrPeerConnection = errors.New("problem connecting to peer")
|
||||||
|
var ErrReadingResponseBody = errors.New("problem reading response body")
|
|
@ -30,34 +30,3 @@ func SpikeNexusDataFolder() string {
|
||||||
// The data dir is not configurable for security reasons.
|
// The data dir is not configurable for security reasons.
|
||||||
return filepath.Join(spikeDir, "/data")
|
return filepath.Join(spikeDir, "/data")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SpikePilotRecoveryFolder returns the path to the directory where Pilot stores
|
|
||||||
// recovery material for its root key.
|
|
||||||
func SpikePilotRecoveryFolder() string {
|
|
||||||
homeDir, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
homeDir = "/tmp"
|
|
||||||
}
|
|
||||||
|
|
||||||
spikeDir := filepath.Join(homeDir, ".spike")
|
|
||||||
|
|
||||||
// Create directory if it doesn't exist
|
|
||||||
// 0700 because we want to restrict access to the directory
|
|
||||||
// but allow the user to create db files in it.
|
|
||||||
err = os.MkdirAll(spikeDir+"/recovery", 0700)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The data dir is not configurable for security reasons.
|
|
||||||
return filepath.Join(spikeDir, "/recovery")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SpikePilotRootKeyRecoveryFile returns the path to the file where Pilot stores
|
|
||||||
// the root key recovery file.
|
|
||||||
func SpikePilotRootKeyRecoveryFile() string {
|
|
||||||
folder := SpikePilotRecoveryFolder()
|
|
||||||
|
|
||||||
// The file path and file name are NOT configurable for security reasons.
|
|
||||||
return filepath.Join(folder, ".root-key-recovery.spike")
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
// \\ SPIKE: Secure your secrets with SPIFFE.
|
|
||||||
// \\\\\ Copyright 2024-present SPIKE contributors.
|
|
||||||
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package reqres
|
|
||||||
|
|
||||||
import "github.com/spiffe/spike-sdk-go/api/internal/entity/data"
|
|
||||||
|
|
||||||
// AdminTokenWriteRequest is to persist the admin token in memory.
|
|
||||||
// Admin token can be persisted only once. It is used to receive a
|
|
||||||
// short-lived session token.
|
|
||||||
type AdminTokenWriteRequest struct {
|
|
||||||
Data string `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AdminTokenWriteResponse is to persist the admin token in memory.
|
|
||||||
type AdminTokenWriteResponse struct {
|
|
||||||
Err data.ErrorCode `json:"err,omitempty"`
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
// \\ SPIKE: Secure your secrets with SPIFFE.
|
|
||||||
// \\\\\ Copyright 2024-present SPIKE contributors.
|
|
||||||
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package reqres
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/entity"
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/entity/data"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CheckInitStateRequest is to check if the SPIKE Keep is initialized.
|
|
||||||
type CheckInitStateRequest struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckInitStateResponse is to check if the SPIKE Keep is initialized.
|
|
||||||
type CheckInitStateResponse struct {
|
|
||||||
State entity.InitState `json:"state"`
|
|
||||||
Err data.ErrorCode `json:"err,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitRequest is to initialize SPIKE as a superuser.
|
|
||||||
type InitRequest struct {
|
|
||||||
// Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitResponse is to initialize SPIKE as a superuser.
|
|
||||||
type InitResponse struct {
|
|
||||||
RecoveryToken string `json:"token"`
|
|
||||||
Err data.ErrorCode `json:"err,omitempty"`
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
// \\ SPIKE: Secure your secrets with SPIFFE.
|
|
||||||
// \\\\\ Copyright 2024-present SPIKE contributors.
|
|
||||||
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package reqres
|
|
||||||
|
|
||||||
import "github.com/spiffe/spike-sdk-go/api/internal/entity/data"
|
|
||||||
|
|
||||||
// RootKeyCacheRequest is to cache the generated root key in SPIKE Keep.
|
|
||||||
// If the root key is lost due to a crash, it will be retrieved from SPIKE Keep.
|
|
||||||
type RootKeyCacheRequest struct {
|
|
||||||
RootKey string `json:"rootKey"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RootKeyCacheResponse is to cache the generated root key in SPIKE Keep.
|
|
||||||
type RootKeyCacheResponse struct {
|
|
||||||
Err data.ErrorCode `json:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RootKeyReadRequest is a request to get the root key back from remote cache.
|
|
||||||
type RootKeyReadRequest struct{}
|
|
||||||
|
|
||||||
// RootKeyReadResponse is a response to get the root key back from remote cache.
|
|
||||||
type RootKeyReadResponse struct {
|
|
||||||
RootKey string `json:"rootKey"`
|
|
||||||
Err data.ErrorCode `json:"err,omitempty"`
|
|
||||||
}
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"github.com/spiffe/spike-sdk-go/api/url"
|
||||||
|
|
||||||
|
"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/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreatePolicy creates a new policy in the system using the provided SPIFFE
|
||||||
|
// X.509 source and policy details. It establishes a mutual TLS connection using
|
||||||
|
// the X.509 source and sends a policy creation request to the server.
|
||||||
|
//
|
||||||
|
// The function takes the following parameters:
|
||||||
|
// - source: A pointer to a workloadapi.X509Source for establishing mTLS
|
||||||
|
// connection
|
||||||
|
// - name: The name of the policy to be created
|
||||||
|
// - spiffeIdPattern: The SPIFFE ID pattern that this policy will apply to
|
||||||
|
// - pathPattern: The path pattern that this policy will match against
|
||||||
|
// - permissions: A slice of PolicyPermission defining the access rights for
|
||||||
|
// this policy
|
||||||
|
//
|
||||||
|
// The function returns an error if any of the following operations fail:
|
||||||
|
// - Marshaling the policy creation request
|
||||||
|
// - Creating the mTLS client
|
||||||
|
// - Making the HTTP POST request
|
||||||
|
// - Unmarshaling the response
|
||||||
|
// - Server-side policy creation (indicated in the response)
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// source, err := workloadapi.NewX509Source(context.Background())
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
// defer source.Close()
|
||||||
|
//
|
||||||
|
// permissions := []data.PolicyPermission{
|
||||||
|
// {
|
||||||
|
// Action: "read",
|
||||||
|
// Resource: "documents/*",
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// err = CreatePolicy(
|
||||||
|
// source,
|
||||||
|
// "doc-reader",
|
||||||
|
// "spiffe://example.org/service/*",
|
||||||
|
// "/api/documents/*",
|
||||||
|
// permissions,
|
||||||
|
// )
|
||||||
|
// if err != nil {
|
||||||
|
// log.Printf("Failed to create policy: %v", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
func CreatePolicy(source *workloadapi.X509Source,
|
||||||
|
name string, spiffeIdPattern string, pathPattern string,
|
||||||
|
permissions []data.PolicyPermission,
|
||||||
|
) error {
|
||||||
|
r := reqres.PolicyCreateRequest{
|
||||||
|
Name: name,
|
||||||
|
SpiffeIdPattern: spiffeIdPattern,
|
||||||
|
PathPattern: pathPattern,
|
||||||
|
Permissions: permissions,
|
||||||
|
}
|
||||||
|
|
||||||
|
mr, err := json.Marshal(r)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Join(
|
||||||
|
errors.New(
|
||||||
|
"createPolicy: I am having problem generating the payload",
|
||||||
|
),
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := net.CreateMtlsClient(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := net.Post(client, url.PolicyCreate(), mr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := reqres.PolicyCreateResponse{}
|
||||||
|
err = json.Unmarshal(body, &res)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Join(
|
||||||
|
errors.New("createPolicy: Problem parsing response body"),
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if res.Err != "" {
|
||||||
|
return errors.New(string(res.Err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"github.com/spiffe/spike-sdk-go/api/url"
|
||||||
|
|
||||||
|
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
||||||
|
|
||||||
|
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
|
||||||
|
"github.com/spiffe/spike-sdk-go/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeletePolicy removes an existing policy from the system using its ID.
|
||||||
|
// It requires a SPIFFE X.509 source for establishing a mutual TLS connection
|
||||||
|
// to make the deletion request.
|
||||||
|
//
|
||||||
|
// The function takes the following parameters:
|
||||||
|
// - source: A pointer to a workloadapi.X509Source for establishing mTLS
|
||||||
|
// connection
|
||||||
|
// - id: The unique identifier of the policy to be deleted
|
||||||
|
//
|
||||||
|
// The function returns an error if any of the following operations fail:
|
||||||
|
// - Marshaling the policy deletion request
|
||||||
|
// - Creating the mTLS client
|
||||||
|
// - Making the HTTP POST request
|
||||||
|
// - Unmarshaling the response
|
||||||
|
// - Server-side policy deletion (indicated in the response)
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// source, err := workloadapi.NewX509Source(context.Background())
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
// defer source.Close()
|
||||||
|
//
|
||||||
|
// err = DeletePolicy(source, "policy-123")
|
||||||
|
// if err != nil {
|
||||||
|
// log.Printf("Failed to delete policy: %v", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
func DeletePolicy(source *workloadapi.X509Source, id string) error {
|
||||||
|
r := reqres.PolicyDeleteRequest{
|
||||||
|
Id: id,
|
||||||
|
}
|
||||||
|
|
||||||
|
mr, err := json.Marshal(r)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Join(
|
||||||
|
errors.New(
|
||||||
|
"deletePolicy: I am having problem generating the payload",
|
||||||
|
),
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := net.CreateMtlsClient(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := net.Post(client, url.PolicyDelete(), mr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := reqres.PolicyDeleteResponse{}
|
||||||
|
err = json.Unmarshal(body, &res)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Join(
|
||||||
|
errors.New("deletePolicy: Problem parsing response body"),
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if res.Err != "" {
|
||||||
|
return errors.New(string(res.Err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"github.com/spiffe/spike-sdk-go/api/url"
|
||||||
|
|
||||||
|
"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/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetPolicy retrieves a policy from the system using its ID.
|
||||||
|
// It requires a SPIFFE X.509 source for establishing a mutual TLS connection
|
||||||
|
// to make the retrieval request.
|
||||||
|
//
|
||||||
|
// The function takes the following parameters:
|
||||||
|
// - source: A pointer to a workloadapi.X509Source for establishing mTLS
|
||||||
|
// connection
|
||||||
|
// - id: The unique identifier of the policy to retrieve
|
||||||
|
//
|
||||||
|
// The function returns:
|
||||||
|
// - (*data.Policy, nil) if the policy is found
|
||||||
|
// - (nil, nil) if the policy is not found
|
||||||
|
// - (nil, error) if an error occurs during the operation
|
||||||
|
//
|
||||||
|
// Errors can occur during:
|
||||||
|
// - Marshaling the policy retrieval request
|
||||||
|
// - Creating the mTLS client
|
||||||
|
// - Making the HTTP POST request (except for not found cases)
|
||||||
|
// - Unmarshaling the response
|
||||||
|
// - Server-side policy retrieval (indicated in the response)
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// source, err := workloadapi.NewX509Source(context.Background())
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
// defer source.Close()
|
||||||
|
//
|
||||||
|
// policy, err := GetPolicy(source, "policy-123")
|
||||||
|
// if err != nil {
|
||||||
|
// log.Printf("Error retrieving policy: %v", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// if policy == nil {
|
||||||
|
// log.Printf("Policy not found")
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// log.Printf("Found policy: %+v", policy)
|
||||||
|
func GetPolicy(
|
||||||
|
source *workloadapi.X509Source, id string,
|
||||||
|
) (*data.Policy, error) {
|
||||||
|
r := reqres.PolicyReadRequest{Id: id}
|
||||||
|
|
||||||
|
mr, err := json.Marshal(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Join(
|
||||||
|
errors.New("getPolicy: I am having problem generating the payload"),
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := net.CreateMtlsClient(source)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := net.Post(client, url.PolicyGet(), mr)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, net.ErrNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res reqres.PolicyReadResponse
|
||||||
|
err = json.Unmarshal(body, &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Join(
|
||||||
|
errors.New("getPolicy: Problem parsing response body"),
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if res.Err != "" {
|
||||||
|
return nil, errors.New(string(res.Err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res.Policy, nil
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"github.com/spiffe/spike-sdk-go/api/url"
|
||||||
|
|
||||||
|
"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/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListPolicies retrieves all policies from the system.
|
||||||
|
// It requires a SPIFFE X.509 source for establishing a mutual TLS connection
|
||||||
|
// to make the list request.
|
||||||
|
//
|
||||||
|
// The function takes:
|
||||||
|
// - source: A pointer to a workloadapi.X509Source for establishing mTLS
|
||||||
|
// connection
|
||||||
|
//
|
||||||
|
// The function returns:
|
||||||
|
// - (*[]data.Policy, nil) containing all policies if successful
|
||||||
|
// - (nil, nil) if no policies are found
|
||||||
|
// - (nil, error) if an error occurs during the operation
|
||||||
|
//
|
||||||
|
// Note: The returned slice pointer should be dereferenced before use:
|
||||||
|
//
|
||||||
|
// policies := *result
|
||||||
|
//
|
||||||
|
// Errors can occur during:
|
||||||
|
// - Marshaling the policy list request
|
||||||
|
// - Creating the mTLS client
|
||||||
|
// - Making the HTTP POST request (except for not found cases)
|
||||||
|
// - Unmarshaling the response
|
||||||
|
// - Server-side policy listing (indicated in the response)
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// source, err := workloadapi.NewX509Source(context.Background())
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
// defer source.Close()
|
||||||
|
//
|
||||||
|
// result, err := ListPolicies(source)
|
||||||
|
// if err != nil {
|
||||||
|
// log.Printf("Error listing policies: %v", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// if result == nil {
|
||||||
|
// log.Printf("No policies found")
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// policies := *result
|
||||||
|
// for _, policy := range policies {
|
||||||
|
// log.Printf("Found policy: %+v", policy)
|
||||||
|
// }
|
||||||
|
func ListPolicies(source *workloadapi.X509Source) (*[]data.Policy, error) {
|
||||||
|
r := reqres.PolicyListRequest{}
|
||||||
|
mr, err := json.Marshal(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Join(
|
||||||
|
errors.New(
|
||||||
|
"listPolicies: I am having problem generating the payload",
|
||||||
|
),
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := net.CreateMtlsClient(source)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := net.Post(client, url.PolicyList(), mr)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, net.ErrNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res reqres.PolicyListResponse
|
||||||
|
err = json.Unmarshal(body, &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Join(
|
||||||
|
errors.New("listPolicies: Problem parsing response body"),
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if res.Err != "" {
|
||||||
|
return nil, errors.New(string(res.Err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res.Policies, nil
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
// \\ 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/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:
|
||||||
|
// - map[int]*[32]byte: Map of shard indices to shard byte arrays 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) (map[int]*[32]byte, 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[int]*[32]byte)
|
||||||
|
|
||||||
|
for i, shard := range res.Shards {
|
||||||
|
result[i] = shard
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
// \\ 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/url"
|
||||||
|
"github.com/spiffe/spike-sdk-go/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Restore submits a recovery shard to continue the restoration process.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - source *workloadapi.X509Source: X509Source used for mTLS client
|
||||||
|
// authentication
|
||||||
|
// - shardIndex int: Index of the recovery shard
|
||||||
|
// - shardValue *[32]byte: Pointer to a 32-byte array containing the recovery
|
||||||
|
// shard
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *data.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, shardIndex, shardValue)
|
||||||
|
func Restore(
|
||||||
|
source *workloadapi.X509Source, shardIndex int, shardValue *[32]byte,
|
||||||
|
) (*data.RestorationStatus, error) {
|
||||||
|
r := reqres.RestoreRequest{
|
||||||
|
Id: shardIndex,
|
||||||
|
Shard: shardValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
mr, err := json.Marshal(r)
|
||||||
|
// Security: Zero out r.Shard as soon as we're done with it
|
||||||
|
for i := range r.Shard {
|
||||||
|
r.Shard[i] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Join(
|
||||||
|
errors.New("restore: failed to marshal recover request"),
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := net.CreateMtlsClient(source)
|
||||||
|
if err != nil {
|
||||||
|
// Security: Zero out mr before returning error
|
||||||
|
for i := range mr {
|
||||||
|
mr[i] = 0
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := net.Post(client, url.Restore(), mr)
|
||||||
|
// Security: Zero out mr after post request is complete
|
||||||
|
for i := range mr {
|
||||||
|
mr[i] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -1,18 +1,21 @@
|
||||||
package api
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package secret
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"github.com/spiffe/spike-sdk-go/api/url"
|
||||||
|
|
||||||
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
||||||
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/entity/v1/reqres"
|
"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"
|
"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.
|
// mTLS authentication.
|
||||||
//
|
//
|
||||||
// It converts string version numbers to integers, constructs a delete request,
|
// It converts string version numbers to integers, constructs a delete request,
|
||||||
|
@ -30,25 +33,12 @@ import (
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// err := DeleteSecret(x509Source, "secret/path", []string{"1", "2"})
|
// err := deleteSecret(x509Source, "secret/path", []string{"1", "2"})
|
||||||
func DeleteSecret(source *workloadapi.X509Source,
|
func Delete(source *workloadapi.X509Source,
|
||||||
path string, versions []string) error {
|
path string, versions []int) error {
|
||||||
var vv []int
|
|
||||||
if len(versions) == 0 {
|
|
||||||
vv = []int{}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, version := range versions {
|
|
||||||
v, e := strconv.Atoi(version)
|
|
||||||
if e != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
vv = append(vv, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
r := reqres.SecretDeleteRequest{
|
r := reqres.SecretDeleteRequest{
|
||||||
Path: path,
|
Path: path,
|
||||||
Versions: vv,
|
Versions: versions,
|
||||||
}
|
}
|
||||||
|
|
||||||
mr, err := json.Marshal(r)
|
mr, err := json.Marshal(r)
|
||||||
|
@ -61,8 +51,7 @@ func DeleteSecret(source *workloadapi.X509Source,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var truer = func(string) bool { return true }
|
client, err := net.CreateMtlsClient(source)
|
||||||
client, err := net.CreateMtlsClient(source, truer)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
|
@ -2,22 +2,22 @@
|
||||||
// \\\\\ Copyright 2024-present SPIKE contributors.
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package api
|
package secret
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/spiffe/spike-sdk-go/api/url"
|
||||||
|
|
||||||
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
||||||
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/entity"
|
"github.com/spiffe/spike-sdk-go/api/entity/data"
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/entity/v1/reqres"
|
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
|
||||||
code "github.com/spiffe/spike-sdk-go/api/internal/errors"
|
code "github.com/spiffe/spike-sdk-go/api/errors"
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/url"
|
|
||||||
"github.com/spiffe/spike-sdk-go/net"
|
"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.
|
// mTLS authentication.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
|
@ -32,9 +32,9 @@ import (
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// secret, err := GetSecret(x509Source, "secret/path", 1)
|
// secret, err := getSecret(x509Source, "secret/path", 1)
|
||||||
func GetSecret(source *workloadapi.X509Source,
|
func Get(source *workloadapi.X509Source,
|
||||||
path string, version int) (*entity.Secret, error) {
|
path string, version int) (*data.Secret, error) {
|
||||||
r := reqres.SecretReadRequest{
|
r := reqres.SecretReadRequest{
|
||||||
Path: path,
|
Path: path,
|
||||||
Version: version,
|
Version: version,
|
||||||
|
@ -48,8 +48,7 @@ func GetSecret(source *workloadapi.X509Source,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
truer := func(string) bool { return true }
|
client, err := net.CreateMtlsClient(source)
|
||||||
client, err := net.CreateMtlsClient(source, truer)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -74,5 +73,5 @@ func GetSecret(source *workloadapi.X509Source,
|
||||||
return nil, errors.New(string(res.Err))
|
return nil, errors.New(string(res.Err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &entity.Secret{Data: res.Data}, nil
|
return &data.Secret{Data: res.Data}, nil
|
||||||
}
|
}
|
|
@ -2,20 +2,20 @@
|
||||||
// \\\\\ Copyright 2024-present SPIKE contributors.
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package api
|
package secret
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/spiffe/spike-sdk-go/api/url"
|
||||||
|
|
||||||
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
||||||
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/entity/v1/reqres"
|
"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"
|
"github.com/spiffe/spike-sdk-go/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListSecretKeys retrieves all secret keys using mTLS authentication.
|
// ListKeys retrieves all secret keys using mTLS authentication.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - source: X509Source for mTLS client authentication
|
// - source: X509Source for mTLS client authentication
|
||||||
|
@ -27,12 +27,12 @@ import (
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// keys, err := ListSecretKeys(x509Source)
|
// keys, err := listSecretKeys(x509Source)
|
||||||
func ListSecretKeys(source *workloadapi.X509Source) ([]string, error) {
|
func ListKeys(source *workloadapi.X509Source) (*[]string, error) {
|
||||||
r := reqres.SecretListRequest{}
|
r := reqres.SecretListRequest{}
|
||||||
mr, err := json.Marshal(r)
|
mr, err := json.Marshal(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{}, errors.Join(
|
return nil, errors.Join(
|
||||||
errors.New(
|
errors.New(
|
||||||
"listSecretKeys: I am having problem generating the payload",
|
"listSecretKeys: I am having problem generating the payload",
|
||||||
),
|
),
|
||||||
|
@ -40,31 +40,30 @@ func ListSecretKeys(source *workloadapi.X509Source) ([]string, error) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var truer = func(string) bool { return true }
|
client, err := net.CreateMtlsClient(source)
|
||||||
client, err := net.CreateMtlsClient(source, truer)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := net.Post(client, url.SecretList(), mr)
|
body, err := net.Post(client, url.SecretList(), mr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, net.ErrNotFound) {
|
if errors.Is(err, net.ErrNotFound) {
|
||||||
return []string{}, nil
|
return &[]string{}, nil
|
||||||
}
|
}
|
||||||
return []string{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var res reqres.SecretListResponse
|
var res reqres.SecretListResponse
|
||||||
err = json.Unmarshal(body, &res)
|
err = json.Unmarshal(body, &res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{}, errors.Join(
|
return nil, errors.Join(
|
||||||
errors.New("getSecret: Problem parsing response body"),
|
errors.New("getSecret: Problem parsing response body"),
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if res.Err != "" {
|
if res.Err != "" {
|
||||||
return []string{}, errors.New(string(res.Err))
|
return nil, errors.New(string(res.Err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.Keys, nil
|
return &res.Keys, nil
|
||||||
}
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package secret
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"github.com/spiffe/spike-sdk-go/api/url"
|
||||||
|
|
||||||
|
"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/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetMetadata 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 GetMetadata(
|
||||||
|
source *workloadapi.X509Source, path string, version int,
|
||||||
|
) (*data.SecretMetadata, error) {
|
||||||
|
r := reqres.SecretMetadataRequest{
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := net.Post(client, url.SecretMetadataGet(), 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 &data.SecretMetadata{
|
||||||
|
Versions: res.Versions,
|
||||||
|
Metadata: res.Metadata,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -2,20 +2,20 @@
|
||||||
// \\\\\ Copyright 2024-present SPIKE contributors.
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package api
|
package secret
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/spiffe/spike-sdk-go/api/url"
|
||||||
|
|
||||||
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
||||||
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/entity/v1/reqres"
|
"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"
|
"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.
|
// values using mTLS authentication.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
|
@ -29,8 +29,9 @@ import (
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// err := PutSecret(x509Source, "secret/path", map[string]string{"key": "value"})
|
// err := putSecret(x509Source, "secret/path",
|
||||||
func PutSecret(source *workloadapi.X509Source,
|
// map[string]string{"key": "value"})
|
||||||
|
func Put(source *workloadapi.X509Source,
|
||||||
path string, values map[string]string) error {
|
path string, values map[string]string) error {
|
||||||
|
|
||||||
r := reqres.SecretPutRequest{
|
r := reqres.SecretPutRequest{
|
||||||
|
@ -46,8 +47,7 @@ func PutSecret(source *workloadapi.X509Source,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var truer = func(string) bool { return true }
|
client, err := net.CreateMtlsClient(source)
|
||||||
client, err := net.CreateMtlsClient(source, truer)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
|
@ -2,21 +2,20 @@
|
||||||
// \\\\\ Copyright 2024-present SPIKE contributors.
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package api
|
package secret
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"github.com/spiffe/spike-sdk-go/api/url"
|
||||||
|
|
||||||
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
||||||
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/entity/v1/reqres"
|
"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"
|
"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.
|
// specified path using mTLS authentication.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
|
@ -31,22 +30,14 @@ import (
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// err := UndeleteSecret(x509Source, "secret/path", []string{"1", "2"})
|
// err := undeleteSecret(x509Source, "secret/path", []string{"1", "2"})
|
||||||
func UndeleteSecret(source *workloadapi.X509Source,
|
func Undelete(source *workloadapi.X509Source,
|
||||||
path string, versions []string) error {
|
path string, versions []int) error {
|
||||||
var vv []int
|
var vv []int
|
||||||
if len(versions) == 0 {
|
if len(versions) == 0 {
|
||||||
vv = []int{}
|
vv = []int{}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, version := range versions {
|
|
||||||
v, e := strconv.Atoi(version)
|
|
||||||
if e != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
vv = append(vv, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
r := reqres.SecretUndeleteRequest{
|
r := reqres.SecretUndeleteRequest{
|
||||||
Path: path,
|
Path: path,
|
||||||
Versions: vv,
|
Versions: vv,
|
||||||
|
@ -62,8 +53,7 @@ func UndeleteSecret(source *workloadapi.X509Source,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var truer = func(string) bool { return true }
|
client, err := net.CreateMtlsClient(source)
|
||||||
client, err := net.CreateMtlsClient(source, truer)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
|
@ -1,30 +0,0 @@
|
||||||
// \\ SPIKE: Secure your secrets with SPIFFE.
|
|
||||||
// \\\\\ Copyright 2024-present SPIKE contributors.
|
|
||||||
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package url
|
|
||||||
|
|
||||||
type SpikeNexusApiAction string
|
|
||||||
|
|
||||||
const keyApiAction = "action"
|
|
||||||
|
|
||||||
const actionNexusCheck SpikeNexusApiAction = "check"
|
|
||||||
const actionNexusGet SpikeNexusApiAction = "get"
|
|
||||||
const actionNexusDelete SpikeNexusApiAction = "delete"
|
|
||||||
const actionNexusUndelete SpikeNexusApiAction = "undelete"
|
|
||||||
const actionNexusList SpikeNexusApiAction = "list"
|
|
||||||
const actionNexusDefault SpikeNexusApiAction = ""
|
|
||||||
|
|
||||||
type SpikeKeeperApiAction string
|
|
||||||
|
|
||||||
const actionKeeperRead SpikeKeeperApiAction = "read"
|
|
||||||
const actionKeeperDefault SpikeKeeperApiAction = ""
|
|
||||||
|
|
||||||
type ApiUrl string
|
|
||||||
|
|
||||||
const spikeNexusUrlSecrets ApiUrl = "/v1/store/secrets"
|
|
||||||
const spikeNexusUrlInit ApiUrl = "/v1/auth/init"
|
|
||||||
|
|
||||||
const spikeNexusUrlPolicy ApiUrl = "/v1/acl/policy"
|
|
||||||
|
|
||||||
const spikeKeeperUrlKeep ApiUrl = "/v1/store/keep"
|
|
|
@ -1,64 +0,0 @@
|
||||||
// \\ 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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UrlSecretGet returns the URL for getting a secret.
|
|
||||||
func SecretGet() string {
|
|
||||||
u, _ := url.JoinPath(
|
|
||||||
env.NexusApiRoot(),
|
|
||||||
string(spikeNexusUrlSecrets),
|
|
||||||
)
|
|
||||||
params := url.Values{}
|
|
||||||
params.Add(keyApiAction, string(actionNexusGet))
|
|
||||||
return u + "?" + params.Encode()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UrlSecretPut returns the URL for putting a secret.
|
|
||||||
func SecretPut() string {
|
|
||||||
u, _ := url.JoinPath(
|
|
||||||
env.NexusApiRoot(),
|
|
||||||
string(spikeNexusUrlSecrets),
|
|
||||||
)
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
|
|
||||||
// UrlSecretDelete returns the URL for deleting a secret.
|
|
||||||
func SecretDelete() string {
|
|
||||||
u, _ := url.JoinPath(
|
|
||||||
env.NexusApiRoot(),
|
|
||||||
string(spikeNexusUrlSecrets),
|
|
||||||
)
|
|
||||||
params := url.Values{}
|
|
||||||
params.Add(keyApiAction, string(actionNexusDelete))
|
|
||||||
return u + "?" + params.Encode()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UrlSecretUndelete returns the URL for undeleting a secret.
|
|
||||||
func SecretUndelete() string {
|
|
||||||
u, _ := url.JoinPath(
|
|
||||||
env.NexusApiRoot(),
|
|
||||||
string(spikeNexusUrlSecrets),
|
|
||||||
)
|
|
||||||
params := url.Values{}
|
|
||||||
params.Add(keyApiAction, string(actionNexusUndelete))
|
|
||||||
return u + "?" + params.Encode()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UrlSecretList returns the URL for listing secrets.
|
|
||||||
func SecretList() string {
|
|
||||||
u, _ := url.JoinPath(
|
|
||||||
env.NexusApiRoot(),
|
|
||||||
string(spikeNexusUrlSecrets),
|
|
||||||
)
|
|
||||||
params := url.Values{}
|
|
||||||
params.Add(keyApiAction, string(actionNexusList))
|
|
||||||
return u + "?" + params.Encode()
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
// \\ SPIKE: Secure your secrets with SPIFFE.
|
|
||||||
// \\\\\ Copyright 2024-present SPIKE contributors.
|
|
||||||
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
|
||||||
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/entity"
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/config"
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/entity/v1/reqres"
|
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/url"
|
|
||||||
"github.com/spiffe/spike-sdk-go/net"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Init sends an initialization request to SPIKE Nexus.
|
|
||||||
func Init(source *workloadapi.X509Source) error {
|
|
||||||
r := reqres.InitRequest{}
|
|
||||||
mr, err := json.Marshal(r)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Join(
|
|
||||||
errors.New("initialization: I am having problem generating the payload"),
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: maybe restrict init and ACL operations at this level without doing any policy checks.
|
|
||||||
var truer = func(string) bool { return true }
|
|
||||||
client, err := net.CreateMtlsClient(source, truer)
|
|
||||||
|
|
||||||
body, err := net.Post(client, url.Init(), mr)
|
|
||||||
|
|
||||||
var res reqres.InitResponse
|
|
||||||
err = json.Unmarshal(body, &res)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Join(
|
|
||||||
errors.New("initialization: Problem parsing response body"),
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.RecoveryToken == "" {
|
|
||||||
fmt.Println("Failed to get recovery token")
|
|
||||||
return errors.New("failed to get recovery token")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile(
|
|
||||||
config.SpikePilotRootKeyRecoveryFile(), []byte(res.RecoveryToken), 0600,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Failed to save token to file:")
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
return errors.New("failed to save token to file")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckInitState sends a checkInitState request to SPIKE Nexus.
|
|
||||||
func CheckInitState(source *workloadapi.X509Source) (entity.InitState, error) {
|
|
||||||
r := reqres.CheckInitStateRequest{}
|
|
||||||
mr, err := json.Marshal(r)
|
|
||||||
if err != nil {
|
|
||||||
return entity.NotInitialized, errors.Join(
|
|
||||||
errors.New(
|
|
||||||
"checkInitState: I am having problem generating the payload",
|
|
||||||
),
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var truer = func(string) bool { return true }
|
|
||||||
client, err := net.CreateMtlsClient(source, truer)
|
|
||||||
body, err := net.Post(client, url.InitState(), mr)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return entity.NotInitialized, errors.Join(
|
|
||||||
errors.New(
|
|
||||||
"checkInitState: I am having problem sending the request",
|
|
||||||
), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var res reqres.CheckInitStateResponse
|
|
||||||
err = json.Unmarshal(body, &res)
|
|
||||||
if err != nil {
|
|
||||||
return entity.NotInitialized, errors.Join(
|
|
||||||
errors.New("checkInitState: Problem parsing response body"),
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
state := res.State
|
|
||||||
|
|
||||||
return state, nil
|
|
||||||
}
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package url
|
||||||
|
|
||||||
|
type ApiAction string
|
||||||
|
|
||||||
|
const KeyApiAction = "action"
|
||||||
|
|
||||||
|
const ActionCheck ApiAction = "check"
|
||||||
|
const ActionGet ApiAction = "get"
|
||||||
|
const ActionDelete ApiAction = "delete"
|
||||||
|
const ActionUndelete ApiAction = "undelete"
|
||||||
|
const ActionList ApiAction = "list"
|
||||||
|
const ActionDefault ApiAction = ""
|
||||||
|
const ActionRead ApiAction = "read"
|
||||||
|
|
||||||
|
type ApiUrl string
|
||||||
|
|
||||||
|
const SpikeNexusUrlSecrets ApiUrl = "/v1/store/secrets"
|
||||||
|
const SpikeNexusUrlSecretsMetadata ApiUrl = "/v1/store/secrets/metadata"
|
||||||
|
const SpikeNexusUrlInit ApiUrl = "/v1/auth/initialization"
|
||||||
|
|
||||||
|
const SpikeNexusUrlPolicy ApiUrl = "/v1/acl/policy"
|
||||||
|
|
||||||
|
const SpikeNexusUrlOperatorRecover ApiUrl = "/v1/operator/recover"
|
||||||
|
const SpikeNexusUrlOperatorRestore ApiUrl = "/v1/operator/restore"
|
||||||
|
|
||||||
|
const SpikeKeeperUrlKeep ApiUrl = "/v1/store/keep"
|
||||||
|
const SpikeKeeperUrlContribute ApiUrl = "/v1/store/contribute"
|
||||||
|
const SpikeKeeperUrlShard ApiUrl = "/v1/store/shard"
|
|
@ -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(SpikeNexusUrlOperatorRestore),
|
||||||
|
)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func Recover() string {
|
||||||
|
u, _ := url.JoinPath(
|
||||||
|
env.NexusApiRoot(),
|
||||||
|
string(SpikeNexusUrlOperatorRecover),
|
||||||
|
)
|
||||||
|
return u
|
||||||
|
}
|
|
@ -10,44 +10,44 @@ import (
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/env"
|
"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 {
|
func PolicyCreate() string {
|
||||||
u, _ := url.JoinPath(
|
u, _ := url.JoinPath(
|
||||||
env.NexusApiRoot(),
|
env.NexusApiRoot(),
|
||||||
string(spikeNexusUrlPolicy),
|
string(SpikeNexusUrlPolicy),
|
||||||
)
|
)
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
// UrlPolicyList returns the URL for listing policies.
|
// PolicyList returns the URL for listing policies.
|
||||||
func PolicyList() string {
|
func PolicyList() string {
|
||||||
u, _ := url.JoinPath(
|
u, _ := url.JoinPath(
|
||||||
env.NexusApiRoot(),
|
env.NexusApiRoot(),
|
||||||
string(spikeNexusUrlPolicy),
|
string(SpikeNexusUrlPolicy),
|
||||||
)
|
)
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
params.Add(keyApiAction, string(actionNexusList))
|
params.Add(KeyApiAction, string(ActionList))
|
||||||
return u + "?" + params.Encode()
|
return u + "?" + params.Encode()
|
||||||
}
|
}
|
||||||
|
|
||||||
// UrlPolicyDelete returns the URL for deleting a policy.
|
// PolicyDelete returns the URL for deleting a policy.
|
||||||
func PolicyDelete() string {
|
func PolicyDelete() string {
|
||||||
u, _ := url.JoinPath(
|
u, _ := url.JoinPath(
|
||||||
env.NexusApiRoot(),
|
env.NexusApiRoot(),
|
||||||
string(spikeNexusUrlPolicy),
|
string(SpikeNexusUrlPolicy),
|
||||||
)
|
)
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
params.Add(keyApiAction, string(actionNexusDelete))
|
params.Add(KeyApiAction, string(ActionDelete))
|
||||||
return u + "?" + params.Encode()
|
return u + "?" + params.Encode()
|
||||||
}
|
}
|
||||||
|
|
||||||
// UrlPolicyGet returns the URL for getting a policy.
|
// PolicyGet returns the URL for getting a policy.
|
||||||
func PolicyGet() string {
|
func PolicyGet() string {
|
||||||
u, _ := url.JoinPath(
|
u, _ := url.JoinPath(
|
||||||
env.NexusApiRoot(),
|
env.NexusApiRoot(),
|
||||||
string(spikeNexusUrlPolicy),
|
string(SpikeNexusUrlPolicy),
|
||||||
)
|
)
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
params.Add(keyApiAction, string(actionNexusGet))
|
params.Add(KeyApiAction, string(ActionGet))
|
||||||
return u + "?" + params.Encode()
|
return u + "?" + params.Encode()
|
||||||
}
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
// \\ 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SecretGet returns the URL for getting a secret.
|
||||||
|
func SecretGet() string {
|
||||||
|
u, _ := url.JoinPath(
|
||||||
|
env.NexusApiRoot(),
|
||||||
|
string(SpikeNexusUrlSecrets),
|
||||||
|
)
|
||||||
|
params := url.Values{}
|
||||||
|
params.Add(KeyApiAction, string(ActionGet))
|
||||||
|
return u + "?" + params.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecretPut returns the URL for putting a secret.
|
||||||
|
func SecretPut() string {
|
||||||
|
u, _ := url.JoinPath(
|
||||||
|
env.NexusApiRoot(),
|
||||||
|
string(SpikeNexusUrlSecrets),
|
||||||
|
)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecretDelete returns the URL for deleting a secret.
|
||||||
|
func SecretDelete() string {
|
||||||
|
u, _ := url.JoinPath(
|
||||||
|
env.NexusApiRoot(),
|
||||||
|
string(SpikeNexusUrlSecrets),
|
||||||
|
)
|
||||||
|
params := url.Values{}
|
||||||
|
params.Add(KeyApiAction, string(ActionDelete))
|
||||||
|
return u + "?" + params.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecretUndelete returns the URL for undeleting a secret.
|
||||||
|
func SecretUndelete() string {
|
||||||
|
u, _ := url.JoinPath(
|
||||||
|
env.NexusApiRoot(),
|
||||||
|
string(SpikeNexusUrlSecrets),
|
||||||
|
)
|
||||||
|
params := url.Values{}
|
||||||
|
params.Add(KeyApiAction, string(ActionUndelete))
|
||||||
|
return u + "?" + params.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecretList returns the URL for listing secrets.
|
||||||
|
func SecretList() string {
|
||||||
|
u, _ := url.JoinPath(
|
||||||
|
env.NexusApiRoot(),
|
||||||
|
string(SpikeNexusUrlSecrets),
|
||||||
|
)
|
||||||
|
params := url.Values{}
|
||||||
|
params.Add(KeyApiAction, string(ActionList))
|
||||||
|
return u + "?" + params.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecretMetadataGet returns the URL for getting a secret metadata.
|
||||||
|
func SecretMetadataGet() string {
|
||||||
|
u, _ := url.JoinPath(
|
||||||
|
env.NexusApiRoot(),
|
||||||
|
string(SpikeNexusUrlSecretsMetadata),
|
||||||
|
)
|
||||||
|
params := url.Values{}
|
||||||
|
params.Add(KeyApiAction, string(ActionGet))
|
||||||
|
return u + "?" + params.Encode()
|
||||||
|
}
|
|
@ -10,23 +10,23 @@ import (
|
||||||
"github.com/spiffe/spike-sdk-go/api/internal/env"
|
"github.com/spiffe/spike-sdk-go/api/internal/env"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UrlInit returns the URL for initializing SPIKE Nexus.
|
// Init returns the URL for initializing SPIKE Nexus.
|
||||||
func Init() string {
|
func Init() string {
|
||||||
u, _ := url.JoinPath(
|
u, _ := url.JoinPath(
|
||||||
env.NexusApiRoot(),
|
env.NexusApiRoot(),
|
||||||
string(spikeNexusUrlInit),
|
string(SpikeNexusUrlInit),
|
||||||
)
|
)
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
// UrlInitState returns the URL for checking the initialization state of
|
// InitState returns the URL for checking the initialization state of
|
||||||
// SPIKE Nexus.
|
// SPIKE Nexus.
|
||||||
func InitState() string {
|
func InitState() string {
|
||||||
u, _ := url.JoinPath(
|
u, _ := url.JoinPath(
|
||||||
env.NexusApiRoot(),
|
env.NexusApiRoot(),
|
||||||
string(spikeNexusUrlInit),
|
string(SpikeNexusUrlInit),
|
||||||
)
|
)
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
params.Add(keyApiAction, string(actionNexusCheck))
|
params.Add(KeyApiAction, string(ActionCheck))
|
||||||
return u + "?" + params.Encode()
|
return u + "?" + params.Encode()
|
||||||
}
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var reader = rand.Read
|
||||||
|
|
||||||
|
const aes256KeySize = 32
|
||||||
|
|
||||||
|
// Aes256Seed generates a cryptographically secure random 256-bit key suitable
|
||||||
|
// for use with AES-256 encryption. The key is returned as a hexadecimal-encoded
|
||||||
|
// string.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: A 64-character hexadecimal string representing the 256-bit key.
|
||||||
|
// - error: Returns nil on successful key generation, or an error if the random
|
||||||
|
// number generation fails.
|
||||||
|
//
|
||||||
|
// The function uses a cryptographically secure random number generator to ensure
|
||||||
|
// the generated key is suitable for cryptographic use. The resulting hex string
|
||||||
|
// can be decoded back to bytes using hex.DecodeString when needed for encryption.
|
||||||
|
func Aes256Seed() (string, error) {
|
||||||
|
// Generate a 256-bit key
|
||||||
|
key := make([]byte, aes256KeySize)
|
||||||
|
|
||||||
|
_, err := reader(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Join(
|
||||||
|
err,
|
||||||
|
errors.New("Aes256Seed: failed to generate random key"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(key), nil
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
const letters = "abcdefghijklmnopqrstuvwxyz" +
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
|
||||||
|
// RandomString generates a cryptographically-unique secure random string.
|
||||||
|
func RandomString(n int) (string, error) {
|
||||||
|
bytes := make([]byte, n)
|
||||||
|
|
||||||
|
if _, err := reader(bytes); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, b := range bytes {
|
||||||
|
bytes[i] = letters[b%byte(len(letters))]
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(bytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token generates a cryptographically-unique secure random string.
|
||||||
|
func Token() string {
|
||||||
|
id, err := RandomString(26)
|
||||||
|
if err != nil {
|
||||||
|
id = fmt.Sprintf("CRYPTO-ERR: %s", err.Error())
|
||||||
|
}
|
||||||
|
return "spike." + id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Id generates a cryptographically-unique secure random string.
|
||||||
|
func Id() string {
|
||||||
|
id, err := RandomString(8)
|
||||||
|
if err != nil {
|
||||||
|
id = fmt.Sprintf("CRYPTO-ERR: %s", err.Error())
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import "crypto/sha256"
|
||||||
|
|
||||||
|
// DeterministicReader implements io.Reader to generate deterministic
|
||||||
|
// pseudo-random data based on a seed. It uses SHA-256 hashing to create a
|
||||||
|
// repeatable stream of bytes.
|
||||||
|
type DeterministicReader struct {
|
||||||
|
data []byte
|
||||||
|
pos int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read implements io.Reader interface. It returns deterministic data by reading
|
||||||
|
// from the internal buffer and generating new data using SHA-256 when needed.
|
||||||
|
//
|
||||||
|
// If the current position reaches the end of the data buffer, it generates
|
||||||
|
// a new block by hashing the current data. This ensures a continuous,
|
||||||
|
// deterministic stream of data.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - p []byte: Buffer to read data into
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - n int: Number of bytes read
|
||||||
|
// - err error: Always nil as reads never fail
|
||||||
|
func (r *DeterministicReader) Read(p []byte) (n int, err error) {
|
||||||
|
if r.pos >= len(r.data) {
|
||||||
|
// Generate more deterministic data if needed
|
||||||
|
hash := sha256.Sum256(r.data)
|
||||||
|
r.data = hash[:]
|
||||||
|
r.pos = 0
|
||||||
|
}
|
||||||
|
n = copy(p, r.data[r.pos:])
|
||||||
|
r.pos += n
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDeterministicReader creates a new DeterministicReader initialized with
|
||||||
|
// the SHA-256 hash of the provided seed data.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - seed []byte: Initial seed data to generate the deterministic stream
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *DeterministicReader: New reader instance initialized with the seed
|
||||||
|
func NewDeterministicReader(seed []byte) *DeterministicReader {
|
||||||
|
hash := sha256.Sum256(seed)
|
||||||
|
return &DeterministicReader{
|
||||||
|
data: hash[:],
|
||||||
|
pos: 0,
|
||||||
|
}
|
||||||
|
}
|
32
go.mod
32
go.mod
|
@ -1,18 +1,28 @@
|
||||||
module github.com/spiffe/spike-sdk-go
|
module github.com/spiffe/spike-sdk-go
|
||||||
|
|
||||||
go 1.23.3
|
go 1.24
|
||||||
|
|
||||||
require github.com/spiffe/go-spiffe/v2 v2.4.0
|
toolchain go1.24.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/spiffe/go-spiffe/v2 v2.5.0
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/zeebo/errs v1.3.0 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||||
golang.org/x/crypto v0.26.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
golang.org/x/net v0.28.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
golang.org/x/sys v0.24.0 // indirect
|
github.com/zeebo/errs v1.4.0 // indirect
|
||||||
golang.org/x/text v0.17.0 // indirect
|
golang.org/x/net v0.39.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
google.golang.org/grpc v1.67.1 // indirect
|
golang.org/x/text v0.24.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
|
||||||
|
google.golang.org/grpc v1.72.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
163
go.sum
163
go.sum
|
@ -1,32 +1,171 @@
|
||||||
|
cel.dev/expr v0.16.0 h1:yloc84fytn4zmJX2GU3TkXGsaieaV7dQ057Qs4sIG2Y=
|
||||||
|
cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg=
|
||||||
|
cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0=
|
||||||
|
cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
|
||||||
|
cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI=
|
||||||
|
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
|
||||||
|
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
|
||||||
|
cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
|
||||||
|
cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
|
||||||
|
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||||
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw=
|
||||||
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM=
|
||||||
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 h1:f2Qw/Ehhimh5uO1fayV0QIW7DShEQqhtUfhYc+cBPlw=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 h1:N+3sFI5GUjRKBi+i0TxYVST9h4Ie192jJWpHvthBBgg=
|
||||||
|
github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||||
|
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI=
|
||||||
|
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||||
|
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk=
|
||||||
|
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
|
github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
|
github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||||
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY=
|
||||||
|
github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||||
|
github.com/golang/glog v1.2.3 h1:oDTdz9f5VGVVNGu/Q7UXKWYsD0873HXLHdJUNBsSEKM=
|
||||||
|
github.com/golang/glog v1.2.3/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||||
|
github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=
|
||||||
|
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||||
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/spiffe/go-spiffe/v2 v2.4.0 h1:j/FynG7hi2azrBG5cvjRcnQ4sux/VNj8FAVc99Fl66c=
|
github.com/spiffe/go-spiffe/v2 v2.4.0 h1:j/FynG7hi2azrBG5cvjRcnQ4sux/VNj8FAVc99Fl66c=
|
||||||
github.com/spiffe/go-spiffe/v2 v2.4.0/go.mod h1:m5qJ1hGzjxjtrkGHZupoXHo/FDWwCB1MdSyBzfHugx0=
|
github.com/spiffe/go-spiffe/v2 v2.4.0/go.mod h1:m5qJ1hGzjxjtrkGHZupoXHo/FDWwCB1MdSyBzfHugx0=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
|
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
|
||||||
github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
|
github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
|
||||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
|
||||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
|
||||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
go.opentelemetry.io/contrib/detectors/gcp v1.32.0 h1:P78qWqkLSShicHmAzfECaTgvslqHxblNE9j62Ws1NK8=
|
||||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
go.opentelemetry.io/contrib/detectors/gcp v1.32.0/go.mod h1:TVqo0Sda4Cv8gCIixd7LuLwW4EylumVWfhjZJjDD4DU=
|
||||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao=
|
||||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
||||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
||||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||||
|
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
||||||
|
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
||||||
|
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||||
|
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
||||||
|
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
||||||
|
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||||
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
|
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||||
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
|
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||||
|
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
|
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||||
|
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||||
|
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
|
||||||
|
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
|
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||||
|
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
|
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||||
|
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||||
|
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||||
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||||
|
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||||
|
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
|
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||||
|
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a h1:OAiGFfOiA0v9MRYsSidp3ubZaBnteRUyn3xB2ZQ5G/E=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||||
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
|
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
|
||||||
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||||
|
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
|
||||||
|
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
|
||||||
|
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
|
||||||
|
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||||
|
google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI=
|
||||||
|
google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0=
|
||||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||||
|
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||||
|
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
# \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
# \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
VERSION="v0.5.13"
|
||||||
|
|
||||||
|
git tag -s "$VERSION" -m "$VERSION"
|
||||||
|
git push origin --tags
|
|
@ -0,0 +1,54 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package kv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Delete marks secret versions as deleted for a given path. If no versions are
|
||||||
|
// specified, it marks only the current version as deleted. If specific versions
|
||||||
|
// are provided, it marks each existing version in the list as deleted. The
|
||||||
|
// deletion is performed by setting the DeletedTime to the current time. If the
|
||||||
|
// path doesn't exist, the function returns without making any changes.
|
||||||
|
func (kv *KV) Delete(path string, versions []int) error {
|
||||||
|
secret, exists := kv.data[path]
|
||||||
|
if !exists {
|
||||||
|
return ErrItemNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
cv := secret.Metadata.CurrentVersion
|
||||||
|
|
||||||
|
// If no versions specified, mark the latest version as deleted
|
||||||
|
if len(versions) == 0 {
|
||||||
|
|
||||||
|
if v, exists := secret.Versions[cv]; exists {
|
||||||
|
v.DeletedTime = &now // Mark as deleted.
|
||||||
|
secret.Versions[cv] = v
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete specific versions
|
||||||
|
for _, version := range versions {
|
||||||
|
if version == 0 {
|
||||||
|
v, exists := secret.Versions[cv]
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
v.DeletedTime = &now // Mark as deleted.
|
||||||
|
secret.Versions[cv] = v
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, exists := secret.Versions[version]; exists {
|
||||||
|
v.DeletedTime = &now // Mark as deleted.
|
||||||
|
secret.Versions[version] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
package kv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKV_Delete(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setup func() *KV
|
||||||
|
path string
|
||||||
|
versions []int
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "non_existent_path",
|
||||||
|
setup: func() *KV {
|
||||||
|
return &KV{
|
||||||
|
data: make(map[string]*Value),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
path: "non/existent/path",
|
||||||
|
versions: nil,
|
||||||
|
wantErr: ErrItemNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "delete_current_version_no_versions_specified",
|
||||||
|
setup: func() *KV {
|
||||||
|
kv := &KV{
|
||||||
|
data: make(map[string]*Value),
|
||||||
|
}
|
||||||
|
kv.data["test/path"] = &Value{
|
||||||
|
Metadata: Metadata{
|
||||||
|
CurrentVersion: 1,
|
||||||
|
},
|
||||||
|
Versions: map[int]Version{
|
||||||
|
1: {
|
||||||
|
Data: map[string]string{
|
||||||
|
"key": "test_value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return kv
|
||||||
|
},
|
||||||
|
path: "test/path",
|
||||||
|
versions: nil,
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "delete_specific_versions",
|
||||||
|
setup: func() *KV {
|
||||||
|
kv := &KV{
|
||||||
|
data: make(map[string]*Value),
|
||||||
|
}
|
||||||
|
kv.data["test/path"] = &Value{
|
||||||
|
Metadata: Metadata{
|
||||||
|
CurrentVersion: 2,
|
||||||
|
},
|
||||||
|
Versions: map[int]Version{
|
||||||
|
1: {
|
||||||
|
Data: map[string]string{
|
||||||
|
"key": "value1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
Data: map[string]string{
|
||||||
|
"key": "value2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return kv
|
||||||
|
},
|
||||||
|
path: "test/path",
|
||||||
|
versions: []int{1, 2},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
kv := tt.setup()
|
||||||
|
err := kv.Delete(tt.path, tt.versions)
|
||||||
|
|
||||||
|
if err != tt.wantErr {
|
||||||
|
t.Errorf("Delete() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantErr == nil {
|
||||||
|
secret, exists := kv.data[tt.path]
|
||||||
|
if !exists {
|
||||||
|
t.Errorf("Value should still exist after deletion")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tt.versions) == 0 {
|
||||||
|
cv := secret.Metadata.CurrentVersion
|
||||||
|
if v, exists := secret.Versions[cv]; exists {
|
||||||
|
if v.DeletedTime == nil {
|
||||||
|
t.Errorf("Current version should be marked as deleted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, version := range tt.versions {
|
||||||
|
if version == 0 {
|
||||||
|
version = secret.Metadata.CurrentVersion
|
||||||
|
}
|
||||||
|
if v, exists := secret.Versions[version]; exists {
|
||||||
|
if v.DeletedTime == nil {
|
||||||
|
t.Errorf("Version %d should be marked as deleted", version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// Package kv provides a secure in-memory key-value store for managing secret
|
||||||
|
// data. The store supports versioning of secrets, allowing operations on
|
||||||
|
// specific versions and tracking deleted versions. It is designed for scenarios
|
||||||
|
// where secrets need to be securely managed, updated, and deleted.
|
||||||
|
package kv
|
|
@ -0,0 +1,56 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package kv
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Version represents a single version of a secret's data along with its
|
||||||
|
// metadata. Each version maintains its own set of key-value pairs and tracking
|
||||||
|
// information.
|
||||||
|
type Version struct {
|
||||||
|
// Data contains the actual key-value pairs stored in this version
|
||||||
|
Data map[string]string
|
||||||
|
|
||||||
|
// CreatedTime is when this version was created
|
||||||
|
CreatedTime time.Time
|
||||||
|
|
||||||
|
// Version is the numeric identifier for this version
|
||||||
|
Version int
|
||||||
|
|
||||||
|
// DeletedTime indicates when this version was marked as deleted
|
||||||
|
// A nil value means the version is active/not deleted
|
||||||
|
DeletedTime *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata tracks control information for a secret and its versions.
|
||||||
|
// It maintains version boundaries and timestamps for the overall secret.
|
||||||
|
type Metadata struct {
|
||||||
|
// CurrentVersion is the newest/latest version number of the secret
|
||||||
|
CurrentVersion int
|
||||||
|
|
||||||
|
// OldestVersion is the oldest available version number of the secret
|
||||||
|
OldestVersion int
|
||||||
|
|
||||||
|
// CreatedTime is when the secret was first created
|
||||||
|
CreatedTime time.Time
|
||||||
|
|
||||||
|
// UpdatedTime is when the secret was last modified
|
||||||
|
UpdatedTime time.Time
|
||||||
|
|
||||||
|
// MaxVersions is the maximum number of versions to retain
|
||||||
|
// When exceeded, older versions are automatically pruned
|
||||||
|
MaxVersions int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value represents a versioned collection of key-value pairs stored at a
|
||||||
|
// specific path. It maintains both the version history and metadata about the
|
||||||
|
// collection as a whole.
|
||||||
|
type Value struct {
|
||||||
|
// Versions maps version numbers to their corresponding Version objects
|
||||||
|
Versions map[int]Version
|
||||||
|
|
||||||
|
// Metadata contains control information about this secret
|
||||||
|
Metadata Metadata
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package kv
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrVersionNotFound = errors.New("version not found")
|
||||||
|
ErrItemNotFound = errors.New("item not found")
|
||||||
|
ErrItemSoftDeleted = errors.New("item marked as deleted")
|
||||||
|
ErrInvalidVersion = errors.New("invalid version")
|
||||||
|
)
|
|
@ -0,0 +1,80 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package kv
|
||||||
|
|
||||||
|
// Get retrieves a versioned key-value data map from the store at the specified
|
||||||
|
// path.
|
||||||
|
//
|
||||||
|
// The function supports versioned data retrieval with the following behavior:
|
||||||
|
// - If version is 0, returns the current version of the data
|
||||||
|
// - If version is specified, returns that specific version if it exists
|
||||||
|
// - Returns nil and false if the path doesn't exist
|
||||||
|
// - Returns nil and false if the specified version doesn't exist
|
||||||
|
// - Returns nil and false if the version has been deleted
|
||||||
|
// (DeletedTime is set)
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - path: The path to retrieve data from
|
||||||
|
// - version: The specific version to retrieve (0 for current version)
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - map[string]string: The key-value data at the specified path and version
|
||||||
|
// - bool: true if data was found and is valid, false otherwise
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// kv := &KV{}
|
||||||
|
// // Get current version
|
||||||
|
// data, exists := kv.Get("secret/myapp", 0)
|
||||||
|
//
|
||||||
|
// // Get specific version
|
||||||
|
// historicalData, exists := kv.Get("secret/myapp", 2)
|
||||||
|
func (kv *KV) Get(path string, version int) (map[string]string, error) {
|
||||||
|
secret, exists := kv.data[path]
|
||||||
|
if !exists {
|
||||||
|
return nil, ErrItemNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// #region debug
|
||||||
|
// fmt.Println("########")
|
||||||
|
// vv := secret.Versions
|
||||||
|
// for i, v := range vv {
|
||||||
|
// fmt.Println("version", i, "version:", v.Version, "created:",
|
||||||
|
// v.CreatedTime, "deleted:", v.DeletedTime, "data:", v.Data)
|
||||||
|
// }
|
||||||
|
// fmt.Println("########")
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// If version not specified, use current version
|
||||||
|
if version == 0 {
|
||||||
|
version = secret.Metadata.CurrentVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
v, exists := secret.Versions[version]
|
||||||
|
if !exists || v.DeletedTime != nil {
|
||||||
|
return nil, ErrItemSoftDeleted
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRawSecret retrieves a raw secret from the store at the specified path.
|
||||||
|
// This function is similar to Get, but it returns the raw Value object instead
|
||||||
|
// of the key-value data map.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - path: The path to retrieve the secret from
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *Value: The secret at the specified path, or nil if it doesn't exist
|
||||||
|
// or has been deleted.
|
||||||
|
func (kv *KV) GetRawSecret(path string) (*Value, error) {
|
||||||
|
secret, exists := kv.data[path]
|
||||||
|
if !exists {
|
||||||
|
return nil, ErrItemNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return secret, nil
|
||||||
|
}
|
|
@ -0,0 +1,281 @@
|
||||||
|
package kv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKV_Get(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setup func() *KV
|
||||||
|
path string
|
||||||
|
version int
|
||||||
|
want map[string]string
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "non_existent_path",
|
||||||
|
setup: func() *KV {
|
||||||
|
return &KV{
|
||||||
|
data: make(map[string]*Value),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
path: "non/existent/path",
|
||||||
|
version: 0,
|
||||||
|
want: nil,
|
||||||
|
wantErr: ErrItemNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "get_current_version",
|
||||||
|
setup: func() *KV {
|
||||||
|
kv := &KV{
|
||||||
|
data: make(map[string]*Value),
|
||||||
|
}
|
||||||
|
kv.data["test/path"] = &Value{
|
||||||
|
Metadata: Metadata{
|
||||||
|
CurrentVersion: 1,
|
||||||
|
},
|
||||||
|
Versions: map[int]Version{
|
||||||
|
1: {
|
||||||
|
Data: map[string]string{
|
||||||
|
"key": "current_value",
|
||||||
|
},
|
||||||
|
Version: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return kv
|
||||||
|
},
|
||||||
|
path: "test/path",
|
||||||
|
version: 0,
|
||||||
|
want: map[string]string{
|
||||||
|
"key": "current_value",
|
||||||
|
},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "get_specific_version",
|
||||||
|
setup: func() *KV {
|
||||||
|
kv := &KV{
|
||||||
|
data: make(map[string]*Value),
|
||||||
|
}
|
||||||
|
kv.data["test/path"] = &Value{
|
||||||
|
Metadata: Metadata{
|
||||||
|
CurrentVersion: 2,
|
||||||
|
},
|
||||||
|
Versions: map[int]Version{
|
||||||
|
1: {
|
||||||
|
Data: map[string]string{
|
||||||
|
"key": "old_value",
|
||||||
|
},
|
||||||
|
Version: 1,
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
Data: map[string]string{
|
||||||
|
"key": "current_value",
|
||||||
|
},
|
||||||
|
Version: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return kv
|
||||||
|
},
|
||||||
|
path: "test/path",
|
||||||
|
version: 1,
|
||||||
|
want: map[string]string{
|
||||||
|
"key": "old_value",
|
||||||
|
},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "get_deleted_version",
|
||||||
|
setup: func() *KV {
|
||||||
|
deletedTime := time.Now()
|
||||||
|
kv := &KV{
|
||||||
|
data: make(map[string]*Value),
|
||||||
|
}
|
||||||
|
kv.data["test/path"] = &Value{
|
||||||
|
Metadata: Metadata{
|
||||||
|
CurrentVersion: 1,
|
||||||
|
},
|
||||||
|
Versions: map[int]Version{
|
||||||
|
1: {
|
||||||
|
Data: map[string]string{
|
||||||
|
"key": "deleted_value",
|
||||||
|
},
|
||||||
|
Version: 1,
|
||||||
|
DeletedTime: &deletedTime,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return kv
|
||||||
|
},
|
||||||
|
path: "test/path",
|
||||||
|
version: 1,
|
||||||
|
want: nil,
|
||||||
|
wantErr: ErrItemSoftDeleted,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non_existent_version",
|
||||||
|
setup: func() *KV {
|
||||||
|
kv := &KV{
|
||||||
|
data: make(map[string]*Value),
|
||||||
|
}
|
||||||
|
kv.data["test/path"] = &Value{
|
||||||
|
Metadata: Metadata{
|
||||||
|
CurrentVersion: 1,
|
||||||
|
},
|
||||||
|
Versions: map[int]Version{
|
||||||
|
1: {
|
||||||
|
Data: map[string]string{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
Version: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return kv
|
||||||
|
},
|
||||||
|
path: "test/path",
|
||||||
|
version: 999,
|
||||||
|
want: nil,
|
||||||
|
wantErr: ErrItemSoftDeleted,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
kv := tt.setup()
|
||||||
|
got, err := kv.Get(tt.path, tt.version)
|
||||||
|
|
||||||
|
if err != tt.wantErr {
|
||||||
|
t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantErr == nil {
|
||||||
|
if len(got) != len(tt.want) {
|
||||||
|
t.Errorf("Get() got = %v, want %v", got, tt.want)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for k, v := range got {
|
||||||
|
if tt.want[k] != v {
|
||||||
|
t.Errorf("Get() got[%s] = %v, want[%s] = %v", k, v, k, tt.want[k])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKV_GetRawSecret(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setup func() *KV
|
||||||
|
path string
|
||||||
|
want *Value
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "non_existent_path",
|
||||||
|
setup: func() *KV {
|
||||||
|
return &KV{
|
||||||
|
data: make(map[string]*Value),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
path: "non/existent/path",
|
||||||
|
want: nil,
|
||||||
|
wantErr: ErrItemNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "existing_secret",
|
||||||
|
setup: func() *KV {
|
||||||
|
secret := &Value{
|
||||||
|
Metadata: Metadata{
|
||||||
|
CurrentVersion: 1,
|
||||||
|
},
|
||||||
|
Versions: map[int]Version{
|
||||||
|
1: {
|
||||||
|
Data: map[string]string{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
Version: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
kv := &KV{
|
||||||
|
data: make(map[string]*Value),
|
||||||
|
}
|
||||||
|
kv.data["test/path"] = secret
|
||||||
|
return kv
|
||||||
|
},
|
||||||
|
path: "test/path",
|
||||||
|
want: &Value{
|
||||||
|
Metadata: Metadata{
|
||||||
|
CurrentVersion: 1,
|
||||||
|
},
|
||||||
|
Versions: map[int]Version{
|
||||||
|
1: {
|
||||||
|
Data: map[string]string{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
Version: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
kv := tt.setup()
|
||||||
|
got, err := kv.GetRawSecret(tt.path)
|
||||||
|
|
||||||
|
if err != tt.wantErr {
|
||||||
|
t.Errorf("GetRawSecret() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantErr == nil {
|
||||||
|
if got.Metadata.CurrentVersion != tt.want.Metadata.CurrentVersion {
|
||||||
|
t.Errorf("GetRawSecret() got CurrentVersion = %v, want %v",
|
||||||
|
got.Metadata.CurrentVersion, tt.want.Metadata.CurrentVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(got.Versions) != len(tt.want.Versions) {
|
||||||
|
t.Errorf("GetRawSecret() got Versions length = %v, want %v",
|
||||||
|
len(got.Versions), len(tt.want.Versions))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for version, gotV := range got.Versions {
|
||||||
|
wantV, exists := tt.want.Versions[version]
|
||||||
|
if !exists {
|
||||||
|
t.Errorf("GetRawSecret() unexpected version %v in result", version)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotV.Version != wantV.Version {
|
||||||
|
t.Errorf("GetRawSecret() version %v: got Version = %v, want %v",
|
||||||
|
version, gotV.Version, wantV.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(gotV.Data) != len(wantV.Data) {
|
||||||
|
t.Errorf("GetRawSecret() version %v: got Data length = %v, want %v",
|
||||||
|
version, len(gotV.Data), len(wantV.Data))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range gotV.Data {
|
||||||
|
if wantV.Data[k] != v {
|
||||||
|
t.Errorf("GetRawSecret() version %v: got Data[%s] = %v, want %v",
|
||||||
|
version, k, v, wantV.Data[k])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package kv
|
||||||
|
|
||||||
|
// ImportSecrets hydrates the key-value store with secrets loaded from
|
||||||
|
// persistent storage or a similar medium. It takes a map of path to secret
|
||||||
|
// values and adds them to the in-memory store. This is typically used during
|
||||||
|
// initialization or recovery after a system crash.
|
||||||
|
//
|
||||||
|
// If a secret already exists in the store, it will be overwritten with the
|
||||||
|
// imported value. The method preserves all version history and metadata from
|
||||||
|
// the imported secrets.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// secrets, err := persistentStore.LoadAllSecrets(context.Background())
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatalf("Failed to load secrets: %v", err)
|
||||||
|
// }
|
||||||
|
// kvStore.ImportSecrets(secrets)
|
||||||
|
func (kv *KV) ImportSecrets(secrets map[string]*Value) {
|
||||||
|
for path, secret := range secrets {
|
||||||
|
// Create a deep copy of the secret to avoid sharing memory
|
||||||
|
newSecret := &Value{
|
||||||
|
Versions: make(map[int]Version, len(secret.Versions)),
|
||||||
|
Metadata: Metadata{
|
||||||
|
CreatedTime: secret.Metadata.CreatedTime,
|
||||||
|
UpdatedTime: secret.Metadata.UpdatedTime,
|
||||||
|
MaxVersions: kv.maxSecretVersions, // Use the KV store's setting
|
||||||
|
CurrentVersion: secret.Metadata.CurrentVersion,
|
||||||
|
OldestVersion: secret.Metadata.OldestVersion,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy all versions
|
||||||
|
for versionNum, version := range secret.Versions {
|
||||||
|
// Deep copy the data map
|
||||||
|
dataCopy := make(map[string]string, len(version.Data))
|
||||||
|
for k, v := range version.Data {
|
||||||
|
dataCopy[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the version copy
|
||||||
|
versionCopy := Version{
|
||||||
|
Data: dataCopy,
|
||||||
|
CreatedTime: version.CreatedTime,
|
||||||
|
Version: versionNum,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy deleted time if set
|
||||||
|
if version.DeletedTime != nil {
|
||||||
|
deletedTime := *version.DeletedTime
|
||||||
|
versionCopy.DeletedTime = &deletedTime
|
||||||
|
}
|
||||||
|
|
||||||
|
newSecret.Versions[versionNum] = versionCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the copied secret
|
||||||
|
kv.data[path] = newSecret
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package kv
|
||||||
|
|
||||||
|
// KV represents an in-memory key-value store with versioning
|
||||||
|
type KV struct {
|
||||||
|
maxSecretVersions int
|
||||||
|
data map[string]*Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config represents the configuration for a KV instance
|
||||||
|
type Config struct {
|
||||||
|
MaxSecretVersions int
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new KV instance
|
||||||
|
func New(config Config) *KV {
|
||||||
|
return &KV{
|
||||||
|
maxSecretVersions: config.MaxSecretVersions,
|
||||||
|
data: make(map[string]*Value),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package kv
|
||||||
|
|
||||||
|
// List returns a slice containing all keys stored in the key-value store.
|
||||||
|
// The order of keys in the returned slice is not guaranteed to be stable
|
||||||
|
// between calls.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - []string: A slice containing all keys present in the store
|
||||||
|
func (kv *KV) List() []string {
|
||||||
|
keys := make([]string, 0, len(kv.data))
|
||||||
|
|
||||||
|
for k := range kv.data {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package kv
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestKV_List(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setup func() *KV
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty_store",
|
||||||
|
setup: func() *KV {
|
||||||
|
return &KV{
|
||||||
|
data: make(map[string]*Value),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
want: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non_empty_store",
|
||||||
|
setup: func() *KV {
|
||||||
|
return &KV{
|
||||||
|
data: map[string]*Value{
|
||||||
|
"test/path": {
|
||||||
|
Metadata: Metadata{
|
||||||
|
CurrentVersion: 1,
|
||||||
|
},
|
||||||
|
Versions: map[int]Version{
|
||||||
|
1: {
|
||||||
|
Data: map[string]string{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
Version: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
want: []string{"test/path"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
kv := tt.setup()
|
||||||
|
got := kv.List()
|
||||||
|
|
||||||
|
if len(got) != len(tt.want) {
|
||||||
|
t.Errorf("got %v want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range got {
|
||||||
|
if got[i] != tt.want[i] {
|
||||||
|
t.Errorf("got %v want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package kv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Put stores a new version of key-value pairs at the specified path in the
|
||||||
|
// store. It implements automatic versioning with a maximum of 3 versions per
|
||||||
|
// path.
|
||||||
|
//
|
||||||
|
// When storing values:
|
||||||
|
// - If the path doesn't exist, it creates a new secret with initial metadata
|
||||||
|
// - Each put operation creates a new version with an incremented version
|
||||||
|
// number
|
||||||
|
// - Old versions are automatically pruned when exceeding MaxVersions
|
||||||
|
// (default: 10)
|
||||||
|
// - Timestamps are updated for both creation and modification times
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - path: The location where the secret will be stored
|
||||||
|
// - values: A map of key-value pairs to store at this path
|
||||||
|
//
|
||||||
|
// The function maintains metadata including:
|
||||||
|
// - CreatedTime: When the secret was first created
|
||||||
|
// - UpdatedTime: When the most recent version was added
|
||||||
|
// - CurrentVersion: The latest version number
|
||||||
|
// - OldestVersion: The oldest available version number
|
||||||
|
// - MaxVersions: Maximum number of versions to keep (fixed at 10)
|
||||||
|
func (kv *KV) Put(path string, values map[string]string) {
|
||||||
|
rightNow := time.Now()
|
||||||
|
|
||||||
|
secret, exists := kv.data[path]
|
||||||
|
if !exists {
|
||||||
|
secret = &Value{
|
||||||
|
Versions: make(map[int]Version),
|
||||||
|
Metadata: Metadata{
|
||||||
|
CreatedTime: rightNow,
|
||||||
|
UpdatedTime: rightNow,
|
||||||
|
MaxVersions: kv.maxSecretVersions,
|
||||||
|
// Versions start at 1, so that passing 0 as version will
|
||||||
|
// default to the current version.
|
||||||
|
CurrentVersion: 1,
|
||||||
|
OldestVersion: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
kv.data[path] = secret
|
||||||
|
} else {
|
||||||
|
secret.Metadata.CurrentVersion++
|
||||||
|
}
|
||||||
|
|
||||||
|
newVersion := secret.Metadata.CurrentVersion
|
||||||
|
|
||||||
|
// Add new version
|
||||||
|
secret.Versions[newVersion] = Version{
|
||||||
|
Data: values,
|
||||||
|
CreatedTime: rightNow,
|
||||||
|
Version: newVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update metadata
|
||||||
|
secret.Metadata.UpdatedTime = rightNow
|
||||||
|
|
||||||
|
// Cleanup old versions if exceeding MaxVersions
|
||||||
|
var deletedAny bool
|
||||||
|
for version := range secret.Versions {
|
||||||
|
if newVersion-version >= secret.Metadata.MaxVersions {
|
||||||
|
delete(secret.Versions, version)
|
||||||
|
deletedAny = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if deletedAny {
|
||||||
|
oldestVersion := secret.Metadata.CurrentVersion
|
||||||
|
for version := range secret.Versions {
|
||||||
|
if version < oldestVersion {
|
||||||
|
oldestVersion = version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
secret.Metadata.OldestVersion = oldestVersion
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
package kv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKV_Put(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setup func() *KV
|
||||||
|
path string
|
||||||
|
values map[string]string
|
||||||
|
versions []int
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
setup: func() *KV {
|
||||||
|
return &KV{
|
||||||
|
data: make(map[string]*Value),
|
||||||
|
maxSecretVersions: 10,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
name: "it creates a new secret with initial metadata if the path doesn't exist",
|
||||||
|
path: "new/secret/path",
|
||||||
|
versions: []int{1},
|
||||||
|
values: map[string]string{"key": "value"},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "it creates a new version with an incremented version number",
|
||||||
|
setup: func() *KV {
|
||||||
|
kv := &KV{data: make(map[string]*Value), maxSecretVersions: 10}
|
||||||
|
kv.Put("existing/secret/path", map[string]string{"key": "value1"})
|
||||||
|
return kv
|
||||||
|
},
|
||||||
|
path: "existing/secret/path",
|
||||||
|
versions: []int{1, 2},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "it automatically prunes old versions when exceeding MaxVersions",
|
||||||
|
setup: func() *KV {
|
||||||
|
kv := &KV{data: make(map[string]*Value), maxSecretVersions: 2}
|
||||||
|
kv.Put("prune/old/versions", map[string]string{"key": "value1"})
|
||||||
|
kv.Put("prune/old/versions", map[string]string{"key": "value2"})
|
||||||
|
kv.Put("prune/old/versions", map[string]string{"key": "value3"})
|
||||||
|
return kv
|
||||||
|
},
|
||||||
|
path: "prune/old/versions",
|
||||||
|
versions: []int{4, 3},
|
||||||
|
values: map[string]string{
|
||||||
|
"key": "value4",
|
||||||
|
},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "it updates timestamps for both creation and modification times",
|
||||||
|
setup: func() *KV {
|
||||||
|
kv := &KV{data: make(map[string]*Value), maxSecretVersions: 10}
|
||||||
|
kv.Put("update/timestamps", map[string]string{"key": "value1"})
|
||||||
|
return kv
|
||||||
|
},
|
||||||
|
versions: []int{1, 2},
|
||||||
|
path: "update/timestamps",
|
||||||
|
values: map[string]string{"key": "value2"},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
kv := tt.setup()
|
||||||
|
kv.Put(tt.path, tt.values)
|
||||||
|
|
||||||
|
secret, exists := kv.data[tt.path]
|
||||||
|
if !exists {
|
||||||
|
t.Fatalf("expected secret to exist at path %q", tt.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(secret.Versions) != len(tt.versions) {
|
||||||
|
t.Fatalf("expected %d versions, got %d", len(tt.versions), len(secret.Versions))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, version := range tt.versions {
|
||||||
|
if _, exists := secret.Versions[version]; !exists {
|
||||||
|
t.Fatalf("expected version %d to exist", version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package kv
|
||||||
|
|
||||||
|
// Undelete restores previously deleted versions of a secret at the specified
|
||||||
|
// path. It sets the DeletedTime to nil for each specified version that exists.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - path: The location of the secret in the store
|
||||||
|
// - versions: A slice of version numbers to undelete
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: ErrItemNotFound if the path doesn't exist, nil on success
|
||||||
|
//
|
||||||
|
// If a version number in the versions slice doesn't exist, it is silently
|
||||||
|
// skipped without returning an error. Only existing versions are modified.
|
||||||
|
func (kv *KV) Undelete(path string, versions []int) error {
|
||||||
|
secret, exists := kv.data[path]
|
||||||
|
if !exists {
|
||||||
|
return ErrItemNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
cv := secret.Metadata.CurrentVersion
|
||||||
|
|
||||||
|
// If no versions specified, mark the latest version as undeleted
|
||||||
|
if len(versions) == 0 {
|
||||||
|
if v, exists := secret.Versions[cv]; exists {
|
||||||
|
v.DeletedTime = nil // Mark as undeleted.
|
||||||
|
secret.Versions[cv] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete specific versions
|
||||||
|
for _, version := range versions {
|
||||||
|
if version == 0 {
|
||||||
|
v, exists := secret.Versions[cv]
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
v.DeletedTime = nil // Mark as undeleted.
|
||||||
|
secret.Versions[cv] = v
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, exists := secret.Versions[version]; exists {
|
||||||
|
v.DeletedTime = nil // Mark as undeleted.
|
||||||
|
secret.Versions[version] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,182 @@
|
||||||
|
package kv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKV_Undelete(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setup func() *KV
|
||||||
|
path string
|
||||||
|
values map[string]string
|
||||||
|
versions []int
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "undelete latest version if no versions specified",
|
||||||
|
setup: func() *KV {
|
||||||
|
kv := &KV{
|
||||||
|
data: make(map[string]*Value),
|
||||||
|
}
|
||||||
|
kv.data["test/path"] = &Value{
|
||||||
|
Metadata: Metadata{
|
||||||
|
CurrentVersion: 1,
|
||||||
|
},
|
||||||
|
Versions: map[int]Version{
|
||||||
|
1: {
|
||||||
|
Data: map[string]string{"key": "value"},
|
||||||
|
Version: 1,
|
||||||
|
DeletedTime: &time.Time{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return kv
|
||||||
|
},
|
||||||
|
path: "test/path",
|
||||||
|
versions: []int{},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "undelete spesific versions",
|
||||||
|
setup: func() *KV {
|
||||||
|
kv := &KV{
|
||||||
|
data: make(map[string]*Value),
|
||||||
|
}
|
||||||
|
kv.data["test/path"] = &Value{
|
||||||
|
Metadata: Metadata{
|
||||||
|
CurrentVersion: 2,
|
||||||
|
},
|
||||||
|
Versions: map[int]Version{
|
||||||
|
1: {
|
||||||
|
Data: map[string]string{"key": "value1"},
|
||||||
|
Version: 1,
|
||||||
|
DeletedTime: &time.Time{},
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
Data: map[string]string{"key": "value2"},
|
||||||
|
Version: 2,
|
||||||
|
DeletedTime: &time.Time{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return kv
|
||||||
|
},
|
||||||
|
path: "test/path",
|
||||||
|
versions: []int{1, 2},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "if secret does not exist",
|
||||||
|
setup: func() *KV {
|
||||||
|
return &KV{
|
||||||
|
data: make(map[string]*Value),
|
||||||
|
maxSecretVersions: 10,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
path: "path/undelete/notExist",
|
||||||
|
versions: []int{1},
|
||||||
|
values: map[string]string{"key": "value"},
|
||||||
|
wantErr: ErrItemNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "skip non-existent versions",
|
||||||
|
setup: func() *KV {
|
||||||
|
kv := &KV{
|
||||||
|
data: make(map[string]*Value),
|
||||||
|
}
|
||||||
|
kv.data["test/path"] = &Value{
|
||||||
|
Metadata: Metadata{
|
||||||
|
CurrentVersion: 1,
|
||||||
|
},
|
||||||
|
Versions: map[int]Version{
|
||||||
|
1: {
|
||||||
|
Data: map[string]string{"key": "value"},
|
||||||
|
Version: 1,
|
||||||
|
DeletedTime: &time.Time{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return kv
|
||||||
|
},
|
||||||
|
path: "test/path",
|
||||||
|
versions: []int{1, 2},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "skip non-existent versions",
|
||||||
|
setup: func() *KV {
|
||||||
|
kv := &KV{
|
||||||
|
data: make(map[string]*Value),
|
||||||
|
}
|
||||||
|
kv.data["test/path"] = &Value{
|
||||||
|
Metadata: Metadata{
|
||||||
|
CurrentVersion: 2,
|
||||||
|
},
|
||||||
|
Versions: map[int]Version{
|
||||||
|
1: {
|
||||||
|
Data: map[string]string{"key": "value"},
|
||||||
|
Version: 1,
|
||||||
|
DeletedTime: &time.Time{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return kv
|
||||||
|
},
|
||||||
|
path: "test/path",
|
||||||
|
versions: []int{0},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "if version is 0 undelete current version",
|
||||||
|
setup: func() *KV {
|
||||||
|
kv := &KV{
|
||||||
|
data: make(map[string]*Value),
|
||||||
|
}
|
||||||
|
kv.data["test/path"] = &Value{
|
||||||
|
Metadata: Metadata{
|
||||||
|
CurrentVersion: 1,
|
||||||
|
},
|
||||||
|
Versions: map[int]Version{
|
||||||
|
1: {
|
||||||
|
Data: map[string]string{"key": "value"},
|
||||||
|
Version: 1,
|
||||||
|
DeletedTime: &time.Time{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return kv
|
||||||
|
},
|
||||||
|
path: "test/path",
|
||||||
|
values: map[string]string{"key": "value"},
|
||||||
|
versions: []int{0},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
kv := tt.setup()
|
||||||
|
err := kv.Undelete(tt.path, tt.versions)
|
||||||
|
assert.Equal(t, tt.wantErr, err)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
secret, exist := kv.data[tt.path]
|
||||||
|
assert.True(t, exist)
|
||||||
|
|
||||||
|
for _, version := range tt.versions {
|
||||||
|
if version == 0 {
|
||||||
|
version = secret.Metadata.CurrentVersion
|
||||||
|
}
|
||||||
|
if v, exist := secret.Versions[version]; exist {
|
||||||
|
assert.True(t, exist)
|
||||||
|
assert.Nil(t, v.DeletedTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
121
net/net.go
121
net/net.go
|
@ -7,13 +7,32 @@ package net
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/spiffe/go-spiffe/v2/spiffeid"
|
"github.com/spiffe/go-spiffe/v2/spiffeid"
|
||||||
"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
|
"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
|
||||||
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func RequestBody(r *http.Request) (bod []byte, err error) {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(b io.ReadCloser) {
|
||||||
|
if b == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = errors.Join(err, b.Close())
|
||||||
|
}(r.Body)
|
||||||
|
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
|
||||||
// CreateMtlsServer creates an HTTP server configured for mutual TLS (mTLS)
|
// CreateMtlsServer creates an HTTP server configured for mutual TLS (mTLS)
|
||||||
// authentication using SPIFFE X.509 certificates. It sets up the server with a
|
// authentication using SPIFFE X.509 certificates. It sets up the server with a
|
||||||
// custom authorizer that validates client SPIFFE IDs against a provided
|
// custom authorizer that validates client SPIFFE IDs against a provided
|
||||||
|
@ -56,8 +75,28 @@ func CreateMtlsServer(source *workloadapi.X509Source,
|
||||||
return server, nil
|
return server, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: document.
|
// CreateMtlsClientWithPredicate creates an HTTP client configured for mutual TLS
|
||||||
func CreateMtlsClient(
|
// authentication using SPIFFE workload identities.
|
||||||
|
// It uses the provided X.509 source for client certificates and validates peer
|
||||||
|
// certificates against a predicate function.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - source: An X509Source that provides the client's identity certificates
|
||||||
|
// and trusted roots
|
||||||
|
// - predicate: A function that evaluates SPIFFE IDs (as strings) and returns
|
||||||
|
// true if the ID should be trusted
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *http.Client: A configured HTTP client that will use mTLS for all
|
||||||
|
// connections
|
||||||
|
// - error: An error if the client creation fails
|
||||||
|
//
|
||||||
|
// The returned client will:
|
||||||
|
// - Present client certificates from the provided X509Source
|
||||||
|
// - Validate peer certificates using the same X509Source
|
||||||
|
// - Only accept peer certificates with SPIFFE IDs that pass the predicate
|
||||||
|
// function
|
||||||
|
func CreateMtlsClientWithPredicate(
|
||||||
source *workloadapi.X509Source,
|
source *workloadapi.X509Source,
|
||||||
predicate func(string) bool,
|
predicate func(string) bool,
|
||||||
) (*http.Client, error) {
|
) (*http.Client, error) {
|
||||||
|
@ -75,22 +114,58 @@ func CreateMtlsClient(
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
TLSClientConfig: tlsConfig,
|
TLSClientConfig: tlsConfig,
|
||||||
|
IdleConnTimeout: 30 * time.Second,
|
||||||
|
MaxIdleConns: 100,
|
||||||
|
MaxConnsPerHost: 10,
|
||||||
|
MaxIdleConnsPerHost: 10,
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}).DialContext,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ResponseHeaderTimeout: 10 * time.Second,
|
||||||
|
ExpectContinueTimeout: 5 * time.Second,
|
||||||
},
|
},
|
||||||
|
Timeout: 60 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve initializes and starts an HTTPS server using mTLS authentication with
|
// CreateMtlsClient creates an HTTP client configured for mutual TLS
|
||||||
// SPIFFE X.509 certificates. It sets up the server routes using the provided
|
// authentication using SPIFFE workload identities.
|
||||||
// initialization function and listens for incoming connections on the specified
|
// It uses the provided X.509 source for client certificates and validates peer
|
||||||
// port.
|
// certificates against a predicate function.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - source: An X509Source that provides the client's identity certificates
|
||||||
|
// and trusted roots
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *http.Client: A configured HTTP client that will use mTLS for all
|
||||||
|
// connections
|
||||||
|
// - error: An error if the client creation fails
|
||||||
|
//
|
||||||
|
// The returned client will:
|
||||||
|
// - Present client certificates from the provided X509Source
|
||||||
|
// - Validate peer certificates using the same X509Source
|
||||||
|
// - Only accept peer certificates with SPIFFE IDs that pass the predicate
|
||||||
|
// function
|
||||||
|
func CreateMtlsClient(source *workloadapi.X509Source) (*http.Client, error) {
|
||||||
|
return CreateMtlsClientWithPredicate(source, func(string) bool { return true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeWithPredicate initializes and starts an HTTPS server using mTLS
|
||||||
|
// authentication with SPIFFE X.509 certificates. It sets up the server routes
|
||||||
|
// using the provided initialization function and listens for incoming
|
||||||
|
// connections on the specified port.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - source: An X509Source that provides the server's identity credentials and
|
// - source: An X509Source that provides the server's identity credentials and
|
||||||
// validates client certificates. Must not be nil.
|
// validates client certificates. Must not be nil.
|
||||||
// - initializeRoutes: A function that sets up the HTTP route handlers for the
|
// - initializeRoutes: A function that sets up the HTTP route handlers for the
|
||||||
// server. This function is called before the server starts.
|
// server. This function is called before the server starts.
|
||||||
|
// - predicate: a predicate function to pass to CreateMtlsServer.
|
||||||
// - tlsPort: The network address and port for the server to listen on
|
// - tlsPort: The network address and port for the server to listen on
|
||||||
// (e.g., ":8443").
|
// (e.g., ":8443").
|
||||||
//
|
//
|
||||||
|
@ -104,7 +179,7 @@ func CreateMtlsClient(
|
||||||
// The function uses empty strings for the certificate and key file parameters
|
// The function uses empty strings for the certificate and key file parameters
|
||||||
// in ListenAndServeTLS as the certificates are provided by the X509Source. The
|
// in ListenAndServeTLS as the certificates are provided by the X509Source. The
|
||||||
// server's mTLS configuration is determined by the CreateMtlsServer function.
|
// server's mTLS configuration is determined by the CreateMtlsServer function.
|
||||||
func Serve(source *workloadapi.X509Source,
|
func ServeWithPredicate(source *workloadapi.X509Source,
|
||||||
initializeRoutes func(),
|
initializeRoutes func(),
|
||||||
predicate func(string) bool,
|
predicate func(string) bool,
|
||||||
tlsPort string) error {
|
tlsPort string) error {
|
||||||
|
@ -128,3 +203,35 @@ func Serve(source *workloadapi.X509Source,
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serve initializes and starts an HTTPS server using mTLS
|
||||||
|
// authentication with SPIFFE X.509 certificates. It sets up the server routes
|
||||||
|
// using the provided initialization function and listens for incoming
|
||||||
|
// connections on the specified port.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - source: An X509Source that provides the server's identity credentials and
|
||||||
|
// validates client certificates. Must not be nil.
|
||||||
|
// - initializeRoutes: A function that sets up the HTTP route handlers for the
|
||||||
|
// server. This function is called before the server starts.
|
||||||
|
// - tlsPort: The network address and port for the server to listen on
|
||||||
|
// (e.g., ":8443").
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: Returns nil if the server starts successfully, otherwise returns
|
||||||
|
// an error explaining the failure. Specific error cases include:
|
||||||
|
// - If source is nil
|
||||||
|
// - If server creation fails
|
||||||
|
// - If the server fails to start or encounters an error while running
|
||||||
|
//
|
||||||
|
// The function uses empty strings for the certificate and key file parameters
|
||||||
|
// in ListenAndServeTLS as the certificates are provided by the X509Source. The
|
||||||
|
// server's mTLS configuration is determined by the CreateMtlsServer function.
|
||||||
|
func Serve(
|
||||||
|
source *workloadapi.X509Source,
|
||||||
|
initializeRoutes func(),
|
||||||
|
tlsPort string) error {
|
||||||
|
return ServeWithPredicate(
|
||||||
|
source, initializeRoutes,
|
||||||
|
func(string) bool { return true }, tlsPort)
|
||||||
|
}
|
||||||
|
|
11
net/post.go
11
net/post.go
|
@ -13,6 +13,8 @@ import (
|
||||||
|
|
||||||
var ErrNotFound = errors.New("not found")
|
var ErrNotFound = errors.New("not found")
|
||||||
var ErrUnauthorized = errors.New("unauthorized")
|
var ErrUnauthorized = errors.New("unauthorized")
|
||||||
|
var ErrBadRequest = errors.New("bad request")
|
||||||
|
var ErrNotReady = errors.New("not ready")
|
||||||
|
|
||||||
func body(r *http.Response) (bod []byte, err error) {
|
func body(r *http.Response) (bod []byte, err error) {
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
|
@ -84,6 +86,15 @@ func Post(client *http.Client, path string, mr []byte) ([]byte, error) {
|
||||||
return []byte{}, ErrUnauthorized
|
return []byte{}, ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.StatusCode == http.StatusBadRequest {
|
||||||
|
return []byte{}, ErrBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// SPIKE Nexus is likely not initialized or in bad shape:
|
||||||
|
if r.StatusCode == http.StatusServiceUnavailable {
|
||||||
|
return []byte{}, ErrNotReady
|
||||||
|
}
|
||||||
|
|
||||||
return []byte{}, errors.New("post: Problem connecting to peer")
|
return []byte{}, errors.New("post: Problem connecting to peer")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
#-------------------------------------------------------------------------------#
|
||||||
|
# Qodana analysis is configured by qodana.yaml file #
|
||||||
|
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
||||||
|
#-------------------------------------------------------------------------------#
|
||||||
|
version: "1.0"
|
||||||
|
|
||||||
|
#Specify inspection profile for code analysis
|
||||||
|
profile:
|
||||||
|
name: qodana.starter
|
||||||
|
|
||||||
|
#Enable inspections
|
||||||
|
#include:
|
||||||
|
# - name: <SomeEnabledInspectionId>
|
||||||
|
|
||||||
|
#Disable inspections
|
||||||
|
#exclude:
|
||||||
|
# - name: <SomeDisabledInspectionId>
|
||||||
|
# paths:
|
||||||
|
# - <path/where/not/run/inspection>
|
||||||
|
|
||||||
|
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
||||||
|
#bootstrap: sh ./prepare-qodana.sh
|
||||||
|
|
||||||
|
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
||||||
|
#plugins:
|
||||||
|
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
||||||
|
|
||||||
|
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
|
||||||
|
linter: jetbrains/qodana-go:2024.3
|
|
@ -0,0 +1,25 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package mock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockRetrier implements Retrier for testing
|
||||||
|
type MockRetrier struct {
|
||||||
|
RetryFunc func(context.Context, func() error) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetryWithBackoff implements the Retrier interface
|
||||||
|
func (m *MockRetrier) RetryWithBackoff(
|
||||||
|
ctx context.Context,
|
||||||
|
operation func() error,
|
||||||
|
) error {
|
||||||
|
if m.RetryFunc != nil {
|
||||||
|
return m.RetryFunc(ctx, operation)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,251 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// Package retry provides a flexible and type-safe retry mechanism with
|
||||||
|
// exponential backoff. It allows for customizable retry strategies and
|
||||||
|
// notifications while maintaining context awareness and cancellation support.
|
||||||
|
package retry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Default configuration values for the exponential backoff strategy
|
||||||
|
const (
|
||||||
|
// Initial wait time between retries
|
||||||
|
defaultInitialInterval = 500 * time.Millisecond
|
||||||
|
// Maximum wait time between retries
|
||||||
|
defaultMaxInterval = 3 * time.Second
|
||||||
|
// Maximum total time for all retry attempts
|
||||||
|
defaultMaxElapsedTime = 30 * time.Second
|
||||||
|
// Factor by which the wait time increases
|
||||||
|
defaultMultiplier = 2.0
|
||||||
|
)
|
||||||
|
|
||||||
|
// Retrier defines the interface for retry operations with backoff support.
|
||||||
|
// Implementations of this interface provide different retry strategies.
|
||||||
|
type Retrier interface {
|
||||||
|
// RetryWithBackoff executes an operation with backoff strategy.
|
||||||
|
// It will repeatedly execute the operation until it succeeds or
|
||||||
|
// the context is cancelled. The backoff strategy determines the
|
||||||
|
// delay between retry attempts.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - ctx: Context for cancellation and timeout control
|
||||||
|
// - op: The operation to retry, returns error if attempt failed
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: Last error from the operation or nil if successful
|
||||||
|
RetryWithBackoff(ctx context.Context, op func() error) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypedRetrier provides type-safe retry operations for functions that return
|
||||||
|
// both a value and an error. It wraps a base Retrier to provide typed results.
|
||||||
|
type TypedRetrier[T any] struct {
|
||||||
|
retrier Retrier
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTypedRetrier creates a new TypedRetrier with the given base Retrier.
|
||||||
|
// This allows for type-safe retry operations while reusing existing retry logic.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// retrier := NewTypedRetrier[string](NewExponentialRetrier())
|
||||||
|
// result, err := retrier.RetryWithBackoff(ctx, func() (string, error) {
|
||||||
|
// return callExternalService()
|
||||||
|
// })
|
||||||
|
func NewTypedRetrier[T any](r Retrier) *TypedRetrier[T] {
|
||||||
|
return &TypedRetrier[T]{retrier: r}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetryWithBackoff executes a typed operation with backoff strategy.
|
||||||
|
// It preserves the return value while maintaining retry functionality.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - ctx: Context for cancellation and timeout control
|
||||||
|
// - op: The operation to retry, returns both a value and an error
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - T: The result value from the successful operation
|
||||||
|
// - error: Last error from the operation or nil if successful
|
||||||
|
func (r *TypedRetrier[T]) RetryWithBackoff(
|
||||||
|
ctx context.Context,
|
||||||
|
op func() (T, error),
|
||||||
|
) (T, error) {
|
||||||
|
var result T
|
||||||
|
err := r.retrier.RetryWithBackoff(ctx, func() error {
|
||||||
|
var err error
|
||||||
|
result, err = op()
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyFn is a callback function type for retry notifications.
|
||||||
|
// It provides information about each retry attempt including the error,
|
||||||
|
// current interval duration, and total elapsed time.
|
||||||
|
type NotifyFn func(err error, duration, totalDuration time.Duration)
|
||||||
|
|
||||||
|
// RetrierOption is a function type for configuring ExponentialRetrier.
|
||||||
|
// It follows the functional options pattern for flexible configuration.
|
||||||
|
type RetrierOption func(*ExponentialRetrier)
|
||||||
|
|
||||||
|
// ExponentialRetrier implements Retrier using exponential backoff strategy.
|
||||||
|
// It provides configurable retry intervals and maximum attempt durations..
|
||||||
|
type ExponentialRetrier struct {
|
||||||
|
newBackOff func() backoff.BackOff
|
||||||
|
notify NotifyFn
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackOffOption is a function type for configuring ExponentialBackOff.
|
||||||
|
// It allows fine-tuning of the backoff strategy parameters.
|
||||||
|
type BackOffOption func(*backoff.ExponentialBackOff)
|
||||||
|
|
||||||
|
// NewExponentialRetrier creates a new ExponentialRetrier with configurable
|
||||||
|
// settings.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// retrier := NewExponentialRetrier(
|
||||||
|
// WithBackOffOptions(
|
||||||
|
// WithInitialInterval(100 * time.Millisecond),
|
||||||
|
// WithMaxInterval(5 * time.Second),
|
||||||
|
// ),
|
||||||
|
// WithNotify(func(err error, d, total time.Duration) {
|
||||||
|
// log.Printf("Retry attempt failed: %v", err)
|
||||||
|
// }),
|
||||||
|
// )
|
||||||
|
func NewExponentialRetrier(opts ...RetrierOption) *ExponentialRetrier {
|
||||||
|
b := backoff.NewExponentialBackOff()
|
||||||
|
b.InitialInterval = defaultInitialInterval
|
||||||
|
b.MaxInterval = defaultMaxInterval
|
||||||
|
b.MaxElapsedTime = defaultMaxElapsedTime
|
||||||
|
b.Multiplier = defaultMultiplier
|
||||||
|
|
||||||
|
r := &ExponentialRetrier{
|
||||||
|
newBackOff: func() backoff.BackOff {
|
||||||
|
return b
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetryWithBackoff implements the Retrier interface
|
||||||
|
func (r *ExponentialRetrier) RetryWithBackoff(
|
||||||
|
ctx context.Context,
|
||||||
|
operation func() error,
|
||||||
|
) error {
|
||||||
|
b := r.newBackOff()
|
||||||
|
totalDuration := time.Duration(0)
|
||||||
|
return backoff.RetryNotify(
|
||||||
|
operation,
|
||||||
|
backoff.WithContext(b, ctx),
|
||||||
|
func(err error, duration time.Duration) {
|
||||||
|
totalDuration += duration
|
||||||
|
if r.notify != nil {
|
||||||
|
r.notify(err, duration, totalDuration)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBackOffOptions configures the backoff settings using the provided
|
||||||
|
// options. Multiple options can be combined to customize the retry behavior.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// retrier := NewExponentialRetrier(
|
||||||
|
// WithBackOffOptions(
|
||||||
|
// WithInitialInterval(1 * time.Second),
|
||||||
|
// WithMaxElapsedTime(1 * time.Minute),
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
func WithBackOffOptions(opts ...BackOffOption) RetrierOption {
|
||||||
|
return func(r *ExponentialRetrier) {
|
||||||
|
b := r.newBackOff().(*backoff.ExponentialBackOff)
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithInitialInterval sets the initial interval between retries.
|
||||||
|
// This is the starting point for the exponential backoff calculation.
|
||||||
|
func WithInitialInterval(d time.Duration) BackOffOption {
|
||||||
|
return func(b *backoff.ExponentialBackOff) {
|
||||||
|
b.InitialInterval = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMaxInterval sets the maximum interval between retries.
|
||||||
|
// The interval will never exceed this value, regardless of the multiplier.s
|
||||||
|
func WithMaxInterval(d time.Duration) BackOffOption {
|
||||||
|
return func(b *backoff.ExponentialBackOff) {
|
||||||
|
b.MaxInterval = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMaxElapsedTime sets the maximum total time for retries.
|
||||||
|
// The retry operation will stop after this duration, even if not successful.
|
||||||
|
func WithMaxElapsedTime(d time.Duration) BackOffOption {
|
||||||
|
return func(b *backoff.ExponentialBackOff) {
|
||||||
|
b.MaxElapsedTime = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMultiplier sets the multiplier for increasing intervals.
|
||||||
|
// Each retry interval is multiplied by this value, up to MaxInterval.
|
||||||
|
func WithMultiplier(m float64) BackOffOption {
|
||||||
|
return func(b *backoff.ExponentialBackOff) {
|
||||||
|
b.Multiplier = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithNotify is an option to set the notification callback.
|
||||||
|
// The callback is called after each failed attempt with retry statistics.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// retrier := NewExponentialRetrier(
|
||||||
|
// WithNotify(func(err error, d, total time.Duration) {
|
||||||
|
// log.Printf("Attempt failed after %v, total time %v: %v",
|
||||||
|
// d, total, err)
|
||||||
|
// }),
|
||||||
|
// )
|
||||||
|
func WithNotify(fn NotifyFn) RetrierOption {
|
||||||
|
return func(r *ExponentialRetrier) {
|
||||||
|
r.notify = fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler represents a function that returns a value and an error.
|
||||||
|
// It's used with the Do helper function for simple retry operations.
|
||||||
|
type Handler[T any] func() (T, error)
|
||||||
|
|
||||||
|
// Do provides a simplified way to retry a typed operation with default
|
||||||
|
// settings.
|
||||||
|
// It creates a TypedRetrier with default exponential backoff configuration.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// result, err := Do(ctx, func() (string, error) {
|
||||||
|
// return fetchData()
|
||||||
|
// })
|
||||||
|
func Do[T any](
|
||||||
|
ctx context.Context,
|
||||||
|
handler Handler[T],
|
||||||
|
options ...RetrierOption,
|
||||||
|
) (T, error) {
|
||||||
|
return NewTypedRetrier[T](
|
||||||
|
NewExponentialRetrier(options...),
|
||||||
|
).RetryWithBackoff(ctx, handler)
|
||||||
|
}
|
|
@ -0,0 +1,187 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package retry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExponentialRetrier_Success(t *testing.T) {
|
||||||
|
retrier := NewExponentialRetrier()
|
||||||
|
|
||||||
|
// Operation that succeeds immediately
|
||||||
|
err := retrier.RetryWithBackoff(context.Background(), func() error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExponentialRetrier_EventualSuccess(t *testing.T) {
|
||||||
|
attempts := 0
|
||||||
|
maxAttempts := 3
|
||||||
|
|
||||||
|
retrier := NewExponentialRetrier(
|
||||||
|
WithBackOffOptions(
|
||||||
|
WithInitialInterval(1*time.Millisecond),
|
||||||
|
WithMaxInterval(5*time.Millisecond),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
err := retrier.RetryWithBackoff(context.Background(), func() error {
|
||||||
|
attempts++
|
||||||
|
if attempts < maxAttempts {
|
||||||
|
return errors.New("temporary error")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, maxAttempts, attempts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExponentialRetrier_Failure(t *testing.T) {
|
||||||
|
retrier := NewExponentialRetrier(
|
||||||
|
WithBackOffOptions(
|
||||||
|
WithMaxElapsedTime(10*time.Millisecond),
|
||||||
|
WithInitialInterval(1*time.Millisecond),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expectedErr := errors.New("persistent error")
|
||||||
|
err := retrier.RetryWithBackoff(context.Background(), func() error {
|
||||||
|
return expectedErr
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, expectedErr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExponentialRetrier_ContextCancellation(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
retrier := NewExponentialRetrier(
|
||||||
|
WithBackOffOptions(
|
||||||
|
WithInitialInterval(100 * time.Millisecond),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := retrier.RetryWithBackoff(ctx, func() error {
|
||||||
|
return errors.New("keep retrying")
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, context.Canceled, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExponentialRetrier_Notification(t *testing.T) {
|
||||||
|
var notifications []time.Duration
|
||||||
|
totalDurations := make([]time.Duration, 0)
|
||||||
|
|
||||||
|
retrier := NewExponentialRetrier(
|
||||||
|
WithNotify(func(_ error, duration, totalDuration time.Duration) {
|
||||||
|
notifications = append(notifications, duration)
|
||||||
|
totalDurations = append(totalDurations, totalDuration)
|
||||||
|
}),
|
||||||
|
WithBackOffOptions(
|
||||||
|
WithInitialInterval(1*time.Millisecond),
|
||||||
|
WithMaxInterval(5*time.Millisecond),
|
||||||
|
WithMaxElapsedTime(20*time.Millisecond),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
attempts := 0
|
||||||
|
_ = retrier.RetryWithBackoff(context.Background(), func() error {
|
||||||
|
attempts++
|
||||||
|
if attempts < 3 {
|
||||||
|
return errors.New("temporary error")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, 2, len(notifications))
|
||||||
|
assert.Equal(t, 2, len(totalDurations))
|
||||||
|
|
||||||
|
// Verify that durations are increasing
|
||||||
|
assert.Less(t, notifications[0], notifications[1])
|
||||||
|
assert.Less(t, totalDurations[0], totalDurations[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTypedRetrier_Success(t *testing.T) {
|
||||||
|
baseRetrier := NewExponentialRetrier()
|
||||||
|
typedRetrier := NewTypedRetrier[string](baseRetrier)
|
||||||
|
|
||||||
|
expected := "success"
|
||||||
|
result, err := typedRetrier.RetryWithBackoff(context.Background(), func() (string, error) {
|
||||||
|
return expected, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTypedRetrier_Failure(t *testing.T) {
|
||||||
|
baseRetrier := NewExponentialRetrier(
|
||||||
|
WithBackOffOptions(
|
||||||
|
WithMaxElapsedTime(10*time.Millisecond),
|
||||||
|
WithInitialInterval(1*time.Millisecond),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
typedRetrier := NewTypedRetrier[int](baseRetrier)
|
||||||
|
|
||||||
|
expectedErr := errors.New("typed error")
|
||||||
|
result, err := typedRetrier.RetryWithBackoff(context.Background(), func() (int, error) {
|
||||||
|
return 0, expectedErr
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, expectedErr, err)
|
||||||
|
assert.Equal(t, 0, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackOffOptions(t *testing.T) {
|
||||||
|
initialInterval := 100 * time.Millisecond
|
||||||
|
maxInterval := 1 * time.Second
|
||||||
|
maxElapsedTime := 5 * time.Second
|
||||||
|
multiplier := 2.5
|
||||||
|
|
||||||
|
retrier := NewExponentialRetrier(
|
||||||
|
WithBackOffOptions(
|
||||||
|
WithInitialInterval(initialInterval),
|
||||||
|
WithMaxInterval(maxInterval),
|
||||||
|
WithMaxElapsedTime(maxElapsedTime),
|
||||||
|
WithMultiplier(multiplier),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Access the backoff configuration
|
||||||
|
b := retrier.newBackOff().(*backoff.ExponentialBackOff)
|
||||||
|
|
||||||
|
assert.Equal(t, initialInterval, b.InitialInterval)
|
||||||
|
assert.Equal(t, maxInterval, b.MaxInterval)
|
||||||
|
assert.Equal(t, maxElapsedTime, b.MaxElapsedTime)
|
||||||
|
assert.Equal(t, multiplier, b.Multiplier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultSettings(t *testing.T) {
|
||||||
|
retrier := NewExponentialRetrier()
|
||||||
|
b := retrier.newBackOff().(*backoff.ExponentialBackOff)
|
||||||
|
|
||||||
|
assert.Equal(t, defaultInitialInterval, b.InitialInterval)
|
||||||
|
assert.Equal(t, defaultMaxInterval, b.MaxInterval)
|
||||||
|
assert.Equal(t, defaultMaxElapsedTime, b.MaxElapsedTime)
|
||||||
|
assert.Equal(t, defaultMultiplier, b.Multiplier)
|
||||||
|
}
|
|
@ -0,0 +1,195 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// Package mem provides utilities for secure mem operations.
|
||||||
|
package mem
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClearRawBytes securely erases all bytes in the provided value by overwriting
|
||||||
|
// its mem with zeros. This ensures sensitive data like root keys and
|
||||||
|
// Shamir shards are properly cleaned from mem before garbage collection.
|
||||||
|
//
|
||||||
|
// According to NIST SP 800-88 Rev. 1 (Guidelines for Media Sanitization),
|
||||||
|
// a single overwrite pass with zeros is sufficient for modern storage
|
||||||
|
// devices, including RAM.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - s: A pointer to any type of data that should be securely erased
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// type SensitiveData struct {
|
||||||
|
// Key [32]byte
|
||||||
|
// Token string
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// data := &SensitiveData{...}
|
||||||
|
// defer mem.Clear(data)
|
||||||
|
// // Use data...
|
||||||
|
func ClearRawBytes[T any](s *T) {
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p := unsafe.Pointer(s)
|
||||||
|
size := unsafe.Sizeof(*s)
|
||||||
|
b := (*[1 << 30]byte)(p)[:size:size]
|
||||||
|
|
||||||
|
// Zero out all bytes in mem
|
||||||
|
for i := range b {
|
||||||
|
b[i] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the data is actually wiped before gc has time to interfere
|
||||||
|
runtime.KeepAlive(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearRawBytesParanoid provides a more thorough memory wiping method for
|
||||||
|
// highly-sensitive data.
|
||||||
|
//
|
||||||
|
// It performs multiple passes using different patterns (zeros, ones,
|
||||||
|
// random data, and alternating bits) to minimize potential data remanence
|
||||||
|
// concerns from sophisticated physical memory attacks.
|
||||||
|
//
|
||||||
|
// This method is designed for extremely security-sensitive applications where:
|
||||||
|
// 1. An attacker might have physical access to RAM
|
||||||
|
// 2. Cold boot attacks or specialized memory forensics equipment might be
|
||||||
|
// employed
|
||||||
|
// 3. The data being protected is critically sensitive (e.g., high-value
|
||||||
|
// encryption keys)
|
||||||
|
//
|
||||||
|
// For most applications, the standard Clear() method is sufficient as:
|
||||||
|
// - Modern RAM technologies (DDR4/DDR5) make data remanence attacks
|
||||||
|
// increasingly difficult
|
||||||
|
// - Successful attacks typically require specialized equipment and immediate
|
||||||
|
// (sub-second) physical access.
|
||||||
|
// - The time window for such attacks is extremely short after power loss
|
||||||
|
// - The detectable signal from previous memory states diminishes rapidly with
|
||||||
|
// a single overwrite
|
||||||
|
//
|
||||||
|
// This method is provided for users with extreme security requirements or in
|
||||||
|
// regulated environments where multiple-pass overwrite policies are mandated.
|
||||||
|
func ClearRawBytesParanoid[T any](s *T) {
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p := unsafe.Pointer(s)
|
||||||
|
size := unsafe.Sizeof(*s)
|
||||||
|
b := (*[1 << 30]byte)(p)[:size:size]
|
||||||
|
|
||||||
|
// Pattern overwrite cycles:
|
||||||
|
// 1. All zeros
|
||||||
|
// 2. All ones (0xFF)
|
||||||
|
// 3. Random data
|
||||||
|
// 4. Alternating 0x55/0xAA (01010101/10101010)
|
||||||
|
// 5. Final zero out
|
||||||
|
|
||||||
|
// Zero out all bytes (first pass)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = 0
|
||||||
|
}
|
||||||
|
runtime.KeepAlive(s)
|
||||||
|
|
||||||
|
// Fill with ones (second pass)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = 0xFF
|
||||||
|
}
|
||||||
|
runtime.KeepAlive(s)
|
||||||
|
|
||||||
|
// Fill with random data (third pass)
|
||||||
|
_, err := rand.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
panic("")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
runtime.KeepAlive(s)
|
||||||
|
|
||||||
|
// Alternating bit pattern (fourth pass)
|
||||||
|
for i := range b {
|
||||||
|
if i%2 == 0 {
|
||||||
|
b[i] = 0x55 // 01010101
|
||||||
|
} else {
|
||||||
|
b[i] = 0xAA // 10101010
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runtime.KeepAlive(s)
|
||||||
|
|
||||||
|
// Final zero out (fifth pass)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = 0
|
||||||
|
}
|
||||||
|
runtime.KeepAlive(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeroed32 checks if a 32-byte array contains only zero values.
|
||||||
|
// Returns true if all bytes are zero, false otherwise.
|
||||||
|
func Zeroed32(ar *[32]byte) bool {
|
||||||
|
for _, v := range ar {
|
||||||
|
if v != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearBytes securely erases a byte slice by overwriting all bytes with zeros.
|
||||||
|
// This is a convenience wrapper around Clear for byte slices.
|
||||||
|
//
|
||||||
|
// This is especially important for slices because executing `mem.Clear` on
|
||||||
|
// a slice it will only zero out the slice header structure itself, NOT the
|
||||||
|
// underlying array data that the slice points to.
|
||||||
|
//
|
||||||
|
// When we pass a byte slice s to the function Clear[T any](s *T),
|
||||||
|
// we are passing a pointer to the slice header, not a pointer to the
|
||||||
|
// underlying array. The slice header contains three fields:
|
||||||
|
// - A pointer to the underlying array
|
||||||
|
// - The length of the slice
|
||||||
|
// - The capacity of the slice
|
||||||
|
//
|
||||||
|
// mem.Clear(s) will zero out this slice header structure, but not the
|
||||||
|
// actual array data the slice points to
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - b: A byte slice that should be securely erased
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// key := []byte{...} // Sensitive cryptographic key
|
||||||
|
// defer mem.ClearBytes(key)
|
||||||
|
// // Use key...
|
||||||
|
func ClearBytes(b []byte) {
|
||||||
|
if len(b) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range b {
|
||||||
|
b[i] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the data is actually wiped before gc has time to interfere
|
||||||
|
runtime.KeepAlive(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock attempts to lock the process memory to prevent swapping.
|
||||||
|
// Returns true if successful, false if not supported or failed.
|
||||||
|
func Lock() bool {
|
||||||
|
// `mlock` is only available on Unix-like systems
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to lock all current and future memory
|
||||||
|
if err := syscall.Mlockall(syscall.MCL_CURRENT | syscall.MCL_FUTURE); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package mem
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClear(t *testing.T) {
|
||||||
|
type testStruct struct {
|
||||||
|
Key [32]byte
|
||||||
|
Token string
|
||||||
|
UserId int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test data with non-zero values
|
||||||
|
key := [32]byte{}
|
||||||
|
for i := range key {
|
||||||
|
key[i] = byte(i + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := &testStruct{
|
||||||
|
Key: key,
|
||||||
|
Token: "secret-token-value",
|
||||||
|
UserId: 12345,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call Clear on the data
|
||||||
|
Clear(data)
|
||||||
|
|
||||||
|
// Verify all fields are zeroed
|
||||||
|
for i, b := range data.Key {
|
||||||
|
if b != 0 {
|
||||||
|
t.Errorf("Expected byte at index %d to be 0, got %d", i, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: String contents won't be zeroed directly as strings are immutable in Go
|
||||||
|
// The string header will point to the same backing array
|
||||||
|
// In a real application, sensitive strings should be stored as byte slices
|
||||||
|
|
||||||
|
if data.UserId != 0 {
|
||||||
|
t.Errorf("Expected UserId to be 0, got %d", data.UserId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClearBytes(t *testing.T) {
|
||||||
|
// Create a non-zero byte slice
|
||||||
|
bytes := make([]byte, 64)
|
||||||
|
for i := range bytes {
|
||||||
|
bytes[i] = byte(i + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a copy to verify later
|
||||||
|
original := make([]byte, len(bytes))
|
||||||
|
copy(original, bytes)
|
||||||
|
|
||||||
|
// Verify bytes are non-zero initially
|
||||||
|
for i, b := range bytes {
|
||||||
|
if b != original[i] {
|
||||||
|
t.Fatalf("Test setup issue: bytes changed before ClearBytes call")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call ClearBytes
|
||||||
|
ClearBytes(bytes)
|
||||||
|
|
||||||
|
// Verify all bytes are zeroed
|
||||||
|
for i, b := range bytes {
|
||||||
|
if b != 0 {
|
||||||
|
t.Errorf("Expected byte at index %d to be 0, got %d", i, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,150 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package spiffe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spiffe/go-spiffe/v2/spiffeid"
|
||||||
|
"github.com/spiffe/go-spiffe/v2/svid/x509svid"
|
||||||
|
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EndpointSocket returns the UNIX domain socket address for the SPIFFE
|
||||||
|
// Workload API endpoint.
|
||||||
|
//
|
||||||
|
// The function first checks for the SPIFFE_ENDPOINT_SOCKET environment
|
||||||
|
// variable. If set, it returns that value. Otherwise, it returns a default
|
||||||
|
// development
|
||||||
|
//
|
||||||
|
// socket path:
|
||||||
|
//
|
||||||
|
// "unix:///tmp/spire-agent/public/api.sock"
|
||||||
|
//
|
||||||
|
// For production deployments, especially in Kubernetes environments, it's
|
||||||
|
// recommended to set SPIFFE_ENDPOINT_SOCKET to a more restricted socket path,
|
||||||
|
// such as: "unix:///run/spire/agent/sockets/spire.sock"
|
||||||
|
//
|
||||||
|
// Default socket paths by environment:
|
||||||
|
// - Development (Linux): unix:///tmp/spire-agent/public/api.sock
|
||||||
|
// - Kubernetes: unix:///run/spire/agent/sockets/spire.sock
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: The UNIX domain socket address for the SPIFFE Workload API
|
||||||
|
// endpoint
|
||||||
|
//
|
||||||
|
// Environment Variables:
|
||||||
|
// - SPIFFE_ENDPOINT_SOCKET: Override the default socket path
|
||||||
|
func EndpointSocket() string {
|
||||||
|
p := os.Getenv("SPIFFE_ENDPOINT_SOCKET")
|
||||||
|
if p != "" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unix:///tmp/spire-agent/public/api.sock"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source creates a new SPIFFE X.509 source and returns the associated SVID ID.
|
||||||
|
// It establishes a connection to the Workload API at the specified socket path
|
||||||
|
// and retrieves the X.509 SVID for the workload.
|
||||||
|
//
|
||||||
|
// The function takes a context for cancellation and timeout control, and a
|
||||||
|
// socket path string specifying the Workload API endpoint location.
|
||||||
|
//
|
||||||
|
// It returns:
|
||||||
|
// - An X509Source that can be used to fetch and monitor X.509 SVIDs
|
||||||
|
// - The string representation of the current SVID ID
|
||||||
|
// - An error if the source creation or initial SVID fetch fails
|
||||||
|
//
|
||||||
|
// The returned X509Source should be closed when no longer needed.
|
||||||
|
func Source(ctx context.Context, socketPath string) (
|
||||||
|
*workloadapi.X509Source, string, error,
|
||||||
|
) {
|
||||||
|
source, err := workloadapi.NewX509Source(ctx,
|
||||||
|
workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", errors.Join(
|
||||||
|
errors.New("failed to create X509Source"),
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
svid, err := source.GetX509SVID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", errors.Join(
|
||||||
|
errors.New("unable to get X509SVID"),
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return source, svid.ID.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IdFromRequest extracts the SPIFFE ID from the TLS peer certificate of
|
||||||
|
// an HTTP request.
|
||||||
|
// It checks if the incoming request has a valid TLS connection and at least one
|
||||||
|
// peer certificate.
|
||||||
|
// The first certificate in the chain is used to extract the SPIFFE ID.
|
||||||
|
//
|
||||||
|
// Params:
|
||||||
|
//
|
||||||
|
// r *http.Request - The HTTP request from which the SPIFFE ID is to be
|
||||||
|
// extracted.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
//
|
||||||
|
// *spiffeid.ID - The SPIFFE ID extracted from the first peer certificate,
|
||||||
|
// or nil if extraction fails.
|
||||||
|
// error - An error object indicating the failure reason. Possible errors
|
||||||
|
// include the absence of peer certificates or a failure in extracting the
|
||||||
|
// SPIFFE ID from the certificate.
|
||||||
|
//
|
||||||
|
// Note:
|
||||||
|
//
|
||||||
|
// This function assumes that the request is already over a secured TLS
|
||||||
|
// connection and will fail if the TLS connection state is not available or
|
||||||
|
// the peer certificates are missing.
|
||||||
|
func IdFromRequest(r *http.Request) (*spiffeid.ID, error) {
|
||||||
|
tlsConnectionState := r.TLS
|
||||||
|
if len(tlsConnectionState.PeerCertificates) == 0 {
|
||||||
|
return nil, errors.New("no peer certs")
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := x509svid.IDFromCert(tlsConnectionState.PeerCertificates[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Join(
|
||||||
|
err,
|
||||||
|
errors.New("problem extracting svid"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseSource safely closes an X509Source.
|
||||||
|
//
|
||||||
|
// This function should be called when the X509Source is no longer needed,
|
||||||
|
// typically during application shutdown or cleanup. It handles nil sources
|
||||||
|
// gracefully and logs any errors that occur during closure without failing.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - source: The X509Source to close, may be nil
|
||||||
|
//
|
||||||
|
// If an error occurs during closure, it will be logged but will not cause the
|
||||||
|
// function to panic or return an error.
|
||||||
|
func CloseSource(source *workloadapi.X509Source) {
|
||||||
|
if source == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := source.Close(); err != nil {
|
||||||
|
log.Printf("Unable to close X509Source: %v", err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,272 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package spiffeid
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// IsPilot checks if a given SPIFFE ID matches the SPIKE Pilot's SPIFFE ID pattern.
|
||||||
|
//
|
||||||
|
// This function is used for identity verification to determine if the provided
|
||||||
|
// SPIFFE ID belongs to a SPIKE pilot instance. It compares the input against
|
||||||
|
// the expected pilot SPIFFE ID pattern.
|
||||||
|
//
|
||||||
|
// The function supports two formats:
|
||||||
|
// - Exact match: "spiffe://<trustRoot>/spike/pilot"
|
||||||
|
// - Extended match with metadata: "spiffe://<trustRoot>/spike/pilot/<metadata>"
|
||||||
|
//
|
||||||
|
// This allows for instance-specific identifiers while maintaining compatibility
|
||||||
|
// with the base pilot identity.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - trustRoots: Comma-delimited list of trust domain roots
|
||||||
|
// (e.g., "example.org,other.org")
|
||||||
|
// - id: The SPIFFE ID string to check
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - bool: true if the provided SPIFFE ID matches either the exact pilot ID
|
||||||
|
// or an extended ID with additional path segments for any of the trust roots,
|
||||||
|
// false otherwise
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// baseId := "spiffe://example.org/spike/pilot"
|
||||||
|
// extendedId := "spiffe://example.org/spike/pilot/instance-0"
|
||||||
|
//
|
||||||
|
// // Both will return true
|
||||||
|
// if IsPilot("example.org,other.org", baseId) {
|
||||||
|
// // Handle pilot-specific logic
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if IsPilot("example.org,other.org", extendedId) {
|
||||||
|
// // Also recognized as a pilot, with instance metadata
|
||||||
|
// }
|
||||||
|
func IsPilot(trustRoots, id string) bool {
|
||||||
|
for _, root := range strings.Split(trustRoots, ",") {
|
||||||
|
baseId := SpikePilot(strings.TrimSpace(root))
|
||||||
|
// Check if the ID is either exactly the base ID or starts with the base ID
|
||||||
|
// followed by "/"
|
||||||
|
if id == baseId || strings.HasPrefix(id, baseId+"/") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPilotRecover checks if a given SPIFFE ID matches the SPIKE Pilot's
|
||||||
|
// recovery SPIFFE ID pattern.
|
||||||
|
//
|
||||||
|
// This function verifies if the provided SPIFFE ID corresponds to a SPIKE Pilot
|
||||||
|
// instance with recovery capabilities by comparing it against the expected
|
||||||
|
// recovery SPIFFE ID pattern.
|
||||||
|
//
|
||||||
|
// The function supports two formats:
|
||||||
|
// - Exact match: "spiffe://<trustRoot>/spike/pilot/recover"
|
||||||
|
// - Extended match with metadata: "spiffe://<trustRoot>/spike/pilot/recover/<metadata>"
|
||||||
|
//
|
||||||
|
// This allows for instance-specific identifiers while maintaining compatibility
|
||||||
|
// with the base pilot recovery identity.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - trustRoots: Comma-delimited list of trust domain roots
|
||||||
|
// (e.g., "example.org,other.org")
|
||||||
|
// - id: The SPIFFE ID string to check
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - bool: true if the provided SPIFFE ID matches either the exact pilot recovery ID
|
||||||
|
// or an extended ID with additional path segments for any of the trust roots,
|
||||||
|
// false otherwise
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// baseId := "spiffe://example.org/spike/pilot/recover"
|
||||||
|
// extendedId := "spiffe://example.org/spike/pilot/recover/instance-0"
|
||||||
|
//
|
||||||
|
// // Both will return true
|
||||||
|
// if IsPilotRecover("example.org,other.org", baseId) {
|
||||||
|
// // Handle recovery-specific logic
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if IsPilotRecover("example.org,other.org", extendedId) {
|
||||||
|
// // Also recognized as a pilot recovery, with instance metadata
|
||||||
|
// }
|
||||||
|
func IsPilotRecover(trustRoots, id string) bool {
|
||||||
|
for _, root := range strings.Split(trustRoots, ",") {
|
||||||
|
baseId := SpikePilotRecover(strings.TrimSpace(root))
|
||||||
|
// Check if the ID is either exactly the base ID or starts with the base ID
|
||||||
|
// followed by "/"
|
||||||
|
if id == baseId || strings.HasPrefix(id, baseId+"/") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPilotRestore checks if a given SPIFFE ID matches the SPIKE Pilot's restore
|
||||||
|
// SPIFFE ID pattern.
|
||||||
|
//
|
||||||
|
// This function verifies if the provided SPIFFE ID corresponds to a pilot
|
||||||
|
// instance with restore capabilities by comparing it against the expected
|
||||||
|
// restore SPIFFE ID pattern.
|
||||||
|
//
|
||||||
|
// The function supports two formats:
|
||||||
|
// - Exact match: "spiffe://<trustRoot>/spike/pilot/restore"
|
||||||
|
// - Extended match with metadata: "spiffe://<trustRoot>/spike/pilot/restore/<metadata>"
|
||||||
|
//
|
||||||
|
// This allows for instance-specific identifiers while maintaining compatibility
|
||||||
|
// with the base pilot restore identity.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - trustRoots: Comma-delimited list of trust domain roots
|
||||||
|
// (e.g., "example.org,other.org")
|
||||||
|
// - id: The SPIFFE ID string to check
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - bool: true if the provided SPIFFE ID matches either the exact pilot restore ID
|
||||||
|
// or an extended ID with additional path segments for any of the trust roots,
|
||||||
|
// false otherwise
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// baseId := "spiffe://example.org/spike/pilot/restore"
|
||||||
|
// extendedId := "spiffe://example.org/spike/pilot/restore/instance-0"
|
||||||
|
//
|
||||||
|
// // Both will return true
|
||||||
|
// if IsPilotRestore("example.org,other.org", baseId) {
|
||||||
|
// // Handle restore-specific logic
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if IsPilotRestore("example.org,other.org", extendedId) {
|
||||||
|
// // Also recognized as a pilot restore, with instance metadata
|
||||||
|
// }
|
||||||
|
func IsPilotRestore(trustRoots, id string) bool {
|
||||||
|
for _, root := range strings.Split(trustRoots, ",") {
|
||||||
|
baseId := SpikePilotRestore(strings.TrimSpace(root))
|
||||||
|
// Check if the ID is either exactly the base ID or starts with the base ID
|
||||||
|
// followed by "/"
|
||||||
|
if id == baseId || strings.HasPrefix(id, baseId+"/") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsKeeper checks if a given SPIFFE ID matches the SPIKE Keeper's SPIFFE ID.
|
||||||
|
//
|
||||||
|
// This function is used for identity verification to determine if the provided
|
||||||
|
// SPIFFE ID belongs to a SPIKE Keeper instance. It compares the input against
|
||||||
|
// the expected keeper SPIFFE ID pattern.
|
||||||
|
//
|
||||||
|
// The function supports two formats:
|
||||||
|
// - Exact match: "spiffe://<trustRoot>/spike/keeper"
|
||||||
|
// - Extended match with metadata: "spiffe://<trustRoot>/spike/keeper/<metadata>"
|
||||||
|
//
|
||||||
|
// This allows for instance-specific identifiers while maintaining compatibility
|
||||||
|
// with the base keeper identity.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - trustRoots: Comma-delimited list of trust domain roots
|
||||||
|
// (e.g., "example.org,other.org")
|
||||||
|
// - id: The SPIFFE ID string to check
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - bool: true if the provided SPIFFE ID matches either the exact
|
||||||
|
// SPIKE Keeper's ID or an extended ID with additional path segments for any
|
||||||
|
// of the trust roots, false otherwise
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// baseId := "spiffe://example.org/spike/keeper"
|
||||||
|
// extendedId := "spiffe://example.org/spike/keeper/instance-0"
|
||||||
|
//
|
||||||
|
// // Both will return true
|
||||||
|
// if IsKeeper("example.org", baseId) {
|
||||||
|
// // Handle keeper-specific logic
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if IsKeeper("example.org", extendedId) {
|
||||||
|
// // Also recognized as a keeper, with instance metadata
|
||||||
|
// }
|
||||||
|
func IsKeeper(trustRoots, id string) bool {
|
||||||
|
for _, root := range strings.Split(trustRoots, ",") {
|
||||||
|
baseId := SpikeKeeper(strings.TrimSpace(root))
|
||||||
|
// Check if the ID is either exactly the base ID or starts with the base ID
|
||||||
|
// followed by "/"
|
||||||
|
if id == baseId || strings.HasPrefix(id, baseId+"/") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNexus checks if the provided SPIFFE ID matches the SPIKE Nexus SPIFFE ID.
|
||||||
|
//
|
||||||
|
// The function compares the input SPIFFE ID against the configured Spike Nexus
|
||||||
|
// SPIFFE ID pattern. This is typically used for validating whether a given
|
||||||
|
// identity represents the Nexus service.
|
||||||
|
//
|
||||||
|
// The function supports two formats:
|
||||||
|
// - Exact match: "spiffe://<trustRoot>/spike/nexus"
|
||||||
|
// - Extended match with metadata: "spiffe://<trustRoot>/spike/nexus/<metadata>"
|
||||||
|
//
|
||||||
|
// This allows for instance-specific identifiers while maintaining compatibility
|
||||||
|
// with the base Nexus identity.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - trustRoots: Comma-delimited list of trust domain roots
|
||||||
|
// (e.g., "example.org,other.org")
|
||||||
|
// - id: The SPIFFE ID string to check
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - bool: true if the SPIFFE ID matches either the exact Nexus SPIFFE ID
|
||||||
|
// or an extended ID with additional path segments for any of the
|
||||||
|
// trust roots, false otherwise
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// baseId := "spiffe://example.org/spike/nexus"
|
||||||
|
// extendedId := "spiffe://example.org/spike/nexus/instance-0"
|
||||||
|
//
|
||||||
|
// // Both will return true
|
||||||
|
// if IsNexus("example.org", baseId) {
|
||||||
|
// // Handle Nexus-specific logic
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if IsNexus("example.org", extendedId) {
|
||||||
|
// // Also recognized as a Nexus, with instance metadata
|
||||||
|
// }
|
||||||
|
func IsNexus(trustRoots, id string) bool {
|
||||||
|
for _, root := range strings.Split(trustRoots, ",") {
|
||||||
|
baseId := SpikeNexus(strings.TrimSpace(root))
|
||||||
|
// Check if the ID is either exactly the base ID or starts with the base ID
|
||||||
|
// followed by "/"
|
||||||
|
if id == baseId || strings.HasPrefix(id, baseId+"/") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeerCanTalkToAnyone is used for debugging purposes
|
||||||
|
func PeerCanTalkToAnyone(_, _ string) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeerCanTalkToKeeper checks if the provided SPIFFE ID matches the SPIKE Nexus
|
||||||
|
// SPIFFE ID.
|
||||||
|
//
|
||||||
|
// This is used as a validator in SPIKE Keeper because currently only SPIKE
|
||||||
|
// Nexus can talk to SPIKE Keeper.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - trustRoots: Comma-delimited list of trust domain roots
|
||||||
|
// (e.g., "example.org,other.org")
|
||||||
|
// - peerSpiffeId: The SPIFFE ID string to check
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - bool: true if the SPIFFE ID matches SPIKE Nexus' SPIFFE ID for any of
|
||||||
|
// the trust roots, false otherwise
|
||||||
|
func PeerCanTalkToKeeper(trustRoots, peerSpiffeId string) bool {
|
||||||
|
return IsNexus(trustRoots, peerSpiffeId)
|
||||||
|
}
|
|
@ -0,0 +1,259 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package spiffeid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsPilot(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
beforeTest func()
|
||||||
|
spiffeid string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default valid spiffeid",
|
||||||
|
beforeTest: nil,
|
||||||
|
spiffeid: "spiffe://spike.ist/spike/pilot/role/superuser",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default invalid spiffeid",
|
||||||
|
beforeTest: nil,
|
||||||
|
spiffeid: "spiffe://test/spike/pilot/role/superuser",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom valid spiffeid",
|
||||||
|
beforeTest: func() {
|
||||||
|
if err := os.Setenv("SPIKE_TRUST_ROOT", "corp.com"); err != nil {
|
||||||
|
panic("failed to set env SPIKE_TRUST_ROOT")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spiffeid: "spiffe://corp.com/spike/pilot/role/superuser",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom invalid spiffeid",
|
||||||
|
beforeTest: func() {
|
||||||
|
if err := os.Setenv("SPIKE_TRUST_ROOT", "corp.com"); err != nil {
|
||||||
|
panic("failed to set env SPIKE_TRUST_ROOT")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spiffeid: "spiffe://invalid/spike/pilot/role/superuser",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.beforeTest != nil {
|
||||||
|
tt.beforeTest()
|
||||||
|
}
|
||||||
|
if got := IsPilot(tt.spiffeid); got != tt.want {
|
||||||
|
t.Errorf("IsPilot() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err := os.Unsetenv("SPIKE_TRUST_ROOT"); err != nil {
|
||||||
|
panic("failed to unset env SPIKE_TRUST_ROOT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsKeeper(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
beforeTest func()
|
||||||
|
spiffeid string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default valid spiffeid",
|
||||||
|
beforeTest: nil,
|
||||||
|
spiffeid: "spiffe://spike.ist/spike/keeper",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default invalid spiffeid",
|
||||||
|
beforeTest: nil,
|
||||||
|
spiffeid: "spiffe://test/spike/keeper",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom valid spiffeid",
|
||||||
|
beforeTest: func() {
|
||||||
|
if err := os.Setenv("SPIKE_TRUST_ROOT", "corp.com"); err != nil {
|
||||||
|
panic("failed to set env SPIKE_TRUST_ROOT")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spiffeid: "spiffe://corp.com/spike/keeper",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom invalid spiffeid",
|
||||||
|
beforeTest: func() {
|
||||||
|
if err := os.Setenv("SPIKE_TRUST_ROOT", "corp.com"); err != nil {
|
||||||
|
panic("failed to set env SPIKE_TRUST_ROOT")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spiffeid: "spiffe://invalid/spike/keeper",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.beforeTest != nil {
|
||||||
|
tt.beforeTest()
|
||||||
|
}
|
||||||
|
if got := IsKeeper(tt.spiffeid); got != tt.want {
|
||||||
|
t.Errorf("IsKeeper() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err := os.Unsetenv("SPIKE_TRUST_ROOT"); err != nil {
|
||||||
|
panic("failed to unset env SPIKE_TRUST_ROOT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsNexus(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
beforeTest func()
|
||||||
|
spiffeid string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default valid spiffeid",
|
||||||
|
beforeTest: nil,
|
||||||
|
spiffeid: "spiffe://spike.ist/spike/nexus",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default invalid spiffeid",
|
||||||
|
beforeTest: nil,
|
||||||
|
spiffeid: "spiffe://test/spike/nexus",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom valid spiffeid",
|
||||||
|
beforeTest: func() {
|
||||||
|
if err := os.Setenv("SPIKE_TRUST_ROOT", "corp.com"); err != nil {
|
||||||
|
panic("failed to set env SPIKE_TRUST_ROOT")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spiffeid: "spiffe://corp.com/spike/nexus",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom invalid spiffeid",
|
||||||
|
beforeTest: func() {
|
||||||
|
if err := os.Setenv("SPIKE_TRUST_ROOT", "corp.com"); err != nil {
|
||||||
|
panic("failed to set env SPIKE_TRUST_ROOT")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spiffeid: "spiffe://invalid/spike/nexus",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.beforeTest != nil {
|
||||||
|
tt.beforeTest()
|
||||||
|
}
|
||||||
|
if got := IsNexus(tt.spiffeid); got != tt.want {
|
||||||
|
t.Errorf("IsNexus() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err := os.Unsetenv("SPIKE_TRUST_ROOT"); err != nil {
|
||||||
|
panic("failed to unset env SPIKE_TRUST_ROOT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanTalkToAnyone(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default",
|
||||||
|
in: "",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := PeerCanTalkToAnyone(tt.in); got != tt.want {
|
||||||
|
t.Errorf("PeerCanTalkToAnyone() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanTalkToKeeper(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
beforeTest func()
|
||||||
|
spiffeid string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default nexus spiffe id",
|
||||||
|
beforeTest: nil,
|
||||||
|
spiffeid: "spiffe://spike.ist/spike/nexus",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default keeper spiffe id",
|
||||||
|
beforeTest: nil,
|
||||||
|
spiffeid: "spiffe://spike.ist/spike/keeper",
|
||||||
|
// Keepers cannot talk to keepers.
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom nexus spiffe id",
|
||||||
|
beforeTest: func() {
|
||||||
|
if err := os.Setenv("SPIKE_TRUST_ROOT", "corp.com"); err != nil {
|
||||||
|
panic("failed to set env SPIKE_TRUST_ROOT")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spiffeid: "spiffe://corp.com/spike/nexus",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom keeper spiffe id",
|
||||||
|
beforeTest: func() {
|
||||||
|
if err := os.Setenv("SPIKE_TRUST_ROOT", "corp.com"); err != nil {
|
||||||
|
panic("failed to set env SPIKE_TRUST_ROOT")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spiffeid: "spiffe://corp.com/spike/keeper",
|
||||||
|
// Keepers cannot talk to keepers; only Nexus can talk to Keepers.
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pilot spiffe id",
|
||||||
|
beforeTest: nil,
|
||||||
|
spiffeid: "spiffe://spike.ist/spike/pilot/role/superuser",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.beforeTest != nil {
|
||||||
|
tt.beforeTest()
|
||||||
|
}
|
||||||
|
if got := PeerCanTalkToKeeper(tt.spiffeid); got != tt.want {
|
||||||
|
t.Errorf("PeerCanTalkToKeeper() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err := os.Unsetenv("SPIKE_TRUST_ROOT"); err != nil {
|
||||||
|
panic("failed to unset env SPIKE_TRUST_ROOT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,9 +2,14 @@
|
||||||
// \\\\\ Copyright 2024-present SPIKE contributors.
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package entity
|
package env
|
||||||
|
|
||||||
type InitState string
|
import "os"
|
||||||
|
|
||||||
const AlreadyInitialized InitState = "AlreadyInitialized"
|
func TrustRoot() string {
|
||||||
const NotInitialized InitState = "NotInitialized"
|
tr := os.Getenv("SPIKE_TRUST_ROOT")
|
||||||
|
if tr == "" {
|
||||||
|
return "spike.ist"
|
||||||
|
}
|
||||||
|
return tr
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package spiffeid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/spiffe/spike-sdk-go/spiffeid/internal/env"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SpikeKeeper constructs and returns the SPIKE Keeper's SPIFFE ID string.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - trustRoot: The trust domain for the SPIFFE ID. If empty, the value is
|
||||||
|
// obtained from the environment.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: The complete SPIFFE ID in the format:
|
||||||
|
// "spiffe://<trustRoot>/spike/keeper"
|
||||||
|
func SpikeKeeper(trustRoot string) string {
|
||||||
|
if trustRoot == "" {
|
||||||
|
trustRoot = env.TrustRoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
return "spiffe://" + path.Join(trustRoot, "spike", "keeper")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpikeNexus constructs and returns the SPIFFE ID for SPIKE Nexus.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - trustRoot: The trust domain for the SPIFFE ID. If empty, the value is
|
||||||
|
// obtained from the environment.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: The complete SPIFFE ID in the format:
|
||||||
|
// "spiffe://<trustRoot>/spike/nexus"
|
||||||
|
func SpikeNexus(trustRoot string) string {
|
||||||
|
if trustRoot == "" {
|
||||||
|
trustRoot = env.TrustRoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
return "spiffe://" + path.Join(trustRoot, "spike", "nexus")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpikePilot generates the SPIFFE ID for a SPIKE Pilot superuser role.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - trustRoot: The trust domain for the SPIFFE ID. If empty, the value is
|
||||||
|
// obtained from the environment.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: The complete SPIFFE ID in the format:
|
||||||
|
// "spiffe://<trustRoot>/spike/pilot/role/superuser"
|
||||||
|
func SpikePilot(trustRoot string) string {
|
||||||
|
if trustRoot == "" {
|
||||||
|
trustRoot = env.TrustRoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
return "spiffe://" + path.Join(trustRoot, "spike", "pilot", "role", "superuser")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpikePilotRecover generates the SPIFFE ID for a SPIKE Pilot recovery role.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - trustRoot: The trust domain for the SPIFFE ID. If empty, the value is
|
||||||
|
// obtained from the environment.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: The complete SPIFFE ID in the format:
|
||||||
|
// "spiffe://<trustRoot>/spike/pilot/role/recover"
|
||||||
|
func SpikePilotRecover(trustRoot string) string {
|
||||||
|
if trustRoot == "" {
|
||||||
|
trustRoot = env.TrustRoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
return "spiffe://" + path.Join(trustRoot, "spike", "pilot", "role", "recover")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpikePilotRestore generates the SPIFFE ID for a SPIKE Pilot restore role.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - trustRoot: The trust domain for the SPIFFE ID. If empty, the value is
|
||||||
|
// obtained from the environment.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: The complete SPIFFE ID in the format:
|
||||||
|
// "spiffe://<trustRoot>/spike/pilot/role/restore"
|
||||||
|
func SpikePilotRestore(trustRoot string) string {
|
||||||
|
if trustRoot == "" {
|
||||||
|
trustRoot = env.TrustRoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
return "spiffe://" + path.Join(trustRoot, "spike", "pilot", "role", "restore")
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
// \\ SPIKE: Secure your secrets with SPIFFE.
|
||||||
|
// \\\\\ Copyright 2024-present SPIKE contributors.
|
||||||
|
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeepAlive blocks the current goroutine until it receives either a
|
||||||
|
// SIGINT (Ctrl+C) or SIGTERM signal, enabling graceful shutdown of the
|
||||||
|
// application. Upon receiving a termination signal, it logs the signal type
|
||||||
|
// and begins the shutdown process.
|
||||||
|
//
|
||||||
|
// The function creates a buffered channel to handle OS signals and uses
|
||||||
|
// signal.Notify to register for SIGINT and SIGTERM signals. It then blocks
|
||||||
|
// until a signal is received.
|
||||||
|
//
|
||||||
|
// This is typically used in the main function to prevent the program from
|
||||||
|
// exiting immediately and to ensure proper cleanup when the program is
|
||||||
|
// terminated.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// func main() {
|
||||||
|
// // Initialize your application
|
||||||
|
// setupApp()
|
||||||
|
//
|
||||||
|
// // Keep the application running until shutdown signal
|
||||||
|
// KeepAlive()
|
||||||
|
//
|
||||||
|
// // Perform cleanup
|
||||||
|
// cleanup()
|
||||||
|
// }
|
||||||
|
func KeepAlive() {
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
sig := <-sigChan
|
||||||
|
|
||||||
|
log.Printf("\nReceived %v signal, shutting down gracefully...\n", sig)
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/spiffe/spike-sdk-go/api/entity/data"
|
||||||
|
|
||||||
|
"github.com/spiffe/spike-sdk-go/api/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const validNamePattern = `^[a-zA-Z0-9-_ ]+$`
|
||||||
|
const maxNameLength = 250
|
||||||
|
const validSpiffeIdPattern = `^\^?spiffe://[\\a-zA-Z0-9.\-*()+?\[\]]+(/[\\/a-zA-Z0-9._\-*()+?\[\]]+)*\$?$`
|
||||||
|
const validRawSpiffeIdPattern = `^spiffe://[a-zA-Z0-9.-]+(/[a-zA-Z0-9._-]+)*$`
|
||||||
|
const maxPathPatternLength = 500
|
||||||
|
const validPathPattern = `^[a-zA-Z0-9._\-/^$()?+*|[\]{}\\]+$`
|
||||||
|
const validPath = `^[a-zA-Z0-9._\-/()?+*|[\]{}\\]+$`
|
||||||
|
|
||||||
|
// ValidateName checks if the provided name meets length and format constraints.
|
||||||
|
// It returns an error if the name is invalid, otherwise it returns nil.
|
||||||
|
func ValidateName(name string) error {
|
||||||
|
// Validate length
|
||||||
|
if len(name) == 0 || len(name) > maxNameLength {
|
||||||
|
return errors.ErrInvalidInput
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate format
|
||||||
|
if match, _ := regexp.MatchString(validNamePattern, name); !match {
|
||||||
|
return errors.ErrInvalidInput
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSpiffeIdPattern validates whether the given SPIFFE ID pattern string
|
||||||
|
// conforms to the expected format and returns an error if it does not.
|
||||||
|
func ValidateSpiffeIdPattern(spiffeIdPattern string) error {
|
||||||
|
// Validate SpiffeIdPattern
|
||||||
|
if match, _ := regexp.MatchString(
|
||||||
|
validSpiffeIdPattern, spiffeIdPattern); !match {
|
||||||
|
return errors.ErrInvalidInput
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSpiffeId validates if the given SPIFFE ID matches the expected format.
|
||||||
|
// Returns an error if the SPIFFE ID is invalid.
|
||||||
|
func ValidateSpiffeId(spiffeId string) error {
|
||||||
|
if match, _ := regexp.MatchString(
|
||||||
|
validRawSpiffeIdPattern, spiffeId); !match {
|
||||||
|
return errors.ErrInvalidInput
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePathPattern validates the given path pattern string for correctness.
|
||||||
|
// Returns an error if the pattern is empty, too long, or has invalid characters.
|
||||||
|
func ValidatePathPattern(pathPattern string) error {
|
||||||
|
// Validate length
|
||||||
|
if len(pathPattern) == 0 || len(pathPattern) > maxPathPatternLength {
|
||||||
|
return errors.ErrInvalidInput
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate format
|
||||||
|
// Allow regex special characters along with alphanumeric and basic symbols
|
||||||
|
if match, _ := regexp.MatchString(validPathPattern, pathPattern); !match {
|
||||||
|
return errors.ErrInvalidInput
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePath checks if the given path is valid based on predefined rules.
|
||||||
|
// It returns an error if the path is empty, too long, or contains invalid
|
||||||
|
// characters.
|
||||||
|
func ValidatePath(path string) error {
|
||||||
|
if len(path) == 0 || len(path) > maxPathPatternLength {
|
||||||
|
return errors.ErrInvalidInput
|
||||||
|
}
|
||||||
|
if match, _ := regexp.MatchString(validPath, path); !match {
|
||||||
|
return errors.ErrInvalidInput
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePolicyId verifies if the given policyId is a valid UUID format.
|
||||||
|
// Returns errors.ErrInvalidInput if the validation fails.
|
||||||
|
func ValidatePolicyId(policyId string) error {
|
||||||
|
err := uuid.Validate(policyId)
|
||||||
|
if err != nil {
|
||||||
|
return errors.ErrInvalidInput
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePermissions checks if all provided permissions are valid.
|
||||||
|
// Permissions are compared against a predefined list of allowed permissions.
|
||||||
|
// Returns ErrInvalidInput if any permission is invalid, nil otherwise.
|
||||||
|
func ValidatePermissions(permissions []data.PolicyPermission) error {
|
||||||
|
allowedPermissions := []data.PolicyPermission{
|
||||||
|
data.PermissionList,
|
||||||
|
data.PermissionRead,
|
||||||
|
data.PermissionWrite,
|
||||||
|
data.PermissionSuper,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, permission := range permissions {
|
||||||
|
isAllowed := false
|
||||||
|
for _, allowedPermission := range allowedPermissions {
|
||||||
|
if permission == allowedPermission {
|
||||||
|
isAllowed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isAllowed {
|
||||||
|
return errors.ErrInvalidInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue