Issue UI: redact private data like (user, locations, ...) (#25039)
This commit is contained in:
parent
f8dcd4dd78
commit
a65a1ce2d7
23 changed files with 277 additions and 109 deletions
|
|
@ -475,7 +475,8 @@ export default defineComponent({
|
||||||
|
|
||||||
for (const endpoint of endpoints) {
|
for (const endpoint of endpoints) {
|
||||||
try {
|
try {
|
||||||
const response = await api.get(endpoint);
|
// Add private=false for device endpoints to hide private data in bug reports
|
||||||
|
const response = await api.get(endpoint, { params: { private: false } });
|
||||||
if (response.data && Object.keys(response.data).length > 0) {
|
if (response.data && Object.keys(response.data).length > 0) {
|
||||||
const key = endpoint.replace("config/", "").replace("devices/", "");
|
const key = endpoint.replace("config/", "").replace("devices/", "");
|
||||||
let data = response.data;
|
let data = response.data;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/evcc-io/evcc/util/config"
|
"github.com/evcc-io/evcc/util/config"
|
||||||
|
"github.com/evcc-io/evcc/util/redact"
|
||||||
"github.com/evcc-io/evcc/util/templates"
|
"github.com/evcc-io/evcc/util/templates"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -54,7 +55,7 @@ func runConfig(cmd *cobra.Command, args []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range configurable {
|
for _, c := range configurable {
|
||||||
fmt.Println(config.NameForID(c.ID), fmt.Sprintf("%+v", c.Properties), redactMap(c.Data))
|
fmt.Println(config.NameForID(c.ID), fmt.Sprintf("%+v", c.Properties), redact.Map(c.Data))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/cli/browser"
|
"github.com/cli/browser"
|
||||||
"github.com/evcc-io/evcc/util"
|
"github.com/evcc-io/evcc/util"
|
||||||
|
"github.com/evcc-io/evcc/util/redact"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -44,7 +45,7 @@ func runDiscuss(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
var redacted string
|
var redacted string
|
||||||
if src, err := os.ReadFile(cfgFile); err == nil {
|
if src, err := os.ReadFile(cfgFile); err == nil {
|
||||||
redacted = redact(string(src))
|
redacted = redact.String(string(src))
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl := template.Must(template.New("discuss").Parse(discussTmpl))
|
tmpl := template.Must(template.New("discuss").Parse(discussTmpl))
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/evcc-io/evcc/core"
|
"github.com/evcc-io/evcc/core"
|
||||||
"github.com/evcc-io/evcc/util"
|
"github.com/evcc-io/evcc/util"
|
||||||
"github.com/evcc-io/evcc/util/config"
|
"github.com/evcc-io/evcc/util/config"
|
||||||
|
"github.com/evcc-io/evcc/util/redact"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -67,7 +68,7 @@ func runDump(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
var redacted string
|
var redacted string
|
||||||
if src, err := os.ReadFile(cfgFile); err == nil {
|
if src, err := os.ReadFile(cfgFile); err == nil {
|
||||||
redacted = redact(string(src))
|
redacted = redact.String(string(src))
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl := template.Must(
|
tmpl := template.Must(
|
||||||
|
|
|
||||||
|
|
@ -49,14 +49,6 @@ func unwrap(err error) (res []string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func redact(src string) string {
|
|
||||||
return util.RedactConfigString(src)
|
|
||||||
}
|
|
||||||
|
|
||||||
func redactMap(src map[string]any) map[string]any {
|
|
||||||
return util.RedactConfigMap(src)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fatal logs a fatal error and runs shutdown functions before terminating
|
// fatal logs a fatal error and runs shutdown functions before terminating
|
||||||
func fatal(err error) {
|
func fatal(err error) {
|
||||||
log.FATAL.Println(err)
|
log.FATAL.Println(err)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/util/redact"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUnwrap(t *testing.T) {
|
func TestUnwrap(t *testing.T) {
|
||||||
|
|
@ -23,17 +25,17 @@ func TestRedact(t *testing.T) {
|
||||||
sponsortoken: geheim
|
sponsortoken: geheim
|
||||||
user: geheim
|
user: geheim
|
||||||
password: geheim
|
password: geheim
|
||||||
secret: geheim
|
clientsecret: geheim
|
||||||
token:
|
token:
|
||||||
access: geheim
|
accesstoken: geheim
|
||||||
refresh: geheim
|
refreshtoken: geheim
|
||||||
pin: geheim
|
pin: geheim
|
||||||
mac: geheim
|
mac: geheim
|
||||||
secret: geheim # comment
|
clientsecret: geheim # comment
|
||||||
secret : geheim
|
clientsecret : geheim
|
||||||
`
|
`
|
||||||
|
|
||||||
if res := redact(secret); strings.Contains(res, "geheim") || !strings.Contains(res, "public") {
|
if res := redact.String(secret); strings.Contains(res, "geheim") || !strings.Contains(res, "public") {
|
||||||
t.Errorf("secret exposed: %v", res)
|
t.Errorf("secret exposed: %v", res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export default defineConfig({
|
||||||
video: "on-first-retry",
|
video: "on-first-retry",
|
||||||
screenshot: "only-on-failure",
|
screenshot: "only-on-failure",
|
||||||
permissions: ["clipboard-write"],
|
permissions: ["clipboard-write"],
|
||||||
|
actionTimeout: 20000, // 20s for individual actions
|
||||||
},
|
},
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,11 @@ import (
|
||||||
"go.yaml.in/yaml/v4"
|
"go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func devicesConfig[T any](class templates.Class, h config.Handler[T]) ([]map[string]any, error) {
|
func devicesConfig[T any](class templates.Class, h config.Handler[T], hidePrivate bool) ([]map[string]any, error) {
|
||||||
var res []map[string]any
|
var res []map[string]any
|
||||||
|
|
||||||
for _, dev := range h.Devices() {
|
for _, dev := range h.Devices() {
|
||||||
dc, err := deviceConfigMap(class, dev)
|
dc, err := deviceConfigMap(class, dev, hidePrivate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -54,20 +54,23 @@ func devicesConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if private data should be hidden (default: true, showing private data)
|
||||||
|
hidePrivate := r.URL.Query().Get("private") == "false"
|
||||||
|
|
||||||
var res []map[string]any
|
var res []map[string]any
|
||||||
|
|
||||||
switch class {
|
switch class {
|
||||||
case templates.Meter:
|
case templates.Meter:
|
||||||
res, err = devicesConfig(class, config.Meters())
|
res, err = devicesConfig(class, config.Meters(), hidePrivate)
|
||||||
|
|
||||||
case templates.Charger:
|
case templates.Charger:
|
||||||
res, err = devicesConfig(class, config.Chargers())
|
res, err = devicesConfig(class, config.Chargers(), hidePrivate)
|
||||||
|
|
||||||
case templates.Vehicle:
|
case templates.Vehicle:
|
||||||
res, err = devicesConfig(class, config.Vehicles())
|
res, err = devicesConfig(class, config.Vehicles(), hidePrivate)
|
||||||
|
|
||||||
case templates.Circuit:
|
case templates.Circuit:
|
||||||
res, err = devicesConfig(class, config.Circuits())
|
res, err = devicesConfig(class, config.Circuits(), hidePrivate)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -78,7 +81,7 @@ func devicesConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
jsonWrite(w, res)
|
jsonWrite(w, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deviceConfigMap[T any](class templates.Class, dev config.Device[T]) (map[string]any, error) {
|
func deviceConfigMap[T any](class templates.Class, dev config.Device[T], hidePrivate bool) (map[string]any, error) {
|
||||||
conf := dev.Config()
|
conf := dev.Config()
|
||||||
|
|
||||||
dc := map[string]any{
|
dc := map[string]any{
|
||||||
|
|
@ -102,7 +105,7 @@ func deviceConfigMap[T any](class templates.Class, dev config.Device[T]) (map[st
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.Type == typeTemplate {
|
if conf.Type == typeTemplate {
|
||||||
params, err := sanitizeMasked(class, conf.Other)
|
params, err := sanitizeMasked(class, conf.Other, hidePrivate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -146,13 +149,13 @@ func deviceConfigMap[T any](class templates.Class, dev config.Device[T]) (map[st
|
||||||
return dc, nil
|
return dc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deviceConfig[T any](class templates.Class, id int, h config.Handler[T]) (map[string]any, error) {
|
func deviceConfig[T any](class templates.Class, id int, h config.Handler[T], hidePrivate bool) (map[string]any, error) {
|
||||||
dev, err := h.ByName(config.NameForID(id))
|
dev, err := h.ByName(config.NameForID(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return deviceConfigMap(class, dev)
|
return deviceConfigMap(class, dev, hidePrivate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// deviceConfigHandler returns a device configuration by class
|
// deviceConfigHandler returns a device configuration by class
|
||||||
|
|
@ -171,20 +174,23 @@ func deviceConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if private data should be hidden (default: true, showing private data)
|
||||||
|
hidePrivate := r.URL.Query().Get("private") == "false"
|
||||||
|
|
||||||
var res map[string]any
|
var res map[string]any
|
||||||
|
|
||||||
switch class {
|
switch class {
|
||||||
case templates.Meter:
|
case templates.Meter:
|
||||||
res, err = deviceConfig(class, id, config.Meters())
|
res, err = deviceConfig(class, id, config.Meters(), hidePrivate)
|
||||||
|
|
||||||
case templates.Charger:
|
case templates.Charger:
|
||||||
res, err = deviceConfig(class, id, config.Chargers())
|
res, err = deviceConfig(class, id, config.Chargers(), hidePrivate)
|
||||||
|
|
||||||
case templates.Vehicle:
|
case templates.Vehicle:
|
||||||
res, err = deviceConfig(class, id, config.Vehicles())
|
res, err = deviceConfig(class, id, config.Vehicles(), hidePrivate)
|
||||||
|
|
||||||
case templates.Circuit:
|
case templates.Circuit:
|
||||||
res, err = deviceConfig(class, id, config.Circuits())
|
res, err = deviceConfig(class, id, config.Circuits(), hidePrivate)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ func filterValidTemplateParams(tmpl *templates.Template, conf map[string]any) ma
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func sanitizeMasked(class templates.Class, conf map[string]any) (map[string]any, error) {
|
func sanitizeMasked(class templates.Class, conf map[string]any, hidePrivate bool) (map[string]any, error) {
|
||||||
tmpl, err := templateForConfig(class, conf)
|
tmpl, err := templateForConfig(class, conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -142,8 +142,12 @@ func sanitizeMasked(class templates.Class, conf map[string]any) (map[string]any,
|
||||||
res := make(map[string]any, len(conf))
|
res := make(map[string]any, len(conf))
|
||||||
|
|
||||||
for k, v := range conf {
|
for k, v := range conf {
|
||||||
if i, p := tmpl.ParamByName(k); i >= 0 && p.IsMasked() {
|
if i, p := tmpl.ParamByName(k); i >= 0 {
|
||||||
v = masked
|
if p.IsMasked() {
|
||||||
|
v = masked
|
||||||
|
} else if hidePrivate && p.IsPrivate() {
|
||||||
|
v = masked
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res[k] = v
|
res[k] = v
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/evcc-io/evcc/util"
|
"github.com/evcc-io/evcc/util/redact"
|
||||||
)
|
)
|
||||||
|
|
||||||
// configYamlHandler returns the redacted evcc.yaml configuration file
|
// configYamlHandler returns the redacted evcc.yaml configuration file
|
||||||
|
|
@ -25,7 +25,7 @@ func configYamlHandler(configFilePath string) http.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redact sensitive information
|
// Redact sensitive information
|
||||||
redacted := util.RedactConfigString(string(src))
|
redacted := redact.String(string(src))
|
||||||
|
|
||||||
// Return the redacted content as plain text
|
// Return the redacted content as plain text
|
||||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
"github.com/evcc-io/evcc/server/db/settings"
|
"github.com/evcc-io/evcc/server/db/settings"
|
||||||
"github.com/evcc-io/evcc/util"
|
"github.com/evcc-io/evcc/util"
|
||||||
|
"github.com/evcc-io/evcc/util/redact"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"go.yaml.in/yaml/v4"
|
"go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
@ -18,6 +19,12 @@ import (
|
||||||
func settingsGetStringHandler(key string) http.HandlerFunc {
|
func settingsGetStringHandler(key string) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
res, _ := settings.String(key)
|
res, _ := settings.String(key)
|
||||||
|
|
||||||
|
// Check if private data should be hidden
|
||||||
|
if r.URL.Query().Get("private") == "false" && res != "" {
|
||||||
|
res = redact.String(res)
|
||||||
|
}
|
||||||
|
|
||||||
jsonWrite(w, res)
|
jsonWrite(w, res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ params:
|
||||||
required: true
|
required: true
|
||||||
- name: pin
|
- name: pin
|
||||||
required: true
|
required: true
|
||||||
mask: true
|
|
||||||
render: |
|
render: |
|
||||||
type: nrgkick-bluetooth
|
type: nrgkick-bluetooth
|
||||||
mac: {{ .mac }}
|
mac: {{ .mac }}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ params:
|
||||||
- name: usage
|
- name: usage
|
||||||
choice: ["grid"]
|
choice: ["grid"]
|
||||||
- name: token
|
- name: token
|
||||||
mask: true
|
|
||||||
required: true
|
required: true
|
||||||
example: 5K4MVS-OjfWhK_4yrjOlFe1F6kJXPVf7eQYggo8ebAE
|
example: 5K4MVS-OjfWhK_4yrjOlFe1F6kJXPVf7eQYggo8ebAE
|
||||||
- name: homeid
|
- name: homeid
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ params:
|
||||||
- name: vin
|
- name: vin
|
||||||
example: ZFAE...
|
example: ZFAE...
|
||||||
- name: pin
|
- name: pin
|
||||||
mask: true
|
|
||||||
help:
|
help:
|
||||||
en: Required for evcc to wake up the vehicle for charging and to refresh the SoC while charging. When connected to TWC3, used to start/stop charging.
|
en: Required for evcc to wake up the vehicle for charging and to refresh the SoC while charging. When connected to TWC3, used to start/stop charging.
|
||||||
de: Benötigt um das Fahrzeug zum Laden aufzuwecken and zur Aktualisierung des Ladestands während der Ladung. Bei Nutzung mit TWC3 kann der Ladevorgang mittels PIN gestartet und gestoppt werden.
|
de: Benötigt um das Fahrzeug zum Laden aufzuwecken and zur Aktualisierung des Ladestands während der Ladung. Bei Nutzung mit TWC3 kann der Ladevorgang mittels PIN gestartet und gestoppt werden.
|
||||||
|
|
|
||||||
|
|
@ -6,24 +6,21 @@ products:
|
||||||
en: Auth Demo Meter
|
en: Auth Demo Meter
|
||||||
auth:
|
auth:
|
||||||
type: demo
|
type: demo
|
||||||
params: ["region", "token"]
|
params: ["region", "server"]
|
||||||
params:
|
params:
|
||||||
- name: usage
|
- name: usage
|
||||||
choice: ["grid"]
|
choice: ["grid"]
|
||||||
- name: region
|
- name: region
|
||||||
description:
|
description:
|
||||||
de: Server
|
generic: Region
|
||||||
en: Server
|
|
||||||
type: choice
|
type: choice
|
||||||
choice: ["EU", "US", "CN"]
|
choice: ["EU", "US", "CN"]
|
||||||
- name: token
|
- name: server
|
||||||
description:
|
description:
|
||||||
de: Token
|
generic: Server
|
||||||
en: Token
|
|
||||||
- name: power
|
- name: power
|
||||||
description:
|
description:
|
||||||
de: Leistung
|
generic: Power
|
||||||
en: Power
|
|
||||||
unit: W
|
unit: W
|
||||||
type: int
|
type: int
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,13 +35,13 @@ test.describe("config device auth", async () => {
|
||||||
await meterModal.getByLabel("Manufacturer").selectOption("Auth Demo Meter");
|
await meterModal.getByLabel("Manufacturer").selectOption("Auth Demo Meter");
|
||||||
|
|
||||||
// step 1: auth view
|
// step 1: auth view
|
||||||
|
await expect(meterModal.getByLabel("Region")).toBeVisible();
|
||||||
await expect(meterModal.getByLabel("Server")).toBeVisible();
|
await expect(meterModal.getByLabel("Server")).toBeVisible();
|
||||||
await expect(meterModal.getByLabel("Token")).toBeVisible();
|
|
||||||
await expect(meterModal.getByLabel("Power")).not.toBeVisible();
|
await expect(meterModal.getByLabel("Power")).not.toBeVisible();
|
||||||
await expect(meterModal.getByRole("button", { name: "Validate & save" })).not.toBeVisible();
|
await expect(meterModal.getByRole("button", { name: "Validate & save" })).not.toBeVisible();
|
||||||
await expect(meterModal.getByRole("button", { name: "Save" })).not.toBeVisible();
|
await expect(meterModal.getByRole("button", { name: "Save" })).not.toBeVisible();
|
||||||
await meterModal.getByLabel("Server").selectOption("EU");
|
await meterModal.getByLabel("Region").selectOption("EU");
|
||||||
await meterModal.getByLabel("Token").fill("test-token-123");
|
await meterModal.getByLabel("Server").fill("my.server.org");
|
||||||
await meterModal.getByRole("button", { name: "Prepare connection" }).click();
|
await meterModal.getByRole("button", { name: "Prepare connection" }).click();
|
||||||
await expect(meterModal.getByRole("link", { name: "Connect with localhost" })).toBeVisible();
|
await expect(meterModal.getByRole("link", { name: "Connect with localhost" })).toBeVisible();
|
||||||
|
|
||||||
|
|
@ -51,8 +51,8 @@ test.describe("config device auth", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// step 2: show regular device form
|
// step 2: show regular device form
|
||||||
await expect(meterModal.getByLabel("Server")).toHaveValue("EU");
|
await expect(meterModal.getByLabel("Region")).toHaveValue("EU");
|
||||||
await expect(meterModal.getByLabel("Token")).toHaveValue("test-token-123");
|
await expect(meterModal.getByLabel("Server")).toHaveValue("my.server.org");
|
||||||
await expect(meterModal.getByLabel("Power")).toBeVisible();
|
await expect(meterModal.getByLabel("Power")).toBeVisible();
|
||||||
await meterModal.getByLabel("Power").fill("5000");
|
await meterModal.getByLabel("Power").fill("5000");
|
||||||
await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible();
|
await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible();
|
||||||
|
|
@ -69,8 +69,8 @@ test.describe("config device auth", async () => {
|
||||||
// re-open meter for editing
|
// re-open meter for editing
|
||||||
await page.getByTestId("grid").getByRole("button", { name: "edit" }).click();
|
await page.getByTestId("grid").getByRole("button", { name: "edit" }).click();
|
||||||
await expectModalVisible(meterModal);
|
await expectModalVisible(meterModal);
|
||||||
await expect(meterModal.getByLabel("Server")).toHaveValue("EU");
|
await expect(meterModal.getByLabel("Region")).toHaveValue("EU");
|
||||||
await expect(meterModal.getByLabel("Token")).toHaveValue("test-token-123");
|
await expect(meterModal.getByLabel("Server")).toHaveValue("my.server.org");
|
||||||
await expect(meterModal.getByLabel("Power")).toHaveValue("5000");
|
await expect(meterModal.getByLabel("Power")).toHaveValue("5000");
|
||||||
await expect(meterModal.getByRole("button", { name: "Prepare connection" })).not.toBeVisible();
|
await expect(meterModal.getByRole("button", { name: "Prepare connection" })).not.toBeVisible();
|
||||||
await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible();
|
await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible();
|
||||||
|
|
@ -84,8 +84,8 @@ test.describe("config device auth", async () => {
|
||||||
// re-open meter for editing after restart, auth status as to be reestablished
|
// re-open meter for editing after restart, auth status as to be reestablished
|
||||||
await page.getByTestId("grid").getByRole("button", { name: "edit" }).click();
|
await page.getByTestId("grid").getByRole("button", { name: "edit" }).click();
|
||||||
await expectModalVisible(meterModal);
|
await expectModalVisible(meterModal);
|
||||||
await expect(meterModal.getByLabel("Server")).toHaveValue("EU");
|
await expect(meterModal.getByLabel("Region")).toHaveValue("EU");
|
||||||
await expect(meterModal.getByLabel("Token")).toHaveValue("test-token-123");
|
await expect(meterModal.getByLabel("Server")).toHaveValue("my.server.org");
|
||||||
await expect(meterModal.getByLabel("Power")).not.toBeVisible();
|
await expect(meterModal.getByLabel("Power")).not.toBeVisible();
|
||||||
// note: prepare connection step is auto-executed, since all required fields (server, token) are already present
|
// note: prepare connection step is auto-executed, since all required fields (server, token) are already present
|
||||||
await expect(meterModal.getByRole("link", { name: "Connect with localhost" })).toBeVisible();
|
await expect(meterModal.getByRole("link", { name: "Connect with localhost" })).toBeVisible();
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,18 @@
|
||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
import { start, stop, restart, baseUrl } from "./evcc";
|
import { start, stop, restart, baseUrl } from "./evcc";
|
||||||
|
import { startSimulator, stopSimulator, simulatorHost } from "./simulator";
|
||||||
import { enableExperimental, expectModalVisible, expectModalHidden } from "./utils";
|
import { enableExperimental, expectModalVisible, expectModalHidden } from "./utils";
|
||||||
|
|
||||||
test.use({ baseURL: baseUrl() });
|
test.use({ baseURL: baseUrl() });
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
await startSimulator();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await stopSimulator();
|
||||||
|
});
|
||||||
|
|
||||||
test.afterEach(async () => {
|
test.afterEach(async () => {
|
||||||
await stop();
|
await stop();
|
||||||
});
|
});
|
||||||
|
|
@ -47,16 +56,18 @@ test.describe("issue creation", () => {
|
||||||
// Enable experimental features
|
// Enable experimental features
|
||||||
await enableExperimental(page, false);
|
await enableExperimental(page, false);
|
||||||
|
|
||||||
// Create a battery meter
|
// Create a Shelly meter with username (to test private data redaction)
|
||||||
await page.getByRole("button", { name: "Add solar or battery" }).click();
|
await page.getByRole("button", { name: "Add grid meter" }).click();
|
||||||
await page.getByRole("button", { name: "Add battery meter" }).click();
|
|
||||||
const meterModal = page.getByTestId("meter-modal");
|
const meterModal = page.getByTestId("meter-modal");
|
||||||
await expectModalVisible(meterModal);
|
await expectModalVisible(meterModal);
|
||||||
await meterModal.getByLabel("Title").fill("BigBlueBattery");
|
await meterModal.getByLabel("Manufacturer").selectOption("Shelly 1PM");
|
||||||
await meterModal.getByLabel("Manufacturer").selectOption("Demo battery");
|
await meterModal.getByLabel("IP address or hostname").fill(simulatorHost());
|
||||||
|
await meterModal.getByLabel("Username").fill("testuser@example.com");
|
||||||
|
await meterModal.getByLabel("Password").fill("secretpass");
|
||||||
|
|
||||||
await meterModal.getByRole("button", { name: "Validate & save" }).click();
|
await meterModal.getByRole("button", { name: "Validate & save" }).click();
|
||||||
await expectModalHidden(meterModal);
|
await expectModalHidden(meterModal);
|
||||||
await expect(page.getByTestId("battery")).toBeVisible();
|
await expect(page.getByTestId("grid")).toBeVisible();
|
||||||
|
|
||||||
// Restart to apply changes
|
// Restart to apply changes
|
||||||
await restart(CONFIG);
|
await restart(CONFIG);
|
||||||
|
|
@ -77,7 +88,7 @@ test.describe("issue creation", () => {
|
||||||
.fill("This is a test issue created from the config page workflow");
|
.fill("This is a test issue created from the config page workflow");
|
||||||
await page
|
await page
|
||||||
.getByLabel("Steps to reproduce")
|
.getByLabel("Steps to reproduce")
|
||||||
.fill("1. Go to config\n2. Enable experimental\n3. Add battery\n4. Report issue");
|
.fill("1. Go to config\n2. Enable experimental\n3. Add meter\n4. Report issue");
|
||||||
|
|
||||||
// check yaml data
|
// check yaml data
|
||||||
const yamlItem = page.getByTestId("issueYamlConfig-additional-item");
|
const yamlItem = page.getByTestId("issueYamlConfig-additional-item");
|
||||||
|
|
@ -88,12 +99,19 @@ test.describe("issue creation", () => {
|
||||||
await yamlModal.getByRole("button", { name: "Close" }).first().click();
|
await yamlModal.getByRole("button", { name: "Close" }).first().click();
|
||||||
await expectModalHidden(yamlModal);
|
await expectModalHidden(yamlModal);
|
||||||
|
|
||||||
// check ui data
|
// check ui data and verify private data redaction
|
||||||
const uiItem = page.getByTestId("issueUiConfig-additional-item");
|
const uiItem = page.getByTestId("issueUiConfig-additional-item");
|
||||||
await uiItem.getByRole("button", { name: "show details" }).click();
|
await uiItem.getByRole("button", { name: "show details" }).click();
|
||||||
const uiModal = page.getByTestId("issueUiConfig-modal");
|
const uiModal = page.getByTestId("issueUiConfig-modal");
|
||||||
await expectModalVisible(uiModal);
|
await expectModalVisible(uiModal);
|
||||||
await expect(uiModal.getByRole("textbox")).toHaveValue(/BigBlueBattery/);
|
const uiContent = await uiModal.getByRole("textbox").inputValue();
|
||||||
|
|
||||||
|
// Verify meter is present but private data is redacted
|
||||||
|
expect(uiContent).toContain("shelly"); // meter type should be visible
|
||||||
|
expect(uiContent).not.toContain("testuser@example.com"); // user should be redacted
|
||||||
|
expect(uiContent).not.toContain("secretpass"); // password should be redacted
|
||||||
|
expect(uiContent).toContain("***"); // redaction marker should be present
|
||||||
|
|
||||||
await uiModal.getByRole("button", { name: "Close" }).first().click();
|
await uiModal.getByRole("button", { name: "Close" }).first().click();
|
||||||
await expectModalHidden(uiModal);
|
await expectModalHidden(uiModal);
|
||||||
|
|
||||||
|
|
@ -133,7 +151,7 @@ test.describe("issue creation", () => {
|
||||||
await expect(textarea).toBeVisible();
|
await expect(textarea).toBeVisible();
|
||||||
const textareaContent = await textarea.inputValue();
|
const textareaContent = await textarea.inputValue();
|
||||||
expect(textareaContent).toContain("carport_pv"); // from evcc.yaml
|
expect(textareaContent).toContain("carport_pv"); // from evcc.yaml
|
||||||
expect(textareaContent).toContain("BigBlueBattery"); // from ui config
|
expect(textareaContent).toContain("shelly"); // from ui config
|
||||||
expect(textareaContent).toContain("DEBUG"); // from logs
|
expect(textareaContent).toContain("DEBUG"); // from logs
|
||||||
expect(textareaContent).toContain('"telemetry":'); // from state
|
expect(textareaContent).toContain('"telemetry":'); // from state
|
||||||
|
|
||||||
|
|
@ -142,7 +160,7 @@ test.describe("issue creation", () => {
|
||||||
.getByRole("link", { name: "Create GitHub Issue" })
|
.getByRole("link", { name: "Create GitHub Issue" })
|
||||||
.getAttribute("href");
|
.getAttribute("href");
|
||||||
expect(href).toContain("https://github.com/evcc-io/evcc/issues/new?title=Kaboom&body=");
|
expect(href).toContain("https://github.com/evcc-io/evcc/issues/new?title=Kaboom&body=");
|
||||||
expect(href).not.toContain("BigBlueBattery"); // from ui config
|
expect(href).not.toContain("TestShelly"); // from ui config
|
||||||
expect(href).not.toContain("carport_pv"); // from evcc.yaml
|
expect(href).not.toContain("carport_pv"); // from evcc.yaml
|
||||||
|
|
||||||
// close modal
|
// close modal
|
||||||
|
|
@ -167,7 +185,7 @@ test.describe("issue creation", () => {
|
||||||
.getByRole("link", { name: "Create GitHub Issue" })
|
.getByRole("link", { name: "Create GitHub Issue" })
|
||||||
.getAttribute("href");
|
.getAttribute("href");
|
||||||
expect(href).toContain("https://github.com/evcc-io/evcc/issues/new?title=Kaboom&body=");
|
expect(href).toContain("https://github.com/evcc-io/evcc/issues/new?title=Kaboom&body=");
|
||||||
expect(href).toContain("BigBlueBattery"); // from ui config
|
expect(href).toContain("shelly"); // from ui config
|
||||||
expect(href).toContain("carport_pv"); // from evcc.yaml
|
expect(href).toContain("carport_pv"); // from evcc.yaml
|
||||||
expect(href).toContain("DEBUG"); // from logs
|
expect(href).toContain("DEBUG"); // from logs
|
||||||
expect(href).toContain("MyFancyState"); // from state
|
expect(href).toContain("MyFancyState"); // from state
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
package util
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"maps"
|
|
||||||
"regexp"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// configRedactSecrets defines keys that should be redacted from configuration files
|
|
||||||
var configRedactSecrets = []string{
|
|
||||||
"mac", // infrastructure
|
|
||||||
"sponsortoken", "plant", // global settings
|
|
||||||
"apikey", "user", "password", "pin", // users
|
|
||||||
"token", "access", "refresh", "accesstoken", "refreshtoken", // tokens, including template variations
|
|
||||||
"ain", "secret", "serial", "deviceid", "machineid", "idtag", // devices
|
|
||||||
"app", "chats", "recipients", // push messaging
|
|
||||||
"vin", // vehicles
|
|
||||||
"lat", "lon", "zip", // solar forecast
|
|
||||||
}
|
|
||||||
|
|
||||||
var configRedactRegex = regexp.MustCompile(fmt.Sprintf(`(?i)\b(%s)\b.*?:.*`, strings.Join(configRedactSecrets, "|")))
|
|
||||||
|
|
||||||
// RedactConfigString redacts a configuration string by replacing sensitive values with *****
|
|
||||||
func RedactConfigString(src string) string {
|
|
||||||
return configRedactRegex.ReplaceAllString(src, "$1: *****")
|
|
||||||
}
|
|
||||||
|
|
||||||
// RedactConfigMap redacts sensitive keys in a configuration map
|
|
||||||
func RedactConfigMap(src map[string]any) map[string]any {
|
|
||||||
res := maps.Clone(src)
|
|
||||||
for k := range res {
|
|
||||||
if slices.Contains(configRedactSecrets, k) {
|
|
||||||
res[k] = "*****"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
64
util/redact/redactor.go
Normal file
64
util/redact/redactor.go
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
package redact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/util/templates"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
configRedactRegex *regexp.Regexp
|
||||||
|
configRedactSecrets []string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// fields that are not covered by template params (yet)
|
||||||
|
additional := []string{
|
||||||
|
"sponsortoken", "plant", // global settings
|
||||||
|
"app", "chats", "recipients", // push messaging
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine generated params with additional fields
|
||||||
|
configRedactSecrets = slices.Concat(redactableParams(), additional)
|
||||||
|
|
||||||
|
configRedactRegex = regexp.MustCompile(fmt.Sprintf(`(?i)\b(%s)\b.*?:.*`, strings.Join(configRedactSecrets, "|")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func redactableParams() []string {
|
||||||
|
// Collect all sensitive params from templates (includes defaults)
|
||||||
|
var params []string
|
||||||
|
for _, class := range templates.ClassValues() {
|
||||||
|
for _, tmpl := range templates.ByClass(class) {
|
||||||
|
for _, p := range tmpl.Params {
|
||||||
|
if p.IsMasked() || p.IsPrivate() {
|
||||||
|
params = append(params, strings.ToLower(p.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lo.Uniq(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String redacts a configuration string by replacing sensitive values with *****
|
||||||
|
func String(src string) string {
|
||||||
|
return configRedactRegex.ReplaceAllString(src, "$1: *****")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map redacts sensitive keys in a configuration map
|
||||||
|
func Map(src map[string]any) map[string]any {
|
||||||
|
res := maps.Clone(src)
|
||||||
|
for k := range res {
|
||||||
|
if slices.ContainsFunc(configRedactSecrets, func(s string) bool {
|
||||||
|
return strings.EqualFold(k, s)
|
||||||
|
}) {
|
||||||
|
res[k] = "*****"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
87
util/redact/redactor_test.go
Normal file
87
util/redact/redactor_test.go
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
package redact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestString(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected []string // Strings that should appear
|
||||||
|
redacted []string // Strings that should NOT appear
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "redact password",
|
||||||
|
input: `site:
|
||||||
|
title: My Home
|
||||||
|
meters:
|
||||||
|
grid: my-grid
|
||||||
|
user: testuser
|
||||||
|
password: secretpass123`,
|
||||||
|
expected: []string{"password: *****", "title: My Home", "grid: my-grid"},
|
||||||
|
redacted: []string{"secretpass123"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "redact params marked as private",
|
||||||
|
input: `vehicle:
|
||||||
|
vin: W1234567890123456
|
||||||
|
user: john@example.com
|
||||||
|
capacity: 50`,
|
||||||
|
expected: []string{"vin: *****", "user: *****", "capacity: 50"},
|
||||||
|
redacted: []string{"W1234567890123456", "john@example.com"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "redact multiple sensitive fields",
|
||||||
|
input: `config:
|
||||||
|
lat: 52.520008
|
||||||
|
lon: 13.404954
|
||||||
|
zip: 10115
|
||||||
|
sponsortoken: abc123
|
||||||
|
apikey: xyz789`,
|
||||||
|
expected: []string{"lat: *****", "lon: *****", "zip: *****", "sponsortoken: *****", "apikey: *****"},
|
||||||
|
redacted: []string{"52.520008", "13.404954", "10115", "abc123", "xyz789"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := String(tt.input)
|
||||||
|
|
||||||
|
// Check expected strings are present
|
||||||
|
for _, exp := range tt.expected {
|
||||||
|
assert.Contains(t, result, exp, "Expected to find %q in result", exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check redacted strings are NOT present
|
||||||
|
for _, red := range tt.redacted {
|
||||||
|
assert.NotContains(t, result, red, "Expected %q to be redacted", red)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMap(t *testing.T) {
|
||||||
|
input := map[string]any{
|
||||||
|
"title": "My Home",
|
||||||
|
"password": "secret123",
|
||||||
|
"user": "john@example.com",
|
||||||
|
"vin": "W1234567890",
|
||||||
|
"capacity": 50,
|
||||||
|
"apikey": "abc-def-123",
|
||||||
|
}
|
||||||
|
|
||||||
|
result := Map(input)
|
||||||
|
|
||||||
|
// Check non-sensitive fields are unchanged
|
||||||
|
assert.Equal(t, "My Home", result["title"], "title should not be redacted")
|
||||||
|
assert.Equal(t, 50, result["capacity"], "capacity should not be redacted")
|
||||||
|
|
||||||
|
// Check sensitive fields are redacted
|
||||||
|
sensitiveFields := []string{"password", "user", "vin", "apikey"}
|
||||||
|
for _, field := range sensitiveFields {
|
||||||
|
assert.Equal(t, "*****", result[field], "%s should be redacted", field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -43,6 +43,7 @@ params:
|
||||||
choice: ["https", "http"]
|
choice: ["https", "http"]
|
||||||
default: https
|
default: https
|
||||||
- name: user
|
- name: user
|
||||||
|
private: true
|
||||||
description:
|
description:
|
||||||
de: Benutzerkonto
|
de: Benutzerkonto
|
||||||
en: Username
|
en: Username
|
||||||
|
|
@ -76,6 +77,7 @@ params:
|
||||||
usages: ["pv"]
|
usages: ["pv"]
|
||||||
advanced: true
|
advanced: true
|
||||||
- name: vin
|
- name: vin
|
||||||
|
private: true
|
||||||
description:
|
description:
|
||||||
de: Fahrzeugidentifikationsnummer
|
de: Fahrzeugidentifikationsnummer
|
||||||
en: Vehicle Identification Number
|
en: Vehicle Identification Number
|
||||||
|
|
@ -247,6 +249,7 @@ params:
|
||||||
|
|
||||||
- name: ski
|
- name: ski
|
||||||
required: true
|
required: true
|
||||||
|
private: true
|
||||||
description:
|
description:
|
||||||
en: Subject Key Identifier (SKI)
|
en: Subject Key Identifier (SKI)
|
||||||
de: Identifikationsschlüssel (SKI)
|
de: Identifikationsschlüssel (SKI)
|
||||||
|
|
@ -254,16 +257,18 @@ params:
|
||||||
en: Usually found on the web interface of the wallbox
|
en: Usually found on the web interface of the wallbox
|
||||||
de: Üblicherweise im Web Interface der Wallbox zu finden
|
de: Üblicherweise im Web Interface der Wallbox zu finden
|
||||||
- name: uri
|
- name: uri
|
||||||
|
private: true
|
||||||
description:
|
description:
|
||||||
generic: URI
|
generic: URI
|
||||||
help:
|
help:
|
||||||
en: HTTP(S) address
|
en: HTTP(S) address
|
||||||
de: HTTP(S) Adresse
|
de: HTTP(S) Adresse
|
||||||
- name: url
|
- name: url
|
||||||
|
private: true
|
||||||
description:
|
description:
|
||||||
generic: URL
|
generic: URL
|
||||||
- name: ain
|
- name: ain
|
||||||
mask: true
|
private: true
|
||||||
example: "307788992233"
|
example: "307788992233"
|
||||||
description:
|
description:
|
||||||
en: Actor Identification Number (AIN)
|
en: Actor Identification Number (AIN)
|
||||||
|
|
@ -340,12 +345,15 @@ params:
|
||||||
advanced: true
|
advanced: true
|
||||||
|
|
||||||
- name: accesstoken
|
- name: accesstoken
|
||||||
|
mask: true
|
||||||
description:
|
description:
|
||||||
generic: Access token
|
generic: Access token
|
||||||
- name: refreshtoken
|
- name: refreshtoken
|
||||||
|
mask: true
|
||||||
description:
|
description:
|
||||||
generic: Refresh token
|
generic: Refresh token
|
||||||
- name: serial
|
- name: serial
|
||||||
|
private: true
|
||||||
description:
|
description:
|
||||||
en: Serial
|
en: Serial
|
||||||
de: Seriennummer
|
de: Seriennummer
|
||||||
|
|
@ -360,9 +368,11 @@ params:
|
||||||
description:
|
description:
|
||||||
generic: Client ID
|
generic: Client ID
|
||||||
- name: clientsecret
|
- name: clientsecret
|
||||||
|
mask: true
|
||||||
description:
|
description:
|
||||||
generic: Client Secret
|
generic: Client Secret
|
||||||
- name: token
|
- name: token
|
||||||
|
mask: true
|
||||||
description:
|
description:
|
||||||
generic: Token
|
generic: Token
|
||||||
- name: interval
|
- name: interval
|
||||||
|
|
@ -370,13 +380,16 @@ params:
|
||||||
de: Intervall
|
de: Intervall
|
||||||
en: Interval
|
en: Interval
|
||||||
- name: mac
|
- name: mac
|
||||||
|
private: true
|
||||||
description:
|
description:
|
||||||
en: MAC Address
|
en: MAC Address
|
||||||
de: MAC Adresse
|
de: MAC Adresse
|
||||||
- name: pin
|
- name: pin
|
||||||
|
mask: true
|
||||||
description:
|
description:
|
||||||
generic: PIN
|
generic: PIN
|
||||||
- name: serial_number
|
- name: serial_number
|
||||||
|
private: true
|
||||||
description:
|
description:
|
||||||
en: Serial Number
|
en: Serial Number
|
||||||
de: Seriennummer
|
de: Seriennummer
|
||||||
|
|
@ -384,9 +397,11 @@ params:
|
||||||
description:
|
description:
|
||||||
generic: Watchdog
|
generic: Watchdog
|
||||||
- name: uuid
|
- name: uuid
|
||||||
|
private: true
|
||||||
description:
|
description:
|
||||||
generic: UUID
|
generic: UUID
|
||||||
- name: zip
|
- name: zip
|
||||||
|
private: true
|
||||||
description:
|
description:
|
||||||
en: ZIP code
|
en: ZIP code
|
||||||
de: Postleitzahl
|
de: Postleitzahl
|
||||||
|
|
@ -420,12 +435,14 @@ params:
|
||||||
de: Temperaturquelle
|
de: Temperaturquelle
|
||||||
en: Temperature source
|
en: Temperature source
|
||||||
- name: lat
|
- name: lat
|
||||||
|
private: true
|
||||||
description:
|
description:
|
||||||
en: Latitude
|
en: Latitude
|
||||||
de: Breitengrad
|
de: Breitengrad
|
||||||
type: float
|
type: float
|
||||||
example: 55.7351
|
example: 55.7351
|
||||||
- name: lon
|
- name: lon
|
||||||
|
private: true
|
||||||
description:
|
description:
|
||||||
en: Longitude
|
en: Longitude
|
||||||
de: Längengrad
|
de: Längengrad
|
||||||
|
|
@ -601,6 +618,7 @@ presets:
|
||||||
en: Only enable this option if there is no way to initiate transactions from the charger side! This is only the case if e.g. no RFID reader is available and charging processes would have to be released individually via app. Normally, the charger should always be configured at the device so that either an RFID card is used for activation or the charger is set to "Autostart", "Free Charging" or similar. First check the documentation and configuration possibilities of the charger, ask the manufacturer if necessary! (Uses OCPP RemoteStartTransaction)
|
en: Only enable this option if there is no way to initiate transactions from the charger side! This is only the case if e.g. no RFID reader is available and charging processes would have to be released individually via app. Normally, the charger should always be configured at the device so that either an RFID card is used for activation or the charger is set to "Autostart", "Free Charging" or similar. First check the documentation and configuration possibilities of the charger, ask the manufacturer if necessary! (Uses OCPP RemoteStartTransaction)
|
||||||
- name: idtag
|
- name: idtag
|
||||||
advanced: true
|
advanced: true
|
||||||
|
private: true
|
||||||
type: string
|
type: string
|
||||||
description:
|
description:
|
||||||
en: Authentication token
|
en: Authentication token
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,11 @@ func (t *Template) Validate() error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate that a param cannot be both masked and private
|
||||||
|
if p.Mask && p.Private {
|
||||||
|
return fmt.Errorf("param %s: 'mask' and 'private' cannot be used together. Use 'mask' for sensitive data like passwords/tokens that should be hidden in UI. Use 'private' for personal data like emails/locations that should only be redacted from bug reports", p.Name)
|
||||||
|
}
|
||||||
|
|
||||||
if p.Description.String("en") == "" || p.Description.String("de") == "" {
|
if p.Description.String("en") == "" || p.Description.String("de") == "" {
|
||||||
return fmt.Errorf("param %s: description can't be empty", p.Name)
|
return fmt.Errorf("param %s: description can't be empty", p.Name)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,7 @@ type Param struct {
|
||||||
Preset string `json:"-"` // Reference a predefined set of params
|
Preset string `json:"-"` // Reference a predefined set of params
|
||||||
Required bool `json:",omitempty"` // cli if the user has to provide a non empty value
|
Required bool `json:",omitempty"` // cli if the user has to provide a non empty value
|
||||||
Mask bool `json:",omitempty"` // cli if the value should be masked, e.g. for passwords
|
Mask bool `json:",omitempty"` // cli if the value should be masked, e.g. for passwords
|
||||||
|
Private bool `json:",omitempty"` // value should be redacted in bug reports, e.g. email, locations, ...
|
||||||
Advanced bool `json:",omitempty"` // cli if the user does not need to be asked. Requires a "Default" to be defined.
|
Advanced bool `json:",omitempty"` // cli if the user does not need to be asked. Requires a "Default" to be defined.
|
||||||
Deprecated bool `json:",omitempty"` // if the parameter is deprecated and thus should not be presented in the cli or docs
|
Deprecated bool `json:",omitempty"` // if the parameter is deprecated and thus should not be presented in the cli or docs
|
||||||
Default string `json:",omitempty"` // default value if no user value is provided in the configuration
|
Default string `json:",omitempty"` // default value if no user value is provided in the configuration
|
||||||
|
|
@ -238,6 +239,10 @@ func (p *Param) IsMasked() bool {
|
||||||
return p.Mask
|
return p.Mask
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Param) IsPrivate() bool {
|
||||||
|
return p.Private
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Param) IsRequired() bool {
|
func (p *Param) IsRequired() bool {
|
||||||
return p.Required
|
return p.Required
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue