Skip to content

Commit

Permalink
Merge branch 'main' of github.com:tiithansen/sealed-secrets-web
Browse files Browse the repository at this point in the history
  • Loading branch information
Robert Wiesner committed May 12, 2023
2 parents 211c4fc + 17a0580 commit 131360a
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 12 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ on:
types:
- published

permissions:
packages: write

jobs:
build-images:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ func setupRouter(coreClient corev1.CoreV1Interface, ssClient ssClient.BitnamiV1a
api.GET("/certificate", h.Certificate)
api.POST("/kubeseal", h.KubeSeal)
api.POST("/dencode", h.Dencode)
api.POST("/validate", h.Validate)

api.GET("/secret/:namespace/:name", sHandler.Secret)
api.GET("/secrets", sHandler.AllSecrets)
Expand Down
18 changes: 18 additions & 0 deletions pkg/handler/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package handler

import (
"log"
"net/http"

"github.com/gin-gonic/gin"
)

func (h *Handler) Validate(c *gin.Context) {
err := h.sealer.Validate(c.Request.Body)
if err != nil {
log.Printf("Error in %s: %v\n", Sanitize(c.Request.URL.Path), err)
c.Data(http.StatusBadRequest, "text/plain", []byte(err.Error()))
} else {
c.Data(http.StatusOK, "text/plain", []byte("OK"))
}
}
62 changes: 62 additions & 0 deletions pkg/handler/validate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package handler

import (
"bytes"
"errors"
"net/http"
"net/http/httptest"

"github.com/bakito/sealed-secrets-web/pkg/mocks/seal"
"github.com/gin-gonic/gin"
"github.com/golang/mock/gomock"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("Handler ", func() {
Context("Validate", func() {
var (
recorder *httptest.ResponseRecorder
c *gin.Context
mock *gomock.Controller
sealer *seal.MockSealer
h *Handler
)
BeforeEach(func() {
gin.SetMode(gin.ReleaseMode)
recorder = httptest.NewRecorder()
c, _ = gin.CreateTestContext(recorder)
mock = gomock.NewController(GinkgoT())
sealer = seal.NewMockSealer(mock)
h = &Handler{
sealer: sealer,
}
})

It("should return success if validation succeeds", func() {
c.Request, _ = http.NewRequest("POST", "/v1/validate", bytes.NewReader([]byte(stringDataAsYAML)))
c.Request.Header.Set("Content-Type", "application/x-yaml")

sealer.EXPECT().Validate(gomock.Any()).Return(nil)

h.Validate(c)

Ω(recorder.Code).Should(Equal(http.StatusOK))
Ω(recorder.Body.String()).Should(Equal("OK"))
Ω(recorder.Header().Get("Content-Type")).Should(Equal("text/plain"))
})

It("should return an error if validation fails", func() {
c.Request, _ = http.NewRequest("POST", "/v1/validate", bytes.NewReader([]byte(stringDataAsYAML)))
c.Request.Header.Set("Content-Type", "application/x-yaml")

sealer.EXPECT().Validate(gomock.Any()).Return(errors.New("Validation failed"))

h.Validate(c)

Ω(recorder.Code).Should(Equal(http.StatusBadRequest))
Ω(recorder.Body.String()).Should(Equal("Validation failed"))
Ω(recorder.Header().Get("Content-Type")).Should(Equal("text/plain"))
})
})
})
14 changes: 14 additions & 0 deletions pkg/mocks/seal/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions pkg/seal/seal.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Sealer interface {
Raw(data Raw) ([]byte, error)
Certificate(ctx context.Context) ([]byte, error)
Seal(outputFormat string, secret io.Reader) ([]byte, error)
Validate(secret io.Reader) error
}

var _ Sealer = &apiSealer{}
Expand Down Expand Up @@ -100,6 +101,16 @@ func (a *apiSealer) Raw(data Raw) ([]byte, error) {
return buf.Bytes(), nil
}

func (a *apiSealer) Validate(secret io.Reader) error {
return kubeseal.ValidateSealedSecret(
context.TODO(),
a.clientConfig,
a.ss.Namespace,
a.ss.Service,
secret,
)
}

type Raw struct {
Value string `json:"value"`
Name string `json:"name"`
Expand Down
50 changes: 38 additions & 12 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<v-btn @click="dencode" text>Encode / Decode</v-btn>
{{ if eq .DisableLoadSecrets false}}<v-btn @click="loadSecrets" text>Secrets</v-btn>{{end}}
<v-btn @click="seal" text>Seal</v-btn>
<v-btn @click="validate" text>Validate</v-btn>
</v-app-bar>

<v-main>
Expand Down Expand Up @@ -71,10 +72,11 @@
</v-card>
</v-dialog>

<v-snackbar :bottom="true" :multi-line="true" :right="true" :timeout="5000" v-model="snackbar" color="error">
{{"{{error}}"}}
<v-btn @click="error = ''" dark text>Close</v-btn>
<v-snackbar :bottom="true" :multi-line="true" :right="true" :timeout="5000" v-model="snackbar" :color="messageType">
{{"{{message}}"}}
<v-btn @click="message = ''" dark text>Close</v-btn>
</v-snackbar>
</v-snackbar>
<v-footer color="primary" padless >
<v-col class="primary py-1 text-center white--text text-caption" cols="12" >
Sealed Secrets Web ({{.Version}})
Expand All @@ -100,7 +102,10 @@
return {
secrets: Object,
dialogVisible: false,
error: '',
message: '',
messageType: '',
successMessage: '',
showSuccess: false,
editor1: Object,
editor2: Object,
editor1Content: INITIAL_SECRET,
Expand Down Expand Up @@ -130,11 +135,11 @@
computed: {
snackbar: {
get() {
return this.error !== ''
return this.message !== ''
},
set(newValue) {
if (newValue === false) {
this.error = ''
this.message = ''
}
}
},
Expand Down Expand Up @@ -177,15 +182,17 @@
this.editor2Content = res.data
this.editor2.setValue(this.editor2Content, 1)
}).catch(err => {
this.error = err.response.data
this.messageType = 'error'
this.message = err.response.data
});
},
loadSecrets() {
axios.get('{{.WebContext}}api/secrets').then(res => {
this.secrets = res.data.secrets
this.dialogVisible = true
}).catch(err => {
this.error = err.response.data
this.messageType = 'error'
this.message = err.response.data
});
},
loadSecret(namespace, name) {
Expand All @@ -198,7 +205,8 @@
this.editor1.setValue(this.editor1Content, 1)
this.dialogVisible = false
}).catch(err => {
this.error = err.response.data
this.messageType = 'error'
this.message = err.response.data
});
},
dencode() {
Expand All @@ -211,7 +219,8 @@
this.editor1Content = res.data
this.editor1.setValue(this.editor1Content, 1)
}).catch(err => {
this.error = err.response
this.messageType = 'error'
this.message = err.response
});
},
changeSecretFormat(selected) {
Expand All @@ -224,7 +233,8 @@
}
this.editor1.setValue(this.editor1Content, 1)
} catch (err) {
this.error = err
this.messageType = 'error'
this.message = err
}
},
changeSealedSecretFormat(selected) {
Expand All @@ -237,14 +247,30 @@
}
this.editor2.setValue(this.editor2Content, 1)
} catch (err) {
this.error = err
this.messageType = 'error'
this.message = err
}
},
contentType(c) {
if (c === "json") {
return 'application/json'
}
return 'application/x-yaml'
},
validate() {
axios.post('{{.WebContext}}api/validate', this.editor2Content, {
headers: {
'Content-Type': this.contentType(this.secretFormat),
'Accept': 'text/plain'
},
transformResponse: (r) => r
}).then(res => {
this.messageType = 'success'
this.message = 'Sealed secret is valid'
}).catch(err => {
this.messageType = 'error'
this.message = err.response.data
});
}
}
})
Expand Down
40 changes: 40 additions & 0 deletions testdata/e2e/runTestValidate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/bin/bash
set -e

curl --version

echo "Test /api/validate should respond 200 if sealed secret is valid"

SEALED_SECRET=$(curl --silent --show-error --request POST 'http://localhost/ssw/api/kubeseal' \
--header 'Accept: application/x-yaml' \
--data-binary '@stringData.yaml')

echo "$SEALED_SECRET" | yq -r .apiVersion | grep --quiet "bitnami.com/v1alpha1"
echo "$SEALED_SECRET" | yq -r .kind | grep --quiet "SealedSecret"
echo "$SEALED_SECRET" | yq -r .metadata.name | grep --quiet "mysecretname"
echo "$SEALED_SECRET" | yq -r .metadata.namespace | grep --quiet "mysecretnamespace"

RESPONSE=$(curl --silent --show-error --request POST 'http://localhost/ssw/api/validate' \
--header 'Accept: text/plain' \
--data-binary "$SEALED_SECRET" \
--output /dev/null -w "%{http_code}" )

echo "$RESPONSE" | grep --quiet 200

echo "Test /api/validate should respond 400 if sealed secret is invalid"

INVALID_SECRET=$(curl --silent --show-error --request POST 'http://localhost/ssw/api/kubeseal' \
--header 'Accept: application/x-yaml' \
--data-binary '@stringData.yaml' | yq '.metadata.name = "wrongname"')

echo "$INVALID_SECRET" | yq -r .apiVersion | grep --quiet "bitnami.com/v1alpha1"
echo "$INVALID_SECRET" | yq -r .kind | grep --quiet "SealedSecret"
echo "$INVALID_SECRET" | yq -r .metadata.name | grep --quiet "wrongname"
echo "$INVALID_SECRET" | yq -r .metadata.namespace | grep --quiet "mysecretnamespace"

RESPONSE=$(curl --silent --show-error --request POST 'http://localhost/ssw/api/validate' \
--header 'Accept: text/plain' \
--data-binary "$INVALID_SECRET" \
--output /dev/null -w "%{http_code}" )

echo "$RESPONSE" | grep --quiet 400
2 changes: 2 additions & 0 deletions testdata/e2e/runTests.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#!/bin/bash
set -e

./runTestValidate.sh

./runTestKubeseal.sh

./runTestCertificate.sh
Expand Down

0 comments on commit 131360a

Please sign in to comment.