secrets: add plugin

Also modify rest to use secrets plugin
This commit is contained in:
Chris Sexton 2021-05-20 09:59:28 -04:00 committed by Chris Sexton
parent 0cd1b5e7ed
commit a1c5a4c580
5 changed files with 346 additions and 3 deletions

View File

@ -22,7 +22,16 @@ import (
type Config struct {
*sqlx.DB
DBFile string
DBFile string
secrets map[string]Secret
}
// Secret is a config value that is loaded permanently and not ever displayed
type Secret struct {
// Key is the key field of the table
Key string `db:"key"`
// Value represents the secret that must not be shared
Value string `db:"value"`
}
// GetFloat64 returns the config value for a string key
@ -85,6 +94,9 @@ func (c *Config) GetString(key, fallback string) string {
if v, found := os.LookupEnv(envkey(key)); found {
return v
}
if v, found := c.secrets[key]; found {
return v.Value
}
var configValue string
q := `select value from config where key=?`
err := c.DB.Get(&configValue, q, key)
@ -162,6 +174,33 @@ func (c *Config) Set(key, value string) error {
return nil
}
func (c *Config) RefreshSecrets() error {
q := `select key, value from secrets`
var secrets []Secret
err := c.Select(&secrets, q)
if err != nil {
return err
}
secretMap := map[string]Secret{}
for _, s := range secrets {
secretMap[s.Key] = s
}
c.secrets = secretMap
return nil
}
func (c *Config) GetAllSecrets() map[string]Secret {
return c.secrets
}
func (c *Config) SecretKeys() []string {
keys := []string{}
for k := range c.secrets {
keys = append(keys, k)
}
return keys
}
func (c *Config) SetMap(key string, values map[string]string) error {
b, err := json.Marshal(values)
if err != nil {
@ -199,7 +238,8 @@ func ReadConfig(dbpath string) *Config {
log.Fatal().Err(err)
}
c := Config{
DBFile: dbpath,
DBFile: dbpath,
secrets: map[string]Secret{},
}
c.DB = sqlDB
@ -208,7 +248,19 @@ func ReadConfig(dbpath string) *Config {
value string,
primary key (key)
);`); err != nil {
panic(err)
log.Fatal().Err(err).Msgf("failed to initialize config")
}
if _, err := c.Exec(`create table if not exists secrets (
key string,
value string,
primary key (key)
);`); err != nil {
log.Fatal().Err(err).Msgf("failed to initialize config")
}
if err := c.RefreshSecrets(); err != nil {
log.Fatal().Err(err).Msgf("failed to initialize config")
}
log.Info().Msgf("catbase is running.")

View File

@ -15,6 +15,7 @@ import (
"github.com/velour/catbase/plugins/giphy"
"github.com/velour/catbase/plugins/last"
"github.com/velour/catbase/plugins/rest"
"github.com/velour/catbase/plugins/secrets"
"github.com/velour/catbase/plugins/achievements"
"github.com/velour/catbase/plugins/aoc"
@ -121,6 +122,7 @@ func main() {
b := bot.New(c, client)
b.AddPlugin(admin.New(b))
b.AddPlugin(secrets.New(b))
b.AddPlugin(giphy.New(b))
b.AddPlugin(emojifyme.New(b))
b.AddPlugin(last.New(b))

View File

@ -15,6 +15,7 @@ import (
"text/template"
"github.com/itchyny/gojq"
"github.com/rs/zerolog/log"
"github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot"
@ -235,9 +236,14 @@ func (p *RestPlugin) mkHandler(w *wire) bot.ResponseHandler {
return false
}
values := bot.RegexValues{}
for _, s := range p.b.Config().GetAllSecrets() {
values[s.Key] = s.Value
}
log.Debug().Interface("values", values).Msgf("secrets")
for k := range r.Values {
values[k] = url.QueryEscape(r.Values[k])
}
log.Debug().Interface("values", values).Msgf("r.Values")
urlStr := w.URL.String()
parse, err := template.New(urlStr).Parse(urlStr)
if p.handleErr(err, r) {
@ -249,6 +255,10 @@ func (p *RestPlugin) mkHandler(w *wire) bot.ResponseHandler {
return true
}
newURL, err := url.Parse(buf.String())
log.Debug().
Interface("values", values).
Str("URL", buf.String()).
Msg("Querying URL with values")
if p.handleErr(err, r) {
return true
}

154
plugins/secrets/secrets.go Normal file
View File

@ -0,0 +1,154 @@
package secrets
import (
"encoding/json"
"fmt"
"net/http"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/config"
)
type SecretsPlugin struct {
b bot.Bot
c *config.Config
db *sqlx.DB
}
func New(b bot.Bot) *SecretsPlugin {
p := &SecretsPlugin{
b: b,
c: b.Config(),
db: b.DB(),
}
p.registerWeb()
return p
}
func (p *SecretsPlugin) registerWeb() {
http.HandleFunc("/secrets/add", p.handleRegister)
http.HandleFunc("/secrets/remove", p.handleRemove)
http.HandleFunc("/secrets/all", p.handleAll)
http.HandleFunc("/secrets/test", func(w http.ResponseWriter, r *http.Request) {
value := r.URL.Query().Get("test")
j, _ := json.Marshal(map[string]string{"value": value})
w.Write(j)
})
http.HandleFunc("/secrets", p.handleIndex)
p.b.RegisterWeb("/secrets", "Secrets")
}
func (p *SecretsPlugin) registerSecret(key, value string) error {
q := `insert into secrets (key, value) values (?, ?)`
_, err := p.db.Exec(q, key, value)
if err != nil {
return err
}
return p.c.RefreshSecrets()
}
func (p *SecretsPlugin) removeSecret(key string) error {
q := `delete from secrets where key=?`
_, err := p.db.Exec(q, key)
if err != nil {
return err
}
return p.c.RefreshSecrets()
}
func (p *SecretsPlugin) updateSecret(key, value string) error {
q := `update secrets set value=? where key=?)`
_, err := p.db.Exec(q, value, key)
if err != nil {
return err
}
return p.c.RefreshSecrets()
}
func mkCheckError(w http.ResponseWriter) func(error) bool {
return func(err error) bool {
if err != nil {
log.Error().Stack().Err(err).Msgf("secret failed")
w.WriteHeader(500)
e, _ := json.Marshal(err)
w.Write(e)
return true
}
return false
}
}
func checkMethod(method string, w http.ResponseWriter, r *http.Request) bool {
if r.Method != method {
w.WriteHeader(405)
fmt.Fprintf(w, "Incorrect HTTP method")
return true
}
return false
}
func (p *SecretsPlugin) sendKeys(w http.ResponseWriter, r *http.Request) {
checkError := mkCheckError(w)
log.Debug().Msgf("Keys before refresh: %v", p.c.SecretKeys())
err := p.c.RefreshSecrets()
log.Debug().Msgf("Keys after refresh: %v", p.c.SecretKeys())
if checkError(err) {
return
}
keys, err := json.Marshal(p.c.SecretKeys())
if checkError(err) {
return
}
w.WriteHeader(200)
w.Write(keys)
}
func (p *SecretsPlugin) handleAll(w http.ResponseWriter, r *http.Request) {
p.sendKeys(w, r)
}
func (p *SecretsPlugin) handleRegister(w http.ResponseWriter, r *http.Request) {
log.Debug().Msgf("handleRegister")
if checkMethod(http.MethodPost, w, r) {
log.Debug().Msgf("failed post %s", r.Method)
return
}
checkError := mkCheckError(w)
decoder := json.NewDecoder(r.Body)
secret := config.Secret{}
err := decoder.Decode(&secret)
log.Debug().Msgf("decoding: %s", err)
if checkError(err) {
return
}
log.Debug().Msgf("Secret: %s", secret)
err = p.registerSecret(secret.Key, secret.Value)
if checkError(err) {
return
}
p.sendKeys(w, r)
}
func (p *SecretsPlugin) handleRemove(w http.ResponseWriter, r *http.Request) {
if checkMethod(http.MethodDelete, w, r) {
return
}
checkError := mkCheckError(w)
decoder := json.NewDecoder(r.Body)
secret := config.Secret{}
err := decoder.Decode(&secret)
if checkError(err) {
return
}
err = p.removeSecret(secret.Key)
if checkError(err) {
return
}
p.sendKeys(w, r)
}
func (p *SecretsPlugin) handleIndex(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(indexTpl))
}

125
plugins/secrets/web.go Normal file
View File

@ -0,0 +1,125 @@
package secrets
var indexTpl = `
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Load required Bootstrap and BootstrapVue CSS -->
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css">
<link type="text/css" rel="stylesheet" href="//cdn.jsdelivr.net/npm/bootstrap-vue@2.21.2/dist/bootstrap-vue.min.css"/>
<!-- Load polyfills to support older browsers -->
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CMutationObserver"></script>
<!-- Load Vue followed by BootstrapVue -->
<script src="//cdn.jsdelivr.net/npm/vue"></script>
<script src="//cdn.jsdelivr.net/npm/bootstrap-vue@2.21.2/dist/bootstrap-vue.js"></script>
<script src="//cdn.jsdelivr.net/npm/bootstrap-vue@2.21.2/dist/bootstrap-vue-icons.js"></script>
<script src="//cdn.jsdelivr.net/npm/vue-router@3.5.1/dist/vue-router.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/axios@0.21.1/dist/axios.min.js"></script>
<meta charset="UTF-8">
<title>Memes</title>
</head>
<body>
<div id="app">
<b-navbar>
<b-navbar-brand>Memes</b-navbar-brand>
<b-navbar-nav>
<b-nav-item v-for="item in nav" :href="item.url" :active="item.name === 'Meme'" :key="item.key">{{ item.name }}</b-nav-item>
</b-navbar-nav>
</b-navbar>
<b-alert
dismissable
variant="error"
:show="err != ''"
@dismissed="err = ''">
{{ err }}
</b-alert>
<b-form @submit="add">
<b-container>
<b-row>
<b-col cols="3">
<b-input placeholder="Key..." v-model="secret.key"></b-input>
</b-col>
<b-col cols="3">
<b-input placeholder="Value..." v-model="secret.value"></b-input>
</b-col>
<b-col cols="3">
<b-button type="submit">Add Secret</b-button>
</b-col>
</b-row>
<b-row style="padding-top: 2em;">
<b-col>
<ul>
<li v-for="key in results" key="key"><a @click="rm(key)" href="#">X</a> {{key}}</li>
</ul>
</b-col>
</b-row>
</b-container>
</b-form>
</div>
<script>
var router = new VueRouter({
mode: 'history',
routes: []
});
var app = new Vue({
el: '#app',
router,
data: {
err: '',
nav: [],
secret: {key: '', value: ''},
results: [],
fields: [
{key: 'key', sortable: true},
]
},
mounted() {
axios.get('/nav')
.then(resp => {
this.nav = resp.data;
})
.catch(err => console.log(err))
this.refresh();
},
methods: {
refresh: function () {
axios.get('/secrets/all')
.then(resp => {
this.results = resp.data
this.err = ''
})
.catch(err => (this.err = err))
},
add: function (evt) {
if (evt) {
evt.preventDefault();
evt.stopPropagation();
}
axios.post('/secrets/add', this.secret)
.then(resp => {
this.results = resp.data;
this.secret.key = '';
this.secret.value = '';
this.refresh();
})
.catch(err => this.err = err)
},
rm: function (key) {
if (confirm("Are you sure you want to delete this meme?")) {
axios.delete('/secrets/remove', {data: {key: key}})
.then(resp => {
this.refresh();
})
.catch(err => this.err = err)
}
}
}
})
</script>
</body>
</html>
`