Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 122 additions & 4 deletions api/dms/service/v2/db_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package v2

import (
"bytes"
"encoding/json"

base "github.com/actiontech/dms/pkg/dms-common/api/base/v1"
dmsCommonV1 "github.com/actiontech/dms/pkg/dms-common/api/dms/v1"
Expand Down Expand Up @@ -107,9 +108,10 @@ type DBService struct {
// DB Service admin user
// Required: true
User string `json:"user"`
// DB Service admin password
// Required: true
Password string `json:"password" validate:"required"`
// Legacy plaintext password; create path rejects when JSON key present
Password string `json:"password"`
// Transport ciphertext: Base64(AES-256-CBC(password)) with fixed SecretKey
SecretPassword string `json:"secret_password,omitempty"`
// DB Service environment tag
// Required: true
EnvironmentTagUID string `json:"environment_tag_uid" validate:"required"`
Expand All @@ -130,6 +132,60 @@ type DBService struct {
// backup switch
// Required: false
BackupMaxRows *uint64 `json:"backup_max_rows,omitempty"`

// passwordKeyPresent is set by UnmarshalJSON when the JSON object contains a "password" key.
passwordKeyPresent bool `json:"-"`
}

// UnmarshalJSON detects whether the wire JSON contains a plaintext "password" key
// (including empty string), which the create path must reject.
func (d *DBService) UnmarshalJSON(data []byte) error {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
_, d.passwordKeyPresent = raw["password"]

type plain struct {
Name string `json:"name"`
DBType string `json:"db_type"`
Host string `json:"host"`
Port string `json:"port"`
User string `json:"user"`
Password string `json:"password"`
SecretPassword string `json:"secret_password"`
EnvironmentTagUID string `json:"environment_tag_uid"`
MaintenanceTimes []*dmsCommonV1.MaintenanceTime `json:"maintenance_times"`
AdditionalParams []*dmsCommonV1.AdditionalParam `json:"additional_params"`
Desc string `json:"desc"`
SQLEConfig *dmsCommonV1.SQLEConfig `json:"sqle_config"`
EnableBackup bool `json:"enable_backup"`
BackupMaxRows *uint64 `json:"backup_max_rows,omitempty"`
}
var p plain
if err := json.Unmarshal(data, &p); err != nil {
return err
}
d.Name = p.Name
d.DBType = p.DBType
d.Host = p.Host
d.Port = p.Port
d.User = p.User
d.Password = p.Password
d.SecretPassword = p.SecretPassword
d.EnvironmentTagUID = p.EnvironmentTagUID
d.MaintenanceTimes = p.MaintenanceTimes
d.AdditionalParams = p.AdditionalParams
d.Desc = p.Desc
d.SQLEConfig = p.SQLEConfig
d.EnableBackup = p.EnableBackup
d.BackupMaxRows = p.BackupMaxRows
return nil
}

// HasPasswordKey reports whether the request JSON included a "password" field.
func (d *DBService) HasPasswordKey() bool {
return d.passwordKeyPresent
}

// swagger:model AddDBServiceReqV2
Expand Down Expand Up @@ -162,8 +218,10 @@ type UpdateDBService struct {
// DB Service admin user
// Required: true
User string `json:"user"`
// DB Service admin password
// Legacy plaintext password; update path rejects when JSON key present
Password *string `json:"password"`
// Transport ciphertext: Base64(AES-256-CBC(password)); only when updating password
SecretPassword string `json:"secret_password,omitempty"`
// DB Service environment tag
// Required: true
EnvironmentTagUID string `json:"environment_tag_uid" validate:"required"`
Expand All @@ -183,6 +241,66 @@ type UpdateDBService struct {
// backup switch
// Required: false
BackupMaxRows *uint64 `json:"backup_max_rows,omitempty"`

// passwordKeyPresent is set by UnmarshalJSON when the JSON object contains a "password" key.
passwordKeyPresent bool `json:"-"`
// secretPasswordKeyPresent is set when JSON contains "secret_password" (even if empty).
secretPasswordKeyPresent bool `json:"-"`
}

// UnmarshalJSON detects whether the wire JSON contains a plaintext "password" key
// (including empty string / null), which the update path must reject.
func (u *UpdateDBService) UnmarshalJSON(data []byte) error {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
_, u.passwordKeyPresent = raw["password"]
_, u.secretPasswordKeyPresent = raw["secret_password"]

type plain struct {
DBType string `json:"db_type"`
Host string `json:"host"`
Port string `json:"port"`
User string `json:"user"`
Password *string `json:"password"`
SecretPassword string `json:"secret_password"`
EnvironmentTagUID string `json:"environment_tag_uid"`
MaintenanceTimes []*dmsCommonV1.MaintenanceTime `json:"maintenance_times"`
AdditionalParams []*dmsCommonV1.AdditionalParam `json:"additional_params"`
Desc *string `json:"desc"`
SQLEConfig *dmsCommonV1.SQLEConfig `json:"sqle_config"`
EnableBackup bool `json:"enable_backup"`
BackupMaxRows *uint64 `json:"backup_max_rows,omitempty"`
}
var p plain
if err := json.Unmarshal(data, &p); err != nil {
return err
}
u.DBType = p.DBType
u.Host = p.Host
u.Port = p.Port
u.User = p.User
u.Password = p.Password
u.SecretPassword = p.SecretPassword
u.EnvironmentTagUID = p.EnvironmentTagUID
u.MaintenanceTimes = p.MaintenanceTimes
u.AdditionalParams = p.AdditionalParams
u.Desc = p.Desc
u.SQLEConfig = p.SQLEConfig
u.EnableBackup = p.EnableBackup
u.BackupMaxRows = p.BackupMaxRows
return nil
}

// HasPasswordKey reports whether the request JSON included a "password" field.
func (u *UpdateDBService) HasPasswordKey() bool {
return u.passwordKeyPresent
}

// HasSecretPasswordKey reports whether the request JSON included a "secret_password" field.
func (u *UpdateDBService) HasSecretPasswordKey() bool {
return u.secretPasswordKeyPresent
}

// swagger:model ImportDBServicesOfOneProjectReqV2
Expand Down
121 changes: 82 additions & 39 deletions api/dms/service/v2/db_service_password_test.go
Original file line number Diff line number Diff line change
@@ -1,49 +1,32 @@
package v2

import (
"encoding/json"
"strings"
"testing"

utilConf "github.com/actiontech/dms/pkg/dms-common/pkg/config"
)

func TestAddDBServiceReq_EmptyPasswordRejected(t *testing.T) {
func TestAddDBServiceReq_CipherFieldsPassValidation(t *testing.T) {
t.Parallel()

base := func(password string) *AddDBServiceReq {
return &AddDBServiceReq{
ProjectUid: "700300",
DBService: &DBService{
Name: "gbase8a_empty_pwd",
DBType: "GBase-8a",
Host: "10.186.16.126",
Port: "5258",
User: "root",
Password: password,
EnvironmentTagUID: "2086752861772845056",
MaintenanceTimes: nil,
},
}
req := &AddDBServiceReq{
ProjectUid: "700300",
DBService: &DBService{
Name: "mysql_cipher_create",
DBType: "MySQL",
Host: "10.186.16.126",
Port: "3307",
User: "testuser",
SecretPassword: "cipher-b64",
EnvironmentTagUID: "2086752861772845056",
MaintenanceTimes: nil,
},
}
if err := utilConf.Validate(req); err != nil {
t.Fatalf("expected cipher create payload to pass Add validation, got: %v", err)
}

t.Run("password_empty_string", func(t *testing.T) {
t.Parallel()
err := utilConf.Validate(base(""))
if err == nil {
t.Fatal("expected empty password to fail Add validation")
}
msg := strings.ToLower(err.Error())
if !strings.Contains(msg, "password") || !strings.Contains(msg, "required") {
t.Fatalf("expected Password required validation error, got: %v", err)
}
})

t.Run("password_non_empty_passes_password_rule", func(t *testing.T) {
t.Parallel()
if err := utilConf.Validate(base("not-empty")); err != nil {
t.Fatalf("expected non-empty password to pass Add validation, got: %v", err)
}
})
}

func TestAddDBServiceReq_MissingHostStillRequired(t *testing.T) {
Expand All @@ -52,12 +35,12 @@ func TestAddDBServiceReq_MissingHostStillRequired(t *testing.T) {
req := &AddDBServiceReq{
ProjectUid: "700300",
DBService: &DBService{
Name: "gbase8a_missing_host",
DBType: "GBase-8a",
Name: "mysql_missing_host",
DBType: "MySQL",
Host: "",
Port: "5258",
User: "root",
Password: "not-empty",
Port: "3307",
User: "testuser",
SecretPassword: "cipher-b64",
EnvironmentTagUID: "2086752861772845056",
},
}
Expand All @@ -71,3 +54,63 @@ func TestAddDBServiceReq_MissingHostStillRequired(t *testing.T) {
t.Fatalf("expected Host required validation error, got: %v", err)
}
}

func TestDBService_HasPasswordKey(t *testing.T) {
t.Parallel()

var withKey DBService
if err := json.Unmarshal([]byte(`{"name":"n","db_type":"MySQL","host":"127.0.0.1","port":"3306","user":"u","password":"","secret_password":"x","environment_tag_uid":"1"}`), &withKey); err != nil {
t.Fatal(err)
}
if !withKey.HasPasswordKey() {
t.Fatal("expected password key present")
}

var withoutKey DBService
if err := json.Unmarshal([]byte(`{"name":"n","db_type":"MySQL","host":"127.0.0.1","port":"3306","user":"u","secret_password":"x","environment_tag_uid":"1"}`), &withoutKey); err != nil {
t.Fatal(err)
}
if withoutKey.HasPasswordKey() {
t.Fatal("expected password key absent")
}
}

func TestUpdateDBService_HasPasswordKey(t *testing.T) {
t.Parallel()

var withKey UpdateDBService
if err := json.Unmarshal([]byte(`{"db_type":"MySQL","host":"127.0.0.1","port":"3306","user":"u","password":"","secret_password":"x","environment_tag_uid":"1"}`), &withKey); err != nil {
t.Fatal(err)
}
if !withKey.HasPasswordKey() {
t.Fatal("expected password key present on update")
}
if withKey.SecretPassword != "x" {
t.Fatalf("expected cipher field preserved, got secret=%q", withKey.SecretPassword)
}
if !withKey.HasSecretPasswordKey() {
t.Fatal("expected secret_password key present on update")
}

var withoutKey UpdateDBService
if err := json.Unmarshal([]byte(`{"db_type":"MySQL","host":"127.0.0.1","port":"3306","user":"u","environment_tag_uid":"1","desc":"keep-pwd"}`), &withoutKey); err != nil {
t.Fatal(err)
}
if withoutKey.HasPasswordKey() {
t.Fatal("expected password key absent on update without password fields")
}
if withoutKey.HasSecretPasswordKey() {
t.Fatal("expected secret_password key absent when omitted")
}
if withoutKey.SecretPassword != "" {
t.Fatal("expected no cipher fields when omitted")
}

var emptySecret UpdateDBService
if err := json.Unmarshal([]byte(`{"db_type":"MySQL","host":"127.0.0.1","port":"3306","user":"u","secret_password":"","environment_tag_uid":"1"}`), &emptySecret); err != nil {
t.Fatal(err)
}
if !emptySecret.HasSecretPasswordKey() {
t.Fatal("expected empty secret_password placeholder to still set key present")
}
}
Loading
Loading