refactor(secrets): move secrets to environment entity

This commit is contained in:
fiftin 2024-07-02 23:45:59 +05:00
parent 891b27649e
commit c75e79efad
No known key found for this signature in database
GPG Key ID: 044381366A5D4731
8 changed files with 215 additions and 106 deletions

View File

@ -10,6 +10,55 @@ import (
"github.com/gorilla/context"
)
func updateEnvironmentSecrets(store db.Store, env db.Environment) error {
for _, secret := range env.Secrets {
var err error
var key db.AccessKey
switch secret.Operation {
case db.EnvironmentSecretCreate:
key, err = store.CreateAccessKey(db.AccessKey{
Name: secret.Name,
String: secret.Secret,
EnvironmentID: &env.ID,
ProjectID: &env.ProjectID,
Type: db.AccessKeyString,
})
case db.EnvironmentSecretDelete:
key, err = store.GetAccessKey(env.ProjectID, secret.ID)
if err != nil {
continue
}
if key.EnvironmentID == nil && *key.EnvironmentID == env.ID {
continue
}
err = store.DeleteAccessKey(env.ProjectID, secret.ID)
case db.EnvironmentSecretUpdate:
key, err = store.GetAccessKey(env.ProjectID, secret.ID)
if err != nil {
continue
}
if key.EnvironmentID == nil && *key.EnvironmentID == env.ID {
continue
}
err = store.UpdateAccessKey(db.AccessKey{
Name: secret.Name,
String: secret.Secret,
Type: db.AccessKeyString,
})
}
}
return nil
}
// EnvironmentMiddleware ensures an environment exists and loads it to the context
func EnvironmentMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -27,6 +76,20 @@ func EnvironmentMiddleware(next http.Handler) http.Handler {
return
}
keys, err := helpers.Store(r).GetEnvironmentSecrets(env.ProjectID, env.ID)
if err != nil {
helpers.WriteError(w, err)
return
}
for _, k := range keys {
env.Secrets = append(env.Secrets, db.EnvironmentSecret{
ID: k.ID,
Name: k.Name,
})
}
context.Set(r, "environment", env)
next.ServeHTTP(w, r)
})
@ -99,6 +162,11 @@ func UpdateEnvironment(w http.ResponseWriter, r *http.Request) {
Description: fmt.Sprintf("Environment %s updated", env.Name),
})
if err := updateEnvironmentSecrets(helpers.Store(r), env); err != nil {
helpers.WriteError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
@ -131,6 +199,11 @@ func AddEnvironment(w http.ResponseWriter, r *http.Request) {
Description: fmt.Sprintf("Environment %s created", newEnv.Name),
})
if err = updateEnvironmentSecrets(helpers.Store(r), newEnv); err != nil {
//helpers.WriteError(w, err)
//return
}
w.WriteHeader(http.StatusNoContent)
}
@ -162,92 +235,3 @@ func RemoveEnvironment(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
type environmentSecretOperation string
const (
environmentSecretCreate environmentSecretOperation = "create"
environmentSecretUpdate environmentSecretOperation = "update"
environmentSecretDelete environmentSecretOperation = "delete"
)
type environmentSecret struct {
ID int `json:"id"`
Name string `json:"name"`
Secret string `json:"secret"`
Operation environmentSecretOperation `json:"operation"`
}
func GetEnvironmentSecrets(w http.ResponseWriter, r *http.Request) {
env := context.Get(r, "environment").(db.Environment)
store := helpers.Store(r)
keys, err := store.GetEnvironmentSecrets(env.ProjectID, env.ID)
if err != nil {
helpers.WriteError(w, err)
return
}
helpers.WriteJSON(w, http.StatusOK, keys)
}
func UpdateEnvironmentSecrets(w http.ResponseWriter, r *http.Request) {
env := context.Get(r, "environment").(db.Environment)
var secrets []environmentSecret
if !helpers.Bind(w, r, &secrets) {
return
}
store := helpers.Store(r)
for _, secret := range secrets {
var err error
var key db.AccessKey
switch secret.Operation {
case environmentSecretCreate:
key, err = store.CreateAccessKey(db.AccessKey{
Name: secret.Name,
String: secret.Secret,
EnvironmentID: &env.ID,
ProjectID: &env.ProjectID,
Type: db.AccessKeyString,
})
case environmentSecretDelete:
key, err = store.GetAccessKey(env.ProjectID, secret.ID)
if err != nil {
continue
}
if key.EnvironmentID == nil && *key.EnvironmentID == env.ID {
continue
}
err = store.DeleteAccessKey(env.ProjectID, secret.ID)
case environmentSecretUpdate:
key, err = store.GetAccessKey(env.ProjectID, secret.ID)
if err != nil {
continue
}
if key.EnvironmentID == nil && *key.EnvironmentID == env.ID {
continue
}
err = store.UpdateAccessKey(db.AccessKey{
Name: secret.Name,
String: secret.Secret,
Type: db.AccessKeyString,
})
}
}
w.WriteHeader(http.StatusNoContent)
}

View File

@ -249,9 +249,6 @@ func Route() *mux.Router {
projectEnvManagement.HandleFunc("/{environment_id}", projects.UpdateEnvironment).Methods("PUT")
projectEnvManagement.HandleFunc("/{environment_id}", projects.RemoveEnvironment).Methods("DELETE")
projectEnvManagement.HandleFunc("/{environment_id}/secrets", projects.GetEnvironmentSecrets).Methods("GET", "HEAD")
projectEnvManagement.HandleFunc("/{environment_id}/secrets", projects.UpdateEnvironmentSecrets).Methods("POST")
projectTmplManagement := projectUserAPI.PathPrefix("/templates").Subrouter()
projectTmplManagement.Use(projects.TemplatesMiddleware)

View File

@ -171,6 +171,8 @@ func (key *AccessKey) SerializeSecret() error {
var err error
switch key.Type {
case AccessKeyString:
plaintext = []byte(key.String)
case AccessKeySSH:
plaintext, err = json.Marshal(key.SshKey)
if err != nil {
@ -225,6 +227,8 @@ func (key *AccessKey) SerializeSecret() error {
func (key *AccessKey) unmarshalAppropriateField(secret []byte) (err error) {
switch key.Type {
case AccessKeyString:
key.String = string(secret)
case AccessKeySSH:
sshKey := SshKey{}
err = json.Unmarshal(secret, &sshKey)

View File

@ -4,14 +4,30 @@ import (
"encoding/json"
)
type EnvironmentSecretOperation string
const (
EnvironmentSecretCreate EnvironmentSecretOperation = "create"
EnvironmentSecretUpdate EnvironmentSecretOperation = "update"
EnvironmentSecretDelete EnvironmentSecretOperation = "delete"
)
type EnvironmentSecret struct {
ID int `json:"id"`
Name string `json:"name"`
Secret string `json:"secret"`
Operation EnvironmentSecretOperation `json:"operation"`
}
// Environment is used to pass additional arguments, in json form to ansible
type Environment struct {
ID int `db:"id" json:"id"`
Name string `db:"name" json:"name" binding:"required"`
ProjectID int `db:"project_id" json:"project_id"`
Password *string `db:"password" json:"password"`
JSON string `db:"json" json:"json" binding:"required"`
ENV *string `db:"env" json:"env" binding:"required"`
ID int `db:"id" json:"id"`
Name string `db:"name" json:"name" binding:"required"`
ProjectID int `db:"project_id" json:"project_id"`
Password *string `db:"password" json:"password"`
JSON string `db:"json" json:"json" binding:"required"`
ENV *string `db:"env" json:"env" binding:"required"`
Secrets []EnvironmentSecret `db:"-" json:"secrets"`
}
func (env *Environment) Validate() error {

View File

@ -69,6 +69,7 @@ func GetMigrations() []Migration {
{Version: "2.9.97"},
{Version: "2.9.100"},
{Version: "2.10.12"},
{Version: "2.10.15"},
}
}

View File

@ -17,8 +17,8 @@ func (d *SqlDb) GetAccessKeyRefs(projectID int, keyID int) (db.ObjectReferrers,
func (d *SqlDb) GetAccessKeys(projectID int, params db.RetrieveQueryParams) (keys []db.AccessKey, err error) {
keys = make([]db.AccessKey, 0)
q := d.makeObjectsQuery(projectID, db.AccessKeyProps, params).Where("pe.environment_is IS NULL")
q := d.makeObjectsQuery(projectID, db.AccessKeyProps, params).Where("pe.environment_id IS NULL")
query, args, err := q.ToSql()
@ -75,11 +75,12 @@ func (d *SqlDb) CreateAccessKey(key db.AccessKey) (newKey db.AccessKey, err erro
insertID, err := d.insert(
"id",
"insert into access_key (name, type, project_id, secret) values (?, ?, ?, ?)",
"insert into access_key (name, type, project_id, secret, environment_id) values (?, ?, ?, ?, ?)",
key.Name,
key.Type,
key.ProjectID,
key.Secret)
key.Secret,
key.EnvironmentID)
if err != nil {
return

View File

@ -68,7 +68,7 @@ func (d *SqlDb) DeleteEnvironment(projectID int, environmentID int) error {
func (d *SqlDb) GetEnvironmentSecrets(projectID int, environmentID int) (keys []db.AccessKey, err error) {
keys = make([]db.AccessKey, 0)
q := d.makeObjectsQuery(projectID, db.AccessKeyProps, db.RetrieveQueryParams{}).Where("pe.environment_is = ?", environmentID)
q := d.makeObjectsQuery(projectID, db.AccessKeyProps, db.RetrieveQueryParams{}).Where("pe.environment_id = ?", environmentID)
query, args, err := q.ToSql()

View File

@ -55,7 +55,6 @@
/>
<div>
<v-subheader class="px-0 mt-4">
{{ $t('environmentVariables') }}
@ -71,7 +70,6 @@
<span>Variables passed as process environment variables.</span>
</v-tooltip>
</v-subheader>
<v-data-table
:items="env"
:items-per-page="-1"
@ -111,7 +109,6 @@
</tr>
</template>
</v-data-table>
<div class="text-right mt-2 mb-4">
<v-btn
color="primary"
@ -120,6 +117,72 @@
</div>
</div>
<div>
<v-subheader class="px-0 mt-4">
{{ $t('Secrets') }}
<v-tooltip bottom color="black" open-delay="300">
<template v-slot:activator="{ on, attrs }">
<v-icon
class="ml-1"
v-bind="attrs"
v-on="on"
color="lightgray"
>mdi-help-circle</v-icon>
</template>
<span>Secrets.</span>
</v-tooltip>
</v-subheader>
<v-data-table
:items="secrets.filter(s => !s.removed)"
:items-per-page="-1"
class="elevation-1"
hide-default-footer
no-data-text="No values"
>
<template v-slot:item="props">
<tr>
<td class="pa-1">
<v-text-field
solo-inverted
flat
hide-details
v-model="props.item.name"
class="v-text-field--solo--no-min-height"
></v-text-field>
</td>
<td class="pa-1">
<v-text-field
solo-inverted
flat
hide-details
v-model="props.item.value"
placeholder="*******"
class="v-text-field--solo--no-min-height"
></v-text-field>
</td>
<td style="width: 38px;">
<v-icon
small
class="pa-1"
@click="removeSecret(props.item)"
>
mdi-delete
</v-icon>
</td>
</tr>
</template>
</v-data-table>
<div class="text-right mt-2 mb-4">
<v-btn
color="primary"
@click="addSecret()"
>New Secret</v-btn>
</div>
</div>
</v-form>
</template>
@ -157,8 +220,10 @@ export default {
'dind-runner:latest',
],
advancedOptions: false,
json: '{}',
env: [],
secrets: [],
cmOptions: {
tabSize: 2,
@ -186,6 +251,21 @@ export default {
}
},
addSecret(name = '', value = '') {
this.secrets.push({ name, value, new: true });
},
removeSecret(val) {
const i = this.secrets.findIndex((v) => v.id === val.id);
if (i > -1) {
if (this.secrets[i].new) {
this.secrets.splice(i, 1);
} else {
this.secrets[i].remove = true;
}
}
},
setExtraVar(name, value) {
try {
const obj = JSON.parse(this.json || '{}');
@ -212,7 +292,25 @@ export default {
env[predefinedVar.name] = predefinedVar.value;
});
const secrets = (this.secrets || []).map((s) => {
let operation;
if (s.new) {
operation = 'create';
} else if (s.remove) {
operation = 'remove';
} else if (s.value !== '') {
operation = 'update';
}
return {
id: s.id,
name: s.name,
secret: s.value,
operation,
};
}).filter((s) => s.operation != null);
this.item.env = JSON.stringify(env);
this.item.secrets = secrets;
},
afterLoadData() {
@ -220,6 +318,8 @@ export default {
const env = JSON.parse(this.item?.env || '{}');
const secrets = this.item?.secrets || [];
this.env = Object.keys(env)
.filter((x) => {
const index = PREDEFINED_ENV_VARS.findIndex((v) => v.name === x);
@ -230,6 +330,12 @@ export default {
value: env[x],
}));
this.secrets = secrets.map((x) => ({
id: x.id,
name: x.name,
value: '',
}));
Object.keys(env).forEach((x) => {
const index = PREDEFINED_ENV_VARS.findIndex((v) => v.name === x);
if (index !== -1 && PREDEFINED_ENV_VARS[index].value === env[x]) {