数据脱敏插件完成

This commit is contained in:
Liujian
2024-09-14 16:38:41 +08:00
parent 63c41cc339
commit 090025f434
32 changed files with 1825 additions and 69 deletions

View File

@@ -6,6 +6,7 @@ import (
"github.com/eolinker/apinto/drivers/discovery/eureka"
"github.com/eolinker/apinto/drivers/discovery/nacos"
"github.com/eolinker/apinto/drivers/discovery/static"
data_mask_strategy "github.com/eolinker/apinto/drivers/strategy/data-mask-strategy"
"github.com/eolinker/apinto/application/auth"
"github.com/eolinker/apinto/drivers/discovery/polaris"
@@ -74,6 +75,7 @@ func driverRegister(extenderRegister eosc.IExtenderDriverRegister) {
grey_strategy.Register(extenderRegister)
visit_strategy.Register(extenderRegister)
fuse_strategy.Register(extenderRegister)
data_mask_strategy.Register(extenderRegister)
// 编码器
protocbuf.Register(extenderRegister)

View File

@@ -21,6 +21,7 @@ import (
response_filter "github.com/eolinker/apinto/drivers/plugins/response-filter"
response_rewrite_v2 "github.com/eolinker/apinto/drivers/plugins/response-rewrite_v2"
rsa_filter "github.com/eolinker/apinto/drivers/plugins/rsa-filter"
data_mask "github.com/eolinker/apinto/drivers/plugins/strategy/data-mask"
access_log "github.com/eolinker/apinto/drivers/plugins/access-log"
body_check "github.com/eolinker/apinto/drivers/plugins/body-check"
@@ -57,6 +58,7 @@ func pluginRegister(extenderRegister eosc.IExtenderDriverRegister) {
grey.Register(extenderRegister)
visit.Register(extenderRegister)
fuse.Register(extenderRegister)
data_mask.Register(extenderRegister)
// Dubbo协议相关插件
dubbo2_proxy_rewrite.Register(extenderRegister)

View File

@@ -165,6 +165,12 @@ func ApintoProfession() []*eosc.ProfessionConfig {
Label: "熔断策略",
Desc: "熔断策略",
},
{
Id: "eolinker.com:apinto:strategy-data_mask",
Name: "data_mask",
Label: "数据脱敏策略",
Desc: "数据脱敏策略",
},
},
Mod: eosc.ProfessionConfig_Worker,
},

View File

@@ -12,12 +12,13 @@ import (
"encoding/pem"
"errors"
"fmt"
"github.com/ohler55/ojg/jp"
"github.com/ohler55/ojg/oj"
"math/big"
"reflect"
"strings"
"time"
"github.com/ohler55/ojg/jp"
"github.com/ohler55/ojg/oj"
)
type jwtToken struct {
@@ -83,18 +84,18 @@ func (m *signingMethod) Verify(signingString, signature string, key interface{})
if !ok {
return errInvalidKeyType
}
// Decode signature, for comparison
sig, err := decodeSegment(signature)
if err != nil {
return err
}
// Can we use the specified hashing method?
if !m.Hash.Available() {
return errHashUnavailable
}
// This signing method is symmetric, so we validate the signature
// by reproducing the signature from the signing string and key, then
// comparing that against the provided signature.
@@ -103,47 +104,47 @@ func (m *signingMethod) Verify(signingString, signature string, key interface{})
if !hmac.Equal(sig, hasher.Sum(nil)) {
return errSignatureInvalid
}
// No validation errors. Signature is good.
return nil
}
case "RS256", "RS384", "RS512":
{
var err error
// Decode the signature
var sig []byte
if sig, err = decodeSegment(signature); err != nil {
return err
}
var rsaKey *rsa.PublicKey
var ok bool
if rsaKey, ok = key.(*rsa.PublicKey); !ok {
return errInvalidKeyType
}
// Create hasher
if !m.Hash.Available() {
return errHashUnavailable
}
hasher := m.Hash.New()
hasher.Write([]byte(signingString))
// Verify the signature
return rsa.VerifyPKCS1v15(rsaKey, m.Hash, hasher.Sum(nil), sig)
}
case "ES256", "ES384", "ES512":
{
var err error
// Decode the signature
var sig []byte
if sig, err = decodeSegment(signature); err != nil {
return err
}
// GetEmployee the key
var ecdsaKey *ecdsa.PublicKey
switch k := key.(type) {
@@ -152,26 +153,26 @@ func (m *signingMethod) Verify(signingString, signature string, key interface{})
default:
return errInvalidKeyType
}
if len(sig) != 2*m.KeySize {
return errECDSAVerification
}
r := big.NewInt(0).SetBytes(sig[:m.KeySize])
s := big.NewInt(0).SetBytes(sig[m.KeySize:])
// Create hasher
if !m.Hash.Available() {
return errHashUnavailable
}
hasher := m.Hash.New()
hasher.Write([]byte(signingString))
// Verify the signature
if verifystatus := ecdsa.Verify(ecdsaKey, hasher.Sum(nil), r, s); verifystatus == true {
return nil
}
return errECDSAVerification
}
default:
@@ -187,33 +188,33 @@ func (m *signingMethod) Sign(signingString string, key interface{}) (string, err
if !m.Hash.Available() {
return "", errHashUnavailable
}
hasher := hmac.New(m.Hash.New, keyBytes)
hasher.Write([]byte(signingString))
return encodeSegment(hasher.Sum(nil)), nil
}
return "", errInvalidKeyType
}
case "RS256", "RS384", "RS512":
{
var rsaKey *rsa.PrivateKey
var ok bool
// Validate type of key
if rsaKey, ok = key.(*rsa.PrivateKey); !ok {
return "", errInvalidKey
}
// Create the hasher
if !m.Hash.Available() {
return "", errHashUnavailable
}
hasher := m.Hash.New()
hasher.Write([]byte(signingString))
// Sign the string and return the encoded bytes
if sigBytes, err := rsa.SignPKCS1v15(rand.Reader, rsaKey, m.Hash, hasher.Sum(nil)); err == nil {
return encodeSegment(sigBytes), nil
@@ -231,41 +232,41 @@ func (m *signingMethod) Sign(signingString string, key interface{}) (string, err
default:
return "", errInvalidKeyType
}
// Create the hasher
if !m.Hash.Available() {
return "", errHashUnavailable
}
hasher := m.Hash.New()
hasher.Write([]byte(signingString))
// Sign the string and return r, s
if r, s, err := ecdsa.Sign(rand.Reader, ecdsaKey, hasher.Sum(nil)); err == nil {
curveBits := ecdsaKey.Curve.Params().BitSize
if m.CurveBits != curveBits {
return "", errInvalidKey
}
keyBytes := curveBits / 8
if curveBits%8 > 0 {
keyBytes++
}
// We serialize the outpus (r and s) into big-endian byte arrays and pad
// them with zeros on the left to make sure the sizes work out. Both arrays
// must be keyBytes long, and the output must be 2*keyBytes long.
rBytes := r.Bytes()
rBytesPadded := make([]byte, keyBytes)
copy(rBytesPadded[keyBytes-len(rBytes):], rBytes)
sBytes := s.Bytes()
sBytesPadded := make([]byte, keyBytes)
copy(sBytesPadded[keyBytes-len(sBytes):], sBytes)
out := append(rBytesPadded, sBytesPadded...)
return encodeSegment(out), nil
} else {
return "", err
@@ -290,7 +291,7 @@ func decodeSegment(seg string) ([]byte, error) {
if l := len(seg) % 4; l > 0 {
seg += strings.Repeat("=", 4-l)
}
return base64.URLEncoding.DecodeString(seg)
}
@@ -299,16 +300,16 @@ func encodeSegment(seg []byte) string {
return strings.TrimRight(base64.URLEncoding.EncodeToString(seg), "=")
}
//ParseRSAPublicKeyFromPEM parse PEM encoded PKCS1 or PKCS8 public key
// ParseRSAPublicKeyFromPEM parse PEM encoded PKCS1 or PKCS8 public key
func ParseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error) {
var err error
// parse PEM block
var block *pem.Block
if block, _ = pem.Decode(key); block == nil {
return nil, errKeyMustBePEMEncoded
}
// parse the key
var parsedKey interface{}
if parsedKey, err = x509.ParsePKIXPublicKey(block.Bytes); err != nil {
@@ -318,26 +319,26 @@ func ParseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error) {
return nil, err
}
}
var pkey *rsa.PublicKey
var ok bool
if pkey, ok = parsedKey.(*rsa.PublicKey); !ok {
return nil, errNotRSAPublicKey
}
return pkey, nil
}
//ParseECPublicKeyFromPEM parse PEM encoded PKCS1 or PKCS8 public key
// ParseECPublicKeyFromPEM parse PEM encoded PKCS1 or PKCS8 public key
func ParseECPublicKeyFromPEM(key []byte) (*ecdsa.PublicKey, error) {
var err error
// parse PEM block
var block *pem.Block
if block, _ = pem.Decode(key); block == nil {
return nil, errKeyMustBePEMEncoded
}
// parse the key
var parsedKey interface{}
if parsedKey, err = x509.ParsePKIXPublicKey(block.Bytes); err != nil {
@@ -347,13 +348,13 @@ func ParseECPublicKeyFromPEM(key []byte) (*ecdsa.PublicKey, error) {
return nil, err
}
}
var pkey *ecdsa.PublicKey
var ok bool
if pkey, ok = parsedKey.(*ecdsa.PublicKey); !ok {
return nil, errNotECPublicKey
}
return pkey, nil
}
@@ -365,7 +366,7 @@ func b64Decode(input string) (string, error) {
padlen := 4 - remainder
input = input + strings.Repeat("=", padlen)
}
// 将原字符串中的"_","-"分别用"/"和"+"替换
// 将原字符串中的"_","-"分别用"/"和"+"替换x
input = strings.Replace(strings.Replace(input, "_", "/", -1), "-", "+", -1)
result, err := base64.StdEncoding.DecodeString(input)
return string(result), err
@@ -377,7 +378,7 @@ func tokenize(token string) []string {
if len(parts) == 3 {
return parts
}
return nil
}
@@ -396,7 +397,7 @@ func decodeToken(token string) (*jwtToken, error) {
if err != nil {
return nil, errors.New("[jwt_auth] Invalid base64 encoded JSON")
}
if err = json.Unmarshal([]byte(headerD64), &header); err != nil {
return nil, errors.New("[jwt_auth] Invalid JSON")
}
@@ -426,9 +427,9 @@ func decodeToken(token string) (*jwtToken, error) {
return &jwtToken{Token: token, Header64: header64, Claims64: claims64, Signature64: signature64, Header: header, Claims: claims, Signature: signature}, nil
}
//verifySignature 验证签名
// verifySignature 验证签名
func verifySignature(token *jwtToken, key string) error {
var k interface{}
switch token.Header["alg"].(string) {
case "HS256", "HS384", "HS512":
@@ -459,23 +460,23 @@ func verifySignature(token *jwtToken, key string) error {
return newSigningMethod(token.Header["alg"].(string)).Verify(token.Header64+"."+token.Claims64, token.Signature64, k)
}
//verifyRegisteredClaims 验证签发字段
// verifyRegisteredClaims 验证签发字段
func verifyRegisteredClaims(token *jwtToken, claimsToVerify []string) error {
if claimsToVerify == nil {
claimsToVerify = []string{}
}
for _, claimName := range claimsToVerify {
var claim int64 = 0
if _, ok := token.Claims[claimName]; ok {
if typeOfData(token.Claims[claimName]) == reflect.Float64 {
claimFloat64, success := token.Claims[claimName].(float64)
if success {
claim = int64(claimFloat64)
}
}
}
if claim < 1 {
return errors.New("[jwt_auth] " + claimName + " must be a number")
@@ -496,7 +497,7 @@ func verifyRegisteredClaims(token *jwtToken, claimsToVerify []string) error {
return nil
}
//获取数据的类型
// 获取数据的类型
func typeOfData(data interface{}) reflect.Kind {
value := reflect.ValueOf(data)
valueType := value.Kind()
@@ -506,13 +507,13 @@ func typeOfData(data interface{}) reflect.Kind {
return valueType
}
//doJWTAuthentication 进行JWT鉴权
// doJWTAuthentication 进行JWT鉴权
func (j *jwt) doJWTAuthentication(tokenStr string) (string, error) {
token, err := decodeToken(tokenStr)
if err != nil {
return "", errors.New("Bad token; " + err.Error())
}
key := ""
keyClaimName := "iss"
if _, ok := token.Claims[keyClaimName]; ok {
@@ -520,11 +521,11 @@ func (j *jwt) doJWTAuthentication(tokenStr string) (string, error) {
} else if _, ok = token.Header[keyClaimName]; ok {
key = token.Header[keyClaimName].(string)
}
if key == "" {
return "", errors.New("no mandatory " + keyClaimName + " in claims")
}
algorithm := token.Header["alg"].(string)
if j.cfg.Algorithm != algorithm {
return "", fmt.Errorf("no match algorithm,need %s,now %s", j.cfg.Algorithm, algorithm)
@@ -541,20 +542,20 @@ func (j *jwt) doJWTAuthentication(tokenStr string) (string, error) {
} else {
jwtSecretValue = j.cfg.RsaPublicKey
}
if jwtSecretValue == "" {
return "", errors.New("invalid key/secret")
}
if err = verifySignature(token, jwtSecretValue); err != nil {
return "", errors.New("[jwt_auth] Invalid signature")
}
if err = verifyRegisteredClaims(token, j.cfg.ClaimsToVerify); err != nil {
return "", err
}
data, _ := json.Marshal(token.Claims)
return getUserByPath(data, j.cfg.Path)
}

View File

@@ -27,6 +27,7 @@ type Config struct {
PartitionKey string `json:"partition_key" yaml:"partition_key" switch:"partition_type==='hash'"`
Type string `json:"type" yaml:"type" enum:"json,line" label:"输出格式"`
ContentResize []ContentResize `json:"content_resize" yaml:"content_resize" label:"内容截断配置" switch:"type===json"`
Filters []*Filter `json:"filters" yaml:"conditions" label:"过滤条件"`
Formatter eosc.FormatterConfig `json:"formatter" yaml:"formatter" label:"格式化配置"`
}
@@ -35,6 +36,15 @@ type ContentResize struct {
Suffix string `json:"suffix" label:"匹配标签后缀"`
}
const (
equalCondition = "equal"
)
type Filter struct {
Key string `json:"key"`
Value string `json:"value"`
}
type ProducerConfig struct {
Scopes []string `json:"scopes" yaml:"scopes"`
Address []string `json:"address" yaml:"address"`
@@ -46,6 +56,7 @@ type ProducerConfig struct {
Type string `json:"type" yaml:"type"`
ContentResize []ContentResize `json:"content_resize" yaml:"content_resize"`
Formatter eosc.FormatterConfig `json:"formatter" yaml:"formatter"`
Filters []*Filter `json:"filters" yaml:"conditions" label:"过滤条件"`
}
func (c *Config) doCheck() (*ProducerConfig, error) {
@@ -115,5 +126,6 @@ func (c *Config) doCheck() (*ProducerConfig, error) {
p.ContentResize = conf.ContentResize
p.Formatter = conf.Formatter
p.Conf = s
p.Filters = conf.Filters
return p, nil
}

View File

@@ -1,7 +1,12 @@
package kafka
import (
"fmt"
"reflect"
"strconv"
"github.com/eolinker/apinto/checker"
"github.com/eolinker/eosc/log"
scope_manager "github.com/eolinker/apinto/scope-manager"
@@ -14,15 +19,58 @@ import (
var _ output.IEntryOutput = (*Output)(nil)
var _ eosc.IWorker = (*Output)(nil)
type filter struct {
key string
checker.Checker
}
func parseFilters(filters []*Filter) []*filter {
result := make([]*filter, 0, len(filters))
for _, f := range filters {
c, err := checker.Parse(f.Value)
if err != nil {
log.Errorf("parse filter value(%s) error: %v", f.Value, err)
continue
}
result = append(result, &filter{
key: f.Key,
Checker: c,
})
}
return result
}
type Output struct {
drivers.WorkerBase
producer Producer
//scopes []string
producer Producer
filters []*filter
config *ProducerConfig
isRunning bool
}
func (o *Output) Output(entry eosc.IEntry) error {
for _, f := range o.filters {
val := entry.Read(f.key)
switch v := val.(type) {
case string:
ok := f.Check(v, true)
if !ok {
return nil
}
case bool:
ok := f.Check(strconv.FormatBool(v), true)
if !ok {
return nil
}
case int, int64:
ok := f.Check(fmt.Sprintf("%d", v), true)
if !ok {
return nil
}
default:
continue
}
}
p := o.producer
if p != nil {
return p.output(entry)
@@ -44,6 +92,7 @@ func (o *Output) Start() error {
return err
}
o.producer = p
o.filters = parseFilters(o.config.Filters)
scope_manager.Set(o.Id(), o, o.config.Scopes...)
return nil
}
@@ -65,12 +114,13 @@ func (o *Output) Reset(conf interface{}, workers map[eosc.RequireId]eosc.IWorker
if p == nil {
p = newTProducer(o.config)
}
err = p.reset(o.config)
if err != nil {
return err
}
//err = p.reset(o.config)
//if err != nil {
// return err
//}
o.producer = p
}
o.filters = parseFilters(cfg.Filters)
scope_manager.Set(o.Id(), o, o.config.Scopes...)
return nil
}

View File

@@ -0,0 +1,15 @@
package data_mask
import (
"github.com/eolinker/apinto/drivers"
"github.com/eolinker/eosc"
)
type Config struct {
}
func Create(id, name string, conf *Config, workers map[eosc.RequireId]eosc.IWorker) (eosc.IWorker, error) {
return &Strategy{
WorkerBase: drivers.Worker(id, name),
}, nil
}

View File

@@ -0,0 +1,17 @@
package data_mask
import (
"github.com/eolinker/apinto/drivers"
"github.com/eolinker/eosc"
)
const (
Name = "strategy-plugin-data_mask"
)
func Register(register eosc.IExtenderDriverRegister) {
register.RegisterExtenderDriver(Name, NewFactory())
}
func NewFactory() eosc.IExtenderDriverFactory {
return drivers.NewFactory[Config](Create)
}

View File

@@ -0,0 +1,36 @@
package data_mask
import (
"github.com/eolinker/apinto/drivers"
data_mask_strategy "github.com/eolinker/apinto/drivers/strategy/data-mask-strategy"
"github.com/eolinker/eosc"
eoscContext "github.com/eolinker/eosc/eocontext"
)
type Strategy struct {
drivers.WorkerBase
}
func (s *Strategy) DoFilter(ctx eoscContext.EoContext, next eoscContext.IChain) (err error) {
return data_mask_strategy.DoStrategy(ctx, next)
}
func (s *Strategy) Destroy() {
return
}
func (s *Strategy) Start() error {
return nil
}
func (s *Strategy) Reset(conf interface{}, workers map[eosc.RequireId]eosc.IWorker) error {
return nil
}
func (s *Strategy) Stop() error {
return nil
}
func (s *Strategy) CheckSkill(skill string) bool {
return eoscContext.FilterSkillName == skill
}

View File

@@ -0,0 +1,106 @@
package data_mask_strategy
import (
"sort"
"sync"
http_service "github.com/eolinker/eosc/eocontext/http-context"
"github.com/eolinker/apinto/strategy"
"github.com/eolinker/eosc/eocontext"
)
var (
actuatorSet ActuatorSet
)
func init() {
actuator := newtActuator()
actuatorSet = actuator
}
type ActuatorSet interface {
strategy.IStrategyHandler
Set(string, *handler)
Del(id string)
}
type tActuator struct {
lock sync.RWMutex
all map[string]*handler
handlers []*handler
}
func (a *tActuator) Destroy() {
}
func (a *tActuator) Set(id string, val *handler) {
// 调用来源有锁
a.all[id] = val
a.rebuild()
}
func (a *tActuator) Del(id string) {
// 调用来源有锁
delete(a.all, id)
a.rebuild()
}
func (a *tActuator) rebuild() {
handlers := make([]*handler, 0, len(a.all))
for _, h := range a.all {
handlers = append(handlers, h)
}
sort.Slice(handlers, func(i, j int) bool {
return handlers[i].priority < handlers[j].priority
})
a.lock.Lock()
defer a.lock.Unlock()
a.handlers = handlers
}
func newtActuator() *tActuator {
return &tActuator{
all: make(map[string]*handler),
}
}
func (a *tActuator) Strategy(ctx eocontext.EoContext, next eocontext.IChain) error {
httpCtx, err := http_service.Assert(ctx)
if err != nil {
return err
}
if next != nil {
err = next.DoChain(ctx)
if err != nil {
return err
}
}
a.lock.RLock()
handlers := a.handlers
a.lock.RUnlock()
//var execHandler *handler
for _, h := range handlers {
// 匹配Filter
if !h.filter.Check(httpCtx) {
// 未命中,下一条规则
continue
}
err = h.ResponseExec(httpCtx)
if err != nil {
return err
}
//execHandler = h
// 匹配中后,跳出循环
break
}
return nil
}
func DoStrategy(ctx eocontext.EoContext, next eocontext.IChain) error {
return actuatorSet.Strategy(ctx, next)
}

View File

@@ -0,0 +1,15 @@
package data_mask_strategy
import (
"github.com/eolinker/apinto/drivers/strategy/data-mask-strategy/mask"
"github.com/eolinker/apinto/strategy"
)
type Config struct {
Name string `json:"name" skip:"skip"`
Description string `json:"description" skip:"skip"`
//Stop bool `json:"stop"`
Priority int `json:"priority" label:"优先级" description:"1-999"`
Filters strategy.FilterConfig `json:"filters" label:"过滤规则"`
DataMask mask.DataMask `json:"data_mask" label:"规则"`
}

View File

@@ -0,0 +1,81 @@
package data_mask_strategy
import (
"reflect"
"github.com/eolinker/eosc"
)
var (
controller = NewController()
_ eosc.ISetting = controller
)
type Controller struct {
profession string
driver string
all map[string]struct{}
}
func (c *Controller) Store(id string) {
c.all[id] = struct{}{}
}
func (c *Controller) Del(id string) {
delete(c.all, id)
}
func (c *Controller) ConfigType() reflect.Type {
return configType
}
func (c *Controller) Set(conf interface{}) (err error) {
return eosc.ErrorUnsupportedKind
}
func (c *Controller) Get() interface{} {
return nil
}
func (c *Controller) Mode() eosc.SettingMode {
return eosc.SettingModeBatch
}
func (c *Controller) Check(cfg interface{}) (profession, name, driver, desc string, err error) {
conf, ok := cfg.(*Config)
if !ok {
err = eosc.ErrorConfigIsNil
return
}
if empty(conf.Name) {
err = eosc.ErrorConfigFieldUnknown
return
}
err = checkConfig(conf)
if err != nil {
return
}
return c.profession, conf.Name, c.driver, conf.Description, nil
}
func empty(vs ...string) bool {
for _, v := range vs {
if len(v) == 0 {
return true
}
}
return false
}
func (c *Controller) AllWorkers() []string {
ws := make([]string, 0, len(c.all))
for id := range c.all {
ws = append(ws, id)
}
return ws
}
func NewController() *Controller {
return &Controller{
all: map[string]struct{}{},
}
}

View File

@@ -0,0 +1,69 @@
package data_mask_strategy
import (
"fmt"
"github.com/eolinker/apinto/drivers/strategy/data-mask-strategy/mask"
"github.com/eolinker/apinto/strategy"
"github.com/eolinker/eosc"
)
func checkConfig(conf *Config) error {
_, err := strategy.ParseFilter(conf.Filters)
if err != nil {
return err
}
if err != nil {
return err
}
if len(conf.DataMask.Rules) < 1 {
return fmt.Errorf("at least one rule is required")
}
for _, rule := range conf.DataMask.Rules {
if rule.Match == nil {
return fmt.Errorf("match is required")
}
if rule.Mask == nil {
return fmt.Errorf("mask is required")
}
if rule.Mask.Begin < 0 {
return fmt.Errorf("begin must be greater than or equal to 0")
}
if rule.Mask.Length < -1 {
return fmt.Errorf("length must be greater than or equal to -1")
}
if rule.Match.Type == mask.MatchInner && (rule.Match.Value == mask.MatchInnerValueDate || rule.Match.Value == mask.MatchInnerValueAmount) && rule.Mask.Type == mask.MaskShuffling {
return fmt.Errorf("date and amount cannot be shuffled")
}
if rule.Mask.Type == mask.MaskReplacement && rule.Mask.Replace == nil {
return fmt.Errorf("replace is required")
}
}
return nil
}
func Check(cfg *Config, workers map[eosc.RequireId]eosc.IWorker) error {
return checkConfig(cfg)
}
func Create(id, name string, v *Config, workers map[eosc.RequireId]eosc.IWorker) (eosc.IWorker, error) {
if err := Check(v, workers); err != nil {
return nil, err
}
lg := &executor{
id: id,
name: name,
}
err := lg.reset(v, workers)
if err != nil {
return nil, err
}
controller.Store(id)
return lg, nil
}

View File

@@ -0,0 +1,75 @@
package data_mask_strategy
import (
"reflect"
"github.com/eolinker/eosc"
)
var (
_ eosc.IWorker = (*executor)(nil)
_ eosc.IWorkerDestroy = (*executor)(nil)
)
type executor struct {
id string
name string
config *Config
//handler *handler
}
func (l *executor) Destroy() error {
controller.Del(l.id)
return nil
}
func (l *executor) Id() string {
return l.id
}
func (l *executor) Start() error {
//if l.isRunning == 0 {
// l.isRunning = 1
//}
return nil
}
func (l *executor) Reset(v interface{}, workers map[eosc.RequireId]eosc.IWorker) error {
conf, ok := v.(*Config)
if !ok {
return eosc.ErrorConfigType
}
return l.reset(conf, workers)
}
func (l *executor) reset(conf *Config, workers map[eosc.RequireId]eosc.IWorker) error {
confCore := conf
if reflect.DeepEqual(l.config, confCore) {
return nil
}
h, err := newHandler(confCore)
if err != nil {
return err
}
l.config = confCore
//l.handler = h
//if l.isRunning != 0 {
actuatorSet.Set(l.id, h)
//}
return nil
}
func (l *executor) Stop() error {
//if l.isRunning != 0 {
// l.isRunning = 0
//}
actuatorSet.Del(l.id)
return nil
}
func (l *executor) CheckSkill(skill string) bool {
return false
}

View File

@@ -0,0 +1,50 @@
package data_mask_strategy
import (
"reflect"
"sync"
"github.com/eolinker/apinto/drivers/strategy/data-mask-strategy/mask/inner"
json_path "github.com/eolinker/apinto/drivers/strategy/data-mask-strategy/mask/json-path"
"github.com/eolinker/apinto/drivers/strategy/data-mask-strategy/mask/keyword"
"github.com/eolinker/apinto/drivers/strategy/data-mask-strategy/mask/regex"
"github.com/eolinker/apinto/drivers"
"github.com/eolinker/eosc"
"github.com/eolinker/eosc/setting"
)
const Name = "strategy-data_mask"
var (
configType = reflect.TypeOf((*Config)(nil))
once sync.Once
)
// Register 注册http路由驱动工厂
func Register(register eosc.IExtenderDriverRegister) {
register.RegisterExtenderDriver(Name, newFactory())
setting.RegisterSetting("strategies-data_mask", controller)
}
type factory struct {
eosc.IExtenderDriverFactory
}
func newFactory() eosc.IExtenderDriverFactory {
return &factory{
IExtenderDriverFactory: drivers.NewFactory[Config](Create, Check),
}
}
func (f *factory) Create(profession string, name string, label string, desc string, params map[string]interface{}) (eosc.IExtenderDriver, error) {
once.Do(func() {
inner.Register()
json_path.Register()
keyword.Register()
regex.Register()
})
controller.driver = name
controller.profession = profession
return f.IExtenderDriverFactory.Create(profession, name, label, desc, params)
}

View File

@@ -0,0 +1,93 @@
package data_mask_strategy
import (
"fmt"
"github.com/eolinker/apinto/drivers/strategy/data-mask-strategy/mask"
"github.com/eolinker/apinto/strategy"
"github.com/eolinker/eosc/eocontext"
http_context "github.com/eolinker/eosc/eocontext/http-context"
"github.com/eolinker/eosc/log"
)
type handler struct {
name string
filter strategy.IFilter
priority int
maskExecutors []mask.IMaskDriver
}
func newHandler(conf *Config) (*handler, error) {
filter, err := strategy.ParseFilter(conf.Filters)
if err != nil {
return nil, err
}
maskExecutors := make([]mask.IMaskDriver, 0, len(conf.DataMask.Rules))
for _, rule := range conf.DataMask.Rules {
maskFunc, err := mask.GenMaskFunc(rule.Mask)
if err != nil {
return nil, err
}
fac, has := mask.GetMaskFactory(rule.Match.Type)
if !has {
return nil, fmt.Errorf("match type not found: %s", rule.Match.Type)
}
e, err := fac.Create(rule, maskFunc)
if err != nil {
return nil, err
}
maskExecutors = append(maskExecutors, e)
}
return &handler{
name: conf.Name,
filter: filter,
priority: conf.Priority,
maskExecutors: maskExecutors,
}, nil
}
func (h *handler) RequestExec(ctx eocontext.EoContext) error {
httpCtx, err := http_context.Assert(ctx)
if err != nil {
return err
}
body, err := httpCtx.Proxy().Body().RawBody()
if err != nil {
return err
}
contentType := httpCtx.Proxy().ContentType()
for _, e := range h.maskExecutors {
body, err = e.Exec(body)
if err != nil {
log.Errorf("request mask exec error: (%v),handler name: (%s),rule: (%s)", err, h.name, e.String())
continue
}
}
httpCtx.Proxy().Body().SetRaw(contentType, body)
return nil
}
func (h *handler) ResponseExec(ctx eocontext.EoContext) error {
httpCtx, err := http_context.Assert(ctx)
if err != nil {
return err
}
body := httpCtx.Response().GetBody()
if len(body) < 1 {
return nil
}
for _, e := range h.maskExecutors {
body, err = e.Exec(body)
if err != nil {
log.Errorf("response mask exec error: (%v),handler name: (%s),rule: (%s)", err, h.name, e.String())
continue
}
}
httpCtx.Response().SetBody(body)
return nil
}

View File

@@ -0,0 +1,22 @@
package mask
type DataMask struct {
Rules []*Rule `json:"rules"`
}
type Rule struct {
Match *BasicItem `json:"match"`
Mask *Mask `json:"mask"`
}
type BasicItem struct {
Type string `json:"type"`
Value string `json:"value"`
}
type Mask struct {
Type string `json:"type"`
Begin int `json:"begin"`
Length int `json:"length"`
Replace *BasicItem `json:"replace"`
}

View File

@@ -0,0 +1,63 @@
package inner
import (
"regexp"
"unicode"
"github.com/eolinker/apinto/drivers/strategy/data-mask-strategy/mask"
)
var cardRegex = regexp.MustCompile(`[a-zA-Z0-9]{0,1}\d{16,19}[a-zA-Z0-9]{0,1}`)
func validateLuhn(cardNumber string) bool {
var sum int
// 反向遍历卡号字符串
nDigits := len(cardNumber)
parity := nDigits % 2
for i, r := range cardNumber {
if !unicode.IsDigit(r) {
return false // 如果包含非数字字符,则为无效卡号
}
digit := int(r - '0') // 将字符转为数字
// 根据Luhn算法当索引与奇偶性不同时数字需要翻倍
if i%2 == parity {
digit *= 2
}
// 如果数字大于9减去9
if digit > 9 {
digit -= 9
}
sum += digit
}
// 卡号有效性取决于sum是否可以被10整除
return sum%10 == 0
}
func newBankCardMaskDriver(maskFunc mask.MaskFunc) mask.IInnerMask {
return newCommonMaskDriver(func(origin string) string {
// 判断是否是合法的银行卡号,若非合法,则不做处理,避免误判
if !validateLuhn(origin) {
return origin
}
size := len(origin)
if size > 19 {
return origin
}
arr := []byte{
origin[0],
origin[1],
}
for _, o := range arr {
if o < 48 || o > 57 {
return origin
}
}
return maskFunc(origin)
}, []*regexp.Regexp{cardRegex})
}

View File

@@ -0,0 +1,63 @@
package inner
import (
"regexp"
"strconv"
"strings"
"github.com/eolinker/apinto/drivers/strategy/data-mask-strategy/mask"
)
var (
idCardRegx = regexp.MustCompile(`[a-zA-Z0-9]{0,1}[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx][a-zA-Z0-9]{0,1}`)
//idCardRegx = regexp.MustCompile(`[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]`)
)
// 权重系数
var weight = []int{7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}
// 校验码对应表
var checkCodeMap = []string{"1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"}
// 验证身份证号的最后一位校验码
func validateIDCard(id string) bool {
// 身份证必须为18位
if len(id) != 18 {
return false
}
// 前17位必须都是数字
id17 := id[:17]
if _, err := strconv.Atoi(id17); err != nil {
return false
}
// 计算加权和
sum := 0
for i := 0; i < 17; i++ {
num, _ := strconv.Atoi(string(id[i]))
sum += num * weight[i]
}
// 求模11的余数
mod := sum % 11
// 校验码
checkCode := checkCodeMap[mod]
// 比较第18位是否匹配
return strings.ToUpper(string(id[17])) == checkCode
}
func newIDCardMaskDriver(maskFunc mask.MaskFunc) mask.IInnerMask {
return newCommonMaskDriver(func(origin string) string {
if len(origin) != 18 {
return origin
}
// 判断是否是合法的身份证号,若非合法,则不做处理,避免误判
if !validateIDCard(origin) {
return origin
}
return maskFunc(origin)
}, []*regexp.Regexp{idCardRegx})
}

View File

@@ -0,0 +1,124 @@
package inner
import (
"fmt"
"regexp"
"github.com/eolinker/apinto/drivers/strategy/data-mask-strategy/mask"
)
func Register() {
mask.RegisterMaskFactory(mask.MatchInner, &factory{})
}
type factory struct {
}
func (i *factory) Create(cfg *mask.Rule, maskFunc mask.MaskFunc) (mask.IMaskDriver, error) {
return newDriver(cfg, maskFunc)
}
var _ mask.IMaskDriver = (*driver)(nil)
type driver struct {
value string
maskCfg *mask.Mask
mask.IInnerMask
}
func newDriver(cfg *mask.Rule, maskFunc mask.MaskFunc) (mask.IMaskDriver, error) {
var innerMask mask.IInnerMask
switch cfg.Match.Value {
case mask.MatchInnerValueName:
innerMask = NewNameMaskDriver(maskFunc)
case mask.MatchInnerValuePhone:
innerMask = newPhoneMaskDriver(maskFunc)
case mask.MatchInnerValueIDCard:
innerMask = newIDCardMaskDriver(maskFunc)
case mask.MatchInnerValueBankCard:
innerMask = newBankCardMaskDriver(maskFunc)
case mask.MatchInnerValueAmount:
innerMask = newAmountMaskDriver(maskFunc)
case mask.MatchInnerValueDate:
innerMask = newDateMaskDriver(maskFunc)
default:
return nil, fmt.Errorf("invalid inner value: %s", cfg.Match.Value)
}
return &driver{
value: cfg.Match.Value,
maskCfg: cfg.Mask,
IInnerMask: innerMask,
}, nil
}
func (k *driver) Exec(body []byte) ([]byte, error) {
if k.IInnerMask == nil {
return body, nil
}
return k.IInnerMask.Exec(body)
}
func (k *driver) String() string {
return fmt.Sprintf("mask driver: inner,value: %s,detail: %v", k.value, k.maskCfg)
}
var (
moneyRegex = regexp.MustCompile(`^(-?\d+)(\.\d{1,2})?$`)
dateTimeRegex = regexp.MustCompile(`^(\d{4})[-/.](0[1-9]|1[0-2])[-/.](0[1-9]|[12][0-9]|3[01])(?:[\sT](\d{2}):([0-5][0-9])(:([0-5][0-9]))?)?$`)
)
func newAmountMaskDriver(maskFunc mask.MaskFunc) mask.IInnerMask {
return newCommonMaskDriver(maskFunc, []*regexp.Regexp{moneyRegex})
}
func newDateMaskDriver(maskFunc mask.MaskFunc) mask.IInnerMask {
return newCommonMaskDriver(maskFunc, []*regexp.Regexp{dateTimeRegex})
}
type commonMaskDriver struct {
mask.MaskFunc
regexps []*regexp.Regexp
}
func newCommonMaskDriver(maskFunc mask.MaskFunc, regexps []*regexp.Regexp) *commonMaskDriver {
return &commonMaskDriver{MaskFunc: maskFunc, regexps: regexps}
}
func (i *commonMaskDriver) Exec(body []byte) ([]byte, error) {
if i.MaskFunc == nil || i.regexps == nil {
return body, nil
}
for _, re := range i.regexps {
body = re.ReplaceAllFunc(body, func(bytes []byte) []byte {
return []byte(i.MaskFunc(string(bytes)))
})
}
return body, nil
}
// 定义递归函数来遍历并修改包含 `key` 字段的部分
func traverseAndModify(data interface{}, keys []string, f mask.MaskFunc) {
// 根据不同类型处理
switch value := data.(type) {
case map[string]interface{}:
// 如果是 map检查是否包含指定字段
for _, key := range keys {
if val, exist := value[key]; exist {
if valStr, ok := val.(string); ok {
value[key] = f(valStr)
}
}
}
// 继续遍历嵌套的字段
for _, v := range value {
traverseAndModify(v, keys, f)
}
case []interface{}:
// 如果是数组,遍历每个元素
for _, v := range value {
traverseAndModify(v, keys, f)
}
}
}

View File

@@ -0,0 +1,246 @@
package inner
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func mockMaskFunc(input string) string {
if input == "" {
return ""
}
return "MASKED"
}
func TestNameMaskDriver(t *testing.T) {
driver := NewNameMaskDriver(mockMaskFunc)
tests := []struct {
input string
output string
}{
{`{"name": "John Doe"}`, `{"name":"MASKED"}`},
{`{"cname": "Jane Doe"}`, `{"cname":"MASKED"}`},
}
for _, tt := range tests {
masked, err := driver.Exec([]byte(tt.input))
assert.NoError(t, err)
assert.JSONEq(t, tt.output, string(masked))
}
}
func TestPhoneMaskDriver(t *testing.T) {
driver := newPhoneMaskDriver(mockMaskFunc)
tests := []struct {
input string
output string
}{
{`12345678901`, "12345678901"},
{`13456278901`, "MASKED"},
{`19876543210`, "MASKED"},
{`198765432102`, "198765432102"},
{`19876543210a`, "19876543210a"},
{"normal text", "normal text"},
}
for _, tt := range tests {
masked, err := driver.Exec([]byte(tt.input))
assert.NoError(t, err)
assert.Equal(t, tt.output, string(masked))
}
}
func TestIDCardMaskDriver(t *testing.T) {
driver := newIDCardMaskDriver(mockMaskFunc)
tests := []struct {
input string
output string
}{
{`12345678901234567X`, "12345678901234567X"}, // Assuming valid ID for mock
{`invalid-idformat`, "invalid-idformat"},
}
for _, tt := range tests {
masked, err := driver.Exec([]byte(tt.input))
assert.NoError(t, err)
assert.Equal(t, tt.output, string(masked))
}
}
func TestBankCardMaskDriver(t *testing.T) {
driver := newBankCardMaskDriver(mockMaskFunc)
tests := []struct {
input string
output string
}{
{`1234567890123456`, "1234567890123456"}, // Assuming valid card number for mock
{`invalid-card`, "invalid-card"},
}
for _, tt := range tests {
masked, err := driver.Exec([]byte(tt.input))
assert.NoError(t, err)
assert.Equal(t, tt.output, string(masked))
}
}
func TestAmountMaskDriver(t *testing.T) {
driver := newAmountMaskDriver(mockMaskFunc)
tests := []struct {
input string
output string
}{
{`123.45`, "MASKED"}, // Assuming this is valid for testing
{`not-amount`, "not-amount"},
}
for _, tt := range tests {
masked, err := driver.Exec([]byte(tt.input))
assert.NoError(t, err)
assert.Equal(t, tt.output, string(masked))
}
}
func TestDateMaskDriver(t *testing.T) {
driver := newDateMaskDriver(mockMaskFunc)
tests := []struct {
input string
output string
}{
{`2023-01-01`, "MASKED"}, // Assuming this is valid for testing
{`not-date`, "not-date"},
}
for _, tt := range tests {
masked, err := driver.Exec([]byte(tt.input))
assert.NoError(t, err)
assert.Equal(t, tt.output, string(masked))
}
}
func TestNameMaskDriver_LongText(t *testing.T) {
driver := NewNameMaskDriver(mockMaskFunc)
tests := []struct {
input string
output string
}{
{`{"name": "Johnathon Maximillian Doe the Third"}`, `{"name":"MASKED"}`},
{`{"normal_text": "This is a sentence with a name Johnathan Does embedded"}`, `{"normal_text":"This is a sentence with a name Johnathan Does embedded"}`},
{`{"name": "", "cname": ""}`, `{"name":"", "cname":""}`},
}
for _, tt := range tests {
masked, err := driver.Exec([]byte(tt.input))
assert.NoError(t, err)
assert.JSONEq(t, tt.output, string(masked))
}
}
func TestPhoneMaskDriver_LongText(t *testing.T) {
driver := newPhoneMaskDriver(mockMaskFunc)
tests := []struct {
input string
output string
}{
{strings.Repeat("1", 50) + "3987654321" + strings.Repeat("0", 50), strings.Repeat("1", 50) + "3987654321" + strings.Repeat("0", 50)},
{"Contact numbers: 13987654321, 15800001111", "Contact numbers: MASKED, MASKED"},
{strings.Repeat("normal-text", 100), strings.Repeat("normal-text", 100)},
{"Dialing sequence: 08005555555", "Dialing sequence: 08005555555"},
}
for _, tt := range tests {
masked, err := driver.Exec([]byte(tt.input))
assert.NoError(t, err)
assert.Equal(t, tt.output, string(masked))
}
}
func TestIDCardMaskDriver_LongText(t *testing.T) {
driver := newIDCardMaskDriver(mockMaskFunc)
tests := []struct {
input string
output string
}{
{strings.Repeat("1", 50) + "12345678901234567X" + strings.Repeat("X", 50), strings.Repeat("1", 50) + "12345678901234567X" + strings.Repeat("X", 50)},
{strings.Repeat("normal-text", 100), strings.Repeat("normal-text", 100)},
{"Incorrect format: 123-456-789-00", "Incorrect format: 123-456-789-00"},
}
for _, tt := range tests {
masked, err := driver.Exec([]byte(tt.input))
assert.NoError(t, err)
assert.Equal(t, tt.output, string(masked))
}
}
func TestBankCardMaskDriver_LongText(t *testing.T) {
driver := newBankCardMaskDriver(mockMaskFunc)
tests := []struct {
input string
output string
}{
{strings.Repeat("1", 50) + "1234567890123456" + strings.Repeat("9", 50), strings.Repeat("1", 50) + "1234567890123456" + strings.Repeat("9", 50)},
{"Valid card numbers: dasdw, 8765432187654321", "Valid card numbers: MASKED, MASKED"},
{strings.Repeat("normal-text", 100), strings.Repeat("normal-text", 100)},
{"Fake card number: 1111-2222-3333-4444", "Fake card number: 1111-2222-3333-4444"},
}
for _, tt := range tests {
masked, err := driver.Exec([]byte(tt.input))
assert.NoError(t, err)
assert.Equal(t, tt.output, string(masked))
}
}
func TestAmountMaskDriver_LongText(t *testing.T) {
driver := newAmountMaskDriver(mockMaskFunc)
tests := []struct {
input string
output string
}{
{"Invoice total: $1234567890.99", "MASKED"},
{"Transaction amounts: $12345.67, fee: $0.02", "MASKED, fee: MASKED"},
{strings.Repeat("normal-text", 100), strings.Repeat("normal-text", 100)},
{"Plain text values: twenty five dollars", "Plain text values: twenty five dollars"},
}
for _, tt := range tests {
masked, err := driver.Exec([]byte(tt.input))
assert.NoError(t, err)
assert.Equal(t, tt.output, string(masked))
}
}
func TestDateMaskDriver_LongText(t *testing.T) {
driver := newDateMaskDriver(mockMaskFunc)
tests := []struct {
input string
output string
}{
{"Event date is 2077-12-31 extended overlap text", "MASKED extended overlap text"},
{"Date strings: 2025-09-24T13:45:00, obsolete: 1912-04-15T00:00:00", "MASKED, obsolete: MASKED"},
{strings.Repeat("normal-text", 100), strings.Repeat("normal-text", 100)},
{"Formatted text: the year nineteen ninety nine", "Formatted text: the year nineteen ninety nine"},
}
for _, tt := range tests {
masked, err := driver.Exec([]byte(tt.input))
assert.NoError(t, err)
assert.Equal(t, tt.output, string(masked))
}
}

View File

@@ -0,0 +1,32 @@
package inner
import (
"encoding/json"
"github.com/eolinker/apinto/drivers/strategy/data-mask-strategy/mask"
)
type nameMaskDriver struct {
maskFunc mask.MaskFunc
}
func NewNameMaskDriver(maskFunc mask.MaskFunc) mask.IInnerMask {
return &nameMaskDriver{maskFunc: maskFunc}
}
func (i *nameMaskDriver) Exec(body []byte) ([]byte, error) {
if i.maskFunc == nil {
// 未设置脱敏方法,不做处理
return body, nil
}
var jsonData interface{}
err := json.Unmarshal(body, &jsonData)
if err == nil {
traverseAndModify(jsonData, []string{
"name", "cname",
}, i.maskFunc)
return json.Marshal(jsonData)
}
return body, nil
}

View File

@@ -0,0 +1,37 @@
package inner
import (
"regexp"
"github.com/eolinker/apinto/drivers/strategy/data-mask-strategy/mask"
)
var (
phoneRegx = regexp.MustCompile(`1[3-9]\d{9}`)
fatherPhoneRegx = regexp.MustCompile(`[a-zA-Z0-9]{0,1}\d{11}[a-zA-Z0-9]{0,1}`)
)
func newPhoneMaskDriver(maskFunc mask.MaskFunc) mask.IInnerMask {
return &phoneMaskDriver{
MaskFunc: maskFunc,
}
}
type phoneMaskDriver struct {
mask.MaskFunc
}
func (i *phoneMaskDriver) Exec(body []byte) ([]byte, error) {
if i.MaskFunc == nil {
return body, nil
}
body = fatherPhoneRegx.ReplaceAllFunc(body, func(b []byte) []byte {
if len(b) > 11 || !phoneRegx.Match(b) {
return b
}
return []byte(i.MaskFunc(string(b)))
})
return body, nil
}

View File

@@ -0,0 +1,68 @@
package json_path
import (
"fmt"
"github.com/eolinker/apinto/drivers/strategy/data-mask-strategy/mask"
"github.com/ohler55/ojg/jp"
"github.com/ohler55/ojg/oj"
)
func Register() {
mask.RegisterMaskFactory(mask.MatchJsonPath, &factory{})
}
type factory struct {
}
func (j *factory) Create(cfg *mask.Rule, maskFunc mask.MaskFunc) (mask.IMaskDriver, error) {
return newDriver(cfg, maskFunc)
}
var _ mask.IMaskDriver = (*driver)(nil)
type driver struct {
value string
maskCfg *mask.Mask
mask.MaskFunc
expr jp.Expr
}
func newDriver(cfg *mask.Rule, maskFunc mask.MaskFunc) (mask.IMaskDriver, error) {
expr, err := jp.ParseString(cfg.Match.Value)
if err != nil {
return nil, err
}
return &driver{
value: cfg.Match.Value,
maskCfg: cfg.Mask,
MaskFunc: maskFunc,
expr: expr,
}, nil
}
func (k *driver) Exec(body []byte) ([]byte, error) {
if k.MaskFunc == nil || k.expr == nil {
return body, nil
}
n, err := oj.Parse(body)
if err != nil {
return nil, err
}
result := k.expr.Get(n)
if len(result) > 0 {
val, ok := result[0].(string)
if ok {
k.expr.Set(n, k.MaskFunc(val))
}
}
return oj.Marshal(n)
}
func (k *driver) String() string {
return fmt.Sprintf("mask driver: json-path,value: %s,detail: %v", k.value, k.maskCfg)
}

View File

@@ -0,0 +1,7 @@
package json_path
import "testing"
func TestJsonPath(t *testing.T) {
}

View File

@@ -0,0 +1,47 @@
package keyword
import (
"fmt"
"strings"
"github.com/eolinker/apinto/drivers/strategy/data-mask-strategy/mask"
)
func Register() {
mask.RegisterMaskFactory(mask.MatchKeyword, &factory{})
}
type factory struct {
}
func (k *factory) Create(cfg *mask.Rule, maskFunc mask.MaskFunc) (mask.IMaskDriver, error) {
return NewKeywordMaskDriver(cfg, maskFunc)
}
var _ mask.IMaskDriver = (*driver)(nil)
type driver struct {
value string
maskCfg *mask.Mask
mask.MaskFunc
}
func NewKeywordMaskDriver(cfg *mask.Rule, maskFunc mask.MaskFunc) (*driver, error) {
return &driver{
value: cfg.Match.Value,
maskCfg: cfg.Mask,
MaskFunc: maskFunc,
}, nil
}
func (k *driver) Exec(body []byte) ([]byte, error) {
if k.MaskFunc == nil {
return body, nil
}
target := strings.Replace(string(body), k.value, k.MaskFunc(k.value), -1)
return []byte(target), nil
}
func (k *driver) String() string {
return fmt.Sprintf("mask driver: keyword,value: %s,detail: %v", k.value, k.maskCfg)
}

View File

@@ -0,0 +1,43 @@
package mask
import (
"github.com/eolinker/eosc"
)
var (
maskManager = NewMaskManager()
)
type IMaskFactory interface {
Create(cfg *Rule, maskFunc MaskFunc) (IMaskDriver, error)
}
type MaskManager struct {
factories eosc.Untyped[string, IMaskFactory]
}
func NewMaskManager() *MaskManager {
return &MaskManager{
factories: eosc.BuildUntyped[string, IMaskFactory](),
}
}
func (m *MaskManager) Register(name string, driver IMaskFactory) {
m.factories.Set(name, driver)
}
func (m *MaskManager) Get(name string) (IMaskFactory, bool) {
return m.factories.Get(name)
}
func (m *MaskManager) Del(name string) {
m.factories.Del(name)
}
func RegisterMaskFactory(name string, factory IMaskFactory) {
maskManager.Register(name, factory)
}
func GetMaskFactory(name string) (IMaskFactory, bool) {
return maskManager.Get(name)
}

View File

@@ -0,0 +1,188 @@
package mask
import (
"fmt"
"math/rand"
"strings"
"time"
)
type IMaskDriver interface {
Exec(body []byte) ([]byte, error)
String() string
}
type MaskFunc func(origin string) string
var (
maskByte = '*'
)
const (
MaskPartialDisplay = "partial-display"
MaskPartialMask = "partial-masking"
MaskTruncation = "truncation"
MaskReplacement = "replacement"
MaskShuffling = "shuffling"
)
func GenMaskFunc(cfg *Mask) (MaskFunc, error) {
switch cfg.Type {
case MaskPartialDisplay:
return partialDisplay(cfg.Begin, cfg.Length), nil
case MaskPartialMask:
return partialMasking(cfg.Begin, cfg.Length), nil
case MaskTruncation:
return truncation(cfg.Begin, cfg.Length), nil
case MaskReplacement:
if cfg.Replace == nil {
return nil, fmt.Errorf("replace is nil")
}
return replacement(cfg.Replace.Type, cfg.Replace.Value)
case MaskShuffling:
return shuffling(cfg.Begin, cfg.Length), nil
default:
return nil, fmt.Errorf("unknown mask type %s", cfg.Type)
}
}
func partialDisplay(begin int, length int) MaskFunc {
return func(origin string) string {
target := strings.Builder{}
runes := []rune(origin)
size := len(runes)
if begin > size {
for i := 0; i < size; i++ {
target.WriteRune(maskByte)
}
} else if length == -1 || begin+length > size {
for i := 0; i < begin; i++ {
target.WriteRune(maskByte)
}
for i := begin; i < size; i++ {
target.WriteRune(runes[i])
}
} else {
for i := 0; i < begin; i++ {
target.WriteRune(maskByte)
}
for i := begin; i < begin+length; i++ {
target.WriteRune(runes[i])
}
for i := begin + length; i < size; i++ {
target.WriteRune(maskByte)
}
}
return target.String()
}
}
func partialMasking(begin int, length int) MaskFunc {
return func(origin string) string {
target := strings.Builder{}
runes := []rune(origin)
size := len(runes)
if begin > size {
return origin
} else if length == -1 || begin+length > size {
for i := 0; i < begin; i++ {
target.WriteRune(runes[i])
}
for i := begin; i < size; i++ {
target.WriteRune(maskByte)
}
} else {
for i := 0; i < begin; i++ {
target.WriteRune(runes[i])
}
for i := begin; i < begin+length; i++ {
target.WriteRune(maskByte)
}
for i := begin + length; i < size; i++ {
target.WriteRune(runes[i])
}
}
return target.String()
}
}
func truncation(begin int, length int) MaskFunc {
return func(origin string) string {
target := strings.Builder{}
runes := []rune(origin)
size := len(runes)
if begin > size {
return ""
} else if length == -1 || begin+length > size {
for i := begin; i < size; i++ {
target.WriteRune(runes[i])
}
} else {
for i := begin; i < begin+length; i++ {
target.WriteRune(runes[i])
}
}
return target.String()
}
}
const (
ReplaceRandom = "random"
ReplaceCustom = "custom"
)
func replacement(replaceType string, value string) (MaskFunc, error) {
switch replaceType {
case ReplaceRandom:
return func(origin string) string {
return replaceWithRandomString(origin)
}, nil
case ReplaceCustom:
return func(origin string) string {
return value
}, nil
default:
return nil, fmt.Errorf("unknown replace type %s", replaceType)
}
}
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
// 随机生成指定长度的字符串
func randomString(length int) string {
// 随机种子
rand.Seed(time.Now().UnixNano())
// 创建一个切片,用于存储生成的随机字符
result := make([]byte, length)
// 根据长度逐个生成随机字符
for i := range result {
result[i] = charset[rand.Intn(len(charset))]
}
return string(result)
}
// 将原字符串替换为随机生成的小写字母和数字的字符串
func replaceWithRandomString(text string) string {
// 计算原字符串的长度
length := len([]rune(text)) // 处理多字节字符
// 生成与原字符串长度一致的随机字符串
return randomString(length)
}
func shuffling(begin int, length int) MaskFunc {
return func(origin string) string {
runes := []rune(origin)
// 设置随机数种子,以保证每次运行结果不同
rand.Seed(time.Now().UnixNano())
// 使用 Shuffle 函数打乱 nums 切片的顺序
rand.Shuffle(len(runes), func(i, j int) {
runes[i], runes[j] = runes[j], runes[i] // 交换切片中第 i 和 j 个元素
})
return string(runes)
}
}

View File

@@ -0,0 +1,113 @@
package mask
import (
"strings"
"testing"
)
func TestPartialDisplay(t *testing.T) {
tests := []struct {
origin string
begin int
length int
expected string
}{
{"abcdef", 2, 3, "**cde*"},
{"abcdef", 0, 4, "abcd**"},
{"abcdef", 10, 2, "******"},
{"abcdef", 2, -1, "**cdef"},
}
for _, test := range tests {
f := partialDisplay(test.begin, test.length)
result := f(test.origin)
if result != test.expected {
t.Errorf("Expected %s but got %s", test.expected, result)
}
}
}
func TestPartialMask(t *testing.T) {
tests := []struct {
origin string
begin int
length int
expected string
}{
{"abcdef", 2, 3, "ab***f"},
{"abcdef", 0, 4, "****ef"},
{"abcdef", 10, 2, "abcdef"},
{"abcdef", 2, -1, "ab****"},
}
for _, test := range tests {
f := partialMasking(test.begin, test.length)
result := f(test.origin)
if result != test.expected {
t.Errorf("Expected %s but got %s", test.expected, result)
}
}
}
func TestTruncation(t *testing.T) {
tests := []struct {
origin string
begin int
length int
expected string
}{
{"abcdef", 2, 3, "cde"},
{"abcdef", 0, 4, "abcd"},
{"abcdef", 10, 2, ""},
{"abcdef", 2, -1, "cdef"},
}
for _, test := range tests {
f := truncation(test.begin, test.length)
result := f(test.origin)
if result != test.expected {
t.Errorf("Expected %s but got %s", test.expected, result)
}
}
}
func TestReplacement(t *testing.T) {
randomFunc, _ := replacement(ReplaceRandom, "")
customFunc, _ := replacement(ReplaceCustom, "custom")
tests := []struct {
origin string
maskFunc MaskFunc
funcType string
expected string
}{
{"abcdef", randomFunc, ReplaceRandom, ""},
{"abcdef", customFunc, ReplaceCustom, "custom"},
}
for _, test := range tests {
result := test.maskFunc(test.origin)
// For random, just check length
if test.funcType == ReplaceRandom {
if len(result) != len(test.origin) {
t.Errorf("Expected length %d but got %d", len(test.origin), len(result))
}
} else {
if result != test.expected {
t.Errorf("Expected %s but got %s", test.expected, result)
}
}
}
}
func TestShuffling(t *testing.T) {
origin := "abcdef"
f := shuffling(0, len(origin))
result := f(origin)
if len(result) != len(origin) {
t.Errorf("Expected length %d but got %d", len(origin), len(result))
}
// Check that all original characters are present
for _, char := range origin {
if strings.Count(result, string(char)) != strings.Count(origin, string(char)) {
t.Errorf("Character %c count mismatch", char)
}
}
}

View File

@@ -0,0 +1,21 @@
package mask
const (
MatchInner = "inner"
MatchKeyword = "keyword"
MatchRegex = "regex"
MatchJsonPath = "json_path"
)
const (
MatchInnerValueName = "name"
MatchInnerValuePhone = "phone"
MatchInnerValueIDCard = "id-card"
MatchInnerValueBankCard = "bank-card"
MatchInnerValueDate = "date"
MatchInnerValueAmount = "amount"
)
type IInnerMask interface {
Exec(body []byte) ([]byte, error)
}

View File

@@ -0,0 +1,49 @@
package regex
import (
"fmt"
"regexp"
"github.com/eolinker/apinto/drivers/strategy/data-mask-strategy/mask"
)
func Register() {
mask.RegisterMaskFactory(mask.MatchRegex, &factory{})
}
type factory struct {
}
func (r *factory) Create(cfg *mask.Rule, maskFunc mask.MaskFunc) (mask.IMaskDriver, error) {
return newDriver(cfg, maskFunc)
}
var _ mask.IMaskDriver = (*driver)(nil)
type driver struct {
value string
maskCfg *mask.Mask
mask.MaskFunc
regexp *regexp.Regexp
}
func newDriver(cfg *mask.Rule, maskFunc mask.MaskFunc) (*driver, error) {
return &driver{
value: cfg.Match.Value,
maskCfg: cfg.Mask,
MaskFunc: maskFunc,
regexp: regexp.MustCompile(cfg.Match.Value),
}, nil
}
func (k *driver) Exec(body []byte) ([]byte, error) {
if k.MaskFunc == nil || k.regexp == nil {
return body, nil
}
target := k.regexp.ReplaceAllStringFunc(string(body), k.MaskFunc)
return []byte(target), nil
}
func (k *driver) String() string {
return fmt.Sprintf("mask driver: regex,value: %s,detail: %v", k.value, k.maskCfg)
}

3
go.mod
View File

@@ -27,6 +27,7 @@ require (
github.com/polarismesh/polaris-go v1.1.0
github.com/robertkrimen/otto v0.0.0-20211024170158-b87d35c0b86f
github.com/soheilhy/cmux v0.1.5
github.com/stretchr/testify v1.9.0
github.com/urfave/cli/v2 v2.23.4
github.com/valyala/fasthttp v1.47.0
golang.org/x/crypto v0.21.0
@@ -79,6 +80,7 @@ require (
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pelletier/go-toml v1.7.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/statsd_exporter v0.21.0 // indirect
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect
@@ -94,6 +96,7 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect
gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (