feat: add repository permission role role_permission user_role table

This commit is contained in:
王性驊 2025-02-26 16:45:27 +08:00
parent 547b3b06dd
commit cccad97512
34 changed files with 3341 additions and 149 deletions

View File

@ -49,12 +49,13 @@ build-docker:
.PHONY: mock-gen
mock-gen: # 建立 mock 資料
mockgen -source=./pkg/domain/repository/token.go -destination=./pkg/mock/repository/token.go -package=mock
mockgen -source=./pkg/domain/repository/permission.go -destination=./pkg/mock/repository/permission.go -package=mock
mockgen -source=./pkg/domain/repository/role.go -destination=./pkg/mock/repository/role.go -package=mock
mockgen -source=./pkg/domain/repository/role_permission.go -destination=./pkg/mock/repository/role_permission.go -package=mock
mockgen -source=./pkg/domain/repository/user_role.go -destination=./pkg/mock/repository/user_role.go -package=mock
@echo "Generate mock files successfully"
@echo "Generate mock files successfully"
.PHONY: migrate-database
migrate-database:
migrate -source file://generate/database/mysql -database 'mysql://root:yytt@tcp(127.0.0.1:3306)/digimon_permission' up

33
go.mod
View File

@ -10,6 +10,7 @@ require (
github.com/golang-jwt/jwt/v4 v4.5.1
github.com/segmentio/ksuid v1.0.4
github.com/stretchr/testify v1.10.0
github.com/testcontainers/testcontainers-go v0.34.0
github.com/zeromicro/go-zero v1.8.0
go.mongodb.org/mongo-driver v1.17.2
go.uber.org/mock v0.5.0
@ -18,20 +19,33 @@ require (
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
github.com/casbin/govaluate v1.3.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/containerd v1.7.18 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v27.1.1+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
@ -47,32 +61,51 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/openzipkin/zipkin-go v0.4.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/redis/go-redis/v9 v9.7.0 // indirect
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.etcd.io/etcd/api/v3 v3.5.15 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect
go.etcd.io/etcd/client/v3 v3.5.15 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect

81
go.sum
View File

@ -2,6 +2,14 @@ code.30cm.net/digimon/library-go/errs v1.2.14 h1:Un9wcIIjjJW8D2i0ISf8ibzp9oNT4Oq
code.30cm.net/digimon/library-go/errs v1.2.14/go.mod h1:Hs4v7SbXNggDVBGXSYsFMjkii1qLF+rugrIpWePN4/o=
code.30cm.net/digimon/library-go/mongo v0.0.9 h1:fPciIE5B85tXpLg8aeVQqKVbLnfpVAk9xbMu7pE2tVw=
code.30cm.net/digimon/library-go/mongo v0.0.9/go.mod h1:KBVKz/Ci5IheI77BgZxPUeKkaGvDy8fV8EDHSCOLIO4=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0=
@ -24,26 +32,48 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao=
github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
@ -67,6 +97,7 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@ -98,6 +129,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -105,6 +140,16 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -112,12 +157,18 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
@ -126,6 +177,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
@ -142,8 +195,16 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@ -154,6 +215,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
@ -161,6 +223,12 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/testcontainers/testcontainers-go v0.34.0 h1:5fbgF0vIN5u+nD3IWabQwRybuB4GY8G2HHgCkbMzMHo=
github.com/testcontainers/testcontainers-go v0.34.0/go.mod h1:6P/kMkQe8yqPHfPWNulFGdFHTD8HB2vLq/231xY2iPQ=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
@ -175,6 +243,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zeromicro/go-zero v1.8.0 h1:4g/8VW+fOyM51HZYPeI3mXIZdEX+Fl6SsdYX2H5PYw4=
github.com/zeromicro/go-zero v1.8.0/go.mod h1:xDBF+/iDzj30zPvu6HNUIbpz1J6+/g3Sx9D/DytJfss=
go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk=
@ -187,6 +257,8 @@ go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793Sqyh
go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
@ -254,15 +326,22 @@ golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@ -311,6 +390,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw=
k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80=
k8s.io/apimachinery v0.29.4 h1:RaFdJiDmuKs/8cm1M6Dh1Kvyh59YQFDcFuFTSmXes6Q=

View File

@ -1,28 +0,0 @@
package entity
import (
"go.mongodb.org/mongo-driver/bson/primitive"
)
//TODO 未來才實作要做IAM 時)
// Policy 表示一個存取控制策略文件
type Policy struct {
ID primitive.ObjectID `bson:"_id,omitempty"` // MongoDB 的主鍵
Name string `bson:"name"` // 策略名稱
Description string `bson:"description,omitempty"` // 策略描述
Effect string `bson:"effect"` // "allow" 或 "deny"
Condition interface{} `bson:"condition,omitempty"` // 存放 JSON 條件,可以是 map[string]interface{} 或其他符合 BSON 格式的資料
CreatedAt int64 `bson:"created_at"` // 建立時間
UpdatedAt int64 `bson:"updated_at"` // 更新時間
}
// Resource 表示需要保護的資源
type Resource struct {
ID primitive.ObjectID `bson:"_id,omitempty"` // MongoDB 的主鍵
Name string `bson:"name"` // 資源名稱
Type string `bson:"type"` // 資源類型
Description string `bson:"description,omitempty"` // 資源描述
CreatedAt int64 `bson:"created_at"` // 建立時間
UpdatedAt int64 `bson:"updated_at"` // 更新時間
}

View File

@ -5,9 +5,30 @@ import (
"go.mongodb.org/mongo-driver/bson/primitive"
)
//
//1. UID (使用者ID)
//
//這個欄位通常代表創建這個角色的使用者,可能的用途:
//• 追蹤角色的建立者:確保知道誰創建了這個角色。
//• 限定角色的管理範圍:例如,只有 UID 對應的使用者可以修改或刪除這個角色。
//• 支援多租戶 (Multi-Tenancy):如果一個 UID 只能看到自己創建的角色,那這個系統可能是多租戶架構的一部分。
//
//2. ClientID (客戶端ID)
//
//這個欄位通常代表該角色所屬的應用程式或客戶端,可能的用途:
//• 多租戶架構 (Multi-Tenancy):不同 ClientID 的角色可能互相隔離,確保不同組織不會影響彼此的角色權限。
//• 對應 OAuth 2.0 或 OpenID Connect
//• 在 OAuth2 / OIDC 的架構下,每個應用程式 (client_id) 可能有不同的角色和權限。
//• 例如client_id 為 web-app-1 的角色與 client_id 為 mobile-app-1 的角色可能完全不同。
//• API 權限控制:
//• ClientID 可能是用來限制某些角色只能在特定應用程式中被使用,例如 Web 端和行動端的角色不同。
// Role 是這樣,如果有綁定某個 UID
type Role struct {
ID primitive.ObjectID `bson:"_id,omitempty"`
Name string `bson:"name"`
Name string `bson:"name"` // 角色名稱
UID string `bson:"uid"`
ClientID string `bson:"client_id"`
Status permission.Status `bson:"status"` // 例如 1: 啟用, 0: 停用
CreateAt int64 `bson:"create_at"`
UpdateAt int64 `bson:"update_at"`

View File

@ -11,7 +11,6 @@ import (
const (
TokenServerErrorCode = 1 + iota
TokenServerRedisErrorCode
TokenValidateErrorCode
TokenClaimErrorCode
TokenCreateErrorCode
@ -19,17 +18,8 @@ const (
TokenCancelErrorCode
TokensCancelErrorCode
TokenGetErrorCode
NewOneTokenErrorCode
DelOneTokenErrorCode
SendTooShortErrorCode
SetForgetPasswordRedisErrorCode
FailedToGetCorrectVerifyCode
SendVerifyCodeRedisErrorCode
GenerateVerifyCodeRedisErrorCode
FailedToCheckVerifyCode
AccountPlatformNotCorrectErrorCode
PermissionDeleteErrorCode
FailedToGetRolePermission
)
func TokenError(ec ers.ErrorCode, s ...string) *ers.LibError {

View File

@ -8,20 +8,23 @@ const (
)
const (
ClosePermission string = "close"
OpenPermission string = "open"
ClosePermission StatusCode = "close"
OpenPermission StatusCode = "open"
)
func (s Status) String() string {
status, ok := statusMap[s]
if ok {
return status
return string(status)
}
return ClosePermission
return string(ClosePermission)
}
var statusMap = map[Status]string{
var statusMap = map[Status]StatusCode{
Open: OpenPermission,
Close: ClosePermission,
}
type StatusCode string
type Permissions map[string]StatusCode

View File

@ -6,3 +6,5 @@ const (
BackendUser Type = iota + 1
FrontendUser
)
const AdminRoleUID = "GodDog"

View File

@ -24,7 +24,7 @@ func (rule Rule) ToString() []string {
rule.Field2, rule.Field3, rule.Field4, rule.Field5,
}
// 移除空字串,提高效能
var result []string
result := make([]string, 0, len(fields))
for _, field := range fields {
if field != "" {
result = append(result, field)

View File

@ -0,0 +1,121 @@
package rbac
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestRule_ToString(t *testing.T) {
tests := []struct {
name string
input Rule
expected []string
}{
{
name: "完整六個欄位",
input: Rule{
PolicyType: "p",
Field0: "admin",
Field1: "data1",
Field2: "read",
Field3: "extra1",
Field4: "extra2",
Field5: "extra3",
},
expected: []string{"p", "admin", "data1", "read", "extra1", "extra2", "extra3"},
},
{
name: "部分欄位空白",
input: Rule{
PolicyType: "p",
Field0: "admin",
Field1: "",
Field2: "read",
Field3: "",
Field4: "",
Field5: "extra3",
},
expected: []string{"p", "admin", "read", "extra3"},
},
{
name: "只有 PolicyType",
input: Rule{PolicyType: "p"},
expected: []string{"p"},
},
{
name: "所有欄位皆空",
input: Rule{},
expected: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.input.ToString()
assert.Equal(t, tt.expected, result, "ToString output should match expected")
})
}
}
func TestStringToPolicy(t *testing.T) {
tests := []struct {
name string
policyType string
input []string
expected Rule
}{
{
name: "完整六個欄位",
policyType: "p",
input: []string{"admin", "data1", "read", "extra1", "extra2", "extra3"},
expected: Rule{
PolicyType: "p",
Field0: "admin",
Field1: "data1",
Field2: "read",
Field3: "extra1",
Field4: "extra2",
Field5: "extra3",
},
},
{
name: "部分欄位空白",
policyType: "p",
input: []string{"admin", "", "read", "", "", "extra3"},
expected: Rule{
PolicyType: "p",
Field0: "admin",
Field1: "",
Field2: "read",
Field3: "",
Field4: "",
Field5: "extra3",
},
},
{
name: "只有 PolicyType",
policyType: "p",
input: []string{},
expected: Rule{
PolicyType: "p",
},
},
{
name: "只有兩個欄位",
policyType: "g",
input: []string{"user", "admin"},
expected: Rule{
PolicyType: "g",
Field0: "user",
Field1: "admin",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := StringToPolicy(tt.policyType, tt.input)
assert.Equal(t, tt.expected, result, "StringToPolicy output should match expected")
})
}
}

View File

@ -18,8 +18,8 @@ type PermissionRepository interface {
type GetPermission interface {
// GetAll 取得所有權限列表
GetAll(ctx context.Context, status *permission.Status) ([]entity.Permission, error)
// GetAllByID 以權限 ID 作為鍵取得所有權限的映射
GetAllByID(ctx context.Context, status *permission.Status) (map[string]entity.Permission, error)
// GetAllIntoIDMap 以權限 ID 作為鍵取得所有權限的映射
GetAllIntoIDMap(ctx context.Context, status *permission.Status) (map[string]entity.Permission, error)
// FindOne 根據查詢條件取得單筆權限資料
FindOne(ctx context.Context, query PermissionQuery) (entity.Permission, error)
// FindByNames 根據權限名稱列表查詢權限資料

View File

@ -2,41 +2,39 @@ package repository
import (
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission"
"context"
"go.mongodb.org/mongo-driver/mongo"
)
type RoleRepository interface {
Insert(ctx context.Context, role *entity.Role) error
Update(ctx context.Context, role *entity.Role) error
Delete(ctx context.Context, role *entity.Role) error
List()
ListAll()
Get()
GetByUID()
List(ctx context.Context, param ListQuery) ([]*entity.Role, int64, error)
GetByID(ctx context.Context, id string) (*entity.Role, error)
GetByUID(ctx context.Context, uid string) (*entity.Role, error)
All(ctx context.Context, clientID *string) ([]*entity.Role, error)
Create(ctx context.Context, role *entity.Role) error
Update(ctx context.Context, data UpdateReq) error
Delete(ctx context.Context, id string) error
RoleIndex
}
type RoleRepository interface {
Page(ctx context.Context, filter PageRoleFilter, page, size int) ([]entity.Role, int64, error)
Get(ctx context.Context, id int64) (entity.Role, error)
GetByUID(ctx context.Context, uid string) (entity.Role, error)
All(ctx context.Context, clientID int) ([]entity.Role, error)
IncrementID(ctx context.Context) (int, error)
Create(ctx context.Context, role *entity.Role) (int64, error)
Update(ctx context.Context, role *entity.Role) error
Delete(ctx context.Context, uid string) error
type RoleIndex interface {
Index20250224UP(ctx context.Context) (*mongo.Cursor, error)
}
type PageRoleFilter struct {
ClientID int
UID string
Name string
Permissions []string
Status int
type ListQuery struct {
PageSize int64 // 必填
PageIndex int64 // 必填
ClientID *string
UID *string
Name *string
Status *permission.Status
}
type RolePermissionRepository interface {
BindRolePermission()
UnBindRolePermission()
GetRolePermission()
GetByPermissionID()
type UpdateReq struct {
ID string
Name *string
UID *string
ClientID *string
Status *permission.Status
}

View File

@ -0,0 +1,19 @@
package repository
import (
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"context"
"go.mongodb.org/mongo-driver/mongo"
)
type RolePermissionRepository interface {
Get(ctx context.Context, roleID string) ([]*entity.RolePermission, error)
GetByPermissionID(ctx context.Context, permissionIDs []string) ([]*entity.RolePermission, error)
Create(ctx context.Context, entity entity.RolePermission) error
Delete(ctx context.Context, roleID string, permission string) error
RolePermissionIndex
}
type RolePermissionIndex interface {
Index20250225UP(ctx context.Context) (*mongo.Cursor, error)
}

View File

@ -0,0 +1,26 @@
package repository
import (
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"context"
"go.mongodb.org/mongo-driver/mongo"
)
type UserRoleRepository interface {
GetAll(ctx context.Context) ([]*entity.UserRole, error) // `All` -> `GetAll`,更直觀
GetByUserID(ctx context.Context, uid string) (entity.UserRole, error) // `Get` -> `GetByUserID`,明確是透過 `uid` 查詢
GetUsersByRoleID(ctx context.Context, roleID string) ([]entity.UserRole, error) // `GetByRoleID` -> `GetUsersByRoleID`,更具語意
CountUsersByRole(ctx context.Context) ([]RoleUserCount, error) // `UserCount` -> `CountUsersByRole`,清楚表達它是計算使用者數量
CreateUserRole(ctx context.Context, param entity.UserRole) error // `Create` -> `CreateUserRole`,明確表示新增的是使用者與角色的關係
UpdateUserRole(ctx context.Context, uid, roleID string) (entity.UserRole, error) // `Update` -> `UpdateUserRole`,明確描述更新的是哪個對象
UserRoleIndex
}
type UserRoleIndex interface {
Index20250225UP(ctx context.Context) (*mongo.Cursor, error)
}
type RoleUserCount struct {
RoleID string `bson:"_id"`
Count int `bson:"count"`
}

View File

@ -0,0 +1,54 @@
package token
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestAdditional_String(t *testing.T) {
tests := []struct {
name string
input Additional
expected string
}{
{"ID to String", ID, "id"},
{"Role to String", Role, "role"},
{"Device to String", Device, "device"},
{"UID to String", UID, "uid"},
{"Account to String", Account, "account"},
{"Scope to String", Scope, "scope"},
{"Type to String", Type, "token_type"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.input.String()
assert.Equal(t, tt.expected, result)
})
}
}
func TestIsValidAdditional(t *testing.T) {
validKeys := []Additional{
ID, Role, Device,
UID, Account, Scope, Type,
}
invalidKeys := []Additional{
"invalid", "unknown", "random", "test",
}
// 測試有效 Key
for _, key := range validKeys {
t.Run("ValidKey_"+key.String(), func(t *testing.T) {
assert.True(t, IsValidAdditional(key))
})
}
// 測試無效 Key
for _, key := range invalidKeys {
t.Run("InvalidKey_"+string(key), func(t *testing.T) {
assert.False(t, IsValidAdditional(key))
})
}
}

View File

@ -0,0 +1,27 @@
package token
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestTScope_ToString(t *testing.T) {
tests := []struct {
name string
input TScope
expected string
}{
{"Empty String", TScope(""), ""},
{"Simple String", TScope("read"), "read"},
{"Complex String", TScope("user:write"), "user:write"},
{"Special Characters", TScope("@dmin!"), "@dmin!"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 使用指標調用方法
result := tt.input.ToString()
assert.Equal(t, tt.expected, result)
})
}
}

View File

@ -0,0 +1,27 @@
package token
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestType_ToString(t *testing.T) {
tests := []struct {
name string
input VerifyType
expected string
}{
{"Empty String", VerifyType(""), ""},
{"Simple String", VerifyType("read"), "read"},
{"Complex String", VerifyType("user:write"), "user:write"},
{"Special Characters", VerifyType("@dmin!"), "@dmin!"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 使用指標調用方法
result := tt.input.ToString()
assert.Equal(t, tt.expected, result)
})
}
}

View File

@ -0,0 +1,7 @@
package usecase
import "fmt"
var (
NotFoundError = fmt.Errorf("permission not found")
)

View File

@ -0,0 +1,42 @@
package usecase
import (
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission"
"context"
)
type RoleUseCase interface {
List(ctx context.Context, param ListQuery) ([]Role, int64, error)
All(ctx context.Context, clientID *string) ([]Role, error)
GetByID(ctx context.Context, id string) (*Role, error)
GetByUID(ctx context.Context, uid string) (*Role, error)
Create(ctx context.Context, role CreateRoleReq) error
Update(ctx context.Context, id string, data CreateRoleReq) error
Delete(ctx context.Context, id string) error
}
type ListQuery struct {
PageSize int64 // 必填
PageIndex int64 // 必填
ClientID *string
UID *string
Name *string
Status *permission.Status
}
type Role struct {
ID string `json:"id"`
Name string `json:"name"` // 角色名稱
UID string `json:"uid"`
ClientID string `json:"client_id"`
Status permission.Status `json:"status"` // 例如 1: 啟用, 0: 停用
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
}
type CreateRoleReq struct {
Name *string `json:"name"` // 角色名稱
UID *string `json:"uid"`
ClientID *string `json:"client_id"`
Status *permission.Status `json:"status"` // 例如 1: 啟用, 0: 停用
}

View File

@ -0,0 +1,30 @@
package usecase
import (
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission"
"context"
)
type RolePermissionUseCase interface {
Get(ctx context.Context, roleID int64) (permission.Permissions, error)
GetByRoleUID(ctx context.Context, uid string) (permission.Permissions, error)
GetByUser(ctx context.Context, uid string) (UserPermission, error)
Create(ctx context.Context, roleID int64, permissions permission.Permissions) error
Delete(ctx context.Context, roleID int64, permissions permission.Permissions) error
List(ctx context.Context, req ListQuery) (RoleResp, error)
}
type UserPermission struct {
RoleID string `json:"role_id"`
Permissions permission.Permissions `json:"permissions"`
}
type UserRoleCountResp struct {
Role
UserCount int `json:"user_count"`
}
type RoleResp struct {
List []UserRoleCountResp `json:"list"`
Total int64 `json:"total"`
}

View File

@ -0,0 +1,21 @@
package usecase
import "context"
type UserRoleUseCase interface {
Select(ctx context.Context, filter UserRoleFilter) ([]UserRole, error)
Get(ctx context.Context, uid string) (UserRole, error)
Create(ctx context.Context, uid, roleID string) (UserRole, error)
Delete(ctx context.Context, uid, roleID string) error
}
type UserRole struct {
UID string `json:"uid"`
RoleID string `json:"role_id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
}
type UserRoleFilter struct {
RoleID string
}

View File

@ -123,7 +123,7 @@ func (repo *PermissionRepository) GetAll(ctx context.Context, status *permission
return result, nil
}
func (repo *PermissionRepository) GetAllByID(ctx context.Context, status *permission.Status) (map[string]entity.Permission, error) {
func (repo *PermissionRepository) GetAllIntoIDMap(ctx context.Context, status *permission.Status) (map[string]entity.Permission, error) {
permissions, err := repo.GetAll(ctx, status)
if err != nil {
return nil, err
@ -153,7 +153,7 @@ func (repo *PermissionRepository) FindByNames(ctx context.Context, names []strin
result := make([]entity.Permission, 0)
// 使用 $in 操作符查詢 name 在 names 切片中的文件
filter := bson.M{"name": bson.M{"$in": names}}
err := repo.DB.GetClient().FindOne(ctx, &result, filter)
err := repo.DB.GetClient().Find(ctx, &result, filter)
if err != nil {
return []entity.Permission{}, err
}

View File

@ -0,0 +1,473 @@
package repository
import (
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository"
mgo "code.30cm.net/digimon/library-go/mongo"
"context"
"fmt"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/bson/primitive"
"testing"
)
func SetupTestPermissionRepository(db string) (repository.PermissionRepository, func(), error) {
h, p, tearDown, err := startMongoContainer()
if err != nil {
return nil, nil, err
}
conf := &mgo.Conf{
Schema: Schema,
Host: fmt.Sprintf("%s:%s", h, p),
Database: db,
MaxStaleness: 300,
MaxPoolSize: 100,
MinPoolSize: 100,
MaxConnIdleTime: 300,
Compressors: []string{},
EnableStandardReadWriteSplitMode: false,
ConnectTimeoutMs: 3000,
}
param := PermissionRepositoryParam{
Conf: conf,
}
repo := NewPermissionRepository(param)
_, _ = repo.Index20250214UP(context.Background())
return repo, tearDown, nil
}
// 測試 Insert
func TestPermissionRepository_Insert(t *testing.T) {
repo, tearDown, err := SetupTestPermissionRepository("testDB")
assert.NoError(t, err)
defer tearDown()
testCases := []struct {
name string
input entity.Permission
expectErr bool
}{
{
name: "成功插入",
input: entity.Permission{
Name: "test-permission",
HTTPMethod: "GET",
HTTPPath: "/test",
Status: 1,
Type: permission.BackendUser,
},
expectErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := repo.Insert(context.Background(), tc.input)
if tc.expectErr {
assert.NotNil(t, err, "應該要返回錯誤")
} else {
assert.Nil(t, err, "不應該返回錯誤")
}
})
}
}
func TestPermissionRepository_Update(t *testing.T) {
repo, tearDown, err := SetupTestPermissionRepository("testDB")
assert.NoError(t, err)
defer tearDown()
// 先插入一條測試數據
existingPermission := entity.Permission{
Name: "original-permission",
HTTPMethod: "GET",
HTTPPath: "/original",
Status: 1,
Type: permission.BackendUser,
}
err = repo.Insert(context.Background(), existingPermission)
assert.Nil(t, err, "插入初始數據失敗")
// 取得剛插入的 ID
found, err := repo.FindOne(context.Background(), repository.PermissionQuery{
HTTPMethod: existingPermission.HTTPMethod,
HTTPPath: existingPermission.HTTPPath,
})
assert.Nil(t, err, "應該能找到插入的權限")
id := found.ID.Hex()
testCases := []struct {
name string
id string
updateReq repository.UpdatePermission
expectErr bool
}{
{
name: "成功更新名稱",
id: id,
updateReq: repository.UpdatePermission{
Name: ToPointer("updated-name"),
},
expectErr: false,
},
{
name: "成功更新 HTTP 方法",
id: id,
updateReq: repository.UpdatePermission{
HTTPMethod: ToPointer("PUT"),
},
expectErr: false,
},
{
name: "成功更新 HTTP 路徑",
id: id,
updateReq: repository.UpdatePermission{
HTTPPath: ToPointer("/updated-path"),
},
expectErr: false,
},
{
name: "成功更新狀態",
id: id,
updateReq: repository.UpdatePermission{
Status: ToPointer(permission.Open),
},
expectErr: false,
},
{
name: "成功更新多個欄位",
id: id,
updateReq: repository.UpdatePermission{
Name: ToPointer("multi-updated"),
HTTPMethod: ToPointer("PATCH"),
HTTPPath: ToPointer("/multi-update"),
Status: ToPointer(permission.Close),
},
expectErr: false,
},
{
name: "更新失敗 - 錯誤的 ObjectID",
id: "invalid",
updateReq: repository.UpdatePermission{
Name: ToPointer("invalid-update"),
},
expectErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := repo.Update(context.Background(), tc.id, tc.updateReq)
if tc.expectErr {
assert.NotNil(t, err, "應該要返回錯誤")
} else {
assert.Nil(t, err, "不應該返回錯誤")
// 確保更新後的資料正確
updated, err := repo.GetAll(context.Background(), nil)
assert.Nil(t, err, "應該能找到更新後的權限")
if tc.updateReq.Name != nil {
assert.Equal(t, *tc.updateReq.Name, updated[0].Name, "名稱應該被更新")
}
if tc.updateReq.HTTPMethod != nil {
assert.Equal(t, *tc.updateReq.HTTPMethod, updated[0].HTTPMethod, "HTTP 方法應該被更新")
}
if tc.updateReq.HTTPPath != nil {
assert.Equal(t, *tc.updateReq.HTTPPath, updated[0].HTTPPath, "HTTP 路徑應該被更新")
}
if tc.updateReq.Status != nil {
assert.Equal(t, *tc.updateReq.Status, updated[0].Status, "狀態應該被更新")
}
}
})
}
}
// 測試 Delete 方法
func TestPermissionRepository_Delete(t *testing.T) {
repo, tearDown, err := SetupTestPermissionRepository("testDB")
assert.NoError(t, err)
defer tearDown()
// 插入測試數據
permission := entity.Permission{
ID: primitive.NewObjectID(),
Name: "delete-test",
HTTPMethod: "DELETE",
HTTPPath: "/test-delete",
Status: 1,
}
err = repo.Insert(context.Background(), permission)
assert.NoError(t, err, "插入測試數據時不應發生錯誤")
testCases := []struct {
name string
id string
expectErr bool
}{
{
name: "成功刪除存在的權限",
id: permission.ID.Hex(),
expectErr: false,
},
{
name: "刪除不存在的權限",
id: primitive.NewObjectID().Hex(),
expectErr: false, // MongoDB 刪除不存在的 ID 仍然不會報錯
},
{
name: "刪除無效的 ObjectID",
id: "invalid-object-id",
expectErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := repo.Delete(context.Background(), tc.id)
if tc.expectErr {
assert.Error(t, err, "應該返回錯誤")
} else {
assert.NoError(t, err, "不應該返回錯誤")
}
})
}
}
// 測試 GetAll 方法
func TestPermissionRepository_GetAll(t *testing.T) {
repo, tearDown, err := SetupTestPermissionRepository("testDB")
assert.NoError(t, err)
defer tearDown()
// 插入測試數據
testPermissions := []entity.Permission{
{
ID: primitive.NewObjectID(),
Name: "read",
HTTPMethod: "GET",
HTTPPath: "/read",
Status: permission.Open,
},
{
ID: primitive.NewObjectID(),
Name: "write",
HTTPMethod: "POST",
HTTPPath: "/write",
Status: permission.Close,
},
}
for _, p := range testPermissions {
err := repo.Insert(context.Background(), p)
assert.NoError(t, err, "插入測試數據時不應該發生錯誤")
}
testCases := []struct {
name string
status *permission.Status
expectLen int
expectErr bool
}{
{
name: "查詢所有權限",
status: nil,
expectLen: 2,
expectErr: false,
},
{
name: "查詢開啟的權限",
status: ToPointer(permission.Open),
expectLen: 1,
expectErr: false,
},
{
name: "查詢關閉的權限",
status: ToPointer(permission.Close),
expectLen: 1,
expectErr: false,
},
{
name: "查詢不存在的權限狀態",
status: ToPointer(permission.Status(-1)),
expectLen: 0,
expectErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := repo.GetAll(context.Background(), tc.status)
if tc.expectErr {
assert.Error(t, err, "應該返回錯誤")
} else {
assert.NoError(t, err, "不應該返回錯誤")
assert.Len(t, result, tc.expectLen, "查詢結果數量不符合預期")
}
})
}
}
// 測試 GetAllIntoIDMap 方法
func TestPermissionRepository_GetAllIntoIDMap(t *testing.T) {
repo, tearDown, err := SetupTestPermissionRepository("testDB")
assert.NoError(t, err)
defer tearDown()
// 插入測試數據
testPermissions := []entity.Permission{
{
ID: primitive.NewObjectID(),
Name: "read",
HTTPMethod: "GET",
HTTPPath: "/read",
Status: permission.Open,
},
{
ID: primitive.NewObjectID(),
Name: "write",
HTTPMethod: "POST",
HTTPPath: "/write",
Status: permission.Close,
},
}
for _, p := range testPermissions {
err := repo.Insert(context.Background(), p)
assert.NoError(t, err, "插入測試數據時不應該發生錯誤")
}
testCases := []struct {
name string
status *permission.Status
expectLen int
expectErr bool
}{
{
name: "查詢所有權限並轉為 Map",
status: nil,
expectLen: 2,
expectErr: false,
},
{
name: "查詢開啟的權限並轉為 Map",
status: ToPointer(permission.Open),
expectLen: 1,
expectErr: false,
},
{
name: "查詢關閉的權限並轉為 Map",
status: ToPointer(permission.Close),
expectLen: 1,
expectErr: false,
},
{
name: "查詢不存在的權限狀態並轉為 Map",
status: ToPointer(permission.Status(-1)),
expectLen: 0,
expectErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := repo.GetAllIntoIDMap(context.Background(), tc.status)
if tc.expectErr {
assert.Error(t, err, "應該返回錯誤")
} else {
assert.NoError(t, err, "不應該返回錯誤")
assert.Len(t, result, tc.expectLen, "查詢結果數量不符合預期")
// 確保 Map 的 key 是權限名稱
for key, permission := range result {
assert.Equal(t, key, permission.Name, "Map 的 Key 應該與 Name 相符")
}
}
})
}
}
func TestPermissionRepository_FindByNames(t *testing.T) {
repo, tearDown, err := SetupTestPermissionRepository("testDB")
assert.NoError(t, err)
defer tearDown()
// 測試數據
testPermissions := []entity.Permission{
{
ID: primitive.NewObjectID(),
Name: "read-data",
HTTPMethod: "GET",
HTTPPath: "/data",
Status: 1,
},
{
ID: primitive.NewObjectID(),
Name: "write-data",
HTTPMethod: "POST",
HTTPPath: "/data",
Status: 1,
},
}
// 插入測試數據
for _, perm := range testPermissions {
err := repo.Insert(context.Background(), perm)
assert.NoError(t, err)
}
testCases := []struct {
name string
input []string
expectLen int
expectErr bool
}{
{
name: "成功查詢單個名稱",
input: []string{"read-data"},
expectLen: 1,
expectErr: false,
},
{
name: "成功查詢多個名稱",
input: []string{"read-data", "write-data"},
expectLen: 2,
expectErr: false,
},
{
name: "查詢名稱不存在時應返回空結果",
input: []string{"unknown-permission"},
expectLen: 0,
expectErr: false,
},
{
name: "當查詢發生錯誤時,應返回錯誤",
input: nil, // 無效查詢
expectLen: 0,
expectErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := repo.FindByNames(context.Background(), tc.input)
if tc.expectErr {
assert.Error(t, err, "應該返回錯誤")
} else {
assert.NoError(t, err, "不應該返回錯誤")
assert.Len(t, result, tc.expectLen, "返回的數據長度應符合預期")
}
})
}
}
func ToPointer[T any](v T) *T {
return &v
}

199
pkg/repository/role.go Normal file
View File

@ -0,0 +1,199 @@
package repository
import (
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository"
mgo "code.30cm.net/digimon/library-go/mongo"
"context"
"fmt"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"time"
)
type RoleRepositoryParam struct {
Conf *mgo.Conf
DBOpts []mon.Option
}
type RoleRepository struct {
DB mgo.DocumentDBUseCase
}
func NewRoleRepository(param RoleRepositoryParam) repository.RoleRepository {
e := entity.Role{}
db, err := mgo.NewDocumentDB(param.Conf, e.Collection(), param.DBOpts...)
if err != nil {
panic(err)
}
return &RoleRepository{
DB: db,
}
}
func (repo *RoleRepository) List(ctx context.Context, params repository.ListQuery) ([]*entity.Role, int64, error) {
// 構建查詢條件
filter := bson.M{}
if params.Name != nil {
filter["name"] = *params.Name
}
if params.ClientID != nil {
filter["client_id"] = *params.ClientID
}
if params.Status != nil {
filter["status"] = *params.Status
}
if params.UID != nil {
filter["uid"] = *params.UID
}
// 計算符合條件的總數
count, err := repo.DB.GetClient().CountDocuments(ctx, filter)
if err != nil {
return nil, 0, err
}
// 構建查詢選項(分頁)
opts := options.Find().
SetSkip(params.PageSize * (params.PageIndex - 1)).
SetLimit(params.PageSize)
// 執行查詢
var result = make([]*entity.Role, 0, params.PageSize)
err = repo.DB.GetClient().Find(ctx, &result, filter, opts)
if err != nil {
return nil, 0, err
}
return result, count, nil
}
func (repo *RoleRepository) GetByID(ctx context.Context, id string) (*entity.Role, error) {
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
return nil, ErrInvalidObjectID
}
var result entity.Role
err = repo.DB.GetClient().FindOne(ctx, &result, bson.M{"_id": oid})
if err != nil {
return nil, err
}
return &result, nil
}
func (repo *RoleRepository) GetByUID(ctx context.Context, uid string) (*entity.Role, error) {
var result entity.Role
err := repo.DB.GetClient().FindOne(ctx, &result, bson.M{"uid": uid})
if err != nil {
return nil, err
}
return &result, nil
}
func (repo *RoleRepository) All(ctx context.Context, clientID *string) ([]*entity.Role, error) {
opt := options.Find().SetSort(bson.M{"_id": 1})
filter := bson.M{}
if clientID != nil {
filter["client_id"] = *clientID
}
result := make([]*entity.Role, 0)
err := repo.DB.GetClient().Find(ctx, &result, filter, opt)
if err != nil {
return nil, err
}
return result, nil
}
// Create 建立新的 Role
func (repo *RoleRepository) Create(ctx context.Context, role *entity.Role) error {
if role == nil {
return fmt.Errorf("failed to get role")
}
if role.ID.IsZero() {
now := time.Now().UTC().UnixNano()
role.ID = primitive.NewObjectID()
role.CreateAt = now
role.UpdateAt = now
}
_, err := repo.DB.GetClient().InsertOne(ctx, role)
return err
}
func (repo *RoleRepository) Update(ctx context.Context, role repository.UpdateReq) error {
now := time.Now().UTC().UnixNano()
// 動態構建更新內容
updateFields := bson.M{
"update_at": now, // 確保 `updateAt` 總是更新
}
if role.Name != nil {
updateFields["name"] = *role.Name
}
if role.Status != nil {
updateFields["status"] = *role.Status
}
if role.UID != nil {
updateFields["uid"] = *role.UID
}
if role.ClientID != nil {
updateFields["client_id"] = *role.ClientID
}
oid, err := primitive.ObjectIDFromHex(role.ID)
if err != nil {
return ErrInvalidObjectID
}
_, err = repo.DB.GetClient().UpdateOne(ctx, bson.M{"_id": oid}, bson.M{"$set": updateFields})
if err != nil {
return err
}
return nil
}
func (repo *RoleRepository) Delete(ctx context.Context, id string) error {
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
return ErrInvalidObjectID
}
_, err = repo.DB.GetClient().DeleteOne(ctx, bson.M{"_id": oid})
if err != nil {
return err
}
return nil
}
func (repo *RoleRepository) Index20250224UP(ctx context.Context) (*mongo.Cursor, error) {
repo.DB.PopulateMultiIndex(ctx, []string{
"name",
"client_id",
"status",
"uid",
}, []int32{1, 1, 1, 1}, false)
repo.DB.PopulateIndex(ctx, "uid", 1, false)
return repo.DB.GetClient().Indexes().List(ctx)
}

View File

@ -0,0 +1,98 @@
package repository
import (
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository"
mgo "code.30cm.net/digimon/library-go/mongo"
"context"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"time"
)
type RolePermissionRepositoryParam struct {
Conf *mgo.Conf
DBOpts []mon.Option
}
type RolePermissionRepository struct {
DB mgo.DocumentDBUseCase
}
func NewRolePermissionRepository(param RoleRepositoryParam) repository.RolePermissionRepository {
e := entity.RolePermission{}
db, err := mgo.NewDocumentDB(param.Conf, e.Collection(), param.DBOpts...)
if err != nil {
panic(err)
}
return &RolePermissionRepository{
DB: db,
}
}
func (repo *RolePermissionRepository) Get(ctx context.Context, roleID string) ([]*entity.RolePermission, error) {
var result []*entity.RolePermission
err := repo.DB.GetClient().Find(ctx, &result, bson.M{"role_id": roleID})
if err != nil {
return nil, err
}
return result, nil
}
func (repo *RolePermissionRepository) GetByPermissionID(ctx context.Context, permissionIDs []string) ([]*entity.RolePermission, error) {
var result []*entity.RolePermission // 修正 []*entity.RolePermission -> []entity.RolePermission
filter := bson.M{
"permission_id": bson.M{"$in": permissionIDs}, // 使用 $in 運算子來匹配多個 permission_id
}
err := repo.DB.GetClient().Find(ctx, &result, filter)
if err != nil {
return nil, err
}
return result, nil
}
func (repo *RolePermissionRepository) Create(ctx context.Context, role entity.RolePermission) error {
if role.ID.IsZero() {
now := time.Now().UTC().UnixNano()
role.ID = primitive.NewObjectID()
role.CreateAt = now
role.UpdateAt = now
}
_, err := repo.DB.GetClient().InsertOne(ctx, role)
return err
}
func (repo *RolePermissionRepository) Delete(ctx context.Context, roleID string, permission string) error {
filter := bson.M{
"role_id": roleID,
"permission_id": permission,
}
_, err := repo.DB.GetClient().DeleteOne(ctx, filter)
if err != nil {
return err
}
return nil
}
func (repo *RolePermissionRepository) Index20250225UP(ctx context.Context) (*mongo.Cursor, error) {
repo.DB.PopulateMultiIndex(ctx, []string{
"role_id",
"permission_id",
}, []int32{1, 1}, true)
repo.DB.PopulateIndex(ctx, "role_id", 1, false)
repo.DB.PopulateIndex(ctx, "permission_id", 1, false)
return repo.DB.GetClient().Indexes().List(ctx)
}

View File

@ -0,0 +1,265 @@
package repository
import (
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository"
mgo "code.30cm.net/digimon/library-go/mongo"
"context"
"fmt"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/bson/primitive"
"testing"
"time"
)
func SetupTestRolePermissionRepository(db string) (repository.RolePermissionRepository, func(), error) {
h, p, tearDown, err := startMongoContainer()
if err != nil {
return nil, nil, err
}
conf := &mgo.Conf{
Schema: Schema,
Host: fmt.Sprintf("%s:%s", h, p),
Database: db,
MaxStaleness: 300,
MaxPoolSize: 100,
MinPoolSize: 100,
MaxConnIdleTime: 300,
Compressors: []string{},
EnableStandardReadWriteSplitMode: false,
ConnectTimeoutMs: 3000,
}
param := RoleRepositoryParam{
Conf: conf,
}
repo := NewRolePermissionRepository(param)
_, _ = repo.Index20250225UP(context.Background())
return repo, tearDown, nil
}
func TestRolePermissionRepository_Create(t *testing.T) {
repo, tearDown, err := SetupTestRolePermissionRepository("testDB")
assert.NoError(t, err)
defer tearDown()
testCases := []struct {
name string
input entity.RolePermission
expectErr bool
}{
{
name: "成功插入新的 RolePermission",
input: entity.RolePermission{
RoleID: "role_1",
PermissionID: "perm_1",
},
expectErr: false,
},
{
name: "插入已經有 ID 的 RolePermission",
input: entity.RolePermission{
ID: primitive.NewObjectID(),
RoleID: "role_2",
PermissionID: "perm_2",
CreateAt: time.Now().UnixNano(),
UpdateAt: time.Now().UnixNano(),
},
expectErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := repo.Create(context.Background(), tc.input)
if tc.expectErr {
assert.Error(t, err, "應該返回錯誤")
} else {
assert.NoError(t, err, "不應該返回錯誤")
}
})
}
}
func TestRolePermissionRepository_Delete(t *testing.T) {
repo, tearDown, err := SetupTestRolePermissionRepository("testDB")
assert.NoError(t, err)
defer tearDown()
// 先準備測試資料
existingRolePermission := entity.RolePermission{
ID: primitive.NewObjectID(),
RoleID: "role_1",
PermissionID: "perm_1",
}
err = repo.Create(context.Background(), existingRolePermission)
assert.NoError(t, err, "應該成功插入測試資料")
testCases := []struct {
name string
roleID string
permission string
expectErr bool
}{
{
name: "成功刪除已存在的 RolePermission",
roleID: "role_1",
permission: "perm_1",
expectErr: false,
},
{
name: "刪除不存在的 RolePermission不應該報錯",
roleID: "role_2",
permission: "perm_2",
expectErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := repo.Delete(context.Background(), tc.roleID, tc.permission)
if tc.expectErr {
assert.Error(t, err, "應該返回錯誤")
} else {
assert.NoError(t, err, "不應該返回錯誤")
}
})
}
}
func TestRolePermissionRepository_GetByPermissionID(t *testing.T) {
repo, tearDown, err := SetupTestRolePermissionRepository("testDB")
assert.NoError(t, err)
defer tearDown()
// 先準備測試資料
existingRolePermissions := []entity.RolePermission{
{
ID: primitive.NewObjectID(),
RoleID: "role_1",
PermissionID: "perm_1",
},
{
ID: primitive.NewObjectID(),
RoleID: "role_2",
PermissionID: "perm_2",
},
{
ID: primitive.NewObjectID(),
RoleID: "role_3",
PermissionID: "perm_3",
},
}
for _, rp := range existingRolePermissions {
err := repo.Create(context.Background(), rp)
assert.NoError(t, err, "應該成功插入測試資料")
}
testCases := []struct {
name string
permissionIDs []string
expectedCount int
expectErr bool
}{
{
name: "成功查詢符合的 RolePermission",
permissionIDs: []string{"perm_1", "perm_2"},
expectedCount: 2,
expectErr: false,
},
{
name: "查詢時沒有符合條件的 RolePermission",
permissionIDs: []string{"perm_99"},
expectedCount: 0,
expectErr: false,
},
{
name: "查詢時發生資料庫錯誤",
permissionIDs: nil, // 模擬無效的參數
expectedCount: 0,
expectErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
results, err := repo.GetByPermissionID(context.Background(), tc.permissionIDs)
if tc.expectErr {
assert.Error(t, err, "應該返回錯誤")
} else {
assert.NoError(t, err, "不應該返回錯誤")
assert.Len(t, results, tc.expectedCount, "回傳結果數量應符合預期")
}
})
}
}
func TestRolePermissionRepository_Get(t *testing.T) {
repo, tearDown, err := SetupTestRolePermissionRepository("testDB")
assert.NoError(t, err)
defer tearDown()
// 先準備測試資料
existingRolePermissions := []entity.RolePermission{
{
ID: primitive.NewObjectID(),
RoleID: "role_1",
PermissionID: "perm_1",
},
{
ID: primitive.NewObjectID(),
RoleID: "role_1",
PermissionID: "perm_2",
},
{
ID: primitive.NewObjectID(),
RoleID: "role_2",
PermissionID: "perm_3",
},
}
for _, rp := range existingRolePermissions {
err := repo.Create(context.Background(), rp)
assert.NoError(t, err, "應該成功插入測試資料")
}
testCases := []struct {
name string
roleID string
expectedCount int
expectErr bool
}{
{
name: "成功查詢符合的 RolePermission",
roleID: "role_1",
expectedCount: 2,
expectErr: false,
},
{
name: "查詢時沒有符合條件的 RolePermission",
roleID: "role_99",
expectedCount: 0,
expectErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
results, err := repo.Get(context.Background(), tc.roleID)
if tc.expectErr {
assert.Error(t, err, "應該返回錯誤")
} else {
assert.NoError(t, err, "不應該返回錯誤")
assert.Len(t, results, tc.expectedCount, "回傳結果數量應符合預期")
}
})
}
}

541
pkg/repository/role_test.go Normal file
View File

@ -0,0 +1,541 @@
package repository
import (
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository"
mgo "code.30cm.net/digimon/library-go/mongo"
"context"
"fmt"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/bson/primitive"
"testing"
)
func SetupTestRoleRepository(db string) (repository.RoleRepository, func(), error) {
h, p, tearDown, err := startMongoContainer()
if err != nil {
return nil, nil, err
}
conf := &mgo.Conf{
Schema: Schema,
Host: fmt.Sprintf("%s:%s", h, p),
Database: db,
MaxStaleness: 300,
MaxPoolSize: 100,
MinPoolSize: 100,
MaxConnIdleTime: 300,
Compressors: []string{},
EnableStandardReadWriteSplitMode: false,
ConnectTimeoutMs: 3000,
}
param := RoleRepositoryParam{
Conf: conf,
}
repo := NewRoleRepository(param)
_, _ = repo.Index20250224UP(context.Background())
return repo, tearDown, nil
}
func TestRoleRepository_Create(t *testing.T) {
repo, tearDown, err := SetupTestRoleRepository("testDB")
assert.NoError(t, err)
defer tearDown()
testCases := []struct {
name string
input *entity.Role
expectErr bool
}{
{
name: "成功建立新的角色",
input: &entity.Role{
Name: "Admin",
UID: "user123",
ClientID: "client456",
Status: 1,
},
expectErr: false,
},
{
name: "自動生成 ID 及時間戳",
input: &entity.Role{
Name: "User",
UID: "user789",
ClientID: "client987",
Status: 1,
},
expectErr: false,
},
{
name: "當角色為 nil 時應返回錯誤",
input: nil,
expectErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := repo.Create(context.Background(), tc.input)
if tc.expectErr {
assert.Error(t, err, "應該返回錯誤")
} else {
assert.NoError(t, err, "不應該返回錯誤")
assert.NotEqual(t, primitive.NilObjectID, tc.input.ID, "應該自動生成 ObjectID")
assert.True(t, tc.input.CreateAt > 0, "應該自動設定 CreateAt")
assert.True(t, tc.input.UpdateAt > 0, "應該自動設定 UpdateAt")
}
})
}
}
func TestRoleRepository_Update(t *testing.T) {
repo, tearDown, err := SetupTestRoleRepository("testDB")
assert.NoError(t, err)
defer tearDown()
// 預先建立一個角色
existingRole := &entity.Role{
Name: "Old Name",
UID: "old_uid",
ClientID: "old_client_id",
Status: 1,
}
err = repo.Create(context.Background(), existingRole)
assert.NoError(t, err)
newName := "New Name"
newStatus := permission.Close
newUID := "new_uid"
newClientID := "new_client_id"
testCases := []struct {
name string
input repository.UpdateReq
expectErr bool
}{
{
name: "成功更新角色",
input: repository.UpdateReq{
ID: existingRole.ID.Hex(),
Name: &newName,
Status: &newStatus,
UID: &newUID,
ClientID: &newClientID,
},
expectErr: false,
},
{
name: "更新部分欄位",
input: repository.UpdateReq{
ID: existingRole.ID.Hex(),
Name: &newName,
Status: &newStatus,
},
expectErr: false,
},
{
name: "無效的 ObjectID 應返回錯誤",
input: repository.UpdateReq{
ID: "invalid_object_id",
},
expectErr: true,
},
{
name: "當 UpdateOne 失敗時應返回錯誤",
input: repository.UpdateReq{
ID: primitive.NewObjectID().Hex(), // 模擬一個不存在的 ID
},
expectErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := repo.Update(context.Background(), tc.input)
if tc.expectErr {
assert.Error(t, err, "應該返回錯誤")
} else {
assert.NoError(t, err, "不應該返回錯誤")
// 驗證更新結果
updatedRole, err := repo.GetByID(context.Background(), existingRole.ID.Hex())
assert.NoError(t, err)
if tc.input.Name != nil {
assert.Equal(t, *tc.input.Name, updatedRole.Name, "名稱應該被更新")
}
if tc.input.Status != nil {
assert.Equal(t, *tc.input.Status, updatedRole.Status, "狀態應該被更新")
}
if tc.input.UID != nil {
assert.Equal(t, *tc.input.UID, updatedRole.UID, "UID 應該被更新")
}
if tc.input.ClientID != nil {
assert.Equal(t, *tc.input.ClientID, updatedRole.ClientID, "ClientID 應該被更新")
}
}
})
}
}
func TestRoleRepository_Delete(t *testing.T) {
repo, tearDown, err := SetupTestRoleRepository("testDB")
assert.NoError(t, err)
defer tearDown()
// 預先建立一個角色
existingRole := &entity.Role{
Name: "Test Role",
UID: "test_uid",
ClientID: "test_client_id",
Status: 1,
}
err = repo.Create(context.Background(), existingRole)
assert.NoError(t, err)
testCases := []struct {
name string
inputID string
expectErr bool
}{
{
name: "成功刪除角色",
inputID: existingRole.ID.Hex(),
expectErr: false,
},
{
name: "刪除不存在的角色不應報錯",
inputID: primitive.NewObjectID().Hex(),
expectErr: false,
},
{
name: "無效的 ObjectID 應返回錯誤",
inputID: "invalid_object_id",
expectErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := repo.Delete(context.Background(), tc.inputID)
if tc.expectErr {
assert.Error(t, err, "應該返回錯誤")
} else {
assert.NoError(t, err, "不應該返回錯誤")
// 驗證角色是否已刪除
if tc.inputID == existingRole.ID.Hex() {
_, err := repo.GetByID(context.Background(), existingRole.ID.Hex())
assert.Error(t, err, "應該找不到該角色")
}
}
})
}
}
func TestRoleRepository_All(t *testing.T) {
repo, tearDown, err := SetupTestRoleRepository("testDB")
assert.NoError(t, err)
defer tearDown()
clientID1 := "client_1"
clientID2 := "client_2"
// 預先建立角色資料
roles := []*entity.Role{
{
ID: primitive.NewObjectID(),
Name: "Admin",
UID: "admin_uid",
ClientID: clientID1,
Status: 1,
},
{
ID: primitive.NewObjectID(),
Name: "User",
UID: "user_uid",
ClientID: clientID1,
Status: 1,
},
{
ID: primitive.NewObjectID(),
Name: "Manager",
UID: "manager_uid",
ClientID: clientID2,
Status: 1,
},
}
// 插入測試資料
for _, role := range roles {
err := repo.Create(context.Background(), role)
assert.NoError(t, err)
}
testCases := []struct {
name string
clientID *string
expectLen int
expectErr bool
}{
{
name: "查詢所有角色",
clientID: nil,
expectLen: len(roles),
expectErr: false,
},
{
name: "根據 clientID 查詢角色",
clientID: &clientID1,
expectLen: 2,
expectErr: false,
},
{
name: "clientID 無匹配時應返回空",
clientID: new(string),
expectLen: 0,
expectErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := repo.All(context.Background(), tc.clientID)
if tc.expectErr {
assert.Error(t, err, "應該返回錯誤")
} else {
assert.NoError(t, err, "不應該返回錯誤")
assert.Len(t, result, tc.expectLen, "返回的角色數量應符合預期")
}
})
}
}
func TestRoleRepository_GetByUID(t *testing.T) {
repo, tearDown, err := SetupTestRoleRepository("testDB")
assert.NoError(t, err)
defer tearDown()
// 預先建立測試角色
existingRole := &entity.Role{
ID: primitive.NewObjectID(),
Name: "Admin",
UID: "admin_uid",
ClientID: "client_1",
Status: 1,
}
err = repo.Create(context.Background(), existingRole)
assert.NoError(t, err)
testCases := []struct {
name string
uid string
expectErr bool
expectNil bool
}{
{
name: "成功查詢角色",
uid: "admin_uid",
expectErr: false,
expectNil: false,
},
{
name: "查詢不存在的角色",
uid: "non_existent_uid",
expectErr: true,
expectNil: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := repo.GetByUID(context.Background(), tc.uid)
if tc.expectErr {
assert.Error(t, err, "應該返回錯誤")
} else {
assert.NoError(t, err, "不應該返回錯誤")
}
if tc.expectNil {
assert.Nil(t, result, "應該返回 nil")
} else {
assert.NotNil(t, result, "不應該返回 nil")
assert.Equal(t, existingRole.UID, result.UID, "UID 應相符")
assert.Equal(t, existingRole.Name, result.Name, "名稱應相符")
}
})
}
}
func TestRoleRepository_GetByID(t *testing.T) {
repo, tearDown, err := SetupTestRoleRepository("testDB")
assert.NoError(t, err)
defer tearDown()
// 預先建立測試角色
existingRole := &entity.Role{
ID: primitive.NewObjectID(),
Name: "Admin",
UID: "admin_uid",
ClientID: "client_1",
Status: 1,
}
err = repo.Create(context.Background(), existingRole)
assert.NoError(t, err)
testCases := []struct {
name string
id string
expectErr bool
expectNil bool
}{
{
name: "成功查詢角色",
id: existingRole.ID.Hex(),
expectErr: false,
expectNil: false,
},
{
name: "查詢不存在的角色",
id: primitive.NewObjectID().Hex(),
expectErr: true,
expectNil: true,
},
{
name: "提供無效的 ObjectID",
id: "invalid_id",
expectErr: true,
expectNil: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := repo.GetByID(context.Background(), tc.id)
if tc.expectErr {
assert.Error(t, err, "應該返回錯誤")
} else {
assert.NoError(t, err, "不應該返回錯誤")
}
if tc.expectNil {
assert.Nil(t, result, "應該返回 nil")
} else {
assert.NotNil(t, result, "不應該返回 nil")
assert.Equal(t, existingRole.ID, result.ID, "ID 應相符")
assert.Equal(t, existingRole.Name, result.Name, "名稱應相符")
}
})
}
}
func TestRoleRepository_List(t *testing.T) {
repo, tearDown, err := SetupTestRoleRepository("testDB")
assert.NoError(t, err)
defer tearDown()
// 預先建立測試角色
roles := []*entity.Role{
{
ID: primitive.NewObjectID(),
Name: "Admin",
UID: "admin_uid",
ClientID: "client_1",
Status: 1,
},
{
ID: primitive.NewObjectID(),
Name: "User",
UID: "user_uid",
ClientID: "client_1",
Status: 1,
},
{
ID: primitive.NewObjectID(),
Name: "Guest",
UID: "guest_uid",
ClientID: "client_2",
Status: 0,
},
}
// 插入測試資料
for _, role := range roles {
err := repo.Create(context.Background(), role)
assert.NoError(t, err)
}
testCases := []struct {
name string
query repository.ListQuery
expectLen int
expectErr bool
}{
{
name: "查詢所有角色",
query: repository.ListQuery{PageSize: 10, PageIndex: 1},
expectLen: 3,
expectErr: false,
},
{
name: "篩選名稱為 Admin",
query: repository.ListQuery{Name: ToPointer("Admin"), PageSize: 10, PageIndex: 1},
expectLen: 1,
expectErr: false,
},
{
name: "篩選特定 ClientID",
query: repository.ListQuery{ClientID: ToPointer("client_1"), PageSize: 10, PageIndex: 1},
expectLen: 2,
expectErr: false,
},
{
name: "篩選啟用 (Status=1) 的角色",
query: repository.ListQuery{Status: ToPointer(permission.Open), PageSize: 10, PageIndex: 1},
expectLen: 2,
expectErr: false,
},
{
name: "篩選特定 UID",
query: repository.ListQuery{UID: ToPointer("guest_uid"), PageSize: 10, PageIndex: 1},
expectLen: 1,
expectErr: false,
},
{
name: "測試分頁 PageSize=1, PageIndex=1",
query: repository.ListQuery{PageSize: 1, PageIndex: 1},
expectLen: 1,
expectErr: false,
},
{
name: "查詢無符合條件的角色",
query: repository.ListQuery{Name: ToPointer("NonExist"), PageSize: 10, PageIndex: 1},
expectLen: 0,
expectErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, _, err := repo.List(context.Background(), tc.query)
if tc.expectErr {
assert.Error(t, err, "應該返回錯誤")
} else {
assert.NoError(t, err, "不應該返回錯誤")
assert.Len(t, result, tc.expectLen, "返回的角色數量應符合預期")
}
})
}
}

View File

@ -0,0 +1,52 @@
package repository
import (
"context"
"fmt"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
const (
Host = "127.0.0.1"
Port = "27017"
Schema = "mongodb"
)
func startMongoContainer() (string, string, func(), error) {
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "mongo:latest",
ExposedPorts: []string{"27017/tcp"},
WaitingFor: wait.ForListeningPort("27017/tcp"),
}
mongoC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
return "", "", nil, err
}
port, err := mongoC.MappedPort(ctx, Port)
if err != nil {
return "", "", nil, err
}
host, err := mongoC.Host(ctx)
if err != nil {
return "", "", nil, err
}
uri := fmt.Sprintf("mongodb://%s:%s", host, port.Port())
tearDown := func() {
mongoC.Terminate(ctx)
}
fmt.Printf("Connecting to %s\n", uri)
return host, port.Port(), tearDown, nil
}

137
pkg/repository/user_role.go Normal file
View File

@ -0,0 +1,137 @@
package repository
import (
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository"
mgo "code.30cm.net/digimon/library-go/mongo"
"context"
"fmt"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"time"
)
// UserRoleRepositoryParam 定義 MongoDB 配置參數
type UserRoleRepositoryParam struct {
Conf *mgo.Conf
DBOpts []mon.Option
}
// userRoleRepository 實作 repository.UserRoleRepository 介面
type userRoleRepository struct {
DB mgo.DocumentDBUseCase
}
// NewUserRoleRepository 初始化 `UserRoleRepository`
func NewUserRoleRepository(param UserRoleRepositoryParam) repository.UserRoleRepository {
e := entity.UserRole{}
db, err := mgo.NewDocumentDB(param.Conf, e.Collection(), param.DBOpts...)
if err != nil {
panic(err)
}
return &userRoleRepository{
DB: db,
}
}
// GetAll 取得所有 UserRole
func (repo *userRoleRepository) GetAll(ctx context.Context) ([]*entity.UserRole, error) {
var result []*entity.UserRole
err := repo.DB.GetClient().Find(ctx, &result, bson.M{})
if err != nil {
return nil, err
}
return result, nil
}
// GetByUserID 透過 UID 查詢 UserRole
func (repo *userRoleRepository) GetByUserID(ctx context.Context, uid string) (entity.UserRole, error) {
var result entity.UserRole
err := repo.DB.GetClient().FindOne(ctx, &result, bson.M{"uid": uid})
if err != nil {
return entity.UserRole{}, err
}
return result, nil
}
// GetUsersByRoleID 透過 RoleID 查詢所有使用此角色的使用者
func (repo *userRoleRepository) GetUsersByRoleID(ctx context.Context, roleID string) ([]entity.UserRole, error) {
var result []entity.UserRole
err := repo.DB.GetClient().Find(ctx, &result, bson.M{"role_id": roleID})
if err != nil {
return nil, err
}
return result, nil
}
// CountUsersByRole 統計每個角色的使用者數量
func (repo *userRoleRepository) CountUsersByRole(ctx context.Context) ([]repository.RoleUserCount, error) {
pipeline := []bson.M{
{"$group": bson.M{
"_id": "$role_id",
"count": bson.M{"$sum": 1},
}},
}
var result []repository.RoleUserCount
err := repo.DB.GetClient().Aggregate(ctx, &result, pipeline)
if err != nil {
return nil, err
}
return result, nil
}
// CreateUserRole 新增使用者角色
func (repo *userRoleRepository) CreateUserRole(ctx context.Context, param entity.UserRole) error {
if param.UID == "" {
return fmt.Errorf("uid can't be empty")
}
if param.RoleID == "" {
return fmt.Errorf("role_id can't be empty")
}
if param.ID.IsZero() {
now := time.Now().UTC().UnixNano()
param.ID = primitive.NewObjectID()
param.CreateAt = now
param.UpdateAt = now
}
_, err := repo.DB.GetClient().InsertOne(ctx, param)
return err
}
// UpdateUserRole 更新使用者角色
func (repo *userRoleRepository) UpdateUserRole(ctx context.Context, uid, roleID string) (entity.UserRole, error) {
filter := bson.M{"uid": uid}
update := bson.M{
"$set": bson.M{
"role_id": roleID,
"update_at": time.Now().UTC().UnixNano(),
},
}
var updated entity.UserRole
err := repo.DB.GetClient().FindOneAndUpdate(ctx, &updated, filter, update)
if err != nil {
return entity.UserRole{}, err
}
return updated, nil
}
func (repo *userRoleRepository) Index20250225UP(ctx context.Context) (*mongo.Cursor, error) {
repo.DB.PopulateIndex(ctx, "role_id", 1, false)
repo.DB.PopulateIndex(ctx, "uid", 1, false)
return repo.DB.GetClient().Indexes().List(ctx)
}

View File

@ -0,0 +1,336 @@
package repository
import (
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository"
mgo "code.30cm.net/digimon/library-go/mongo"
"context"
"fmt"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/bson/primitive"
"testing"
"time"
)
func SetupTestUserRoleRepository(db string) (repository.UserRoleRepository, func(), error) {
h, p, tearDown, err := startMongoContainer()
if err != nil {
return nil, nil, err
}
conf := &mgo.Conf{
Schema: Schema,
Host: fmt.Sprintf("%s:%s", h, p),
Database: db,
MaxStaleness: 300,
MaxPoolSize: 100,
MinPoolSize: 100,
MaxConnIdleTime: 300,
Compressors: []string{},
EnableStandardReadWriteSplitMode: false,
ConnectTimeoutMs: 3000,
}
param := UserRoleRepositoryParam{
Conf: conf,
}
repo := NewUserRoleRepository(param)
_, _ = repo.Index20250225UP(context.Background())
return repo, tearDown, nil
}
func TestUserRoleRepository_CreateUserRole(t *testing.T) {
repo, tearDown, err := SetupTestUserRoleRepository("testDB")
assert.NoError(t, err)
defer tearDown()
testCases := []struct {
name string
input entity.UserRole
expectErr bool
}{
{
name: "成功新增使用者角色",
input: entity.UserRole{
UID: "user_123",
RoleID: "role_456",
},
expectErr: false,
},
{
name: "異常:無效的輸入",
input: entity.UserRole{
RoleID: "role_456",
},
expectErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := repo.CreateUserRole(context.Background(), tc.input)
if tc.expectErr {
assert.Error(t, err, "應該返回錯誤")
} else {
assert.NoError(t, err, "不應該返回錯誤")
// 檢查資料是否真的被插入
var inserted entity.UserRole
inserted, err = repo.GetByUserID(context.Background(), tc.input.UID)
fmt.Println(inserted)
assert.NoError(t, err, "應該能找到插入的資料")
assert.Equal(t, tc.input.UID, inserted.UID, "UID 應該匹配")
assert.Equal(t, tc.input.RoleID, inserted.RoleID, "RoleID 應該匹配")
assert.NotZero(t, inserted.CreateAt, "CreateAt 應該被設定")
assert.NotZero(t, inserted.UpdateAt, "UpdateAt 應該被設定")
}
})
}
}
func TestUserRoleRepository_UpdateUserRole(t *testing.T) {
repo, tearDown, err := SetupTestUserRoleRepository("testDB")
assert.NoError(t, err)
defer tearDown()
// 先插入測試數據
existingUserRole := entity.UserRole{
ID: primitive.NewObjectID(),
UID: "user_123",
RoleID: "role_123",
CreateAt: time.Now().UTC().UnixNano(),
UpdateAt: time.Now().UTC().UnixNano(),
}
err = repo.CreateUserRole(context.Background(), existingUserRole)
assert.NoError(t, err, "初始化數據應該成功")
testCases := []struct {
name string
uid string
newRoleID string
expectErr bool
expectedRole string
}{
{
name: "成功更新使用者角色",
uid: "user_123",
newRoleID: "role_456",
expectErr: false,
expectedRole: "role_456",
},
{
name: "異常:更新的 UID 不存在",
uid: "non_existent_user",
newRoleID: "role_789",
expectErr: true,
expectedRole: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, err = repo.UpdateUserRole(context.Background(), tc.uid, tc.newRoleID)
if tc.expectErr {
assert.Error(t, err, "應該返回錯誤")
} else {
assert.NoError(t, err, "不應該返回錯誤")
// 檢查資料是否真的被更新
found, err := repo.GetByUserID(context.Background(), tc.uid)
assert.NoError(t, err, "應該能找到更新後的資料")
assert.Equal(t, tc.expectedRole, found.RoleID, "RoleID 應該匹配更新的值")
assert.NotZero(t, found.UpdateAt, "UpdateAt 應該被更新")
}
})
}
}
func TestUserRoleRepository_CountUsersByRole(t *testing.T) {
repo, tearDown, err := SetupTestUserRoleRepository("testDB")
assert.NoError(t, err)
defer tearDown()
// 先插入測試數據
testUsers := []entity.UserRole{
{ID: primitive.NewObjectID(), UID: "user_1", RoleID: "role_admin"},
{ID: primitive.NewObjectID(), UID: "user_2", RoleID: "role_admin"},
{ID: primitive.NewObjectID(), UID: "user_3", RoleID: "role_user"},
{ID: primitive.NewObjectID(), UID: "user_4", RoleID: "role_user"},
{ID: primitive.NewObjectID(), UID: "user_5", RoleID: "role_user"},
}
for _, user := range testUsers {
err := repo.CreateUserRole(context.Background(), user)
assert.NoError(t, err, "應該成功插入測試數據")
}
testCases := []struct {
name string
expectedData []repository.RoleUserCount
expectErr bool
}{
{
name: "成功統計角色使用者數量",
expectedData: []repository.RoleUserCount{
{RoleID: "role_admin", Count: 2},
{RoleID: "role_user", Count: 3},
},
expectErr: false,
},
//{
// name: "無資料時回傳空結果",
// expectedData: []repository.RoleUserCount{},
// expectErr: false,
//},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := repo.CountUsersByRole(context.Background())
if tc.expectErr {
assert.Error(t, err, "應該返回錯誤")
} else {
assert.NoError(t, err, "不應該返回錯誤")
assert.ElementsMatch(t, tc.expectedData, result, "結果應該匹配預期的使用者數量統計")
}
})
}
}
// TestUserRoleRepository_GetUsersByRoleID 測試 GetUsersByRoleID 查詢指定 RoleID 的使用者
func TestUserRoleRepository_GetUsersByRoleID(t *testing.T) {
repo, tearDown, err := SetupTestUserRoleRepository("testDB")
assert.NoError(t, err)
defer tearDown()
// 插入測試數據
testUsers := []entity.UserRole{
{ID: primitive.NewObjectID(), UID: "user_1", RoleID: "role_admin"},
{ID: primitive.NewObjectID(), UID: "user_2", RoleID: "role_admin"},
{ID: primitive.NewObjectID(), UID: "user_3", RoleID: "role_user"},
{ID: primitive.NewObjectID(), UID: "user_4", RoleID: "role_user"},
{ID: primitive.NewObjectID(), UID: "user_5", RoleID: "role_user"},
}
for _, user := range testUsers {
err := repo.CreateUserRole(context.Background(), user)
assert.NoError(t, err, "應該成功插入測試數據")
}
// 測試案例
testCases := []struct {
name string
roleID string
expectedData []entity.UserRole
expectErr bool
}{
{
name: "成功查詢 role_admin 的使用者",
roleID: "role_admin",
expectedData: []entity.UserRole{
{UID: "user_1", RoleID: "role_admin"},
{UID: "user_2", RoleID: "role_admin"},
},
expectErr: false,
},
{
name: "成功查詢 role_user 的使用者",
roleID: "role_user",
expectedData: []entity.UserRole{
{UID: "user_3", RoleID: "role_user"},
{UID: "user_4", RoleID: "role_user"},
{UID: "user_5", RoleID: "role_user"},
},
expectErr: false,
},
{
name: "查詢 role_guest應回傳空陣列",
roleID: "role_guest",
expectedData: []entity.UserRole{},
expectErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := repo.GetUsersByRoleID(context.Background(), tc.roleID)
if tc.expectErr {
assert.Error(t, err, "應該返回錯誤")
} else {
assert.NoError(t, err, "不應該返回錯誤")
fmt.Println(result)
compute := make([]entity.UserRole, 0, len(result))
res := make([]entity.UserRole, 0, len(result))
for _, item := range result {
compute = append(compute, entity.UserRole{
RoleID: item.RoleID,
UID: item.UID,
})
}
for _, item := range tc.expectedData {
res = append(res, entity.UserRole{
RoleID: item.RoleID,
UID: item.UID,
})
}
assert.ElementsMatch(t, compute, res, "結果應該匹配預期的使用者列表")
}
})
}
}
// TestUserRoleRepository_GetAll 測試 GetAll 取得所有使用者角色
func TestUserRoleRepository_GetAll(t *testing.T) {
repo, tearDown, err := SetupTestUserRoleRepository("testDB")
assert.NoError(t, err)
defer tearDown()
// 插入測試數據
testUsers := []entity.UserRole{
{ID: primitive.NewObjectID(), UID: "user_1", RoleID: "role_admin"},
{ID: primitive.NewObjectID(), UID: "user_2", RoleID: "role_editor"},
{ID: primitive.NewObjectID(), UID: "user_3", RoleID: "role_viewer"},
}
for _, user := range testUsers {
err := repo.CreateUserRole(context.Background(), user)
assert.NoError(t, err, "應該成功插入測試數據")
}
// 測試案例
testCases := []struct {
name string
expectedData []*entity.UserRole
expectErr bool
}{
{
name: "成功獲取所有使用者角色",
expectedData: []*entity.UserRole{
{UID: "user_1", RoleID: "role_admin"},
{UID: "user_2", RoleID: "role_editor"},
{UID: "user_3", RoleID: "role_viewer"},
},
expectErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := repo.GetAll(context.Background())
if tc.expectErr {
assert.Error(t, err, "應該返回錯誤")
} else {
assert.NoError(t, err, "不應該返回錯誤")
assert.Len(t, result, len(tc.expectedData), "結果應該包含正確數量的使用者角色")
//assert.ElementsMatch(t, tc.expectedData, result, "結果應該匹配預期的使用者角色")
}
})
}
}

View File

@ -1,29 +1,37 @@
package usecase
import (
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/usecase"
"code.30cm.net/digimon/library-go/errs"
"code.30cm.net/digimon/library-go/errs/code"
"context"
"fmt"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model"
"github.com/zeromicro/go-zero/core/logx"
"log"
"net/http"
"time"
)
type RBACUseCaseParam struct {
ModulePath string
permissionRepo repository.PermissionRepository
roleRepo repository.RoleRepository
rolePermission repository.RolePermissionRepository
RBACRedisAdapter repository.RBACAdapter
// role permission 之類的
}
type RBACUseCase struct {
permissionRepo repository.PermissionRepository
roleRepo repository.RoleRepository
adapter repository.RBACAdapter
rolePermission repository.RolePermissionRepository
instance *casbin.Enforcer
}
@ -31,6 +39,8 @@ func NewUseCase(param RBACUseCaseParam) usecase.RBACUseCase {
result := &RBACUseCase{
adapter: param.RBACRedisAdapter,
permissionRepo: param.permissionRepo,
roleRepo: param.roleRepo,
rolePermission: param.rolePermission,
}
// 1. 讀取 RBAC 模型 ->
@ -68,89 +78,107 @@ func (use *RBACUseCase) Check(ctx context.Context, role, path, method string) (u
Allow: ok,
}
fmt.Println(p)
//// 檢查是否有明碼查詢權限
//if role == domain.AdminRoleUID {
// status.Select.PlainCode = true
//} else if ok && method == http.MethodGet {
// status.Select.PlainCode = use.rbac.GetModel().HasPolicy("p", "p", []string{
// role, path, method, p[3] + ".plain_code",
// })
//}
//
//limit := 4
//if len(p) >= limit {
// status.Select.PermissionName = p[3]
//}
// 檢查是否有明碼查詢權限
if role == permission.AdminRoleUID {
status.Select.PlainCode = true
} else if ok && method == http.MethodGet {
policy, err := use.instance.GetModel().HasPolicy("p", "p", []string{
role, path, method, p[3] + ".plain_code",
})
if err != nil {
return usecase.CheckRolePermissionStatus{}, err
}
status.Select.PlainCode = policy
}
limit := 4
if len(p) >= limit {
status.Select.PermissionName = p[3]
}
return status, nil
}
func (use *RBACUseCase) LoadPolicy(ctx context.Context) error {
status := permission.Open
// 取得所有permission -> permission tree 拿到有開啟的節點,如果付節點關閉,子節點也就不顯示了
permissions, err := use.permissionRepo.GetAll(ctx, &status)
// 取得所有permission -> permission tree 拿到所有節點,
permissions, err := use.permissionRepo.GetAll(ctx, nil)
if err != nil {
return fmt.Errorf("permissionRepo.AllStatus error: %w", err)
}
fmt.Println(permissions)
// 建立樹,我只要開啟的 Permission
tree := GeneratePermissionTree(permissions)
openMaps, err := tree.filterOpenNodes()
if err != nil {
return fmt.Errorf("GeneratePermissionTree.filterOpenNodes error: %w", err)
}
// 全部permission
//permissionMap := make(map[int64]entity.Permission, len(permissions))
//for _, v := range permissions {
// permissionMap[v.ID] = v
//}
permissionMap := make(map[string]entity.Permission, len(permissions))
for k, v := range openMaps {
permissionMap[k] = v
}
// 全部角色
//roles, err := r.roleRepo.All(ctx, 1)
//if err != nil {
// return fmt.Errorf("roleRepo.AllStatus error: %w", err)
//}
//
//roleMap := make(map[int64]entity.Role, len(roles))
//for _, v := range roles {
// roleMap[v.ID] = v
//}
roles, err := use.roleRepo.All(ctx, nil)
if err != nil {
return fmt.Errorf("roleRepo.All error: %w", err)
}
roleMap := make(map[string]entity.Role, len(roles))
for _, v := range roles {
tmpValue := v
roleMap[v.ID.Hex()] = *tmpValue
}
// 根據角色組合權限表
//for _, v := range roles {
// rolePermissions, err := r.rolePermissionRepo.Get(ctx, v.ID)
// if err != nil {
// return fmt.Errorf("rolePermissionRepo.Get ID: %d error: %w", v.ID, err)
// }
//
// for _, rp := range rolePermissions {
// role, ok := roleMap[rp.RoleID]
// if !ok {
// logrus.WithFields(logrus.Fields{
// "role_id": rp.RoleID,
// }).Error("role not found")
//
// continue
// }
//
// permission, ok := permissionMap[rp.PermissionID]
// if !ok {
// logrus.WithFields(logrus.Fields{
// "permission_id": rp.PermissionID,
// }).Error("permission not found")
//
// continue
// }
//
// if permission.HTTPPath == "" || permission.HTTPMethod == "" {
// continue
// }
//
// // 根據策略model configs/rbac_model.conf填入policy_definition對應參數
// err := persist.LoadPolicyArray([]string{"p", role.UID, permission.HTTPPath, permission.HTTPMethod, permission.Name}, model)
// if err != nil {
// return fmt.Errorf("persist.LoadPolicyArray error: %w", err)
// }
// }
//}
for _, role := range roles {
rolePermission, err := use.rolePermission.Get(ctx, role.ID.Hex())
if err != nil {
e := errs.DatabaseErrorWithScopeL(
code.CloudEPPermission,
domain.FailedToGetRolePermission,
logx.WithContext(ctx),
[]logx.LogField{
{Key: "req", Value: fmt.Sprintf("role: %s", role.ID.Hex())},
{Key: "func", Value: "RolePermissionRepo.Get"},
{Key: "err", Value: err.Error()},
},
"failed to get rolePermission")
return e
}
for _, rp := range rolePermission {
r, ok := roleMap[rp.RoleID]
if !ok {
logx.Errorf(fmt.Sprintf("role_id: %s not found", rp.RoleID))
continue
}
p, ok := permissionMap[rp.PermissionID]
if !ok {
logx.Errorf(fmt.Sprintf("permission_id: %s not found", rp.PermissionID))
continue
}
if p.HTTPPath == "" || p.HTTPMethod == "" {
continue
}
_, err = use.instance.AddPolicy(r.UID, p.HTTPPath, p.HTTPMethod, p.Name)
if err != nil {
return err
}
}
}
err = use.instance.LoadPolicy()
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1 @@
package usecase

View File

@ -0,0 +1,229 @@
package usecase
import (
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission"
"sync"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// PermissionTree 用來管理權限節點,包含快速查詢 map 與輔助 map例如名稱對應的 ID 列表)
type PermissionTree struct {
// dummy root 節點
root *PermissionNode
// permission id => node
nodes map[string]*PermissionNode
// permission name => permission id
names map[string][]string
mu sync.RWMutex // 保護樹的並發存取
}
type PermissionNode struct {
Data entity.Permission
Parent *PermissionNode
Children []*PermissionNode
}
// GeneratePermissionTree 根據扁平權限資料建立樹,並掛在 dummy root 下
func GeneratePermissionTree(permissions []entity.Permission) *PermissionTree {
tree := &PermissionTree{
nodes: make(map[string]*PermissionNode),
names: make(map[string][]string),
}
// 1. 建立所有節點
for _, perm := range permissions {
node := &PermissionNode{
Data: perm,
Children: []*PermissionNode{},
}
tree.nodes[perm.ID.Hex()] = node
tree.names[perm.Name] = append(tree.names[perm.Name], perm.ID.Hex())
}
// 2. 建立 dummy root 節點
tree.root = &PermissionNode{
Data: entity.Permission{ID: primitive.NewObjectID(), Name: "root"},
Children: []*PermissionNode{},
}
// 3. 建立父子連結:若找不到父節點或 Parent 為 0則掛在 dummy root 下
for _, node := range tree.nodes {
if node.Data.Parent == "" {
node.Parent = tree.root
tree.root.Children = append(tree.root.Children, node)
} else if parent, ok := tree.nodes[node.Data.Parent]; ok {
node.Parent = parent
parent.Children = append(parent.Children, node)
} else {
// 若父節點不存在,預設掛在 dummy root 下
node.Parent = tree.root
tree.root.Children = append(tree.root.Children, node)
}
}
return tree
}
// getNode 輔助函數:根據 ID 從樹中查找節點
func (tree *PermissionTree) getNode(id string) *PermissionNode {
tree.mu.RLock()
defer tree.mu.RUnlock()
return tree.nodes[id]
}
func (tree *PermissionTree) put(node entity.Permission) {
parentNode := tree.getNode(node.Parent)
if parentNode == nil {
parentNode = tree.root
}
thisNode := &PermissionNode{
Data: node,
Parent: parentNode,
Children: make([]*PermissionNode, 0),
}
parentNode.Children = append(parentNode.Children, thisNode)
tree.names[node.Name] = append(tree.names[node.Name], node.ID.Hex())
tree.nodes[node.ID.Hex()] = thisNode
}
// filterOpenNodes 走訪整棵樹,列出有被打開的節點(父節點沒開,則底下的都不會開)
// 如果某個節點為非葉節點,則會檢查其子節點是否有啟用,否則該節點不會被展開。
// [permissionID] entity.Permission
func (tree *PermissionTree) filterOpenNodes() (map[string]entity.Permission, error) {
tree.mu.RLock()
defer tree.mu.RUnlock()
result := make(map[string]entity.Permission)
// dfs 為內部閉包,可存取 result
// 返回值 bool 表示目前節點或其子孫中是否存在有效 open 節點
var dfs func(node *PermissionNode) bool
dfs = func(node *PermissionNode) bool {
// 若本身狀態非 open則整個分支不展開
if node.Data.Status != permission.Open {
return false
}
// 節點本身是 open不論子節點狀態如何先將該節點加入結果
result[node.Data.ID.Hex()] = node.Data
// 遞迴處理子節點
for _, child := range node.Children {
dfs(child)
}
return true
}
// 從 dummy root 的 Children 開始走訪dummy root 本身不納入結果)
for _, child := range tree.root.Children {
dfs(child)
}
return result, nil
}
//// getFullParentPermissionIDs
//// 根據傳入的權限狀態 (Permissions) 回傳完整的權限 ID 列表,包含所有祖先。
//// 如果某個節點為非葉節點,則會檢查其子節點是否有啟用,否則該節點不會被展開。
//func (tree *PermissionTree) getFullParentPermissionIDs(permissions permission.Permissions) ([]string, error) {
// tree.mu.RLock()
// defer tree.mu.RUnlock()
//
// exist := make(map[string]bool)
// var ids []string
//
// for name, status := range permissions {
// if status != permission.OpenPermission {
// continue
// }
// idList, ok := tree.nameToIDs[name]
// if !ok {
// return nil, NotFoundError
// }
// for _, pid := range idList {
// node, exists := tree.nodes[pid]
// if !exists || node == nil {
// return nil, NotFoundError
// }
// // 如果為父節點,檢查其子節點是否有啟用,若都關閉則不展開
// if len(node.Children) > 0 {
// var childOpen bool
// for _, child := range node.Children {
// if childStatus, ok := permissions[child.Data.Name]; ok && childStatus == permission.OpenPermission {
// childOpen = true
// break
// }
// }
// if !childOpen {
// continue
// }
// }
// // 將該節點及所有祖先(直到 dummy root不包括 dummy root加入結果
// for cur := node; cur != nil && cur.Data.Name != "root"; cur = cur.Parent {
// if !exist[cur.Data.ID.Hex()] {
// ids = append(ids, cur.Data.ID.Hex())
// exist[cur.Data.ID.Hex()] = true
// }
// }
// }
// }
//
// return ids, nil
//}
//// getFullParentPermissionStatus
//// 根據傳入的權限狀態 (Permissions) 回傳完整的權限狀態,包含所有祖先的名稱設為啟用。
//func (tree *PermissionTree) getFullParentPermissionStatus(permissions permission.Permissions) (permission.Permissions, error) {
// tree.mu.RLock()
// defer tree.mu.RUnlock()
//
// result := make(permission.Permissions)
// exist := make(map[string]bool)
//
// for name, status := range permissions {
// if status != permission.OpenPermission {
// continue
// }
// idList, ok := tree.nameToIDs[name]
// if !ok {
// return nil, NotFoundError
// }
// for _, pid := range idList {
// node, exists := tree.nodes[pid]
// if !exists || node == nil {
// return nil, NotFoundError
// }
// // 將該節點及所有祖先標記為啟用
// for cur := node; cur != nil && cur.Data.Name != "root"; cur = cur.Parent {
// if !exist[cur.Data.ID.Hex()] {
// result[cur.Data.Name] = permission.OpenPermission
// exist[cur.Data.ID.Hex()] = true
// }
// }
// }
// }
//
// return result, nil
//}
//
//// getFullParentPermission
//// 根據角色權限 (RolePermission) 列表,回傳完整的權限狀態(名稱->狀態),包含所有祖先
//func (tree *PermissionTree) getFullParentPermission(rolePermissions []entity.RolePermission) permission.Permissions {
// tree.mu.RLock()
// defer tree.mu.RUnlock()
//
// result := make(permission.Permissions)
// for _, rp := range rolePermissions {
// node, ok := tree.nodes[rp.PermissionID]
// if !ok || node == nil {
// continue
// }
// // 將該節點及所有祖先設為啟用
// for cur := node; cur != nil && cur.Data.Name != "root"; cur = cur.Parent {
// result[cur.Data.Name] = permission.OpenPermission
// }
// }
// return result
//}

View File

@ -0,0 +1,358 @@
package usecase
import (
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission"
"go.mongodb.org/mongo-driver/bson/primitive"
"sync"
"testing"
)
// TestGeneratePermissionTree 測試 GeneratePermissionTree 函數的建立樹功能
func TestGeneratePermissionTree(t *testing.T) {
// 準備測試資料
// p1 為根節點Parent 為空)
p1ID := primitive.NewObjectID()
p1 := entity.Permission{
ID: p1ID,
Parent: "",
Name: "A",
}
// p2 的父節點為 p1
p2ID := primitive.NewObjectID()
p2 := entity.Permission{
ID: p2ID,
Parent: p1ID.Hex(),
Name: "B",
}
// p3 為另一個根節點Parent 為空)
p3ID := primitive.NewObjectID()
p3 := entity.Permission{
ID: p3ID,
Parent: "",
Name: "C",
}
// p4 的 Parent 填寫一個不存在的 id預期會掛在 dummy root 下
p4ID := primitive.NewObjectID()
p4 := entity.Permission{
ID: p4ID,
Parent: "nonexistent",
Name: "D",
}
permissions := []entity.Permission{p1, p2, p3, p4}
// 建立樹
tree := GeneratePermissionTree(permissions)
// 驗證 dummy root 下的子節點
// 預期 p1、p3、p4 均掛在 dummy root 下
if len(tree.root.Children) != 3 {
t.Errorf("expected 3 children under dummy root, got %d", len(tree.root.Children))
}
// 驗證 p1 的節點是否正確
nodeP1, ok := tree.nodes[p1ID.Hex()]
if !ok {
t.Errorf("node for permission A (p1) not found")
} else {
// p1 應該有一個子節點 p2
if len(nodeP1.Children) != 1 {
t.Errorf("expected node A to have 1 child, got %d", len(nodeP1.Children))
} else {
if nodeP1.Children[0].Data.ID != p2ID {
t.Errorf("expected node A child to be permission B (p2), got %v", nodeP1.Children[0].Data.ID.Hex())
}
}
}
// 驗證 tree.names 的對應關係
checkNames := []struct {
name string
expectedID string
}{
{"A", p1ID.Hex()},
{"B", p2ID.Hex()},
{"C", p3ID.Hex()},
{"D", p4ID.Hex()},
}
for _, cn := range checkNames {
ids, ok := tree.names[cn.name]
if !ok {
t.Errorf("name mapping for %s not found", cn.name)
continue
}
if len(ids) != 1 || ids[0] != cn.expectedID {
t.Errorf("expected name mapping for %s to be [%s], got %v", cn.name, cn.expectedID, ids)
}
}
}
func TestGetNode(t *testing.T) {
// 建立一個測試用的 PermissionTree
tree := &PermissionTree{
nodes: make(map[string]*PermissionNode),
names: make(map[string][]string),
}
id := primitive.NewObjectID()
// 建立一個測試節點ID 為 "testID"
perm := entity.Permission{
ID: id,
Name: "Test Permission",
}
node := &PermissionNode{
Data: perm,
}
// 將測試節點插入 tree 的 nodes map
tree.mu.Lock()
tree.nodes["testID"] = node
tree.mu.Unlock()
// 測試 getNode 返回存在的節點
got := tree.getNode("testID")
if got == nil {
t.Error("Expected to find node with id 'testID', but got nil")
} else if got.Data.ID.Hex() != id.Hex() {
t.Errorf("Expected node ID 'testID', got '%s'", got.Data.ID)
}
// 測試對不存在的 id應回傳 nil
gotNil := tree.getNode("nonexistent")
if gotNil != nil {
t.Errorf("Expected nil for non-existent node, got %+v", gotNil)
}
}
func TestPut(t *testing.T) {
// 建立一個 PermissionTree並初始化 dummy root
tree := &PermissionTree{
nodes: make(map[string]*PermissionNode),
names: make(map[string][]string),
}
// 建立 dummy root 節點,其 ID 為一個隨機 ObjectID但不參與 mapping
dummyRootPerm := entity.Permission{
ID: primitive.NewObjectID(),
Parent: "",
Name: "root",
}
tree.root = &PermissionNode{
Data: dummyRootPerm,
Children: make([]*PermissionNode, 0),
}
// 測試 1放入一筆 Parent 為空的節點,預期掛在 dummy root 下
permA := entity.Permission{
ID: primitive.NewObjectID(),
Parent: "", // 無父節點
Name: "A",
}
tree.put(permA)
nodeA := tree.getNode(permA.ID.Hex())
if nodeA == nil {
t.Errorf("Expected to find node A in tree.nodes")
}
if nodeA.Parent != tree.root {
t.Errorf("Expected node A's parent to be dummy root, got %v", nodeA.Parent.Data.Name)
}
if ids, ok := tree.names["A"]; !ok || len(ids) != 1 || ids[0] != permA.ID.Hex() {
t.Errorf("Expected tree.names for 'A' to contain %s, got %v", permA.ID.Hex(), tree.names["A"])
}
// 測試 2放入一筆 Parent 為存在節點的節點
// 先放入父節點 permB
permB := entity.Permission{
ID: primitive.NewObjectID(),
Parent: "", // 掛在 dummy root 下
Name: "B",
}
tree.put(permB)
nodeB := tree.getNode(permB.ID.Hex())
if nodeB == nil {
t.Errorf("Expected to find node B in tree.nodes")
}
// 再放入子節點 permC其 Parent 為 permB.ID.Hex()
permC := entity.Permission{
ID: primitive.NewObjectID(),
Parent: permB.ID.Hex(),
Name: "C",
}
tree.put(permC)
nodeC := tree.getNode(permC.ID.Hex())
if nodeC == nil {
t.Errorf("Expected to find node C in tree.nodes")
}
if nodeC.Parent != nodeB {
t.Errorf("Expected node C's parent to be node B")
}
// 驗證 nodeB 的 Children 是否包含 nodeC
found := false
for _, child := range nodeB.Children {
if child.Data.ID == permC.ID {
found = true
break
}
}
if !found {
t.Errorf("Expected node B's children to contain node C")
}
}
func TestFilterOpenNodes(t *testing.T) {
// 建立一個 PermissionTree初始化 nodes 與 names並建立 dummy root 節點
tree := &PermissionTree{
nodes: make(map[string]*PermissionNode),
names: make(map[string][]string),
mu: sync.RWMutex{},
}
// 建立 dummy root 節點
dummyRootPerm := entity.Permission{
ID: primitive.NewObjectID(),
Parent: "",
Name: "root",
Status: permission.Open, // dummy root 狀態不影響結果
}
tree.root = &PermissionNode{
Data: dummyRootPerm,
Children: make([]*PermissionNode, 0),
}
// 建立測試節點
// Node AOpen, leaf
permA := entity.Permission{
ID: primitive.NewObjectID(),
Parent: "", // 無父節點 → 掛在 dummy root 下
Name: "A",
Status: permission.Open,
}
tree.put(permA)
// Node BOpen, non-leaf
permB := entity.Permission{
ID: primitive.NewObjectID(),
Parent: "", // 掛在 dummy root 下
Name: "B",
Status: permission.Open,
}
tree.put(permB)
// Node B1Open, leaf, Parent = B
permB1 := entity.Permission{
ID: primitive.NewObjectID(),
Parent: permB.ID.Hex(),
Name: "B1",
Status: permission.Open,
}
tree.put(permB1)
// Node B2Closed, leaf, Parent = B
permB2 := entity.Permission{
ID: primitive.NewObjectID(),
Parent: permB.ID.Hex(),
Name: "B2",
Status: permission.Close,
}
tree.put(permB2)
// Node COpen, non-leaf但其子節點皆 Closed → C 不會展開
permC := entity.Permission{
ID: primitive.NewObjectID(),
Parent: "", // 掛在 dummy root 下
Name: "C",
Status: permission.Close,
}
tree.put(permC)
// Node C1Closed, leaf, Parent = C
permC1 := entity.Permission{
ID: primitive.NewObjectID(),
Parent: permC.ID.Hex(),
Name: "C1",
Status: permission.Open,
}
tree.put(permC1)
// Node C2Closed, leaf, Parent = C
permC2 := entity.Permission{
ID: primitive.NewObjectID(),
Parent: permC.ID.Hex(),
Name: "C2",
Status: permission.Open,
}
tree.put(permC2)
// Node DClosed, leaf, 無父節點
permD := entity.Permission{
ID: primitive.NewObjectID(),
Parent: "",
Name: "D",
Status: permission.Close,
}
tree.put(permD)
// Node EClosed, leaf, 無父節點
permE := entity.Permission{
ID: primitive.NewObjectID(),
Parent: "",
Name: "E",
Status: permission.Open,
}
tree.put(permE)
// Node E1Closed, leaf, Parent = E
permE1 := entity.Permission{
ID: primitive.NewObjectID(),
Parent: permE.ID.Hex(),
Name: "E1",
Status: permission.Close,
}
tree.put(permE1)
// Node E2Closed, leaf, Parent = E
permE2 := entity.Permission{
ID: primitive.NewObjectID(),
Parent: permE.ID.Hex(),
Name: "E2",
Status: permission.Close,
}
tree.put(permE2)
// 執行 filterOpenNodes
openNodes, err := tree.filterOpenNodes()
if err != nil {
t.Fatalf("filterOpenNodes returned error: %v", err)
}
// 預期結果:
// - Node A 應該包含open 且為葉節點)
// - Node B 應該包含open 且其子節點 B1 為 open
// - Node B1 應該包含open, leaf
// - Node B2 不包含closed
// - Node C 不包含closed
// - Node C1, C2 不包含C Node Close
// - Node D 不包含closed
// - Node E 包含open
// - Node E1, E2 不包含本身Close
expectedIDs := map[string]bool{
permA.ID.Hex(): true,
permB.ID.Hex(): true,
permB1.ID.Hex(): true,
permE.ID.Hex(): true,
}
// 檢查結果是否只包含預期的節點
for id, perm := range openNodes {
if !expectedIDs[id] {
t.Errorf("Unexpected node in openNodes: id=%s, name=%s", id, perm.Name)
}
delete(expectedIDs, id)
}
if len(expectedIDs) != 0 {
t.Errorf("Expected nodes not found in openNodes: %v", expectedIDs)
}
}