diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e660fd9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bin/ diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..73e9192 --- /dev/null +++ b/build.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# Run this script after the system recovers from the fork resource issue. +# It fetches new dependencies, builds, and runs tests. + +set -e +cd "$(dirname "$0")" + +export GOPRIVATE=github.com/GoCodeAlone/* + +echo "==> go mod tidy" +go mod tidy + +echo "==> go build" +go build ./... + +echo "==> go vet" +go vet ./... + +echo "==> go test (race detector)" +go test ./... -v -race -count=1 diff --git a/go.mod b/go.mod index 5d9df5f..52c48fb 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,11 @@ go 1.26 require ( github.com/GoCodeAlone/workflow v0.2.2 github.com/casbin/casbin/v2 v2.104.0 + github.com/casbin/gorm-adapter/v3 v3.24.0 + gorm.io/driver/mysql v1.5.7 + gorm.io/driver/postgres v1.5.11 + gorm.io/driver/sqlite v1.5.7 + gorm.io/gorm v1.25.12 ) require ( @@ -74,10 +79,15 @@ require ( github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/glebarez/go-sqlite v1.20.3 // indirect + github.com/glebarez/sqlite v1.7.0 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/golobby/cast v1.3.3 // indirect github.com/google/s2a-go v0.1.9 // indirect @@ -109,10 +119,14 @@ require ( github.com/jcmturner/gofork v1.7.6 // indirect github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect github.com/jcmturner/rpc/v2 v2.0.3 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.3 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/microsoft/go-mssqldb v1.6.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -169,6 +183,8 @@ require ( google.golang.org/grpc v1.79.1 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/sqlserver v1.5.3 // indirect + gorm.io/plugin/dbresolver v1.3.0 // indirect modernc.org/libc v1.67.6 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index bf41053..f3368b0 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,26 @@ cloud.google.com/go/storage v1.60.0 h1:oBfZrSOCimggVNz9Y/bXY35uUcts7OViubeddTTVz cloud.google.com/go/storage v1.60.0/go.mod h1:q+5196hXfejkctrnx+VYU8RKQr/L3c0cBIlrjmiAKE0= cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1 h1:/iHxaJhsFr0+xVFfbMr5vxz848jyiWuIEDhYq3y5odY= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0 h1:yfJe15aSwEQ6Oo6J+gdfdulPNoZ3TEhmbhLIoxZcA+U= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0 h1:T028gtTPiYt/RMUfs8nVsAL7FDQrfLlrm/NnRG/zcC4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0 h1:HCc0+LpPfpCKs6LGGLAhwBARt9632unrVcI6i8s/8os= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CrisisTextLine/modular v1.11.11 h1:6rx271wWZ1r+RoPWuQRmhvpd5kmgGPAk1qYlX3kFsYs= @@ -107,6 +125,8 @@ github.com/bufbuild/protocompile v0.10.0 h1:+jW/wnLMLxaCEG8AX9lD0bQ5v9h1RUiMKOBO github.com/bufbuild/protocompile v0.10.0/go.mod h1:G9qQIQo0xZ6Uyj6CMNz0saGmx2so+KONo8/KrELABiY= github.com/casbin/casbin/v2 v2.104.0 h1:qDakyBZ4jUg1VskF1+UzIwkg+uXWcp0u0M9PMm1RsTA= github.com/casbin/casbin/v2 v2.104.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco= +github.com/casbin/gorm-adapter/v3 v3.24.0 h1:WeLetCTkS1V4zpqF+UJ87PnDOYvdA8K3qp+T/Fj31+E= +github.com/casbin/gorm-adapter/v3 v3.24.0/go.mod h1:aftWi0cla0CC1bHQVrSFzBcX/98IFK28AvuPppCQgTs= github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc= github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -140,6 +160,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r 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/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= @@ -171,6 +193,10 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4= +github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0= +github.com/glebarez/sqlite v1.7.0 h1:A7Xj/KN2Lvie4Z4rrgQHY8MsbebX3NyWsL3n2i82MVI= +github.com/glebarez/sqlite v1.7.0/go.mod h1:PkeevrRlF/1BhQBCnzcMWzgrIk7IOop+qS2jUYLfHhk= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -178,12 +204,23 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/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-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= @@ -204,6 +241,7 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17k github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= @@ -276,6 +314,11 @@ github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZ github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jhump/protoreflect v1.16.0 h1:54fZg+49widqXYQ0b+usAFHbMkBGR4PpXrsHc8+TBDg= github.com/jhump/protoreflect v1.16.0/go.mod h1:oYPd7nPvcBw/5wlDfm/AVmU9zH9BgqGCI469pGxfj/8= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= @@ -289,6 +332,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= @@ -297,6 +344,10 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 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/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/microsoft/go-mssqldb v1.6.0 h1:mM3gYdVwEPFrlg/Dvr2DNVEgYFG7L42l+dGc67NNNpc= +github.com/microsoft/go-mssqldb v1.6.0/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU= github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk= github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -318,6 +369,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g= @@ -340,6 +393,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -387,6 +442,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 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= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -433,25 +490,34 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= @@ -459,6 +525,7 @@ golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwE golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -466,11 +533,13 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/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-20210124154548-22da62e12c0c/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-20210616045830-e2b7044e8c71/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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -479,15 +548,23 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= @@ -497,6 +574,7 @@ golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -519,10 +597,30 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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= +gorm.io/driver/mysql v1.3.2/go.mod h1:ChK6AHbHgDCFZyJp0F+BmVGb06PSIoh9uVYKAlRbb2U= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= +gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/driver/sqlserver v1.5.3 h1:rjupPS4PVw+rjJkfvr8jn2lJ8BMhT4UW5FwuJY0P3Z0= +gorm.io/driver/sqlserver v1.5.3/go.mod h1:B+CZ0/7oFJ6tAlefsKoyxdgDCXJKSgwS2bMOQZT0I00= +gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= +gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +gorm.io/plugin/dbresolver v1.3.0 h1:uFDX3bIuH9Lhj5LY2oyqR/bU6pqWuDgas35NAPF4X3M= +gorm.io/plugin/dbresolver v1.3.0/go.mod h1:Pr7p5+JFlgDaiM6sOrli5olekJD16YRunMyA2S7ZfKk= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= diff --git a/internal/module_casbin.go b/internal/module_casbin.go index 50527d0..dbe5d7b 100644 --- a/internal/module_casbin.go +++ b/internal/module_casbin.go @@ -5,29 +5,68 @@ import ( "fmt" "strings" "sync" + "time" "github.com/casbin/casbin/v2" "github.com/casbin/casbin/v2/model" "github.com/casbin/casbin/v2/persist" + fileadapter "github.com/casbin/casbin/v2/persist/file-adapter" + gormadapter "github.com/casbin/gorm-adapter/v3" + "gorm.io/driver/mysql" + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + gormlogger "gorm.io/gorm/logger" ) // CasbinModule implements sdk.ModuleInstance and holds a Casbin enforcer -// loaded from inline config (model text + policy rows + role assignments). +// loaded from inline config (model text + policy rows + role assignments), +// a file adapter, or a GORM adapter backed by postgres/mysql/sqlite3. type CasbinModule struct { name string config casbinConfig mu sync.RWMutex enforcer *casbin.Enforcer + + // polling watcher fields + stopCh chan struct{} + doneCh chan struct{} +} + +// adapterConfig describes the storage backend for policies. +type adapterConfig struct { + // Type is "memory" (default), "file", or "gorm". + Type string `yaml:"type"` + + // File adapter fields. + Path string `yaml:"path"` + + // GORM adapter fields. + Driver string `yaml:"driver"` // "postgres", "mysql", or "sqlite3" + DSN string `yaml:"dsn"` + TableName string `yaml:"table_name"` // optional; defaults to "casbin_rule" +} + +// watcherConfig describes the optional polling reload behaviour. +type watcherConfig struct { + // Type is "none" (default) or "polling". + Type string `yaml:"type"` + // Interval is the reload interval for polling watcher (default 30s). + Interval time.Duration `yaml:"interval"` } // casbinConfig holds the parsed configuration for an authz.casbin module. type casbinConfig struct { // Model is a PERM model definition (ini-style text). Model string `yaml:"model"` - // Policies is a list of [sub, obj, act] policy rows. + // Policies is a list of [sub, obj, act] policy rows (memory adapter only). Policies [][]string `yaml:"policies"` - // RoleAssignments is a list of [user, role] grouping rows. + // RoleAssignments is a list of [user, role] grouping rows (memory adapter only). RoleAssignments [][]string `yaml:"roleAssignments"` + // Adapter describes the storage backend. + Adapter adapterConfig `yaml:"adapter"` + // Watcher describes the optional polling watcher. + Watcher watcherConfig `yaml:"watcher"` } // newCasbinModule parses the config map and returns a CasbinModule. @@ -78,9 +117,42 @@ func parseCasbinConfig(raw map[string]any) (casbinConfig, error) { } } + // Parse adapter section. + if adapterRaw, ok := raw["adapter"].(map[string]any); ok { + cfg.Adapter = parseAdapterConfig(adapterRaw) + } + + // Parse watcher section. + if watcherRaw, ok := raw["watcher"].(map[string]any); ok { + cfg.Watcher = parseWatcherConfig(watcherRaw) + } + return cfg, nil } +// parseAdapterConfig parses the adapter sub-map. +func parseAdapterConfig(raw map[string]any) adapterConfig { + var a adapterConfig + a.Type, _ = raw["type"].(string) + a.Path, _ = raw["path"].(string) + a.Driver, _ = raw["driver"].(string) + a.DSN, _ = raw["dsn"].(string) + a.TableName, _ = raw["table_name"].(string) + return a +} + +// parseWatcherConfig parses the watcher sub-map. +func parseWatcherConfig(raw map[string]any) watcherConfig { + var w watcherConfig + w.Type, _ = raw["type"].(string) + if iv, ok := raw["interval"].(string); ok && iv != "" { + if d, err := time.ParseDuration(iv); err == nil { + w.Interval = d + } + } + return w +} + // toStringSlice converts an []any (from YAML/JSON) to []string. func toStringSlice(v any) ([]string, error) { switch t := v.(type) { @@ -101,7 +173,67 @@ func toStringSlice(v any) ([]string, error) { } } -// Init builds the Casbin enforcer from inline model and policies. +// buildAdapter returns a persist.Adapter for the configured backend. +func (m *CasbinModule) buildAdapter() (persist.Adapter, error) { + switch strings.ToLower(m.config.Adapter.Type) { + case "", "memory": + return newInMemoryAdapter(m.config.Policies, m.config.RoleAssignments), nil + + case "file": + if m.config.Adapter.Path == "" { + return nil, fmt.Errorf("authz.casbin %q: adapter.path is required for file adapter", m.name) + } + return fileadapter.NewAdapter(m.config.Adapter.Path), nil + + case "gorm": + return m.buildGORMAdapter() + + default: + return nil, fmt.Errorf("authz.casbin %q: unknown adapter type %q", m.name, m.config.Adapter.Type) + } +} + +// buildGORMAdapter opens a GORM connection and returns a gorm-adapter. +func (m *CasbinModule) buildGORMAdapter() (persist.Adapter, error) { + if m.config.Adapter.DSN == "" { + return nil, fmt.Errorf("authz.casbin %q: adapter.dsn is required for gorm adapter", m.name) + } + + silentLogger := gormlogger.Default.LogMode(gormlogger.Silent) + gormCfg := &gorm.Config{Logger: silentLogger} + + var db *gorm.DB + var err error + + switch strings.ToLower(m.config.Adapter.Driver) { + case "postgres", "postgresql": + db, err = gorm.Open(postgres.Open(m.config.Adapter.DSN), gormCfg) + case "mysql": + db, err = gorm.Open(mysql.Open(m.config.Adapter.DSN), gormCfg) + case "sqlite3", "sqlite": + db, err = gorm.Open(sqlite.Open(m.config.Adapter.DSN), gormCfg) + default: + return nil, fmt.Errorf("authz.casbin %q: unsupported gorm driver %q (supported: postgres, mysql, sqlite3)", m.name, m.config.Adapter.Driver) + } + if err != nil { + return nil, fmt.Errorf("authz.casbin %q: open gorm db: %w", m.name, err) + } + + // Use NewAdapterByDBUseTableName with empty prefix. + // When table_name is empty, the gorm-adapter defaults to "casbin_rule". + tableName := m.config.Adapter.TableName + if tableName == "" { + tableName = "casbin_rule" + } + + a, err := gormadapter.NewAdapterByDBUseTableName(db, "", tableName) + if err != nil { + return nil, fmt.Errorf("authz.casbin %q: create gorm adapter: %w", m.name, err) + } + return a, nil +} + +// Init builds the Casbin enforcer from the configured adapter. func (m *CasbinModule) Init() error { m.mu.Lock() defer m.mu.Unlock() @@ -111,7 +243,11 @@ func (m *CasbinModule) Init() error { return fmt.Errorf("authz.casbin %q: parse model: %w", m.name, err) } - adapter := newInMemoryAdapter(m.config.Policies, m.config.RoleAssignments) + adapter, err := m.buildAdapter() + if err != nil { + return fmt.Errorf("authz.casbin %q: build adapter: %w", m.name, err) + } + e, err := casbin.NewEnforcer(md, adapter) if err != nil { return fmt.Errorf("authz.casbin %q: create enforcer: %w", m.name, err) @@ -121,11 +257,63 @@ func (m *CasbinModule) Init() error { return nil } -// Start is a no-op; the enforcer is ready after Init. -func (m *CasbinModule) Start(_ context.Context) error { return nil } +// Start begins the polling watcher goroutine if watcher.type is "polling". +func (m *CasbinModule) Start(_ context.Context) error { + if strings.ToLower(m.config.Watcher.Type) != "polling" { + return nil + } + + interval := m.config.Watcher.Interval + if interval <= 0 { + interval = 30 * time.Second + } -// Stop is a no-op. -func (m *CasbinModule) Stop(_ context.Context) error { return nil } + m.mu.Lock() + m.stopCh = make(chan struct{}) + m.doneCh = make(chan struct{}) + m.mu.Unlock() + + go m.pollLoop(interval) + return nil +} + +// pollLoop reloads policies from the adapter on each tick. +func (m *CasbinModule) pollLoop(interval time.Duration) { + defer close(m.doneCh) + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-m.stopCh: + return + case <-ticker.C: + m.mu.Lock() + if m.enforcer != nil { + _ = m.enforcer.LoadPolicy() + } + m.mu.Unlock() + } + } +} + +// Stop shuts down the polling watcher if running. +func (m *CasbinModule) Stop(_ context.Context) error { + m.mu.RLock() + stopCh := m.stopCh + doneCh := m.doneCh + m.mu.RUnlock() + + if stopCh != nil { + close(stopCh) + <-doneCh + m.mu.Lock() + m.stopCh = nil + m.doneCh = nil + m.mu.Unlock() + } + return nil +} // Enforce checks whether sub can perform act on obj. // It is safe for concurrent use. @@ -139,26 +327,119 @@ func (m *CasbinModule) Enforce(sub, obj, act string) (bool, error) { return e.Enforce(sub, obj, act) } +// AddPolicy adds a policy rule and saves it to the adapter. +func (m *CasbinModule) AddPolicy(rule []string) (bool, error) { + m.mu.Lock() + defer m.mu.Unlock() + if m.enforcer == nil { + return false, fmt.Errorf("authz.casbin %q: enforcer not initialized", m.name) + } + ok, err := m.enforcer.AddPolicy(toInterfaceSlice(rule)...) + if err != nil { + return false, err + } + if ok { + if err := m.enforcer.SavePolicy(); err != nil { + return false, err + } + } + return ok, nil +} + +// RemovePolicy removes a policy rule and saves the adapter. +func (m *CasbinModule) RemovePolicy(rule []string) (bool, error) { + m.mu.Lock() + defer m.mu.Unlock() + if m.enforcer == nil { + return false, fmt.Errorf("authz.casbin %q: enforcer not initialized", m.name) + } + ok, err := m.enforcer.RemovePolicy(toInterfaceSlice(rule)...) + if err != nil { + return false, err + } + if ok { + if err := m.enforcer.SavePolicy(); err != nil { + return false, err + } + } + return ok, nil +} + +// AddGroupingPolicy adds a role mapping and saves the adapter. +func (m *CasbinModule) AddGroupingPolicy(rule []string) (bool, error) { + m.mu.Lock() + defer m.mu.Unlock() + if m.enforcer == nil { + return false, fmt.Errorf("authz.casbin %q: enforcer not initialized", m.name) + } + ok, err := m.enforcer.AddGroupingPolicy(toInterfaceSlice(rule)...) + if err != nil { + return false, err + } + if ok { + if err := m.enforcer.SavePolicy(); err != nil { + return false, err + } + } + return ok, nil +} + +// RemoveGroupingPolicy removes a role mapping and saves the adapter. +func (m *CasbinModule) RemoveGroupingPolicy(rule []string) (bool, error) { + m.mu.Lock() + defer m.mu.Unlock() + if m.enforcer == nil { + return false, fmt.Errorf("authz.casbin %q: enforcer not initialized", m.name) + } + ok, err := m.enforcer.RemoveGroupingPolicy(toInterfaceSlice(rule)...) + if err != nil { + return false, err + } + if ok { + if err := m.enforcer.SavePolicy(); err != nil { + return false, err + } + } + return ok, nil +} + +// toInterfaceSlice converts []string to []interface{} for casbin variadic calls. +func toInterfaceSlice(ss []string) []interface{} { + out := make([]interface{}, len(ss)) + for i, s := range ss { + out[i] = s + } + return out +} + // Name returns the module name. func (m *CasbinModule) Name() string { return m.name } // --- in-memory Casbin adapter --- -// inMemoryAdapter implements persist.Adapter with an in-memory policy store. +// inMemoryAdapter implements persist.Adapter with an in-memory policy store +// that is fully mutable: AddPolicy / RemovePolicy / SavePolicy all work. type inMemoryAdapter struct { + mu sync.RWMutex policies [][]string roleAssignments [][]string } -func newInMemoryAdapter(policies, roleAssignments [][]string) persist.Adapter { +func newInMemoryAdapter(policies, roleAssignments [][]string) *inMemoryAdapter { + p := make([][]string, len(policies)) + copy(p, policies) + r := make([][]string, len(roleAssignments)) + copy(r, roleAssignments) return &inMemoryAdapter{ - policies: policies, - roleAssignments: roleAssignments, + policies: p, + roleAssignments: r, } } // LoadPolicy loads all policy rules into the model. func (a *inMemoryAdapter) LoadPolicy(m model.Model) error { + a.mu.RLock() + defer a.mu.RUnlock() for _, p := range a.policies { line := "p, " + strings.Join(p, ", ") if err := persist.LoadPolicyLine(line, m); err != nil { @@ -174,22 +455,124 @@ func (a *inMemoryAdapter) LoadPolicy(m model.Model) error { return nil } -// SavePolicy is not supported for the in-memory adapter (read-only config). -func (a *inMemoryAdapter) SavePolicy(_ model.Model) error { - return fmt.Errorf("inMemoryAdapter: SavePolicy is not supported") +// SavePolicy persists the current model state back to the in-memory store. +// This replaces a.policies and a.roleAssignments with whatever the model holds. +func (a *inMemoryAdapter) SavePolicy(m model.Model) error { + a.mu.Lock() + defer a.mu.Unlock() + + a.policies = nil + if pMap, ok := m["p"]; ok { + if policy, ok := pMap["p"]; ok { + for _, tokens := range policy.Policy { + row := make([]string, len(tokens)) + copy(row, tokens) + a.policies = append(a.policies, row) + } + } + } + + a.roleAssignments = nil + if gMap, ok := m["g"]; ok { + if grp, ok := gMap["g"]; ok { + for _, tokens := range grp.Policy { + row := make([]string, len(tokens)) + copy(row, tokens) + a.roleAssignments = append(a.roleAssignments, row) + } + } + } + + return nil } -// AddPolicy is not supported. -func (a *inMemoryAdapter) AddPolicy(_ string, _ string, _ []string) error { - return fmt.Errorf("inMemoryAdapter: AddPolicy is not supported") +// AddPolicy appends a policy row to the in-memory store. +// sec is "p" for normal policies, "g" for role assignments. +// SavePolicy is always called after AddPolicy by CasbinModule methods, which +// overwrites both slices from the model, so we append to the correct bucket +// here to keep the intermediate state consistent. +func (a *inMemoryAdapter) AddPolicy(sec string, _ string, rule []string) error { + a.mu.Lock() + defer a.mu.Unlock() + row := make([]string, len(rule)) + copy(row, rule) + if sec == "g" { + a.roleAssignments = append(a.roleAssignments, row) + } else { + a.policies = append(a.policies, row) + } + return nil } -// RemovePolicy is not supported. -func (a *inMemoryAdapter) RemovePolicy(_ string, _ string, _ []string) error { - return fmt.Errorf("inMemoryAdapter: RemovePolicy is not supported") +// RemovePolicy removes a policy row from the in-memory store. +func (a *inMemoryAdapter) RemovePolicy(sec string, _ string, rule []string) error { + a.mu.Lock() + defer a.mu.Unlock() + if sec == "g" { + a.roleAssignments = removeRow(a.roleAssignments, rule) + } else { + a.policies = removeRow(a.policies, rule) + } + return nil } -// RemoveFilteredPolicy is not supported. -func (a *inMemoryAdapter) RemoveFilteredPolicy(_ string, _ string, _ int, _ ...string) error { - return fmt.Errorf("inMemoryAdapter: RemoveFilteredPolicy is not supported") +// RemoveFilteredPolicy removes rows matching the prefix filter. +func (a *inMemoryAdapter) RemoveFilteredPolicy(sec string, _ string, fieldIndex int, fieldValues ...string) error { + a.mu.Lock() + defer a.mu.Unlock() + if sec == "g" { + var kept [][]string + for _, row := range a.roleAssignments { + if !matchesFilter(row, fieldIndex, fieldValues) { + kept = append(kept, row) + } + } + a.roleAssignments = kept + } else { + var kept [][]string + for _, row := range a.policies { + if !matchesFilter(row, fieldIndex, fieldValues) { + kept = append(kept, row) + } + } + a.policies = kept + } + return nil +} + +// removeRow removes the first row that equals target (element-wise). +func removeRow(rows [][]string, target []string) [][]string { + for i, row := range rows { + if sliceEqual(row, target) { + return append(rows[:i:i], rows[i+1:]...) + } + } + return rows +} + +// matchesFilter returns true when row[fieldIndex:fieldIndex+len(values)] == values. +func matchesFilter(row []string, fieldIndex int, values []string) bool { + for i, v := range values { + idx := fieldIndex + i + if idx >= len(row) { + return false + } + if v != "" && row[idx] != v { + return false + } + } + return true +} + +// sliceEqual reports whether a and b have identical elements. +func sliceEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true } diff --git a/internal/module_casbin_storage_test.go b/internal/module_casbin_storage_test.go new file mode 100644 index 0000000..3681e9c --- /dev/null +++ b/internal/module_casbin_storage_test.go @@ -0,0 +1,325 @@ +package internal + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" +) + +// --- file adapter tests --- + +func TestFileAdapter_LoadPolicy(t *testing.T) { + dir := t.TempDir() + csvPath := filepath.Join(dir, "policy.csv") + + // Write a minimal CSV file (casbin file adapter format). + content := "p, admin, /api/*, *\ng, alice, admin\n" + if err := os.WriteFile(csvPath, []byte(content), 0644); err != nil { + t.Fatalf("write CSV: %v", err) + } + + m, err := newCasbinModule("authz", map[string]any{ + "model": testModel, + "adapter": map[string]any{ + "type": "file", + "path": csvPath, + }, + }) + if err != nil { + t.Fatalf("newCasbinModule: %v", err) + } + if err := m.Init(); err != nil { + t.Fatalf("Init: %v", err) + } + + // alice inherits admin → allowed + allowed, err := m.Enforce("alice", "/api/*", "*") + if err != nil { + t.Fatalf("Enforce: %v", err) + } + if !allowed { + t.Error("expected alice to be allowed with file adapter") + } +} + +func TestFileAdapter_MissingPath(t *testing.T) { + m, err := newCasbinModule("authz", map[string]any{ + "model": testModel, + "adapter": map[string]any{ + "type": "file", + // path intentionally omitted + }, + }) + if err != nil { + t.Fatalf("newCasbinModule: %v", err) + } + if err := m.Init(); err == nil { + t.Error("expected Init to fail when adapter.path is missing") + } +} + +// --- polling watcher tests --- + +func TestPollingWatcher_StartStop(t *testing.T) { + m := buildModule(t, + [][]string{{"admin", "/api/data", "GET"}}, + [][]string{{"alice", "admin"}}, + ) + + // Override watcher config to use a short interval. + m.config.Watcher = watcherConfig{ + Type: "polling", + Interval: 50 * time.Millisecond, + } + + ctx := context.Background() + if err := m.Start(ctx); err != nil { + t.Fatalf("Start: %v", err) + } + + // Let at least one reload tick fire. + time.Sleep(120 * time.Millisecond) + + if err := m.Stop(ctx); err != nil { + t.Fatalf("Stop: %v", err) + } + + // Ensure enforcement still works after stop. + allowed, err := m.Enforce("alice", "/api/data", "GET") + if err != nil { + t.Fatalf("Enforce after stop: %v", err) + } + if !allowed { + t.Error("expected alice to still be allowed after watcher stop") + } +} + +func TestPollingWatcher_StopBeforeStart(t *testing.T) { + m := buildModule(t, + [][]string{{"admin", "/", "GET"}}, + nil, + ) + // Stop without Start should be a no-op. + if err := m.Stop(context.Background()); err != nil { + t.Fatalf("Stop without Start: %v", err) + } +} + +func TestPollingWatcher_ReloadsPolicy(t *testing.T) { + // Use an in-memory module but verify that LoadPolicy is being called + // by checking that manually added policies are visible after a reload. + m := buildModule(t, + [][]string{{"viewer", "/news", "GET"}}, + nil, + ) + m.config.Watcher = watcherConfig{ + Type: "polling", + Interval: 30 * time.Millisecond, + } + + ctx := context.Background() + if err := m.Start(ctx); err != nil { + t.Fatalf("Start: %v", err) + } + defer func() { _ = m.Stop(ctx) }() + + // Directly add a policy to the underlying adapter (bypasses enforcer cache). + // The polling goroutine should call LoadPolicy which re-reads the adapter. + _, err := m.AddPolicy([]string{"editor", "/news", "POST"}) + if err != nil { + t.Fatalf("AddPolicy: %v", err) + } + + // Give at least two ticks for the policy to be reloaded. + time.Sleep(100 * time.Millisecond) + + allowed, err := m.Enforce("editor", "/news", "POST") + if err != nil { + t.Fatalf("Enforce: %v", err) + } + if !allowed { + t.Error("expected editor to be allowed POST /news after polling reload") + } +} + +// --- GORM adapter (SQLite in-memory) tests --- + +func TestGORMAdapter_SQLite(t *testing.T) { + m, err := newCasbinModule("authz", map[string]any{ + "model": testModel, + "adapter": map[string]any{ + "type": "gorm", + "driver": "sqlite3", + "dsn": ":memory:", + }, + }) + if err != nil { + t.Fatalf("newCasbinModule: %v", err) + } + if err := m.Init(); err != nil { + t.Fatalf("Init with gorm/sqlite: %v", err) + } + + // Fresh DB has no policies. + allowed, err := m.Enforce("alice", "/api", "GET") + if err != nil { + t.Fatalf("Enforce on empty gorm DB: %v", err) + } + if allowed { + t.Error("expected no access on empty gorm policy store") + } + + // Add a policy dynamically. + if _, err := m.AddPolicy([]string{"alice", "/api", "GET"}); err != nil { + t.Fatalf("AddPolicy (gorm): %v", err) + } + + allowed, err = m.Enforce("alice", "/api", "GET") + if err != nil { + t.Fatalf("Enforce after AddPolicy (gorm): %v", err) + } + if !allowed { + t.Error("expected alice to be allowed GET /api after AddPolicy in gorm") + } +} + +func TestGORMAdapter_UnknownDriver(t *testing.T) { + m, err := newCasbinModule("authz", map[string]any{ + "model": testModel, + "adapter": map[string]any{ + "type": "gorm", + "driver": "oracle", + "dsn": "whatever", + }, + }) + if err != nil { + t.Fatalf("newCasbinModule: %v", err) + } + if err := m.Init(); err == nil { + t.Error("expected Init to fail for unknown gorm driver") + } +} + +func TestGORMAdapter_MissingDSN(t *testing.T) { + m, err := newCasbinModule("authz", map[string]any{ + "model": testModel, + "adapter": map[string]any{ + "type": "gorm", + "driver": "sqlite3", + // dsn omitted + }, + }) + if err != nil { + t.Fatalf("newCasbinModule: %v", err) + } + if err := m.Init(); err == nil { + t.Error("expected Init to fail when adapter.dsn is missing") + } +} + +// --- inMemoryAdapter mutation tests --- + +func TestInMemoryAdapter_AddRemovePolicy(t *testing.T) { + m := buildModule(t, + [][]string{{"admin", "/api", "GET"}}, + [][]string{{"alice", "admin"}}, + ) + + // Add a new policy. + added, err := m.AddPolicy([]string{"editor", "/api/posts", "POST"}) + if err != nil { + t.Fatalf("AddPolicy: %v", err) + } + if !added { + t.Error("expected AddPolicy to return true") + } + + // bob is editor → can now POST /api/posts + _, _ = m.AddGroupingPolicy([]string{"bob", "editor"}) + + allowed, err := m.Enforce("bob", "/api/posts", "POST") + if err != nil { + t.Fatalf("Enforce after AddPolicy: %v", err) + } + if !allowed { + t.Error("expected bob to be allowed POST /api/posts after AddPolicy") + } + + // Remove the policy. + removed, err := m.RemovePolicy([]string{"editor", "/api/posts", "POST"}) + if err != nil { + t.Fatalf("RemovePolicy: %v", err) + } + if !removed { + t.Error("expected RemovePolicy to return true") + } + + // bob should no longer be allowed. + allowed, err = m.Enforce("bob", "/api/posts", "POST") + if err != nil { + t.Fatalf("Enforce after RemovePolicy: %v", err) + } + if allowed { + t.Error("expected bob to be denied after RemovePolicy") + } +} + +func TestInMemoryAdapter_AddGroupingPolicy(t *testing.T) { + m := buildModule(t, + [][]string{{"admin", "/admin", "GET"}}, + nil, + ) + + // dave has no role → denied + allowed, err := m.Enforce("dave", "/admin", "GET") + if err != nil { + t.Fatalf("Enforce before AddGroupingPolicy: %v", err) + } + if allowed { + t.Error("expected dave to be denied before role assignment") + } + + // Assign admin role to dave. + if _, err := m.AddGroupingPolicy([]string{"dave", "admin"}); err != nil { + t.Fatalf("AddGroupingPolicy: %v", err) + } + + allowed, err = m.Enforce("dave", "/admin", "GET") + if err != nil { + t.Fatalf("Enforce after AddGroupingPolicy: %v", err) + } + if !allowed { + t.Error("expected dave to be allowed after AddGroupingPolicy") + } + + // Remove the assignment. + if _, err := m.RemoveGroupingPolicy([]string{"dave", "admin"}); err != nil { + t.Fatalf("RemoveGroupingPolicy: %v", err) + } + + allowed, err = m.Enforce("dave", "/admin", "GET") + if err != nil { + t.Fatalf("Enforce after RemoveGroupingPolicy: %v", err) + } + if allowed { + t.Error("expected dave to be denied after RemoveGroupingPolicy") + } +} + +func TestInMemoryAdapter_NotInitialised_AddPolicy(t *testing.T) { + m := &CasbinModule{name: "uninit"} + _, err := m.AddPolicy([]string{"x", "/y", "z"}) + if err == nil { + t.Error("expected error from uninitialised module AddPolicy") + } +} + +func TestInMemoryAdapter_NotInitialised_RemovePolicy(t *testing.T) { + m := &CasbinModule{name: "uninit"} + _, err := m.RemovePolicy([]string{"x", "/y", "z"}) + if err == nil { + t.Error("expected error from uninitialised module RemovePolicy") + } +} diff --git a/internal/plugin.go b/internal/plugin.go index b7e961c..d42138c 100644 --- a/internal/plugin.go +++ b/internal/plugin.go @@ -49,7 +49,12 @@ func (p *authzPlugin) CreateModule(typeName, name string, config map[string]any) // StepTypes returns the step type names this plugin provides. func (p *authzPlugin) StepTypes() []string { - return []string{"step.authz_check"} + return []string{ + "step.authz_check", + "step.authz_add_policy", + "step.authz_remove_policy", + "step.authz_role_assign", + } } // CreateStep creates a step instance of the given type. @@ -57,6 +62,12 @@ func (p *authzPlugin) CreateStep(typeName, name string, config map[string]any) ( switch typeName { case "step.authz_check": return newAuthzCheckStep(name, config) + case "step.authz_add_policy": + return newAuthzAddPolicyStep(name, config) + case "step.authz_remove_policy": + return newAuthzRemovePolicyStep(name, config) + case "step.authz_role_assign": + return newAuthzRoleAssignStep(name, config) default: return nil, fmt.Errorf("authz plugin: unknown step type %q", typeName) } diff --git a/internal/step_authz_add_policy.go b/internal/step_authz_add_policy.go new file mode 100644 index 0000000..e8d5fb3 --- /dev/null +++ b/internal/step_authz_add_policy.go @@ -0,0 +1,131 @@ +package internal + +import ( + "bytes" + "context" + "fmt" + "text/template" + + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" +) + +// authzAddPolicyStep implements sdk.StepInstance. It adds a policy rule to the +// Casbin enforcer at runtime and persists it via the adapter's SavePolicy. +// +// Config: +// +// module: "authz" # name of the authz.casbin module (default: "authz") +// rule: ["admin", "/api/*", "*"] # policy rule; each element may be a Go template +type authzAddPolicyStep struct { + name string + moduleName string + ruleTmpls []*template.Template // one per rule element; nil entry = static string + ruleStatic []string // static value for the corresponding index (when tmpl is nil) + registry moduleRegistry +} + +func newAuthzAddPolicyStep(name string, config map[string]any) (*authzAddPolicyStep, error) { + s := &authzAddPolicyStep{ + name: name, + moduleName: "authz", + registry: globalRegistry, + } + if v, ok := config["module"].(string); ok && v != "" { + s.moduleName = v + } + + rule, err := parseRuleConfig(name, "step.authz_add_policy", config) + if err != nil { + return nil, err + } + s.ruleStatic, s.ruleTmpls = compileRuleTemplates(rule) + return s, nil +} + +// Execute adds the policy rule to the enforcer. +func (s *authzAddPolicyStep) Execute( + _ context.Context, + triggerData map[string]any, + stepOutputs map[string]map[string]any, + current map[string]any, + _ map[string]any, +) (*sdk.StepResult, error) { + tmplData := buildTemplateData(triggerData, stepOutputs, current) + + rule, err := resolveRule(s.ruleStatic, s.ruleTmpls, tmplData) + if err != nil { + return nil, fmt.Errorf("step.authz_add_policy %q: resolve rule: %w", s.name, err) + } + + mod, ok := s.registry.GetEnforcer(s.moduleName) + if !ok { + return nil, fmt.Errorf("step.authz_add_policy %q: authz module %q not found", s.name, s.moduleName) + } + + added, err := mod.AddPolicy(rule) + if err != nil { + return nil, fmt.Errorf("step.authz_add_policy %q: add policy: %w", s.name, err) + } + + return &sdk.StepResult{ + Output: map[string]any{ + "authz_policy_added": added, + "authz_rule": rule, + }, + }, nil +} + +// --- helpers shared by add/remove/role steps --- + +// parseRuleConfig extracts the "rule" field from config as []string. +func parseRuleConfig(stepName, stepType string, config map[string]any) ([]string, error) { + ruleAny, ok := config["rule"] + if !ok { + return nil, fmt.Errorf("%s %q: config.rule is required", stepType, stepName) + } + rule, err := toStringSlice(ruleAny) + if err != nil { + return nil, fmt.Errorf("%s %q: config.rule: %w", stepType, stepName, err) + } + if len(rule) == 0 { + return nil, fmt.Errorf("%s %q: config.rule must not be empty", stepType, stepName) + } + return rule, nil +} + +// compileRuleTemplates returns parallel slices: static strings and compiled templates. +// For each element, exactly one of static[i] or tmpls[i] is meaningful. +func compileRuleTemplates(rule []string) (static []string, tmpls []*template.Template) { + static = make([]string, len(rule)) + tmpls = make([]*template.Template, len(rule)) + for i, elem := range rule { + if isTemplate(elem) { + t, err := template.New(fmt.Sprintf("rule[%d]", i)).Parse(elem) + if err == nil { + tmpls[i] = t + } else { + static[i] = elem // fall back to static on parse error + } + } else { + static[i] = elem + } + } + return static, tmpls +} + +// resolveRule evaluates templates against data and returns the resolved rule. +func resolveRule(static []string, tmpls []*template.Template, data map[string]any) ([]string, error) { + out := make([]string, len(static)) + for i := range static { + if tmpls[i] != nil { + var buf bytes.Buffer + if err := tmpls[i].Execute(&buf, data); err != nil { + return nil, fmt.Errorf("rule[%d]: %w", i, err) + } + out[i] = buf.String() + } else { + out[i] = static[i] + } + } + return out, nil +} diff --git a/internal/step_authz_new_steps_test.go b/internal/step_authz_new_steps_test.go new file mode 100644 index 0000000..dfe2e7a --- /dev/null +++ b/internal/step_authz_new_steps_test.go @@ -0,0 +1,380 @@ +package internal + +import ( + "context" + "testing" +) + +// --- step.authz_add_policy tests --- + +func TestAuthzAddPolicyStep_AddsRule(t *testing.T) { + mod := buildModule(t, + [][]string{{"admin", "/api", "GET"}}, + [][]string{{"alice", "admin"}}, + ) + reg := &testRegistry{mod: mod} + + s, err := newAuthzAddPolicyStep("add-step", map[string]any{ + "rule": []any{"editor", "/api/posts", "POST"}, + }) + if err != nil { + t.Fatalf("newAuthzAddPolicyStep: %v", err) + } + s.registry = reg + + result, err := s.Execute(context.Background(), nil, nil, nil, nil) + if err != nil { + t.Fatalf("Execute: %v", err) + } + if result.Output["authz_policy_added"] != true { + t.Errorf("expected authz_policy_added=true, got %v", result.Output["authz_policy_added"]) + } + + // Assign bob to editor and check enforcement. + if _, err := mod.AddGroupingPolicy([]string{"bob", "editor"}); err != nil { + t.Fatalf("AddGroupingPolicy: %v", err) + } + allowed, err := mod.Enforce("bob", "/api/posts", "POST") + if err != nil { + t.Fatalf("Enforce: %v", err) + } + if !allowed { + t.Error("expected bob to be allowed POST /api/posts after step.authz_add_policy") + } +} + +func TestAuthzAddPolicyStep_TemplateRule(t *testing.T) { + mod := buildModule(t, nil, nil) + reg := &testRegistry{mod: mod} + + s, err := newAuthzAddPolicyStep("add-tmpl", map[string]any{ + "rule": []any{"{{.role}}", "{{.resource}}", "{{.method}}"}, + }) + if err != nil { + t.Fatalf("newAuthzAddPolicyStep: %v", err) + } + s.registry = reg + + result, err := s.Execute(context.Background(), + map[string]any{"role": "viewer", "resource": "/news", "method": "GET"}, + nil, nil, nil, + ) + if err != nil { + t.Fatalf("Execute: %v", err) + } + _ = result + + allowed, err := mod.Enforce("viewer", "/news", "GET") + if err != nil { + t.Fatalf("Enforce: %v", err) + } + if !allowed { + t.Error("expected viewer to be allowed GET /news after template add_policy") + } +} + +func TestAuthzAddPolicyStep_MissingRule(t *testing.T) { + _, err := newAuthzAddPolicyStep("bad", map[string]any{}) + if err == nil { + t.Error("expected error for missing rule") + } +} + +func TestAuthzAddPolicyStep_ModuleNotFound(t *testing.T) { + reg := &testRegistry{} + s, err := newAuthzAddPolicyStep("no-mod", map[string]any{ + "rule": []any{"admin", "/api", "GET"}, + }) + if err != nil { + t.Fatalf("newAuthzAddPolicyStep: %v", err) + } + s.registry = reg + + _, err = s.Execute(context.Background(), nil, nil, nil, nil) + if err == nil { + t.Error("expected error when module not found") + } +} + +// --- step.authz_remove_policy tests --- + +func TestAuthzRemovePolicyStep_RemovesRule(t *testing.T) { + mod := buildModule(t, + [][]string{ + {"admin", "/api", "GET"}, + {"editor", "/api/posts", "POST"}, + }, + [][]string{ + {"alice", "admin"}, + {"bob", "editor"}, + }, + ) + reg := &testRegistry{mod: mod} + + // Bob should currently be allowed. + allowed, _ := mod.Enforce("bob", "/api/posts", "POST") + if !allowed { + t.Fatal("pre-condition: bob should be allowed POST /api/posts") + } + + s, err := newAuthzRemovePolicyStep("remove-step", map[string]any{ + "rule": []any{"editor", "/api/posts", "POST"}, + }) + if err != nil { + t.Fatalf("newAuthzRemovePolicyStep: %v", err) + } + s.registry = reg + + result, err := s.Execute(context.Background(), nil, nil, nil, nil) + if err != nil { + t.Fatalf("Execute: %v", err) + } + if result.Output["authz_policy_removed"] != true { + t.Errorf("expected authz_policy_removed=true, got %v", result.Output["authz_policy_removed"]) + } + + // Bob should no longer be allowed. + allowed, err = mod.Enforce("bob", "/api/posts", "POST") + if err != nil { + t.Fatalf("Enforce: %v", err) + } + if allowed { + t.Error("expected bob to be denied POST /api/posts after remove_policy") + } +} + +func TestAuthzRemovePolicyStep_TemplateRule(t *testing.T) { + mod := buildModule(t, + [][]string{{"viewer", "/news", "GET"}}, + nil, + ) + reg := &testRegistry{mod: mod} + + s, err := newAuthzRemovePolicyStep("remove-tmpl", map[string]any{ + "rule": []any{"{{.role}}", "{{.resource}}", "{{.method}}"}, + }) + if err != nil { + t.Fatalf("newAuthzRemovePolicyStep: %v", err) + } + s.registry = reg + + _, err = s.Execute(context.Background(), + map[string]any{"role": "viewer", "resource": "/news", "method": "GET"}, + nil, nil, nil, + ) + if err != nil { + t.Fatalf("Execute: %v", err) + } + + allowed, _ := mod.Enforce("viewer", "/news", "GET") + if allowed { + t.Error("expected viewer to be denied after template remove_policy") + } +} + +func TestAuthzRemovePolicyStep_MissingRule(t *testing.T) { + _, err := newAuthzRemovePolicyStep("bad", map[string]any{}) + if err == nil { + t.Error("expected error for missing rule") + } +} + +func TestAuthzRemovePolicyStep_ModuleNotFound(t *testing.T) { + reg := &testRegistry{} + s, err := newAuthzRemovePolicyStep("no-mod", map[string]any{ + "rule": []any{"admin", "/api", "GET"}, + }) + if err != nil { + t.Fatalf("newAuthzRemovePolicyStep: %v", err) + } + s.registry = reg + + _, err = s.Execute(context.Background(), nil, nil, nil, nil) + if err == nil { + t.Error("expected error when module not found") + } +} + +// --- step.authz_role_assign tests --- + +func TestAuthzRoleAssignStep_Add(t *testing.T) { + mod := buildModule(t, + [][]string{{"admin", "/admin", "GET"}}, + nil, + ) + reg := &testRegistry{mod: mod} + + s, err := newAuthzRoleAssignStep("assign-add", map[string]any{ + "action": "add", + "assignments": []any{[]any{"dave", "admin"}}, + }) + if err != nil { + t.Fatalf("newAuthzRoleAssignStep: %v", err) + } + s.registry = reg + + result, err := s.Execute(context.Background(), nil, nil, nil, nil) + if err != nil { + t.Fatalf("Execute add: %v", err) + } + if result.Output["authz_role_action"] != "add" { + t.Errorf("unexpected action: %v", result.Output["authz_role_action"]) + } + + allowed, err := mod.Enforce("dave", "/admin", "GET") + if err != nil { + t.Fatalf("Enforce: %v", err) + } + if !allowed { + t.Error("expected dave to be allowed GET /admin after role assign add") + } +} + +func TestAuthzRoleAssignStep_Remove(t *testing.T) { + mod := buildModule(t, + [][]string{{"admin", "/admin", "GET"}}, + [][]string{{"dave", "admin"}}, + ) + reg := &testRegistry{mod: mod} + + // Pre-condition: dave is allowed. + allowed, _ := mod.Enforce("dave", "/admin", "GET") + if !allowed { + t.Fatal("pre-condition: dave should be allowed before role remove") + } + + s, err := newAuthzRoleAssignStep("assign-remove", map[string]any{ + "action": "remove", + "assignments": []any{[]any{"dave", "admin"}}, + }) + if err != nil { + t.Fatalf("newAuthzRoleAssignStep: %v", err) + } + s.registry = reg + + _, err = s.Execute(context.Background(), nil, nil, nil, nil) + if err != nil { + t.Fatalf("Execute remove: %v", err) + } + + allowed, err = mod.Enforce("dave", "/admin", "GET") + if err != nil { + t.Fatalf("Enforce after remove: %v", err) + } + if allowed { + t.Error("expected dave to be denied after role assign remove") + } +} + +func TestAuthzRoleAssignStep_TemplateAssignment(t *testing.T) { + mod := buildModule(t, + [][]string{{"superuser", "/root", "GET"}}, + nil, + ) + reg := &testRegistry{mod: mod} + + s, err := newAuthzRoleAssignStep("assign-tmpl", map[string]any{ + "action": "add", + "assignments": []any{[]any{"{{.user}}", "superuser"}}, + }) + if err != nil { + t.Fatalf("newAuthzRoleAssignStep: %v", err) + } + s.registry = reg + + _, err = s.Execute(context.Background(), + map[string]any{"user": "eve"}, + nil, nil, nil, + ) + if err != nil { + t.Fatalf("Execute template assign: %v", err) + } + + allowed, err := mod.Enforce("eve", "/root", "GET") + if err != nil { + t.Fatalf("Enforce: %v", err) + } + if !allowed { + t.Error("expected eve to be allowed GET /root after template role assign") + } +} + +func TestAuthzRoleAssignStep_InvalidAction(t *testing.T) { + _, err := newAuthzRoleAssignStep("bad-action", map[string]any{ + "action": "grant", // invalid + "assignments": []any{[]any{"user", "role"}}, + }) + if err == nil { + t.Error("expected error for invalid action") + } +} + +func TestAuthzRoleAssignStep_MissingAssignments(t *testing.T) { + _, err := newAuthzRoleAssignStep("no-assign", map[string]any{ + "action": "add", + }) + if err == nil { + t.Error("expected error for missing assignments") + } +} + +func TestAuthzRoleAssignStep_AssignmentTooShort(t *testing.T) { + _, err := newAuthzRoleAssignStep("short-assign", map[string]any{ + "action": "add", + "assignments": []any{[]any{"only-one"}}, + }) + if err == nil { + t.Error("expected error for assignment with < 2 elements") + } +} + +func TestAuthzRoleAssignStep_ModuleNotFound(t *testing.T) { + reg := &testRegistry{} + s, err := newAuthzRoleAssignStep("no-mod", map[string]any{ + "action": "add", + "assignments": []any{[]any{"user", "role"}}, + }) + if err != nil { + t.Fatalf("newAuthzRoleAssignStep: %v", err) + } + s.registry = reg + + _, err = s.Execute(context.Background(), nil, nil, nil, nil) + if err == nil { + t.Error("expected error when module not found") + } +} + +func TestAuthzRoleAssignStep_MultipleAssignments(t *testing.T) { + mod := buildModule(t, + [][]string{ + {"admin", "/admin", "GET"}, + {"editor", "/posts", "POST"}, + }, + nil, + ) + reg := &testRegistry{mod: mod} + + s, err := newAuthzRoleAssignStep("multi-assign", map[string]any{ + "action": "add", + "assignments": []any{ + []any{"frank", "admin"}, + []any{"frank", "editor"}, + }, + }) + if err != nil { + t.Fatalf("newAuthzRoleAssignStep: %v", err) + } + s.registry = reg + + _, err = s.Execute(context.Background(), nil, nil, nil, nil) + if err != nil { + t.Fatalf("Execute: %v", err) + } + + a1, _ := mod.Enforce("frank", "/admin", "GET") + a2, _ := mod.Enforce("frank", "/posts", "POST") + if !a1 || !a2 { + t.Errorf("expected frank to have both roles; admin=%v editor=%v", a1, a2) + } +} diff --git a/internal/step_authz_remove_policy.go b/internal/step_authz_remove_policy.go new file mode 100644 index 0000000..7fe95e9 --- /dev/null +++ b/internal/step_authz_remove_policy.go @@ -0,0 +1,75 @@ +package internal + +import ( + "context" + "fmt" + "text/template" + + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" +) + +// authzRemovePolicyStep implements sdk.StepInstance. It removes a policy rule +// from the Casbin enforcer at runtime. +// +// Config: +// +// module: "authz" # name of the authz.casbin module (default: "authz") +// rule: ["admin", "/api/*", "*"] # policy rule; each element may be a Go template +type authzRemovePolicyStep struct { + name string + moduleName string + ruleTmpls []*template.Template + ruleStatic []string + registry moduleRegistry +} + +func newAuthzRemovePolicyStep(name string, config map[string]any) (*authzRemovePolicyStep, error) { + s := &authzRemovePolicyStep{ + name: name, + moduleName: "authz", + registry: globalRegistry, + } + if v, ok := config["module"].(string); ok && v != "" { + s.moduleName = v + } + + rule, err := parseRuleConfig(name, "step.authz_remove_policy", config) + if err != nil { + return nil, err + } + s.ruleStatic, s.ruleTmpls = compileRuleTemplates(rule) + return s, nil +} + +// Execute removes the policy rule from the enforcer. +func (s *authzRemovePolicyStep) Execute( + _ context.Context, + triggerData map[string]any, + stepOutputs map[string]map[string]any, + current map[string]any, + _ map[string]any, +) (*sdk.StepResult, error) { + tmplData := buildTemplateData(triggerData, stepOutputs, current) + + rule, err := resolveRule(s.ruleStatic, s.ruleTmpls, tmplData) + if err != nil { + return nil, fmt.Errorf("step.authz_remove_policy %q: resolve rule: %w", s.name, err) + } + + mod, ok := s.registry.GetEnforcer(s.moduleName) + if !ok { + return nil, fmt.Errorf("step.authz_remove_policy %q: authz module %q not found", s.name, s.moduleName) + } + + removed, err := mod.RemovePolicy(rule) + if err != nil { + return nil, fmt.Errorf("step.authz_remove_policy %q: remove policy: %w", s.name, err) + } + + return &sdk.StepResult{ + Output: map[string]any{ + "authz_policy_removed": removed, + "authz_rule": rule, + }, + }, nil +} diff --git a/internal/step_authz_role_assign.go b/internal/step_authz_role_assign.go new file mode 100644 index 0000000..e7f2724 --- /dev/null +++ b/internal/step_authz_role_assign.go @@ -0,0 +1,114 @@ +package internal + +import ( + "context" + "fmt" + "text/template" + + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" +) + +// authzRoleAssignStep implements sdk.StepInstance. It adds or removes a role +// mapping (grouping policy) in the Casbin enforcer at runtime. +// +// Config: +// +// module: "authz" # name of the authz.casbin module (default: "authz") +// action: "add" # "add" (default) or "remove" +// assignments: # list of [user, role] pairs; values may be Go templates +// - ["{{.user}}", "admin"] +type authzRoleAssignStep struct { + name string + moduleName string + action string // "add" or "remove" + assignments []roleAssignment + registry moduleRegistry +} + +// roleAssignment holds the compiled template data for a single [user, role] pair. +type roleAssignment struct { + static []string + tmpls []*template.Template +} + +func newAuthzRoleAssignStep(name string, config map[string]any) (*authzRoleAssignStep, error) { + s := &authzRoleAssignStep{ + name: name, + moduleName: "authz", + action: "add", + registry: globalRegistry, + } + if v, ok := config["module"].(string); ok && v != "" { + s.moduleName = v + } + if v, ok := config["action"].(string); ok && v != "" { + switch v { + case "add", "remove": + s.action = v + default: + return nil, fmt.Errorf("step.authz_role_assign %q: action must be \"add\" or \"remove\", got %q", name, v) + } + } + + rawAssignments, ok := config["assignments"].([]any) + if !ok || len(rawAssignments) == 0 { + return nil, fmt.Errorf("step.authz_role_assign %q: config.assignments is required", name) + } + + for i, a := range rawAssignments { + row, err := toStringSlice(a) + if err != nil { + return nil, fmt.Errorf("step.authz_role_assign %q: assignments[%d]: %w", name, i, err) + } + if len(row) < 2 { + return nil, fmt.Errorf("step.authz_role_assign %q: assignments[%d]: expected [user, role], got %v", name, i, row) + } + st, tmpls := compileRuleTemplates(row) + s.assignments = append(s.assignments, roleAssignment{static: st, tmpls: tmpls}) + } + + return s, nil +} + +// Execute adds or removes role mappings in the enforcer. +func (s *authzRoleAssignStep) Execute( + _ context.Context, + triggerData map[string]any, + stepOutputs map[string]map[string]any, + current map[string]any, + _ map[string]any, +) (*sdk.StepResult, error) { + tmplData := buildTemplateData(triggerData, stepOutputs, current) + + mod, ok := s.registry.GetEnforcer(s.moduleName) + if !ok { + return nil, fmt.Errorf("step.authz_role_assign %q: authz module %q not found", s.name, s.moduleName) + } + + var processed [][]string + for i, a := range s.assignments { + rule, err := resolveRule(a.static, a.tmpls, tmplData) + if err != nil { + return nil, fmt.Errorf("step.authz_role_assign %q: resolve assignments[%d]: %w", s.name, i, err) + } + + var opErr error + switch s.action { + case "add": + _, opErr = mod.AddGroupingPolicy(rule) + case "remove": + _, opErr = mod.RemoveGroupingPolicy(rule) + } + if opErr != nil { + return nil, fmt.Errorf("step.authz_role_assign %q: %s assignment[%d]: %w", s.name, s.action, i, opErr) + } + processed = append(processed, rule) + } + + return &sdk.StepResult{ + Output: map[string]any{ + "authz_role_action": s.action, + "authz_role_assignments": processed, + }, + }, nil +} diff --git a/plugin.json b/plugin.json index 7c56b24..0939969 100644 --- a/plugin.json +++ b/plugin.json @@ -13,7 +13,12 @@ "capabilities": { "configProvider": false, "moduleTypes": ["authz.casbin"], - "stepTypes": ["step.authz_check"], + "stepTypes": [ + "step.authz_check", + "step.authz_add_policy", + "step.authz_remove_policy", + "step.authz_role_assign" + ], "triggerTypes": [] } }